Compare commits

..
112 Commits
Author SHA1 Message Date
José Valim 29fdd09c0f Release v1.15.8 2024-05-21 00:16:44 +02:00
José Valim a1934d6cd5 Fix --dbg handling in bin/elixir, closes #13482 2024-05-06 12:51:31 +02:00
José Valim 26627272a9 Add .bat/.com disclaimers to System.cmd and Port 2024-05-06 12:51:27 +02:00
Theodor Fiedler 0c4cc6c992 Translate :undefined URI port to nil (#13464)
Resolves #13462

When a url string is schema less and the host is followed by a colon
without setting the actual port, `:uri_string.parse/1` returns the
port to be `:undefined`. As long as the url string is parseable,
we should translate `:undefined` to `nil`, in order to ensure we return
a valid URI struct.
2024-05-06 12:50:50 +02:00
José Valim a5b730a900 Ensure translators are persisted across logger restarts 2024-05-02 00:36:16 +02:00
José Valim 4afc03bece Ensure compile paths are available during compilation, closes #13458 2024-05-02 00:35:51 +02:00
Łukasz Samson e236cbb164 Elixir 1.14.5 supports Erlang/OTP 26 (#13071) 2023-11-03 11:54:14 +01:00
José Valim 927b10df80 Release v1.15.7 2023-10-14 12:15:24 +02:00
José Valim bbe04b4ab9 Address compilation on Erlang/OTP 24 2023-10-14 11:38:28 +02:00
José Valim 9153d73f2b Address compilation on Erlang/OTP 24 2023-10-14 11:35:31 +02:00
José Valim 6d4811ad1c Use remote version of external handler 2023-10-14 11:28:19 +02:00
Juha 94326a3544 Document need of Tasks to trap exits for Task.Supervisor :shutdown to have effect (#13004) 2023-10-13 22:49:50 +02:00
José Valim ef527cad43 Address compilation warnings on Erlang/OTP 24 2023-10-13 22:49:39 +02:00
José Valim e91b1fa66b Do not tie eval functions to Elixir version 2023-10-11 10:36:16 +02:00
José Valim 863e66884d Clarify docs on tasks requirements 2023-10-11 10:33:38 +02:00
José Valim 54516c5684 Address tests on Erlang/OTP 26.1, closes #12975 2023-10-01 15:46:26 +02:00
José Valim 08d2bb170d Ensure full directory match on mix format, closes #12969 2023-09-28 21:34:22 +02:00
José Valim c2a0d8ead6 Do not emit duplicate warnings from tokenizer, closes #12961 2023-09-25 12:56:29 +02:00
José Valim a16517edfb Release v1.15.6 2023-09-20 10:32:46 +02:00
Wojtek Mach 26136e53c6 Improve Windows Installer (#12945)
1. If we couldn't verify installed OTP, don't quit but let user proceed with installing just Elixir

2. When checking installed OTPs, pick the recent most installed. Previously we were picking the first one which was basically guaranteed to be too old.

3. If we find an existing Elixir installation but it doesn't match the OTP version, offer to download the OTP version this Elixir installer was compiled against (which usually would be the newer version)

4. Add "Verify Erlang/OTP" button which re-checks for installed OTP.

Closes #12678.
2023-09-20 10:30:17 +02:00
José Valim 6654f1b922 Load plugins on formatter_for_file, closes #12930 2023-09-14 09:13:14 +02:00
José Valim e30f8df8fe Do not loop when invalid block is given to formatter, closes #12922 2023-09-13 09:50:05 +02:00
José Valim 2fe5b77498 Trace functions before they are inlined, closes #12925 2023-09-13 09:39:03 +02:00
José Valim f1a5e6a0b1 Properly print warnings from tokenizer in EEx, closes #12926 2023-09-13 09:17:45 +02:00
José Valim fadf5c6519 Do not always load applications during convergence, closes #12682 2023-09-12 11:54:57 +02:00
José Valim e975613c9d Load extra applications for umbrellas 2023-09-12 10:12:01 +02:00
Jean Klingler b1a1dd04a5 Fix formatter for :* in bitstring modifiers (#12923) 2023-09-12 08:12:09 +02:00
José Valim e4e2f1a79e Include environment in mix format missing dependency error 2023-09-11 17:36:21 +02:00
José Valim 138b442e07 Improve error message on Access module 2023-09-04 14:01:09 +02:00
Wojtek Mach 119580f544 Fix PATH handling on Windows Installer (#12900) 2023-08-30 17:09:56 +02:00
José Valim 9fd97c4663 Release v1.15.5 2023-08-28 13:45:12 +02:00
José Valim 78cee0e384 Speed up loading of struct suggestions, closes #12674 2023-08-27 10:12:24 +02:00
Zach Allaun fb0379287c Fix Code.Fragment.surround_context/2 for submodules of non-aliases (#12890) 2023-08-24 20:55:53 +02:00
José Valim def65ab120 Do not store anonymous functions in cache 2023-08-23 14:15:55 +02:00
José Valim dc8cfcd4c9 Load and compile plugins from cache, closes #12880 2023-08-23 12:24:22 +02:00
sabiwara 84c8d23710 Always pass stracktrace when necessary 2023-08-23 12:06:07 +02:00
Jean Klingler 36c3f7594d Fix bug in surround_context alias handling (#12830) 2023-08-03 23:22:18 +09:00
José Valim bdaffc380b Fix infinite loop when diffing functions, closes #12828 2023-08-02 20:45:57 +02:00
José Valim 5d79b34cb5 Force group leader to run as a binary and unicode in IEx 2023-07-31 15:46:39 +02:00
José Valim 14cddf357b Also handle charlist returns from stdio
Closes #12687.
2023-07-28 12:24:26 +02:00
José Valim 92eee10bcf Optional arguments must be counted from the end, closes #10095 2023-07-19 13:52:17 +02:00
José Valim 9b7b7d655d Do not assume blake is always available
Closes #12809.
2023-07-19 00:22:32 +02:00
José Valim f40aa10436 Fix type in changelog 2023-07-18 13:21:23 +02:00
José Valim c521bdb91a Release v1.15.4 2023-07-18 11:38:35 +02:00
José Valim bdbdf52122 Disable consolidation on archive.install 2023-07-17 19:14:29 +02:00
José Valim 7ff97fb930 Compute remove entries per dependency, closes #12801 2023-07-17 09:40:31 +02:00
Santiago Ferreira af378de03f Remove duplicated async: true option from example (#12798) 2023-07-17 00:13:06 +02:00
Jean Klingler 82c264bc36 Remove erlang doc duplicates in IEx (#12797) 2023-07-17 00:12:51 +02:00
José Valim 4fbed9fe12 Consider significant chunks from Erlang/OTP 26 2023-07-17 00:12:24 +02:00
Ievgen Pyrogov 1dbf121a78 Update io.ex (#12800)
Fix newlines in documentation for IO module
2023-07-17 00:12:24 +02:00
José Valim 694b9b0218 Release v1.15.3 2023-07-15 09:50:33 +02:00
José Valim 0a3df5068c Remove unsupport --no-deps-loading 2023-07-15 09:38:48 +02:00
José Valim 9e177fd592 Ensure load path is enabled on __mix_recompile__? 2023-07-15 09:23:32 +02:00
Thibaut Barrère 8422af4a2e Clarify upgrade path for Regex.regex? (#12785) (#12786) 2023-07-12 18:05:00 +02:00
José Valim b79c5a3b4c Consider optional apps in Mix.ensure_application! 2023-07-12 09:27:40 +02:00
José Valim 3bb0a8379d Disable tail call optimization on file root entries 2023-07-07 13:37:57 +02:00
Łukasz Samson fe0d57c4da Allow to opt out of starting apps in Mix.install (#12766) 2023-07-07 08:39:53 +02:00
José Valim 727a958c48 Clarify --no-pry vs --dbg pry 2023-07-06 12:36:21 +02:00
José Valim aa93d654ab Propagate diagnostics from inner compiler process 2023-07-06 09:42:54 +02:00
José Valim f5b71a79b2 Improve error message when IEx cannot boot 2023-07-03 16:33:43 +02:00
José Valim 173fc52610 Fix IEx --remsh on Erlang/OTP 25-
Closes #12746.
2023-07-03 16:33:43 +02:00
José Valim 7f7a8bca99 Release v1.15.2 2023-07-01 22:15:55 +02:00
José Valim 0929a431d4 Check for variable definition instead of expansion 2023-07-01 19:28:28 +02:00
José Valim f1e5d770ea Release v1.15.1 2023-06-30 17:03:41 +02:00
José Valim bceda78a42 Consider surround context until end whenever possible 2023-06-30 16:57:48 +02:00
Rich Cavanaugh 6d829120c3 Allow variables defined in doctests to be used in expectation (#12730) 2023-06-28 22:32:21 +02:00
José Valim 785c29210e Do not assume Logger has been loaded at compile-time 2023-06-27 15:02:26 +02:00
José Valim 7db2daea72 mix format 2023-06-27 14:58:50 +02:00
José Valim 0fcee760d8 Remove remaining reference to digest_file! 2023-06-27 14:50:01 +02:00
José Valim e63b0b6217 Do not assume external resources are available 2023-06-27 14:29:25 +02:00
José Valim 9e195f5251 Mention Erlang's :math module in Float 2023-06-27 14:01:05 +02:00
Jean Klingler 60320941aa Always respect options passed to capture_log (#12721) 2023-06-27 14:01:05 +02:00
José Valim 871e737583 Fix heisentest 2023-06-25 09:49:58 +02:00
José Valim 3785e6f71b Return errors on mix compile instead of raising 2023-06-24 20:18:18 +02:00
José Valim a89f8a9913 Track removed modules and exports across local deps, closes #12707 2023-06-24 17:06:18 +02:00
José Valim 93bfbdfc4e Remove --werl from Erlang/OTP 26 release scripts 2023-06-24 13:08:40 +02:00
Jean Klingler c032839468 Parser honors :static_atoms_encoder for multi-letter sigils (#12706)
* Unify sigil token generation

* Parser honors :static_atoms_encoder for multi-letter sigils

* Remove chars from token, reverse engineer from atom
2023-06-24 13:08:32 +02:00
José Valim 52cf991302 Update CHANGELOG 2023-06-23 16:04:56 +02:00
cjschneider2 0c6b44798b Use printf instead of echo to output user flags (#12704)
Fixes #12677.

Using printf here prevents an error when outputting the filtered flag
`-e` as doing with with `echo "-e"` doesn't output anything
(deleting the option we wanted to filter). 

More info: https://unix.stackexchange.com/a/65819
2023-06-23 16:04:50 +02:00
José Valim 4c12bfcb14 Ensure included deps transitive from path deps can be loaded, closes #12682 2023-06-23 15:45:35 +02:00
José Valim fde96ce8dc Handle :function metadata properly, closes #12703 2023-06-23 13:07:14 +02:00
Jannik Becher bc40b6ebb5 :test_type does not affect the test execution (#12701) 2023-06-23 10:12:30 +02:00
Aaron Tinio 3a34f243b8 Provide compile_env tip that fixes the error across all environments (#12702) 2023-06-23 10:12:23 +02:00
José Valim f322d26b7d Include optional dependencies when mode changes, closes #12694 2023-06-23 10:12:23 +02:00
José Valim dcaa468c47 Extract app writing into its own function 2023-06-23 10:12:23 +02:00
José Valim cc2d05654f Fix dbg on Erlang/OTP 25- (#12697) 2023-06-22 20:53:30 +02:00
José Valim c22f7343f0 Fix non-result doctest terminated with fences, closes #12695 2023-06-22 19:06:01 +02:00
José Valim 858835f9f7 yecc and leex are not available on custom compilers 2023-06-21 20:25:15 +02:00
José Valim 367d906403 Assert current project is in the path before invoking compilers
Closes #12686.
2023-06-21 20:24:00 +02:00
José Valim df5bc5abc8 Ensure yecc is available to emit warnings, closes #12691 2023-06-21 19:33:44 +02:00
José Valim 1ca5d789bb Fix race condition on concurrent capture_log, closes #12692 2023-06-21 16:44:48 +02:00
José Valim aab3543dc3 Add release note about potential compatibilities to CHANGELOG, closes #12688 2023-06-21 09:37:28 +02:00
José Valim 9b254e6830 Do not expect OTP to be compiled with docs, closes #12677 2023-06-20 12:33:41 +02:00
José Valim 0dd933b0a9 Fix path on Windows 2023-06-19 16:11:28 +02:00
José Valim 9fd85b06dc Release v1.15.0 2023-06-19 13:37:46 +02:00
José Valim ec81110b3e Improve Logger docs 2023-06-18 17:12:23 +02:00
José Valim 094a76d514 Update release instructions 2023-06-18 10:44:40 +02:00
sabiwara 7d737bc5ec Fix slicing MapSet with stepped range (#12667) 2023-06-14 12:04:51 +02:00
José Valim 3eb074282a Return the value on each step of dbg+pry+pipe integration, closes #12657 2023-06-13 17:11:34 +02:00
José Valim b8732bd248 Improve error message for invalid module attribute usage, closes #12656 2023-06-13 16:54:33 +02:00
José Valim 2252a8ff55 Fix erts handling when loading apps 2023-06-13 15:29:17 +02:00
José Valim 9a9cf419a1 Release v1.15.0-rc.2 2023-06-12 15:10:17 +02:00
Wojtek Mach 0f92512030 Bump Ubuntu version for builds.hex.pm.yml (#12652)
We need nsis 3.08 (not 3.06)
2023-06-12 13:05:11 +02:00
nico piderman e0806f9d6c Mark test cases as invalid when an exit occurs during setup_all (#12651) 2023-06-12 12:54:05 +02:00
Wojtek Mach 5db9d55926 Add offline Windows installer to releases (#12640) 2023-06-12 12:54:05 +02:00
José Valim 3cb3d41368 Keep capture operator on container cursor to quoted 2023-06-12 10:44:16 +02:00
José Valim a3a081c949 Keep erts when pruning load paths (#12642) 2023-06-11 11:27:07 +02:00
José Valim 207350fb44 Improve heredoc warning 2023-06-08 16:15:03 +02:00
José Valim 6f7a68c9e3 Support mix xref graph at umbrella roots, closes #12629 2023-06-07 14:13:11 +02:00
José Valim 0936fb45d5 Mark functions as generated in Docs chunk 2023-06-07 13:14:17 +02:00
José Valim bccfca4ffe Purge Hex before running Mix tests 2023-06-03 20:59:52 +02:00
José Valim 417255fe1b Branch out v1.15 2023-06-03 20:24:12 +02:00
521 changed files with 22942 additions and 64202 deletions
+3 -5
View File
@@ -13,9 +13,7 @@
assert_same: 2,
# Errors tests
assert_eval_raise: 3,
# Float tests
float_assert: 1
]
assert_eval_raise: 3
],
normalize_bitstring_modifiers: false
]
+134
View File
@@ -0,0 +1,134 @@
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: 24
otp_version: '24.3'
upload_generic_version: upload_generic_version
- otp: 25
otp_version: '25.3'
- otp: 26
otp_version: '26.0'
build_docs: build_docs
runs-on: ubuntu-22.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
- name: Upload Precompiled to S3
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
aws s3 cp elixir-otp-${{ matrix.otp }}.zip "s3://${{ env.AWS_S3_BUCKET }}/builds/elixir/${{github.ref_name}}-otp-${{matrix.otp}}.zip" --cache-control "public,max-age=3600" --metadata "{\"surrogate-key\":\"builds builds/elixir builds/elixir/${{github.ref_name}}-otp-${{matrix.otp}}\",\"surrogate-control\":\"public,max-age=604800\"}"
purge builds/elixir/${{github.ref_name}}-otp-${{matrix.otp}}
if [ "${{matrix.upload_generic_version}}" == "upload_generic_version" ]; then
aws s3 cp elixir-otp-${{ matrix.otp }}.zip "s3://${{ env.AWS_S3_BUCKET }}/builds/elixir/${{github.ref_name}}.zip" --cache-control "public,max-age=3600" --metadata "{\"surrogate-key\":\"builds builds/elixir builds/elixir/${{github.ref_name}}\",\"surrogate-control\":\"public,max-age=604800\"}"
purge builds/elixir/${{github.ref_name}}
fi
- name: Update builds txt
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
date=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
build_sha256=$(sha256sum elixir-otp-${{ matrix.otp }}.zip | cut -d ' ' -f 1)
ref_name=${{ github.ref_name }}
aws s3 cp s3://${{ env.AWS_S3_BUCKET }}/builds/elixir/builds.txt builds.txt || true
touch builds.txt
sed -i "/^${ref_name}-otp-${{ matrix.otp }} /d" builds.txt
echo -e "${ref_name}-otp-${{ matrix.otp }} ${{ github.sha }} ${date} ${build_sha256} \n$(cat builds.txt)" > builds.txt
if [ "${{matrix.upload_generic_version}}" == "upload_generic_version" ]; then
sed -i "/^${ref_name} /d" builds.txt
echo -e "${ref_name} ${{ github.sha }} ${date} ${build_sha256} \n$(cat builds.txt)" > builds.txt
fi
sort -u -k1,1 -o builds.txt builds.txt
aws s3 cp builds.txt s3://${{ env.AWS_S3_BUCKET }}/builds/elixir/builds.txt --cache-control "public,max-age=3600" --metadata '{"surrogate-key":"builds builds/elixir builds/elixir/txt","surrogate-control":"public,max-age=604800"}'
purge builds/elixir/txt
-34
View File
@@ -1,34 +0,0 @@
name: CI for Markdown content
on:
push:
branches:
- "main"
paths:
- "lib/**/*.md"
pull_request:
paths:
- "lib/**/*.md"
workflow_dispatch:
jobs:
lint:
name: Lint Markdown content
strategy:
fail-fast: false
runs-on: ubuntu-latest
steps:
- name: Check out the repository
uses: actions/checkout@v4
with:
fetch-depth: 10
- name: Run markdownlint
uses: DavidAnson/markdownlint-cli2-action@v18.0.0
with:
globs: |
lib/elixir/pages/**/*.md
README.md
+17 -21
View File
@@ -3,14 +3,15 @@ name: CI
on:
push:
paths-ignore:
- "lib/**/*.md"
- 'lib/**/*.md'
pull_request:
paths-ignore:
- "lib/**/*.md"
- 'lib/**/*.md'
env:
ELIXIR_ASSERT_TIMEOUT: 2000
ELIXIRC_OPTS: "--warnings-as-errors"
ERLC_OPTS: "warnings_as_errors"
LANG: C.UTF-8
permissions:
@@ -18,34 +19,29 @@ permissions:
jobs:
test_linux:
name: Ubuntu 24.04, Erlang/OTP ${{ matrix.otp_version }}${{ matrix.deterministic && ' (deterministic)' || '' }}
name: Ubuntu 20.04, Erlang/OTP ${{ matrix.otp_version }}
strategy:
fail-fast: false
matrix:
include:
- otp_version: "27.1"
deterministic: true
- otp_version: "27.1"
- otp_version: '26.0'
otp_latest: true
- otp_version: "27.0"
- otp_version: "26.0"
- otp_version: "25.3"
- otp_version: "25.0"
- otp_version: '25.3'
- otp_version: '25.0'
- otp_version: '24.3'
- otp_version: '24.0'
- otp_version: master
development: true
- otp_version: maint
development: true
runs-on: ubuntu-24.04
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
with:
fetch-depth: 50
- uses: erlef/setup-beam@v1
with:
otp-version: ${{ matrix.otp_version }}
- name: Set ERL_COMPILER_OPTIONS
if: ${{ matrix.deterministic }}
run: echo "ERL_COMPILER_OPTIONS=deterministic" >> $GITHUB_ENV
- name: Compile Elixir
run: |
make compile
@@ -70,23 +66,23 @@ jobs:
cd ../elixir/
make docs
- name: Check reproducible builds
if: ${{ matrix.deterministic }}
run: |
rm -rf .git
# Recompile System without .git
cd lib/elixir && ../../bin/elixirc -o ebin lib/system.ex && cd -
taskset 1 make check_reproducible
if: ${{ matrix.otp_latest }}
test_windows:
name: Windows Server 2019, Erlang/OTP ${{ matrix.otp_version }}
strategy:
matrix:
otp_version: ["25.3", "26.2", "27.3"]
runs-on: windows-2022
otp_version: ['24', '25', '26']
runs-on: windows-2019
steps:
- name: Configure Git
run: git config --global core.autocrlf input
- uses: actions/checkout@v4
- uses: actions/checkout@v3
with:
fetch-depth: 50
- uses: erlef/setup-beam@v1
@@ -109,9 +105,9 @@ jobs:
check_posix_compliant:
name: Check POSIX-compliant
runs-on: ubuntu-24.04
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
with:
fetch-depth: 50
- name: Install Shellcheck
+4 -4
View File
@@ -10,16 +10,16 @@ permissions:
jobs:
notify:
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
name: Notify
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
with:
fetch-depth: 50
- uses: erlef/setup-beam@v1
with:
otp-version: "27.3"
elixir-version: "1.18.3"
otp-version: '25.0'
elixir-version: '1.14.0'
- name: Run Elixir script
env:
ELIXIR_FORUM_TOKEN: ${{ secrets.ELIXIR_FORUM_TOKEN }}
+21 -309
View File
@@ -2,20 +2,16 @@ name: Release
on:
push:
branches:
- main
- v*.*
tags:
- v*
env:
ELIXIR_OPTS: "--warnings-as-errors"
ERLC_OPTS: "warnings_as_errors"
LANG: C.UTF-8
permissions:
contents: write
id-token: write
attestations: write
jobs:
create_draft_release:
@@ -24,7 +20,6 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
steps:
- name: Create draft release
if: github.ref_type != 'branch'
run: |
gh release create \
--repo ${{ github.repository }} \
@@ -32,326 +27,43 @@ jobs:
--notes '' \
--draft \
${{ github.ref_name }}
- uses: actions/checkout@v4
if: github.ref_type == 'branch'
with:
fetch-depth: 50
- name: Update ${{ github.ref_name }}-latest
if: github.ref_type == 'branch'
run: |
ref_name=${{ github.ref_name }}-latest
if ! gh release view $ref_name; then
gh release create \
--latest=false \
--title $ref_name \
--notes "Automated release for latest ${{ github.ref_name }}." \
$ref_name
fi
git tag $ref_name --force
git push origin $ref_name --force
build:
release_pre_built:
needs: create_draft_release
strategy:
fail-fast: true
matrix:
include:
- otp: 24
otp_version: '24.3'
- otp: 25
otp_version: "25.3"
otp_version: '25.3'
- otp: 26
otp_version: "26.0"
- otp: 27
otp_version: "27.0"
otp_version: '26.0'
build_docs: build_docs
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v3
with:
fetch-depth: 50
- name: "Build Release"
uses: ./.github/workflows/release_pre_built
- uses: ./.github/workflows/release_pre_built
with:
otp_version: ${{ matrix.otp_version }}
otp: ${{ matrix.otp }}
build_docs: ${{ matrix.build_docs }}
- name: "Attest docs provenance"
uses: actions/attest-build-provenance@v2
id: attest-docs-provenance
if: ${{ matrix.build_docs }}
with:
subject-path: "Docs.zip"
- name: "Copy docs provenance"
if: ${{ matrix.build_docs }}
run: cp "$ATTESTATION" Docs.zip.sigstore
env:
ATTESTATION: "${{ steps.attest-docs-provenance.outputs.bundle-path }}"
- name: Create Docs Hashes
if: ${{ matrix.build_docs }}
run: |
shasum -a 1 Docs.zip > Docs.zip.sha1sum
shasum -a 256 Docs.zip > Docs.zip.sha256sum
- name: "Upload linux release artifacts"
uses: actions/upload-artifact@v4
with:
name: build-linux-elixir-otp-${{ matrix.otp }}
path: elixir-otp-${{ matrix.otp }}.zip
- name: "Upload windows release artifacts"
uses: actions/upload-artifact@v4
with:
name: build-windows-elixir-otp-${{ matrix.otp }}
path: elixir-otp-${{ matrix.otp }}.exe
- name: "Upload doc artifacts"
uses: actions/upload-artifact@v4
if: ${{ matrix.build_docs }}
with:
name: Docs
path: Docs.zip*
sign:
needs: [build]
environment: release
strategy:
fail-fast: true
matrix:
otp: [25, 26, 27]
flavor: [windows, linux]
env:
RELEASE_FILE: elixir-otp-${{ matrix.otp }}.${{ matrix.flavor == 'linux' && 'zip' || 'exe' }}
runs-on: ${{ matrix.flavor == 'linux' && 'ubuntu-22.04' || 'windows-2022' }}
permissions:
contents: write
id-token: write
attestations: write
steps:
- uses: actions/download-artifact@v4
with:
name: build-${{ matrix.flavor }}-elixir-otp-${{ matrix.otp }}
- name: Log in to Azure
if: ${{ matrix.flavor == 'windows' && vars.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: "Sign files with Trusted Signing"
uses: azure/trusted-signing-action@0d74250c661747df006298d0fb49944c10f16e03 # v0.5.1
if: ${{ matrix.flavor == 'windows' && vars.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
with:
endpoint: https://eus.codesigning.azure.net/
trusted-signing-account-name: ${{ vars.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
certificate-profile-name: ${{ vars.AZURE_CERTIFICATE_PROFILE_NAME }}
files-folder: ${{ github.workspace }}
files-folder-filter: exe
file-digest: SHA256
timestamp-rfc3161: http://timestamp.acs.microsoft.com
timestamp-digest: SHA256
- name: "Attest release provenance"
uses: actions/attest-build-provenance@v2
id: attest-provenance
with:
subject-path: ${{ env.RELEASE_FILE }}
- name: "Copy release .zip provenance"
shell: bash
run: cp "$ATTESTATION" "${RELEASE_FILE}.sigstore"
env:
ATTESTATION: "${{ steps.attest-provenance.outputs.bundle-path }}"
- name: Create Release Hashes
if: matrix.flavor == 'windows'
shell: pwsh
run: |
$sha1 = Get-FileHash "$env:RELEASE_FILE" -Algorithm SHA1
$sha1.Hash.ToLower() + " " + $env:RELEASE_FILE | Out-File "$env:RELEASE_FILE.sha1sum"
$sha256 = Get-FileHash "$env:RELEASE_FILE" -Algorithm SHA256
$sha256.Hash.ToLower() + " " + $env:RELEASE_FILE | Out-File "$env:RELEASE_FILE.sha256sum"
- name: Create Release Hashes
if: matrix.flavor == 'linux'
shell: bash
run: |
shasum -a 1 "$RELEASE_FILE" > "${RELEASE_FILE}.sha1sum"
shasum -a 256 "$RELEASE_FILE" > "${RELEASE_FILE}.sha256sum"
- name: "Upload linux release artifacts"
uses: actions/upload-artifact@v4
with:
name: sign-${{ matrix.flavor }}-elixir-otp-${{ matrix.otp }}
path: ${{ env.RELEASE_FILE }}*
upload-release:
needs: [create_draft_release, build, sign]
runs-on: ubuntu-22.04
steps:
- uses: actions/download-artifact@v4
with:
pattern: "{sign-*-elixir-otp-*,Docs}"
merge-multiple: true
- name: Upload Pre-built
shell: bash
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if [ "${{ github.ref_type }}" == "branch" ]; then
tag=${{ github.ref_name }}-latest
else
tag="${{ github.ref_name }}"
fi
gh release upload \
--repo ${{ github.repository }} \
--clobber \
"$tag" \
elixir-otp-*.zip \
elixir-otp-*.zip.sha{1,256}sum \
elixir-otp-*.zip.sigstore \
elixir-otp-*.exe \
elixir-otp-*.exe.sha{1,256}sum \
elixir-otp-*.exe.sigstore \
Docs.zip \
Docs.zip.sha{1,256}sum \
Docs.zip.sigstore
upload-builds-hex-pm:
needs: [build, sign]
runs-on: ubuntu-22.04
concurrency: builds-hex-pm
environment: release
env:
AWS_ACCESS_KEY_ID: ${{ secrets.HEX_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.HEX_AWS_SECRET_ACCESS_KEY }}
AWS_REGION: ${{ vars.HEX_AWS_REGION }}
AWS_S3_BUCKET: ${{ vars.HEX_AWS_S3_BUCKET }}
OTP_GENERIC_VERSION: "25"
steps:
- name: "Check if variables are set up"
if: "${{ ! vars.HEX_AWS_REGION }}"
run: |
echo "Required variables for uploading to hex.pm are not set up, skipping..."
exit 1
- uses: actions/download-artifact@v4
with:
pattern: "{sign-*-elixir-otp-*,Docs}"
merge-multiple: true
- name: Init purge keys file
run: |
touch purge_keys.txt
- name: Upload Precompiled to S3
run: |
ref_name=${{ github.ref_name }}
for zip in $(find . -type f -name 'elixir-otp-*.zip' | sed 's/^\.\///'); do
dest=${zip/elixir/${ref_name}}
surrogate_key=${dest/.zip$/}
aws s3 cp "${zip}" "s3://${AWS_S3_BUCKET}/builds/elixir/${dest}" \
--cache-control "public,max-age=3600" \
--metadata "{\"surrogate-key\":\"builds builds/elixir builds/elixir/${surrogate_key}\",\"surrogate-control\":\"public,max-age=604800\"}"
echo "builds/elixir/${surrogate_key}" >> purge_keys.txt
if [ "$zip" == "elixir-otp-${OTP_GENERIC_VERSION}.zip" ]; then
aws s3 cp "${zip}" "s3://${AWS_S3_BUCKET}/builds/elixir/${ref_name}.zip" \
--cache-control "public,max-age=3600" \
--metadata "{\"surrogate-key\":\"builds builds/elixir builds/elixir/${ref_name}\",\"surrogate-control\":\"public,max-age=604800\"}"
echo builds/elixir/${ref_name} >> purge_keys.txt
fi
done
- name: Upload Docs to S3
run: |
version=$(echo ${{ github.ref_name }} | sed -e 's/^v//g')
unzip Docs.zip
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://${AWS_S3_BUCKET}/docs/${tarball}" \
--cache-control "public,max-age=3600" \
--metadata "{\"surrogate-key\":\"${surrogate_key}\",\"surrogate-control\":\"public,max-age=604800\"}"
echo "${surrogate_key}" >> ../purge_keys.txt
fi
done
- name: Update builds txt
run: |
date="$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
ref_name=${{ github.ref_name }}
aws s3 cp "s3://${AWS_S3_BUCKET}/builds/elixir/builds.txt" builds.txt || true
touch builds.txt
for sha256_file in $(find . -name 'elixir-otp-*.zip.sha256sum' | sed 's/^\.\///'); do
otp_version=$(echo "${sha256_file}" | sed -r 's/^elixir-otp-([[:digit:]]+)\.zip\.sha256sum/otp-\1/')
build_sha256=$(cut -d ' ' -f 1 "${sha256_file}")
sed -i "/^${ref_name}-${otp_version} /d" builds.txt
echo -e "${ref_name}-${otp_version} ${{ github.sha }} ${date} ${build_sha256} \n$(cat builds.txt)" > builds.txt
if [ "${otp_version}" == "otp-${OTP_GENERIC_VERSION}" ]; then
sed -i "/^${ref_name} /d" builds.txt
echo -e "${ref_name} ${{ github.sha }} ${date} ${build_sha256} \n$(cat builds.txt)" > builds.txt
fi
done
sort -u -k1,1 -o builds.txt builds.txt
aws s3 cp builds.txt "s3://${AWS_S3_BUCKET}/builds/elixir/builds.txt" \
--cache-control "public,max-age=3600" \
--metadata '{"surrogate-key":"builds builds/elixir builds/elixir/txt","surrogate-control":"public,max-age=604800"}'
echo 'builds/elixir/txt' >> purge_keys.txt
- name: Flush cache
if: github.repository == 'elixir-lang/elixir'
run: |
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
}
for key in $(cat purge_keys.txt); do
purge "${key}"
done
gh release upload --clobber "${{ github.ref_name }}" \
elixir-otp-${{ matrix.otp }}.zip \
elixir-otp-${{ matrix.otp }}.zip.sha{1,256}sum \
elixir-otp-${{ matrix.otp }}.exe \
elixir-otp-${{ matrix.otp }}.exe.sha{1,256}sum
- name: Upload Docs to GitHub
if: ${{ matrix.build_docs }}
env:
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 }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release upload --clobber "${{ github.ref_name }}" \
Docs.zip \
Docs.zip.sha{1,256}sum
+11 -10
View File
@@ -19,6 +19,8 @@ runs:
run: |
make Precompiled.zip
mv Precompiled.zip elixir-otp-${{ inputs.otp }}.zip
shasum -a 1 elixir-otp-${{ inputs.otp }}.zip > elixir-otp-${{ inputs.otp }}.zip.sha1sum
shasum -a 256 elixir-otp-${{ inputs.otp }}.zip > elixir-otp-${{ inputs.otp }}.zip.sha256sum
echo "$PWD/bin" >> $GITHUB_PATH
- name: Install NSIS
shell: bash
@@ -32,21 +34,19 @@ runs:
export ELIXIR_ZIP=$PWD/elixir-otp-${{ inputs.otp }}.zip
(cd lib/elixir/scripts/windows_installer && ./build.sh)
mv lib/elixir/scripts/windows_installer/tmp/elixir-otp-${{ inputs.otp }}.exe .
- name: Get ExDoc ref
shasum -a 1 elixir-otp-${{ inputs.otp }}.exe > elixir-otp-${{ inputs.otp }}.exe.sha1sum
shasum -a 256 elixir-otp-${{ inputs.otp }}.exe > elixir-otp-${{ inputs.otp }}.exe.sha256sum
- name: Get latest stable ExDoc version
if: ${{ inputs.build_docs }}
shell: bash
run: |
if [ "${{ github.ref_name }}" = "main" ]; then
ref=main
else
ref=v$(curl -s https://hex.pm/api/packages/ex_doc | jq --raw-output '.latest_stable_version')
fi
echo "EX_DOC_REF=$ref" >> $GITHUB_ENV
- uses: actions/checkout@v4
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: ${{ env.EX_DOC_REF }}
ref: v${{ env.EX_DOC_LATEST_STABLE_VERSION }}
path: ex_doc
- name: Build ex_doc
if: ${{ inputs.build_docs }}
@@ -60,5 +60,6 @@ runs:
if: ${{ inputs.build_docs }}
shell: bash
run: |
git fetch --tags
make Docs.zip
shasum -a 1 Docs.zip > Docs.zip.sha1sum
shasum -a 256 Docs.zip > Docs.zip.sha256sum
-42
View File
@@ -1,42 +0,0 @@
{
// Consecutive header levels (h1 -> h2 -> h3). We don't care about this.
"MD001": false,
// Header style. We use #s.
"MD003": {
"style": "atx"
},
// Style of unordered lists..
"MD007": {
"indent": 2,
"start_indented": true
},
// Line length. Who cares.
"MD013": false,
// This warns if you have "console" or "shell" code blocks with a dollar sign $ that
// don't show output. We use those a lot, so this is fine for us.
"MD014": false,
// Multiple headings with the same content. That's fine.
"MD024": false,
// Some headers finish with ! because it refers to a function name
"MD026": false,
// Allow empty line between block quotes. Used by contiguous admonition blocks.
"MD028": false,
// Allowed HTML inline elements.
"MD033": {
"allowed_elements": [
"h1",
"a",
"br",
"img",
"picture",
"source",
"noscript",
"p",
"script"
]
},
// This warns if you have spaces in code blocks. Sometimes, that's fine.
"MD038": false,
// Code block style. We don't care if it's fenced or indented.
"MD046": false
}
+358 -388
View File
@@ -1,505 +1,475 @@
# Changelog for Elixir v1.18
# Changelog for Elixir v1.15
Elixir v1.18 is an impressive release with improvements across the two main efforts happening within the Elixir ecosystem right now: set-theoretic types and language servers. It also comes with built-in JSON support and adds new capabilities to its unit testing library. Here is a quick break down.
This release requires Erlang/OTP 24 and later.
## Type system improvements
Elixir v1.15 is a smaller release with focused improvements
on compilation and boot times. This release also completes
our integration process with Erlang/OTP logger, bringing new
features such as log rotation and compaction out of the box.
The most exciting change in Elixir v1.18 is type checking of function calls, alongside gradual inference of patterns and return types. To understand how this will impact your programs, consider the following code in "lib/user.ex":
You will also find additional convenience functions in `Code`,
`Map`, `Keyword`, all Calendar modules, and others.
## Compile and boot-time improvements
The last several releases brought improvements to compilation
time and this version is no different. In particular, Elixir
now caches and prunes load paths before compilation, ensuring your
project (and dependencies!) compile faster and in an environment
closer to production.
In a nutshell the Erlang VM loads modules from code paths. Each
application that ships with Erlang and Elixir plus each dependency
become an entry in your code path. The larger the code path, the
more work Erlang has to do in order to find a module.
In previous versions, Mix would only add entries to the load paths.
Therefore, if you compiled 20 dependencies and you went to compile
the 21st, the code path would have 21 entries (plus all Erlang and
Elixir apps). This allowed modules from unrelated dependencies to
be seen and made compilation slower the more dependencies you had.
With this release, we will now prune the code paths to only the ones
listed as dependencies, bringing the behaviour closer to `mix release`.
Furthermore, Erlang/OTP 26 allows us to start applications
concurrently and cache the code path lookups, decreasing the cost of
booting applications. The combination of Elixir v1.15 and Erlang/OTP 26
should reduce the boot time of applications, such as when starting
`iex -S mix` or running a single test with `mix test`, from 5% to 30%.
The compiler is also smarter in several ways: `@behaviour` declarations
no longer add compile-time dependencies and aliases in patterns and
guards add no dependency whatsoever, as no dispatching happens. Furthermore,
Mix now tracks the digests of `@external_resource` files, reducing the
amount of recompilation when swapping branches. Finally, dependencies
are automatically recompiled when their compile-time configuration changes.
### Potential incompatibilities
Due to the code path pruning, if you have an application or dependency
that does not specify its dependencies on Erlang and Elixir application,
it may no longer compile successfully in Elixir v1.15. You can temporarily
disable code path pruning by setting `prune_code_paths: false` in your
`mix.exs`, although doing so may lead to runtime bugs that are only
manifested inside a `mix release`.
## Compiler warnings and errors
The Elixir compiler can now emit many errors for a single file, making
sure more feedback is reported to developers before compilation is aborted.
In Elixir v1.14, an undefined function would be reported as:
** (CompileError) undefined function foo/0 (there is no such import)
my_file.exs:1
In Elixir v1.15, the new reports will look like:
error: undefined function foo/0 (there is no such import)
my_file.exs:1
** (CompileError) my_file.exs: cannot compile file (errors have been logged)
A new function, called `Code.with_diagnostics/2`, has been added so this
information can be leveraged by editors, allowing them to point to several
errors at once.
### Potential incompatibilities
As part of this effort, the behaviour where undefined variables were
transformed into nullary function calls, often leading to confusing error
reports, has been disabled during project compilation. You can invoke
`Code.compiler_options(on_undefined_variable: :warn)` at the top of
your `mix.exs` to bring the old behaviour back.
## Integration with Erlang/OTP logger
This release provides additional features such as global logger
metadata and file logging (with rotation and compaction) out-of-the-box!
This release also soft-deprecates Elixir's Logger Backends in
favor of Erlang's Logger handlers. Elixir will automatically
convert your `:console` backend configuration into the new
configuration. Previously, you would set:
```elixir
defmodule User do
defstruct [:age, :car_choice]
def drive(%User{age: age, car_choice: car}, car_choices) when age >= 18 do
if car in car_choices do
{:ok, car}
else
{:error, :no_choice}
end
end
def drive(%User{}, _car_choices) do
{:error, :not_allowed}
end
end
config :logger, :console,
level: :error,
format: "$time $message $metadata"
```
Elixir's type system will infer that the `drive/2` function expects a `%User{}` struct and returns either `{:ok, dynamic()}`, `{:error, :no_choice}`, or `{:error, :not_allowed}`.
Therefore, the following code in a separate module (either in a separate or the same file), should emit a violation, due to an invalid argument:
Which is now translated to the equivalent:
```elixir
User.drive({:ok, %User{}}, car_choices)
config :logger, :default_handler,
level: :error
config :logger, :default_formatter,
format: "$time $message $metadata"
```
Here is the warning:
If you use `Logger.Backends.Console` with a custom device or other
backends, they are still fully supported and functional. If you
implement your own backends, you want to consider migrating to
[`:logger_backends`](https://github.com/elixir-lang/logger_backends)
in the long term.
```
warning: incompatible types given to User.drive/2:
See the new `Logger` documentation for more information on the
new features and on compatibility.
User.drive({:ok, %User{age: nil, car_choice: nil}}, car_choices)
## v1.15.8 (2024-05-21)
given types:
{:ok, %User{age: nil, car_choice: nil}}, empty_list()
but expected one of:
dynamic(%User{age: term(), car_choice: term()}), dynamic()
where "car_choices" was given the type:
# type: empty_list()
# from: lib/foo.ex:21:17
car_choices = []
typing violation found at:
│
22 │ User.drive({:ok, %User{}}, car_choices)
│ ~
│
└─ lib/foo.ex:22:10: Example.run/0
```
> The mismatched arguments are shown in red, if your terminal supports ANSI coloring.
And the next snippet will warn because the `:error` clause will never match, as that's not a valid return type of the `User.drive/2` call:
```elixir
case User.drive(user, car_choices) do
{:ok, car} -> car
:error -> Logger.error("User cannot drive")
end
```
And here is the warning:
```
warning: the following clause will never match:
:error
because it attempts to match on the result of:
User.drive(user, car_choices)
which has type:
dynamic({:ok, term()} or {:error, :no_choice} or {:error, :not_allowed})
typing violation found at:
│
26 │ :error -> Logger.error("User cannot drive")
│ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
│
└─ lib/foo.ex:26: Example.run/0
```
For more details on typing inference and the trade-offs made by the Elixir team, [see our official documentation](https://hexdocs.pm/elixir/1.18/gradual-set-theoretic-types.html#type-inference).
There are many other improvements to the type system, which we will go in detail within the official release. Meanwhile, here is a list summary of the overall improvements done to the type system:
* Type inference of patterns (typing inference of guards will be part of an upcoming release)
* Type checking of all language constructs, including local and remote calls, except `for`, `with`, and closures
* Type checking of all functions inlined by the compiler found in `Kernel`
* Type checking of all conversion functions inlined by the compiler
* [Support for tuples and lists as composite types](https://elixir-lang.org/blog/2024/08/28/typing-lists-and-tuples/) as well as type checking of their basic operations
* Detection of clauses and patterns that will never match from `case`, `cond`, and `=`
* Detection of unused clauses in private functions
## ExUnit improvements
ExUnit now supports parameterized tests to run the same test module multiple times under different parameters.
For example, Elixir ships a local, decentralized and scalable key-value process storage called `Registry`. The registry can be partitioned and its implementation differs depending if partitioning is enabled or not. Therefore, during tests, we want to ensure both modes are exercised. With Elixir v1.18, we can achieve this by writing:
```elixir
defmodule Registry.Test do
use ExUnit.Case,
async: true,
parameterize: [
%{partitions: 1},
%{partitions: 8}
]
# ... the actual tests ...
end
```
ExUnit parameterizes whole test modules. If your modules are configured to run concurrently, as above, so will the parameterized ones.
ExUnit also comes with the ability of specifying test groups. While ExUnit supports running tests concurrently, those tests must not have shared state between them. However, in large applications, it may be common for some tests to depend on some shared state, and other tests to depend on a completely separate state. For example, part of your tests may depend on Cassandra, while others depend on Redis. Prior to Elixir v1.18, these tests could not run concurrently, but in v1.18 they might as long as they are assigned to different groups. Tests modules within the same group do not run concurrently, but across groups, they might.
With features like async tests, suite partitioning, and now grouping, Elixir developers have plenty of flexibility to make the most use of their machine resources, both in development and in CI.
## `mix format --migrate`
The `mix format` command now supports an explicit `--migrate` flag, which will convert constructs that have been deprecated in Elixir to their latest version. Because this flag rewrites the AST, it is not guaranteed the migrated format will always be valid when used in combination with macros that also perform AST rewriting.
As of this release, the following migrations are executed:
* Normalize parens in bitstring modifiers - it removes unnecessary parentheses in known bitstring modifiers, for example `<<foo::binary()>>` becomes `<<foo::binary>>`, or adds parentheses for custom modifiers, where `<<foo::custom_type>>` becomes `<<foo::custom_type()>>`.
* Charlists as sigils - formats charlists as `~c` sigils, for example `'foo'` becomes `~c"foo"`.
* `unless` as negated `if`s - rewrites `unless` expressions using `if` with a negated condition, for example `unless foo do` becomes `if !foo do`.
More migrations may be added in future releases.
## JSON support
This release includes official support for JSON encoding and decoding.
Both encoder and decoder fully conform to [RFC 8259](https://tools.ietf.org/html/rfc8259) and [ECMA 404](https://ecma-international.org/publications-and-standards/standards/ecma-404/) standards.
### Encoding
Encoding can be done via `JSON.encode!/1` and `JSON.encode_to_iodata!/1` functions. The default encoding rules are applied as follows:
| **Elixir** | **JSON** |
|-----------------------------|----------|
| `integer() \| float()` | Number |
| `true \| false ` | Boolean |
| `nil` | Null |
| `binary()` | String |
| `atom()` | String |
| `list()` | Array |
| `%{String.Chars.t() => _}` | Object |
You may also implement the `JSON.Encoder` protocol for custom data structures. Elixir already implements the protocol for all Calendar types.
If you have a struct, you can derive the implementation of the `JSON.Encoder` by specifying which fields should be encoded to JSON:
```elixir
@derive {JSON.Encoder, only: [...]}
defstruct ...
```
### Decoding
Decoding can be done via `JSON.decode/2` and `JSON.decode!/2` functions. The default decoding rules are applied as follows:
| **JSON** | **Elixir** |
|----------|------------------------|
| Number | `integer() \| float()` |
| Boolean | `true \| false` |
| Null | `nil` |
| String | `binary()` |
| Object | `%{binary() => _}` |
## Language server listeners
4 months ago, we welcomed [the Official Language Server team](https://elixir-lang.org/blog/2024/08/15/welcome-elixir-language-server-team/), with the goal of unifying the efforts behind code intelligence, tools, and editors in Elixir. Elixir v1.18 brings new features on this front by introducing locks and listeners to its compilation. Let's understand what it means.
At the moment, all language server implementations have their own compilation environment. This means that your project and dependencies during development are compiled once, for your own use, and then again for the language server. This duplicate effort could cause the language server experience to lag, when it could be relying on the already compiled artifacts of your project.
This release address by introducing a compiler lock, ensuring that only a single operating system process running Elixir compiles your project at a given moment, and by providing the ability for one operating system process to listen to the compilation results of others. In other words, different Elixir instances can now communicate over the same compilation build, instead of racing each other.
These enhancements do not only improve editor tooling, but they also directly benefit projects like IEx and Phoenix. For example, you can invoke `IEx.configure(auto_reload: true)` and IEx will automatically reload modules changed elsewhere, either by a separate terminal or your IDE.
## Potential incompatibilities
This release no longer supports WERL (a graphical user interface on Windows used by Erlang 25 and earlier). For a better user experience on Windows terminals, use Erlang/OTP 26+ (this is also the last Elixir release to support Erlang/OTP 25).
Furthermore, in order to support inference of patterns, Elixir will raise if it finds recursive variable definitions. This means patterns that never match, such as this one, will no longer compile:
def foo(x = {:ok, y}, x = y)
However, recursion of root variables (where variables directly point to each other), will also fail to compile:
def foo(x = y, y = z, z = x)
While the definition above could succeed (as long as all three arguments are equal), the cycle is not necessary and could be removed, as below:
def foo(x = y, y = z, z)
You may also prefer to write using guards:
def foo(x, y, z) when x == y and y == z
## v1.18.5 (2026-08-28)
### 1. Security
* [List] Avoid recursion when invalid charlists are given to `List.to_string/1` or `List.to_charlist/1` (CVE-2026-75758, GHSA-jf5q-v438-665c)
## v1.18.4 (2025-05-21)
This release includes initial support for Erlang/OTP 28, for those who want to try it out. In such cases, you may use Elixir v1.18.4 precompiled for Erlang/OTP 27, as it is binary compatible with Erlang/OTP 28. Note, however, that Erlang/OTP 28 no longer allows regexes to be defined in the module body and interpolated into an attribute. If you do this:
```elixir
@some_attribute ~r/foo/
def some_fun, do: @some_attribute
```
You must rewrite it to:
```elixir
def some_fun, do: ~r/foo/
```
### 1. Enhancements
#### IEx
* [IEx.Helpers] Add `IEx.Helpers.process_info/1` which prints process information
#### Mix
* [mix compile] Support the `--no-listeners` option
* [mix local] Retry HTTP requests with disabled middlebox comp mode depending on the failure reason
* [mix local.hex] Install Hex per OTP release
* [mix local.rebar] Install Hex per OTP release
* [mix run] Support the `--no-listeners` option
### 2. Bug fixes
### 1. Bug fixes
#### Elixir
* [Kernel] Emit trace events for `@on_definition` callbacks
* [Kernel] Emit trace events for `@on_load` callbacks
* [Kernel] Emit trace events for `super` calls
* [Kernel] Emit trace events for imported function calls
* [Kernel] Optimize map unions to avoid building long lists
* [Kernel] Do not crash when type checking nested bitstrings in patterns
* [Kernel] Do not crash when non-binary bitstring is given as struct default value
* [Kernel] Recompile regexes when escaped from module attributes for Erlang/OTP 28 compatibility
* [Kernel] Preserve backwards compatibility in `elixir_erl`
* [bin/elixir] Properly handle the `--dbg` flag in Elixir's CLI
* [System] Add a note that arguments are unsafe when invoking .bat/.com scripts on Windows via `System.cmd/3`
* [Port] Add a note that arguments are unsafe when invoking .bat/.com scripts on Windows
* [URI] Ensure `:undefined` fields are properly converted to `nil` when invoking Erlang's API
#### Logger
* [Logger] Ensure translators are persisted across logger restarts
#### Mix
* [mix deps.get] Ensure git checkout works when there are untracked files in the dependency
* [mix loadpaths] Do not run listeners when not checking the deps
* [mix compile] Ensure compile paths are accessible during compilation
## v1.18.3 (2025-03-06)
## v1.15.7 (2023-10-14)
### 1. Enhancements
#### Elixir
* [JSON] Encode any JSON key to string
* [Kernel] Allow `<<_::3*8>>` in typespecs
* [Elixir] Allow code evaluation across Elixir versions
### 2. Bug fixes
#### EEx
* [EEx] Do not emit duplicate warnings from tokenizer
#### Mix
* [mix loadpaths] Support `--no-listeners` option
* [mix format] Correctly match file to subdirectory in `Mix.Tasks.Format.formatter_for_file/2`
## v1.15.6 (2023-09-20)
This release also includes fixes to the Windows installer.
### 1. Bug fixes
#### EEx
* [EEx] Do not crash when printing tokenizer warnings
#### Elixir
* [Code] Fix formatter for nested `*` in bitstrings
* [Code] Improve feedback when an invalid block is given `Code.quoted_to_algebra/2`
* [Kernel] Trace functions before they are inlined
#### Mix
* [mix compile] Ensure `:extra_applications` declare in umbrella projects are loaded
* [mix deps.get] Do not check for invalid applications before deps.get
* [mix deps.update] Do not check for invalid applications before deps.update
* [mix format] Load plugins when invoking the formatter from an IDE
## v1.15.5 (2023-08-28)
### 1. Enhancements
#### IEx
* [IEx.Autocomplete] Speed up loading of struct suggestions
### 2. Bug fixes
#### Elixir
* [CLI] Fix `--no-color` not setting `:ansi_enabled` to false
* [Protocol] Return correct implementation for an invalid struct pointing to `nil`
* [Stream] Do not raise when `Stream.cycle/1` is explicitly halted
* [Code.Fragment] Fix `Code.Fragment.surround_context/2` for aliases and submodules of non-aliases
* [Kernel] Ensure stacktrace is included when necessary when rescuing multiple exceptions in the same branch
* [Kernel] Fix index in error message for unused optional arguments
#### ExUnit
* [ExUnit.Diff] Fix regression when diffing nested improper lists
* [ExUnit.Diff] Fix scenario where diff would not show up due to a timed-out loop
#### IEx
* [IEx.Autocomplete] Fix autocomplete crash when expanding struct with `__MODULE__`
* [IEx.Helpers] Do not purge on `recompile` if IEx is not running
* [IEx] Force group leader to run as a binary and unicode in IEx
## v1.18.2 (2025-01-22)
#### Mix
* [mix compile] Do not assume `blake` is always available
* [mix format] Load and compile plugins if specified in subdirectories
## v1.15.4 (2023-07-18)
### 1. Bug fixes
#### Mix
* [mix archive.build] Disable protocol consolidation when building archives on archive.install
* [mix compile] Track removed files per local dependency (this addresses a bug where files depending on modules from path dependencies always recompiled)
* [mix release] Do not strip relevant chunks from Erlang/OTP 26
## v1.15.3 (2023-07-15)
### 1. Enhancements
#### Elixir
* [CLI] Add `--color`/`--no-color` for enabling and disabling of ANSI colors
* [Code.Fragment] Provide more AST context when invoking `container_cursor_to_quoted` with trailing fragments
* [Regex] Ensure compatibility with Erlang/OTP 28+ new Regex engine
* [Kernel] Improve stacktraces when executing unnested Elixir code in a file
#### Mix
* [mix] Print compilation lock waiting message to stderr
* [mix] Add an environment variable to optionally disable compilation locking
* [Mix] Allow to opt-out of starting apps in `Mix.install/2`
### 2. Bug fixes
#### Elixir
* [CLI] Temporarily remove PowerShell scripts for `elixir`, `elixirc`, and `mix` on Windows, as they leave the shell broken after quitting Erlang
#### ExUnit
* [ExUnit] Fix crash when diffing bitstring specifiers
* [Code] Ensure `with_diagnostics` propagate warnings from inner Erlang passes
#### IEx
* [IEx.Autocomplete] Fix crashing when autocompleting structs with runtime values
* [IEx] Fix `--remsh` on Erlang/OTP 25 and earlier
#### Mix
* [mix] Track compilation locks per user to avoid permission errors
* [mix deps.update] Ensure Git dependencies can be upgraded by doing so against the origin
* [mix compile.elixir] Ensure `__mix_recompile__?` callbacks are properly invoked
## v1.18.1 (2024-12-24)
## v1.15.2 (2023-07-01)
### 1. Bug fixes
#### IEx
* [IEx] Fix CLI being unable to boot on Windows
## v1.15.1 (2023-06-30)
### 1. Enhancements
* [Kernel] Do not emit type violation warnings when comparing or matching against literals
* [Kernel] Do not validate clauses of private overridable functions
* [Code] `Code.string_to_quoted/2` honors `:static_atoms_encoder` for multi-letter sigils
### 2. Bug fixes
#### Elixir
* [Code.Fragment] Ensure `Code.Fragment.container_cursor_to_quoted/2` with `:trailing_fragment` parses expressions that were supported in previous versions
* [Kernel] Do not crash when typing violation is detected on dynamic dispatch
* [Kernel] Properly annotate the source for warnings emitted by the compiler with the `@file` annotation
* [Kernel] Properly annotate the source for warnings emitted by the type system with the `@file` annotation
* [Kernel] Remove `:no_parens` metadata when using capture with arity on all cases
* [Kernel] Ensure diagnostic traces are kept backwards compatible
#### ExUnit
* [ExUnit.Case] Ensure async groups do not run concurrenly while the test suite is still loading
* [ExUnit.Case] Ensure `--repeat-until-failure` can be combined with groups
* [ExUnit.CaptureLog] Fix race condition on concurrent `capture_log`
* [ExUnit.CaptureLog] Respect options passed to nested `capture_log` calls
* [ExUnit.Doctest] Properly compile doctests without results terminated by fences
* [ExUnit.Doctest] Allow variables defined in doctests to be used in expectation
#### IEx
* [IEx] Ensure `pry` works on Erlang/OTP 25 and earlier while IEx is booting
* [IEx] `Code.Fragment.surround_context` considers surround context around spaces and parens
#### Logger
* [Logger] Do not assume Logger has been loaded at compile-time
* [Logger.Formatter] Properly handle `:function` as metadata
#### Mix
* [mix compile.elixir] Store compilation results if compilation fails due to `--warnings-as-errors`
* [mix deps.loadpaths] Add build lock
* [mix escript.build] Ensure build succeeds when protocol consolidation is disabled
* [Mix.Shell] Ensure encoding is properly respected on Windows and Unix systems
* [mix compile] Ensure the current project is available on the code path after its Elixir sources are compiled
* [mix compile] Guarantee yecc/leex are available when emitting warnings from previous runs
* [mix compile] Fix bug where an external resource was deleted after its
mtime was successfully retrieved
* [mix compile] Track removed modules and exports across local deps
* [mix deps] Fix an issue where dependencies could not be started in an umbrella projects
* [mix release] Properly handle optional dependencies when there is a conflict in the application start mode
* [mix release] Remove `--werl` from release scripts on Erlang/OTP 26
## v1.18.0 (2024-12-19)
## v1.15.0 (2023-06-19)
### 1. Enhancements
#### EEx
* [EEx] Include source code snippets in syntax errors
#### Elixir
* [CLI] Add experimental PowerShell scripts for `elixir`, `elixirc`, and `mix` on Windows. Those provide a safer entry point for running Elixir from other platforms
* [Calendar] Add `Duration.to_string/1`
* [Code] Support several migration options in `Code.format_string!/2`
* [Code] Add parenthesis around `--` and `---` in `Code.format_string!/2` to make precedence clearer
* [Code] Include more metadata in `Code.string_to_quoted/2` when `token_metadata: true` to help compute ranges from the AST
* [Code.Fragment] Have `:capture_arg` as its own entry in `Code.Fragment.surround_context/2`
* [Config] Add `Config.read_config/1`
* [Enumerable] Add `Enum.product_by/2` and `Enum.sum_by/2`
* [Exception] Add `MissingApplicationsError` exception to denote missing applications
* [JSON] Add a new `JSON` module with encoding and decoding functionality
* [JSON] Implement `JSON.Encoder` for all Calendar types
* [Kernel] Update source code parsing to match [UTS #55](https://www.unicode.org/reports/tr55/) latest recommendations. In particular, mixed script is allowed in identifiers as long as they are separate by underscores (`_`), such as `http_сервер`. Previously allowed highly restrictive identifiers, which mixed Latin and other scripts, such as the japanese word for t-shirt, `Tシャツ`, now require the underscore as well
* [Kernel] Warn on bidirectional confusability in identifiers
* [Kernel] Verify the type of the binary generators
* [Kernel] Track the type of tuples in patterns and inside `elem/2`
* [Kernel] Perform validation of root AST nodes in `unquote` and `unquote_splicing` to catch bugs earlier
* [Kernel] Add source, behaviour, and record information to Docs chunk metadata
* [Kernel] Support deterministic builds in tandem with Erlang by setting `ERL_COMPILER_OPTIONS=deterministic`. Keep in mind deterministic builds strip source and other compile time information, which may be relevant for programs
* [Kernel] Allow aliases and imports to be enabled conditionally in module body
* [List] Add `List.ends_with?/2`
* [Macro] Improve `dbg` handling of `if/2`, `with/1` and of code blocks
* [Macro] Add `Macro.struct_info!/2` to return struct information mirroring `mod.__info__(:struct)`
* [Registry] Add `Registry.lock/3` for local locking
* [PartitionSupervisor] Add `PartitionSupervisor.resize!/2` to resize the number of partitions in a supervisor (up to the limit it was started with)
* [Process] Handle arbitrarily high integer values in `Process.sleep/1`
* [Protocol] Add `@undefined_impl_description` to customize error message when an implementation is undefined
* [Protocol] Add `__deriving__/1` as optional macro callback to `Protocol`, no longer requiring empty implementations
* [String] Inspect special whitespace and zero-width characters using their Unicode representation
* [String] Update Unicode to 16.0
* [Calendar] Add support for epoch time (`%s`) to `Calendar.strftime/2`
* [Code] `Code.format_string!/2` now converts `'charlists'` into `~c"charlists"` by default
* [Code] Add `:on_undefined_variable` to the compiler options to preserve the warning behaviour which was deprecated back in Elixir v1.4
* [Code] Add `Code.loaded?/1` and `Code.ensure_all_loaded(!)/1`
* [Code] Add `Code.prepend_paths/1`, `Code.append_paths/1`, and `Code.delete_paths/1`
* [Code] Add `Code.with_diagnostics/2` to return diagnostics when compiling and evaluating code
* [Code.Fragment] Support nested expressions in `Code.Fragment.cursor_context/1`
* [Code.Fragment] Keep operators and no paren calls in `Code.Fragment.container_cursor_to_quoted/1`
* [Date] Add `Date.before?/2` and `Date.after?/2`
* [DateTime] Add `DateTime.before?/2` and `DateTime.after?/2`
* [DateTime] Support precision in `DateTime.utc_now/2`
* [File] Support distributed `File.Stream`
* [Inspect] `Inspect` now renders `'charlists'` as `~c"charlists"` by default
* [Kernel] Break down `case` and `cond` inside `dbg/2`
* [Kernel] Add `t:nonempty_binary/0` and `t:nonempty_bitstring/0`
* [Kernel] Treat `@behaviour`s as runtime dependencies
* [Kernel] Do not add runtime dependencies for alias references in patterns and guards
* [Kernel] Warn for nested calls without parens inside keywords
* [Kernel] Support for multi-letter uppercase sigils
* [Kernel] Introduce mechanism to collect several errors in a module. Previously, as soon as there was a compilation error, compilation would fail. Now the compiler became a bit smarter and will report multiple errors whenever possible as multiple `error: ...` messages, similar to `warning: ...`
* [Kernel] Raise instead of warning on undefined variables. Previously, an undefined variable would attempt to invoke a function of the same name, which led to confusing error messages, especially to newcomers. To enable the previous behaviour, invoke `Code.compiler_options(on_undefined_variable: :warn)` at the top of your `mix.exs`
* [Kernel.CLI] Support `--sname undefined`/`--name undefined` so a name is automatically generated
* [Keyword] Add `Keyword.split_with/2`
* [Macro] Improve error message when piping into an expression ending in bracket-based access
* [Macro.Env] Add `Macro.Env.lookup_alias_as/2`
* [Map] Add `Map.split_with/2`
* [Map] Add `Map.intersect/2` and `Map.intersect/3`
* [MapSet] Add `MapSet.split_with/2`
* [MapSet] Optimize most functions
* [NaiveDateTime] Add `NaiveDateTime.beginning_of_day/1` and `NaiveDateTime.end_of_day/1`
* [NaiveDateTime] Add `NaiveDateTime.before?/2` and `NaiveDateTime.after?/2`
* [NaiveDateTime] Support precision in `NaiveDateTime.utc_now/2`
* [Module] Mark functions as generated in "Docs" chunk
* [Module] Add `Module.get_last_attribute/3`
* [OptionParser] Support `:return_separator` option
* [Process] Add `Process.alias/0,1` and `Process.unalias/1`
* [Range] Add `Range.split/2`
* [String] Update Unicode to version 15.0.0
* [String] Add `:fast_ascii` mode to `String.valid?/2`
* [Supervisor] Add support for automatic shutdown in `Supervisor`
* [System] Support `:lines` in `System.cmd/3` to capture output line by line
* [Task] Remove head of line blocking on `Task.yield_many/2`
* [Task] Enable selective receive optimizations in Erlang/OTP 26+
* [Task] Reduce tasks footprint by avoiding unecessary work during spawning
* [Task.Supervisor] Do not copy args on temporary `Task.Supervisor.start_child/2`
* [Time] Add `Time.before?/2` and `Time.after?/2`
* [URI] Add `URI.append_path/2`
#### ExUnit
* [ExUnit] Support parameterized tests on `ExUnit.Case`
* [ExUnit] Support test groups: tests in the same group never run concurrently
* [ExUnit.Case] Add `test_pid` as a tag
* [ExUnit] Add more color configuration to ExUnit CLI formatter
* [ExUnit.Callbacks] Accept `{module, function}` tuples in ExUnit `setup` callbacks
* [ExUnit.Case] Add `ExUnit.Case.get_last_registered_test/1`
* [ExUnit.Doctest] Add `ExUnit.DocTest.doctest_file/2`
* [ExUnit.Doctest] Include `doctest_data` in doctest tags
* [ExUnit.Formatter] When comparing two anonymous functions, defined at the same place but capturing a different environment, we will now also diff the environments
#### IEx
* [IEx] Add `IEx.configure(auto_reload: true)` to automatically pick up modules recompiled from other operating system processes
* [IEx] Add `:dot_iex` support to `IEx.configure/1`
* [IEx] Add report for normal/shutdown exits in IEx
* [IEx] Make pry opt-in on dbg with `--dbg pry`
* [IEX] Support `IEX_HOME`
* [IEx.Autocomplete] Only provide aliases when autocompleting `alias`, `import`, and `require`
* [IEx.Autocomplete] Provide field completion on map and struct updates
* [IEx.Helpers] Add `runtime_info(:allocators)`
* [IEx.Info] Implement protocol for `Range`, `DateTime`, and `Regex`
#### Logger
* [Logger] Add `Logger.add_handlers/1` and `Logger.default_formatter/1`
* [Logger] Introduce `default_formatter` and `default_handler` configuration for Logger which configures Erlang/OTP logger
* [Logger] Add `:always_evaluate_messages` configuration to Logger
* [Logger.Formatter] Implement the Erlang Logger formatter API
* [Logger.Formatter] Add support for ports in Logger metadata
#### Mix
* [mix compile] Ensure only a single operating system process can compile at a given time
* [mix deps.get] Ensure only a single operating system process can fetch deps at a given time
* [mix format] Add `mix format --migrate` to migrate from deprecated functionality
* [mix format] Add new options and metadata to improve formatting applying by editors and other environments
* [mix test] Taint failure manifest if requiring or compiling tests fail
* [Mix.Project] Add a `:listeners` configuration to listen to compilation events from the current and other operating system processes
* [Mix.Task.Compiler] Add API for fetching all persisted compiler diagnostics
* [Mix.Task.Compiler] Add API for fetching all compiler tasks
* [mix app.start] Allow applications to be started concurrently via the `:start_concurrently` configuration
* [mix compile] Set `--all-warnings` by default
* [mix compile] Reduce the amount of filesystem lookups for path dependencies by storing timestamps in manifests
* [mix compile] Track digests of `@external_resources`
* [mix compile.app] Write `optional_applications` to `.app` file
* [mix compile.elixir] Add `--purge-consolidation-path-if-stale` which will purge the given consolidation path if compilation is required
* [mix deps.compile] Automatically recompile dependencies if their compile env changes
* [mix deps.get] Automatically install Hex and Rebar on `mix deps.get`/`mix deps.update`
* [mix deps.get] Support `--check-locked` which raises if changes to the lockfile are required
* [mix eval] Allow passing additional arguments
* [mix format] Support `--no-exit` option
* [mix format] Allow multiple formatters per file extension and sigil
* [mix format] Show diffs whenever `--check-formatted` fails
* [mix format] Allow the formatting root to be configured
* [mix loadpaths] Cache deps and archive loadpaths in Erlang/OTP 26
* [mix profile.fprof] Support `--trace-to-file` to improve performance when working with large outputs
* [mix release] Allow passing additional arguments to the `eval` command
* [mix xref graph] Support `--output` flag
* [Mix.Project] Support `def cli` to unify all CLI defaults in a single place
* [Mix.Project] Add `Mix.Project.deps_tree/1`
### 2. Bug fixes
#### Elixir
* [Code] Fix delimiter metadata for single quoted atoms and remote calls in `Code.string_to_quoted/2`
* [Code.Formatter] Fix formatter adding extra escapes to quoted remote calls
* [Code.Fragment] Properly handle keyword keys as their own entry
* [Inspect.Algebra] Ensure `next_break_fits` respects `line_length`
* [Kernel] Validate AST on `unquote` and `unquote_splicing` to provide better error reports instead of failing too late inside the compiler
* [Kernel] Avoid crashes when emitting diagnostics on code using \t for indentation
* [Module] Include module attribute line and name when tracing its aliases
* [Stream] Do not halt streams twice in `Stream.transform/5`
* [URI] Fix a bug when a schemaless URI is given to `URI.merge/2`
* [Code.Formatter] Fix a scenario where a keyword followed by parenthesis could go above the maximum line length
* [Code.Formatter] Remove unnecessary parens in nullary type funs
* [Exception] Fix operator precedence when printing guards in `Exception.blame/3`
* [File] Do not raise if there are file system race conditions in `File.cp/2`
* [File] Do not raise when deleting write-only empty directories on `File.rm_rf/1`
* [Kernel] Expand macros on the left side of -> in `try/rescue`
* [Kernel] Raise on misplaced `...` inside typespecs
* [Kernel] Do not import `behaviour_info` and `module_info` functions from Erlang modules
* [Kernel] Raise when macros are given to dialyzer
* [Kernel.ParallelCompiler] Make sure compiler doesn't crash when there are stray messages in the inbox
* [Kernel.ParallelCompiler] Track compile and runtime warnings separately
* [Module] Ensure that `Module.get_attribute/3` returns `nil` and not the given default value when an attribute has been explicitly set as `nil`
* [System] Fix race condition when a script would terminate before `System.stop/1` executes
* [Task] Do not double log Task failure reports
* [URI] Make sure `URI.merge/2` works accordingly with relative paths
#### ExUnit
* [ExUnit.Assertions] Raise if guards are used in `assert/1` with `=`
* [ExUnit.Assertions] Format inserted/deleted maps in list assertions
* [ExUnit] Fix crash when `@tag capture_log: true` was set to true and the Logger application was shut down in the middle of the test
* [ExUnit] Do not merge context as tags inside the runner to reduce memory usage when emitting events to formatters
* [ExUnit] Mark test cases as invalid when an exit occurs during `setup_all`
* [ExUnit] Do not expand or collect vars from quote in ExUnit assertions
* [ExUnit.DocTest] Ensure proper line is returned when failing to parse doctest results
* [ExUnit.Doctest] Fix line information when a doctest with multiple assertions fails
#### IEx
* [IEx.Helpers] `IEx.Helpers.recompile/0` will reload modules changed by other operating system processes
* [IEx] Do not spawn a process to read IO. This fixes a bug where multiline paste stopped working
whenever the input reader was killed
* [IEx] Do not perform completion for prompts triggered during code evaluation
#### Mix
* [mix compile] Ensure warnings from external resources are emitted with `--all-warnings` when files do not change
* [mix deps.compile] Fix escaping issues when invoking `rebar3` in some cases
* [mix escript] Fix escript layout and support storing `priv` directories
* [mix release] Make `.app` files deterministic in releases
* [Mix.Shell] Fix `Mix.Shell` on Windows when outputting non UTF-8 characters
* [mix compile] Include `cwd` in compiler cache key
* [mix release] Fix Windows service when invoking `erlsrv.exe` in path with spaces
* [mix xref] Raise early if `mix xref` is used at the umbrella root
### 3. Soft deprecations (no warnings emitted)
#### Elixir
* [Inspect.Algebra] `color/3` is deprecated in favor of `color_doc/3`
* [Inspect.Algebra] `fold_doc/2` is deprecated in favor of `fold/2`
* [Kernel] Deprecate `unless` in favor of `if`. Use `mix format --migrate` to automate the migration
* [Macro] `Macro.struct!/2` is deprecated in favor of `Macro.struct_info!/2`
* [Protocol] Defining `__deriving__/3` inside the `Any` implementation is deprecated, derive it inside the protocol definition itself
* [File] `File.cp/3` and `File.cp_r/3` with a function as third argument
is deprecated in favor of a keyword list
* [Kernel] Require pin variable when accessing variable inside binary size in match
* [Kernel.ParallelCompiler] Require the `:return_diagnostics` option to be
set to true when compiling or requiring code
### 4. Hard deprecations
#### Logger
#### EEx
* [EEx] `<%#` is deprecated in favor of `<%!--` or `<% #`
* [EEx] `c:EEx.handle_text/2` is deprecated in favor of `c:EEx.handle_text/3`
#### Elixir
* [Code] Setting `:warnings_as_errors` is deprecated via `Code.put_compiler_option/2`. This must not affect developers, as the `:warnings_as_errors` option is managed by Mix tasks, and not directly used via the `Code` module
* [Enumerable] Deprecate returning a two-arity function in `Enumerable.slice/1`
* [List] `List.zip/1` is deprecated in favor of `Enum.zip/1`
* [Module] Deprecate `Module.eval_quoted/3` in favor of `Code.eval_quoted/3`
* [Range] Deprecate inferring negative ranges on `Range.new/2`
* [Tuple] `Tuple.append/2` is deprecated, use `Tuple.insert_at/3` instead
* [Logger] `add_backend/2`, `remove_backend/2`, and `configure_backend/2` have been deprecated
in favor of the new `:logger_backends` dependency
* [Logger] The `:console` configuration has been deprecated in favor of `:default_formatter`
* [Logger] The `:backends` configuration has been deprecated in favor of `Logger.add_handlers/1`
#### Mix
* [mix cmd] Deprecate `mix cmd --app APP` in favor of `mix do --app APP`
* [mix compile] `:warnings_as_errors` configuration in `:elixirc_options` is deprecated. Instead pass the `--warnings-as-errors` flag to `mix compile`. Alternatively, you might alias the task: `aliases: [compile: "compile --warnings-as-errors"]`
* [mix test] `:warnings_as_errors` configuration in `:test_elixirc_options` is deprecated. Instead pass the `--warnings-as-errors` flag to `mix test`. Alternatively, you might alias the task: `aliases: [test: "test --warnings-as-errors"]`
* [Mix.Tasks.Compile] Deprecate `compilers/0` in favor of `Mix.Task.Compiler.compilers/0`
* [Mix.Project] `:preferred_cli_env` is deprecated in favor of `:preferred_envs` in `def cli`
* [Mix.Project] `:preferred_cli_target` is deprecated in favor of `:preferred_targets` in `def cli`
* [mix local] The environment variable `HEX_MIRROR` is deprecated in favor of `HEX_BUILDS_URL`
### 5. Potential breaking changes
### 4. Hard deprecations
#### Elixir
* [Kernel] Using unquote is deprecated when quote is used inside a pattern or guard
* [Calendar] `Calendar.ISO.day_of_week/3` is deprecated in favor of `Calendar.ISO.day_of_week/4`
* [Exception] `Exception.exception?/1` is deprecated in favor of `Kernel.is_exception/1`
* [Kernel] Deprecate `...` as a valid function call identifier
* [Regex] `Regex.regex?/1` is deprecated in favor of `Kernel.is_struct/2`
## v1.17
#### Logger
The CHANGELOG for v1.17 releases can be found [in the v1.17 branch](https://github.com/elixir-lang/elixir/blob/v1.17/CHANGELOG.md).
* [Logger] `Logger.warn/2` is deprecated in favor of `Logger.warning/2`
## v1.14
The CHANGELOG for v1.14 releases can be found [in the v1.14 branch](https://github.com/elixir-lang/elixir/blob/v1.14/CHANGELOG.md).
+39 -33
View File
@@ -2,10 +2,12 @@ PREFIX ?= /usr/local
TEST_FILES ?= "*_test.exs"
SHARE_PREFIX ?= $(PREFIX)/share
MAN_PREFIX ?= $(SHARE_PREFIX)/man
# CANONICAL := main/
CANONICAL := 1.15/
CANONICAL ?= main/
DOCS_FORMAT ?= html
ELIXIRC := bin/elixirc --ignore-module-conflict $(ELIXIRC_OPTS)
ERLC := erlc -I lib/elixir/include
ERL_MAKE := erl -make
ERL_MAKE := if [ -n "$(ERLC_OPTS)" ]; then ERL_COMPILER_OPTIONS=$(ERLC_OPTS) erl -make; else erl -make; fi
ERL := erl -I lib/elixir/include -noshell -pa lib/elixir/ebin
GENERATE_APP := $(CURDIR)/lib/elixir/scripts/generate_app.escript
VERSION := $(strip $(shell cat VERSION))
@@ -17,19 +19,19 @@ INSTALL_DIR = $(INSTALL) -m755 -d
INSTALL_DATA = $(INSTALL) -m644
INSTALL_PROGRAM = $(INSTALL) -m755
GIT_REVISION = $(strip $(shell git rev-parse HEAD 2> /dev/null ))
GIT_TAG = $(strip $(shell head="$(call GIT_REVISION)"; git tag --points-at $$head 2> /dev/null | grep -v latest | tail -1))
GIT_TAG = $(strip $(shell head="$(call GIT_REVISION)"; git tag --points-at $$head 2> /dev/null | tail -1) )
SOURCE_DATE_EPOCH_PATH = lib/elixir/tmp/ebin_reproducible
SOURCE_DATE_EPOCH_FILE = $(SOURCE_DATE_EPOCH_PATH)/SOURCE_DATE_EPOCH
.PHONY: install install_man build_plt clean_plt dialyze test check_reproducible clean clean_elixir clean_man format docs Docs.zip Precompiled.zip zips
.PHONY: install compile erlang elixir unicode app build_plt clean_plt dialyze test check_reproducible clean clean_residual_files format install_man clean_man docs Docs.zip Precompiled.zip zips
.NOTPARALLEL:
#==> Functions
define CHECK_ERLANG_RELEASE
erl -noshell -eval '{V,_} = string:to_integer(erlang:system_info(otp_release)), io:fwrite("~s", [is_integer(V) and (V >= 25)])' -s erlang halt | grep -q '^true'; \
erl -noshell -eval '{V,_} = string:to_integer(erlang:system_info(otp_release)), io:fwrite("~s", [is_integer(V) and (V >= 24)])' -s erlang halt | grep -q '^true'; \
if [ $$? != 0 ]; then \
echo "At least Erlang/OTP 25.0 is required to build Elixir"; \
echo "At least Erlang/OTP 24.0 is required to build Elixir"; \
exit 1; \
fi
endef
@@ -45,7 +47,7 @@ lib/$(1)/ebin/Elixir.$(2).beam: $(wildcard lib/$(1)/lib/*.ex) $(wildcard lib/$(1
@ rm -rf lib/$(1)/ebin
$(Q) cd lib/$(1) && ../../$$(ELIXIRC) "lib/**/*.ex" -o ebin
test_$(1): test_formatted $(1)
test_$(1): compile $(1)
@ echo "==> $(1) (ex_unit)"
$(Q) cd lib/$(1) && ../../bin/elixir -r "test/test_helper.exs" -pr "test/**/$(TEST_FILES)";
endef
@@ -65,21 +67,17 @@ endef
#==> Compilation tasks
APP := lib/elixir/ebin/elixir.app
EEX := lib/eex/ebin/Elixir.EEx.beam
ELIXIR := lib/elixir/ebin/elixir.beam
PARSER := lib/elixir/src/elixir_parser.erl
KERNEL := lib/elixir/ebin/Elixir.Kernel.beam
UNICODE := lib/elixir/ebin/Elixir.String.Unicode.beam
default: compile
compile: erlang elixir
compile: erlang $(APP) elixir
erlang: $(ELIXIR)
$(ELIXIR): $(PARSER) lib/elixir/src/*
erlang: $(PARSER)
$(Q) if [ ! -f $(APP) ]; then $(call CHECK_ERLANG_RELEASE); fi
$(Q) cd lib/elixir && mkdir -p ebin && $(ERL_MAKE)
$(Q) $(GENERATE_APP) $(VERSION)
$(PARSER): lib/elixir/src/elixir_parser.yrl
$(Q) erlc -o $@ +'{verbose,true}' +'{report,true}' $<
@@ -87,27 +85,29 @@ $(PARSER): lib/elixir/src/elixir_parser.yrl
# Since Mix depends on EEx and EEx depends on Mix,
# we first compile EEx without the .app file,
# then Mix, and then compile EEx fully
elixir: stdlib $(EEX) mix ex_unit logger eex iex
stdlib: $(KERNEL) $(UNICODE) $(APP)
elixir: stdlib lib/eex/ebin/Elixir.EEx.beam mix ex_unit logger eex iex
$(KERNEL): lib/elixir/src/* lib/elixir/lib/*.ex lib/elixir/lib/*/*.ex lib/elixir/lib/*/*/*.ex VERSION
stdlib: $(KERNEL) VERSION
$(KERNEL): lib/elixir/lib/*.ex lib/elixir/lib/*/*.ex lib/elixir/lib/*/*/*.ex
$(Q) if [ ! -f $(KERNEL) ]; then \
echo "==> bootstrap (compile)"; \
$(ERL) -s elixir_compiler bootstrap -s erlang halt; \
"$(MAKE)" unicode; \
fi
$(Q) "$(MAKE)" unicode
@ echo "==> elixir (compile)";
$(Q) cd lib/elixir && ../../$(ELIXIRC) "lib/**/*.ex" -o ebin;
$(Q) "$(MAKE)" app
app: $(APP)
$(APP): lib/elixir/src/elixir.app.src lib/elixir/ebin VERSION $(GENERATE_APP)
$(Q) $(GENERATE_APP) $(VERSION)
$(Q) $(GENERATE_APP) $< $@ $(VERSION)
unicode: $(UNICODE)
$(UNICODE): lib/elixir/unicode/*
@ echo "==> unicode (compile)";
$(Q) $(ELIXIRC) lib/elixir/unicode/unicode.ex -o lib/elixir/ebin;
$(Q) $(ELIXIRC) lib/elixir/unicode/tokenizer.ex -o lib/elixir/ebin;
$(Q) $(ELIXIRC) lib/elixir/unicode/security.ex -o lib/elixir/ebin;
$(Q) $(ELIXIRC) lib/elixir/unicode/tokenizer.ex -o lib/elixir/ebin;
$(eval $(call APP_TEMPLATE,ex_unit,ExUnit))
$(eval $(call APP_TEMPLATE,logger,Logger))
@@ -146,7 +146,6 @@ check_reproducible: compile
$(Q) mv lib/iex/ebin/* lib/iex/tmp/ebin_reproducible/
$(Q) mv lib/logger/ebin/* lib/logger/tmp/ebin_reproducible/
$(Q) mv lib/mix/ebin/* lib/mix/tmp/ebin_reproducible/
$(Q) rm -rf lib/*/ebin
SOURCE_DATE_EPOCH=$(call READ_SOURCE_DATE_EPOCH) "$(MAKE)" compile
$(Q) echo "Diffing..."
$(Q) bin/elixir lib/elixir/scripts/diff.exs lib/elixir/ebin/ lib/elixir/tmp/ebin_reproducible/
@@ -157,10 +156,16 @@ check_reproducible: compile
$(Q) bin/elixir lib/elixir/scripts/diff.exs lib/mix/ebin/ lib/mix/tmp/ebin_reproducible/
$(Q) echo "Builds are reproducible"
clean: clean_man
clean:
rm -rf ebin
rm -rf lib/*/ebin
rm -rf $(PARSER)
$(Q) "$(MAKE)" clean_residual_files
clean_elixir:
$(Q) rm -f lib/*/ebin/Elixir.*.beam
clean_residual_files:
rm -rf lib/*/_build/
rm -rf lib/*/tmp/
rm -rf lib/elixir/test/ebin/
@@ -170,14 +175,13 @@ clean: clean_man
rm -rf lib/mix/test/fixtures/git_sparse_repo/
rm -rf lib/mix/test/fixtures/archive/ebin/
rm -f erl_crash.dump
clean_elixir:
$(Q) rm -f lib/*/ebin/Elixir.*.beam
$(Q) "$(MAKE)" clean_man
#==> Documentation tasks
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}")
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)" --logo lib/elixir/pages/images/logo.png --output doc/$(2) --canonical "https://hexdocs.pm/$(2)/$(CANONICAL)" --homepage-url "https://elixir-lang.org/docs.html" $(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
@@ -304,15 +308,17 @@ dialyze: compile $(PLT)
build_man: man/iex.1 man/elixir.1
define BUILD_MANPAGES
man/$(APP).1:
$(Q) cp man/$(APP).1.in man/$(APP).1
$(Q) sed -i.bak "/{COMMON}/r man/common" man/$(APP).1
$(Q) sed -i.bak "/{COMMON}/d" man/$(APP).1
$(Q) rm -f man/$(APP).1.bak
endef
man/iex.1:
$(Q) cp man/iex.1.in man/iex.1
$(Q) sed -i.bak "/{COMMON}/r man/common" man/iex.1
$(Q) sed -i.bak "/{COMMON}/d" man/iex.1
$(Q) rm -f man/iex.1.bak
$(foreach APP, elixir iex, $(eval $(BUILD_MANPAGES)))
man/elixir.1:
$(Q) cp man/elixir.1.in man/elixir.1
$(Q) sed -i.bak "/{COMMON}/r man/common" man/elixir.1
$(Q) sed -i.bak "/{COMMON}/d" man/elixir.1
$(Q) rm -f man/elixir.1.bak
clean_man:
rm -f man/elixir.1
+1 -2
View File
@@ -3,10 +3,9 @@ LEGAL NOTICE INFORMATION
All the files in this distribution are copyright to the terms below.
== lib/elixir/src/elixir_json.erl
== lib/elixir/src/elixir_parser.erl (generated by build scripts)
Copyright Ericsson AB 1996-2024
Copyright Ericsson AB 1996-2015
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
+39 -50
View File
@@ -1,11 +1,7 @@
<h1>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://github.com/elixir-lang/elixir-lang.github.com/raw/main/images/logo/logo-dark.png">
<img alt="Elixir logo" src="https://github.com/elixir-lang/elixir-lang.github.com/raw/main/images/logo/logo.png" width="200">
</picture>
</h1>
<img src="https://github.com/elixir-lang/elixir-lang.github.com/raw/main/images/logo/logo.png#gh-light-mode-only" width="200" alt="Elixir">
<img src="https://github.com/elixir-lang/elixir-lang.github.com/raw/main/images/logo/logo-dark.png#gh-dark-mode-only" width="200" alt="Elixir">
[![CI](https://github.com/elixir-lang/elixir/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/elixir-lang/elixir/actions/workflows/ci.yml?query=branch%3Amain)
[![CI](https://github.com/elixir-lang/elixir/workflows/CI/badge.svg?branch=main)](https://github.com/elixir-lang/elixir/actions?query=branch%3Amain+workflow%3ACI)
Elixir is a dynamic, functional language designed for building scalable
and maintainable applications.
@@ -16,7 +12,7 @@ For more about Elixir, installation and documentation,
## Policies
New releases are announced in the [announcement mailing list][8].
You can subscribe by sending an email to <elixir-lang-ann+subscribe@googlegroups.com>
You can subscribe by sending an email to elixir-lang-ann+subscribe@googlegroups.com
and replying to the confirmation email.
All security releases [will be tagged with `[security]`][10]. For more
@@ -29,28 +25,28 @@ All interactions in our official communication channels follow our
For reporting bugs, [visit our issue tracker][2] and follow the steps
for reporting a new issue. **Please disclose security vulnerabilities
privately at <elixir-security@googlegroups.com>**.
privately at elixir-security@googlegroups.com**.
## Issues tracker management
All currently open bugs related to the Elixir repository are listed
in the issues tracker. The Elixir team uses the issues tracker to focus
on *actionable items*, including planned enhancements in the short and
medium term. We also do our best to label entries for clarity and to ease
on *actionable items*, including planned enhancements in the short- and
medium-term. We also do our best to label entries for clarity and to ease
collaboration.
Our *actionable item policy* has some important consequences, such as:
* Proposing new features as well as requests for support, help, and
* Proposing new features as well as request for support, help, and
guidance must be done in their own spaces, detailed next.
* Issues we have identified to be outside of Elixir's scope,
such as an upstream bug, will be closed (and requested to be moved
* Issues where we have identified to be outside of Elixir scope,
such as a bug upstream, will be closed (and requested to be moved
elsewhere if appropriate).
* We actively close unrelated and non-actionable issues to keep the
issues tracker tidy. We may get things wrong from time to
time and will gladly revisit issues, reopening when necessary.
issues tracker tidy. However, we may get things wrong from time to
time, so we are glad to revisit issues and reopen if necessary.
Keep the tone positive and be kind! For more information, see the
[Code of Conduct][1].
@@ -76,7 +72,7 @@ in the next release are then "closed" and added to the [changelog][7].
### Discussions, support, and help
For general discussions, support, and help, please use the community
For general discussions, support, and help, please use many of the community
spaces [listed on the sidebar of the Elixir website](https://elixir-lang.org/),
such as forums, chat platforms, etc, where the wider community will be available
to help you.
@@ -93,7 +89,7 @@ After that, clone this repository to your machine, compile and test it:
```sh
git clone https://github.com/elixir-lang/elixir.git
cd elixir
make
make clean test
```
> Note: if you are running on Windows,
@@ -103,35 +99,35 @@ on Windows](https://github.com/elixir-lang/elixir/wiki/Windows).
In case you want to use this Elixir version as your system version,
you need to add the `bin` directory to [your PATH environment variable](https://elixir-lang.org/install.html#setting-path-environment-variable).
When updating the repository, you may want to run `make clean` before
recompiling. For deterministic builds, you should set the environment
variable `ERL_COMPILER_OPTIONS=deterministic`.
If Elixir fails to build (specifically when pulling in a new version via
`git`), be sure to remove any previous build artifacts by running
`make clean`, then `make test`.
## Contributing
We invite contributions to Elixir. To contribute, there are a few
We welcome everyone to contribute to Elixir. To do so, there are a few
things you need to know about the code. First, Elixir code is divided
by each application inside the `lib` folder:
in applications inside the `lib` folder:
* `elixir` - Elixir's kernel and standard library
* `elixir` - Elixir's kernel and standard library
* `eex` - EEx is the template engine that allows you to embed Elixir
* `eex` - EEx is the template engine that allows you to embed Elixir
* `ex_unit` - ExUnit is a simple test framework that ships with Elixir
* `ex_unit` - ExUnit is a simple test framework that ships with Elixir
* `iex` - IEx stands for Interactive Elixir: Elixir's interactive shell
* `iex` - IEx stands for Interactive Elixir: Elixir's interactive shell
* `logger` - Logger is the built-in logger
* `logger` - Logger is the built-in logger
* `mix` - Mix is Elixir's build tool
* `mix` - Mix is Elixir's build tool
You can run all tests in the root directory with `make test`. You can
also run tests for a specific framework with `make test_#{APPLICATION}`, for example,
You can run all tests in the root directory with `make test` and you can
also run tests for a specific framework `make test_#{APPLICATION}`, for example,
`make test_ex_unit`. If you just changed something in Elixir's standard
library, you can run only that portion through `make test_stdlib`.
If you are only changing one file, you can choose to compile and run tests
for that specific file for faster development cycles. For example, if you
If you are changing just one file, you can choose to compile and run tests only
for that particular file for fast development cycles. For example, if you
are changing the String module, you can compile it and run its tests as:
```sh
@@ -139,27 +135,20 @@ bin/elixirc lib/elixir/lib/string.ex -o lib/elixir/ebin
bin/elixir lib/elixir/test/elixir/string_test.exs
```
Some test files need their `test_helper.exs` to be explicitly required
before, such as:
```sh
bin/elixir -r lib/logger/test/test_helper.exs lib/logger/test/logger_test.exs
```
You can also use the `LINE` env var to run a single test:
```sh
LINE=123 bin/elixir lib/elixir/test/elixir/string_test.exs
````
To recompile all (including Erlang modules):
To recompile (including Erlang modules):
```sh
make compile
```
After your changes are done, please remember to run `make format` to guarantee
all files are properly formatted, then run the full suite with
all files are properly formatted and then run the full suite with
`make test`.
If your contribution fails during the bootstrapping of the language,
@@ -169,7 +158,7 @@ you can rebuild the language from scratch with:
make clean_elixir compile
```
Similarly, if you can not get Elixir to compile or the tests to pass after
Similarly, if you can't get Elixir to compile or the tests to pass after
updating an existing checkout, run `make clean compile`. You can check
[the official build status](https://github.com/elixir-lang/elixir/actions/workflows/ci.yml).
More tasks can be found by reading the [Makefile](Makefile).
@@ -179,9 +168,9 @@ With tests running and passing, you are ready to contribute to Elixir and
We have saved some excellent pull requests we have received in the past in
case you are looking for some examples:
* [Implement Enum.member? - Pull request](https://github.com/elixir-lang/elixir/pull/992)
* [Add String.valid? - Pull request](https://github.com/elixir-lang/elixir/pull/1058)
* [Implement capture_io for ExUnit - Pull request](https://github.com/elixir-lang/elixir/pull/1059)
* [Implement Enum.member? - Pull request](https://github.com/elixir-lang/elixir/pull/992)
* [Add String.valid? - Pull request](https://github.com/elixir-lang/elixir/pull/1058)
* [Implement capture_io for ExUnit - Pull request](https://github.com/elixir-lang/elixir/pull/1059)
### Reviewing changes
@@ -189,7 +178,7 @@ Once a pull request is sent, the Elixir team will review your changes.
We outline our process below to clarify the roles of everyone involved.
All pull requests must be approved by two committers before being merged into
the repository. If changes are necessary, the team will leave appropriate
the repository. If any changes are necessary, the team will leave appropriate
comments requesting changes to the code. Unfortunately, we cannot guarantee a
pull request will be merged, even when modifications are requested, as the Elixir
team will re-evaluate the contribution as it changes.
@@ -209,13 +198,13 @@ a comment.
## Building documentation
Building the documentation requires that [ExDoc](https://github.com/elixir-lang/ex_doc)
is installed and built alongside Elixir:
Building the documentation requires [ExDoc](https://github.com/elixir-lang/ex_doc)
to be installed and built alongside Elixir:
```sh
# After cloning and compiling Elixir, in its parent directory:
git clone https://github.com/elixir-lang/ex_doc.git
cd ex_doc && ../elixir/bin/elixir ../elixir/bin/mix do deps.get + compile
cd ex_doc && ../elixir/bin/mix do deps.get + compile
```
Now go back to Elixir's root directory and run:
+8 -10
View File
@@ -2,27 +2,25 @@
## Shipping a new version
1. Update version in /VERSION, bin/elixir, and bin/elixir.bat
1. Update version in /VERSION, bin/elixir and bin/elixir.bat
2. Ensure /CHANGELOG.md is updated, versioned and add the current date
3. Update "Compatibility and Deprecations" if a new OTP version is supported
4. Commit changes above with title "Release vVERSION" and push it
4. Commit changes above with title "Release vVERSION", generate a new tag, and push it
5. Once GitHub actions completes, generate a new tag, and push it
5. Wait until GitHub Actions publish artifacts to the draft release and the CI is green
6. Wait until GitHub Actions publish artifacts to the draft release
6. Copy the relevant bits from /CHANGELOG.md to the GitHub release and publish it
7. Copy the relevant bits from /CHANGELOG.md to the GitHub release and publish it (link to the announcement if there is one)
7. Update `_data/elixir-versions.yml` (except for RCs) in `elixir-lang/elixir-lang.github.com`
8. Update `_data/elixir-versions.yml` (except for RCs) in `elixir-lang/elixir-lang.github.com`
## Creating a new vMAJOR.MINOR branch (before first rc)
## Creating a new vMAJOR.MINOR branch (after first rc)
### In the new branch
1. Comment out `CANONICAL=` in /Makefile
1. Set `CANONICAL=` in /Makefile
2. Update tables in /SECURITY.md and "Compatibility and Deprecations"
@@ -30,7 +28,7 @@
### Back in main
1. Bump /VERSION file, bin/elixir, bin/elixir.bat, and bin/elixir.ps1
1. Bump /VERSION file, bin/elixir and bin/elixir.bat
2. Start new /CHANGELOG.md
+7 -7
View File
@@ -6,18 +6,18 @@ Elixir applies bug fixes only to the latest minor branch. Security patches are a
Elixir version | Support
:------------- | :-----------------------------
1.18 | Bug fixes and security patches
1.17 | Security patches only
1.16 | Security patches only
1.15 | Security patches only
1.15 | Bug fixes and security patches
1.14 | Security patches only
1.13 | Security patches only
1.12 | Security patches only
1.11 | Security patches only
## Announcements
New releases are announced in the read-only [announcements mailing list](https://groups.google.com/group/elixir-lang-ann). You can subscribe by sending an email to elixir-lang-ann+subscribe@googlegroups.com and replying to the confirmation email. Security notifications [will be tagged with `[security]`](https://groups.google.com/forum/#!searchin/elixir-lang-ann/%5Bsecurity%5D%7Csort:date).
New releases are announced in the read-only [announcements mailing list](https://groups.google.com/group/elixir-lang-ann). You can subscribe by sending an email to elixir-lang-ann+subscribe@googlegroups.com and replying to the confirmation email.
You may also see [all releases](https://github.com/elixir-lang/elixir/releases) and [consult all disclosed vulnerabilities](https://github.com/elixir-lang/elixir/security) on GitHub.
Security notifications [will be tagged with `[security]`](https://groups.google.com/forum/#!searchin/elixir-lang-ann/%5Bsecurity%5D%7Csort:date).
## Reporting a vulnerability
[Please disclose security vulnerabilities privately via GitHub](https://github.com/elixir-lang/elixir/security).
Please disclose security vulnerabilities privately at elixir-security@googlegroups.com
+1 -1
View File
@@ -1 +1 @@
1.18.5
1.15.8
+8 -5
View File
@@ -1,7 +1,7 @@
#!/bin/sh
set -e
ELIXIR_VERSION=1.18.5
ELIXIR_VERSION=1.15.8
if [ $# -eq 0 ] || { [ $# -eq 1 ] && { [ "$1" = "--help" ] || [ "$1" = "-h" ]; }; }; then
cat <<USAGE >&2
@@ -18,13 +18,13 @@ Usage: $(basename "$0") [options] [.exs file] [data]
-pz "PATH" Appends the given path to Erlang code path (*)
-v, --version Prints Erlang/OTP and Elixir versions (standalone)
--color, --no-color Enables or disables ANSI coloring
--erl "SWITCHES" Switches to be passed down to Erlang (*)
--eval "COMMAND" Evaluates the given command, same as -e (*)
--logger-otp-reports BOOL Enables or disables OTP reporting
--logger-sasl-reports BOOL Enables or disables SASL reporting
--no-halt Does not halt the Erlang VM after execution
--short-version Prints Elixir version (standalone)
--werl Uses Erlang's Windows shell GUI (Windows only)
Options given after the .exs file or -- are passed down to the executed code.
Options can be passed to the Erlang runtime using \$ELIXIR_ERL_OPTIONS or --erl.
@@ -112,7 +112,7 @@ while [ $I -le $LENGTH ]; do
C=1
MODE="iex"
;;
-v|--no-halt|--color|--no-color)
-v|--no-halt)
C=1
;;
-e|-r|-pr|-pa|-pz|--eval|--remsh|--dot-iex|--dbg)
@@ -182,6 +182,10 @@ while [ $I -le $LENGTH ]; do
echo "--pipe-to : LOGDIR cannot be a switch" >&2 && exit 1
fi
;;
--werl)
S=1
if [ "$OS" = "Windows_NT" ]; then ERL_EXEC="werl"; fi
;;
*)
while [ $I -le $LENGTH ]; do
I=$((I + 1))
@@ -214,7 +218,6 @@ SELF=$(readlink_f "$0")
SCRIPT_PATH=$(dirname "$SELF")
if [ "$OSTYPE" = "cygwin" ]; then SCRIPT_PATH=$(cygpath -m "$SCRIPT_PATH"); fi
if [ "$MODE" != "iex" ]; then ERL="-s elixir start_cli $ERL"; fi
if [ "$OS" != "Windows_NT" ] && [ -z "$NO_COLOR" ]; then
if test -t 1 -a -t 2; then ERL="-elixir ansi_enabled true $ERL"; fi
@@ -225,7 +228,7 @@ fi
ERTS_BIN=
ERTS_BIN="$ERTS_BIN"
set -- "$ERTS_BIN$ERL_EXEC" -noshell -elixir_root "$SCRIPT_PATH"/../lib -pa "$SCRIPT_PATH"/../lib/elixir/ebin $ELIXIR_ERL_OPTIONS $ERL "$@"
set -- "$ERTS_BIN$ERL_EXEC" -noshell -elixir_root "$SCRIPT_PATH"/../lib -pa "$SCRIPT_PATH"/../lib/elixir/ebin $ELIXIR_ERL_OPTIONS -s elixir start_$MODE $ERL "$@"
if [ -n "$RUN_ERL_PIPE" ]; then
ESCAPED=""
+77 -35
View File
@@ -1,7 +1,8 @@
@echo off
@if defined ELIXIR_CLI_ECHO (@echo on) else (@echo off)
set ELIXIR_VERSION=1.18.5
set ELIXIR_VERSION=1.15.8
setlocal enabledelayedexpansion
if ""%1""=="""" if ""%2""=="""" goto documentation
if /I ""%1""==""--help"" if ""%2""=="""" goto documentation
if /I ""%1""==""-h"" if ""%2""=="""" goto documentation
@@ -24,13 +25,13 @@ echo -pa "PATH" Prepends the given path to Erlang code path
echo -pz "PATH" Appends the given path to Erlang code path (*)
echo -v, --version Prints Erlang/OTP and Elixir versions (standalone)
echo.
echo --color, --no-color Enables or disables ANSI coloring
echo --erl "SWITCHES" Switches to be passed down to Erlang (*)
echo --eval "COMMAND" Evaluates the given command, same as -e (*)
echo --logger-otp-reports BOOL Enables or disables OTP reporting
echo --logger-sasl-reports BOOL Enables or disables SASL reporting
echo --no-halt Does not halt the Erlang VM after execution
echo --short-version Prints Elixir version (standalone)
echo --werl Uses Erlang's Windows shell GUI (Windows only)
echo.
echo Options given after the .exs file or -- are passed down to the executed code.
echo Options can be passed to the Erlang runtime using $ELIXIR_ERL_OPTIONS or --erl.
@@ -63,11 +64,13 @@ echo ** Standalone options can't be combined with other options.
goto end
:shortversion
echo %ELIXIR_VERSION%
echo !ELIXIR_VERSION!
goto end
:parseopts
setlocal enabledelayedexpansion
rem Parameters for Elixir
set parsElixir=
rem Parameters for Erlang
set parsErlang=
@@ -94,51 +97,90 @@ if "!par!"=="" (
)
shift
set par="!par:"=\"!"
if !endLoop! == 1 (
set parsElixir=!parsElixir! !par!
goto startloop
)
rem ******* EXECUTION OPTIONS **********************
if !par!=="+iex" (set useIEx=1 && goto startloop)
if !par!=="+elixirc" (goto startloop)
if !par!=="--werl" (set useWerl=1 && goto startloop)
if !par!=="+iex" (set parsElixir=!parsElixir! +iex && set useIEx=1 && goto startloop)
if !par!=="+elixirc" (set parsElixir=!parsElixir! +elixirc && goto startloop)
rem ******* EVAL PARAMETERS ************************
if ""==!par:-e=! (
set "VAR=%~1"
if not defined VAR (set VAR= )
set parsElixir=!parsElixir! -e "!VAR:"=\"!"
shift
goto startloop
)
if ""==!par:--eval=! (
set "VAR=%~1"
if not defined VAR (set VAR= )
set parsElixir=!parsElixir! --eval "!VAR:"=\"!"
shift
goto startloop
)
if ""==!par:--rpc-eval=! (
set "VAR=%~2"
if not defined VAR (set VAR= )
set parsElixir=!parsElixir! --rpc-eval %1 "!VAR:"=\"!"
shift
shift
goto startloop
)
rem ******* ELIXIR PARAMETERS **********************
if ""==!par:-e=! (shift && goto startloop)
if ""==!par:--eval=! (shift && goto startloop)
if ""==!par:--rpc-eval=! (shift && shift && goto startloop)
if ""==!par:-r=! (shift && goto startloop)
if ""==!par:-pr=! (shift && goto startloop)
if ""==!par:-pa=! (shift && goto startloop)
if ""==!par:-pz=! (shift && goto startloop)
if ""==!par:-v=! (goto startloop)
if ""==!par:--version=! (goto startloop)
if ""==!par:--no-halt=! (goto startloop)
if ""==!par:--color=! (goto startloop)
if ""==!par:--no-color=! (goto startloop)
if ""==!par:--remsh=! (shift && goto startloop)
if ""==!par:--dot-iex=! (shift && goto startloop)
if ""==!par:--dbg=! (shift && goto startloop)
if ""==!par:-r=! (set "parsElixir=!parsElixir! -r %~1" && shift && goto startloop)
if ""==!par:-pr=! (set "parsElixir=!parsElixir! -pr %~1" && shift && goto startloop)
if ""==!par:-pa=! (set "parsElixir=!parsElixir! -pa %~1" && shift && goto startloop)
if ""==!par:-pz=! (set "parsElixir=!parsElixir! -pz %~1" && shift && goto startloop)
if ""==!par:-v=! (set "parsElixir=!parsElixir! -v" && goto startloop)
if ""==!par:--version=! (set "parsElixir=!parsElixir! --version" && goto startloop)
if ""==!par:--no-halt=! (set "parsElixir=!parsElixir! --no-halt" && goto startloop)
if ""==!par:--remsh=! (set "parsElixir=!parsElixir! --remsh %~1" && shift && goto startloop)
if ""==!par:--dot-iex=! (set "parsElixir=!parsElixir! --dot-iex %~1" && shift && goto startloop)
if ""==!par:--dbg=! (set "parsElixir=!parsElixir! --dbg %~1" && shift && goto startloop)
rem ******* ERLANG PARAMETERS **********************
if ""==!par:--boot=! (set "parsErlang=!parsErlang! -boot "%~1"" && shift && goto startloop)
if ""==!par:--boot-var=! (set "parsErlang=!parsErlang! -boot_var "%~1" "%~2"" && shift && shift && goto startloop)
if ""==!par:--cookie=! (set "parsErlang=!parsErlang! -setcookie "%~1"" && shift && goto startloop)
if ""==!par:--boot=! (set "parsErlang=!parsErlang! -boot %~1" && shift && goto startloop)
if ""==!par:--boot-var=! (set "parsErlang=!parsErlang! -boot_var %~1 %~2" && shift && shift && goto startloop)
if ""==!par:--cookie=! (set "parsErlang=!parsErlang! -setcookie %~1" && shift && goto startloop)
if ""==!par:--hidden=! (set "parsErlang=!parsErlang! -hidden" && goto startloop)
if ""==!par:--erl-config=! (set "parsErlang=!parsErlang! -config "%~1"" && shift && goto startloop)
if ""==!par:--erl-config=! (set "parsErlang=!parsErlang! -config %~1" && shift && goto startloop)
if ""==!par:--logger-otp-reports=! (set "parsErlang=!parsErlang! -logger handle_otp_reports %1" && shift && goto startloop)
if ""==!par:--logger-sasl-reports=! (set "parsErlang=!parsErlang! -logger handle_sasl_reports %1" && shift && goto startloop)
if ""==!par:--name=! (set "parsErlang=!parsErlang! -name "%~1"" && shift && goto startloop)
if ""==!par:--sname=! (set "parsErlang=!parsErlang! -sname "%~1"" && shift && goto startloop)
if ""==!par:--vm-args=! (set "parsErlang=!parsErlang! -args_file "%~1"" && shift && goto startloop)
if ""==!par:--name=! (set "parsErlang=!parsErlang! -name %~1" && shift && goto startloop)
if ""==!par:--sname=! (set "parsErlang=!parsErlang! -sname %~1" && shift && goto startloop)
if ""==!par:--vm-args=! (set "parsErlang=!parsErlang! -args_file %~1" && shift && goto startloop)
if ""==!par:--erl=! (set "beforeExtra=!beforeExtra! %~1" && shift && goto startloop)
if ""==!par:--pipe-to=! (echo --pipe-to : Option is not supported on Windows && goto end)
set endLoop=1
set parsElixir=!parsElixir! !par!
goto startloop
:run
setlocal disabledelayedexpansion
if not defined useIEx (
set beforeExtra=-s elixir start_cli %beforeExtra%
reg query HKCU\Console /v VirtualTerminalLevel 2>nul | findstr /e "0x1" >nul 2>nul
if %errorlevel% == 0 (
set beforeExtra=-elixir ansi_enabled true !beforeExtra!
)
if defined useIEx (
set beforeExtra=-s elixir start_iex !beforeExtra!
) else (
set beforeExtra=-s elixir start_cli !beforeExtra!
)
set beforeExtra=-noshell -elixir_root "%SCRIPT_PATH%..\lib" -pa "%SCRIPT_PATH%..\lib\elixir\ebin" %beforeExtra%
set beforeExtra=-noshell -elixir_root "!SCRIPT_PATH!..\lib" -pa "!SCRIPT_PATH!..\lib\elixir\ebin" !beforeExtra!
if defined ELIXIR_CLI_DRY_RUN (
echo "%ERTS_BIN%erl.exe" %ELIXIR_ERL_OPTIONS% %parsErlang% %beforeExtra% -extra %*
if defined useWerl (
echo start "" "!ERTS_BIN!werl.exe" !ext_libs! !ELIXIR_ERL_OPTIONS! !parsErlang! !beforeExtra! -extra !parsElixir!
) else (
echo "!ERTS_BIN!erl.exe" !ext_libs! !ELIXIR_ERL_OPTIONS! !parsErlang! !beforeExtra! -extra !parsElixir!
)
) else (
"%ERTS_BIN%erl.exe" %ELIXIR_ERL_OPTIONS% %parsErlang% %beforeExtra% -extra %*
if defined useWerl (
start "" "!ERTS_BIN!werl.exe" !ext_libs! !ELIXIR_ERL_OPTIONS! !parsErlang! !beforeExtra! -extra !parsElixir!
) else (
"!ERTS_BIN!erl.exe" !ext_libs! !ELIXIR_ERL_OPTIONS! !parsErlang! !beforeExtra! -extra !parsElixir!
)
)
exit /B %ERRORLEVEL%
:end
+1 -1
View File
@@ -1,4 +1,4 @@
@echo off
@if defined ELIXIR_CLI_ECHO (@echo on) else (@echo off)
setlocal
set argc=0
for %%A in (%*) do (
+5 -2
View File
@@ -1,4 +1,4 @@
@echo off
@if defined ELIXIR_CLI_ECHO (@echo on) else (@echo off)
setlocal
if /I ""%1""==""--help"" goto documentation
if /I ""%1""==""-h"" goto documentation
@@ -16,11 +16,14 @@ echo --dot-iex "FILE" Evaluates FILE, line by line, to set up IEx' environm
echo Defaults to evaluating .iex.exs or ~/.iex.exs, if any exists.
echo If FILE is empty, then no file will be loaded.
echo --remsh NAME Connects to a node using a remote shell
echo --werl Uses Erlang's Windows shell GUI (Windows only)
echo.
echo Set the IEX_WITH_WERL environment variable to always use werl.
echo It accepts all other options listed by "elixir --help".
goto end
:run
call "%~dp0\elixir.bat" --no-halt --erl "-user elixir" +iex %*
if defined IEX_WITH_WERL (set __ELIXIR_IEX_FLAGS=--werl) else (set __ELIXIR_IEX_FLAGS=)
call "%~dp0\elixir.bat" --no-halt --erl "-user elixir" +iex %__ELIXIR_IEX_FLAGS% %*
:end
endlocal
+1 -1
View File
@@ -1,2 +1,2 @@
@echo off
@if defined ELIXIR_CLI_ECHO (@echo on) else (@echo off)
call "%~dp0\elixir.bat" "%~dp0\mix" %*
Executable → Regular
+1 -1
View File
@@ -20,4 +20,4 @@ for ($i = 0; $i -lt $args.length; $i++)
}
# Corrected arguments are ready to pass to batch file
& $mixBatPath $newArgs
& $mixBatPath $newArgs
+4 -7
View File
@@ -1,12 +1,9 @@
defmodule EEx.SyntaxError do
defexception [:file, :line, :column, :snippet, message: "syntax error"]
defexception [:message, :file, :line, :column]
@impl true
def message(exception) do
%{file: file, line: line, column: column, message: message, snippet: snippet} = exception
Exception.format_file_line_column(file && Path.relative_to_cwd(file), line, column, " ") <>
message <> (snippet || "")
"#{exception.file}:#{exception.line}:#{exception.column}: #{exception.message}"
end
end
@@ -317,8 +314,8 @@ defmodule EEx do
## Examples
iex> EEx.tokenize(~c"foo", line: 1, column: 1)
{:ok, [{:text, ~c"foo", %{column: 1, line: 1}}, {:eof, %{column: 4, line: 1}}]}
iex> EEx.tokenize('foo', line: 1, column: 1)
{:ok, [{:text, 'foo', %{column: 1, line: 1}}, {:eof, %{column: 4, line: 1}}]}
## Result
+15 -29
View File
@@ -48,14 +48,8 @@ defmodule EEx.Compiler do
end
end
# TODO: Remove me on Elixir v2.0
# TODO: Deprecate this on Elixir v1.18
defp tokenize(~c"<%#" ++ t, line, column, state, buffer, acc) do
IO.warn("<%# is deprecated, use <%!-- or add a space between <% and # instead",
line: line,
column: column,
file: state.file
)
case expr(t, line, column + 3, state, []) do
{:error, message} ->
{:error, message, %{line: line, column: column}}
@@ -77,10 +71,10 @@ defmodule EEx.Compiler do
{:ok, expr, new_line, new_column, rest} ->
{key, expr} =
case :elixir_tokenizer.tokenize(expr, 1, file: "eex", check_terminators: false) do
{:ok, _line, _column, _warnings, rev_tokens, []} ->
{:ok, _line, _column, _warnings, tokens} ->
# We ignore warnings because the code will be tokenized
# again later with the right line+column info
token_key(rev_tokens, expr)
token_key(tokens, expr)
{:error, _, _, _, _} ->
{:expr, expr}
@@ -170,8 +164,8 @@ defmodule EEx.Compiler do
end
# Receives tokens and check if it is a start, middle or an end token.
defp token_key(rev_tokens, expr) do
case {Enum.reverse(rev_tokens), drop_eol(rev_tokens)} do
defp token_key(tokens, expr) do
case {tokens, tokens |> Enum.reverse() |> drop_eol()} do
{[{:end, _} | _], [{:do, _} | _]} ->
{:middle_expr, expr}
@@ -305,14 +299,6 @@ defmodule EEx.Compiler do
}
init = state.engine.init(opts)
if function_exported?(state.engine, :handle_text, 2) and
not function_exported?(state.engine, :handle_text, 3) do
IO.warn(
"#{inspect(state.engine)}.handle_text/2 is deprecated, implement handle_text/3 instead"
)
end
generate_buffer(tokens, init, [], state)
end
@@ -330,7 +316,8 @@ defmodule EEx.Compiler do
meta = [line: meta.line, column: meta.column]
state.engine.handle_text(buffer, meta, IO.chardata_to_string(chars))
else
# TODO: Remove this on Elixir v2.0. The deprecation is on init.
# TODO: Deprecate this branch on Elixir v1.18.
# We should most likely move this check to init to emit the deprecation once.
state.engine.handle_text(buffer, IO.chardata_to_string(chars))
end
@@ -353,6 +340,13 @@ defmodule EEx.Compiler do
scope,
state
) do
if mark == ~c"" do
message =
"the contents of this expression won't be output unless the EEx block starts with \"<%=\""
:elixir_errors.erl_warn({meta.line, meta.column}, state.file, message)
end
{rest, line, contents} = look_ahead_middle(rest, meta.line, chars) || {rest, meta.line, chars}
start_line = meta.line
start_column = column(meta.column, mark)
@@ -365,13 +359,6 @@ defmodule EEx.Compiler do
%{state | quoted: [], line: line}
)
if mark == ~c"" and not match?({:=, _, [_, _]}, contents) do
message =
"the contents of this expression won't be output unless the EEx block starts with \"<%=\""
:elixir_errors.erl_warn({meta.line, meta.column}, state.file, message)
end
buffer = state.engine.handle_expr(buffer, IO.chardata_to_string(mark), contents)
generate_buffer(rest, buffer, scope, state)
end
@@ -502,8 +489,7 @@ defmodule EEx.Compiler do
defp syntax_error!(message, meta, state) do
raise EEx.SyntaxError,
message: message,
snippet: code_snippet(state.source, state.indentation, meta),
message: message <> code_snippet(state.source, state.indentation, meta),
file: state.file,
line: meta.line,
column: meta.column
+9
View File
@@ -43,6 +43,15 @@ defmodule EEx.SmartEngineTest do
assert_received :found
end
test "error with unused \"do\" block without \"<%=\" modifier" do
stderr =
ExUnit.CaptureIO.capture_io(:stderr, fn ->
assert_eval("", "<% if true do %>I'm invisible!<% end %>", assigns: %{})
end)
assert stderr =~ "the contents of this expression won't be output"
end
defp assert_eval(expected, actual, binding \\ []) do
result = EEx.eval_string(actual, binding, file: __ENV__.file, engine: EEx.SmartEngine)
assert result == expected
+54 -12
View File
@@ -126,21 +126,51 @@ defmodule EEx.TokenizerTest do
end
test "EEx comments" do
ExUnit.CaptureIO.capture_io(:stderr, fn ->
exprs = [
{:text, ~c"foo ", %{column: 1, line: 1}},
{:eof, %{column: 16, line: 1}}
]
exprs = [
{:text, ~c"foo ", %{column: 1, line: 1}},
{:eof, %{column: 16, line: 1}}
]
assert EEx.tokenize(~c"foo <%# true %>", @opts) == {:ok, exprs}
assert EEx.tokenize(~c"foo <%# true %>", @opts) == {:ok, exprs}
exprs = [
{:text, ~c"foo ", %{column: 1, line: 1}},
{:eof, %{column: 8, line: 2}}
]
exprs = [
{:text, ~c"foo ", %{column: 1, line: 1}},
{:eof, %{column: 8, line: 2}}
]
assert EEx.tokenize(~c"foo <%#\ntrue %>", @opts) == {:ok, exprs}
end)
assert EEx.tokenize(~c"foo <%#\ntrue %>", @opts) == {:ok, exprs}
end
test "EEx comments with do-end" do
exprs = [
{:text, ~c"foo ", %{column: 1, line: 1}},
{:text, ~c"bar", %{column: 19, line: 1}},
{:eof, %{column: 32, line: 1}}
]
assert EEx.tokenize(~c"foo <%# true do %>bar<%# end %>", @opts) == {:ok, exprs}
end
test "EEx comments inside do-end" do
exprs = [
{:start_expr, ~c"", ~c" if true do ", %{column: 1, line: 1}},
{:text, ~c"bar", %{column: 31, line: 1}},
{:end_expr, [], ~c" end ", %{column: 34, line: 1}},
{:eof, %{column: 43, line: 1}}
]
assert EEx.tokenize(~c"<% if true do %><%# comment %>bar<% end %>", @opts) == {:ok, exprs}
exprs = [
{:start_expr, [], ~c" case true do ", %{column: 1, line: 1}},
{:middle_expr, ~c"", ~c" true -> ", %{column: 33, line: 1}},
{:text, ~c"bar", %{column: 46, line: 1}},
{:end_expr, [], ~c" end ", %{column: 49, line: 1}},
{:eof, %{column: 58, line: 1}}
]
assert EEx.tokenize(~c"<% case true do %><%# comment %><% true -> %>bar<% end %>", @opts) ==
{:ok, exprs}
end
test "EEx multi-line comments" do
@@ -291,6 +321,15 @@ defmodule EEx.TokenizerTest do
assert EEx.tokenize(template, [trim: true] ++ @opts) == {:ok, exprs}
end
test "trim mode with comment" do
exprs = [
{:text, ~c"\n123", %{column: 19, line: 1}},
{:eof, %{column: 4, line: 2}}
]
assert EEx.tokenize(~c" <%# comment %> \n123", [trim: true] ++ @opts) == {:ok, exprs}
end
test "trim mode with multi-line comment" do
exprs = [
{:comment, ~c" comment ", %{column: 3, line: 1}},
@@ -345,6 +384,9 @@ defmodule EEx.TokenizerTest do
assert EEx.tokenize(~c"foo <% :bar", @opts) ==
{:error, message, %{column: 5, line: 1}}
assert EEx.tokenize(~c"<%# true ", @opts) ==
{:error, "expected closing '%>' for EEx expression", %{column: 1, line: 1}}
message = """
expected closing '--%>' for EEx expression
|
+38 -58
View File
@@ -198,6 +198,15 @@ defmodule EExTest do
assert_eval("foo baz", "foo <%= if false do %>bar<% else %>baz<% end %>")
end
test "embedded code with comments in do end" do
assert_eval("foo bar", "foo <%= case true do %><%# comment %><% true -> %>bar<% end %>")
assert_eval(
"foo\n\nbar\n",
"foo\n<%= case true do %>\n<%# comment %>\n<% true -> %>\nbar\n<% end %>"
)
end
test "embedded code with multi-line comments in do end" do
assert_eval("foo bar", "foo <%= case true do %><%!-- comment --%><% true -> %>bar<% end %>")
@@ -264,32 +273,6 @@ defmodule EExTest do
end
describe "raises syntax errors" do
test "with relative file information" do
message = """
foobar.eex:1:5: expected closing '%>' for EEx expression
|
1 | foo <%= bar
| ^\
"""
assert_raise EEx.SyntaxError, message, fn ->
EEx.compile_string("foo <%= bar", file: Path.join(File.cwd!(), "foobar.eex"))
end
end
test "when <%!-- is not closed" do
message = """
my_file.eex:1:5: expected closing '--%>' for EEx expression
|
1 | foo <%!-- bar
| ^\
"""
assert_raise EEx.SyntaxError, message, fn ->
EEx.compile_string("foo <%!-- bar", file: "my_file.eex")
end
end
test "when the token is invalid" do
message = """
nofile:1:5: expected closing '%>' for EEx expression
@@ -534,16 +517,6 @@ defmodule EExTest do
~s[unexpected beginning of EEx tag \"<%=\" on \"<%= end %>\"]
end
test "unused \"do\" block without \"<%=\" modifier" do
assert ExUnit.CaptureIO.capture_io(:stderr, fn ->
EEx.compile_string("<% if true do %>I'm invisible!<% end %>")
end) =~ "the contents of this expression won't be output"
# These are fine though
EEx.compile_string("<% foo = fn -> %>Hello<% end %>")
EEx.compile_string("<% foo = if true do %>Hello<% end %>")
end
test "from tokenizer" do
warning =
ExUnit.CaptureIO.capture_io(:stderr, fn ->
@@ -762,31 +735,38 @@ defmodule EExTest do
end
test "line and column meta" do
indentation = 12
parser_options = Code.get_compiler_option(:parser_options)
Code.put_compiler_option(:parser_options, columns: true)
ast =
EEx.compile_string(
"""
<%= f() %> <% f() %>
<%= f fn -> %>
<%= f() %>
<% end %>
""",
indentation: indentation
)
try do
indentation = 12
{_, calls} =
Macro.prewalk(ast, [], fn
{:f, meta, _args} = expr, acc -> {expr, [meta | acc]}
other, acc -> {other, acc}
end)
ast =
EEx.compile_string(
"""
<%= f() %> <% f() %>
<%= f fn -> %>
<%= f() %>
<% end %>
""",
indentation: indentation
)
assert Enum.reverse(calls) == [
[line: 1, column: indentation + 5],
[line: 1, column: indentation + 15],
[line: 2, column: indentation + 7],
[line: 3, column: indentation + 9]
]
{_, calls} =
Macro.prewalk(ast, [], fn
{:f, meta, _args} = expr, acc -> {expr, [meta | acc]}
other, acc -> {other, acc}
end)
assert Enum.reverse(calls) == [
[line: 1, column: indentation + 5],
[line: 1, column: indentation + 15],
[line: 2, column: indentation + 7],
[line: 3, column: indentation + 9]
]
after
Code.put_compiler_option(:parser_options, parser_options)
end
end
end
+2 -2
View File
@@ -9,8 +9,8 @@
warn_deprecated_function,
warn_obsolete_guard,
warn_exported_vars,
%% Enable this when we require Erlang/OTP 27+
%% warnings_as_errors,
%% warn_missing_spec,
%% warn_untyped_record,
debug_info,
{outdir, "ebin/"}
]}.
+37 -126
View File
@@ -35,74 +35,61 @@ defmodule Access do
iex> nil[:a]
nil
The access syntax can also be used with the `Kernel.put_in/2`,
`Kernel.update_in/2` and `Kernel.get_and_update_in/2` macros
to allow values to be set in nested data structures:
iex> users = %{"john" => %{age: 27}, "meg" => %{age: 23}}
iex> put_in(users["john"][:age], 28)
%{"john" => %{age: 28}, "meg" => %{age: 23}}
## Maps and structs
While the access syntax is allowed in maps via `map[key]`,
if your map is made of predefined atom keys, you should prefer
to access those atom keys with `map.key` instead of `map[key]`,
as `map.key` will raise if the key is missing (which is not
supposed to happen if the keys are predefined) or if `map` is
`nil`.
supposed to happen if the keys are predefined).
Similarly, since structs are maps and structs have predefined
keys, they only allow the `struct.key` syntax and they do not
allow the `struct[key]` access syntax.
allow the `struct[key]` access syntax. `Access.key/1` can also
be used to construct dynamic access to structs and maps.
In other words, the `map[key]` syntax is loose, returning `nil`
for missing keys, while the `map.key` syntax is strict, raising
for both nil values and missing keys.
In a nutshell, when using `put_in/2` and friends:
To bridge this gap, Elixir provides the `get_in/1` and `get_in/2`
functions, which are capable of traversing nested data structures,
even in the presence of `nil`s:
put_in(struct_or_map.key, :value)
put_in(keyword_or_map[:key], :value)
iex> users = %{"john" => %{age: 27}, "meg" => %{age: 23}}
iex> get_in(users["john"].age)
27
iex> get_in(users["unknown"].age)
nil
When using `put_in/3` and friends:
Notice how, even if no user was found, `get_in/1` returned `nil`.
Outside of `get_in/1`, trying to access the field `.age` on `nil`
would raise.
put_in(struct_or_map, [Access.key!(:key)], :value)
put_in(keyword_or_map, [:key], :value)
The `get_in/2` function takes one step further by allowing
different accessors to be mixed in. For example, given a user
map with the `:name` and `:languages` keys, here is how to
access the name of all programming languages:
This covers the dual nature of maps in Elixir, as they can be
either for structured data or as a key-value store. See the `Map`
module for more information.
iex> languages = [
...> %{name: "elixir", type: :functional},
...> %{name: "c", type: :procedural}
...> ]
iex> user = %{name: "john", languages: languages}
iex> get_in(user, [:languages, Access.all(), :name])
["elixir", "c"]
## Nested data structures
This module provides convenience functions for traversing other
structures, like tuples and lists. As we will see next, they can
even be used to update nested data structures.
Both key-based access syntaxes can be used with the nested update
functions and macros in `Kernel`, such as `Kernel.get_in/2`,
`Kernel.put_in/3`, `Kernel.update_in/3`, `Kernel.pop_in/2`, and
`Kernel.get_and_update_in/3`.
If you want to learn more about the dual nature of maps in Elixir,
as they can be either for structured data or as a key-value store,
see the `Map` module.
## Updating nested data structures
The access syntax can also be used with the `Kernel.put_in/2`,
`Kernel.update_in/2`, `Kernel.get_and_update_in/2`, and `Kernel.pop_in/1`
macros to further manipulate values in nested data structures:
For example, to update a map inside another map:
iex> users = %{"john" => %{age: 27}, "meg" => %{age: 23}}
iex> put_in(users["john"].age, 28)
%{"john" => %{age: 28}, "meg" => %{age: 23}}
As shown in the previous section, you can also use the
`Kernel.put_in/3`, `Kernel.update_in/3`, `Kernel.pop_in/2`, and
`Kernel.get_and_update_in/3` functions to provide nested
custom accessors. For instance, given a user map with the
`:name` and `:languages` keys, here is how to deeply traverse
the map and convert all language names to uppercase:
This module provides convenience functions for traversing other
structures, like tuples and lists. These functions can be used
in all the `Access`-related functions and macros in `Kernel`.
For instance, given a user map with the `:name` and `:languages`
keys, here is how to deeply traverse the map and convert all
language names to uppercase:
iex> languages = [
...> %{name: "elixir", type: :functional},
@@ -350,7 +337,7 @@ defmodule Access do
Accessing a list by index is typically discouraged in Elixir, \
instead we prefer to use the Enum module to manipulate lists \
as a whole. If you really must access a list element by index, \
you can use Enum.at/2 or the functions in the List module\
you can Enum.at/1 or the functions in the List module\
"""
end
@@ -418,8 +405,8 @@ defmodule Access do
Accessing a list by index is typically discouraged in Elixir, \
instead we prefer to use the Enum module to manipulate lists \
as a whole. If you really must modify a list element by index, \
you can use Access.at/1 or the functions in the List module\
as a whole. If you really must mostify a list element by index, \
you can Access.at/1 or the functions in the List module\
"""
end
@@ -565,7 +552,7 @@ defmodule Access do
{"john", %{user: %{name: "JOHN"}}}
However, it is not possible to remove fields using the dot notation,
as it is implied those fields must also be present. In any case,
as it is implified those fields must also be present. In any case,
`Access.key!/1` is useful when the key is not known in advance
and must be accessed dynamically.
@@ -1026,80 +1013,4 @@ defmodule Access do
defp get_and_update_slice([], _range, _next, updates, gets, _index) do
{:lists.reverse(gets), :lists.reverse(updates)}
end
@doc ~S"""
Returns a function that accesses the first element of a list that matches the provided predicate.
The returned function is typically passed as an accessor to `Kernel.get_in/2`,
`Kernel.get_and_update_in/3`, and friends.
## Examples
iex> list = [%{name: "john", salary: 10}, %{name: "francine", salary: 30}]
iex> get_in(list, [Access.find(&(&1.salary > 20)), :name])
"francine"
iex> get_and_update_in(list, [Access.find(&(&1.salary <= 40)), :name], fn prev ->
...> {prev, String.upcase(prev)}
...> end)
{"john", [%{name: "JOHN", salary: 10}, %{name: "francine", salary: 30}]}
`find/1` can also be used to pop the first found element out of a list or
a key inside of a list:
iex> list = [%{name: "john", salary: 10}, %{name: "francine", salary: 30}]
iex> pop_in(list, [Access.find(&(&1.salary <= 40))])
{%{name: "john", salary: 10}, [%{name: "francine", salary: 30}]}
When no match is found, nil is returned and the update function is never called
iex> list = [%{name: "john", salary: 10}, %{name: "francine", salary: 30}]
iex> get_in(list, [Access.find(&(&1.salary >= 50)), :name])
nil
iex> get_and_update_in(list, [Access.find(&(&1.salary >= 50)), :name], fn prev ->
...> {prev, String.upcase(prev)}
...> end)
{nil, [%{name: "john", salary: 10}, %{name: "francine", salary: 30}]}
An error is raised if the predicate is not a function or is of the incorrect arity:
iex> get_in([], [Access.find(5)])
** (FunctionClauseError) no function clause matching in Access.find/1
An error is raised if the accessed structure is not a list:
iex> get_in(%{}, [Access.find(fn a -> a == 10 end)])
** (RuntimeError) Access.find/1 expected a list, got: %{}
"""
@doc since: "1.17.0"
@spec find((term -> as_boolean(term))) :: access_fun(data :: list, current_value :: term)
def find(predicate) when is_function(predicate, 1) do
fn op, data, next -> find(op, data, predicate, next) end
end
defp find(:get, data, predicate, next) when is_list(data) do
data |> Enum.find(predicate) |> next.()
end
defp find(:get_and_update, data, predicate, next) when is_list(data) do
get_and_update_find(data, [], predicate, next)
end
defp find(_op, data, _predicate, _next) do
raise "Access.find/1 expected a list, got: #{inspect(data)}"
end
defp get_and_update_find([], updates, _predicate, _next) do
{nil, :lists.reverse(updates)}
end
defp get_and_update_find([head | rest], updates, predicate, next) do
if predicate.(head) do
case next.(head) do
{get, update} -> {get, :lists.reverse([update | updates], rest)}
:pop -> {head, :lists.reverse(updates, rest)}
end
else
get_and_update_find(rest, [head | updates], predicate, next)
end
end
end
+5 -5
View File
@@ -207,7 +207,7 @@ defmodule Agent do
@doc false
defmacro __using__(opts) do
quote location: :keep, bind_quoted: [opts: opts] do
if not Module.has_attribute?(__MODULE__, :doc) do
unless Module.has_attribute?(__MODULE__, :doc) do
@doc """
Returns a specification to start this module under a supervisor.
@@ -284,7 +284,7 @@ defmodule Agent do
instead of an anonymous function; `fun` in `module` will be called with the
given arguments `args` to initialize the state.
"""
@spec start_link(module, atom, [term], GenServer.options()) :: on_start
@spec start_link(module, atom, [any], GenServer.options()) :: on_start
def start_link(module, fun, args, options \\ []) do
GenServer.start_link(Agent.Server, {module, fun, args}, options)
end
@@ -311,7 +311,7 @@ defmodule Agent do
See `start_link/4` for more information.
"""
@spec start(module, atom, [term], GenServer.options()) :: on_start
@spec start(module, atom, [any], GenServer.options()) :: on_start
def start(module, fun, args, options \\ []) do
GenServer.start(Agent.Server, {module, fun, args}, options)
end
@@ -348,7 +348,7 @@ defmodule Agent do
instead of an anonymous function. The state is added as first
argument to the given list of arguments.
"""
@spec get(agent, module, atom, [term], timeout) :: term
@spec get(agent, module, atom, [term], timeout) :: any
def get(agent, module, fun, args, timeout \\ 5000) do
GenServer.call(agent, {:get, {module, fun, args}}, timeout)
end
@@ -389,7 +389,7 @@ defmodule Agent do
instead of an anonymous function. The state is added as first
argument to the given list of arguments.
"""
@spec get_and_update(agent, module, atom, [term], timeout) :: term
@spec get_and_update(agent, module, atom, [term], timeout) :: any
def get_and_update(agent, module, fun, args, timeout \\ 5000) do
GenServer.call(agent, {:get_and_update, {module, fun, args}}, timeout)
end
+24 -43
View File
@@ -13,7 +13,7 @@ defmodule Application do
Developers typically interact with the application environment and its
callback module. Therefore those will be the topics we will cover first
before jumping into details about the application resource file and life cycle.
before jumping into details about the application resource file and life-cycle.
## The application environment
@@ -54,12 +54,12 @@ defmodule Application do
You can also change the application environment dynamically by using functions
such as `put_env/3` and `delete_env/2`.
> #### Application environment in libraries {: .info}
> #### Environment in libraries {: .tip}
>
> If you are writing a library to be used by other developers,
> it is generally recommended to avoid the application environment, as the
> application environment is effectively a global storage. For more information,
> read about this [anti-pattern](design-anti-patterns.md#using-application-configuration-for-libraries).
> The config files `config/config.exs` and `config/runtime.exs`
> are rarely used by libraries. Libraries typically define their environment
> in the `application/0` function of their `mix.exs`. Configuration files
> are rather used by applications to configure their libraries.
> #### Reading the environment of other applications {: .warning}
>
@@ -200,7 +200,7 @@ defmodule Application do
In the sections above, we have configured an application in the
`application/0` section of the `mix.exs` file. Ultimately, Mix will use
this configuration to create an [*application resource
file*](https://www.erlang.org/doc/man/app), which is a file called
file*](https://www.erlang.org/doc/man/application.html), which is a file called
`APP_NAME.app`. For example, the application resource file of the OTP
application `ex_unit` is called `ex_unit.app`.
@@ -258,8 +258,8 @@ defmodule Application do
Application.stop(:ex_unit)
#=> :ok
Stopping an application without a callback module defined, is in practice a
no-op, except for some system tracing.
Stopping an application without a callback module is defined, but except for
some system tracing, it is in practice a no-op.
Stopping an application with a callback module has three steps:
@@ -277,7 +277,7 @@ defmodule Application do
invoked only after termination of the whole supervision tree.
Shutting down a live system cleanly can be done by calling `System.stop/1`. It
will shut down every application in the reverse order they were started.
will shut down every application in the opposite order they had been started.
By default, a SIGTERM from the operating system will automatically translate to
`System.stop/0`. You can also have more explicit control over operating system
@@ -467,9 +467,6 @@ defmodule Application do
* #{Enum.map_join(@application_keys, "\n * ", &"`#{inspect(&1)}`")}
For a description of all fields, see [Erlang's application
specification](https://www.erlang.org/doc/man/app).
Note the environment is not returned as it can be accessed via
`fetch_env/2`. Returns `nil` if the application is not loaded.
"""
@@ -673,6 +670,13 @@ defmodule Application do
> You must use this function to read only your own application
> environment. Do not read the environment of other applications.
> #### Application environment in libraries {: .info}
>
> If you are writing a library to be used by other developers,
> it is generally recommended to avoid the application environment, as the
> application environment is effectively a global storage. For more information,
> read our [library guidelines](library-guidelines.md).
## Examples
`get_env/3` is commonly used to read the configuration of your OTP applications.
@@ -789,12 +793,6 @@ defmodule Application do
@doc """
Puts the `value` in `key` for the given `app`.
> #### Compile environment {: .warning}
>
> Do not use this function to change environment variables read
> via `Application.compile_env/2`. The compile environment must
> be exclusively set before compilation, in your config files.
## Options
* `:timeout` - the timeout for the change (defaults to `5_000` milliseconds)
@@ -816,29 +814,16 @@ defmodule Application do
end
@doc """
Puts the environment for multiple applications at the same time.
Puts the environment for multiple apps at the same time.
The given config should not:
* have the same application listed more than once
* have the same key inside the same application listed more than once
If those conditions are not met, this function will raise.
This function receives the same options as `put_env/4`. Returns `:ok`.
## Examples
Application.put_all_env(
my_app: [
key: :value,
another_key: :another_value
],
another_app: [
key: :value
]
)
If those conditions are not met, it will raise.
It receives the same options as `put_env/4`. Returns `:ok`.
"""
@doc since: "1.9.0"
@spec put_all_env([{app, [{key, value}]}], timeout: timeout, persistent: boolean) :: :ok
@@ -862,10 +847,7 @@ defmodule Application do
# TODO: Remove this deprecation warning on 2.0+ and allow list lookups as in compile_env.
defp maybe_warn_on_app_env_key(app, key) do
message = fn ->
"passing non-atom as application env key is deprecated, got: #{inspect(key)}"
end
message = "passing non-atom as application env key is deprecated, got: #{inspect(key)}"
IO.warn_once({Application, :key, app, key}, message, _stacktrace_drop_levels = 2)
end
@@ -904,12 +886,11 @@ defmodule Application do
## Options
* `:type` - if the application should be started `:temporary` (default),
`:permanent`, or `:transient`. See `t:restart_type/1` for more information.
* `:type` - if the application should be started in `:permanent`,
`:temporary`, or `:transient`. See `t:restart_type/1` for more information.
* `:mode` - (since v1.15.0) if the applications should be started serially
(`:serial`, default) or concurrently (`:concurrent`). This option requires
Erlang/OTP 26+.
or concurrently. This option requires Erlang/OTP 26+.
"""
@spec ensure_all_started(app | [app], type: restart_type(), mode: :serial | :concurrent) ::
+1 -1
View File
@@ -67,7 +67,7 @@ defmodule Atom do
## Examples
iex> Atom.to_charlist(:"An atom")
~c"An atom"
'An atom'
"""
@spec to_charlist(atom) :: charlist
+1 -2
View File
@@ -9,8 +9,7 @@ defmodule Behaviour do
attributes.
Instead of `MyModule.__behaviour__(:callbacks)`,
`MyModule.behaviour_info(:callbacks)` can be used. `behaviour_info/1`
is documented in `Module`.
`MyModule.behaviour_info(:callbacks)` can be used.
"""
@moduledoc deprecated: "Use @callback and @macrocallback attributes instead"
+1 -1
View File
@@ -2,7 +2,7 @@ defmodule Bitwise do
@moduledoc """
A set of functions that perform calculations on bits.
All bitwise functions work only on integers, otherwise an
All bitwise functions work only on integers; otherwise an
`ArithmeticError` is raised. The functions `band/2`,
`bor/2`, `bsl/2`, and `bsr/2` also have operators,
respectively: `&&&/2`, `|||/2`, `<<</2`, and `>>>/2`.
+3 -31
View File
@@ -70,14 +70,14 @@ defmodule Calendar do
@type zone_abbr :: String.t()
@typedoc """
The time zone UTC offset in ISO seconds for standard time.
The time zone UTC offset in seconds for standard time.
See also `t:std_offset/0`.
"""
@type utc_offset :: integer
@typedoc """
The time zone standard offset in ISO seconds (typically not zero in summer times).
The time zone standard offset in seconds (typically not zero in summer times).
It must be added to `t:utc_offset/0` to get the total offset from UTC used for "wall time".
"""
@@ -338,34 +338,6 @@ defmodule Calendar do
@doc since: "1.15.0"
@callback iso_days_to_end_of_day(iso_days) :: iso_days
@doc """
Shifts date by given duration according to its calendar.
"""
@doc since: "1.17.0"
@callback shift_date(year, month, day, Duration.t()) :: {year, month, day}
@doc """
Shifts naive datetime by given duration according to its calendar.
"""
@doc since: "1.17.0"
@callback shift_naive_datetime(
year,
month,
day,
hour,
minute,
second,
microsecond,
Duration.t()
) :: {year, month, day, hour, minute, second, microsecond}
@doc """
Shifts time by given duration according to its calendar.
"""
@doc since: "1.17.0"
@callback shift_time(hour, minute, second, microsecond, Duration.t()) ::
{hour, minute, second, microsecond}
# General Helpers
@doc """
@@ -733,7 +705,7 @@ defmodule Calendar do
# Hour using a 12-hour clock
defp format_modifiers("I" <> rest, width, pad, datetime, format_options, acc) do
result = (rem(datetime.hour + 23, 12) + 1) |> Integer.to_string() |> pad_leading(width, pad)
result = (rem(datetime.hour() + 23, 12) + 1) |> Integer.to_string() |> pad_leading(width, pad)
parse(rest, datetime, format_options, [result | acc])
end
+23 -110
View File
@@ -40,7 +40,7 @@ defmodule Date do
## Using epochs
The `add/2`, `diff/2` and `shift/2` functions can be used for computing dates
The `add/2` and `diff/2` functions can be used for computing dates
or retrieving the number of days between instants. For example, if there
is an interest in computing the number of days from the Unix epoch
(1970-01-01):
@@ -51,9 +51,6 @@ defmodule Date do
iex> Date.add(~D[1970-01-01], 14716)
~D[2010-04-17]
iex> Date.shift(~D[1970-01-01], year: 40, month: 3, week: 2, day: 2)
~D[2010-04-17]
Those functions are optimized to deal with common epochs, such
as the Unix Epoch above or the Gregorian Epoch (0000-01-01).
"""
@@ -74,9 +71,8 @@ defmodule Date do
A range of dates represents a discrete number of dates where
the first and last values are dates with matching calendars.
Ranges of dates can be increasing (`first <= last`) and are
always inclusive. For a decreasing range, use `range/3` with
a step of -1 as first argument.
Ranges of dates can be either increasing (`first <= last`) or
decreasing (`first > last`). They are also always inclusive.
## Examples
@@ -96,6 +92,8 @@ defmodule Date do
true
iex> Enum.take(range, 3)
[~D[2001-01-01], ~D[2001-01-02], ~D[2001-01-03]]
iex> for d <- Date.range(~D[2023-03-01], ~D[2023-04-01]), Date.day_of_week(d) == 7, do: d
[~D[2023-03-05], ~D[2023-03-12], ~D[2023-03-19], ~D[2023-03-26]]
"""
@doc since: "1.5.0"
@@ -103,18 +101,8 @@ defmodule Date do
def range(%{calendar: calendar} = first, %{calendar: calendar} = last) do
{first_days, _} = to_iso_days(first)
{last_days, _} = to_iso_days(last)
step =
if first_days <= last_days do
1
else
IO.warn(
"a negative range was inferred for Date.range/2, call Date.range/3 instead with -1 as third argument"
)
-1
end
# TODO: Deprecate inferring a range with a step of -1 on Elixir v1.16
step = if first_days <= last_days, do: 1, else: -1
range(first, first_days, last, last_days, calendar, step)
end
@@ -581,7 +569,7 @@ defmodule Date do
end
@doc """
Returns `true` if the first date is strictly earlier than the second.
Returns true if the first date is strictly earlier than the second.
## Examples
@@ -600,7 +588,7 @@ defmodule Date do
end
@doc """
Returns `true` if the first date is strictly later than the second.
Returns true if the first date is strictly later than the second.
## Examples
@@ -690,8 +678,6 @@ defmodule Date do
The days are counted as Gregorian days. The date is returned in the same
calendar as it was given in.
To shift a date by a `Duration` and according to its underlying calendar, use `Date.shift/2`.
## Examples
iex> Date.add(~D[2000-01-03], -2)
@@ -708,7 +694,12 @@ defmodule Date do
@spec add(Calendar.date(), integer()) :: t
def add(%{calendar: Calendar.ISO} = date, days) do
%{year: year, month: month, day: day} = date
{year, month, day} = Calendar.ISO.shift_days({year, month, day}, days)
{year, month, day} =
Calendar.ISO.date_to_iso_days(year, month, day)
|> Kernel.+(days)
|> Calendar.ISO.date_from_iso_days()
%Date{calendar: Calendar.ISO, year: year, month: month, day: day}
end
@@ -757,81 +748,6 @@ defmodule Date do
end
end
@doc """
Shifts given `date` by `duration` according to its calendar.
Allowed units are: `:year`, `:month`, `:week`, `:day`.
When using the default ISO calendar, durations are collapsed and
applied in the order of months and then days:
* when shifting by 1 year and 2 months the date is actually shifted by 14 months
* when shifting by 2 weeks and 3 days the date is shifted by 17 days
When shifting by month, days are rounded down to the nearest valid date.
Raises an `ArgumentError` when called with time scale units.
## Examples
iex> Date.shift(~D[2016-01-03], month: 2)
~D[2016-03-03]
iex> Date.shift(~D[2016-01-30], month: -1)
~D[2015-12-30]
iex> Date.shift(~D[2016-01-31], year: 4, day: 1)
~D[2020-02-01]
iex> Date.shift(~D[2016-01-03], Duration.new!(month: 2))
~D[2016-03-03]
# leap years
iex> Date.shift(~D[2024-02-29], year: 1)
~D[2025-02-28]
iex> Date.shift(~D[2024-02-29], year: 4)
~D[2028-02-29]
# rounding down
iex> Date.shift(~D[2015-01-31], month: 1)
~D[2015-02-28]
"""
@doc since: "1.17.0"
@spec shift(Calendar.date(), Duration.t() | [unit_pair]) :: t
when unit_pair: {:year, integer} | {:month, integer} | {:week, integer} | {:day, integer}
def shift(%{calendar: calendar} = date, duration) do
%{year: year, month: month, day: day} = date
{year, month, day} = calendar.shift_date(year, month, day, __duration__!(duration))
%Date{calendar: calendar, year: year, month: month, day: day}
end
@doc false
def __duration__!(%Duration{} = duration) do
duration
end
# This part is inlined by the compiler on constant values
def __duration__!(unit_pairs) do
Enum.each(unit_pairs, &validate_duration_unit!/1)
struct!(Duration, unit_pairs)
end
defp validate_duration_unit!({unit, _value})
when unit in [:hour, :minute, :second, :microsecond] do
raise ArgumentError, "unsupported unit #{inspect(unit)}. Expected :year, :month, :week, :day"
end
defp validate_duration_unit!({unit, _value}) when unit not in [:year, :month, :week, :day] do
raise ArgumentError, "unknown unit #{inspect(unit)}. Expected :year, :month, :week, :day"
end
defp validate_duration_unit!({_unit, value}) when is_integer(value) do
:ok
end
defp validate_duration_unit!({unit, value}) do
raise ArgumentError,
"unsupported value #{inspect(value)} for #{inspect(unit)}. Expected an integer"
end
@doc false
def to_iso_days(%{calendar: Calendar.ISO, year: year, month: month, day: day}) do
{Calendar.ISO.date_to_iso_days(year, month, day), {0, 86_400_000_000}}
@@ -1073,7 +989,13 @@ defmodule Date do
def year_of_era(date)
def year_of_era(%{calendar: calendar, year: year, month: month, day: day}) do
calendar.year_of_era(year, month, day)
# TODO: Remove me on 1.17
# The behaviour implementation already warns on missing callback.
if function_exported?(calendar, :year_of_era, 3) do
calendar.year_of_era(year, month, day)
else
calendar.year_of_era(year)
end
end
@doc """
@@ -1152,17 +1074,8 @@ defmodule Date do
end
defimpl Inspect do
def inspect(%{calendar: calendar, year: year, month: month, day: day}, _)
when calendar != Calendar.ISO or year in -9999..9999 do
"~D[" <> calendar.date_to_string(year, month, day) <> suffix(calendar) <> "]"
end
def inspect(%{calendar: Calendar.ISO, year: year, month: month, day: day}, _) do
"Date.new!(#{Integer.to_string(year)}, #{Integer.to_string(month)}, #{Integer.to_string(day)})"
end
def inspect(%{calendar: calendar, year: year, month: month, day: day}, _) do
"Date.new!(#{Integer.to_string(year)}, #{Integer.to_string(month)}, #{Integer.to_string(day)}, #{inspect(calendar)})"
"~D[" <> calendar.date_to_string(year, month, day) <> suffix(calendar) <> "]"
end
defp suffix(Calendar.ISO), do: ""
+32 -225
View File
@@ -28,12 +28,7 @@ defmodule DateTime do
## Time zone database
Many functions in this module require a time zone database.
A time zone database is a record of the UTC offsets that its locales have
used at various times in the past, are using, and are expected to use in the
future.
Because those plans can change, it needs to be periodically updated.
By default, `DateTime` uses the default time zone database returned by
By default, it uses the default time zone database returned by
`Calendar.get_time_zone_database/0`, which defaults to
`Calendar.UTCOnlyTimeZoneDatabase` which only handles "Etc/UTC"
datetimes and returns `{:error, :utc_only_time_zone_database}`
@@ -48,7 +43,7 @@ defmodule DateTime do
* [`zoneinfo`](https://github.com/smartrent/zoneinfo) -
recommended for embedded devices
To use one of them, first make sure it is added as a dependency in `mix.exs`.
To use them, first make sure it is added as a dependency in `mix.exs`.
It can then be configured either via configuration:
config :elixir, :time_zone_database, Tz.TimeZoneDatabase
@@ -65,7 +60,7 @@ defmodule DateTime do
a date and time at a given time zone". To understand precisely
what we mean, let's see an example.
Imagine someone in Poland who wants to schedule a meeting with someone
Imagine someone in Poland wants to schedule a meeting with someone
in Brazil in the next year. The meeting will happen at 2:30 AM
in the Polish time zone. At what time will the meeting happen in
Brazil?
@@ -77,7 +72,7 @@ defmodule DateTime do
rules, ultimately affecting the result. For example, a country may
choose to enter or abandon "Daylight Saving Time", which is a
process where we adjust the clock one hour forward or one hour
back once per year. Whenever the rules change, the exact instant
back once per year. Whenener the rules change, the exact instant
that 2:30 AM in Polish time will be in Brazil may change.
In other words, whenever working with future DateTimes, there is
@@ -88,18 +83,17 @@ defmodule DateTime do
not a problem, because time zone rules do not change for past
events.
To make matters worse, it may be that 2:30 AM in Polish time
To make matters worse, it may be that the 2:30 AM in Polish time
does not actually even exist or it is ambiguous. If a certain
time zone observes "Daylight Saving Time", they will move their
clock forward once a year. When this happens, there is a whole
hour that does not exist. Then, when they move the clock back,
there is a certain hour that will happen twice. So if you want to
schedule a meeting when this shift back happens, you would need to
explicitly say which occurrence of 2:30 AM you mean: the one in
"Summer Time", which occurs before the shift, or the one
in "Standard Time", which occurs after it. Applications that are
date and time sensitive need to take these scenarios into account
and correctly communicate them to users.
there is a certain hour that will happen twice. So if you want
to schedule a meeting when this shift back happens, you would
need to explicitly say which of the 2:30 AM you precisely mean.
Applications that are date and time sensitive, need to take
these scenarios into account and correctly communicate them to
users.
The good news is: Elixir contains all of the building blocks
necessary to tackle those problems. The default timezone database
@@ -109,24 +103,6 @@ defmodule DateTime do
query the database and return the relevant information. For
example, look at how `DateTime.new/4` returns different results
based on the scenarios described in this section.
## Converting between timezones
Bearing in mind the cautions above, and assuming you've brought in a full
timezone database, here are some examples of common shifts between time
zones.
# Local time to UTC
new_york = DateTime.from_naive!(~N[2023-06-26T09:30:00], "America/New_York")
#=> #DateTime<2023-06-26 09:30:00-04:00 EDT America/New_York>
utc = DateTime.shift_zone!(new_york, "Etc/UTC")
#=> ~U[2023-06-26 13:30:00Z]
# UTC to local time
DateTime.shift_zone!(utc, "Europe/Paris")
#=> #DateTime<2023-06-26 15:30:00+02:00 CEST Europe/Paris>
"""
@enforce_keys [:year, :month, :day, :hour, :minute, :second] ++
@@ -175,9 +151,6 @@ defmodule DateTime do
truncate the resulting datetime. This is available
since v1.15.0.
The default unit if none gets passed is `:native`,
which results on a default resolution of microseconds.
## Examples
iex> datetime = DateTime.utc_now()
@@ -201,7 +174,7 @@ defmodule DateTime do
end
@doc """
Returns the current datetime in UTC, supporting
Returns the current datetime in UTC, supporting
a specific calendar and precision.
If you want the current time in Unix seconds,
@@ -902,7 +875,7 @@ defmodule DateTime do
-17412508655
"""
@spec to_unix(Calendar.datetime(), :native | System.time_unit()) :: integer
@spec to_unix(Calendar.datetime(), System.time_unit()) :: integer
def to_unix(datetime, unit \\ :second)
def to_unix(%{utc_offset: utc_offset, std_offset: std_offset} = datetime, unit) do
@@ -1030,12 +1003,9 @@ defmodule DateTime do
By default, `DateTime.to_iso8601/2` returns datetimes formatted in the "extended"
format, for human readability. It also supports the "basic" format through passing the `:basic` option.
Only supports converting datetimes which are in the ISO calendar,
attempting to convert datetimes from other calendars will raise.
You can also optionally specify an offset for the formatted string.
If none is given, the one in the given `datetime` is used.
Only supports converting datetimes which are in the ISO calendar.
If another calendar is given, it is automatically converted to ISO.
It raises if not possible.
WARNING: the ISO 8601 datetime format does not contain the time zone nor
its abbreviation, which means information is lost when converting to such
@@ -1219,7 +1189,7 @@ defmodule DateTime do
end
@doc """
Converts from ISO8601 specifying both a calendar and a mode.
Converts to ISO8601 specifying both a calendar and a mode.
See `from_iso8601/2` for more information.
@@ -1349,11 +1319,6 @@ defmodule DateTime do
@doc """
Converts the given `datetime` to a string according to its calendar.
Unfortunately, there is no standard that specifies rendering of a
datetime with its complete time zone information, so Elixir uses a
custom (but relatively common) representation which appends the time
zone abbreviation and full name to the datetime.
### Examples
iex> dt = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "CET",
@@ -1462,7 +1427,7 @@ defmodule DateTime do
end
@doc """
Returns `true` if the first datetime is strictly earlier than the second.
Returns true if the first datetime is strictly earlier than the second.
## Examples
@@ -1481,7 +1446,7 @@ defmodule DateTime do
end
@doc """
Returns `true` if the first datetime is strictly later than the second.
Returns true if the first datetime is strictly later than the second.
## Examples
@@ -1510,11 +1475,6 @@ defmodule DateTime do
## Examples
iex> DateTime.diff(~U[2024-01-15 10:00:10Z], ~U[2024-01-15 10:00:00Z])
10
This function also considers timezone offsets:
iex> dt1 = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "AMT",
...> hour: 23, minute: 0, second: 7, microsecond: {0, 0},
...> utc_offset: -14400, std_offset: 0, time_zone: "America/Manaus"}
@@ -1556,12 +1516,6 @@ defmodule DateTime do
%{utc_offset: utc_offset2, std_offset: std_offset2} = datetime2,
unit
) do
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
naive_diff =
(datetime1 |> to_iso_days() |> Calendar.ISO.iso_days_to_unit(unit)) -
(datetime2 |> to_iso_days() |> Calendar.ISO.iso_days_to_unit(unit))
@@ -1578,10 +1532,10 @@ defmodule DateTime do
`t:System.time_unit/0`. It defaults to `:second`. Negative values
will move backwards in time.
This function always considers the unit to be computed according
This function always consider the unit to be computed according
to the `Calendar.ISO`.
This function relies on a contiguous representation of time,
This function uses relies on a contiguous representation of time,
ignoring the wall time and timezone changes. For example, if you add
one day when there are summer time/daylight saving time changes,
it will also change the time forward or backward by one hour,
@@ -1626,8 +1580,6 @@ defmodule DateTime do
iex> result.microsecond
{21000, 3}
To shift a datetime by a `Duration` and according to its underlying calendar, use `DateTime.shift/3`.
"""
@doc since: "1.8.0"
@spec add(
@@ -1656,28 +1608,30 @@ defmodule DateTime do
add(datetime, amount_to_add * 60, :second, time_zone_database)
end
def add(%{calendar: calendar} = datetime, amount_to_add, unit, time_zone_database)
when is_integer(amount_to_add) do
def add(datetime, amount_to_add, unit, time_zone_database) when is_integer(amount_to_add) do
%{
microsecond: {_, precision},
time_zone: time_zone,
utc_offset: utc_offset,
std_offset: std_offset
std_offset: std_offset,
calendar: calendar,
microsecond: {_, precision}
} = datetime
if not is_integer(unit) and unit not in ~w(second millisecond microsecond nanosecond)a do
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
|> to_iso_days()
|> Calendar.ISO.shift_time_unit(amount_to_add, unit)
|> apply_tz_offset(utc_offset + std_offset)
|> shift_zone_for_iso_days_utc(calendar, precision, time_zone, time_zone_database)
# Subtract total offset in order to get UTC and add the integer for the addition
|> Calendar.ISO.add_day_fraction_to_iso_days(amount_to_add - total_offset, ppd)
|> shift_zone_for_iso_days_utc(calendar, precision, datetime.time_zone, time_zone_database)
case result do
{:ok, result_datetime} ->
@@ -1690,152 +1644,6 @@ defmodule DateTime do
end
end
@doc """
Shifts given `datetime` by `duration` according to its calendar.
Allowed units are: `:year`, `:month`, `:week`, `:day`, `:hour`, `:minute`, `:second`, `:microsecond`.
This operation is equivalent to shifting the datetime wall clock
(in other words, the value as someone in that timezone would see
on their watch), then applying the time zone offset to convert it
to UTC, and finally computing the new timezone in case of shifts.
This ensures `shift/3` always returns a valid datetime.
On the other hand, time zones that observe "Daylight Saving Time"
or other changes, across summer/winter time will add/remove hours
from the resulting datetime:
dt = DateTime.new!(~D[2019-03-31], ~T[01:00:00], "Europe/Copenhagen")
DateTime.shift(dt, hour: 1)
#=> #DateTime<2019-03-31 03:00:00+02:00 CEST Europe/Copenhagen>
dt = DateTime.new!(~D[2018-11-04], ~T[00:00:00], "America/Los_Angeles")
DateTime.shift(dt, hour: 2)
#=> #DateTime<2018-11-04 01:00:00-08:00 PST America/Los_Angeles>
In case you don't want these changes to happen automatically or you
want to surface time zone conflicts to the user, you can shift
the datetime as a naive datetime and then use `from_naive/2`:
dt |> NaiveDateTime.shift(duration) |> DateTime.from_naive(dt.time_zone)
When using the default ISO calendar, durations are collapsed and
applied in the order of months, then seconds and microseconds:
* when shifting by 1 year and 2 months the date is actually shifted by 14 months
* weeks, days and smaller units are collapsed into seconds and microseconds
When shifting by month, days are rounded down to the nearest valid date.
## Examples
iex> DateTime.shift(~U[2016-01-01 00:00:00Z], month: 2)
~U[2016-03-01 00:00:00Z]
iex> DateTime.shift(~U[2016-01-01 00:00:00Z], year: 1, week: 4)
~U[2017-01-29 00:00:00Z]
iex> DateTime.shift(~U[2016-01-01 00:00:00Z], minute: -25)
~U[2015-12-31 23:35:00Z]
iex> DateTime.shift(~U[2016-01-01 00:00:00Z], minute: 5, microsecond: {500, 4})
~U[2016-01-01 00:05:00.0005Z]
# leap years
iex> DateTime.shift(~U[2024-02-29 00:00:00Z], year: 1)
~U[2025-02-28 00:00:00Z]
iex> DateTime.shift(~U[2024-02-29 00:00:00Z], year: 4)
~U[2028-02-29 00:00:00Z]
# rounding down
iex> DateTime.shift(~U[2015-01-31 00:00:00Z], month: 1)
~U[2015-02-28 00:00:00Z]
"""
@doc since: "1.17.0"
@spec shift(Calendar.datetime(), Duration.duration(), Calendar.time_zone_database()) :: t
def shift(datetime, duration, time_zone_database \\ Calendar.get_time_zone_database())
def shift(%{calendar: calendar, time_zone: "Etc/UTC"} = datetime, duration, _time_zone_database) do
%{
year: year,
month: month,
day: day,
hour: hour,
minute: minute,
second: second,
microsecond: microsecond
} = datetime
{year, month, day, hour, minute, second, microsecond} =
calendar.shift_naive_datetime(
year,
month,
day,
hour,
minute,
second,
microsecond,
__duration__!(duration)
)
%DateTime{
year: year,
month: month,
day: day,
hour: hour,
minute: minute,
second: second,
microsecond: microsecond,
time_zone: "Etc/UTC",
zone_abbr: "UTC",
std_offset: 0,
utc_offset: 0
}
end
def shift(%{calendar: calendar} = datetime, duration, time_zone_database) do
%{
year: year,
month: month,
day: day,
hour: hour,
minute: minute,
second: second,
microsecond: microsecond,
std_offset: std_offset,
utc_offset: utc_offset,
time_zone: time_zone
} = datetime
{year, month, day, hour, minute, second, {_, precision} = microsecond} =
calendar.shift_naive_datetime(
year,
month,
day,
hour,
minute,
second,
microsecond,
__duration__!(duration)
)
result =
calendar.naive_datetime_to_iso_days(year, month, day, hour, minute, second, microsecond)
|> apply_tz_offset(utc_offset + std_offset)
|> shift_zone_for_iso_days_utc(calendar, precision, time_zone, time_zone_database)
case result do
{:ok, result_datetime} ->
result_datetime
{:error, error} ->
raise ArgumentError,
"cannot shift #{inspect(datetime)} to #{inspect(duration)} (with time zone " <>
"database #{inspect(time_zone_database)}), reason: #{inspect(error)}"
end
end
@doc false
defdelegate __duration__!(params), to: Duration, as: :new!
@doc """
Returns the given datetime with the microsecond field truncated to the given
precision (`:microsecond`, `:millisecond` or `:second`).
@@ -2092,8 +1900,7 @@ defmodule DateTime do
)
case datetime do
%{utc_offset: 0, std_offset: 0, time_zone: "Etc/UTC", year: year}
when calendar != Calendar.ISO or year in -9999..9999 ->
%{utc_offset: 0, std_offset: 0, time_zone: "Etc/UTC"} ->
"~U[" <> formatted <> suffix(calendar) <> "]"
_ ->
-542
View File
@@ -1,542 +0,0 @@
defmodule Duration do
@moduledoc """
Struct and functions for handling durations.
A `Duration` struct represents a collection of time scale units,
allowing for manipulation and calculation of durations.
Date and time scale units are represented as integers, allowing for
both positive and negative values.
Microseconds are represented using a tuple `{microsecond, precision}`.
This ensures compatibility with other calendar types implementing time,
such as `Time`, `DateTime`, and `NaiveDateTime`.
## Shifting
The most common use of durations in Elixir's standard library is to
"shift" the calendar types.
iex> Date.shift(~D[2016-01-03], month: 2)
~D[2016-03-03]
In the example above, `Date.shift/2` automatically converts the units
into a `Duration` struct, although one can also be given directly:
iex> Date.shift(~D[2016-01-03], Duration.new!(month: 2))
~D[2016-03-03]
It is important to note that shifting is not an arithmetic operation.
For example, adding `date + 1 month + 1 month` does not yield the same
result as `date + 2 months`. Let's see an example:
iex> ~D[2016-01-31] |> Date.shift(month: 1) |> Date.shift(month: 1)
~D[2016-03-29]
iex> ~D[2016-01-31] |> Date.shift(month: 2)
~D[2016-03-31]
As you can see above, the results differ, which explains why operations
with durations are called "shift" rather than "add". This happens because,
once we add one month to `2016-01-31`, we get `2016-02-29`. Then adding
one extra month gives us `2016-03-29` instead of `2016-03-31`.
In particular, when applying durations to `Calendar.ISO` types:
* larger units (such as years and months) are applied before
smaller ones (such as weeks, hours, days, and so on)
* units are collapsed into months (`:year` and `:month`),
seconds (`:week`, `:day`, `:hour`, `:minute`, `:second`)
and microseconds (`:microsecond`) before they are applied
* 1 year is equivalent to 12 months, 1 week is equivalent to 7 days.
Therefore, 4 weeks _are not_ equivalent to 1 month
* in case of non-existing dates, the results are rounded down to the
nearest valid date
As the `shift/2` functions are calendar aware, they are guaranteed to return
valid date/times, considering leap years as well as DST in applicable time zones.
## Intervals
Durations in Elixir can be combined with stream operations to build intervals.
For example, to retrieve the next three Wednesdays starting from 17th April, 2024:
iex> ~D[2024-04-17] |> Stream.iterate(&Date.shift(&1, week: 1)) |> Enum.take(3)
[~D[2024-04-17], ~D[2024-04-24], ~D[2024-05-01]]
However, once again, it is important to remember that shifting a duration is not
arithmetic, so you may want to use the functions in this module depending on what
you to achieve. Compare the results of both examples below:
# Adding one month after the other
iex> date = ~D[2016-01-31]
iex> duration = Duration.new!(month: 1)
iex> stream = Stream.iterate(date, fn prev_date -> Date.shift(prev_date, duration) end)
iex> Enum.take(stream, 3)
[~D[2016-01-31], ~D[2016-02-29], ~D[2016-03-29]]
# Multiplying durations by an index
iex> date = ~D[2016-01-31]
iex> duration = Duration.new!(month: 1)
iex> stream = Stream.from_index(fn i -> Date.shift(date, Duration.multiply(duration, i)) end)
iex> Enum.take(stream, 3)
[~D[2016-01-31], ~D[2016-02-29], ~D[2016-03-31]]
The second example consistently points to the last day of the month,
as it performs operations on the duration, rather than shifting date
after date.
"""
@moduledoc since: "1.17.0"
@derive {Inspect, optional: [:year, :month, :week, :day, :hour, :minute, :second, :microsecond]}
defstruct year: 0,
month: 0,
week: 0,
day: 0,
hour: 0,
minute: 0,
second: 0,
microsecond: {0, 0}
@typedoc """
The duration struct type.
"""
@type t :: %Duration{
year: integer,
month: integer,
week: integer,
day: integer,
hour: integer,
minute: integer,
second: integer,
microsecond: {integer, 0..6}
}
@typedoc """
The unit pair type specifies a pair of a valid duration unit key and value.
"""
@type unit_pair ::
{:year, integer}
| {:month, integer}
| {:week, integer}
| {:day, integer}
| {:hour, integer}
| {:minute, integer}
| {:second, integer}
| {:microsecond, {integer, 0..6}}
@typedoc """
The duration type specifies a `%Duration{}` struct or a keyword list of valid duration unit pairs.
"""
@type duration :: t | [unit_pair]
@microseconds_per_second 1_000_000
@doc """
Creates a new `Duration` struct from given `unit_pairs`.
Raises an `ArgumentError` when called with invalid unit pairs.
## Examples
iex> Duration.new!(year: 1, week: 3, hour: 4, second: 1)
%Duration{year: 1, week: 3, hour: 4, second: 1}
iex> Duration.new!(second: 1, microsecond: {1000, 6})
%Duration{second: 1, microsecond: {1000, 6}}
iex> Duration.new!(month: 2)
%Duration{month: 2}
"""
@spec new!(duration()) :: t
def new!(%Duration{} = duration) do
duration
end
def new!(unit_pairs) do
Enum.each(unit_pairs, &validate_unit!/1)
struct!(Duration, unit_pairs)
end
defp validate_unit!({:microsecond, {ms, precision}})
when is_integer(ms) and precision in 0..6 do
:ok
end
defp validate_unit!({:microsecond, microsecond}) do
raise ArgumentError,
"unsupported value #{inspect(microsecond)} for :microsecond. Expected a tuple {ms, precision} where precision is an integer from 0 to 6"
end
defp validate_unit!({unit, _value})
when unit not in [:year, :month, :week, :day, :hour, :minute, :second] do
raise ArgumentError,
"unknown unit #{inspect(unit)}. Expected :year, :month, :week, :day, :hour, :minute, :second, :microsecond"
end
defp validate_unit!({_unit, value}) when is_integer(value) do
:ok
end
defp validate_unit!({unit, value}) do
raise ArgumentError,
"unsupported value #{inspect(value)} for #{inspect(unit)}. Expected an integer"
end
@doc """
Adds units of given durations `d1` and `d2`.
Respects the the highest microsecond precision of the two.
## Examples
iex> Duration.add(Duration.new!(week: 2, day: 1), Duration.new!(day: 2))
%Duration{week: 2, day: 3}
iex> Duration.add(Duration.new!(microsecond: {400, 3}), Duration.new!(microsecond: {600, 6}))
%Duration{microsecond: {1000, 6}}
"""
@spec add(t, t) :: t
def add(%Duration{} = d1, %Duration{} = d2) do
{m1, p1} = d1.microsecond
{m2, p2} = d2.microsecond
%Duration{
year: d1.year + d2.year,
month: d1.month + d2.month,
week: d1.week + d2.week,
day: d1.day + d2.day,
hour: d1.hour + d2.hour,
minute: d1.minute + d2.minute,
second: d1.second + d2.second,
microsecond: {m1 + m2, max(p1, p2)}
}
end
@doc """
Subtracts units of given durations `d1` and `d2`.
Respects the the highest microsecond precision of the two.
## Examples
iex> Duration.subtract(Duration.new!(week: 2, day: 1), Duration.new!(day: 2))
%Duration{week: 2, day: -1}
iex> Duration.subtract(Duration.new!(microsecond: {400, 6}), Duration.new!(microsecond: {600, 3}))
%Duration{microsecond: {-200, 6}}
"""
@spec subtract(t, t) :: t
def subtract(%Duration{} = d1, %Duration{} = d2) do
{m1, p1} = d1.microsecond
{m2, p2} = d2.microsecond
%Duration{
year: d1.year - d2.year,
month: d1.month - d2.month,
week: d1.week - d2.week,
day: d1.day - d2.day,
hour: d1.hour - d2.hour,
minute: d1.minute - d2.minute,
second: d1.second - d2.second,
microsecond: {m1 - m2, max(p1, p2)}
}
end
@doc """
Multiplies `duration` units by given `integer`.
## Examples
iex> Duration.multiply(Duration.new!(day: 1, minute: 15, second: -10), 3)
%Duration{day: 3, minute: 45, second: -30}
iex> Duration.multiply(Duration.new!(microsecond: {200, 4}), 3)
%Duration{microsecond: {600, 4}}
"""
@spec multiply(t, integer) :: t
def multiply(%Duration{microsecond: {ms, p}} = duration, integer) when is_integer(integer) do
%Duration{
year: duration.year * integer,
month: duration.month * integer,
week: duration.week * integer,
day: duration.day * integer,
hour: duration.hour * integer,
minute: duration.minute * integer,
second: duration.second * integer,
microsecond: {ms * integer, p}
}
end
@doc """
Negates `duration` units.
## Examples
iex> Duration.negate(Duration.new!(day: 1, minute: 15, second: -10))
%Duration{day: -1, minute: -15, second: 10}
iex> Duration.negate(Duration.new!(microsecond: {500000, 4}))
%Duration{microsecond: {-500000, 4}}
"""
@spec negate(t) :: t
def negate(%Duration{microsecond: {ms, p}} = duration) do
%Duration{
year: -duration.year,
month: -duration.month,
week: -duration.week,
day: -duration.day,
hour: -duration.hour,
minute: -duration.minute,
second: -duration.second,
microsecond: {-ms, p}
}
end
@doc """
Parses an [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601#Durations) formatted duration string to a `Duration` struct.
Duration strings, as well as individual units, may be prefixed with plus/minus signs so that:
- `-PT6H3M` parses as `%Duration{hour: -6, minute: -3}`
- `-PT6H-3M` parses as `%Duration{hour: -6, minute: 3}`
- `+PT6H3M` parses as `%Duration{hour: 6, minute: 3}`
- `+PT6H-3M` parses as `%Duration{hour: 6, minute: -3}`
Duration designators must be provided in order of magnitude: `P[n]Y[n]M[n]W[n]DT[n]H[n]M[n]S`.
Only seconds may be specified with a decimal fraction, using either a comma or a full stop: `P1DT4,5S`.
## Examples
iex> Duration.from_iso8601("P1Y2M3DT4H5M6S")
{:ok, %Duration{year: 1, month: 2, day: 3, hour: 4, minute: 5, second: 6}}
iex> Duration.from_iso8601("P3Y-2MT3H")
{:ok, %Duration{year: 3, month: -2, hour: 3}}
iex> Duration.from_iso8601("-PT10H-30M")
{:ok, %Duration{hour: -10, minute: 30}}
iex> Duration.from_iso8601("PT4.650S")
{:ok, %Duration{second: 4, microsecond: {650000, 3}}}
"""
@spec from_iso8601(String.t()) :: {:ok, t} | {:error, atom}
def from_iso8601(string) when is_binary(string) do
case Calendar.ISO.parse_duration(string) do
{:ok, duration} ->
{:ok, new!(duration)}
error ->
error
end
end
@doc """
Same as `from_iso8601/1` but raises an `ArgumentError`.
## Examples
iex> Duration.from_iso8601!("P1Y2M3DT4H5M6S")
%Duration{year: 1, month: 2, day: 3, hour: 4, minute: 5, second: 6}
iex> Duration.from_iso8601!("P10D")
%Duration{day: 10}
"""
@spec from_iso8601!(String.t()) :: t
def from_iso8601!(string) when is_binary(string) do
case from_iso8601(string) do
{:ok, duration} ->
duration
{:error, reason} ->
raise ArgumentError, ~s/failed to parse duration "#{string}". reason: #{inspect(reason)}/
end
end
@doc """
Converts the given `duration` to a human readable representation.
## Options
* `:units` - the units to be used alongside each duration component.
The default units follow the ISO 80000-3 standard:
[
year: "a",
month: "mo",
week: "wk",
day: "d",
hour: "h",
minute: "min",
second: "s"
]
* `:separator` - a string used to separate the distinct components. Defaults to `" "`.
## Examples
iex> Duration.to_string(Duration.new!(second: 30))
"30s"
iex> Duration.to_string(Duration.new!(day: 40, hour: 12, minute: 42, second: 12))
"40d 12h 42min 12s"
By default, this function uses ISO 80000-3 units, which uses "a" for years.
But you can customize all units via the units option:
iex> Duration.to_string(Duration.new!(year: 3))
"3a"
iex> Duration.to_string(Duration.new!(year: 3), units: [year: "y"])
"3y"
You may also choose the separator:
iex> Duration.to_string(Duration.new!(day: 40, hour: 12, minute: 42, second: 12), separator: ", ")
"40d, 12h, 42min, 12s"
A duration without components is rendered as "0s":
iex> Duration.to_string(Duration.new!([]))
"0s"
Microseconds are rendered as part of seconds with the appropriate precision:
iex> Duration.to_string(Duration.new!(second: 1, microsecond: {2_200, 3}))
"1.002s"
iex> Duration.to_string(Duration.new!(second: 1, microsecond: {-1_200_000, 4}))
"-0.2000s"
"""
@doc since: "1.18.0"
def to_string(%Duration{} = duration, opts \\ []) do
units = Keyword.get(opts, :units, [])
separator = Keyword.get(opts, :separator, " ")
case to_string_year(duration, [], units) do
[] ->
"0" <> Keyword.get(units, :second, "s")
[part] ->
IO.iodata_to_binary(part)
parts ->
parts |> Enum.reduce(&[&1, separator | &2]) |> IO.iodata_to_binary()
end
end
defp to_string_part(0, _units, _key, _default, acc),
do: acc
defp to_string_part(x, units, key, default, acc),
do: [[Integer.to_string(x) | Keyword.get(units, key, default)] | acc]
defp to_string_year(%{year: year} = duration, acc, units) do
to_string_month(duration, to_string_part(year, units, :year, "a", acc), units)
end
defp to_string_month(%{month: month} = duration, acc, units) do
to_string_week(duration, to_string_part(month, units, :month, "mo", acc), units)
end
defp to_string_week(%{week: week} = duration, acc, units) do
to_string_day(duration, to_string_part(week, units, :week, "wk", acc), units)
end
defp to_string_day(%{day: day} = duration, acc, units) do
to_string_hour(duration, to_string_part(day, units, :day, "d", acc), units)
end
defp to_string_hour(%{hour: hour} = duration, acc, units) do
to_string_minute(duration, to_string_part(hour, units, :hour, "h", acc), units)
end
defp to_string_minute(%{minute: minute} = duration, acc, units) do
to_string_second(duration, to_string_part(minute, units, :minute, "min", acc), units)
end
defp to_string_second(%{second: 0, microsecond: {0, _}}, acc, _units) do
acc
end
defp to_string_second(%{second: s, microsecond: {ms, p}}, acc, units) do
[[second_component(s, ms, p) | Keyword.get(units, :second, "s")] | acc]
end
@doc """
Converts the given `duration` to an [ISO 8601-2:2019](https://en.wikipedia.org/wiki/ISO_8601) formatted string.
This function implements the extension of ISO 8601:2019, allowing weeks to appear between months and days: `P3M3W3D`.
## Examples
iex> Duration.to_iso8601(Duration.new!(year: 3))
"P3Y"
iex> Duration.to_iso8601(Duration.new!(day: 40, hour: 12, minute: 42, second: 12))
"P40DT12H42M12S"
iex> Duration.to_iso8601(Duration.new!(second: 30))
"PT30S"
iex> Duration.to_iso8601(Duration.new!([]))
"PT0S"
iex> Duration.to_iso8601(Duration.new!(second: 1, microsecond: {2_200, 3}))
"PT1.002S"
iex> Duration.to_iso8601(Duration.new!(second: 1, microsecond: {-1_200_000, 4}))
"PT-0.2000S"
"""
@spec to_iso8601(t) :: String.t()
def to_iso8601(%Duration{} = duration) do
case {to_iso8601_duration_date(duration), to_iso8601_duration_time(duration)} do
{[], []} -> "PT0S"
{date, time} -> IO.iodata_to_binary([?P, date, time])
end
end
defp to_iso8601_duration_date(%{year: 0, month: 0, week: 0, day: 0}) do
[]
end
defp to_iso8601_duration_date(%{year: year, month: month, week: week, day: day}) do
[pair(year, ?Y), pair(month, ?M), pair(week, ?W), pair(day, ?D)]
end
defp to_iso8601_duration_time(%{hour: 0, minute: 0, second: 0, microsecond: {0, _}}) do
[]
end
defp to_iso8601_duration_time(%{hour: hour, minute: minute} = d) do
[?T, pair(hour, ?H), pair(minute, ?M), second_component(d)]
end
defp second_component(%{second: 0, microsecond: {0, _}}) do
[]
end
defp second_component(%{second: second, microsecond: {ms, p}}) do
[second_component(second, ms, p), ?S]
end
defp second_component(second, _ms, 0) do
Integer.to_string(second)
end
defp second_component(second, ms, p) do
total_ms = second * @microseconds_per_second + ms
second = total_ms |> div(@microseconds_per_second) |> abs()
ms = total_ms |> rem(@microseconds_per_second) |> abs()
sign = if total_ms < 0, do: ?-, else: []
[
sign,
Integer.to_string(second),
?.,
ms |> Integer.to_string() |> String.pad_leading(6, "0") |> binary_part(0, p)
]
end
@compile {:inline, pair: 2}
defp pair(0, _key), do: []
defp pair(num, key), do: [Integer.to_string(num), key]
end
+10 -327
View File
@@ -18,8 +18,7 @@ defmodule Calendar.ISO do
The standard library supports a minimal set of possible ISO 8601 features.
Specifically, the parser only supports calendar dates and does not support
ordinal and week formats. Additionally, it supports parsing ISO 8601
formatted durations, including negative time units and fractional seconds.
ordinal and week formats.
By default Elixir only parses extended-formatted date/times. You can opt-in
to parse basic-formatted date/times.
@@ -30,7 +29,7 @@ defmodule Calendar.ISO do
Elixir does not support reduced accuracy formats (for example, a date without
the day component) nor decimal precisions in the lowest component (such as
`10:01:25,5`).
`10:01:25,5`). No functions exist to parse ISO 8601 durations or time intervals.
#### Examples
@@ -231,9 +230,9 @@ defmodule Calendar.ISO do
]
end
defguardp is_year(year) when is_integer(year)
defguardp is_year_BCE(year) when year <= 0
defguardp is_year_CE(year) when year >= 1
defguardp is_year(year) when year in -9999..9999
defguardp is_year_BCE(year) when year in -9999..0
defguardp is_year_CE(year) when year in 1..9999
defguardp is_month(month) when month in 1..12
defguardp is_day(day) when day in 1..31
defguardp is_hour(hour) when hour in 0..23
@@ -664,87 +663,6 @@ defmodule Calendar.ISO do
end
end
@doc """
Parses an ISO 8601 formatted duration string to a list of `Duration` compabitble unit pairs.
See `Duration.from_iso8601/1`.
"""
@doc since: "1.17.0"
@spec parse_duration(String.t()) :: {:ok, [Duration.unit_pair()]} | {:error, atom}
def parse_duration("P" <> string) when byte_size(string) > 0 do
parse_duration_date(string, [], year: ?Y, month: ?M, week: ?W, day: ?D)
end
def parse_duration("+P" <> string) when byte_size(string) > 0 do
parse_duration_date(string, [], year: ?Y, month: ?M, week: ?W, day: ?D)
end
def parse_duration("-P" <> string) when byte_size(string) > 0 do
with {:ok, fields} <- parse_duration_date(string, [], year: ?Y, month: ?M, week: ?W, day: ?D) do
{:ok,
Enum.map(fields, fn
{:microsecond, {value, precision}} -> {:microsecond, {-value, precision}}
{unit, value} -> {unit, -value}
end)}
end
end
def parse_duration(_) do
{:error, :invalid_duration}
end
defp parse_duration_date("", acc, _allowed), do: {:ok, acc}
defp parse_duration_date("T" <> string, acc, _allowed) when byte_size(string) > 0 do
parse_duration_time(string, acc, hour: ?H, minute: ?M, second: ?S)
end
defp parse_duration_date(string, acc, allowed) do
with {integer, <<next, rest::binary>>} <- Integer.parse(string),
{key, allowed} <- find_unit(allowed, next) do
parse_duration_date(rest, [{key, integer} | acc], allowed)
else
_ -> {:error, :invalid_date_component}
end
end
defp parse_duration_time("", acc, _allowed), do: {:ok, acc}
defp parse_duration_time(string, acc, allowed) do
case Integer.parse(string) do
{second, <<delimiter, _::binary>> = rest} when delimiter in [?., ?,] ->
case parse_microsecond(rest) do
{{ms, precision}, "S"} ->
ms =
case string do
"-" <> _ ->
-ms
_ ->
ms
end
{:ok, [second: second, microsecond: {ms, precision}] ++ acc}
_ ->
{:error, :invalid_time_component}
end
{integer, <<next, rest::binary>>} ->
case find_unit(allowed, next) do
{key, allowed} -> parse_duration_time(rest, [{key, integer} | acc], allowed)
false -> {:error, :invalid_time_component}
end
_ ->
{:error, :invalid_time_component}
end
end
defp find_unit([{key, unit} | rest], unit), do: {key, rest}
defp find_unit([_ | rest], unit), do: find_unit(rest, unit)
defp find_unit([], _unit), do: false
@doc """
Returns the `t:Calendar.iso_days/0` format of the specified date.
@@ -891,7 +809,7 @@ defmodule Calendar.ISO do
# Converts count of days since 0000-01-01 to {year, month, day} tuple.
@doc false
def date_from_iso_days(days) do
def date_from_iso_days(days) when days in -3_652_059..3_652_424 do
{year, day_of_year} = days_to_year(days)
extra_day = if leap_year?(year), do: 1, else: 0
{month, day_in_month} = year_day_to_year_date(extra_day, day_of_year)
@@ -1446,7 +1364,7 @@ defmodule Calendar.ISO do
@spec valid_date?(year, month, day) :: boolean
def valid_date?(year, month, day)
when is_integer(year) and is_integer(month) and is_integer(day) do
is_month(month) and day in 1..days_in_month(year, month)
is_year(year) and is_month(month) and day in 1..days_in_month(year, month)
end
@doc """
@@ -1537,230 +1455,6 @@ defmodule Calendar.ISO do
{days, {@parts_per_day - 1, @parts_per_day}}
end
@doc """
Shifts Date by Duration according to its calendar.
## Examples
iex> Calendar.ISO.shift_date(2016, 1, 3, Duration.new!(month: 2))
{2016, 3, 3}
iex> Calendar.ISO.shift_date(2016, 2, 29, Duration.new!(month: 1))
{2016, 3, 29}
iex> Calendar.ISO.shift_date(2016, 1, 31, Duration.new!(month: 1))
{2016, 2, 29}
iex> Calendar.ISO.shift_date(2016, 1, 31, Duration.new!(year: 4, day: 1))
{2020, 2, 1}
"""
@impl true
@spec shift_date(year, month, day, Duration.t()) :: {year, month, day}
def shift_date(year, month, day, duration) do
shift_options = shift_date_options(duration)
Enum.reduce(shift_options, {year, month, day}, fn
{_, 0}, date ->
date
{:month, value}, date ->
shift_months(date, value)
{:day, value}, date ->
shift_days(date, value)
end)
end
@doc """
Shifts NaiveDateTime by Duration according to its calendar.
## Examples
iex> Calendar.ISO.shift_naive_datetime(2016, 1, 3, 0, 0, 0, {0, 0}, Duration.new!(hour: 1))
{2016, 1, 3, 1, 0, 0, {0, 0}}
iex> Calendar.ISO.shift_naive_datetime(2016, 1, 3, 0, 0, 0, {0, 0}, Duration.new!(hour: 30))
{2016, 1, 4, 6, 0, 0, {0, 0}}
iex> Calendar.ISO.shift_naive_datetime(2016, 1, 3, 0, 0, 0, {0, 0}, Duration.new!(microsecond: {100, 6}))
{2016, 1, 3, 0, 0, 0, {100, 6}}
"""
@impl true
@spec shift_naive_datetime(
year,
month,
day,
hour,
minute,
second,
microsecond,
Duration.t()
) :: {year, month, day, hour, minute, second, microsecond}
def shift_naive_datetime(year, month, day, hour, minute, second, microsecond, duration) do
shift_options = shift_datetime_options(duration)
Enum.reduce(shift_options, {year, month, day, hour, minute, second, microsecond}, fn
{_, 0}, naive_datetime ->
naive_datetime
{:month, value}, {year, month, day, hour, minute, second, microsecond} ->
{new_year, new_month, new_day} = shift_months({year, month, day}, value)
{new_year, new_month, new_day, hour, minute, second, microsecond}
{time_unit, value}, naive_datetime ->
shift_time_unit(naive_datetime, value, time_unit)
end)
end
@doc """
Shifts Time by Duration units according to its calendar.
## Examples
iex> Calendar.ISO.shift_time(13, 0, 0, {0, 0}, Duration.new!(hour: 2))
{15, 0, 0, {0, 0}}
iex> Calendar.ISO.shift_time(13, 0, 0, {0, 0}, Duration.new!(microsecond: {100, 6}))
{13, 0, 0, {100, 6}}
"""
@impl true
@spec shift_time(hour, minute, second, microsecond, Duration.t()) ::
{hour, minute, second, microsecond}
def shift_time(hour, minute, second, microsecond, duration) do
shift_options = shift_time_options(duration)
Enum.reduce(shift_options, {hour, minute, second, microsecond}, fn
{_, 0}, time ->
time
{time_unit, value}, time ->
shift_time_unit(time, value, time_unit)
end)
end
@doc false
def shift_days({year, month, day}, days) do
{year, month, day} =
date_to_iso_days(year, month, day)
|> Kernel.+(days)
|> date_from_iso_days()
{year, month, day}
end
defp shift_months({year, month, day}, months) do
months_in_year = 12
total_months = year * months_in_year + month + months - 1
new_year = Integer.floor_div(total_months, months_in_year)
new_month =
case rem(total_months, months_in_year) + 1 do
new_month when new_month < 1 -> new_month + months_in_year
new_month -> new_month
end
new_day = min(day, days_in_month(new_year, new_month))
{new_year, new_month, new_day}
end
@doc false
def shift_time_unit({year, month, day, hour, minute, second, microsecond}, value, unit)
when unit in [:second, :millisecond, :microsecond, :nanosecond] or is_integer(unit) do
{value, precision} = shift_time_unit_values(value, microsecond)
{year, month, day, hour, minute, second, {ms_value, _}} =
naive_datetime_to_iso_days(year, month, day, hour, minute, second, microsecond)
|> shift_time_unit(value, unit)
|> naive_datetime_from_iso_days()
{year, month, day, hour, minute, second, {ms_value, precision}}
end
def shift_time_unit({hour, minute, second, microsecond}, value, unit)
when unit in [:second, :millisecond, :microsecond, :nanosecond] or is_integer(unit) do
{value, precision} = shift_time_unit_values(value, microsecond)
{_days, day_fraction} =
shift_time_unit({0, time_to_day_fraction(hour, minute, second, microsecond)}, value, unit)
{hour, minute, second, {microsecond, _}} = time_from_day_fraction(day_fraction)
{hour, minute, second, {microsecond, precision}}
end
def shift_time_unit({_days, _day_fraction} = iso_days, value, unit)
when unit in [:second, :millisecond, :microsecond, :nanosecond] or is_integer(unit) do
ppd = System.convert_time_unit(86400, :second, unit)
add_day_fraction_to_iso_days(iso_days, value, ppd)
end
defp shift_time_unit_values({0, _}, {_, original_precision}) do
{0, original_precision}
end
defp shift_time_unit_values({ms_value, ms_precision}, {_, _}) do
{ms_value, ms_precision}
end
defp shift_time_unit_values(value, {_, original_precision}) do
{value, original_precision}
end
defp shift_date_options(%Duration{
year: year,
month: month,
week: week,
day: day,
hour: 0,
minute: 0,
second: 0,
microsecond: {0, _precision}
}) do
[
month: year * 12 + month,
day: week * 7 + day
]
end
defp shift_date_options(_duration) do
raise ArgumentError,
"cannot shift date by time scale unit. Expected :year, :month, :week, :day"
end
defp shift_datetime_options(%Duration{
year: year,
month: month,
week: week,
day: day,
hour: hour,
minute: minute,
second: second,
microsecond: microsecond
}) do
[
month: year * 12 + month,
second: week * 7 * 86400 + day * 86400 + hour * 3600 + minute * 60 + second,
microsecond: microsecond
]
end
defp shift_time_options(%Duration{
year: 0,
month: 0,
week: 0,
day: 0,
hour: hour,
minute: minute,
second: second,
microsecond: microsecond
}) do
[
second: hour * 3600 + minute * 60 + second,
microsecond: microsecond
]
end
defp shift_time_options(_duration) do
raise ArgumentError,
"cannot shift time by date scale unit. Expected :hour, :minute, :second, :microsecond"
end
## Helpers
@doc false
@@ -1948,22 +1642,11 @@ defmodule Calendar.ISO do
defp days_in_previous_years(0), do: 0
# A concise version of the algorithm would use floor_div instead of div.
# However, floor_div would check the operands on every operation.
# We optimize this by providing a positive and negative version of each algorithm.
defp days_in_previous_years(year) when year > 0 do
defp days_in_previous_years(year) do
previous_year = year - 1
div(previous_year, 4) - div(previous_year, 100) +
div(previous_year, 400) + previous_year * @days_per_nonleap_year +
@days_per_leap_year
end
defp days_in_previous_years(year) when year < 0 do
previous_year = year - 1
div(year, 4) - div(year, 100) +
div(year, 400) - 1 + previous_year * @days_per_nonleap_year +
Integer.floor_div(previous_year, 4) - Integer.floor_div(previous_year, 100) +
Integer.floor_div(previous_year, 400) + previous_year * @days_per_nonleap_year +
@days_per_leap_year
end
+39 -175
View File
@@ -110,12 +110,30 @@ defmodule NaiveDateTime do
@spec utc_now(Calendar.calendar() | :native | :microsecond | :millisecond | :second) :: t
def utc_now(calendar_or_time_unit \\ Calendar.ISO)
def utc_now(Calendar.ISO) do
{:ok, {year, month, day}, {hour, minute, second}, microsecond} =
Calendar.ISO.from_unix(:os.system_time(), :native)
%NaiveDateTime{
year: year,
month: month,
day: day,
hour: hour,
minute: minute,
second: second,
microsecond: microsecond,
calendar: Calendar.ISO
}
end
def utc_now(time_unit) when time_unit in [:microsecond, :millisecond, :second, :native] do
utc_now(time_unit, Calendar.ISO)
end
def utc_now(calendar) do
utc_now(:native, calendar)
calendar
|> DateTime.utc_now()
|> DateTime.to_naive()
end
@doc """
@@ -140,20 +158,7 @@ defmodule NaiveDateTime do
@spec utc_now(:native | :microsecond | :millisecond | :second, Calendar.calendar()) :: t
def utc_now(time_unit, calendar)
when time_unit in [:native, :microsecond, :millisecond, :second] do
{:ok, {year, month, day}, {hour, minute, second}, microsecond} =
Calendar.ISO.from_unix(System.os_time(time_unit), time_unit)
%NaiveDateTime{
year: year,
month: month,
day: day,
hour: hour,
minute: minute,
second: second,
microsecond: microsecond,
calendar: Calendar.ISO
}
|> convert!(calendar)
DateTime.utc_now(time_unit, calendar) |> DateTime.to_naive()
end
@doc """
@@ -247,7 +252,7 @@ defmodule NaiveDateTime do
Calendar.hour(),
Calendar.minute(),
Calendar.second(),
Calendar.microsecond() | non_neg_integer(),
Calendar.microsecond() | non_neg_integer,
Calendar.calendar()
) :: {:ok, t} | {:error, atom}
def new(year, month, day, hour, minute, second, microsecond \\ {0, 0}, calendar \\ Calendar.ISO)
@@ -312,7 +317,7 @@ defmodule NaiveDateTime do
Calendar.hour(),
Calendar.minute(),
Calendar.second(),
Calendar.microsecond() | non_neg_integer(),
Calendar.microsecond() | non_neg_integer,
Calendar.calendar()
) :: t
def new!(
@@ -443,8 +448,6 @@ defmodule NaiveDateTime do
iex> NaiveDateTime.add(dt, 21, :second)
~N[2000-02-29 23:00:28]
To shift a naive datetime by a `Duration` and according to its underlying calendar, use `NaiveDateTime.shift/2`.
"""
@doc since: "1.4.0"
@spec add(Calendar.naive_datetime(), integer, :day | :hour | :minute | System.time_unit()) :: t
@@ -463,21 +466,17 @@ defmodule NaiveDateTime do
end
def add(
%{calendar: calendar, microsecond: {_, precision}} = naive_datetime,
%{microsecond: {_, precision}, calendar: calendar} = naive_datetime,
amount_to_add,
unit
)
when is_integer(amount_to_add) do
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)
precision = max(Calendar.ISO.time_unit_to_precision(unit), precision)
naive_datetime
|> to_iso_days()
|> Calendar.ISO.shift_time_unit(amount_to_add, unit)
|> Calendar.ISO.add_day_fraction_to_iso_days(amount_to_add, ppd)
|> from_iso_days(calendar, precision)
end
@@ -555,94 +554,11 @@ defmodule NaiveDateTime do
"and thus the result would be ambiguous"
end
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
units1 = naive_datetime1 |> to_iso_days() |> Calendar.ISO.iso_days_to_unit(unit)
units2 = naive_datetime2 |> to_iso_days() |> Calendar.ISO.iso_days_to_unit(unit)
units1 - units2
end
@doc """
Shifts given `naive_datetime` by `duration` according to its calendar.
Allowed units are: `:year`, `:month`, `:week`, `:day`, `:hour`, `:minute`, `:second`, `:microsecond`.
When using the default ISO calendar, durations are collapsed and
applied in the order of months, then seconds and microseconds:
* when shifting by 1 year and 2 months the date is actually shifted by 14 months
* weeks, days and smaller units are collapsed into seconds and microseconds
When shifting by month, days are rounded down to the nearest valid date.
## Examples
iex> NaiveDateTime.shift(~N[2016-01-31 00:00:00], month: 1)
~N[2016-02-29 00:00:00]
iex> NaiveDateTime.shift(~N[2016-01-31 00:00:00], year: 4, day: 1)
~N[2020-02-01 00:00:00]
iex> NaiveDateTime.shift(~N[2016-01-31 00:00:00], year: -2, day: 1)
~N[2014-02-01 00:00:00]
iex> NaiveDateTime.shift(~N[2016-01-31 00:00:00], second: 45)
~N[2016-01-31 00:00:45]
iex> NaiveDateTime.shift(~N[2016-01-31 00:00:00], microsecond: {100, 6})
~N[2016-01-31 00:00:00.000100]
# leap years
iex> NaiveDateTime.shift(~N[2024-02-29 00:00:00], year: 1)
~N[2025-02-28 00:00:00]
iex> NaiveDateTime.shift(~N[2024-02-29 00:00:00], year: 4)
~N[2028-02-29 00:00:00]
# rounding down
iex> NaiveDateTime.shift(~N[2015-01-31 00:00:00], month: 1)
~N[2015-02-28 00:00:00]
"""
@doc since: "1.17.0"
@spec shift(Calendar.naive_datetime(), Duration.duration()) :: t
def shift(%{calendar: calendar} = naive_datetime, duration) do
%{
year: year,
month: month,
day: day,
hour: hour,
minute: minute,
second: second,
microsecond: microsecond
} = naive_datetime
{year, month, day, hour, minute, second, microsecond} =
calendar.shift_naive_datetime(
year,
month,
day,
hour,
minute,
second,
microsecond,
__duration__!(duration)
)
%NaiveDateTime{
calendar: calendar,
year: year,
month: month,
day: day,
hour: hour,
minute: minute,
second: second,
microsecond: microsecond
}
end
@doc false
defdelegate __duration__!(params), to: Duration, as: :new!
@doc """
Returns the given naive datetime with the microsecond field truncated to the
given precision (`:microsecond`, `:millisecond` or `:second`).
@@ -754,9 +670,6 @@ defmodule NaiveDateTime do
@doc """
Converts the given naive datetime to a string according to its calendar.
For redability, this function follows the RFC3339 suggestion of removing
the "T" separator between the date and time components.
### Examples
iex> NaiveDateTime.to_string(~N[2000-02-28 23:00:13])
@@ -991,8 +904,6 @@ defmodule NaiveDateTime do
iex> NaiveDateTime.from_erl({{2000, 1, 1}, {13, 30, 15}})
{:ok, ~N[2000-01-01 13:30:15]}
iex> NaiveDateTime.from_erl({{2000, 1, 1}, {13, 30, 15}}, 5000)
{:ok, ~N[2000-01-01 13:30:15.005000]}
iex> NaiveDateTime.from_erl({{2000, 1, 1}, {13, 30, 15}}, {5000, 3})
{:ok, ~N[2000-01-01 13:30:15.005]}
iex> NaiveDateTime.from_erl({{2000, 13, 1}, {13, 30, 15}})
@@ -1001,11 +912,7 @@ defmodule NaiveDateTime do
{:error, :invalid_date}
"""
@spec from_erl(
:calendar.datetime(),
Calendar.microsecond() | non_neg_integer(),
Calendar.calendar()
) ::
@spec from_erl(:calendar.datetime(), Calendar.microsecond(), Calendar.calendar()) ::
{:ok, t} | {:error, atom}
def from_erl(tuple, microsecond \\ {0, 0}, calendar \\ Calendar.ISO)
@@ -1024,19 +931,13 @@ defmodule NaiveDateTime do
iex> NaiveDateTime.from_erl!({{2000, 1, 1}, {13, 30, 15}})
~N[2000-01-01 13:30:15]
iex> NaiveDateTime.from_erl!({{2000, 1, 1}, {13, 30, 15}}, 5000)
~N[2000-01-01 13:30:15.005000]
iex> NaiveDateTime.from_erl!({{2000, 1, 1}, {13, 30, 15}}, {5000, 3})
~N[2000-01-01 13:30:15.005]
iex> NaiveDateTime.from_erl!({{2000, 13, 1}, {13, 30, 15}})
** (ArgumentError) cannot convert {{2000, 13, 1}, {13, 30, 15}} to naive datetime, reason: :invalid_date
"""
@spec from_erl!(
:calendar.datetime(),
Calendar.microsecond() | non_neg_integer(),
Calendar.calendar()
) :: t
@spec from_erl!(:calendar.datetime(), Calendar.microsecond(), Calendar.calendar()) :: t
def from_erl!(tuple, microsecond \\ {0, 0}, calendar \\ Calendar.ISO) do
case from_erl(tuple, microsecond, calendar) do
{:ok, value} ->
@@ -1063,28 +964,11 @@ defmodule NaiveDateTime do
"""
@doc since: "1.11.0"
@spec from_gregorian_seconds(integer(), Calendar.microsecond(), Calendar.calendar()) :: t
def from_gregorian_seconds(seconds, microsecond_precision \\ {0, 0}, calendar \\ Calendar.ISO)
def from_gregorian_seconds(seconds, {microsecond, precision}, Calendar.ISO)
when is_integer(seconds) do
{days, seconds} = div_rem(seconds, 24 * 60 * 60)
{hours, seconds} = div_rem(seconds, 60 * 60)
{minutes, seconds} = div_rem(seconds, 60)
{year, month, day} = Calendar.ISO.date_from_iso_days(days)
%NaiveDateTime{
calendar: Calendar.ISO,
year: year,
month: month,
day: day,
hour: hours,
minute: minutes,
second: seconds,
microsecond: {microsecond, precision}
}
end
def from_gregorian_seconds(seconds, {microsecond, precision}, calendar)
def from_gregorian_seconds(
seconds,
{microsecond, precision} \\ {0, 0},
calendar \\ Calendar.ISO
)
when is_integer(seconds) do
iso_days = Calendar.ISO.gregorian_seconds_to_iso_days(seconds, microsecond)
@@ -1103,17 +987,6 @@ defmodule NaiveDateTime do
}
end
defp div_rem(int1, int2) do
div = div(int1, int2)
rem = int1 - div * int2
if rem >= 0 do
{div, rem}
else
{div - 1, rem + int2}
end
end
@doc """
Converts a `NaiveDateTime` struct to a number of gregorian seconds and microseconds.
@@ -1200,7 +1073,7 @@ defmodule NaiveDateTime do
end
@doc """
Returns `true` if the first `NaiveDateTime` is strictly earlier than the second.
Returns true if the first `NaiveDateTime` is strictly earlier than the second.
## Examples
@@ -1219,7 +1092,7 @@ defmodule NaiveDateTime do
end
@doc """
Returns `true` if the first `NaiveDateTime` is strictly later than the second.
Returns true if the first `NaiveDateTime` is strictly later than the second.
## Examples
@@ -1260,10 +1133,6 @@ defmodule NaiveDateTime do
{:ok, t} | {:error, :incompatible_calendars}
# Keep it multiline for proper function clause errors.
def convert(%NaiveDateTime{calendar: calendar} = ndt, calendar) do
{:ok, ndt}
end
def convert(
%{
calendar: calendar,
@@ -1343,7 +1212,7 @@ defmodule NaiveDateTime do
datetime
|> NaiveDateTime.beginning_of_day()
|> DateTime.from_naive(datetime.time_zone)
|> DateTime.from_naive(datetime.timezone)
Note that the beginning of the day may not exist or be ambiguous
in a given timezone, so you must handle those cases accordingly.
@@ -1370,7 +1239,7 @@ defmodule NaiveDateTime do
datetime
|> NaiveDateTime.end_of_day()
|> DateTime.from_naive(datetime.time_zone)
|> DateTime.from_naive(datetime.timezone)
Note that the end of the day may not exist or be ambiguous
in a given timezone, so you must handle those cases accordingly.
@@ -1465,15 +1334,10 @@ defmodule NaiveDateTime do
calendar: calendar
} = naive_datetime
if calendar != Calendar.ISO or year in -9999..9999 do
formatted =
calendar.naive_datetime_to_string(year, month, day, hour, minute, second, microsecond)
formatted =
calendar.naive_datetime_to_string(year, month, day, hour, minute, second, microsecond)
"~N[" <> formatted <> suffix(calendar) <> "]"
else
"NaiveDateTime.new!(#{Integer.to_string(year)}, #{Integer.to_string(month)}, #{Integer.to_string(day)}, " <>
"#{Integer.to_string(hour)}, #{Integer.to_string(minute)}, #{Integer.to_string(second)}, #{inspect(microsecond)})"
end
"~N[" <> formatted <> suffix(calendar) <> "]"
end
defp suffix(Calendar.ISO), do: ""
+30 -119
View File
@@ -50,6 +50,7 @@ defmodule Time do
calendar: Calendar.calendar()
}
@parts_per_day 86_400_000_000
@seconds_per_day 24 * 60 * 60
@doc """
@@ -115,7 +116,7 @@ defmodule Time do
Calendar.hour(),
Calendar.minute(),
Calendar.second(),
Calendar.microsecond() | non_neg_integer(),
Calendar.microsecond() | non_neg_integer,
Calendar.calendar()
) :: {:ok, t} | {:error, atom}
def new(hour, minute, second, microsecond \\ {0, 0}, calendar \\ Calendar.ISO)
@@ -355,21 +356,13 @@ defmodule Time do
## Examples
iex> Time.from_erl({23, 30, 15})
{:ok, ~T[23:30:15]}
iex> Time.from_erl({23, 30, 15}, 5000)
{:ok, ~T[23:30:15.005000]}
iex> Time.from_erl({23, 30, 15}, {5000, 3})
{:ok, ~T[23:30:15.005]}
iex> Time.from_erl({24, 30, 15})
{:error, :invalid_time}
"""
@spec from_erl(
:calendar.time(),
Calendar.microsecond() | non_neg_integer(),
Calendar.calendar()
) ::
@spec from_erl(:calendar.time(), Calendar.microsecond(), Calendar.calendar()) ::
{:ok, t} | {:error, atom}
def from_erl(tuple, microsecond \\ {0, 0}, calendar \\ Calendar.ISO)
@@ -385,8 +378,6 @@ defmodule Time do
iex> Time.from_erl!({23, 30, 15})
~T[23:30:15]
iex> Time.from_erl!({23, 30, 15}, 5000)
~T[23:30:15.005000]
iex> Time.from_erl!({23, 30, 15}, {5000, 3})
~T[23:30:15.005]
iex> Time.from_erl!({24, 30, 15})
@@ -509,8 +500,6 @@ defmodule Time do
iex> result.microsecond
{21000, 3}
To shift a time by a `Duration` and according to its underlying calendar, use `Time.shift/2`.
"""
@doc since: "1.6.0"
@spec add(Calendar.time(), integer, :hour | :minute | System.time_unit()) :: t
@@ -526,26 +515,13 @@ defmodule Time do
def add(%{calendar: calendar, microsecond: {_, precision}} = time, amount_to_add, unit)
when is_integer(amount_to_add) do
valid? =
if is_integer(unit),
do: unit > 0,
else: unit in ~w(second millisecond microsecond nanosecond)a
if not valid? do
raise ArgumentError,
"unsupported time unit. Expected :hour, :minute, :second, :millisecond, :microsecond, :nanosecond, or a positive integer, got #{inspect(unit)}"
end
%{hour: hour, minute: minute, second: second, microsecond: microsecond} = time
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, _precision}} =
Calendar.ISO.shift_time_unit(
{hour, minute, second, microsecond},
amount_to_add,
unit
)
{hour, minute, second, {microsecond, _}} =
calendar.time_from_day_fraction({parts, @parts_per_day})
%Time{
hour: hour,
@@ -556,91 +532,19 @@ defmodule Time do
}
end
@doc """
Shifts given `time` by `duration` according to its calendar.
Available duration units are: `:hour`, `:minute`, `:second`, `:microsecond`.
When using the default ISO calendar, durations are collapsed to seconds and
microseconds before they are applied.
Raises an `ArgumentError` when called with date scale units.
## Examples
iex> Time.shift(~T[01:00:15], hour: 12)
~T[13:00:15]
iex> Time.shift(~T[01:35:00], hour: 6, minute: -15)
~T[07:20:00]
iex> Time.shift(~T[01:15:00], second: 125)
~T[01:17:05]
iex> Time.shift(~T[01:00:15], microsecond: {100, 6})
~T[01:00:15.000100]
iex> Time.shift(~T[01:15:00], Duration.new!(second: 65))
~T[01:16:05]
"""
@doc since: "1.17.0"
@spec shift(Calendar.time(), Duration.t() | [unit_pair]) :: t
when unit_pair:
{:hour, integer}
| {:minute, integer}
| {:second, integer}
| {:microsecond, {integer, 0..6}}
def shift(%{calendar: calendar} = time, duration) do
%{hour: hour, minute: minute, second: second, microsecond: microsecond} = time
{hour, minute, second, microsecond} =
calendar.shift_time(hour, minute, second, microsecond, __duration__!(duration))
%Time{
calendar: calendar,
hour: hour,
minute: minute,
second: second,
microsecond: microsecond
}
defp time_to_microseconds(%{
calendar: Calendar.ISO,
hour: 0,
minute: 0,
second: 0,
microsecond: {0, _}
}) do
0
end
@doc false
def __duration__!(%Duration{} = duration) do
duration
end
# This part is inlined by the compiler on constant values
def __duration__!(unit_pairs) do
Enum.each(unit_pairs, &validate_duration_unit!/1)
struct!(Duration, unit_pairs)
end
defp validate_duration_unit!({:microsecond, {ms, precision}})
when is_integer(ms) and precision in 0..6 do
:ok
end
defp validate_duration_unit!({:microsecond, microsecond}) do
raise ArgumentError,
"unsupported value #{inspect(microsecond)} for :microsecond. Expected a tuple {ms, precision} where precision is an integer from 0 to 6"
end
defp validate_duration_unit!({unit, _value}) when unit in [:year, :month, :week, :day] do
raise ArgumentError,
"unsupported unit #{inspect(unit)}. Expected :hour, :minute, :second, :microsecond"
end
defp validate_duration_unit!({unit, _value})
when unit not in [:hour, :minute, :second, :microsecond] do
raise ArgumentError,
"unknown unit #{inspect(unit)}. Expected :hour, :minute, :second, :microsecond"
end
defp validate_duration_unit!({_unit, value}) when is_integer(value) do
:ok
end
defp validate_duration_unit!({unit, value}) do
raise ArgumentError,
"unsupported value #{inspect(value)} for #{inspect(unit)}. Expected an integer"
defp time_to_microseconds(time) do
iso_days = {0, to_day_fraction(time)}
Calendar.ISO.iso_days_to_unit(iso_days, :microsecond)
end
@doc """
@@ -695,7 +599,7 @@ defmodule Time do
end
@doc """
Returns `true` if the first time is strictly earlier than the second.
Returns true if the first time is strictly earlier than the second.
## Examples
@@ -714,7 +618,7 @@ defmodule Time do
end
@doc """
Returns `true` if the first time is strictly later than the second.
Returns true if the first time is strictly later than the second.
## Examples
@@ -807,8 +711,15 @@ defmodule Time do
@doc since: "1.5.0"
@spec convert!(Calendar.time(), Calendar.calendar()) :: t
def convert!(time, calendar) do
{:ok, value} = convert(time, calendar)
value
case convert(time, calendar) do
{:ok, value} ->
value
{:error, reason} ->
raise ArgumentError,
"cannot convert #{inspect(time)} to target calendar #{inspect(calendar)}, " <>
"reason: #{inspect(reason)}"
end
end
@doc """
+78 -205
View File
@@ -3,14 +3,14 @@ defmodule Code do
Utilities for managing code compilation, code evaluation, and code loading.
This module complements Erlang's [`:code` module](`:code`)
to add behavior which is specific to Elixir. For functions to
to add behaviour which is specific to Elixir. For functions to
manipulate Elixir's AST (rather than evaluating it), see the
`Macro` module.
## Working with files
This module contains three functions for compiling and evaluating files.
Here is a summary of them and their behavior:
Here is a summary of them and their behaviour:
* `require_file/2` - compiles a file and tracks its name. It does not
compile the file again if it has been previously required.
@@ -123,11 +123,6 @@ defmodule Code do
of the imported function/macro. A :remote_function/:remote_macro event
may still be emitted for the imported module/name/arity.
* `{:imported_quoted, meta, module, name, [arity]}` - traced whenever an
imported function or macro is processed inside a `quote/2`. `meta` is the
call AST metadata, `module` is the module the import is from, followed by
the `name` and a list of `arities` of the imported function/macro.
* `{:alias, meta, alias, as, opts}` - traced whenever `alias` is aliased
to `as`. `meta` is the alias AST metadata and `opts` are the alias options.
@@ -163,10 +158,6 @@ defmodule Code do
of keys to traverse in the application environment and `return` is either
`{:ok, value}` or `:error`.
* `:defmodule` - (since v1.16.2) traced as soon as the definition of a module
starts. This is invoked early on in the module life cycle, `Module.open?/1`
still returns `false` for such traces
* `{:on_module, bytecode, _ignore}` - (since v1.13.0) traced whenever a module
is defined. This is equivalent to the `@after_compile` callback and invoked
after any `@after_compile` in the given module. The third element is currently
@@ -185,7 +176,7 @@ defmodule Code do
defmodule MyTracer do
def trace({:remote_function, _meta, module, name, arity}, env) do
IO.puts("#{env.file}:#{env.line} #{inspect(module)}.#{name}/#{arity}")
IO.puts "#{env.file}:#{env.line} #{inspect(module)}.#{name}/#{arity}"
:ok
end
@@ -205,52 +196,27 @@ defmodule Code do
@typedoc """
Diagnostics returned by the compiler and code evaluation.
The file and position relate to where the diagnostic should be shown.
If there is a file and position, then the diagnostic is precise
and you can use the given file and position for generating snippets,
IDEs annotations, and so on. An optional span is available with
the line and column the diagnostic ends.
Otherwise, a stacktrace may be given, which you can place your own
heuristics to provide better reporting.
The source field points to the source file the compiler tracked
the error to. For example, a file `lib/foo.ex` may embed `.eex`
templates from `lib/foo/bar.eex`. A syntax error on the EEx template
will point to file `lib/foo/bar.eex` but the source is `lib/foo.ex`.
"""
@type diagnostic(severity) :: %{
required(:source) => Path.t() | nil,
required(:file) => Path.t() | nil,
required(:file) => Path.t(),
required(:severity) => severity,
required(:message) => String.t(),
required(:position) => position(),
required(:position) => position,
required(:stacktrace) => Exception.stacktrace(),
required(:span) => {line :: pos_integer(), column :: pos_integer()} | nil,
optional(:details) => term(),
optional(any()) => any()
}
@typedoc "The line. 0 indicates no line."
@type line() :: non_neg_integer()
@typedoc """
The position of the diagnostic.
Can be either a line number or a `{line, column}`.
Line and columns numbers are one-based.
A position of `0` represents unknown.
"""
@type position() :: line() | {line :: pos_integer(), column :: pos_integer()}
@type position() :: line() | {pos_integer(), column :: non_neg_integer}
@boolean_compiler_options [
:docs,
:debug_info,
:ignore_already_consolidated,
:ignore_module_conflict,
:infer_signatures,
:relative_paths
:relative_paths,
:warnings_as_errors
]
@list_compiler_options [:no_warn_undefined, :tracers, :parser_options]
@@ -512,9 +478,14 @@ defmodule Code do
## Options
It accepts the same options as `env_for_eval/1`. Additionally, you may
also pass an environment as second argument, so the evaluation happens
within that environment.
Options can be:
* `:file` - the file to be considered in the evaluation
* `: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.
Returns a tuple of the form `{value, binding}`, where `value` is the value
returned from evaluating `string`. If an error occurs while evaluating
@@ -582,32 +553,14 @@ defmodule Code do
@doc """
Executes the given `fun` and capture all diagnostics.
Diagnostics are warnings and errors emitted during code
evaluation or single-file compilation and by functions
such as `IO.warn/2`.
If using `mix compile` or `Kernel.ParallelCompiler`,
note they already capture and return diagnostics.
Diagnostics are warnings and errors emitted by the compiler
and by functions such as `IO.warn/2`.
## Options
* `:log` - if the diagnostics should be logged as they happen.
Defaults to `false`.
> #### Rescuing errors {: .info}
>
> `with_diagnostics/2` does not automatically handle exceptions.
> You may capture them by adding a `try/1` in `fun`:
>
> {result, all_errors_and_warnings} =
> Code.with_diagnostics(fn ->
> try do
> {:ok, Code.compile_quoted(quoted)}
> rescue
> err -> {:error, err}
> end
> end)
"""
@doc since: "1.15.0"
@spec with_diagnostics(keyword(), (-> result)) :: {result, [diagnostic(:warning | :error)]}
@@ -635,18 +588,11 @@ defmodule Code do
A diagnostic is either returned by `Kernel.ParallelCompiler`
or by `Code.with_diagnostics/2`.
## Options
* `:snippet` - whether to read the code snippet in the diagnostic location.
As it may impact performance, it is not recommended to be used in runtime.
Defaults to `true`.
"""
@doc since: "1.15.0"
@spec print_diagnostic(diagnostic(:warning | :error), keyword()) :: :ok
def print_diagnostic(diagnostic, opts \\ []) do
read_snippet? = Keyword.get(opts, :snippet, true)
:elixir_errors.print_diagnostic(diagnostic, read_snippet?)
@spec print_diagnostic(diagnostic(:warning | :error)) :: :ok
def print_diagnostic(diagnostic) do
:elixir_errors.print_diagnostic(diagnostic)
:ok
end
@@ -659,17 +605,15 @@ defmodule Code do
## Options
Regular options (do not change the AST):
* `:file` - the file which contains the string, used for error
reporting
* `:line` - the line the string starts, used for error reporting
* `:line_length` - the line length to aim for when formatting
the document. Defaults to 98. This value indicates when an expression
should be broken over multiple lines but it is not guaranteed
to do so. See the "Line length" section below for more information
the document. Defaults to 98. Note this value is used as
guideline but there are situations where it is not enforced.
See the "Line length" section below for more information
* `:locals_without_parens` - a keyword list of name and arity
pairs that should be kept without parens whenever possible.
@@ -684,34 +628,28 @@ defmodule Code do
If you set it to `false` later on, `do`-`end` blocks won't be
converted back to keywords.
Migration options (change the AST), see the "Migration formatting" section below:
* `:migrate` (since v1.18.0) - when `true`, sets all other migration options
to `true` by default. Defaults to `false`.
* `:migrate_bitstring_modifiers` (since v1.18.0) - when `true`,
* `:normalize_bitstring_modifiers` (since v1.14.0) - when `true`,
removes unnecessary parentheses in known bitstring
[modifiers](`<<>>/1`), for example `<<foo::binary()>>`
becomes `<<foo::binary>>`, or adds parentheses for custom
modifiers, where `<<foo::custom_type>>` becomes `<<foo::custom_type()>>`.
Defaults to the value of the `:migrate` option. This option changes the AST.
Defaults to `true`. This option changes the AST.
* `:migrate_charlists_as_sigils` (since v1.18.0) - when `true`,
* `:normalize_charlists_as_sigils` (since v1.15.0) - when `true`,
formats charlists as [`~c`](`Kernel.sigil_c/2`) sigils, for example
`'foo'` becomes `~c"foo"`.
Defaults to the value of the `:migrate` option. This option changes the AST.
* `:migrate_unless` (since v1.18.0) - when `true`,
rewrites `unless` expressions using `if` with a negated condition, for example
`unless foo, do:` becomes `if !foo, do:`.
Defaults to the value of the `:migrate` option. This option changes the AST.
Defaults to `true`. This option changes the AST.
## Design principles
The formatter was designed under three principles.
First, the formatter never changes the semantics of the code by default.
First, the formatter never changes the semantics of the code.
This means the input AST and the output AST are almost always equivalent.
The only cases where the formatter will change the AST is when the input AST
would cause *compiler warnings* and the output AST won't. The cases where
the formatter changes the AST can be disabled through formatting options
if desired.
The second principle is to provide as little configuration as possible.
This eases the formatter adoption by removing contention points while
@@ -722,8 +660,8 @@ defmodule Code do
specially because a function is named `defmodule`, `def`, or the like. This
principle mirrors Elixir's goal of being an extensible language where
developers can extend the language with new constructs as if they were
part of the language. When it is absolutely necessary to change behavior
based on the name, this behavior should be configurable, such as the
part of the language. When it is absolutely necessary to change behaviour
based on the name, this behaviour should be configurable, such as the
`:locals_without_parens` option.
## Running the formatter
@@ -806,10 +744,9 @@ defmodule Code do
## Line length
Another point about the formatter is that the `:line_length` configuration
indicates when an expression should be broken over multiple lines but it is
not guaranteed to do so. In many cases, it is not possible for the formatter
to break your code apart, which means it will go over the line length.
For example, if you have a long string:
is a guideline. In many cases, it is not possible for the formatter to break
your code apart, which means it will go over the line length. For example,
if you have a long string:
"this is a very long string that will go over the line length"
@@ -822,15 +759,15 @@ defmodule Code do
The string concatenation makes the code fit on a single line and also
gives more options to the formatter.
This may also appear in keywords such as do/end blocks and operators,
where the `do` keyword may go over the line length because there is no
opportunity for the formatter to introduce a line break in a readable way.
For example, if you do:
This may also appear in do/end blocks, where the `do` keyword (or `->`)
may go over the line length because there is no opportunity for the
formatter to introduce a line break in a readable way. For example,
if you do:
case very_long_expression() do
end
And only the `do` keyword is beyond the line length, Elixir **will not**
And only the `do` keyword is above the line length, Elixir **will not**
emit this:
case very_long_expression()
@@ -868,7 +805,7 @@ defmodule Code do
* Newlines before certain operators (such as the pipeline operators)
and before other operators (such as comparison operators)
The behaviors above are not guaranteed. We may remove or add new
The behaviours above are not guaranteed. We may remove or add new
rules in the future. The goal of documenting them is to provide better
understanding on what to expect from the formatter.
@@ -945,8 +882,9 @@ defmodule Code do
## Code comments
The formatter handles code comments and guarantees a space is always added
between the beginning of the comment (#) and the next character.
The formatter also handles code comments in a way to guarantee a space
is always added between the beginning of the comment (#) and the next
character.
The formatter also extracts all trailing comments to their previous line.
For example, the code below
@@ -958,19 +896,9 @@ defmodule Code do
# world
hello
While the formatter attempts to preserve comments in most situations,
that's not always possible, because code comments are handled apart from
the code representation (AST). While the formatter can preserve code
comments between expressions and function arguments, the formatter
cannot currently preserve them around operators. For example, the following
code will move the code comments to before the operator usage:
foo() ||
# also check for bar
bar()
In some situations, code comments can be seen as ambiguous by the formatter.
For example, the comment in the anonymous function below
Because code comments are handled apart from the code representation (AST),
there are some situations where code comments are seen as ambiguous by the
code formatter. For example, the comment in the anonymous function below
fn
arg1 ->
@@ -999,21 +927,6 @@ defmodule Code do
## Newlines
The formatter converts all newlines in code from `\r\n` to `\n`.
## Migration formatting
As part of the Elixir release cycle, deprecations are being introduced,
emitting warnings which might require existing code to be changed.
In order to reduce the burden on developers when upgrading Elixir to the
next version, the formatter exposes some options, disabled by default,
in order to automate this process.
These options should address most of the typical use cases, but given they
introduce changes to the AST, there is a non-zero risk for meta-programming
heavy projects that relied on a specific AST, or projects that are
re-defining functions from the `Kernel`. In such cases, migrations cannot
be applied blindly and some extra changes might be needed in order to
address the deprecation warnings.
"""
@doc since: "1.6.0"
@spec format_string!(binary, keyword) :: iodata
@@ -1023,9 +936,10 @@ defmodule Code do
to_quoted_opts =
[
unescape: false,
warn_on_unnecessary_quotes: false,
literal_encoder: &{:ok, {:__block__, &2, [&1]}},
token_metadata: true,
emit_warnings: false
warnings: false
] ++ opts
{forms, comments} = string_to_quoted_with_comments!(string, to_quoted_opts)
@@ -1107,8 +1021,6 @@ defmodule Code do
* `:file` - the file to be considered in the evaluation
* `:line` - the line on which the script starts
* `:module` - the module to run the environment on
"""
@doc since: "1.14.0"
def env_for_eval(env_or_opts), do: :elixir.env_for_eval(env_or_opts)
@@ -1132,8 +1044,6 @@ defmodule Code do
"""
@doc since: "1.14.0"
@spec eval_quoted_with_env(Macro.t(), binding, Macro.Env.t(), keyword) ::
{term, binding, Macro.Env.t()}
def eval_quoted_with_env(quoted, binding, %Macro.Env{} = env, opts \\ [])
when is_list(binding) do
eval_verify(:eval_quoted, [quoted, binding, env, opts])
@@ -1163,7 +1073,7 @@ defmodule Code do
For example, `"null byte\\t\\x00"` will be kept as is instead of being
converted to a bitstring literal. Note if you set this option to false, the
resulting AST is no longer valid, but it can be useful to analyze/transform
source code, typically in combination with `quoted_to_algebra/2`.
source code, typically in in combination with `quoted_to_algebra/2`.
Defaults to `true`.
* `:existing_atoms_only` - when `true`, raises an error
@@ -1184,12 +1094,13 @@ defmodule Code do
* `:static_atoms_encoder` - the static atom encoder function, see
"The `:static_atoms_encoder` function" section below. Note this
option overrides the `:existing_atoms_only` behavior for static
option overrides the `:existing_atoms_only` behaviour for static
atoms but `:existing_atoms_only` is still used for dynamic atoms,
such as atoms with interpolations.
* `:emit_warnings` (since v1.16.0) - when `false`, does not emit
tokenizing/parsing related warnings. Defaults to `true`.
* `:warn_on_unnecessary_quotes` - when `false`, does not warn
when atoms, keywords or calls have unnecessary quotes on
them. Defaults to `true`.
## `Macro.to_string/2`
@@ -1253,7 +1164,6 @@ defmodule Code do
It returns the AST if it succeeds,
raises an exception otherwise. The exception is a `TokenMissingError`
in case a token is missing (usually because the expression is incomplete),
`MismatchedDelimiterError` (in case of mismatched opening and closing delimiters) and
`SyntaxError` otherwise.
Check `string_to_quoted/2` for options information.
@@ -1277,7 +1187,7 @@ defmodule Code do
Comments are maps with the following fields:
* `:line` - The line number of the source code
* `:line` - The line number the source code
* `:text` - The full text of the comment, including the leading `#`
@@ -1562,8 +1472,8 @@ defmodule Code do
## Examples
Code.compiler_options(infer_signatures: false)
#=> %{infer_signatures: true}
Code.compiler_options(warnings_as_errors: true)
#=> %{warnings_as_errors: false}
"""
@spec compiler_options(Enumerable.t({atom, term})) :: %{optional(atom) => term}
@@ -1592,12 +1502,6 @@ defmodule Code do
:elixir_config.get(key)
end
# TODO: Remove me in Elixir v2.0
def get_compiler_option(:warnings_as_errors) do
IO.warn(":warnings_as_errors is deprecated as part of Code.get_compiler_option/1")
:ok
end
@doc """
Returns a list with all available compiler options.
@@ -1626,19 +1530,16 @@ defmodule Code do
Defaults to `true`.
* `:debug_info` - when `true`, retains debug information in the compiled
module. This option can also be overridden per module using the `@compile`
directive. Defaults to `true`.
This enables tooling to partially reconstruct the original source code,
for instance, to perform static analysis of code. Therefore, disabling
module. Defaults to `true`.
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.
Other environments, such as `mix test`, automatically disables this
via the `:test_elixirc_options` project configuration, as there is
typically no need to store debug chunks for test files.
Additionally, `mix test` disables it via the `:test_elixirc_options`
project configuration option.
This option can also be overridden per module using the `@compile` directive.
* `:ignore_already_consolidated` (since v1.10.0) - when `true`, does not warn
when a protocol has already been consolidated and a new implementation is added.
@@ -1647,19 +1548,13 @@ defmodule Code do
* `:ignore_module_conflict` - when `true`, does not warn when a module has
already been defined. Defaults to `false`.
* `:infer_signatures` (since v1.18.0) - when `false`, it disables module-local
signature inference used when type checking remote calls to the compiled
module. Type checking will be executed regardless of the value of this option.
Defaults to `true`.
`mix test` automatically disables this option via the `:test_elixirc_options`
project configuration, as there is typically no need to store infer signatures
for test files.
* `: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`.
* `:warnings_as_errors` - causes compilation to fail when warnings are
generated. Defaults to `false`.
* `:no_warn_undefined` (since v1.10.0) - list of modules and `{Mod, fun, arity}`
tuples that will not emit warnings that the module or function does not exist
at compilation time. Pass atom `:all` to skip warning for all undefined
@@ -1672,7 +1567,7 @@ defmodule Code do
to the parser when compiling files. It accepts the same options as
`string_to_quoted/2` (except by the options that change the AST itself).
This can be used in combination with the tracer to retrieve localized
information about events happening during compilation. Defaults to `[columns: true]`.
information about events happening during compilation. Defaults to `[]`.
This option only affects code compilation functions, such as `compile_string/2`
and `compile_file/2` but not `string_to_quoted/2` and friends, as the
latter is used for other purposes beyond compilation.
@@ -1682,9 +1577,8 @@ defmodule Code do
error. You may be set it to `:warn` if you want undefined variables to
emit a warning and expand as to a local call to the zero-arity function
of the same name (for example, `node` would be expanded as `node()`).
This `:warn` behavior only exists for compatibility reasons when working
with old dependencies, its usage is discouraged and it will be removed
in future releases.
This `:warn` behaviour only exists for compatibility reasons when working
with old dependencies.
It always returns `:ok`. Raises an error for invalid options.
@@ -1705,18 +1599,6 @@ defmodule Code do
:ok
end
# TODO: Remove me in Elixir v2.0
def put_compiler_option(:warnings_as_errors, _value) do
IO.warn(
":warnings_as_errors is deprecated as part of Code.put_compiler_option/2, " <>
"instead you must pass it as a --warnings-as-errors flag. " <>
"If you need to set it as a default in a mix task, you can also set it under aliases: " <>
"[compile: \"compile --warnings-as-errors\"]"
)
:ok
end
def put_compiler_option(:no_warn_undefined, value) do
if value != :all and not is_list(value) do
raise "compiler option :no_warn_undefined should be a list or the atom :all, " <>
@@ -1746,8 +1628,7 @@ defmodule Code do
:ok
end
# TODO: Remove this option on Elixir v2.0
# TODO: Warn if mode is :warn on Elixir v1.19
# TODO: Make this option have no effect on Elixir v2.0
def put_compiler_option(:on_undefined_variable, value) when value in [:raise, :warn] do
:elixir_config.put(:on_undefined_variable, value)
:ok
@@ -1786,7 +1667,7 @@ defmodule Code do
Returns a list of tuples where the first element is the module name
and the second one is its bytecode (as a binary). A `file` can be
given as a second argument which will be used for reporting warnings
given as second argument which will be used for reporting warnings
and errors.
**Warning**: `string` can be any Elixir code and code can be executed with
@@ -2030,7 +1911,7 @@ defmodule Code do
@doc """
Returns `true` if the module is loaded.
This function doesn't attempt to load the module. For such behavior,
This function doesn't attempt to load the module. For such behaviour,
`ensure_loaded?/1` can be used.
## Examples
@@ -2049,7 +1930,7 @@ defmodule Code do
end
@doc """
Returns `true` if the current process can await for module compilation.
Returns true if the current process can await for module compilation.
When compiling Elixir code via `Kernel.ParallelCompiler`, which is
used by Mix and `elixirc`, calling a module that has not yet been
@@ -2097,11 +1978,7 @@ defmodule Code do
@spec fetch_docs(module | String.t()) ::
{:docs_v1, annotation, beam_language, format, module_doc :: doc_content, metadata,
docs :: [doc_element]}
| {:error,
:module_not_found
| :chunk_not_found
| {:invalid_chunk, binary}
| :invalid_beam}
| {:error, :module_not_found | :chunk_not_found | {:invalid_chunk, binary}}
when annotation: :erl_anno.anno(),
beam_language: :elixir | :erlang | atom(),
doc_content: %{optional(binary) => binary} | :none | :hidden,
@@ -2152,8 +2029,7 @@ defmodule Code do
defp get_beam_and_path(module) do
with {^module, beam, filename} <- :code.get_object_code(module),
info_pairs when is_list(info_pairs) <- :beam_lib.info(beam),
{:ok, ^module} <- Keyword.fetch(info_pairs, :module) do
{:ok, ^module} <- beam |> :beam_lib.info() |> Keyword.fetch(:module) do
{beam, filename}
else
_ -> :error
@@ -2172,9 +2048,6 @@ defmodule Code do
{:error, :beam_lib, {:file_error, _, :enoent}} ->
{:error, :module_not_found}
{:error, :beam_lib, _} ->
{:error, :invalid_beam}
end
end
+100 -228
View File
@@ -6,8 +6,7 @@ defmodule Code.Formatter do
@double_heredoc "\"\"\""
@single_quote "'"
@single_heredoc "'''"
@sigil_c_double "~c\""
@sigil_c_single "~c'"
@sigil_c "~c\""
@sigil_c_heredoc "~c\"\"\""
@newlines 2
@min_line 0
@@ -16,7 +15,7 @@ defmodule Code.Formatter do
@ampersand_prec Code.Identifier.unary_op(:&) |> elem(1)
# Operators that are composed of multiple binary operators
@multi_binary_operators [:..//]
@multi_binary_operators [:"..//"]
# Operators that do not have space between operands
@no_space_binary_operators [:.., :"//"]
@@ -31,37 +30,30 @@ defmodule Code.Formatter do
@right_new_line_before_binary_operators [:|, :when]
# Operators that are logical cannot be mixed without parens
@required_parens_logical_binary_operands [:|||, :||, :or, :&&&, :&&, :and]
@required_parens_logical_binary_operands [:||, :|||, :or, :&&, :&&&, :and]
# Operators with next break fits
@next_break_fits_operators [:<-, :==, :!=, :=~, :===, :!==, :<, :>, :<=, :>=, :=, :"::"]
# Operators that always require parens even
# when they are their own parents as they are not semantically associative
@required_parens_even_when_parent [:--, :---]
# Operators that always require parens on operands
# when they are the parent of another operator with a difference precedence
# Most operators are listed, except comparison, arithmetic, and low precedence
# Operators that always require parens on operands when they are the parent
@required_parens_on_binary_operands [
:|>,
:<<<,
:>>>,
:|>,
:<~,
:~>,
:<<~,
:~>>,
:<~>,
:"<|>",
:in,
:"^^^",
:"//",
:++,
:--,
:+++,
:---,
:<>,
:..
:in,
:++,
:--,
:..,
:<>
]
@locals_without_parens [
@@ -189,10 +181,8 @@ defmodule Code.Formatter do
locals_without_parens = Keyword.get(opts, :locals_without_parens, [])
file = Keyword.get(opts, :file, nil)
sigils = Keyword.get(opts, :sigils, [])
migrate = Keyword.get(opts, :migrate, false)
migrate_bitstring_modifiers = Keyword.get(opts, :migrate_bitstring_modifiers, migrate)
migrate_charlists_as_sigils = Keyword.get(opts, :migrate_charlists_as_sigils, migrate)
migrate_unless = Keyword.get(opts, :migrate_unless, migrate)
normalize_bitstring_modifiers = Keyword.get(opts, :normalize_bitstring_modifiers, true)
normalize_charlists_as_sigils = Keyword.get(opts, :normalize_charlists_as_sigils, true)
syntax_colors = Keyword.get(opts, :syntax_colors, [])
sigils =
@@ -217,9 +207,8 @@ defmodule Code.Formatter do
comments: comments,
sigils: sigils,
file: file,
migrate_bitstring_modifiers: migrate_bitstring_modifiers,
migrate_charlists_as_sigils: migrate_charlists_as_sigils,
migrate_unless: migrate_unless,
normalize_bitstring_modifiers: normalize_bitstring_modifiers,
normalize_charlists_as_sigils: normalize_charlists_as_sigils,
inspect_opts: %Inspect.Opts{syntax_colors: syntax_colors}
}
end
@@ -276,7 +265,7 @@ defmodule Code.Formatter do
end
defp quoted_to_algebra({var, _meta, var_context}, _context, state) when is_atom(var_context) do
{var |> Atom.to_string() |> string() |> color_doc(:variable, state.inspect_opts), state}
{var |> Atom.to_string() |> string() |> color(:variable, state.inspect_opts), state}
end
defp quoted_to_algebra({:<<>>, meta, entries}, _context, state) do
@@ -300,7 +289,6 @@ defmodule Code.Formatter do
end
end
# TODO: Remove this clause on Elixir v2.0 once single-quoted charlists are removed
defp quoted_to_algebra(
{{:., _, [List, :to_charlist]}, meta, [entries]} = quoted,
context,
@@ -311,7 +299,7 @@ defmodule Code.Formatter do
remote_to_algebra(quoted, context, state)
meta[:delimiter] == ~s['''] ->
{opener, quotes} = get_charlist_quotes(:heredoc, state)
{opener, quotes} = get_charlist_quotes(true, state)
{doc, state} =
entries
@@ -321,7 +309,7 @@ defmodule Code.Formatter do
{force_unfit(doc), state}
true ->
{opener, quotes} = get_charlist_quotes({:regular, entries}, state)
{opener, quotes} = get_charlist_quotes(false, state)
list_interpolation_to_algebra(entries, quotes, state, opener, quotes)
end
end
@@ -377,22 +365,16 @@ defmodule Code.Formatter do
tuple_to_algebra(meta, [left, right], :flex_break, state)
end
# (left -> right)
defp quoted_to_algebra({:__block__, _, [[{:->, _, _} | _] = clauses]}, _context, state) do
paren_fun_to_algebra(clauses, @max_line, @min_line, state)
end
defp quoted_to_algebra({:__block__, meta, [list]}, _context, state) when is_list(list) do
case meta[:delimiter] do
~s['''] ->
{opener, quotes} = get_charlist_quotes(:heredoc, state)
{opener, quotes} = get_charlist_quotes(true, state)
string = list |> List.to_string() |> escape_heredoc(quotes)
{opener |> concat(string) |> concat(quotes) |> force_unfit(), state}
~s['] ->
string = list |> List.to_string()
{opener, quotes} = get_charlist_quotes({:regular, [string]}, state)
string = escape_string(string, quotes)
{opener, quotes} = get_charlist_quotes(false, state)
string = list |> List.to_string() |> escape_string(quotes)
{opener |> concat(string) |> concat(quotes), state}
_other ->
@@ -407,7 +389,7 @@ defmodule Code.Formatter do
{@double_heredoc
|> concat(string)
|> concat(@double_heredoc)
|> color_doc(:string, state.inspect_opts)
|> color(:string, state.inspect_opts)
|> force_unfit(), state}
else
string = escape_string(string, @double_quote)
@@ -415,7 +397,7 @@ defmodule Code.Formatter do
{@double_quote
|> concat(string)
|> concat(@double_quote)
|> color_doc(:string, state.inspect_opts), state}
|> color(:string, state.inspect_opts), state}
end
end
@@ -432,7 +414,6 @@ defmodule Code.Formatter do
{Keyword.fetch!(meta, :token) |> float_to_algebra(state.inspect_opts), state}
end
# (unquote_splicing(...))
defp quoted_to_algebra(
{:__block__, _meta, [{:unquote_splicing, meta, [_] = args}]},
context,
@@ -447,7 +428,7 @@ defmodule Code.Formatter do
end
defp quoted_to_algebra({:__block__, _meta, []}, _context, state) do
{color_doc("nil", nil, state.inspect_opts), state}
{color("nil", nil, state.inspect_opts), state}
end
defp quoted_to_algebra({:__block__, meta, args} = block, _context, state) when is_list(args) do
@@ -464,7 +445,7 @@ defmodule Code.Formatter do
end
{Enum.reduce(tail, doc, &concat(&2, "." <> Atom.to_string(&1)))
|> color_doc(:atom, state.inspect_opts), state}
|> color(:atom, state.inspect_opts), state}
end
# &1
@@ -487,53 +468,6 @@ defmodule Code.Formatter do
binary_op_to_algebra(:in, "not in", meta, left, right, context, state)
end
# disable migrate_unless within defmacro
defp quoted_to_algebra(
{atom, _, [{:unless, _, _}, _]} = ast,
context,
%{migrate_unless: true} = state
)
when atom in [:defmacro, :defmacrop] do
quoted_to_algebra(ast, context, %{state | migrate_unless: false})
end
# rewrite unless as if!
defp quoted_to_algebra(
{:unless, meta, [condition, block]},
context,
%{migrate_unless: true} = state
) do
quoted_to_algebra({:if, meta, [negate_condition(condition), block]}, context, state)
end
# a |> b() |> unless(...) => a |> b() |> Kernel.!() |> unless(...)
defp quoted_to_algebra(
{:|>, meta1, [{:|>, _, _} = condition, {:unless, meta2, [block]}]},
context,
%{migrate_unless: true} = state
) do
negated_condition = {:|>, [], [condition, {{:., [], [Kernel, :!]}, [closing: []], []}]}
quoted_to_algebra(
{:|>, meta1, [negated_condition, {:if, meta2, [block]}]},
context,
state
)
end
# condition |> unless(...) => negated(condition) |> unless(...)
defp quoted_to_algebra(
{:|>, meta1, [condition, {:unless, meta2, [block]}]},
context,
%{migrate_unless: true} = state
) do
quoted_to_algebra(
{:|>, meta1, [negate_condition(condition), {:if, meta2, [block]}]},
context,
state
)
end
# ..
defp quoted_to_algebra({:.., _meta, []}, context, state) do
if context in [:no_parens_arg, :no_parens_one_arg] do
@@ -543,13 +477,8 @@ defmodule Code.Formatter do
end
end
# ...
defp quoted_to_algebra({:..., _meta, []}, _context, state) do
{"...", state}
end
# 1..2//3
defp quoted_to_algebra({:..//, meta, [left, middle, right]}, context, state) do
defp quoted_to_algebra({:"..//", meta, [left, middle, right]}, context, state) do
quoted_to_algebra({:"//", meta, [{:.., meta, [left, middle]}, right]}, context, state)
end
@@ -568,6 +497,11 @@ defmodule Code.Formatter do
remote_to_algebra(quoted, context, state)
end
# (left -> right)
defp quoted_to_algebra([{:->, _, _} | _] = clauses, _context, state) do
paren_fun_to_algebra(clauses, @max_line, @min_line, state)
end
# [keyword: :list] (inner part)
# %{:foo => :bar} (inner part)
defp quoted_to_algebra(list, context, state) when is_list(list) do
@@ -581,12 +515,19 @@ defmodule Code.Formatter do
if keyword_key?(left_arg) do
{left, state} =
case left_arg do
{:__block__, _, [atom]} when is_atom(atom) ->
formatted = Macro.inspect_atom(:key, atom, escape: &escape_atom/2)
# TODO: Remove this clause in v1.16 when we no longer quote operator :..//
{:__block__, _, [:"..//"]} ->
{string(~S{"..//":}), state}
{formatted
|> string()
|> color_doc(:atom, state.inspect_opts), state}
{:__block__, _, [atom]} when is_atom(atom) ->
key =
if Macro.classify_atom(atom) in [:identifier, :unquoted] do
IO.iodata_to_binary([Atom.to_string(atom), ?:])
else
IO.iodata_to_binary([?", Atom.to_string(atom), ?", ?:])
end
{string(key) |> color(:atom, state.inspect_opts), state}
{{:., _, [:erlang, :binary_to_atom]}, _, [{:<<>>, _, entries}, :utf8]} ->
interpolation_to_algebra(entries, @double_quote, state, "\"", "\":")
@@ -610,11 +551,8 @@ defmodule Code.Formatter do
end
# #PID's and #Ref's may appear on regular AST
# Other foreign structures, such as maps and structs,
# may appear from Macro.to_string, so we stick a limit,
# although they won't be formatted accordingly.
defp quoted_to_algebra(unknown, _context, state) do
{inspect(unknown, printable_limit: :infinity), state}
{inspect(unknown), state}
end
## Blocks
@@ -684,12 +622,12 @@ defmodule Code.Formatter do
Atom.to_string(op)
end
{color_doc(op_string, :operator, state.inspect_opts) |> concat(doc), state}
{color(op_string, :operator, state.inspect_opts) |> concat(doc), state}
end
defp maybe_binary_op_to_algebra(fun, meta, args, context, state) do
with [left, right] <- args,
{_, _} <- augmented_binary_op(fun) do
{_, _} <- Code.Identifier.binary_op(fun) do
binary_op_to_algebra(fun, Atom.to_string(fun), meta, left, right, context, state)
else
_ -> :error
@@ -716,7 +654,7 @@ defmodule Code.Formatter do
defp binary_op_to_algebra(op, op_string, meta, left_arg, right_arg, context, state, _nesting)
when op in @right_new_line_before_binary_operators do
op_info = augmented_binary_op(op)
op_info = Code.Identifier.binary_op(op)
op_string = op_string <> " "
left_context = left_op_context(context)
right_context = right_op_context(context)
@@ -753,7 +691,7 @@ defmodule Code.Formatter do
defp binary_op_to_algebra(op, _, meta, left_arg, right_arg, context, state, _nesting)
when op in @pipeline_operators do
op_info = augmented_binary_op(op)
op_info = Code.Identifier.binary_op(op)
left_context = left_op_context(context)
right_context = right_op_context(context)
max_line = line(meta)
@@ -767,7 +705,7 @@ defmodule Code.Formatter do
{{doc, @empty, 1}, state}
{{op, context}, arg}, _args, state ->
op_info = augmented_binary_op(op)
op_info = Code.Identifier.binary_op(op)
op_string = Atom.to_string(op) <> " "
{doc, state} = binary_operand_to_algebra(arg, context, state, op, op_info, :right, 0)
{{concat(op_string, doc), @empty, 1}, state}
@@ -777,7 +715,7 @@ defmodule Code.Formatter do
end
defp binary_op_to_algebra(op, op_string, meta, left_arg, right_arg, context, state, nesting) do
op_info = augmented_binary_op(op)
op_info = Code.Identifier.binary_op(op)
left_context = left_op_context(context)
right_context = right_op_context(context)
@@ -790,11 +728,11 @@ defmodule Code.Formatter do
doc =
cond do
op in @no_space_binary_operators ->
op_doc = color_doc(op_string, :operator, state.inspect_opts)
op_doc = color(op_string, :operator, state.inspect_opts)
concat(concat(group(left), op_doc), group(right))
op in @no_newline_binary_operators ->
op_doc = color_doc(" " <> op_string <> " ", :operator, state.inspect_opts)
op_doc = color(" " <> op_string <> " ", :operator, state.inspect_opts)
concat(concat(group(left), op_doc), group(right))
true ->
@@ -804,7 +742,7 @@ defmodule Code.Formatter do
op in @next_break_fits_operators and next_break_fits?(right_arg, state) and not eol?
with_next_break_fits(next_break_fits?, right, fn right ->
op_doc = color_doc(" " <> op_string, :operator, state.inspect_opts)
op_doc = color(" " <> op_string, :operator, state.inspect_opts)
right = nest(glue(op_doc, group(right)), nesting, :break)
right = if eol?, do: force_unfit(right), else: right
concat(group(left), group(right))
@@ -836,18 +774,19 @@ defmodule Code.Formatter do
{parent_assoc, parent_prec} = parent_info
with {op, meta, [left, right]} <- operand,
op_info = augmented_binary_op(op),
op_info = Code.Identifier.binary_op(op),
{_assoc, prec} <- op_info do
op_string = Atom.to_string(op)
cond do
# If we have the same operator and it is in the correct side,
# we don't add parens unless it is explicitly required.
parent_assoc == side and op == parent_op and op not in @required_parens_even_when_parent ->
# If the operator has the same precedence as the parent and is on
# the correct side, we respect the nesting rule to avoid multiple
# nestings. This only applies for left associativity or same operator.
parent_prec == prec and parent_assoc == side and (side == :left or op == parent_op) ->
binary_op_to_algebra(op, op_string, meta, left, right, context, state, nesting)
# If the operator requires parens (most of them do) or we are mixing logical operators
# or the precedence is inverted or it is in the wrong side, then we *need* parenthesis.
# If the parent requires parens or the precedence is inverted or
# it is in the wrong side, then we *need* parenthesis.
(parent_op in @required_parens_on_binary_operands and op not in @no_space_binary_operators) or
(op in @required_parens_logical_binary_operands and
parent_op in @required_parens_logical_binary_operands) or parent_prec > prec or
@@ -1004,7 +943,7 @@ defmodule Code.Formatter do
)
when is_atom(fun) and is_integer(arity) do
{target_doc, state} = remote_target_to_algebra(target, state)
fun = Macro.inspect_atom(:remote_call, fun, escape: &escape_atom/2)
fun = Macro.inspect_atom(:remote_call, fun)
{target_doc |> nest(1) |> concat(string(".#{fun}/#{arity}")), state}
end
@@ -1051,9 +990,7 @@ defmodule Code.Formatter do
{target_doc, state} = remote_target_to_algebra(target, state)
fun_doc =
Macro.inspect_atom(:remote_call, fun, escape: &escape_atom/2)
|> string()
|> color_doc(:call, state.inspect_opts)
Macro.inspect_atom(:remote_call, fun) |> string() |> color(:call, state.inspect_opts)
remote_doc = target_doc |> concat(".") |> concat(fun_doc)
@@ -1121,7 +1058,7 @@ defmodule Code.Formatter do
fun
|> Atom.to_string()
|> string()
|> color_doc(:call, state.inspect_opts)
|> color(:call, state.inspect_opts)
|> concat(call_doc)
doc = if wrap_in_parens?, do: wrap_in_parens(doc), else: doc
@@ -1484,7 +1421,7 @@ defmodule Code.Formatter do
{doc, state} = quoted_to_algebra(segment, :parens_arg, state)
{spec, state} =
bitstring_spec_to_algebra(spec, state, state.migrate_bitstring_modifiers, :"::")
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
@@ -1562,8 +1499,8 @@ defmodule Code.Formatter do
{args_doc, _join, state} =
args_to_algebra_with_comments(args, meta, false, :none, join, state, fun)
left_bracket = color_doc("[", :list, state.inspect_opts)
right_bracket = color_doc("]", :list, state.inspect_opts)
left_bracket = color("[", :list, state.inspect_opts)
right_bracket = color("]", :list, state.inspect_opts)
{surround(left_bracket, args_doc, right_bracket), state}
end
@@ -1595,8 +1532,8 @@ defmodule Code.Formatter do
end
defp do_map_to_algebra(name_doc, args_doc, state) do
name_doc = "%" |> concat(name_doc) |> concat("{") |> color_doc(:map, state.inspect_opts)
{surround(name_doc, args_doc, color_doc("}", :map, state.inspect_opts)), state}
name_doc = "%" |> concat(name_doc) |> concat("{") |> color(:map, state.inspect_opts)
{surround(name_doc, args_doc, color("}", :map, state.inspect_opts)), state}
end
defp tuple_to_algebra(meta, args, join, state) do
@@ -1606,8 +1543,8 @@ defmodule Code.Formatter do
{args_doc, join, state} =
args_to_algebra_with_comments(args, meta, false, :none, join, state, fun)
left_bracket = color_doc("{", :tuple, state.inspect_opts)
right_bracket = color_doc("}", :tuple, state.inspect_opts)
left_bracket = color("{", :tuple, state.inspect_opts)
right_bracket = color("}", :tuple, state.inspect_opts)
if join == :flex_break do
{left_bracket |> concat(args_doc) |> nest(1) |> concat(right_bracket) |> group(), state}
@@ -1617,11 +1554,16 @@ defmodule Code.Formatter do
end
defp atom_to_algebra(atom, _, inspect_opts) when atom in [true, false] do
Atom.to_string(atom) |> color_doc(:boolean, inspect_opts)
Atom.to_string(atom) |> color(:boolean, inspect_opts)
end
defp atom_to_algebra(nil, _, inspect_opts) do
Atom.to_string(nil) |> color_doc(nil, inspect_opts)
Atom.to_string(nil) |> color(nil, inspect_opts)
end
# TODO: Remove this clause in v1.16 when we no longer quote operator :..//
defp atom_to_algebra(:"..//", _, inspect_opts) do
string(":\"..//\"") |> color(:atom, inspect_opts)
end
defp atom_to_algebra(:\\, meta, inspect_opts) do
@@ -1634,7 +1576,7 @@ defmodule Code.Formatter do
_ -> ":\\\\"
end
string(string) |> color_doc(:atom, inspect_opts)
string(string) |> color(:atom, inspect_opts)
end
defp atom_to_algebra(atom, _, inspect_opts) do
@@ -1647,7 +1589,7 @@ defmodule Code.Formatter do
[?:, ?", String.replace(string, "\"", "\\\""), ?"]
end
iodata |> IO.iodata_to_binary() |> string() |> color_doc(:atom, inspect_opts)
iodata |> IO.iodata_to_binary() |> string() |> color(:atom, inspect_opts)
end
defp integer_to_algebra(text, inspect_otps) do
@@ -1664,7 +1606,7 @@ defmodule Code.Formatter do
decimal ->
insert_underscores(decimal)
end
|> color_doc(:number, inspect_otps)
|> color(:number, inspect_otps)
end
defp float_to_algebra(text, inspect_otps) do
@@ -1672,7 +1614,7 @@ defmodule Code.Formatter do
decimal_part = String.downcase(decimal_part)
string = insert_underscores(int_part) <> "." <> decimal_part
color_doc(string, :number, inspect_otps)
color(string, :number, inspect_otps)
end
defp insert_underscores("-" <> digits) do
@@ -1680,30 +1622,25 @@ defmodule Code.Formatter do
end
defp insert_underscores(digits) do
byte_size = byte_size(digits)
cond do
digits =~ "_" ->
digits
byte_size >= 6 ->
offset = rem(byte_size, 3)
{prefix, rest} = String.split_at(digits, offset)
do_insert_underscores(prefix, rest)
byte_size(digits) >= 6 ->
digits
|> String.to_charlist()
|> Enum.reverse()
|> Enum.chunk_every(3)
|> Enum.intersperse(~c"_")
|> List.flatten()
|> Enum.reverse()
|> List.to_string()
true ->
digits
end
end
defp do_insert_underscores(acc, ""), do: acc
defp do_insert_underscores("", <<next::binary-3, rest::binary>>),
do: do_insert_underscores(next, rest)
defp do_insert_underscores(acc, <<next::binary-3, rest::binary>>),
do: do_insert_underscores(<<acc::binary, "_", next::binary>>, rest)
defp escape_heredoc(string, escape) do
string = String.replace(string, escape, "\\" <> escape)
heredoc_to_algebra(["" | String.split(string, "\n")])
@@ -1741,7 +1678,6 @@ defmodule Code.Formatter do
end
defp heredoc_line(["", _ | _]), do: nest(line(), :reset)
defp heredoc_line(["\r", _ | _]), do: nest(line(), :reset)
defp heredoc_line(_), do: line()
defp args_to_algebra_with_comments(args, meta, skip_parens?, last_arg_mode, join, state, fun) do
@@ -2014,14 +1950,6 @@ defmodule Code.Formatter do
# fn a, b, c when d -> e end
defp clause_args_to_algebra([{:when, meta, args}], state) do
{args, right} = split_last(args)
# If there are any keywords, wrap them in lists
args =
Enum.map(args, fn
[_ | _] = keyword -> {:__block__, [], [keyword]}
other -> other
end)
left = {{:special, :clause_args}, meta, [args]}
binary_op_to_algebra(:when, "when", meta, left, right, :no_parens_arg, state)
end
@@ -2251,15 +2179,10 @@ defmodule Code.Formatter do
unary_operator?(quoted) or binary_operator?(quoted)
end
# We convert ..// into two operators for simplicity,
# so we need to augment the binary table.
defp augmented_binary_op(:"//"), do: {:right, 190}
defp augmented_binary_op(op), do: Code.Identifier.binary_op(op)
defp binary_operator?(quoted) do
case quoted do
{op, _, [_, _, _]} when op in @multi_binary_operators -> true
{op, _, [_, _]} when is_atom(op) -> augmented_binary_op(op) != :error
{op, _, [_, _]} when is_atom(op) -> Code.Identifier.binary_op(op) != :error
_ -> false
end
end
@@ -2295,7 +2218,6 @@ defmodule Code.Formatter do
(not interpolated?(entries) and eol_or_comments?(meta, state))
end
# TODO: Remove this clause on Elixir v2.0 once single-quoted charlists are removed
defp next_break_fits?({{:., _, [List, :to_charlist]}, meta, [[_ | _]]}, _state) do
meta[:delimiter] == ~s[''']
end
@@ -2437,10 +2359,6 @@ defmodule Code.Formatter do
meta[:closing][:line] || @min_line
end
defp escape_atom(string, char) do
String.replace(string, <<char>>, <<?\\, char>>)
end
## Algebra helpers
# Relying on the inner document is brittle and error prone.
@@ -2485,65 +2403,19 @@ defmodule Code.Formatter do
{left, right}
end
defp get_charlist_quotes(:heredoc, state) do
if state.migrate_charlists_as_sigils do
defp get_charlist_quotes(_heredoc = false, state) do
if state.normalize_charlists_as_sigils do
{@sigil_c, @double_quote}
else
{@single_quote, @single_quote}
end
end
defp get_charlist_quotes(_heredoc = true, state) do
if state.normalize_charlists_as_sigils do
{@sigil_c_heredoc, @double_heredoc}
else
{@single_heredoc, @single_heredoc}
end
end
defp get_charlist_quotes({:regular, chunks}, state) do
cond do
!state.migrate_charlists_as_sigils -> {@single_quote, @single_quote}
Enum.any?(chunks, &has_double_quote?/1) -> {@sigil_c_single, @single_quote}
true -> {@sigil_c_double, @double_quote}
end
end
defp has_double_quote?(chunk) do
is_binary(chunk) and chunk =~ @double_quote
end
# Migration rewrites
@bool_operators [
:>,
:>=,
:<,
:<=,
:in
]
@guards [
:is_atom,
:is_boolean,
:is_nil,
:is_number,
:is_integer,
:is_float,
:is_binary,
:is_map,
:is_struct,
:is_non_struct_map,
:is_exception,
:is_list,
:is_tuple,
:is_function,
:is_reference,
:is_pid,
:is_port
]
defp negate_condition(condition) do
case condition do
{neg, _, [condition]} when neg in [:!, :not] -> condition
{op, _, [_, _]} when op in @bool_operators -> {:not, [], [condition]}
{guard, _, [_ | _]} when guard in @guards -> {:not, [], [condition]}
{:==, meta, [left, right]} -> {:!=, meta, [left, right]}
{:===, meta, [left, right]} -> {:!==, meta, [left, right]}
{:!=, meta, [left, right]} -> {:==, meta, [left, right]}
{:!==, meta, [left, right]} -> {:===, meta, [left, right]}
_ -> {:!, [], [condition]}
end
end
end
+42 -191
View File
@@ -31,7 +31,7 @@ defmodule Code.Fragment do
:expr
iex> Code.Fragment.cursor_context("hello_wor")
{:local_or_var, ~c"hello_wor"}
{:local_or_var, 'hello_wor'}
## Return values
@@ -78,9 +78,6 @@ defmodule Code.Fragment do
* `{:local_call, charlist}` - the context is a local (import or local)
call, such as `hello_world(` and `hello_world `
* `{:anonymous_call, inside_caller}` - the context is an anonymous
call, such as `fun.(` and `@fun.(`.
* `{:module_attribute, charlist}` - the context is a module attribute,
such as `@hello_wor`
@@ -143,7 +140,6 @@ defmodule Code.Fragment do
| {:local_or_var, charlist}
| {:local_arity, charlist}
| {:local_call, charlist}
| {:anonymous_call, inside_caller}
| {:module_attribute, charlist}
| {:operator, charlist}
| {:operator_arity, charlist}
@@ -168,8 +164,7 @@ defmodule Code.Fragment do
| {:alias, inside_alias, charlist}
| {:local_or_var, charlist}
| {:module_attribute, charlist}
| {:dot, inside_dot, charlist},
inside_caller: {:var, charlist} | {:module_attribute, charlist}
| {:dot, inside_dot, charlist}
def cursor_context(fragment, opts \\ [])
def cursor_context(fragment, opts)
@@ -247,22 +242,11 @@ defmodule Code.Fragment do
end
defp call_to_cursor_context({reverse, spaces}) do
with [?. | rest] <- reverse,
{rest, spaces} = strip_spaces(rest, spaces),
[h | _] when h not in @non_identifier <- rest do
case identifier_to_cursor_context(rest, spaces, true) do
{{:local_or_var, acc}, count} -> {{:anonymous_call, {:var, acc}}, count + 1}
{{:module_attribute, _} = attr, count} -> {{:anonymous_call, attr}, count + 1}
{_, _} -> {:none, 0}
end
else
_ ->
case identifier_to_cursor_context(reverse, spaces, true) do
{{:local_or_var, acc}, count} -> {{:local_call, acc}, count}
{{:dot, base, acc}, count} -> {{:dot_call, base, acc}, count}
{{:operator, acc}, count} -> {{:operator_call, acc}, count}
{_, _} -> {:none, 0}
end
case identifier_to_cursor_context(reverse, spaces, true) do
{{:local_or_var, acc}, count} -> {{:local_call, acc}, count}
{{:dot, base, acc}, count} -> {{:dot_call, base, acc}, count}
{{:operator, acc}, count} -> {{:operator_call, acc}, count}
{_, _} -> {:none, 0}
end
end
@@ -320,9 +304,6 @@ defmodule Code.Fragment do
_ ->
{{:local_or_var, acc}, count}
end
{:capture_arg, acc, count} ->
{{:capture_arg, acc}, count}
end
end
@@ -365,14 +346,6 @@ defmodule Code.Fragment do
:none
end
defp rest_identifier([?& | tail] = rest, count, acc) when tail == [] or hd(tail) != ?& do
if Enum.all?(acc, &(&1 in ?0..?9)) do
{:capture_arg, [?& | acc], count + 1}
else
tokenize_identifier(rest, count, acc)
end
end
defp rest_identifier(rest, count, acc) do
tokenize_identifier(rest, count, acc)
end
@@ -492,15 +465,15 @@ defmodule Code.Fragment do
defp operator(rest, count, acc, _call_op?) do
case :elixir_tokenizer.tokenize(acc, 1, 1, []) do
{:ok, _, _, _, [{:atom, _, _}], []} ->
{:ok, _, _, _, [{:atom, _, _}]} ->
{{:unquoted_atom, tl(acc)}, count}
{:ok, _, _, _, [{_, _, op}], []} ->
{:ok, _, _, _, [{_, _, op}]} ->
{rest, dot_count} = strip_spaces(rest, count)
cond do
Code.Identifier.unary_op(op) == :error and Code.Identifier.binary_op(op) == :error ->
{:none, 0}
:none
match?([?. | rest] when rest == [] or hd(rest) != ?., rest) ->
dot(tl(rest), dot_count + 1, acc)
@@ -558,7 +531,7 @@ defmodule Code.Fragment do
## Examples
iex> Code.Fragment.surround_context("foo", {1, 1})
%{begin: {1, 1}, context: {:local_or_var, ~c"foo"}, end: {1, 4}}
%{begin: {1, 1}, context: {:local_or_var, 'foo'}, end: {1, 4}}
## Differences to `cursor_context/2`
@@ -573,13 +546,13 @@ defmodule Code.Fragment do
`local_call`/`local_arity` and `local_or_var`, since the latter can
be a local or variable
* `@` when not followed by any identifier is returned as `{:operator, ~c"@"}`
(in contrast to `{:module_attribute, ~c""}` in `cursor_context/2`
* `@` when not followed by any identifier is returned as `{:operator, '@'}`
(in contrast to `{:module_attribute, ''}` in `cursor_context/2`
* This function never returns empty sigils `{:sigil, ~c""}` or empty structs
`{:struct, ~c""}` as context
* This function never returns empty sigils `{:sigil, ''}` or empty structs
`{:struct, ''}` as context
* This function returns keywords as `{:keyword, ~c"do"}`
* This function returns keywords as `{:keyword, 'do'}`
* This function never returns `:expr`
@@ -601,17 +574,14 @@ defmodule Code.Fragment do
| {:sigil, charlist}
| {:struct, inside_struct}
| {:unquoted_atom, charlist}
| {:keyword, charlist}
| {:key, charlist}
| {:capture_arg, charlist},
| {:keyword, charlist},
inside_dot:
{:alias, charlist}
| {:alias, inside_alias, charlist}
| {:dot, inside_dot, charlist}
| {:module_attribute, charlist}
| {:unquoted_atom, charlist}
| {:var, charlist}
| :expr,
| {:var, charlist},
inside_alias:
{:local_or_var, charlist}
| {:module_attribute, charlist},
@@ -649,24 +619,15 @@ defmodule Code.Fragment do
{reversed_pre, post} = adjust_position(reversed_pre, post)
case take_identifier(post, []) do
{_, [], _} ->
:none ->
maybe_operator(reversed_pre, post, line, opts)
{:identifier, reversed_post, rest} ->
{keyword_key?, rest} =
case rest do
[?: | tail] when tail == [] or hd(tail) in @space ->
{true, rest}
_ ->
{rest, _} = strip_spaces(rest, 0)
{false, rest}
end
{rest, _} = strip_spaces(rest, 0)
reversed = reversed_post ++ reversed_pre
case codepoint_cursor_context(reversed, opts) do
{{:struct, acc}, offset} ->
{{:struct, acc}, offset} when acc != [] ->
build_surround({:struct, acc}, reversed, line, offset)
{{:alias, acc}, offset} ->
@@ -678,9 +639,6 @@ defmodule Code.Fragment do
{{:dot, _, [_ | _]} = dot, offset} ->
build_surround(dot, reversed, line, offset)
{{:local_or_var, acc}, offset} when keyword_key? ->
build_surround({:key, acc}, reversed, line, offset)
{{:local_or_var, acc}, offset} when hd(rest) == ?( ->
build_surround({:local_call, acc}, reversed, line, offset)
@@ -708,9 +666,6 @@ defmodule Code.Fragment do
{{:unquoted_atom, acc}, offset} ->
build_surround({:unquoted_atom, acc}, reversed, line, offset)
{{:capture_arg, acc}, offset} ->
build_surround({:capture_arg, acc}, reversed, line, offset)
_ ->
maybe_operator(reversed_pre, post, line, opts)
end
@@ -743,16 +698,6 @@ defmodule Code.Fragment do
reversed = reversed_post ++ reversed_pre
case codepoint_cursor_context(reversed, opts) do
{{:operator, ~c"&"}, offset} when hd(rest) in ?0..?9 ->
arg = Enum.take_while(rest, &(&1 in ?0..?9))
build_surround(
{:capture_arg, ~c"&" ++ arg},
:lists.reverse(arg, reversed),
line,
offset + length(arg)
)
{{:operator, acc}, offset} ->
build_surround({:operator, acc}, reversed, line, offset)
@@ -787,11 +732,27 @@ defmodule Code.Fragment do
do: take_identifier(t, [h | acc])
defp take_identifier(rest, acc) do
with {[?. | t], _} <- strip_spaces(rest, 0),
{stripped, _} = strip_spaces(rest, 0)
with [?. | t] <- stripped,
{[h | _], _} when h in ?A..?Z <- strip_spaces(t, 0) do
take_alias(rest, acc)
else
_ -> {:identifier, acc, rest}
# Consider it an identifier if we are at the end of line
# or if we have spaces not followed by . (call) or / (arity)
_ when acc == [] and (rest == [] or (hd(rest) in @space and hd(stripped) not in ~c"/.")) ->
{:identifier, acc, rest}
# If we are immediately followed by a container, we are still part of the identifier.
# We don't consider << as it _may_ be an operator.
_ when acc == [] and hd(stripped) in ~c"({[" ->
{:identifier, acc, rest}
_ when acc == [] ->
:none
_ ->
{:identifier, acc, rest}
end
end
@@ -1099,13 +1060,6 @@ defmodule Code.Fragment do
iex> Code.Fragment.container_cursor_to_quoted("foo +")
{:ok, {:+, [line: 1], [{:foo, [line: 1], nil}, {:__cursor__, [line: 1], []}]}}
In order to parse the left-side of `->` properly, which appears both
in anonymous functions and do-end blocks, the trailing fragment option
must be given with the rest of the contents:
iex> Code.Fragment.container_cursor_to_quoted("fn x", trailing_fragment: " -> :ok end")
{:ok, {:fn, [line: 1], [{:->, [line: 1], [[{:__cursor__, [line: 1], []}], :ok]}]}}
## Options
* `:file` - the filename to be reported in case of parsing errors.
@@ -1128,117 +1082,14 @@ defmodule Code.Fragment do
* `:literal_encoder` - a function to encode literals in the AST.
See the documentation for `Code.string_to_quoted/2` for more information.
* `:trailing_fragment` (since v1.18.0) - the rest of the contents after
the cursor. This is necessary to correctly complete anonymous functions
and the left-hand side of `->`
"""
@doc since: "1.13.0"
@spec container_cursor_to_quoted(List.Chars.t(), keyword()) ::
{:ok, Macro.t()} | {:error, {location :: keyword, binary | {binary, binary}, binary}}
def container_cursor_to_quoted(fragment, opts \\ []) do
{trailing_fragment, opts} = Keyword.pop(opts, :trailing_fragment)
opts = Keyword.take(opts, [:columns, :token_metadata, :literal_encoder])
opts = [check_terminators: {:cursor, []}, emit_warnings: false] ++ opts
opts =
Keyword.take(opts, [:file, :line, :column, :columns, :token_metadata, :literal_encoder])
file = Keyword.get(opts, :file, "nofile")
line = Keyword.get(opts, :line, 1)
column = Keyword.get(opts, :column, 1)
case :elixir_tokenizer.tokenize(to_charlist(fragment), line, column, opts) do
{:ok, line, column, _warnings, rev_tokens, rev_terminators}
when trailing_fragment == nil ->
{rev_tokens, rev_terminators} =
with [close, open, {_, _, :__cursor__} = cursor | rev_tokens] <- rev_tokens,
{_, [_ | after_fn]} <- Enum.split_while(rev_terminators, &(elem(&1, 0) != :fn)),
true <- maybe_missing_stab?(rev_tokens, false),
[_ | rev_tokens] <- Enum.drop_while(rev_tokens, &(elem(&1, 0) != :fn)) do
{[close, open, cursor | rev_tokens], after_fn}
else
_ -> {rev_tokens, rev_terminators}
end
tokens = reverse_tokens(line, column, rev_tokens, rev_terminators)
:elixir.tokens_to_quoted(tokens, file, opts)
{:ok, line, column, _warnings, rev_tokens, rev_terminators} ->
tokens =
with {before_start, [_ | _] = after_start} <-
Enum.split_while(rev_terminators, &(elem(&1, 0) not in [:do, :fn])),
true <- maybe_missing_stab?(rev_tokens, true),
opts =
Keyword.put(opts, :check_terminators, {:cursor, before_start}),
{:error, {meta, _, ~c"end"}, _rest, _warnings, trailing_rev_tokens} <-
:elixir_tokenizer.tokenize(to_charlist(trailing_fragment), line, column, opts) do
trailing_tokens =
reverse_tokens(meta[:line], meta[:column], trailing_rev_tokens, after_start)
# If the cursor has its own line, then we do not trim new lines trailing tokens.
# Otherwise we want to drop any newline so we drop the next tokens after eol.
trailing_tokens =
case rev_tokens do
[_close, _open, {_, _, :__cursor__}, {:eol, _} | _] -> trailing_tokens
_ -> Enum.drop_while(trailing_tokens, &match?({:eol, _}, &1))
end
Enum.reverse(rev_tokens, drop_tokens(trailing_tokens, 0))
else
_ -> reverse_tokens(line, column, rev_tokens, rev_terminators)
end
:elixir.tokens_to_quoted(tokens, file, opts)
{:error, info, _rest, _warnings, _so_far} ->
{:error, :elixir.format_token_error(info)}
end
Code.string_to_quoted(fragment, [cursor_completion: true, warnings: false] ++ opts)
end
defp reverse_tokens(line, column, tokens, terminators) do
{terminators, _} =
Enum.map_reduce(terminators, column, fn {start, _, _}, column ->
atom = :elixir_tokenizer.terminator(start)
{{atom, {line, column, nil}}, column + length(Atom.to_charlist(atom))}
end)
Enum.reverse(tokens, terminators)
end
# Otherwise we drop all tokens, trying to build a minimal AST
# for cursor completion.
defp drop_tokens([{:"}", _} | _] = tokens, 0), do: tokens
defp drop_tokens([{:"]", _} | _] = tokens, 0), do: tokens
defp drop_tokens([{:")", _} | _] = tokens, 0), do: tokens
defp drop_tokens([{:">>", _} | _] = tokens, 0), do: tokens
defp drop_tokens([{:end, _} | _] = tokens, 0), do: tokens
defp drop_tokens([{:",", _} | _] = tokens, 0), do: tokens
defp drop_tokens([{:";", _} | _] = tokens, 0), do: tokens
defp drop_tokens([{:eol, _} | _] = tokens, 0), do: tokens
defp drop_tokens([{:stab_op, _, :->} | _] = tokens, 0), do: tokens
defp drop_tokens([{:"}", _} | tokens], counter), do: drop_tokens(tokens, counter - 1)
defp drop_tokens([{:"]", _} | tokens], counter), do: drop_tokens(tokens, counter - 1)
defp drop_tokens([{:")", _} | tokens], counter), do: drop_tokens(tokens, counter - 1)
defp drop_tokens([{:">>", _} | tokens], counter), do: drop_tokens(tokens, counter - 1)
defp drop_tokens([{:end, _} | tokens], counter), do: drop_tokens(tokens, counter - 1)
defp drop_tokens([{:"{", _} | tokens], counter), do: drop_tokens(tokens, counter + 1)
defp drop_tokens([{:"[", _} | tokens], counter), do: drop_tokens(tokens, counter + 1)
defp drop_tokens([{:"(", _} | tokens], counter), do: drop_tokens(tokens, counter + 1)
defp drop_tokens([{:"<<", _} | tokens], counter), do: drop_tokens(tokens, counter + 1)
defp drop_tokens([{:fn, _} | tokens], counter), do: drop_tokens(tokens, counter + 1)
defp drop_tokens([{:do, _} | tokens], counter), do: drop_tokens(tokens, counter + 1)
defp drop_tokens([_ | tokens], counter), do: drop_tokens(tokens, counter)
defp drop_tokens([], 0), do: []
defp maybe_missing_stab?([{:after, _} | _], _stab_choice?), do: true
defp maybe_missing_stab?([{:do, _} | _], _stab_choice?), do: true
defp maybe_missing_stab?([{:fn, _} | _], _stab_choice?), do: true
defp maybe_missing_stab?([{:else, _} | _], _stab_choice?), do: true
defp maybe_missing_stab?([{:catch, _} | _], _stab_choice?), do: true
defp maybe_missing_stab?([{:rescue, _} | _], _stab_choice?), do: true
defp maybe_missing_stab?([{:stab_op, _, :->} | _], stab_choice?), do: stab_choice?
defp maybe_missing_stab?([_ | tail], stab_choice?), do: maybe_missing_stab?(tail, stab_choice?)
defp maybe_missing_stab?([], _stab_choice?), do: false
end
+31 -62
View File
@@ -13,7 +13,7 @@ defmodule Code.Identifier do
@spec unary_op(atom) :: {:non_associative, precedence :: pos_integer} | :error
def unary_op(op) do
cond do
op in [:&, :...] -> {:non_associative, 90}
op in [:&] -> {:non_associative, 90}
op in [:!, :^, :not, :+, :-, :"~~~"] -> {:non_associative, 300}
op in [:@] -> {:non_associative, 320}
true -> :error
@@ -44,6 +44,7 @@ defmodule Code.Identifier do
op in [:|>, :<<<, :>>>, :<~, :~>, :<<~, :~>>, :<~>, :"<|>"] -> {:left, 160}
op in [:in] -> {:left, 170}
op in [:"^^^"] -> {:left, 180}
op in [:"//"] -> {:right, 190}
op in [:++, :--, :.., :<>, :+++, :---] -> {:right, 200}
op in [:+, :-] -> {:left, 210}
op in [:*, :/] -> {:left, 220}
@@ -71,11 +72,11 @@ defmodule Code.Identifier do
Escapes the given identifier.
"""
@spec escape(binary(), char() | nil, :infinity | non_neg_integer, (char() -> iolist() | false)) ::
{escaped :: binary(), remaining :: binary()}
{escaped :: iolist(), remaining :: binary()}
def escape(binary, char, limit \\ :infinity, fun \\ &escape_map/1)
when (is_binary(binary) and ((char in 0..0x10FFFF or is_nil(char)) and limit == :infinity)) or
when ((char in 0..0x10FFFF or is_nil(char)) and limit == :infinity) or
(is_integer(limit) and limit >= 0) do
escape(binary, char, limit, <<>>, fun)
escape(binary, char, limit, [], fun)
end
defp escape(<<_, _::binary>> = binary, _char, 0, acc, _fun) do
@@ -83,96 +84,64 @@ defmodule Code.Identifier do
end
defp escape(<<char, t::binary>>, char, count, acc, fun) do
escape(t, char, decrement(count), <<acc::binary, ?\\, char>>, fun)
escape(t, char, decrement(count), [acc | [?\\, char]], fun)
end
defp escape(<<?#, ?{, t::binary>>, char, count, acc, fun) do
escape(t, char, decrement(count), <<acc::binary, ?\\, ?#, ?{>>, fun)
escape(t, char, decrement(count), [acc | [?\\, ?#, ?{]], fun)
end
defp escape(<<h::utf8, t::binary>>, char, count, acc, fun) do
if value = fun.(h) do
value = IO.iodata_to_binary(value)
escape(t, char, decrement(count), <<acc::binary, value::binary>>, fun)
else
escape(t, char, decrement(count), escape_char(h, acc), fun)
end
escaped = if value = fun.(h), do: value, else: escape_char(h)
escape(t, char, decrement(count), [acc | escaped], fun)
end
defp escape(<<a::4, b::4, t::binary>>, char, count, acc, fun) do
escape(t, char, decrement(count), <<acc::binary, ?\\, ?x, to_hex(a), to_hex(b)>>, fun)
escape(t, char, decrement(count), [acc | [?\\, ?x, to_hex(a), to_hex(b)]], fun)
end
defp escape(<<>>, _char, _count, acc, _fun) do
{acc, <<>>}
end
defp escape_char(0, acc), do: <<acc::binary, ?\\, ?0>>
defp escape_char(0), do: [?\\, ?0]
defp escape_char(char, acc)
# Some characters that are confusing (zero-width / alternative spaces) are displayed
# using their unicode representation:
# https://en.wikipedia.org/wiki/Universal_Character_Set_characters#Special-purpose_characters
@escaped_bom :binary.bin_to_list("\\uFEFF")
defp escape_char(65279), do: @escaped_bom
# BOM
when char == 0xFEFF
# Mathematical invisibles
when char in 0x2061..0x2064
# Bidirectional neutral
when char in [0x061C, 0x200E, 0x200F]
# Bidirectional general (source of vulnerabilities)
when char in 0x202A..0x202E
when char in 0x2066..0x2069
# Interlinear annotations
when char in 0xFFF9..0xFFFC
# Zero-width joiners and non-joiners
when char in [0x200C, 0x200D, 0x034F]
# Non-break space / zero-width space
when char in [0x00A0, 0x200B, 0x2060]
# Line/paragraph separators
when char in [0x2028, 0x2029]
# Spaces
when char in 0x2000..0x200A
when char == 0x205F do
<<a::4, b::4, c::4, d::4>> = <<char::16>>
<<acc::binary, ?\\, ?u, to_hex(a), to_hex(b), to_hex(c), to_hex(d)>>
end
defp escape_char(char, acc)
defp escape_char(char)
when char in 0x20..0x7E
when char in 0xA0..0xD7FF
when char in 0xE000..0xFFFD
when char in 0x10000..0x10FFFF do
<<acc::binary, char::utf8>>
<<char::utf8>>
end
defp escape_char(char, acc) when char < 0x100 do
defp escape_char(char) when char < 0x100 do
<<a::4, b::4>> = <<char::8>>
<<acc::binary, ?\\, ?x, to_hex(a), to_hex(b)>>
[?\\, ?x, to_hex(a), to_hex(b)]
end
defp escape_char(char, acc) when char < 0x10000 do
defp escape_char(char) when char < 0x10000 do
<<a::4, b::4, c::4, d::4>> = <<char::16>>
<<acc::binary, ?\\, ?x, ?{, to_hex(a), to_hex(b), to_hex(c), to_hex(d), ?}>>
[?\\, ?x, ?{, to_hex(a), to_hex(b), to_hex(c), to_hex(d), ?}]
end
defp escape_char(char, acc) when char < 0x1000000 do
defp escape_char(char) when char < 0x1000000 do
<<a::4, b::4, c::4, d::4, e::4, f::4>> = <<char::24>>
<<acc::binary, ?\\, ?x, ?{, to_hex(a), to_hex(b), to_hex(c), to_hex(d), to_hex(e), to_hex(f),
?}>>
[?\\, ?x, ?{, to_hex(a), to_hex(b), to_hex(c), to_hex(d), to_hex(e), to_hex(f), ?}]
end
defp escape_map(?\a), do: "\\a"
defp escape_map(?\b), do: "\\b"
defp escape_map(?\d), do: "\\d"
defp escape_map(?\e), do: "\\e"
defp escape_map(?\f), do: "\\f"
defp escape_map(?\n), do: "\\n"
defp escape_map(?\r), do: "\\r"
defp escape_map(?\t), do: "\\t"
defp escape_map(?\v), do: "\\v"
defp escape_map(?\\), do: "\\\\"
defp escape_map(?\a), do: [?\\, ?a]
defp escape_map(?\b), do: [?\\, ?b]
defp escape_map(?\d), do: [?\\, ?d]
defp escape_map(?\e), do: [?\\, ?e]
defp escape_map(?\f), do: [?\\, ?f]
defp escape_map(?\n), do: [?\\, ?n]
defp escape_map(?\r), do: [?\\, ?r]
defp escape_map(?\t), do: [?\\, ?t]
defp escape_map(?\v), do: [?\\, ?v]
defp escape_map(?\\), do: [?\\, ?\\]
defp escape_map(_), do: false
@compile {:inline, to_hex: 1, decrement: 1}
+44 -52
View File
@@ -58,7 +58,7 @@ defmodule Code.Normalizer do
{:.., meta, [left, right]}
else
step = do_normalize(step, state)
{:..//, meta, [left, right, step]}
{:"..//", meta, [left, right, step]}
end
end
@@ -96,7 +96,6 @@ defmodule Code.Normalizer do
end
# Charlists with interpolations
# TODO: Remove this clause on Elixir v2.0 once single-quoted charlists are removed
defp do_normalize({{:., dot_meta, [List, :to_charlist]}, call_meta, [parts]} = quoted, state) do
if list_interpolated?(parts) do
parts =
@@ -126,17 +125,22 @@ defmodule Code.Normalizer do
{:., meta, [Access, :get]}
end
# Only normalize the left side of the dot operator
# The right hand side is an atom in the AST but it's not an atom literal, so
# it should not be wrapped. However, it should be escaped if applicable.
defp do_normalize({:., meta, [left, right]}, state) when is_atom(right) do
# it should not be wrapped
defp do_normalize({:., meta, [left, right]}, state) do
meta = patch_meta_line(meta, state.parent_meta)
left = do_normalize(left, %{state | parent_meta: meta})
right = maybe_escape_literal(right, state)
{:., meta, [left, right]}
end
# A list of left to right arrows is not considered as a list literal, so it's not wrapped
defp do_normalize([{:->, _, [_ | _]} | _] = quoted, state) do
normalize_args(quoted, state)
end
# left -> right
defp do_normalize({:->, meta, [left, right]}, state) do
meta = patch_meta_line(meta, state.parent_meta)
@@ -165,6 +169,9 @@ defmodule Code.Normalizer do
right = normalize_map_args(right, state)
[{:|, pipe_meta, [left, right]}]
[{_, _, _} = call] ->
[do_normalize(call, state)]
args ->
normalize_map_args(args, state)
end
@@ -174,17 +181,16 @@ defmodule Code.Normalizer do
# Sigils
defp do_normalize({sigil, meta, [{:<<>>, _, args} = string, modifiers]} = quoted, state)
when is_atom(sigil) and is_list(args) and is_list(modifiers) do
with "sigil_" <> _ <- Atom.to_string(sigil),
true <- binary_interpolated?(args),
true <- List.ascii_printable?(modifiers) do
meta =
meta
|> patch_meta_line(state.parent_meta)
|> Keyword.put_new(:delimiter, "\"")
when is_list(args) and is_atom(sigil) do
case Atom.to_string(sigil) do
"sigil_" <> _ ->
meta =
meta
|> patch_meta_line(state.parent_meta)
|> Keyword.put_new(:delimiter, "\"")
{sigil, meta, [do_normalize(string, %{state | parent_meta: meta}), modifiers]}
{sigil, meta, [do_normalize(string, %{state | parent_meta: meta}), modifiers]}
else
_ ->
normalize_call(quoted, state)
end
@@ -289,18 +295,21 @@ defmodule Code.Normalizer do
# Lists
defp normalize_literal(list, meta, state) when is_list(list) do
if list != [] and List.ascii_printable?(list) do
# It's a charlist, we normalize it as a ~C sigil
string =
# It's a charlist
list =
if state.escape do
{iolist, _} = Code.Identifier.escape(IO.chardata_to_string(list), nil)
IO.iodata_to_binary(iolist)
{string, _} = Code.Identifier.escape(IO.chardata_to_string(list), nil)
IO.iodata_to_binary(string) |> to_charlist()
else
List.to_string(list)
list
end
meta = patch_meta_line([delimiter: "\""], state.parent_meta)
meta =
meta
|> Keyword.put_new(:delimiter, "'")
|> patch_meta_line(state.parent_meta)
{:sigil_c, meta, [{:<<>>, [], [string]}, []]}
{:__block__, meta, [list]}
else
meta =
if line = state.parent_meta[:line] do
@@ -340,44 +349,29 @@ defmodule Code.Normalizer do
meta
end
last = List.last(args)
cond do
not allow_keyword?(form, arity) ->
args = normalize_args(args, %{state | parent_meta: meta})
{form, meta, args}
Keyword.has_key?(meta, :do) ->
Keyword.has_key?(meta, :do) or match?([{{:__block__, _, [:do]}, _} | _], List.last(args)) ->
# def foo do :ok end
# def foo, do: :ok
normalize_kw_blocks(form, meta, args, state)
match?([{:do, _} | _], last) and Keyword.keyword?(last) ->
match?([{:do, _} | _], List.last(args)) ->
# Non normalized kw blocks
line = state.parent_meta[:line] || meta[:line]
line = state.parent_meta[:line]
meta = meta ++ [do: [line: line], end: [line: line]]
normalize_kw_blocks(form, meta, args, state)
true ->
allow_keyword?(form, arity) ->
args = normalize_args(args, %{state | parent_meta: meta})
{last_arg, leading_args} = List.pop_at(args, -1, [])
last_args =
case last_arg do
{:__block__, _meta, [[{{:__block__, key_meta, _}, _} | _] = keyword]} ->
cond do
key_meta[:format] == :keyword ->
[keyword]
block_keyword?(keyword) ->
[
Enum.map(keyword, fn {{:__block__, meta, args}, value} ->
{{:__block__, [format: :keyword] ++ meta, args}, value}
end)
]
true ->
[last_arg]
{:__block__, _, [[{{:__block__, key_meta, _}, _} | _]] = last_args} ->
if key_meta[:format] == :keyword do
last_args
else
[last_arg]
end
[] ->
@@ -388,15 +382,13 @@ defmodule Code.Normalizer do
end
{form, meta, leading_args ++ last_args}
true ->
args = normalize_args(args, %{state | parent_meta: meta})
{form, meta, args}
end
end
defp block_keyword?([{{:__block__, _, [key]}, _val} | tail]) when is_atom(key),
do: block_keyword?(tail)
defp block_keyword?([]), do: true
defp block_keyword?(_), do: false
defp allow_keyword?(:when, 2), do: true
defp allow_keyword?(:{}, _), do: false
defp allow_keyword?(op, arity), do: not is_atom(op) or not Macro.operator?(op, arity)
+15 -18
View File
@@ -27,6 +27,11 @@ defmodule Code.Typespec do
end
end
def spec_to_quoted(name, {:type, anno, :fun, []}) when is_atom(name) do
meta = meta(anno)
{:"::", meta, [{name, meta, []}, quote(do: term)]}
end
def spec_to_quoted(name, {:type, anno, :bounded_fun, [type, constrs]}) when is_atom(name) do
meta = meta(anno)
{:type, _, :fun, [{:type, _, :product, args}, result]} = type
@@ -116,7 +121,7 @@ defmodule Code.Typespec do
located by the runtime system. The types will be in the Erlang
Abstract Format.
"""
@spec fetch_specs(module | binary) :: {:ok, [tuple]} | :error
@spec fetch_specs(module) :: {:ok, [tuple]} | :error
def fetch_specs(module) when is_atom(module) or is_binary(module) do
case typespecs_abstract_code(module) do
{:ok, abstract_code} ->
@@ -137,7 +142,7 @@ defmodule Code.Typespec do
which can be located by the runtime system. The types will be
in the Erlang Abstract Format.
"""
@spec fetch_callbacks(module | binary) :: {:ok, [tuple]} | :error
@spec fetch_callbacks(module) :: {:ok, [tuple]} | :error
def fetch_callbacks(module) when is_atom(module) or is_binary(module) do
case typespecs_abstract_code(module) do
{:ok, abstract_code} ->
@@ -170,8 +175,7 @@ defmodule Code.Typespec do
defp get_module_and_beam(module) when is_atom(module) do
with {^module, beam, _filename} <- :code.get_object_code(module),
info_pairs when is_list(info_pairs) <- :beam_lib.info(beam),
{:ok, ^module} <- Keyword.fetch(info_pairs, :module) do
{:ok, ^module} <- beam |> :beam_lib.info() |> Keyword.fetch(:module) do
{module, beam}
else
_ -> :error
@@ -283,6 +287,7 @@ defmodule Code.Typespec do
end
defp typespec_to_quoted({:type, anno, :binary, [arg1, arg2]}) do
[arg1, arg2] = for arg <- [arg1, arg2], do: typespec_to_quoted(arg)
line = meta(anno)[:line]
case {typespec_to_quoted(arg1), typespec_to_quoted(arg2)} do
@@ -311,6 +316,10 @@ defmodule Code.Typespec do
[{:->, meta(anno), [[typespec_to_quoted(args)], typespec_to_quoted(result)]}]
end
defp typespec_to_quoted({:type, anno, :fun, []}) do
typespec_to_quoted({:type, anno, :fun, [{:type, anno, :any}, {:type, anno, :any, []}]})
end
defp typespec_to_quoted({:type, anno, :range, [left, right]}) do
{:.., meta(anno), [typespec_to_quoted(left), typespec_to_quoted(right)]}
end
@@ -328,14 +337,10 @@ defmodule Code.Typespec do
{erl_to_ex_var(var), meta(anno), nil}
end
defp typespec_to_quoted({:op, anno, op, arg}) when op in [:+, :-] do
defp typespec_to_quoted({:op, anno, op, arg}) do
{op, meta(anno), [typespec_to_quoted(arg)]}
end
defp typespec_to_quoted({:op, anno, :*, arg1, arg2}) do
{:*, meta(anno), [typespec_to_quoted(arg1), typespec_to_quoted(arg2)]}
end
defp typespec_to_quoted({:remote_type, anno, [mod, name, args]}) do
remote_type(anno, mod, name, args)
end
@@ -414,13 +419,5 @@ defmodule Code.Typespec do
:error
end
defp meta(anno) do
case :erl_anno.location(anno) do
{line, column} ->
[line: line, column: column]
line when is_integer(line) ->
[line: line]
end
end
defp meta(anno), do: [line: :erl_anno.line(anno)]
end
+2 -14
View File
@@ -94,10 +94,10 @@ end
defimpl Collectable, for: List do
def into(list) do
# TODO: Change the behavior so the into always comes last on Elixir v2.0
# TODO: Change the behaviour so the into always comes last on Elixir v2.0
if list != [] do
IO.warn(
"the Collectable protocol is deprecated for non-empty lists. The behavior of " <>
"the Collectable protocol is deprecated for non-empty lists. The behaviour of " <>
"Enum.into/2 and \"for\" comprehensions with an :into option is incorrect " <>
"when collecting into non-empty lists. If you're collecting into a non-empty keyword " <>
"list, consider using Keyword.merge/2 instead. If you're collecting into a non-empty " <>
@@ -140,10 +140,6 @@ defimpl Collectable, for: BitString do
__acc, :halt ->
:ok
_acc, {:cont, other} ->
raise ArgumentError,
"collecting into a binary requires a bitstring, got: #{inspect(other)}"
end
{[binary], fun}
@@ -159,10 +155,6 @@ defimpl Collectable, for: BitString do
_acc, :halt ->
:ok
_acc, {:cont, other} ->
raise ArgumentError,
"collecting into a bitstring requires a bitstring, got: #{inspect(other)}"
end
{bitstring, fun}
@@ -180,10 +172,6 @@ defimpl Collectable, for: Map do
_map_acc, :halt ->
:ok
_map_acc, {:cont, other} ->
raise ArgumentError,
"collecting into a map requires {key, value} tuples, got: #{inspect(other)}"
end
{map, fun}
+8 -34
View File
@@ -41,15 +41,14 @@ defmodule Config do
> application environment is effectively a global storage. Also note that
> the `config/config.exs` of a library is not evaluated when the library is
> used as a dependency, as configuration is always meant to configure the
> current project. For more information, see ["Using application configuration for
> libraries"](design-anti-patterns.md#using-application-configuration-for-libraries).
> current project. For more information, read our [library guidelines](library-guidelines.md).
## Migrating from `use Mix.Config`
The `Config` module in Elixir was introduced in v1.9 as a replacement to
`use Mix.Config`, which was specific to Mix and has been deprecated.
`Mix.Config`, which was specific to Mix and has been deprecated.
You can leverage `Config` instead of `use Mix.Config` in three steps. The first
You can leverage `Config` instead of `Mix.Config` in three steps. The first
step is to replace `use Mix.Config` at the top of your config files by
`import Config`.
@@ -83,11 +82,11 @@ defmodule Config do
...
end
The only places where you may access functions from the `Mix` module are
the `mix.exs` file and inside custom Mix tasks, which are always within
the `Mix.Tasks` namespace.
The only files where you may access functions from the `Mix` module are
the `mix.exs` file and inside custom Mix tasks, which always within the
`Mix.Tasks` namespace.
## `config/runtime.exs`
## config/runtime.exs
For runtime configuration, you can use the `config/runtime.exs` file.
It is executed right before applications start in both Mix and releases
@@ -144,7 +143,7 @@ defmodule Config do
"""
@doc since: "1.9.0"
def config(root_key, opts) when is_atom(root_key) and is_list(opts) do
if not Keyword.keyword?(opts) do
unless Keyword.keyword?(opts) do
raise ArgumentError, "config/2 expected a keyword list, got: #{inspect(opts)}"
end
@@ -195,31 +194,6 @@ defmodule Config do
|> put_config()
end
@doc """
Reads the configuration for the given root key.
This function only reads the configuration from a previous
`config/2` or `config/3` call. If `root_key` points to an
application, it does not read its actual application environment.
Its main use case is to make it easier to access and share
configuration values across files.
If the `root_key` was not configured, it returns `nil`.
## Examples
# In config/config.exs
config :my_app, foo: :bar
# In config/dev.exs
config :another_app, foo: read_config(:my_app)[:foo] || raise "missing parent configuration"
"""
@doc since: "1.18.0"
def read_config(root_key) when is_atom(root_key) do
get_config!()[root_key]
end
@doc """
Returns the environment this configuration file is executed on.
+1 -1
View File
@@ -312,7 +312,7 @@ defmodule Config.Provider do
"""
the application #{inspect(app)} has a different value set #{path(key, path)} \
during runtime compared to compile time. Since this application environment entry was \
marked as compile time, this difference can lead to different behavior than expected:
marked as compile time, this difference can lead to different behaviour than expected:
* Compile time value #{return_to_text(compile_return)}
* Runtime value #{return_to_text(runtime_return)}
+16 -12
View File
@@ -215,7 +215,10 @@ defmodule DynamicSupervisor do
extra_arguments: [term()]
}
@typedoc "Options given to `start_link/1` and `init/1` functions"
@typedoc "Options given to `start_link` functions"
@type option :: GenServer.option()
@typedoc "Options given to `start_link` and `init/1` functions"
@type init_option ::
{:strategy, strategy()}
| {:max_restarts, non_neg_integer()}
@@ -274,7 +277,7 @@ defmodule DynamicSupervisor do
defmacro __using__(opts) do
quote location: :keep, bind_quoted: [opts: opts] do
@behaviour DynamicSupervisor
if not Module.has_attribute?(__MODULE__, :doc) do
unless Module.has_attribute?(__MODULE__, :doc) do
@doc """
Returns a specification to start this module under a supervisor.
@@ -342,11 +345,9 @@ defmodule DynamicSupervisor do
specified in the child spec given to `start_child/2`. Defaults to
an empty list.
* Any of the standard [GenServer options](`t:GenServer.option/0`)
"""
@doc since: "1.6.0"
@spec start_link([init_option | GenServer.option()]) :: Supervisor.on_start()
@spec start_link([option | init_option]) :: Supervisor.on_start()
def start_link(options) when is_list(options) do
keys = [:extra_arguments, :max_children, :max_seconds, :max_restarts, :strategy]
{sup_opts, start_opts} = Keyword.split(options, keys)
@@ -380,15 +381,9 @@ defmodule DynamicSupervisor do
Note that a supervisor started with this function is linked to the parent
process and exits not only on crashes but also if the parent process exits
with `:normal` reason.
## Options
This function accepts any regular [`GenServer` options](`t:GenServer.option/0`).
Options specific to `DynamicSupervisor` must be returned from the `c:init/1`
callback.
"""
@doc since: "1.6.0"
@spec start_link(module, term, [GenServer.option()]) :: Supervisor.on_start()
@spec start_link(module, term, [option]) :: Supervisor.on_start()
def start_link(module, init_arg, opts \\ []) do
GenServer.start_link(__MODULE__, {module, init_arg, opts[:name]}, opts)
end
@@ -1141,6 +1136,15 @@ defmodule DynamicSupervisor do
]
end
@impl true
def format_status(:terminate, [_pdict, state]) do
state
end
def format_status(_, [_pdict, %{mod: mod} = state]) do
[data: [{~c"State", state}], supervisor: [{~c"Callback", mod}]]
end
## Helpers
@compile {:inline, call: 2}
+146 -284
View File
@@ -80,7 +80,7 @@ defprotocol Enumerable do
Returns the accumulator for the next enumeration step.
"""
@type reducer :: (element :: term, element_acc :: term -> acc)
@type reducer :: (element :: term, current_acc :: acc -> updated_acc :: acc)
@typedoc """
The result of the reduce operation.
@@ -256,25 +256,22 @@ defmodule Enum do
iex> Enum.map(map, fn {k, v} -> {k, v * 2} end)
[{"a", 2}, {"b", 4}]
Many other enumerables exist in the language, such as `MapSet`s
However, many other enumerables exist in the language, such as `MapSet`s
and the data type returned by `File.stream!/3` which allows a file to be
traversed as if it was an enumerable.
For a general overview of all functions in the `Enum` module, see
[the `Enum` cheatsheet](enum-cheat.cheatmd).
The functions in this module work in linear time. This means that, the
time it takes to perform an operation grows at the same rate as the length
of the enumerable. This is expected on operations such as `Enum.map/2`.
After all, if we want to traverse every element on a list, the longer the
list, the more elements we need to traverse, and the longer it will take.
This linear behavior should also be expected on operations like `count/1`,
This linear behaviour should also be expected on operations like `count/1`,
`member?/2`, `at/2` and similar. While Elixir does allow data types to
provide performant variants for such operations, you should not expect it
to always be available, since the `Enum` module is meant to work with a
large variety of data types and not all data types can provide optimized
behavior.
behaviour.
Finally, note the functions in the `Enum` module are eager: they will
traverse the enumerable as soon as they are invoked. This is particularly
@@ -815,11 +812,11 @@ defmodule Enum do
@doc """
Enumerates the `enumerable`, returning a list where all consecutive
duplicate elements are collapsed to a single element.
duplicated elements are collapsed to a single element.
Elements are compared using `===/2`.
If you want to remove all duplicate elements, regardless of order,
If you want to remove all duplicated elements, regardless of order,
see `uniq/1`.
## Examples
@@ -837,7 +834,7 @@ defmodule Enum do
end
def dedup(enumerable) do
reduce(enumerable, [], fn x, acc ->
Enum.reduce(enumerable, [], fn x, acc ->
case acc do
[^x | _] -> acc
_ -> [x | acc]
@@ -848,7 +845,7 @@ defmodule Enum do
@doc """
Enumerates the `enumerable`, returning a list where all consecutive
duplicate elements are collapsed to a single element.
duplicated elements are collapsed to a single element.
The function `fun` maps every element to a term which is used to
determine if two elements are duplicates.
@@ -1096,10 +1093,6 @@ defmodule Enum do
iex> Enum.filter([1, 2, 3], fn x -> rem(x, 2) == 0 end)
[2]
iex> Enum.filter(["apple", "pear", "banana"], fn fruit -> String.contains?(fruit, "a") end)
["apple", "pear", "banana"]
iex> Enum.filter([4, 21, 24, 904], fn seconds -> seconds > 1000 end)
[]
Keep in mind that `filter` is not capable of filtering and
transforming an element at the same time. If you would like
@@ -1223,8 +1216,7 @@ defmodule Enum do
"no bools!"
"""
@spec find_value(t, default, (element -> found_value)) :: found_value | default
when found_value: term
@spec find_value(t, any, (element -> any)) :: any | nil
def find_value(enumerable, default \\ nil, fun)
def find_value(enumerable, default, fun) when is_list(enumerable) do
@@ -1257,14 +1249,6 @@ defmodule Enum do
iex> Enum.flat_map([:a, :b, :c], fn x -> [[x]] end)
[[:a], [:b], [:c]]
This is frequently used to to transform and filter in one pass, returning empty
lists to exclude results:
iex> Enum.flat_map([4, 0, 2, 0], fn x ->
...> if x != 0, do: [1 / x], else: []
...> end)
[0.25, 0.5]
"""
@spec flat_map(t, (element -> t)) :: list
def flat_map(enumerable, fun) when is_list(enumerable) do
@@ -1274,7 +1258,6 @@ defmodule Enum do
def flat_map(enumerable, fun) do
reduce(enumerable, [], fn entry, acc ->
case fun.(entry) do
[] -> acc
list when is_list(list) -> [list | acc]
other -> [to_list(other) | acc]
end
@@ -1282,8 +1265,6 @@ defmodule Enum do
|> flat_reverse([])
end
# the first clause is an optimization
defp flat_reverse([[elem] | t], acc), do: flat_reverse(t, [elem | acc])
defp flat_reverse([h | t], acc), do: flat_reverse(t, h ++ acc)
defp flat_reverse([], acc), do: acc
@@ -1443,7 +1424,7 @@ defmodule Enum do
)
# Avoid warnings about Dict
dict_module = String.to_atom("Dict")
dict_module = Dict
reduce(reverse(enumerable), dict, fn entry, categories ->
dict_module.update(categories, fun.(entry), [entry], &[entry | &1])
@@ -1508,9 +1489,6 @@ defmodule Enum do
iex> Enum.into([a: 1, a: 2], %{})
%{a: 2}
iex> Enum.into([a: 2], %{a: 1, b: 3})
%{a: 2, b: 3}
"""
@spec into(Enumerable.t(), Collectable.t()) :: Collectable.t()
def into(enumerable, collectable)
@@ -1543,13 +1521,14 @@ defmodule Enum do
defp into_map(enumerable) when is_list(enumerable), do: :maps.from_list(enumerable)
defp into_map(enumerable), do: enumerable |> Enum.to_list() |> :maps.from_list()
defp into_map(%{} = enumerable, collectable), do: Map.merge(collectable, enumerable)
defp into_map(%{} = enumerable, collectable),
do: Map.merge(collectable, enumerable)
defp into_map(enumerable, collectable) when is_list(enumerable),
do: Map.merge(collectable, :maps.from_list(enumerable))
defp into_map(enumerable, collectable),
do: reduce(enumerable, collectable, fn {key, val}, acc -> Map.put(acc, key, val) end)
do: Enum.reduce(enumerable, collectable, fn {key, val}, acc -> Map.put(acc, key, val) end)
defp into_protocol(enumerable, collectable) do
{initial, fun} = Collectable.into(collectable)
@@ -1592,7 +1571,7 @@ defmodule Enum do
"""
@spec into(Enumerable.t(), Collectable.t(), (term -> term)) :: Collectable.t()
def into(enumerable, [], transform) do
map(enumerable, transform)
Enum.map(enumerable, transform)
end
def into(%_{} = enumerable, collectable, transform) do
@@ -1605,9 +1584,9 @@ defmodule Enum do
def into(enumerable, %{} = collectable, transform) do
if map_size(collectable) == 0 do
enumerable |> map(transform) |> :maps.from_list()
enumerable |> Enum.map(transform) |> :maps.from_list()
else
reduce(enumerable, collectable, fn entry, acc ->
Enum.reduce(enumerable, collectable, fn entry, acc ->
{key, val} = transform.(entry)
Map.put(acc, key, val)
end)
@@ -1650,8 +1629,8 @@ defmodule Enum do
If `joiner` is not passed at all, it defaults to an empty string.
All elements in the `enumerable` must be convertible to a string
or be a binary, otherwise an error is raised.
All elements in the `enumerable` must be convertible to a string,
otherwise an error is raised.
## Examples
@@ -1665,7 +1644,7 @@ defmodule Enum do
"ab cdefg h i"
"""
@spec join(t, binary()) :: binary()
@spec join(t, String.t()) :: String.t()
def join(enumerable, joiner \\ "")
def join(enumerable, "") do
@@ -2376,6 +2355,12 @@ defmodule Enum do
the random value. Check its documentation for setting a
different random algorithm or a different seed.
The implementation is based on the
[reservoir sampling](https://en.wikipedia.org/wiki/Reservoir_sampling#Relation_to_Fisher-Yates_shuffle)
algorithm.
It assumes that the sample being returned can fit into memory;
the input `enumerable` doesn't have to, as it is traversed just once.
If a range is passed into the function, this function will pick a
random value between the range limits, without traversing the whole
range (thus executing in constant time and constant memory).
@@ -2394,12 +2379,6 @@ defmodule Enum do
iex> Enum.random(1..1_000)
309
## Implementation
The random functions in this module implement reservoir sampling,
which allows them to sample infinite collections. In particular,
we implement Algorithm L, as described in by Kim-Hung Li in
"Reservoir-Sampling Algorithms of Time Complexity O(n(1+log(N/n)))".
"""
@spec random(t) :: element
def random(enumerable)
@@ -2407,14 +2386,7 @@ defmodule Enum do
def random(enumerable) when is_list(enumerable) do
case length(enumerable) do
0 -> raise Enum.EmptyError
length -> enumerable |> drop_list(random_count(length)) |> hd()
end
end
def random(first.._//step = range) do
case Range.size(range) do
0 -> raise Enum.EmptyError
size -> first + random_count(size) * step
length -> enumerable |> drop_list(random_integer(0, length - 1)) |> hd()
end
end
@@ -2425,18 +2397,14 @@ defmodule Enum do
[]
{:ok, count, fun} when is_function(fun, 1) ->
slice_list(fun.(enumerable), random_count(count), 1, 1)
slice_list(fun.(enumerable), random_integer(0, count - 1), 1, 1)
# TODO: Deprecate me in Elixir v1.18.
{:ok, count, fun} when is_function(fun, 2) ->
fun.(random_integer(0, count - 1), 1)
{:ok, count, fun} when is_function(fun, 3) ->
fun.(random_count(count), 1, 1)
# TODO: Remove deprecation on Elixir v1.20.
{:ok, count, fun} when is_function(fun, 2) ->
IO.warn(
"#{inspect(Enumerable.impl_for(enumerable))} must return a three arity function on slice/1"
)
fun.(random_count(count), 1)
fun.(random_integer(0, count - 1), 1, 1)
{:error, _} ->
take_random(enumerable, 1)
@@ -2448,10 +2416,6 @@ defmodule Enum do
end
end
defp random_count(count) do
:rand.uniform(count) - 1
end
@doc """
Invokes `fun` for each element in the `enumerable` with the
accumulator.
@@ -2717,11 +2681,11 @@ defmodule Enum do
iex> Enum.slide([:a, :b, :c, :d, :e, :f, :g], 5, 1)
[:a, :f, :b, :c, :d, :e, :g]
# Slide a range of elements towards the head of the list
# Slide a range of elements backward
iex> Enum.slide([:a, :b, :c, :d, :e, :f, :g], 3..5, 1)
[:a, :d, :e, :f, :b, :c, :g]
# Slide a range of elements towards the tail of the list
# Slide a range of elements forward
iex> Enum.slide([:a, :b, :c, :d, :e, :f, :g], 1..3, 5)
[:a, :e, :f, :b, :c, :d, :g]
@@ -2751,7 +2715,7 @@ defmodule Enum do
end
# Normalize negative input ranges like Enum.slice/2
def slide(enumerable, first..last//_, insertion_index)
def slide(enumerable, first..last, insertion_index)
when first < 0 or last < 0 or insertion_index < 0 do
count = Enum.count(enumerable)
normalized_first = if first >= 0, do: first, else: Kernel.max(first + count, 0)
@@ -2768,23 +2732,23 @@ defmodule Enum do
end
end
def slide(enumerable, insertion_index.._//_, insertion_index) do
def slide(enumerable, insertion_index.._, insertion_index) do
Enum.to_list(enumerable)
end
def slide(_, first..last//_, insertion_index)
def slide(_, first..last, insertion_index)
when insertion_index > first and insertion_index <= last do
raise ArgumentError,
"insertion index for slide must be outside the range being moved " <>
"(tried to insert #{first}..#{last} at #{insertion_index})"
end
def slide(enumerable, first..last//_, _insertion_index) when first > last do
def slide(enumerable, first..last, _insertion_index) when first > last do
Enum.to_list(enumerable)
end
# Guarantees at this point: step size == 1 and first <= last and (insertion_index < first or insertion_index > last)
def slide(enumerable, first..last//_, insertion_index) do
def slide(enumerable, first..last, insertion_index) do
impl = if is_list(enumerable), do: &slide_list_start/4, else: &slide_any/4
cond do
@@ -2808,7 +2772,7 @@ defmodule Enum do
# Then at the end, we're going to reassemble and reverse them, and end up with the
# chunks in the correct order.
{_size, pre, post} =
reduce(enumerable, {0, [], []}, fn item, {index, pre, post} ->
Enum.reduce(enumerable, {0, [], []}, fn item, {index, pre, post} ->
{pre, post} =
cond do
index < start -> {[item | pre], post}
@@ -2920,11 +2884,11 @@ defmodule Enum do
the default from Erlang/OTP 22:
# Although not necessary, let's seed the random algorithm
iex> :rand.seed(:exsss, {11, 22, 33})
iex> :rand.seed(:exsss, {1, 2, 3})
iex> Enum.shuffle([1, 2, 3])
[3, 2, 1]
iex> Enum.shuffle([1, 2, 3])
[2, 1, 3]
iex> Enum.shuffle([1, 2, 3])
[2, 3, 1]
"""
@spec shuffle(t) :: list
@@ -2934,12 +2898,9 @@ defmodule Enum do
[{:rand.uniform(), x} | acc]
end)
shuffle_unwrap(:lists.keysort(1, randomized))
shuffle_unwrap(:lists.keysort(1, randomized), [])
end
defp shuffle_unwrap([{_, h} | rest]), do: [h | shuffle_unwrap(rest)]
defp shuffle_unwrap([]), do: []
@doc """
Returns a subset list of the given `enumerable` by `index_range`.
@@ -2999,23 +2960,20 @@ defmodule Enum do
[]
# first is greater than last
iex> Enum.slice([1, 2, 3, 4, 5], 6..5//1)
iex> Enum.slice([1, 2, 3, 4, 5], 6..5)
[]
"""
@doc since: "1.6.0"
@spec slice(t, Range.t()) :: list
def slice(enumerable, first..last//step = index_range) do
# TODO: Deprecate negative steps on Elixir v1.16
# TODO: Support negative steps as a reverse on Elixir v2.0.
cond do
step > 0 ->
slice_range(enumerable, first, last, step)
step == -1 and first > last ->
IO.warn(
"negative steps are not supported in Enum.slice/2, pass #{first}..#{last}//1 instead"
)
slice_range(enumerable, first, last, 1)
true ->
@@ -3353,7 +3311,7 @@ defmodule Enum do
def sort_by(enumerable, mapper, :desc) when is_function(mapper, 1) do
enumerable
|> reduce([], &[{&1, mapper.(&1)} | &2])
|> Enum.reduce([], &[{&1, mapper.(&1)} | &2])
|> List.keysort(1, :asc)
|> List.foldl([], &[elem(&1, 0) | &2])
end
@@ -3458,8 +3416,6 @@ defmodule Enum do
Raises `ArithmeticError` if `enumerable` contains a non-numeric value.
If you need to apply a transformation first, consider using `Enum.sum_by/2` instead.
## Examples
iex> Enum.sum([1, 2, 3])
@@ -3486,47 +3442,11 @@ defmodule Enum do
reduce(enumerable, 0, &+/2)
end
@doc """
Maps and sums the given `enumerable` in one pass.
Raises `ArithmeticError` if `mapper` returns a non-numeric value.
## Examples
iex> Enum.sum_by([%{count: 1}, %{count: 2}, %{count: 3}], fn x -> x.count end)
6
iex> Enum.sum_by(1..3, fn x -> x ** 2 end)
14
iex> Enum.sum_by([], fn x -> x.count end)
0
Filtering can be achieved by returning `0` to ignore elements:
iex> Enum.sum_by([1, -2, 3], fn x -> if x > 0, do: x, else: 0 end)
4
"""
@doc since: "1.18.0"
@spec sum_by(t, (element -> number)) :: number
def sum_by(enumerable, mapper)
def sum_by(list, mapper) when is_list(list) and is_function(mapper, 1) do
sum_by_list(list, mapper, 0)
end
def sum_by(enumerable, mapper) when is_function(mapper, 1) do
reduce(enumerable, 0, fn x, acc -> acc + mapper.(x) end)
end
@doc """
Returns the product of all elements.
Raises `ArithmeticError` if `enumerable` contains a non-numeric value.
If you need to apply a transformation first, consider using `Enum.product_by/2` instead.
## Examples
iex> Enum.product([])
@@ -3543,40 +3463,6 @@ defmodule Enum do
reduce(enumerable, 1, &*/2)
end
@doc """
Maps and computes the product of the given `enumerable` in one pass.
Raises `ArithmeticError` if `mapper` returns a non-numeric value.
## Examples
iex> Enum.product_by([%{count: 2}, %{count: 4}, %{count: 3}], fn x -> x.count end)
24
iex> Enum.product_by(1..3, fn x -> x ** 2 end)
36
iex> Enum.product_by([], fn x -> x.count end)
1
Filtering can be achieved by returning `1` to ignore elements:
iex> Enum.product_by([2, -1, 3], fn x -> if x > 0, do: x, else: 1 end)
6
"""
@doc since: "1.18.0"
@spec product_by(t, (element -> number)) :: number
def product_by(enumerable, mapper)
def product_by(list, mapper) when is_list(list) and is_function(mapper, 1) do
product_by_list(list, mapper, 1)
end
def product_by(enumerable, mapper) when is_function(mapper, 1) do
reduce(enumerable, 1, fn x, acc -> acc * mapper.(x) end)
end
@doc """
Takes an `amount` of elements from the beginning or the end of the `enumerable`.
@@ -3681,115 +3567,98 @@ defmodule Enum do
# Although not necessary, let's seed the random algorithm
iex> :rand.seed(:exsss, {1, 2, 3})
iex> Enum.take_random(1..10, 2)
[6, 1]
[3, 1]
iex> Enum.take_random(?a..?z, 5)
~c"bkzmt"
'mikel'
"""
@spec take_random(t, non_neg_integer) :: list
def take_random(enumerable, count)
def take_random(_enumerable, 0), do: []
def take_random([], _), do: []
def take_random([h | t], 1), do: take_random_list_one(t, h, 1)
def take_random(enumerable, 1) do
enumerable
|> reduce({0, 0, 1.0, nil}, fn
elem, {idx, idx, w, _current} ->
{jdx, w} = take_jdx_w(idx, w, 1)
{idx + 1, jdx, w, elem}
|> reduce([], fn
x, [current | index] ->
if :rand.uniform(index + 1) == 1 do
[x | index + 1]
else
[current | index + 1]
end
_elem, {idx, jdx, w, current} ->
{idx + 1, jdx, w, current}
x, [] ->
[x | 1]
end)
|> case do
{0, 0, 1.0, nil} -> []
{_idx, _jdx, _w, current} -> [current]
[] -> []
[current | _index] -> [current]
end
end
def take_random(enumerable, count) when count in 0..128 do
def take_random(enumerable, count) when is_integer(count) and count in 0..128 do
sample = Tuple.duplicate(nil, count)
reducer = fn
elem, {idx, jdx, w, sample} when idx < count ->
rand = take_index(idx)
sample = sample |> put_elem(idx, elem(sample, rand)) |> put_elem(rand, elem)
reducer = fn elem, {idx, sample} ->
jdx = random_integer(0, idx)
if idx == jdx do
{jdx, w} = take_jdx_w(idx, w, count)
{idx + 1, jdx, w, sample}
else
{idx + 1, jdx, w, sample}
end
cond do
idx < count ->
value = elem(sample, jdx)
{idx + 1, put_elem(sample, idx, value) |> put_elem(jdx, elem)}
elem, {idx, idx, w, sample} ->
pos = :rand.uniform(count) - 1
{jdx, w} = take_jdx_w(idx, w, count)
{idx + 1, jdx, w, put_elem(sample, pos, elem)}
jdx < count ->
{idx + 1, put_elem(sample, jdx, elem)}
_elem, {idx, jdx, w, sample} ->
{idx + 1, jdx, w, sample}
true ->
{idx + 1, sample}
end
end
{size, _, _, sample} = reduce(enumerable, {0, count - 1, 1.0, sample}, reducer)
if count < size do
Tuple.to_list(sample)
else
take_tupled(sample, size, [])
end
{size, sample} = reduce(enumerable, {0, sample}, reducer)
sample |> Tuple.to_list() |> take(Kernel.min(count, size))
end
def take_random(enumerable, count) when is_integer(count) and count >= 0 do
reducer = fn
elem, {idx, jdx, w, sample} when idx < count ->
rand = take_index(idx)
sample = sample |> Map.put(idx, Map.get(sample, rand)) |> Map.put(rand, elem)
reducer = fn elem, {idx, sample} ->
jdx = random_integer(0, idx)
if idx == jdx do
{jdx, w} = take_jdx_w(idx, w, count)
{idx + 1, jdx, w, sample}
else
{idx + 1, jdx, w, sample}
end
cond do
idx < count ->
value = Map.get(sample, jdx)
{idx + 1, Map.put(sample, idx, value) |> Map.put(jdx, elem)}
elem, {idx, idx, w, sample} ->
pos = :rand.uniform(count) - 1
{jdx, w} = take_jdx_w(idx, w, count)
{idx + 1, jdx, w, %{sample | pos => elem}}
jdx < count ->
{idx + 1, Map.put(sample, jdx, elem)}
_elem, {idx, jdx, w, sample} ->
{idx + 1, jdx, w, sample}
true ->
{idx + 1, sample}
end
end
{size, _, _, sample} = reduce(enumerable, {0, count - 1, 1.0, %{}}, reducer)
take_mapped(sample, Kernel.min(count, size), [])
{size, sample} = reduce(enumerable, {0, %{}}, reducer)
take_random(sample, Kernel.min(count, size), [])
end
@compile {:inline, take_jdx_w: 3, take_index: 1}
defp take_jdx_w(idx, w, count) do
w = w * :math.exp(:math.log(:rand.uniform()) / count)
jdx = idx + floor(:math.log(:rand.uniform()) / :math.log(1 - w)) + 1
{jdx, w}
end
defp take_random(_sample, 0, acc), do: acc
defp take_index(0), do: 0
defp take_index(idx), do: :rand.uniform(idx + 1) - 1
defp take_tupled(_sample, 0, acc), do: acc
defp take_tupled(sample, position, acc) do
defp take_random(sample, position, acc) do
position = position - 1
take_tupled(sample, position, [elem(sample, position) | acc])
take_random(sample, position, [Map.get(sample, position) | acc])
end
defp take_mapped(_sample, 0, acc), do: acc
defp take_mapped(sample, position, acc) do
position = position - 1
take_mapped(sample, position, [Map.fetch!(sample, position) | acc])
defp take_random_list_one([h | t], current, index) do
if :rand.uniform(index + 1) == 1 do
take_random_list_one(t, h, index + 1)
else
take_random_list_one(t, current, index + 1)
end
end
defp take_random_list_one([], current, _), do: [current]
@doc """
Takes the elements from the beginning of the `enumerable` while `fun` returns
a truthy value.
@@ -3835,10 +3704,7 @@ defmodule Enum do
def to_list(enumerable), do: reverse(enumerable) |> :lists.reverse()
@doc """
Enumerates the `enumerable`, removing all duplicate elements.
The first occurrence of each element is kept and all following
duplicates are removed. The overall order is preserved.
Enumerates the `enumerable`, removing all duplicated elements.
## Examples
@@ -3865,8 +3731,7 @@ defmodule Enum do
considered duplicates if the return value of `fun` is equal for
both of them.
The first occurrence of each element is kept and all following
duplicates are removed. The overall order is preserved.
The first occurrence of each element is kept.
## Example
@@ -3904,6 +3769,9 @@ defmodule Enum do
iex> Enum.unzip([{:a, 1}, {:b, 2}, {:c, 3}])
{[:a, :b, :c], [1, 2, 3]}
iex> Enum.unzip(%{a: 1, b: 2})
{[:a, :b], [1, 2]}
"""
@spec unzip(t) :: {[element], [element]}
@@ -3934,13 +3802,15 @@ defmodule Enum do
@doc """
Returns the `enumerable` with each element wrapped in a tuple
alongside its index or according to a given function.
alongside its index.
If an integer offset is given as `fun_or_offset`, it will index from the given
offset instead of from zero.
May receive a function or an integer offset.
If a function is given as `fun_or_offset`, it will index by invoking the function
for each element and index (zero-based) of the enumerable.
If an `offset` is given, it will index from the given offset instead of from
zero.
If a `function` is given, it will index by invoking the function for each
element and index (zero-based) of the enumerable.
## Examples
@@ -3982,10 +3852,6 @@ defmodule Enum do
Zips corresponding elements from two enumerables into a list
of tuples.
Because a list of two-element tuples with atoms as the first
tuple element is a keyword list (`Keyword`), zipping a first list
of atoms with a second list of any kind creates a keyword list.
The zipping finishes as soon as either enumerable completes.
## Examples
@@ -3993,9 +3859,6 @@ defmodule Enum do
iex> Enum.zip([1, 2, 3], [:a, :b, :c])
[{1, :a}, {2, :b}, {3, :c}]
iex> Enum.zip([:a, :b, :c], [1, 2, 3])
[a: 1, b: 2, c: 3]
iex> Enum.zip([1, 2, 3, 4, 5], [:a, :b, :c])
[{1, :a}, {2, :b}, {3, :c}]
@@ -4116,7 +3979,7 @@ defmodule Enum do
@doc """
Reduces over two enumerables halting as soon as either enumerable is empty.
In practice, the behavior provided by this function can be achieved with:
In practice, the behaviour provided by this function can be achieved with:
Enum.reduce(Stream.zip(left, right), acc, reducer)
@@ -4140,7 +4003,7 @@ defmodule Enum do
def zip_reduce(left, right, acc, reducer) when is_function(reducer, 3) do
reduce = fn [l, r], acc -> {:cont, reducer.(l, r, acc)} end
R.zip_with([left, right], & &1).({:cont, acc}, reduce) |> elem(1)
Stream.zip_with([left, right], & &1).({:cont, acc}, reduce) |> elem(1)
end
@doc """
@@ -4150,7 +4013,7 @@ defmodule Enum do
The reducer will receive 2 args: a list of elements (one from each enum) and the
accumulator.
In practice, the behavior provided by this function can be achieved with:
In practice, the behaviour provided by this function can be achieved with:
Enum.reduce(Stream.zip(enums), acc, reducer)
@@ -4164,7 +4027,7 @@ defmodule Enum do
...> end)
[{1, 2, 3}, {1, 2, 3}]
iex> enums = [[1, 2], [a: 3, b: 4], [5, 6]]
iex> enums = [[1, 2], %{a: 3, b: 4}, [5, 6]]
...> Enum.zip_reduce(enums, [], fn elements, acc ->
...> [List.to_tuple(elements) | acc]
...> end)
@@ -4175,7 +4038,7 @@ defmodule Enum do
def zip_reduce([], acc, reducer) when is_function(reducer, 2), do: acc
def zip_reduce(enums, acc, reducer) when is_function(reducer, 2) do
R.zip_with(enums, & &1).({:cont, acc}, &{:cont, reducer.(&1, &2)}) |> elem(1)
Stream.zip_with(enums, & &1).({:cont, acc}, &{:cont, reducer.(&1, &2)}) |> elem(1)
end
## Helpers
@@ -4279,6 +4142,18 @@ defmodule Enum do
end)
end
defp random_integer(limit, limit) when is_integer(limit) do
limit
end
defp random_integer(lower_limit, upper_limit) when upper_limit < lower_limit do
random_integer(upper_limit, lower_limit)
end
defp random_integer(lower_limit, upper_limit) do
lower_limit + :rand.uniform(upper_limit - lower_limit + 1) - 1
end
## Implementations
## all?/1
@@ -4440,9 +4315,6 @@ defmodule Enum do
defp flat_map_list([head | tail], fun) do
case fun.(head) do
# the two first clauses are an optimization
[] -> flat_map_list(tail, fun)
[elem] -> [elem | flat_map_list(tail, fun)]
list when is_list(list) -> list ++ flat_map_list(tail, fun)
other -> to_list(other) ++ flat_map_list(tail, fun)
end
@@ -4555,6 +4427,14 @@ defmodule Enum do
[acc | scan_list(rest, acc, fun)]
end
## shuffle
defp shuffle_unwrap([{_, h} | enumerable], t) do
shuffle_unwrap(enumerable, [h | t])
end
defp shuffle_unwrap([], t), do: t
## slice
defp slice_forward(enumerable, start, amount, step) when start < 0 do
@@ -4584,16 +4464,8 @@ defmodule Enum do
amount = Kernel.min(amount, count - start) |> amount_with_step(step)
enumerable |> fun.() |> slice_exact(start, amount, step, count)
{:ok, count, fun} when is_function(fun, 3) ->
amount = Kernel.min(amount, count - start) |> amount_with_step(step)
fun.(start, amount, step)
# TODO: Remove me on v2.0.
# TODO: Deprecate me in Elixir v1.18.
{:ok, count, fun} when is_function(fun, 2) ->
IO.warn(
"#{inspect(Enumerable.impl_for(enumerable))} must return a three arity function on slice/1"
)
amount = Kernel.min(amount, count - start)
if step == 1 do
@@ -4603,6 +4475,10 @@ defmodule Enum do
|> take_every_list(amount, step - 1)
end
{:ok, count, fun} when is_function(fun, 3) ->
amount = Kernel.min(amount, count - start) |> amount_with_step(step)
fun.(start, amount, step)
{:error, module} ->
slice_enum(enumerable, module, start, amount, step)
end
@@ -4661,18 +4537,11 @@ defmodule Enum do
defp slice_count_and_fun(enumerable, step) do
case Enumerable.slice(enumerable) do
{:ok, count, fun} when is_function(fun, 1) ->
{count, &slice_exact(fun.(enumerable), &1, &2, &3, count)}
{:ok, count, fun} when is_function(fun, 3) ->
{count, fun}
# TODO: Remove me on v2.0
# TODO: Deprecate me in Elixir v1.18.
{:ok, count, fun} when is_function(fun, 2) ->
IO.warn(
"#{inspect(Enumerable.impl_for(enumerable))} must return a three arity function on slice/1"
)
if step == 1 do
{count, fn start, amount, 1 -> fun.(start, amount) end}
else
@@ -4683,6 +4552,9 @@ defmodule Enum do
end}
end
{:ok, count, fun} when is_function(fun, 1) ->
{count, &slice_exact(fun.(enumerable), &1, &2, &3, count)}
{:error, module} ->
{list, count} =
enumerable
@@ -4858,16 +4730,6 @@ defmodule Enum do
{:lists.reverse(acc), []}
end
## sum_by
defp sum_by_list([], _, acc), do: acc
defp sum_by_list([h | t], mapper, acc), do: sum_by_list(t, mapper, acc + mapper.(h))
## product_by
defp product_by_list([], _, acc), do: acc
defp product_by_list([h | t], mapper, acc), do: product_by_list(t, mapper, acc * mapper.(h))
## take
defp take_list(_list, 0), do: []
File diff suppressed because it is too large Load Diff
+37 -105
View File
@@ -74,34 +74,10 @@ defmodule File do
Check `:file.open/2` for more information about such options and
other performance considerations.
## Seeking within a file
You may also use any of the functions from the [`:file`](`:file`)
module to interact with files returned by Elixir. For example,
to read from a specific position in a file, use `:file.pread/3`:
File.write!("example.txt", "Eats, Shoots & Leaves")
file = File.open!("example.txt")
:file.pread(file, 15, 6)
#=> {:ok, "Leaves"}
Alternatively, if you need to keep track of the current position,
use `:file.position/2` and `:file.read/2`:
:file.position(file, 6)
#=> {:ok, 6}
:file.read(file, 6)
#=> {:ok, "Shoots"}
:file.position(file, {:cur, -12})
#=> {:ok, 0}
:file.read(file, 4)
#=> {:ok, "Eats"}
"""
@type posix :: :file.posix()
@type io_device :: :file.io_device()
@type file_descriptor :: :file.fd()
@type stat_options :: [time: :local | :universal | :posix]
@type mode ::
:append
@@ -134,7 +110,6 @@ defmodule File do
@type stream_mode ::
encoding_mode()
| read_offset_mode()
| :append
| :compressed
| :delayed_write
@@ -142,8 +117,6 @@ defmodule File do
| {:read_ahead, pos_integer | false}
| {:delayed_write, non_neg_integer, non_neg_integer}
@type read_offset_mode :: {:read_offset, non_neg_integer()}
@type erlang_time ::
{{year :: non_neg_integer(), month :: 1..12, day :: 1..31},
{hour :: 0..23, minute :: 0..59, second :: 0..59}}
@@ -759,7 +732,7 @@ defmodule File do
Note: The command `mv` in Unix-like systems behaves differently depending on
whether `source` is a file and the `destination` is an existing directory.
We have chosen to explicitly disallow this behavior.
We have chosen to explicitly disallow this behaviour.
## Examples
@@ -823,7 +796,7 @@ defmodule File do
The function receives arguments for `source_file` and `destination_file`. It should
return `true` if the existing file should be overwritten, `false` if otherwise.
The default callback returns `true`. On earlier versions, this callback could be
given as third argument, but such behavior is now deprecated.
given as third argument, but such behaviour is now deprecated.
"""
@spec cp(Path.t(), Path.t(), on_conflict: on_conflict_callback) :: :ok | {:error, posix}
@@ -896,7 +869,7 @@ defmodule File do
Note: The command `cp` in Unix-like systems behaves differently depending on
whether `destination` is an existing directory or not. We have chosen to
explicitly disallow this behavior. If `source` is a `file` and `destination`
explicitly disallow this behaviour. If `source` is a `file` and `destination`
is a directory, `{:error, :eisdir}` will be returned.
## Options
@@ -905,7 +878,7 @@ defmodule File do
The function receives arguments for `source` and `destination`. It should return
`true` if the existing file should be overwritten, `false` if otherwise. The default
callback returns `true`. On earlier versions, this callback could be given as third
argument, but such behavior is now deprecated.
argument, but such behaviour is now deprecated.
* `:dereference_symlinks` - (since v1.14.0) By default, this function will copy symlinks
by creating symlinks that point to the same location. This option forces symlinks to be
@@ -1364,11 +1337,6 @@ defmodule File do
{:ok, _} ->
{:ok, :regular}
{:error, :eio} when major == :win32 ->
# unix domain socket returns `{:error, :eio}`
# on other platforms the result is `{:ok, :regular}`
{:ok, :regular}
{:error, reason} ->
{:error, reason}
end
@@ -1395,6 +1363,13 @@ defmodule File do
@doc ~S"""
Opens the given `path`.
In order to write and read files, one must use the functions
in the `IO` module. By default, a file is opened in `:binary` mode,
which requires the functions `IO.binread/2` and `IO.binwrite/2`
to interact with the file. A developer may pass `:utf8` as an
option when opening the file and then all other functions from
`IO` are available, since they work directly with Unicode data.
`modes_or_function` can either be a list of modes or a function. If it's a
list, it's considered to be a list of modes (that are documented below). If
it's a function, then it's equivalent to calling `open(path, [],
@@ -1403,8 +1378,8 @@ defmodule File do
The allowed modes:
* `:binary` - opens the file in binary mode, disabling special handling of
Unicode sequences (default mode).
* `:binary` - opens the file in binary mode, disabling special handling of Unicode sequences
(default mode).
* `:read` - the file, which must exist, is opened for reading.
@@ -1445,33 +1420,19 @@ defmodule File do
This function returns:
* `{:ok, io_device | file_descriptor}` - the file has been opened in
the requested mode. We explore the differences between these two results
in the following section
* `{:ok, io_device}` - the file has been opened in the requested mode.
* `{:error, reason}` - the file could not be opened due to `reason`.
`io_device` is actually the PID of the process which handles the file.
This process monitors the process that originally opened the file (the
owner process). If the owner process terminates, the file is closed and
the process itself terminates too. If any process to which the `io_device`
is linked terminates, the file will be closed and the process itself will
be terminated.
## IO devices
An `io_device` returned from this call can be used as an argument to the
`IO` module functions.
By default, this function returns an IO device. An `io_device` is
a process which handles the file and you can interact with it using
the functions in the `IO` module. By default, a file is opened in
`:binary` mode, which requires the functions `IO.binread/2` and
`IO.binwrite/2` to interact with the file. A developer may pass `:utf8`
as a mode when opening the file and then all other functions from
`IO` are available, since they work directly with Unicode data.
Given the IO device is a file, if the owner process terminates,
the file is closed and the process itself terminates too. If any
process to which the `io_device` is linked terminates, the file
will be closed and the process itself will be terminated.
## File descriptors
When the `:raw` or `:ram` modes are given, this function returns
a low-level file descriptors. This avoids creating a process but
requires using the functions in the [`:file`](`:file`) module to
interact with it.
* `{:error, reason}` - the file could not be opened.
## Examples
@@ -1480,9 +1441,8 @@ defmodule File do
File.close(file)
"""
@spec open(Path.t(), [mode | :ram]) :: {:ok, io_device | file_descriptor} | {:error, posix}
@spec open(Path.t(), (io_device | file_descriptor -> res)) :: {:ok, res} | {:error, posix}
when res: var
@spec open(Path.t(), [mode | :ram]) :: {:ok, io_device} | {:error, posix}
@spec open(Path.t(), (io_device -> res)) :: {:ok, res} | {:error, posix} when res: var
def open(path, modes_or_function \\ [])
def open(path, modes) when is_list(modes) do
@@ -1516,8 +1476,7 @@ defmodule File do
See `open/2` for the list of available `modes`.
"""
@spec open(Path.t(), [mode | :ram], (io_device | file_descriptor -> res)) ::
{:ok, res} | {:error, posix}
@spec open(Path.t(), [mode | :ram], (io_device -> res)) :: {:ok, res} | {:error, posix}
when res: var
def open(path, modes, function) when is_list(modes) and is_function(function, 1) do
case open(path, modes) do
@@ -1539,8 +1498,8 @@ defmodule File do
See `open/2` for the list of available modes.
"""
@spec open!(Path.t(), [mode | :ram]) :: io_device | file_descriptor
@spec open!(Path.t(), (io_device | file_descriptor -> res)) :: res when res: var
@spec open!(Path.t(), [mode | :ram]) :: io_device
@spec open!(Path.t(), (io_device -> res)) :: res when res: var
def open!(path, modes_or_function \\ []) do
case open(path, modes_or_function) do
{:ok, io_device_or_function_result} ->
@@ -1559,7 +1518,7 @@ defmodule File do
See `open/2` for the list of available `modes`.
"""
@spec open!(Path.t(), [mode | :ram], (io_device | file_descriptor -> res)) :: res when res: var
@spec open!(Path.t(), [mode | :ram], (io_device -> res)) :: res when res: var
def open!(path, modes, function) do
case open(path, modes, function) do
{:ok, function_result} ->
@@ -1671,11 +1630,6 @@ defmodule File do
@doc """
Returns the list of files in the given directory.
Hidden files are not ignored and the results are *not* sorted.
Since directories are considered files by the file system,
they are also included in the returned value.
Returns `{:ok, files}` in case of success,
`{:error, reason}` otherwise.
"""
@@ -1717,18 +1671,6 @@ defmodule File do
:file.close(io_device)
end
@doc """
Shortcut for `File.stream!/3`.
"""
@spec stream!(Path.t(), :line | pos_integer | [stream_mode]) :: File.Stream.t()
def stream!(path, line_or_bytes_modes \\ [])
def stream!(path, modes) when is_list(modes),
do: stream!(path, :line, modes)
def stream!(path, line_or_bytes) when is_integer(line_or_bytes) or line_or_bytes == :line,
do: stream!(path, line_or_bytes, [])
@doc ~S"""
Returns a `File.Stream` for the given `path` with the given `modes`.
@@ -1766,37 +1708,27 @@ defmodule File do
One may also consider passing the `:delayed_write` option if the stream
is meant to be written to under a tight loop.
## Byte order marks and read offset
## Byte order marks
If you pass `:trim_bom` in the modes parameter, the stream will
trim UTF-8, UTF-16 and UTF-32 byte order marks when reading from file.
Note that this function does not try to discover the file encoding
based on BOM. From Elixir v1.16.0, you may also pass a `:read_offset`
that is skipped whenever enumerating the stream (if both `:read_offset`
and `:trim_bom` are given, the offset is skipped after the BOM).
based on BOM.
## Examples
# Read a utf8 text file which may include BOM
File.stream!("./test/test.txt", [:trim_bom, encoding: :utf8])
# Read in 2048 byte chunks rather than lines
File.stream!("./test/test.data", 2048)
File.stream!("./test/test.data", [], 2048)
#=> %File.Stream{line_or_bytes: 2048, modes: [:raw, :read_ahead, :binary],
#=> path: "./test/test.data", raw: true}
See `Stream.run/1` for an example of streaming into a file.
"""
@spec stream!(Path.t(), :line | pos_integer, [stream_mode]) :: File.Stream.t()
def stream!(path, line_or_bytes, modes)
def stream!(path, modes, line_or_bytes) when is_list(modes) do
# TODO: Deprecate this on Elixir v1.20
stream!(path, line_or_bytes, modes)
end
def stream!(path, line_or_bytes, modes) do
@spec stream!(Path.t(), [stream_mode], :line | pos_integer) :: File.Stream.t()
def stream!(path, modes \\ [], line_or_bytes \\ :line) do
modes = normalize_modes(modes, true)
File.Stream.__build__(IO.chardata_to_string(path), line_or_bytes, modes)
File.Stream.__build__(IO.chardata_to_string(path), modes, line_or_bytes)
end
@doc """
+13 -13
View File
@@ -60,19 +60,19 @@ defmodule File.Stat do
defstruct keys
@type t :: %__MODULE__{
size: non_neg_integer() | :undefined,
type: :device | :directory | :regular | :other | :symlink | :undefined,
access: :read | :write | :read_write | :none | :undefined,
atime: :calendar.datetime() | integer() | :undefined,
mtime: :calendar.datetime() | integer() | :undefined,
ctime: :calendar.datetime() | integer() | :undefined,
mode: non_neg_integer() | :undefined,
links: non_neg_integer() | :undefined,
major_device: non_neg_integer() | :undefined,
minor_device: non_neg_integer() | :undefined,
inode: non_neg_integer() | :undefined,
uid: non_neg_integer() | :undefined,
gid: non_neg_integer() | :undefined
size: non_neg_integer(),
type: :device | :directory | :regular | :other | :symlink,
access: :read | :write | :read_write | :none,
atime: :calendar.datetime() | integer(),
mtime: :calendar.datetime() | integer(),
ctime: :calendar.datetime() | integer(),
mode: non_neg_integer(),
links: non_neg_integer(),
major_device: non_neg_integer(),
minor_device: non_neg_integer(),
inode: non_neg_integer(),
uid: non_neg_integer(),
gid: non_neg_integer()
}
@doc """
+5 -43
View File
@@ -17,13 +17,7 @@ defmodule File.Stream do
@type t :: %__MODULE__{}
@doc false
def __build__(path, line_or_bytes, modes) do
with {:read_offset, offset} <- :lists.keyfind(:read_offset, 1, modes),
false <- is_integer(offset) and offset >= 0 do
raise ArgumentError,
"expected :read_offset to be a non-negative integer, got: #{inspect(offset)}"
end
def __build__(path, modes, line_or_bytes) do
raw = :lists.keyfind(:encoding, 1, modes) == false
modes =
@@ -94,7 +88,7 @@ defmodule File.Stream do
start_fun = fn ->
case File.Stream.__open__(stream, read_modes(modes)) do
{:ok, device} ->
skip_bom_and_offset(device, raw, modes)
if :trim_bom in modes, do: trim_bom(device, raw) |> elem(0), else: device
{:error, reason} ->
raise File.Error, reason: reason, action: "stream", path: stream.path
@@ -110,14 +104,9 @@ defmodule File.Stream do
Stream.resource(start_fun, next_fun, &:file.close/1).(acc, fun)
end
def count(%{modes: modes, line_or_bytes: :line, path: path, raw: raw} = stream) do
def count(%{modes: modes, line_or_bytes: :line, path: path} = stream) do
pattern = :binary.compile_pattern("\n")
counter = fn device ->
device = skip_bom_and_offset(device, raw, modes)
count_lines(device, path, pattern, read_function(stream), 0)
end
counter = &count_lines(&1, path, pattern, read_function(stream), 0)
{:ok, open!(stream, modes, counter)}
end
@@ -127,11 +116,8 @@ defmodule File.Stream do
{:error, __MODULE__}
{:ok, %{size: size}} ->
bom_offset = count_raw_bom(stream, modes)
offset = get_read_offset(modes)
size = max(size - bom_offset - offset, 0)
remainder = if rem(size, bytes) == 0, do: 0, else: 1
{:ok, div(size, bytes) + remainder}
{:ok, div(size, bytes) + remainder - count_raw_bom(stream, modes)}
{:error, reason} ->
raise File.Error, reason: reason, action: "stream", path: path
@@ -172,23 +158,6 @@ defmodule File.Stream do
end
end
defp skip_bom_and_offset(device, raw, modes) do
device =
if :trim_bom in modes do
device |> trim_bom(raw) |> elem(0)
else
device
end
offset = get_read_offset(modes)
if offset > 0 do
{:ok, _} = :file.position(device, {:cur, offset})
end
device
end
defp trim_bom(device, true) do
bom_length = device |> IO.binread(4) |> bom_length()
{:ok, new_pos} = :file.position(device, bom_length)
@@ -214,13 +183,6 @@ defmodule File.Stream do
defp bom_length(<<254, 255, 0, 0, _rest::binary>>), do: 4
defp bom_length(_binary), do: 0
def get_read_offset(modes) do
case :lists.keyfind(:read_offset, 1, modes) do
{:read_offset, offset} -> offset
false -> 0
end
end
defp read_modes(modes) do
for mode <- modes, mode not in [:write, :append, :trim_bom], do: mode
end
+26 -47
View File
@@ -61,7 +61,6 @@ defmodule Float do
1.7976931348623157e308
"""
@spec max_finite() :: float
def max_finite, do: @max_finite
@doc """
@@ -73,7 +72,6 @@ defmodule Float do
-1.7976931348623157e308
"""
@spec min_finite() :: float
def min_finite, do: @min_finite
@doc """
@@ -139,8 +137,6 @@ defmodule Float do
iex> Float.parse("56.5xyz")
{56.5, "xyz"}
iex> Float.parse(".12")
:error
iex> Float.parse("pi")
:error
iex> Float.parse("1.7976931348623159e+308")
@@ -200,7 +196,7 @@ defmodule Float do
defp add_dot(acc, false), do: acc <> ".0"
@doc """
Rounds a float to the largest float less than or equal to `number`.
Rounds a float to the largest number less than or equal to `num`.
`floor/2` also accepts a precision to round a floating-point value down
to an arbitrary number of fractional digits (between 0 and 15).
@@ -212,7 +208,7 @@ defmodule Float do
## Known issues
The behavior of `floor/2` for floats can be surprising. For example:
The behaviour of `floor/2` for floats can be surprising. For example:
iex> Float.floor(12.52, 2)
12.51
@@ -220,7 +216,7 @@ defmodule Float do
One may have expected it to floor to 12.52. This is not a bug.
Most decimal fractions cannot be represented as a binary floating point
and therefore the number above is internally represented as 12.51999999,
which explains the behavior above.
which explains the behaviour above.
## Examples
@@ -248,7 +244,7 @@ defmodule Float do
end
@doc """
Rounds a float to the smallest float greater than or equal to `number`.
Rounds a float to the smallest integer greater than or equal to `num`.
`ceil/2` also accepts a precision to round a floating-point value down
to an arbitrary number of fractional digits (between 0 and 15).
@@ -256,7 +252,7 @@ defmodule Float do
The operation is performed on the binary floating point, without a
conversion to decimal.
The behavior of `ceil/2` for floats can be surprising. For example:
The behaviour of `ceil/2` for floats can be surprising. For example:
iex> Float.ceil(-12.52, 2)
-12.51
@@ -264,7 +260,7 @@ defmodule Float do
One may have expected it to ceil to -12.52. This is not a bug.
Most decimal fractions cannot be represented as a binary floating point
and therefore the number above is internally represented as -12.51999999,
which explains the behavior above.
which explains the behaviour above.
This function always returns floats. `Kernel.trunc/1` may be used instead to
truncate the result to an integer afterwards.
@@ -277,8 +273,6 @@ defmodule Float do
-56.0
iex> Float.ceil(34.251, 2)
34.26
iex> Float.ceil(-0.01)
-0.0
"""
@spec ceil(float, precision_range) :: float
@@ -309,7 +303,7 @@ defmodule Float do
## Known issues
The behavior of `round/2` for floats can be surprising. For example:
The behaviour of `round/2` for floats can be surprising. For example:
iex> Float.round(5.5675, 3)
5.567
@@ -317,8 +311,8 @@ defmodule Float do
One may have expected it to round to the half up 5.568. This is not a bug.
Most decimal fractions cannot be represented as a binary floating point
and therefore the number above is internally represented as 5.567499999,
which explains the behavior above. If you want exact rounding for decimals,
you must use a decimal library. The behavior above is also in accordance
which explains the behaviour above. If you want exact rounding for decimals,
you must use a decimal library. The behaviour above is also in accordance
to reference implementations, such as "Correctly Rounded Binary-Decimal and
Decimal-Binary Conversions" by David M. Gay.
@@ -336,8 +330,6 @@ defmodule Float do
-6.0
iex> Float.round(12.341444444444441, 15)
12.341444444444441
iex> Float.round(-0.01)
-0.0
"""
@spec round(float, precision_range) :: float
@@ -346,13 +338,8 @@ defmodule Float do
# and could be implemented in the future.
def round(float, precision \\ 0)
def round(float, 0) when float == 0.0, do: float
def round(float, 0) when is_float(float) do
case float |> :erlang.round() |> :erlang.float() do
zero when zero == 0.0 and float < 0.0 -> -0.0
rounded -> rounded
end
float |> :erlang.round() |> :erlang.float()
end
def round(float, precision) when is_float(float) and precision in @precision_range do
@@ -363,7 +350,7 @@ defmodule Float do
raise ArgumentError, invalid_precision_message(precision)
end
defp round(num, _precision, _rounding) when is_float(num) and num == 0.0, do: num
defp round(0.0 = num, _precision, _rounding), do: num
defp round(float, precision, rounding) do
<<sign::1, exp::11, significant::52-bitstring>> = <<float::float>>
@@ -376,8 +363,6 @@ defmodule Float do
case rounding do
:ceil when sign === 0 -> 1 / power_of_10(precision)
:floor when sign === 1 -> -1 / power_of_10(precision)
:ceil when sign === 1 -> minus_zero()
:half_up when sign === 1 -> minus_zero()
_ -> 0.0
end
@@ -407,9 +392,6 @@ defmodule Float do
boundary = den <<< 52
cond do
num == 0 and sign == 1 ->
minus_zero()
num == 0 ->
0.0
@@ -424,11 +406,6 @@ defmodule Float do
end
end
# TODO remove once we require Erlang/OTP 27+
# This function tricks the compiler to avoid this bug in previous versions:
# https://github.com/elixir-lang/elixir/blob/main/lib/elixir/lib/float.ex#L408-L412
defp minus_zero, do: -0.0
defp decompose(significant, initial) do
decompose(significant, 1, 0, initial)
end
@@ -521,7 +498,7 @@ defmodule Float do
"""
@doc since: "1.4.0"
@spec ratio(float) :: {integer, pos_integer}
def ratio(float) when is_float(float) and float == 0.0, do: {0, 1}
def ratio(0.0), do: {0, 1}
def ratio(float) when is_float(float) do
<<sign::1, exp::11, mantissa::52>> = <<float::float>>
@@ -577,23 +554,27 @@ defmodule Float do
Returns a charlist which corresponds to the shortest text representation
of the given float.
It uses the algorithm presented in "Ryū: fast float-to-string conversion"
in Proceedings of the SIGPLAN '2018 Conference on Programming Language
Design and Implementation.
The underlying algorithm changes depending on the Erlang/OTP version:
* For OTP >= 24, it uses the algorithm presented in "Ryū: fast
float-to-string conversion" in Proceedings of the SIGPLAN '2018
Conference on Programming Language Design and Implementation.
* For OTP < 24, it uses the algorithm presented in "Printing Floating-Point
Numbers Quickly and Accurately" in Proceedings of the SIGPLAN '1996
Conference on Programming Language Design and Implementation.
For a configurable representation, use `:erlang.float_to_list/2`.
Inlined by the compiler.
## Examples
iex> Float.to_charlist(7.0)
~c"7.0"
'7.0'
"""
@spec to_charlist(float) :: charlist
def to_charlist(float) do
:erlang.float_to_list(float, [:short])
def to_charlist(float) when is_float(float) do
:io_lib_format.fwrite_g(float)
end
@doc """
@@ -612,8 +593,6 @@ defmodule Float do
For a configurable representation, use `:erlang.float_to_binary/2`.
Inlined by the compiler.
## Examples
iex> Float.to_string(7.0)
@@ -621,8 +600,8 @@ defmodule Float do
"""
@spec to_string(float) :: String.t()
def to_string(float) do
:erlang.float_to_binary(float, [:short])
def to_string(float) when is_float(float) do
IO.iodata_to_binary(:io_lib_format.fwrite_g(float))
end
@doc false
+5 -7
View File
@@ -19,10 +19,10 @@ defmodule GenEvent do
One alternative to GenEvent is a very minimal solution consisting of using a
supervisor and multiple GenServers started under it. The supervisor acts as
the "event manager" and the children GenServers act as the "event handlers".
This approach has some shortcomings (it provides no back-pressure for example)
This approach has some shortcomings (it provides no backpressure for example)
but can still replace GenEvent for low-profile usages of it. [This blog post
by José
Valim](https://dashbit.co/blog/replacing-genevent-by-a-supervisor-plus-genserver)
Valim](http://blog.plataformatec.com.br/2016/11/replacing-genevent-by-a-supervisor-genserver/)
has more detailed information on this approach.
### GenStage
@@ -31,7 +31,7 @@ defmodule GenEvent do
[GenStage](https://github.com/elixir-lang/gen_stage) provides a great
alternative. GenStage is an external Elixir library maintained by the Elixir
team; it provides a tool to implement systems that exchange events in a
demand-driven way with built-in support for back-pressure. See the [GenStage
demand-driven way with built-in support for backpressure. See the [GenStage
documentation](https://hexdocs.pm/gen_stage) for more information.
### `:gen_event`
@@ -46,8 +46,8 @@ defmodule GenEvent do
@callback init(args :: term) ::
{:ok, state}
| {:ok, state, :hibernate}
| {:error, reason :: term}
when state: term
| {:error, reason :: any}
when state: any
@callback handle_event(event :: term, state :: term) ::
{:ok, new_state}
@@ -507,8 +507,6 @@ defmodule GenEvent do
{:ok, states, [name, handlers, hib]}
end
# Keeping deprecated format_status/2 since the current implementation is not
# compatible with format_status/1 and GenEvent is deprecated anyway
@doc false
def format_status(opt, status_data) do
[pdict, sys_state, parent, _debug, [name, handlers, _hib]] = status_data
+25 -79
View File
@@ -8,13 +8,6 @@ defmodule GenServer do
will have a standard set of interface functions and include functionality for
tracing and error reporting. It will also fit into a supervision tree.
```mermaid
graph BT
C(Client #3) ~~~ B(Client #2) ~~~ A(Client #1)
A & B & C -->|request| GenServer
GenServer -.->|reply| A & B & C
```
## Example
The GenServer behaviour abstracts the common client-server interaction.
@@ -143,44 +136,11 @@ defmodule GenServer do
end
end
In practice, it is common to have both server and client functions in
the same module. If the server and/or client implementations are growing
complex, you may want to have them in different modules.
The following diagram summarizes the interactions between client and server.
Both Client and Server are processes and communication happens via messages
(continuous line). The Server <-> Module interaction happens when the
GenServer process calls your code (dotted lines):
```mermaid
sequenceDiagram
participant C as Client (Process)
participant S as Server (Process)
participant M as Module (Code)
note right of C: Typically started by a supervisor
C->>+S: GenServer.start_link(module, arg, options)
S-->>+M: init(arg)
M-->>-S: {:ok, state} | :ignore | {:error, reason}
S->>-C: {:ok, pid} | :ignore | {:error, reason}
note right of C: call is synchronous
C->>+S: GenServer.call(pid, message)
S-->>+M: handle_call(message, from, state)
M-->>-S: {:reply, reply, state} | {:stop, reason, reply, state}
S->>-C: reply
note right of C: cast is asynchronous
C-)S: GenServer.cast(pid, message)
S-->>+M: handle_cast(message, state)
M-->>-S: {:noreply, state} | {:stop, reason, state}
note right of C: send is asynchronous
C-)S: Kernel.send(pid, message)
S-->>+M: handle_info(message, state)
M-->>-S: {:noreply, state} | {:stop, reason, state}
```
## How to supervise
A `GenServer` is most commonly started under a supervision tree.
@@ -400,7 +360,7 @@ defmodule GenServer do
or is suspended, the parent PID, the debugger state, and the state of
the behaviour module, which includes the callback module state
(as returned by `:sys.get_state/2`). It's possible to change how this
status is represented by defining the optional `c:GenServer.format_status/1`
status is represented by defining the optional `c:GenServer.format_status/2`
callback.
* `:sys.trace/3` - prints all the system events to `:stdio`.
* `:sys.statistics/3` - manages collection of process statistics.
@@ -415,7 +375,7 @@ defmodule GenServer do
Let's see how we could use those functions for debugging the stack server
we defined earlier.
iex> {:ok, pid} = Stack.start_link("")
iex> {:ok, pid} = Stack.start_link([])
iex> :sys.statistics(pid, true) # turn on collecting process statistics
iex> :sys.trace(pid, true) # turn on event printing
iex> Stack.push(pid, 1)
@@ -471,7 +431,7 @@ defmodule GenServer do
guide provides a tutorial-like introduction. The documentation and links
in Erlang can also provide extra insight.
* [GenServer - Elixir's Getting Started Guide](genservers.md)
* [GenServer - Elixir's Getting Started Guide](https://elixir-lang.org/getting-started/mix-otp/genserver.html)
* [`:gen_server` module documentation](`:gen_server`)
* [gen_server Behaviour - OTP Design Principles](https://www.erlang.org/doc/design_principles/gen_server_concepts.html)
* [Clients and Servers - Learn You Some Erlang for Great Good!](http://learnyousomeerlang.com/clients-and-servers)
@@ -523,8 +483,8 @@ defmodule GenServer do
{:ok, state}
| {:ok, state, timeout | :hibernate | {:continue, continue_arg :: term}}
| :ignore
| {:stop, reason :: term}
when state: term
| {:stop, reason :: any}
when state: any
@doc """
Invoked to handle synchronous `call/3` messages. `call/3` will block until a
@@ -759,39 +719,22 @@ defmodule GenServer do
when old_vsn: term | {:down, term}
@doc """
This function is called by a `GenServer` process in the following situations:
Invoked in some cases to retrieve a formatted version of the `GenServer` status:
* [`:sys.get_status/1,2`](`:sys.get_status/1`) is invoked to get the `GenServer` status.
* The `GenServer` process terminates abnormally and logs an error.
* one of `:sys.get_status/1` or `:sys.get_status/2` is invoked to get the
status of the `GenServer`; in such cases, `reason` is `:normal`
This callback is used to limit the status of the process returned by
[`:sys.get_status/1,2`](`:sys.get_status/1`) or sent to logger.
* the `GenServer` terminates abnormally and logs an error; in such cases,
`reason` is `:terminate`
The callback gets a map `status` describing the current status and shall return
a map `new_status` with the same keys, but it may transform some values.
Two possible use cases for this callback is to remove sensitive information
from the state to prevent it from being printed in log files, or to compact
large irrelevant status items that would only clutter the logs.
## Example
@impl GenServer
def format_status(status) do
Map.new(status, fn
{:state, state} -> {:state, Map.delete(state, :private_key)}
{:message, {:password, _}} -> {:message, {:password, "redacted"}}
key_value -> key_value
end)
end
This callback can be useful to control the *appearance* of the status of the
`GenServer`. For example, it can be used to return a compact representation of
the `GenServer`'s state to avoid having large state terms printed.
`pdict_and_state` is a two-elements list `[pdict, state]` where `pdict` is a
list of `{key, value}` tuples representing the current process dictionary of
the `GenServer` and `state` is the current state of the `GenServer`.
"""
@doc since: "1.17.0"
@callback format_status(status :: :gen_server.format_status()) ::
new_status :: :gen_server.format_status()
# TODO: Remove this on v2.0
@doc deprecated: "Use format_status/1 callback instead"
@callback format_status(reason, pdict_and_state :: list) :: term
when reason: :normal | :terminate
@@ -800,7 +743,6 @@ defmodule GenServer do
handle_info: 2,
handle_cast: 2,
handle_call: 3,
format_status: 1,
format_status: 2,
handle_continue: 2
@@ -845,7 +787,7 @@ defmodule GenServer do
quote location: :keep, bind_quoted: [opts: opts] do
@behaviour GenServer
if not Module.has_attribute?(__MODULE__, :doc) do
unless Module.has_attribute?(__MODULE__, :doc) do
@doc """
Returns a specification to start this module under a supervisor.
@@ -945,7 +887,7 @@ defmodule GenServer do
end
defmacro __before_compile__(env) do
if not Module.defines?(env.module, {:init, 1}) do
unless Module.defines?(env.module, {:init, 1}) do
message = """
function init/1 required by behaviour GenServer is not implemented \
(in module #{inspect(env.module)}).
@@ -1018,7 +960,7 @@ defmodule GenServer do
or `:ignore`, the process is terminated and this function returns
`{:error, reason}` or `:ignore`, respectively.
"""
@spec start_link(module, term, options) :: on_start
@spec start_link(module, any, options) :: on_start
def start_link(module, init_arg, options \\ []) when is_atom(module) and is_list(options) do
do_start(:link, module, init_arg, options)
end
@@ -1028,7 +970,7 @@ defmodule GenServer do
See `start_link/3` for more information.
"""
@spec start(module, term, options) :: on_start
@spec start(module, any, options) :: on_start
def start(module, init_arg, options \\ []) when is_atom(module) and is_list(options) do
do_start(:nolink, module, init_arg, options)
end
@@ -1120,6 +1062,10 @@ defmodule GenServer do
nil ->
exit({:noproc, {__MODULE__, :call, [server, request, timeout]}})
# TODO: remove this clause when we require Erlang/OTP 25+
pid when pid == self() ->
exit({:calling_self, {__MODULE__, :call, [server, request, timeout]}})
pid ->
try do
:gen.call(pid, :"$gen_call", request, timeout)
+5 -5
View File
@@ -258,13 +258,13 @@ end
defimpl Enumerable, for: HashDict do
def reduce(dict, acc, fun) do
# Avoid warnings about HashDict being deprecated.
module = String.to_atom("HashDict")
module = HashDict
module.reduce(dict, acc, fun)
end
def member?(dict, {key, value}) do
# Avoid warnings about HashDict being deprecated.
module = String.to_atom("HashDict")
module = HashDict
{:ok, match?({:ok, ^value}, module.fetch(dict, key))}
end
@@ -274,7 +274,7 @@ defimpl Enumerable, for: HashDict do
def count(dict) do
# Avoid warnings about HashDict being deprecated.
module = String.to_atom("HashDict")
module = HashDict
{:ok, module.size(dict)}
end
@@ -286,7 +286,7 @@ end
defimpl Collectable, for: HashDict do
def into(original) do
# Avoid warnings about HashDict being deprecated.
module = String.to_atom("HashDict")
module = HashDict
collector_fun = fn
dict, {:cont, {key, value}} -> module.put(dict, key, value)
@@ -303,7 +303,7 @@ defimpl Inspect, for: HashDict do
def inspect(dict, opts) do
# Avoid warnings about HashDict being deprecated.
module = String.to_atom("HashDict")
module = HashDict
concat(["#HashDict<", Inspect.List.inspect(module.to_list(dict), opts), ">"])
end
end
+5 -5
View File
@@ -272,19 +272,19 @@ end
defimpl Enumerable, for: HashSet do
def reduce(set, acc, fun) do
# Avoid warnings about HashSet being deprecated.
module = String.to_atom("HashSet")
module = HashSet
module.reduce(set, acc, fun)
end
def member?(set, term) do
# Avoid warnings about HashSet being deprecated.
module = String.to_atom("HashSet")
module = HashSet
{:ok, module.member?(set, term)}
end
def count(set) do
# Avoid warnings about HashSet being deprecated.
module = String.to_atom("HashSet")
module = HashSet
{:ok, module.size(set)}
end
@@ -296,7 +296,7 @@ end
defimpl Collectable, for: HashSet do
def into(original) do
# Avoid warnings about HashSet being deprecated.
module = String.to_atom("HashSet")
module = HashSet
collector_fun = fn
set, {:cont, term} -> module.put(set, term)
@@ -313,7 +313,7 @@ defimpl Inspect, for: HashSet do
def inspect(set, opts) do
# Avoid warnings about HashSet being deprecated.
module = String.to_atom("HashSet")
module = HashSet
concat(["#HashSet<", Inspect.List.inspect(module.to_list(set), opts), ">"])
end
end
+116 -173
View File
@@ -117,107 +117,19 @@ defprotocol Inspect do
In case there is an error while your structure is being inspected,
Elixir will raise an `ArgumentError` error and will automatically fall back
to a raw representation for printing the structure. Furthermore, you
must be careful when debugging your own Inspect implementation, as calls
to `IO.inspect/2` or `dbg/1` may trigger an infinite loop (as in order to
inspect/debug the data structure, you must call `inspect` itself).
to a raw representation for printing the structure.
Here are some tips:
You can, however, access the underlying error by invoking the `Inspect`
implementation directly. For example, to test `Inspect.MapSet` above,
you can invoke it as:
* For debugging, use `IO.inspect/2` with the `structs: false` option,
which disables custom printing and avoids calling the Inspect
implementation recursively
* To access the underlying error on your custom `Inspect` implementation,
you may invoke the protocol directly. For example, we could invoke the
`Inspect.MapSet` implementation above as:
Inspect.MapSet.inspect(MapSet.new(), %Inspect.Opts{})
Inspect.MapSet.inspect(MapSet.new(), %Inspect.Opts{})
"""
# Handle structs in Any
@fallback_to_any true
@impl true
defmacro __deriving__(module, options) do
info = Macro.struct_info!(module, __CALLER__)
fields = Enum.sort(Enum.map(info, & &1.field) -- [:__exception__, :__struct__])
only = Keyword.get(options, :only, fields)
except = Keyword.get(options, :except, [])
optional = Keyword.get(options, :optional, [])
:ok = validate_option(:only, only, fields, module)
:ok = validate_option(:except, except, fields, module)
:ok = validate_option(:optional, optional, fields, module)
inspect_module =
if fields == Enum.sort(only) and except == [] do
Inspect.Map
else
Inspect.Any
end
filtered_fields =
fields
|> Enum.reject(&(&1 in except))
|> Enum.filter(&(&1 in only))
filtered_guard =
quote do
var!(field) in unquote(filtered_fields)
end
field_guard =
if optional == [] do
filtered_guard
else
optional_map =
for field <- optional, into: %{} do
default = Enum.find(info, %{}, &(&1.field == field)) |> Map.get(:default, nil)
{field, default}
end
quote do
unquote(filtered_guard) and
not case unquote(Macro.escape(optional_map)) do
%{^var!(field) => var!(default)} ->
var!(default) == Map.get(var!(struct), var!(field))
%{} ->
false
end
end
end
quote do
defimpl Inspect, for: unquote(module) do
def inspect(var!(struct), var!(opts)) do
var!(infos) =
for %{field: var!(field)} = var!(info) <- unquote(module).__info__(:struct),
unquote(field_guard),
do: var!(info)
var!(name) = Macro.inspect_atom(:literal, unquote(module))
unquote(inspect_module).inspect(var!(struct), var!(name), var!(infos), var!(opts))
end
end
end
end
defp validate_option(option, option_list, fields, module) do
case option_list -- fields do
[] ->
:ok
unknown_fields ->
raise ArgumentError,
"unknown fields #{Kernel.inspect(unknown_fields)} in #{Kernel.inspect(option)} " <>
"when deriving the Inspect protocol for #{Kernel.inspect(module)}"
end
end
@doc """
Converts `term` into an algebra document.
@@ -234,7 +146,7 @@ defimpl Inspect, for: Atom do
require Macro
def inspect(atom, opts) do
color_doc(Macro.inspect_atom(:literal, atom), color_key(atom), opts)
color(Macro.inspect_atom(:literal, atom), color_key(atom), opts)
end
defp color_key(atom) when is_boolean(atom), do: :boolean
@@ -254,7 +166,7 @@ defimpl Inspect, for: BitString do
{escaped, _} -> [?", escaped, ?", " <> ..."]
end
color_doc(IO.iodata_to_binary(inspected), :string, opts)
color(IO.iodata_to_binary(inspected), :string, opts)
else
inspect_bitstring(term, opts)
end
@@ -265,12 +177,12 @@ defimpl Inspect, for: BitString do
end
defp inspect_bitstring("", opts) do
color_doc("<<>>", :binary, opts)
color("<<>>", :binary, opts)
end
defp inspect_bitstring(bitstring, opts) do
left = color_doc("<<", :binary, opts)
right = color_doc(">>", :binary, opts)
left = color("<<", :binary, opts)
right = color(">>", :binary, opts)
inner = each_bit(bitstring, opts.limit, opts)
group(concat(concat(left, nest(inner, 2)), right))
end
@@ -307,7 +219,7 @@ end
defimpl Inspect, for: List do
def inspect([], opts) do
color_doc("[]", :list, opts)
color("[]", :list, opts)
end
# TODO: Remove :char_list and :as_char_lists handling on v2.0
@@ -338,26 +250,19 @@ defimpl Inspect, for: List do
lists
end
open = color_doc("[", :list, opts)
sep = color_doc(",", :list, opts)
close = color_doc("]", :list, opts)
open = color("[", :list, opts)
sep = color(",", :list, opts)
close = color("]", :list, opts)
cond do
(lists == :as_charlists and unicode_list?(term, printable_limit)) or
(lists == :infer and List.ascii_printable?(term, printable_limit)) ->
{split, tail} =
if is_integer(printable_limit) do
case Enum.split(term, printable_limit) do
{split, []} -> {split, []}
{split, _} -> {split, " ++ ..."}
end
else
{term, []}
lists == :as_charlists or (lists == :infer and List.ascii_printable?(term, printable_limit)) ->
inspected =
case Identifier.escape(IO.chardata_to_string(term), ?", printable_limit) do
{escaped, ""} -> [?~, ?c, ?", escaped, ?"]
{escaped, _} -> [?~, ?c, ?", escaped, ?", " ++ ..."]
end
{escaped, _} = Identifier.escape(IO.chardata_to_string(split), ?")
inspected = [?~, ?c, ?", escaped, ?" | tail]
color_doc(IO.iodata_to_binary(inspected), :charlist, opts)
color(IO.iodata_to_binary(inspected), :charlist, opts)
keyword?(term) ->
container_doc(open, term, close, opts, &keyword/2, separator: sep, break: :strict)
@@ -367,22 +272,9 @@ defimpl Inspect, for: List do
end
end
defp unicode_list?(_, 0), do: true
defp unicode_list?([char | rest], counter)
when char in 0..0xD7FF or char in 0xE000..0x10FFFF,
do: unicode_list?(rest, decrement(counter))
defp unicode_list?([], _counter), do: true
defp unicode_list?(_, _counter), do: false
@compile {:inline, decrement: 1}
defp decrement(:infinity), do: :infinity
defp decrement(counter), do: counter - 1
@doc false
def keyword({key, value}, opts) do
key = color_doc(Macro.inspect_atom(:key, key), :atom, opts)
key = color(Macro.inspect_atom(:key, key), :atom, opts)
concat(key, concat(" ", to_doc(value, opts)))
end
@@ -400,9 +292,9 @@ end
defimpl Inspect, for: Tuple do
def inspect(tuple, opts) do
open = color_doc("{", :tuple, opts)
sep = color_doc(",", :tuple, opts)
close = color_doc("}", :tuple, opts)
open = color("{", :tuple, opts)
sep = color(",", :tuple, opts)
close = color("}", :tuple, opts)
container_opts = [separator: sep, break: :flex]
container_doc(open, Tuple.to_list(tuple), close, opts, &to_doc/2, container_opts)
end
@@ -421,7 +313,7 @@ defimpl Inspect, for: Map do
if Inspect.List.keyword?(list) do
&Inspect.List.keyword/2
else
sep = color_doc(" => ", :map, opts)
sep = color(" => ", :map, opts)
&to_assoc(&1, &2, sep)
end
@@ -438,9 +330,9 @@ defimpl Inspect, for: Map do
end
defp map_container_doc(list, name, opts, fun) do
open = color_doc("%" <> name <> "{", :map, opts)
sep = color_doc(",", :map, opts)
close = color_doc("}", :map, opts)
open = color("%" <> name <> "{", :map, opts)
sep = color(",", :map, opts)
close = color("}", :map, opts)
container_doc(open, list, close, opts, fun, separator: sep, break: :strict)
end
end
@@ -448,7 +340,7 @@ end
defimpl Inspect, for: Integer do
def inspect(term, %Inspect.Opts{base: base} = opts) do
inspected = Integer.to_string(term, base_to_value(base)) |> prepend_prefix(base)
color_doc(inspected, :number, opts)
color(inspected, :number, opts)
end
defp base_to_value(base) do
@@ -486,51 +378,36 @@ defimpl Inspect, for: Float do
if abs >= 1.0 and abs < 1.0e16 and trunc(float) == float do
[Integer.to_string(trunc(float)), ?., ?0]
else
Float.to_charlist(float)
:io_lib_format.fwrite_g(float)
end
color_doc(IO.iodata_to_binary(formatted), :number, opts)
color(IO.iodata_to_binary(formatted), :number, opts)
end
end
defimpl Inspect, for: Regex do
def inspect(regex = %{opts: regex_opts}, opts) when is_list(regex_opts) do
case translate_options(regex_opts, []) do
:error ->
concat([
"Regex.compile!(",
Inspect.BitString.inspect(regex.source, opts),
", ",
Inspect.List.inspect(regex_opts, opts),
")"
])
translated_opts ->
{escaped, _} =
regex.source
|> normalize(<<>>)
|> Identifier.escape(?/, :infinity, &escape_map/1)
source = IO.iodata_to_binary([?~, ?r, ?/, escaped, ?/, translated_opts])
color_doc(source, :regex, opts)
end
concat([
"Regex.compile!(",
Inspect.BitString.inspect(regex.source, opts),
", ",
Inspect.List.inspect(regex_opts, opts),
")"
])
end
defp translate_options([:dotall, {:newline, :anycrlf} | t], acc),
do: translate_options(t, [?s | acc])
def inspect(regex, opts) do
{escaped, _} =
regex.source
|> normalize(<<>>)
|> Identifier.escape(?/, :infinity, &escape_map/1)
defp translate_options([:unicode, :ucp | t], acc), do: translate_options(t, [?u | acc])
defp translate_options([:caseless | t], acc), do: translate_options(t, [?i | acc])
defp translate_options([:extended | t], acc), do: translate_options(t, [?x | acc])
defp translate_options([:firstline | t], acc), do: translate_options(t, [?f | acc])
defp translate_options([:ungreedy | t], acc), do: translate_options(t, [?U | acc])
defp translate_options([:multiline | t], acc), do: translate_options(t, [?m | acc])
defp translate_options([], acc), do: acc
defp translate_options(_t, _acc), do: :error
source = IO.iodata_to_binary([?~, ?r, ?/, escaped, ?/, regex.opts])
color(source, :regex, opts)
end
defp normalize(<<?\\, ?\\, rest::binary>>, acc), do: normalize(rest, <<acc::binary, ?\\, ?\\>>)
defp normalize(<<?\\, ?/, rest::binary>>, acc), do: normalize(rest, <<acc::binary, ?/>>)
defp normalize(<<?\\, ?#, ?{, rest::binary>>, acc), do: normalize(rest, <<acc::binary, ?#, ?{>>)
defp normalize(<<char, rest::binary>>, acc), do: normalize(rest, <<acc::binary, char>>)
defp normalize(<<>>, acc), do: acc
@@ -562,7 +439,7 @@ defimpl Inspect, for: Function do
match?(@elixir_compiler ++ _, Atom.to_charlist(mod)) ->
if function_exported?(mod, :__RELATIVE__, 0) do
"#Function<#{uniq(fun_info)} in file:#{mod.__RELATIVE__()}>"
"#Function<#{uniq(fun_info)} in file:#{mod.__RELATIVE__}>"
else
default_inspect(mod, fun_info)
end
@@ -647,6 +524,72 @@ defimpl Inspect, for: Reference do
end
defimpl Inspect, for: Any do
defmacro __deriving__(module, struct, options) do
fields = Map.keys(struct) -- [:__exception__, :__struct__]
only = Keyword.get(options, :only, fields)
except = Keyword.get(options, :except, [])
optional = Keyword.get(options, :optional, [])
:ok = validate_option(:only, only, fields, module)
:ok = validate_option(:except, except, fields, module)
:ok = validate_option(:optional, optional, fields, module)
inspect_module =
if fields == only and except == [] do
Inspect.Map
else
Inspect.Any
end
filtered_fields =
fields
|> Enum.reject(&(&1 in except))
|> Enum.filter(&(&1 in only))
optional? =
if optional == [] do
false
else
optional_map = for field <- optional, into: %{}, do: {field, Map.fetch!(struct, field)}
quote do
case unquote(Macro.escape(optional_map)) do
%{^var!(field) => var!(default)} ->
var!(default) == Map.get(var!(struct), var!(field))
%{} ->
false
end
end
end
quote do
defimpl Inspect, for: unquote(module) do
def inspect(var!(struct), var!(opts)) do
var!(infos) =
for %{field: var!(field)} = var!(info) <- unquote(module).__info__(:struct),
var!(field) in unquote(filtered_fields) and not unquote(optional?),
do: var!(info)
var!(name) = Macro.inspect_atom(:literal, unquote(module))
unquote(inspect_module).inspect(var!(struct), var!(name), var!(infos), var!(opts))
end
end
end
end
defp validate_option(option, option_list, fields, module) do
case option_list -- fields do
[] ->
:ok
unknown_fields ->
raise ArgumentError,
"unknown fields #{Kernel.inspect(unknown_fields)} in #{Kernel.inspect(option)} " <>
"when deriving the Inspect protocol for #{Kernel.inspect(module)}"
end
end
def inspect(%module{} = struct, opts) do
try do
{module.__struct__(), module.__info__(:struct)}
@@ -668,9 +611,9 @@ defimpl Inspect, for: Any do
end
def inspect(map, name, infos, opts) do
open = color_doc("#" <> name <> "<", :map, opts)
sep = color_doc(",", :map, opts)
close = color_doc(">", :map, opts)
open = color("#" <> name <> "<", :map, opts)
sep = color(",", :map, opts)
close = color(">", :map, opts)
fun = fn
%{field: field}, opts -> Inspect.List.keyword({field, Map.get(map, field)}, opts)
+76 -125
View File
@@ -17,8 +17,8 @@ defmodule Inspect.Opts do
is `:decimal` and if it is printable, otherwise in bit syntax. See
`String.printable?/1` to learn when a string is printable.
* `:charlists` - when `:as_charlists` all charlists will be printed as charlists,
non-printable code points will be escaped. Other lists will be printed as lists.
* `:charlists` - when `:as_charlists` all lists will be printed as charlists,
non-printable elements will be escaped.
When `:as_lists` all lists will be printed as lists.
@@ -143,8 +143,9 @@ defmodule Inspect.Opts do
function as this must be controlled by applications. Libraries
should instead define their own structs with custom inspect
implementations. If a library must change the default inspect
function, then it is best to ask users of your library to explicitly
call `default_inspect_fun/1` with your function of choice.
function, then it is best to define to ask users of your library
to explicitly call `default_inspect_fun/1` with your function of
choice.
The default is `Inspect.inspect/2`.
@@ -333,7 +334,6 @@ defmodule Inspect.Algebra do
defguardp is_width(limit) when limit == :infinity or (is_integer(limit) and limit >= 0)
# Elixir + Inspect.Opts conveniences
# These have the _doc suffix.
@doc """
Converts an Elixir term to an algebra document
@@ -439,7 +439,7 @@ defmodule Inspect.Algebra do
"""
@doc since: "1.6.0"
@spec container_doc(t, [term], t, Inspect.Opts.t(), (term, Inspect.Opts.t() -> t), keyword()) ::
@spec container_doc(t, [any], t, Inspect.Opts.t(), (term, Inspect.Opts.t() -> t), keyword()) ::
t
def container_doc(left, collection, right, inspect_opts, fun, opts \\ [])
when is_doc(left) and is_list(collection) and is_doc(right) and is_function(fun, 2) and
@@ -456,7 +456,7 @@ defmodule Inspect.Algebra do
container_each(collection, inspect_opts.limit, inspect_opts, fun, [], break == :maybe)
flex? = simple? or break == :flex
docs = fold(docs, &join(&1, &2, flex?, separator))
docs = fold_doc(docs, &join(&1, &2, flex?, separator))
case flex? do
true -> group(concat(concat(left, nest(docs, 1)), right))
@@ -526,32 +526,6 @@ defmodule Inspect.Algebra do
container_doc(left, docs, right, inspect, fun, separator: separator)
end
# TODO: Deprecate me on Elixir v1.23
@doc deprecated: "Use color_doc/3 instead"
def color(doc, key, opts) do
color_doc(doc, key, opts)
end
@doc ~S"""
Colors a document if the `color_key` has a color in the options.
"""
@doc since: "1.18.0"
@spec color_doc(t, Inspect.Opts.color_key(), Inspect.Opts.t()) :: t
def color_doc(doc, color_key, %Inspect.Opts{syntax_colors: syntax_colors}) when is_doc(doc) do
if precolor = Keyword.get(syntax_colors, color_key) do
postcolor = Keyword.get(syntax_colors, :reset, :reset)
concat(doc_color(doc, ansi(precolor)), doc_color(empty(), ansi(postcolor)))
else
doc
end
end
defp ansi(color) do
color
|> IO.ANSI.format_fragment(true)
|> IO.iodata_to_binary()
end
# Algebra API
@doc """
@@ -615,21 +589,6 @@ defmodule Inspect.Algebra do
doc_cons(doc1, doc2)
end
@doc ~S"""
Disable any rendering limit while rendering the given document.
## Examples
iex> doc = Inspect.Algebra.glue("hello", "world") |> Inspect.Algebra.group()
iex> Inspect.Algebra.format(doc, 10)
["hello", "\n", "world"]
iex> doc = Inspect.Algebra.no_limit(doc)
iex> Inspect.Algebra.format(doc, 10)
["hello", " ", "world"]
"""
@doc since: "1.14.0"
@spec no_limit(t) :: t
def no_limit(doc) do
doc_limit(doc, :infinity)
end
@@ -646,16 +605,21 @@ defmodule Inspect.Algebra do
"""
@spec concat([t]) :: t
def concat(docs) when is_list(docs) do
fold(docs, &concat(&1, &2))
fold_doc(docs, &concat(&1, &2))
end
@doc ~S"""
Colors a document with the given color (preceding the document itself).
Colors a document if the `color_key` has a color in the options.
"""
@doc since: "1.18.0"
@spec color(t, binary) :: t
def color(doc, color) when is_doc(doc) and is_binary(color) do
doc_color(doc, color)
@doc since: "1.4.0"
@spec color(t, Inspect.Opts.color_key(), Inspect.Opts.t()) :: t
def color(doc, color_key, %Inspect.Opts{syntax_colors: syntax_colors}) when is_doc(doc) do
if precolor = Keyword.get(syntax_colors, color_key) do
postcolor = Keyword.get(syntax_colors, :reset, :reset)
concat(doc_color(doc, precolor), doc_color(empty(), postcolor))
else
doc
end
end
@doc ~S"""
@@ -955,10 +919,6 @@ defmodule Inspect.Algebra do
@spec line(t, t) :: t
def line(doc1, doc2), do: concat(doc1, concat(line(), doc2))
# TODO: Deprecate me on Elixir v1.23
@doc deprecated: "Use fold/2 instead"
def fold_doc(docs, folder_fun), do: fold(docs, folder_fun)
@doc ~S"""
Folds a list of documents into a document using the given folder function.
@@ -970,22 +930,21 @@ defmodule Inspect.Algebra do
iex> docs = ["A", "B", "C"]
iex> docs =
...> Inspect.Algebra.fold(docs, fn doc, acc ->
...> Inspect.Algebra.fold_doc(docs, fn doc, acc ->
...> Inspect.Algebra.concat([doc, "!", acc])
...> end)
iex> Inspect.Algebra.format(docs, 80)
["A", "!", "B", "!", "C"]
"""
@doc since: "1.18.0"
@spec fold([t], (t, t -> t)) :: t
def fold(docs, folder_fun)
@spec fold_doc([t], (t, t -> t)) :: t
def fold_doc(docs, folder_fun)
def fold([], _folder_fun), do: empty()
def fold([doc], _folder_fun), do: doc
def fold_doc([], _folder_fun), do: empty()
def fold_doc([doc], _folder_fun), do: doc
def fold([doc | docs], folder_fun) when is_function(folder_fun, 2),
do: folder_fun.(doc, fold(docs, folder_fun))
def fold_doc([doc | docs], folder_fun) when is_function(folder_fun, 2),
do: folder_fun.(doc, fold_doc(docs, folder_fun))
@doc ~S"""
Formats a given document for a given width.
@@ -1015,17 +974,20 @@ defmodule Inspect.Algebra do
#
# * flat - represents a document with breaks as flats (a break may fit, as it may break)
# * break - represents a document with breaks as breaks (a break always fits, since it breaks)
#
# The following modes are exclusive to fitting:
#
# * flat_no_break - represents a document with breaks as flat not allowed to enter in break mode
# * break_no_flat - represents a document with breaks as breaks not allowed to enter in flat mode
#
@typep mode :: :flat | :flat_no_break | :break | :break_no_flat
@spec fits(
@spec fits?(
width :: non_neg_integer() | :infinity,
column :: non_neg_integer(),
break? :: boolean(),
entries
) :: :fit | :no_fit | :break_next
) :: boolean()
when entries:
maybe_improper_list({integer(), mode(), t()}, {:tail, boolean(), entries} | [])
@@ -1034,68 +996,63 @@ defmodule Inspect.Algebra do
#
# In case we have groups and the group fits, we need to consider the group
# parent without the child breaks, hence {:tail, b?, t} below.
defp fits(w, k, b?, _) when k > w and b?, do: :no_fit
defp fits(_, _, _, []), do: :fit
defp fits(w, k, _, {:tail, b?, t}), do: fits(w, k, b?, t)
defp fits?(w, k, b?, _) when k > w and b?, do: false
defp fits?(_, _, _, []), do: true
defp fits?(w, k, _, {:tail, b?, t}), do: fits?(w, k, b?, t)
## Flat no break
defp fits(w, k, b?, [{i, _, doc_fits(x, :disabled)} | t]),
do: fits(w, k, b?, [{i, :flat_no_break, x} | t])
defp fits?(w, k, b?, [{i, _, doc_fits(x, :disabled)} | t]),
do: fits?(w, k, b?, [{i, :flat_no_break, x} | t])
defp fits(w, k, b?, [{i, :flat_no_break, doc_fits(x, _)} | t]),
do: fits(w, k, b?, [{i, :flat_no_break, x} | t])
defp fits?(w, k, b?, [{i, :flat_no_break, doc_fits(x, _)} | t]),
do: fits?(w, k, b?, [{i, :flat_no_break, x} | t])
## Breaks no flat
defp fits(w, k, b?, [{i, _, doc_fits(x, :enabled)} | t]),
do: fits(w, k, b?, [{i, :break_no_flat, x} | t])
defp fits?(w, k, b?, [{i, _, doc_fits(x, :enabled)} | t]),
do: fits?(w, k, b?, [{i, :break_no_flat, x} | t])
defp fits(w, k, b?, [{i, :break_no_flat, doc_force(x)} | t]),
do: fits(w, k, b?, [{i, :break_no_flat, x} | t])
defp fits?(w, k, b?, [{i, :break_no_flat, doc_force(x)} | t]),
do: fits?(w, k, b?, [{i, :break_no_flat, x} | t])
defp fits(w, k, b?, [{i, :break_no_flat, x} | t])
when x == :doc_line or (is_tuple(x) and elem(x, 0) == :doc_break) do
case fits(w, k, b?, [{i, :flat, x} | t]) do
:no_fit -> :break_next
fits -> fits
end
end
defp fits?(_, _, _, [{_, :break_no_flat, doc_break(_, _)} | _]), do: true
defp fits?(_, _, _, [{_, :break_no_flat, :doc_line} | _]), do: true
## Breaks
defp fits(_, _, _, [{_, :break, doc_break(_, _)} | _]), do: :fit
defp fits(_, _, _, [{_, :break, :doc_line} | _]), do: :fit
defp fits?(_, _, _, [{_, :break, doc_break(_, _)} | _]), do: true
defp fits?(_, _, _, [{_, :break, :doc_line} | _]), do: true
defp fits(w, k, b?, [{i, :break, doc_group(x, _)} | t]),
do: fits(w, k, b?, [{i, :flat, x} | {:tail, b?, t}])
defp fits?(w, k, b?, [{i, :break, doc_group(x, _)} | t]),
do: fits?(w, k, b?, [{i, :flat, x} | {:tail, b?, t}])
## Catch all
defp fits(w, _, _, [{i, _, :doc_line} | t]), do: fits(w, i, false, t)
defp fits(w, k, b?, [{_, _, :doc_nil} | t]), do: fits(w, k, b?, t)
defp fits(w, _, b?, [{i, _, doc_collapse(_)} | t]), do: fits(w, i, b?, t)
defp fits(w, k, b?, [{i, m, doc_color(x, _)} | t]), do: fits(w, k, b?, [{i, m, x} | t])
defp fits(w, k, b?, [{_, _, doc_string(_, l)} | t]), do: fits(w, k + l, b?, t)
defp fits(w, k, b?, [{_, _, s} | t]) when is_binary(s), do: fits(w, k + byte_size(s), b?, t)
defp fits(_, _, _, [{_, _, doc_force(_)} | _]), do: :no_fit
defp fits(w, k, _, [{_, _, doc_break(s, _)} | t]), do: fits(w, k + byte_size(s), true, t)
defp fits(w, k, b?, [{i, m, doc_nest(x, _, :break)} | t]), do: fits(w, k, b?, [{i, m, x} | t])
defp fits?(w, _, _, [{i, _, :doc_line} | t]), do: fits?(w, i, false, t)
defp fits?(w, k, b?, [{_, _, :doc_nil} | t]), do: fits?(w, k, b?, t)
defp fits?(w, _, b?, [{i, _, doc_collapse(_)} | t]), do: fits?(w, i, b?, t)
defp fits?(w, k, b?, [{i, m, doc_color(x, _)} | t]), do: fits?(w, k, b?, [{i, m, x} | t])
defp fits?(w, k, b?, [{_, _, doc_string(_, l)} | t]), do: fits?(w, k + l, b?, t)
defp fits?(w, k, b?, [{_, _, s} | t]) when is_binary(s), do: fits?(w, k + byte_size(s), b?, t)
defp fits?(_, _, _, [{_, _, doc_force(_)} | _]), do: false
defp fits?(w, k, _, [{_, _, doc_break(s, _)} | t]), do: fits?(w, k + byte_size(s), true, t)
defp fits?(w, k, b?, [{i, m, doc_nest(x, _, :break)} | t]), do: fits?(w, k, b?, [{i, m, x} | t])
defp fits(w, k, b?, [{i, m, doc_nest(x, j, _)} | t]),
do: fits(w, k, b?, [{apply_nesting(i, k, j), m, x} | t])
defp fits?(w, k, b?, [{i, m, doc_nest(x, j, _)} | t]),
do: fits?(w, k, b?, [{apply_nesting(i, k, j), m, x} | t])
defp fits(w, k, b?, [{i, m, doc_cons(x, y)} | t]),
do: fits(w, k, b?, [{i, m, x}, {i, m, y} | t])
defp fits?(w, k, b?, [{i, m, doc_cons(x, y)} | t]),
do: fits?(w, k, b?, [{i, m, x}, {i, m, y} | t])
defp fits(w, k, b?, [{i, m, doc_group(x, _)} | t]),
do: fits(w, k, b?, [{i, m, x} | {:tail, b?, t}])
defp fits?(w, k, b?, [{i, m, doc_group(x, _)} | t]),
do: fits?(w, k, b?, [{i, m, x} | {:tail, b?, t}])
defp fits(w, k, b?, [{i, m, doc_limit(x, :infinity)} | t]) when w != :infinity,
do: fits(:infinity, k, b?, [{i, :flat, x}, {i, m, doc_limit(empty(), w)} | t])
defp fits?(w, k, b?, [{i, m, doc_limit(x, :infinity)} | t]) when w != :infinity,
do: fits?(:infinity, k, b?, [{i, :flat, x}, {i, m, doc_limit(empty(), w)} | t])
defp fits(_w, k, b?, [{i, m, doc_limit(x, w)} | t]),
do: fits(w, k, b?, [{i, m, x} | t])
defp fits?(_w, k, b?, [{i, m, doc_limit(x, w)} | t]),
do: fits?(w, k, b?, [{i, m, x} | t])
@spec format(
width :: non_neg_integer() | :infinity,
@@ -1106,14 +1063,10 @@ defmodule Inspect.Algebra do
defp format(w, k, [{_, _, :doc_nil} | t]), do: format(w, k, t)
defp format(w, _, [{i, _, :doc_line} | t]), do: [indent(i) | format(w, i, t)]
defp format(w, k, [{i, m, doc_cons(x, y)} | t]), do: format(w, k, [{i, m, x}, {i, m, y} | t])
defp format(w, k, [{i, m, doc_color(x, c)} | t]), do: [c | format(w, k, [{i, m, x} | t])]
defp format(w, k, [{i, m, doc_color(x, c)} | t]), do: [ansi(c) | format(w, k, [{i, m, x} | t])]
defp format(w, k, [{_, _, doc_string(s, l)} | t]), do: [s | format(w, k + l, t)]
defp format(w, k, [{_, _, s} | t]) when is_binary(s), do: [s | format(w, k + byte_size(s), t)]
defp format(w, k, [{i, m, doc_force(x)} | t]), do: format(w, k, [{i, m, x} | t])
defp format(w, k, [{i, :flat_no_break, doc_fits(x, :enabled)} | t]),
do: format(w, k, [{i, :break_no_flat, x} | t])
defp format(w, k, [{i, m, doc_fits(x, _)} | t]), do: format(w, k, [{i, m, x} | t])
defp format(w, _, [{i, _, doc_collapse(max)} | t]), do: collapse(format(w, i, t), max, 0, i)
@@ -1121,7 +1074,7 @@ defmodule Inspect.Algebra do
defp format(w, k, [{i, m, doc_break(s, :flex)} | t]) do
k = k + byte_size(s)
if w == :infinity or m == :flat or fits(w, k, true, t) != :no_fit do
if w == :infinity or m == :flat or fits?(w, k, true, t) do
[s | format(w, k, t)]
else
[indent(i) | format(w, i, t)]
@@ -1151,17 +1104,11 @@ defmodule Inspect.Algebra do
format(w, k, [{i, :break, x} | t])
end
defp format(w, k, [{i, :break_no_flat, doc_group(x, _)} | t]) do
format(w, k, [{i, :break, x} | t])
end
defp format(w, k, [{i, _, doc_group(x, _)} | t]) do
fits = if w == :infinity, do: :fit, else: fits(w, k, false, [{i, :flat, x}])
case fits do
:fit -> format(w, k, [{i, :flat, x} | t])
:no_fit -> format(w, k, [{i, :break, x} | t])
:break_next -> format(w, k, [{i, :flat_no_break, x} | t])
if w == :infinity or fits?(w, k, false, [{i, :flat, x}]) do
format(w, k, [{i, :flat, x} | t])
else
format(w, k, [{i, :break, x} | t])
end
end
@@ -1190,6 +1137,10 @@ defmodule Inspect.Algebra do
defp apply_nesting(_, _, :reset), do: 0
defp apply_nesting(i, _, j), do: i + j
defp ansi(color) do
IO.ANSI.format_fragment(color, true)
end
defp indent(0), do: @newline
defp indent(i), do: @newline <> :binary.copy(" ", i)
end
+17 -20
View File
@@ -161,7 +161,7 @@ defmodule Integer do
@doc since: "1.4.0"
@spec floor_div(integer, neg_integer | pos_integer) :: integer
def floor_div(dividend, divisor) do
if :erlang.xor(dividend < 0, divisor < 0) and rem(dividend, divisor) != 0 do
if dividend * divisor < 0 and rem(dividend, divisor) != 0 do
div(dividend, divisor) - 1
else
div(dividend, divisor)
@@ -189,16 +189,13 @@ defmodule Integer do
@spec digits(integer, pos_integer) :: [integer, ...]
def digits(integer, base \\ 10)
when is_integer(integer) and is_integer(base) and base >= 2 do
case integer do
0 -> [0]
_integer -> digits(integer, base, [])
end
do_digits(integer, base, [])
end
defp digits(0, _base, acc), do: acc
defp do_digits(integer, base, acc) when abs(integer) < base, do: [integer | acc]
defp digits(integer, base, acc),
do: digits(div(integer, base), base, [rem(integer, base) | acc])
defp do_digits(integer, base, acc),
do: do_digits(div(integer, base), base, [rem(integer, base) | acc])
@doc """
Returns the integer represented by the ordered `digits`.
@@ -220,16 +217,16 @@ defmodule Integer do
"""
@spec undigits([integer], pos_integer) :: integer
def undigits(digits, base \\ 10) when is_list(digits) and is_integer(base) and base >= 2 do
undigits(digits, base, 0)
do_undigits(digits, base, 0)
end
defp undigits([], _base, acc), do: acc
defp do_undigits([], _base, acc), do: acc
defp undigits([digit | _], base, _) when is_integer(digit) and digit >= base,
defp do_undigits([digit | _], base, _) when is_integer(digit) and digit >= base,
do: raise(ArgumentError, "invalid digit #{digit} in base #{base}")
defp undigits([digit | tail], base, acc) when is_integer(digit),
do: undigits(tail, base, acc * base + digit)
defp do_undigits([digit | tail], base, acc) when is_integer(digit),
do: do_undigits(tail, base, acc * base + digit)
@doc """
Parses a text representation of an integer.
@@ -365,25 +362,25 @@ defmodule Integer do
## Examples
iex> Integer.to_charlist(123)
~c"123"
'123'
iex> Integer.to_charlist(+456)
~c"456"
'456'
iex> Integer.to_charlist(-789)
~c"-789"
'-789'
iex> Integer.to_charlist(0123)
~c"123"
'123'
iex> Integer.to_charlist(100, 16)
~c"64"
'64'
iex> Integer.to_charlist(-100, 16)
~c"-64"
'-64'
iex> Integer.to_charlist(882_681_651, 36)
~c"ELIXIR"
'ELIXIR'
"""
@spec to_charlist(integer, 2..36) :: charlist
+41 -80
View File
@@ -12,8 +12,6 @@ defmodule IO do
(as shown in typespecs). For more information on chardata, see the
"IO data" section below.
The functions of this module use UNIX-style naming where possible.
## IO devices
An IO device may be an atom or a PID. In case it is an atom,
@@ -130,17 +128,8 @@ defmodule IO do
@doc """
Reads from the IO `device`.
The `device` is iterated as specified by the `line_or_chars` argument:
* if `line_or_chars` is an integer, it represents a number of bytes. The device is
iterated by that number of bytes. This should be the preferred mode for reading
non-textual inputs.
* if `line_or_chars` is `:line`, the device is iterated line by line.
CRFL newlines ("\r\n") are automatically normalized to "\n".
* if `line_or_chars` is `:eof` (since v1.13), the device is iterated until `:eof`.
If the device is already at the end, it returns `:eof` itself.
The `device` is iterated by the given number of characters, line by line if
`:line` is given, or until `:eof`.
It returns:
@@ -156,10 +145,8 @@ defmodule IO do
@spec read(device, :eof | :line | non_neg_integer) :: chardata | nodata
def read(device \\ :stdio, line_or_chars)
# TODO: Remove me on v2.0
# TODO: Deprecate me on v1.17
def read(device, :all) do
IO.warn("IO.read(device, :all) is deprecated, use IO.read(device, :eof) instead")
with :eof <- read(device, :eof) do
with [_ | _] = opts <- :io.getopts(device),
false <- Keyword.get(opts, :binary, true) do
@@ -188,14 +175,14 @@ defmodule IO do
The `device` is iterated as specified by the `line_or_chars` argument:
* if `line_or_chars` is an integer, it represents a number of bytes. The device is
iterated by that number of bytes. This should be the preferred mode for reading
non-textual inputs.
iterated by that number of bytes.
* if `line_or_chars` is `:line`, the device is iterated line by line.
CRFL newlines ("\r\n") are automatically normalized to "\n".
* if `line_or_chars` is `:eof` (since v1.13), the device is iterated until `:eof`.
If the device is already at the end, it returns `:eof` itself.
* if `line_or_chars` is `:eof`, the device is iterated until `:eof`. `line_or_chars`
can only be `:eof` since Elixir 1.13.0. `:eof` replaces the deprecated `:all`,
with the difference that `:all` returns `""` on end of file, while `:eof` returns
`:eof` itself.
It returns:
@@ -213,9 +200,8 @@ defmodule IO do
@spec binread(device, :eof | :line | non_neg_integer) :: iodata | nodata
def binread(device \\ :stdio, line_or_chars)
# TODO: Remove me on v2.0
# TODO: Deprecate me on v1.17
def binread(device, :all) do
IO.warn("IO.binread(device, :all) is deprecated, use IO.binread(device, :eof) instead")
with :eof <- binread(device, :eof), do: ""
end
@@ -282,11 +268,9 @@ defmodule IO do
to stdio with this function will likely result in the wrong data
being sent down the wire.
"""
@spec binwrite(device, iodata) :: :ok
@spec binwrite(device, iodata) :: :ok | {:error, term}
def binwrite(device \\ :stdio, iodata) when is_iodata(iodata) do
with {:error, reason} <- :file.write(map_dev(device), iodata) do
:erlang.error(reason)
end
:file.write(map_dev(device), iodata)
end
@doc """
@@ -296,8 +280,6 @@ defmodule IO do
By default, the `device` is the standard output. It returns `:ok`
if it succeeds.
Trivia: `puts` is shorthand for `put string`.
## Examples
IO.puts("Hello World!")
@@ -324,20 +306,17 @@ defmodule IO do
entry from the compilation environment will be used
* a keyword list with at least the `:file` option representing
a single stacktrace entry (since v1.14.0). The `:line`, `:column`,
`:module`, and `:function` options are also supported
a single stacktrace entry (since v1.14.0). The `:line`, `:module`,
`:function` options are also supported
This function notifies the compiler a warning was printed
and emits a compiler diagnostic (`t:Code.diagnostic/1`).
The diagnostic will include precise file and location information
if a `Macro.Env` is given or those values have been passed as
keyword list, but not for stacktraces, as they are often imprecise.
It returns `:ok` if it succeeds.
This function also notifies the compiler a warning was printed
(in case --warnings-as-errors was enabled). It returns `:ok`
if it succeeds.
## Examples
IO.warn("variable bar is unused", module: MyApp, function: {:main, 1}, line: 4, file: "my_app.ex")
stacktrace = [{MyApp, :main, 1, [file: 'my_app.ex', line: 4]}]
IO.warn("variable bar is unused", stacktrace)
#=> warning: variable bar is unused
#=> my_app.ex:4: MyApp.main/1
@@ -346,46 +325,37 @@ defmodule IO do
:ok
def warn(message, stacktrace_info)
def warn(message, %Macro.Env{line: line, file: file} = env) do
message = to_chardata(message)
def warn(message, %Macro.Env{} = env) do
warn(message, Macro.Env.stacktrace(env))
end
:elixir_errors.emit_diagnostic(:warning, line, file, message, Macro.Env.stacktrace(env),
read_snippet: true
)
def warn(message, []) do
:elixir_errors.emit_diagnostic(:warning, 0, nil, to_chardata(message), [])
end
def warn(message, [{_, _} | _] = keyword) do
if file = keyword[:file] do
line = keyword[:line]
column = keyword[:column]
position = if line && column, do: {line, column}, else: line
message = to_chardata(message)
stacktrace =
Macro.Env.stacktrace(%{
warn(
message,
%{
__ENV__
| module: keyword[:module],
function: keyword[:function],
line: line,
line: keyword[:line],
file: file
})
:elixir_errors.emit_diagnostic(:warning, position, file, message, stacktrace,
read_snippet: true
}
)
else
warn(message, [])
end
end
def warn(message, []) do
def warn(message, [{_, _, _, opts} | _] = stacktrace) do
message = to_chardata(message)
:elixir_errors.emit_diagnostic(:warning, 0, nil, message, [], read_snippet: false)
end
def warn(message, [{_, _, _, _} | _] = stacktrace) do
message = to_chardata(message)
:elixir_errors.emit_diagnostic(:warning, 0, nil, message, stacktrace, read_snippet: false)
line = opts[:line]
file = opts[:file]
file = file && List.to_string(file)
:elixir_errors.emit_diagnostic(:warning, line || 0, file, message, stacktrace)
end
@doc false
@@ -394,7 +364,7 @@ defmodule IO do
stacktrace = Enum.drop(stacktrace, stacktrace_drop_levels)
if :elixir_config.warn(key, stacktrace) do
warn(message.(), stacktrace)
warn(message, stacktrace)
else
:ok
end
@@ -424,7 +394,7 @@ defmodule IO do
end
@doc """
Inspects and writes the given `item` to the standard output.
Inspects and writes the given `item` to the device.
It's important to note that it returns the given `item` unchanged.
This makes it possible to "spy" on values by inserting an
@@ -440,7 +410,6 @@ defmodule IO do
The label will be printed before the inspected `item`.
See `Inspect.Opts` for a full list of remaining formatting options.
To print to other IO devices, see `IO.inspect/3`
## Examples
@@ -573,8 +542,6 @@ defmodule IO do
for instance, `{:error, :estale}` if reading from an
NFS volume
Trivia: `gets` is shorthand for `get string`.
## Examples
To display "What is your name?" as a prompt and await user input:
@@ -596,7 +563,6 @@ defmodule IO do
"""
@doc since: "1.12.0"
@spec stream() :: Enumerable.t(String.t())
def stream, do: stream(:stdio, :line)
@doc """
@@ -606,9 +572,8 @@ defmodule IO do
`Collectable`, allowing it to be used for both read
and write.
The `device` is iterated by the given number of characters
or line by line if `:line` is given. In case `:line` is given,
"\r\n" is automatically normalized to "\n".
The `device` is iterated by the given number of characters or line by line if
`:line` is given.
This reads from the IO as UTF-8. Check out
`IO.binstream/2` to handle the IO as a raw binary.
@@ -651,7 +616,6 @@ defmodule IO do
"""
@doc since: "1.12.0"
@spec binstream() :: Enumerable.t(binary)
def binstream, do: binstream(:stdio, :line)
@doc """
@@ -661,17 +625,14 @@ defmodule IO do
`Collectable`, allowing it to be used for both read
and write.
The `device` is iterated by the given number of bytes or line
by line if `:line` is given. In case `:line` is given, "\r\n"
is automatically normalized to "\n". Passing the number of bytes
should be the preferred mode for reading non-textual inputs.
The `device` is iterated by the given number of bytes or line by line if
`:line` is given. This reads from the IO device as a raw binary.
Note that an IO stream has side effects and every time
you go over the stream you may get different results.
This reads from the IO device as a raw binary. Therefore,
do not use this function on IO devices in Unicode mode as
it will return the wrong result.
Finally, do not use this function on IO devices in Unicode
mode as it will return the wrong result.
`binstream/0` has been introduced in Elixir v1.12.0,
while `binstream/2` has been available since v1.0.0.
+2 -5
View File
@@ -268,14 +268,12 @@ defmodule IO.ANSI do
The named sequences are represented by atoms.
It will also append an `IO.ANSI.reset/0` to the chardata when a conversion is
performed. If you don't want this behavior, use `format_fragment/2`.
performed. If you don't want this behaviour, use `format_fragment/2`.
An optional boolean parameter can be passed to enable or disable
emitting actual ANSI codes. When `false`, no ANSI codes will be emitted.
By default checks if ANSI is enabled using the `enabled?/0` function.
An `ArgumentError` will be raised if an invalid ANSI code is provided.
## Examples
iex> IO.ANSI.format(["Hello, ", :red, :bright, "world!"], true)
@@ -299,7 +297,7 @@ defmodule IO.ANSI do
## Examples
iex> IO.ANSI.format_fragment([:bright, ~c"Word"], true)
iex> IO.ANSI.format_fragment([:bright, 'Word'], true)
[[[[[[] | "\e[1m"], 87], 111], 114], 100]
"""
@@ -317,7 +315,6 @@ defmodule IO.ANSI do
end
defp do_format(term, rem, acc, false, append_reset) when is_atom(term) do
format_sequence(term)
do_format([], rem, acc, false, append_reset)
end
+204 -15
View File
@@ -50,10 +50,6 @@ defmodule IO.ANSI.Docs do
"""
@spec print_headings([String.t()], keyword) :: :ok
def print_headings(headings, options \\ []) do
# It's possible for some of the headings to contain newline characters (`\n`), so in order to prevent it from
# breaking the output from `print_headings/2`, as `print_headings/2` tries to pad the whole heading, we first split
# any heading containgin newline characters into multiple headings, that way each one is padded on its own.
headings = Enum.flat_map(headings, fn heading -> String.split(heading, "\n") end)
options = Keyword.merge(default_options(), options)
newline_after_block(options)
width = options[:width]
@@ -118,10 +114,196 @@ defmodule IO.ANSI.Docs do
print_markdown(doc, options)
end
def print(doc, "application/erlang+html", options) when is_list(options) do
print_erlang_html(doc, options)
end
def print(_doc, format, options) when is_binary(format) and is_list(options) do
IO.puts("\nUnknown documentation format #{inspect(format)}\n")
end
## Erlang+html
def print_erlang_html(doc, options) do
options = Keyword.merge(default_options(), options)
IO.write(traverse_erlang_html(doc, "", options))
end
defp traverse_erlang_html(text, _indent, _options) when is_binary(text) do
text
end
defp traverse_erlang_html(nodes, indent, options) when is_list(nodes) do
for node <- nodes do
traverse_erlang_html(node, indent, options)
end
end
defp traverse_erlang_html({:div, [class: class] ++ _, entries}, indent, options) do
prefix = indent <> quote_prefix(options)
content =
entries
|> traverse_erlang_html(indent, options)
|> IO.iodata_to_binary()
|> String.trim_trailing()
[
prefix,
class |> to_string() |> String.upcase(),
"\n#{prefix}\n#{prefix}" | String.replace(content, "\n", "\n#{prefix}")
]
|> newline_cons()
end
defp traverse_erlang_html({:p, _, entries}, indent, options) do
[indent | handle_erlang_html_text(entries, indent, options)]
end
defp traverse_erlang_html({:h1, _, entries}, indent, options) do
entries |> traverse_erlang_html(indent, options) |> heading(1, options) |> newline_cons()
end
defp traverse_erlang_html({:h2, _, entries}, indent, options) do
entries |> traverse_erlang_html(indent, options) |> heading(2, options) |> newline_cons()
end
defp traverse_erlang_html({:h3, _, entries}, indent, options) do
entries |> traverse_erlang_html(indent, options) |> heading(3, options) |> newline_cons()
end
defp traverse_erlang_html({:h4, _, entries}, indent, options) do
entries |> traverse_erlang_html(indent, options) |> heading(4, options) |> newline_cons()
end
defp traverse_erlang_html({:h5, _, entries}, indent, options) do
entries |> traverse_erlang_html(indent, options) |> heading(5, options) |> newline_cons()
end
defp traverse_erlang_html({:h6, _, entries}, indent, options) do
entries |> traverse_erlang_html(indent, options) |> heading(6, options) |> newline_cons()
end
defp traverse_erlang_html({:br, _, []}, _indent, _options) do
[]
end
defp traverse_erlang_html({:i, _, entries}, indent, options) do
inline_text("_", traverse_erlang_html(entries, indent, options), options)
end
defp traverse_erlang_html({:em, _, entries}, indent, options) do
inline_text("*", traverse_erlang_html(entries, indent, options), options)
end
defp traverse_erlang_html({tag, _, entries}, indent, options) when tag in [:strong, :b] do
inline_text("**", traverse_erlang_html(entries, indent, options), options)
end
defp traverse_erlang_html({:code, _, entries}, indent, options) do
inline_text("`", traverse_erlang_html(entries, indent, options), options)
end
defp traverse_erlang_html({:pre, _, [{:code, _, entries}]}, indent, options) do
string =
entries
|> traverse_erlang_html(indent, options)
|> IO.iodata_to_binary()
["#{indent} ", String.replace(string, "\n", "\n#{indent} ")] |> newline_cons()
end
defp traverse_erlang_html({:a, attributes, entries}, indent, options) do
if href = attributes[:href] do
[traverse_erlang_html(entries, indent, options), ?\s, ?(, href, ?)]
else
traverse_erlang_html(entries, indent, options)
end
end
defp traverse_erlang_html({:dl, _, entries}, indent, options) do
traverse_erlang_html(entries, indent, options)
end
defp traverse_erlang_html({:dt, _, entries}, indent, options) do
[
"#{indent} ",
bullet_text(options) | handle_erlang_html_text(entries, indent <> " ", options)
]
end
defp traverse_erlang_html({:dd, _, entries}, indent, options) do
["#{indent} " | handle_erlang_html_text(entries, indent <> " ", options)]
end
defp traverse_erlang_html({:ul, attributes, entries}, indent, options) do
if attributes[:class] == "types" do
types =
for {:li, _, lines} <- entries,
line <- lines,
do: ["#{indent} ", traverse_erlang_html(line, indent <> " ", options), ?\n]
if types != [] do
["#{indent}Typespecs:\n\n", types, ?\n]
else
[]
end
else
for {:li, _, lines} <- entries do
[
"#{indent} ",
bullet_text(options) | handle_erlang_html_text(lines, indent <> " ", options)
]
end
end
end
defp traverse_erlang_html({:ol, _, entries}, indent, options) do
for {{:li, _, lines}, i} <- Enum.with_index(entries, 1) do
[
"#{indent} ",
Integer.to_string(i),
". " | handle_erlang_html_text(lines, indent <> " ", options)
]
end
end
defp traverse_erlang_html({tag, _, entries}, indent, options) do
[
indent <> "<#{tag}>\n",
traverse_erlang_html(entries, indent <> " ", options)
|> IO.iodata_to_binary()
|> String.trim_trailing(),
"\n" <> indent <> "</#{tag}>"
]
|> newline_cons()
end
defp newline_cons(text) do
[text | "\n\n"]
end
defp handle_erlang_html_text(entries, indent, options) do
if Enum.all?(entries, &inline_html?/1) do
entries
|> traverse_erlang_html(indent, options)
|> IO.iodata_to_binary()
|> String.split(@spaces)
|> wrap_text(options[:width], indent, true, "", [])
|> tl()
|> newline_cons()
else
entries
|> traverse_erlang_html(indent, options)
|> IO.iodata_to_binary()
|> String.trim_leading()
end
end
defp inline_html?(binary) when is_binary(binary), do: true
defp inline_html?({tag, _, _}) when tag in [:a, :code, :em, :i, :strong, :b, :br], do: true
defp inline_html?(_), do: false
## Markdown
def print_markdown(doc, options) do
@@ -176,19 +358,14 @@ defmodule IO.ANSI.Docs do
process_code(rest, [line], indent, options)
end
defp process(["```mermaid" <> _line | rest], text, indent, options) do
write_text(text, indent, options)
rest
|> Enum.drop_while(&(&1 != "```"))
|> Enum.drop(1)
|> process([], indent, options)
end
defp process(["```" <> _line | rest], text, indent, options) do
process_fenced_code_block(rest, text, indent, options, _delimiter = "```")
end
defp process(["~~~" <> _line | rest], text, indent, options) do
process_fenced_code_block(rest, text, indent, options, _delimiter = "~~~")
end
defp process(["<!--" <> line | rest], text, indent, options) do
process(drop_comment([line | rest]), text, indent, options)
end
@@ -221,6 +398,10 @@ defmodule IO.ANSI.Docs do
### Quotes
defp process_quote([], lines, indent, options) do
write_quote(lines, indent, options, false)
end
defp process_quote([">", ">" <> line | rest], lines, indent, options) do
write_quote(lines, indent, options, true)
write_empty_quote_line(options)
@@ -350,7 +531,7 @@ defmodule IO.ANSI.Docs do
|> String.split(@spaces)
|> write_with_wrap(options[:width] - byte_size(indent), indent, no_wrap, prefix)
if !no_wrap, do: newline_after_block(options)
unless no_wrap, do: newline_after_block(options)
end
defp format_text(text, options) do
@@ -361,6 +542,10 @@ defmodule IO.ANSI.Docs do
### Code blocks
defp process_code([], code, indent, options) do
write_code(code, indent, options)
end
# Blank line between code blocks
defp process_code(["", " " <> line | rest], code, indent, options) do
process_code(rest, [line, "" | code], indent, options)
@@ -385,7 +570,7 @@ defmodule IO.ANSI.Docs do
end
defp process_fenced_code([line | rest], code, indent, options, delimiter) do
if line == delimiter do
if line === delimiter do
process_code(rest, code, indent, options)
else
process_fenced_code(rest, [line | code], indent, options, delimiter)
@@ -764,6 +949,10 @@ defmodule IO.ANSI.Docs do
defp quote_prefix(options), do: "#{color(:doc_quote, options)}> #{maybe_reset(options)}"
defp heading(text, n, options) do
[color(:doc_headings, options), String.duplicate("#", n), " ", text, maybe_reset(options)]
end
defp inline_text(mark, text, options) do
if options[:enabled] do
[[color_for(mark, options) | text] | IO.ANSI.reset()]
+5 -3
View File
@@ -1,9 +1,11 @@
defmodule IO.StreamError do
defexception [:reason]
defexception [:reason, :message]
@impl true
def message(%{reason: reason}) do
"error during streaming: #{inspect(reason)}"
def exception(opts) do
reason = opts[:reason]
formatted = IO.iodata_to_binary(:file.format_error(reason))
%IO.StreamError{message: "error during streaming: #{formatted}", reason: reason}
end
end
-430
View File
@@ -1,430 +0,0 @@
defprotocol JSON.Encoder do
@moduledoc """
A protocol for custom JSON encoding of data structures.
If you have a struct, you can derive the implementation of this protocol
by specifying which fields should be encoded to JSON:
@derive {JSON.Encoder, only: [....]}
defstruct ...
It is also possible to encode all fields or skip some fields via the
`:except` option:
@derive JSON.Encoder
defstruct ...
> #### Leaking Private Information {: .error}
>
> The `:except` approach should be used carefully to avoid
> accidentally leaking private information when new fields are added.
Finally, if you don't own the struct you want to encode to JSON,
you may use `Protocol.derive/3` placed outside of any module:
Protocol.derive(JSON.Encoder, NameOfTheStruct, only: [...])
Protocol.derive(JSON.Encoder, NameOfTheStruct)
"""
@undefined_impl_description """
the protocol must be explicitly implemented.
If you have a struct, you can derive the implementation specifying \
which fields should be encoded to JSON:
@derive {JSON.Encoder, only: [....]}
defstruct ...
It is also possible to encode all fields, although this should be \
used carefully to avoid accidentally leaking private information \
when new fields are added:
@derive JSON.Encoder
defstruct ...
Finally, if you don't own the struct you want to encode to JSON, \
you may use Protocol.derive/3 placed outside of any module:
Protocol.derive(JSON.Encoder, NameOfTheStruct, only: [...])
Protocol.derive(JSON.Encoder, NameOfTheStruct)\
"""
@impl true
defmacro __deriving__(module, opts) do
fields = module |> Macro.struct_info!(__CALLER__) |> Enum.map(& &1.field)
fields = fields_to_encode(fields, opts)
vars = Macro.generate_arguments(length(fields), __MODULE__)
kv = Enum.zip(fields, vars)
{io, _prefix} =
Enum.flat_map_reduce(kv, ?{, fn {field, value}, prefix ->
key = IO.iodata_to_binary([prefix, :elixir_json.encode_binary(Atom.to_string(field)), ?:])
{[key, quote(do: encoder.(unquote(value), encoder))], ?,}
end)
io = if io == [], do: "{}", else: io ++ [?}]
quote do
defimpl JSON.Encoder, for: unquote(module) do
def encode(%{unquote_splicing(kv)}, encoder) do
unquote(io)
end
end
end
end
defp fields_to_encode(fields, opts) do
cond do
only = Keyword.get(opts, :only) ->
case only -- fields do
[] ->
only
error_keys ->
raise ArgumentError,
"unknown struct fields #{inspect(error_keys)} specified in :only. Expected one of: " <>
"#{inspect(fields -- [:__struct__])}"
end
except = Keyword.get(opts, :except) ->
case except -- fields do
[] ->
fields -- [:__struct__ | except]
error_keys ->
raise ArgumentError,
"unknown struct fields #{inspect(error_keys)} specified in :except. Expected one of: " <>
"#{inspect(fields -- [:__struct__])}"
end
true ->
fields -- [:__struct__]
end
end
@doc """
A function invoked to encode the given term to `t:iodata/0`.
"""
def encode(term, encoder)
end
defimpl JSON.Encoder, for: Atom do
def encode(value, encoder) do
case value do
nil -> "null"
true -> "true"
false -> "false"
_ -> encoder.(Atom.to_string(value), encoder)
end
end
end
defimpl JSON.Encoder, for: BitString do
def encode(value, _encoder) do
:elixir_json.encode_binary(value)
end
end
defimpl JSON.Encoder, for: List do
def encode(value, encoder) do
:elixir_json.encode_list(value, encoder)
end
end
defimpl JSON.Encoder, for: Integer do
def encode(value, _encoder) do
:elixir_json.encode_integer(value)
end
end
defimpl JSON.Encoder, for: Float do
def encode(value, _encoder) do
:elixir_json.encode_float(value)
end
end
defimpl JSON.Encoder, for: Map do
def encode(value, encoder) do
case :maps.next(:maps.iterator(value)) do
:none ->
"{}"
{key, value, iterator} ->
[?{, key(key, encoder), ?:, encoder.(value, encoder) | next(iterator, encoder)]
end
end
defp next(iterator, encoder) do
case :maps.next(iterator) do
:none ->
"}"
{key, value, iterator} ->
[?,, key(key, encoder), ?:, encoder.(value, encoder) | next(iterator, encoder)]
end
end
# Erlang supports only numbers, binaries, and atoms as keys,
# we support anything that implements the String.Chars protocol.
defp key(key, encoder) when is_atom(key), do: encoder.(Atom.to_string(key), encoder)
defp key(key, encoder) when is_binary(key), do: encoder.(key, encoder)
defp key(key, encoder), do: encoder.(String.Chars.to_string(key), encoder)
end
defimpl JSON.Encoder, for: [Date, Time, NaiveDateTime, DateTime, Duration] do
def encode(value, _encoder) do
[?", @for.to_iso8601(value), ?"]
end
end
defmodule JSON.DecodeError do
@moduledoc """
The exception raised by `JSON.decode!/1`.
"""
defexception [:message, :offset, :data]
end
defmodule JSON do
@moduledoc ~S"""
JSON encoding and decoding.
Both encoder and decoder fully conform to [RFC 8259](https://tools.ietf.org/html/rfc8259) and
[ECMA 404](https://ecma-international.org/publications-and-standards/standards/ecma-404/)
standards.
## Encoding
Elixir built-in data structures are encoded to JSON as follows:
| **Elixir** | **JSON** |
|----------------------------|----------|
| `integer() \| float()` | Number |
| `true \| false ` | Boolean |
| `nil` | Null |
| `binary()` | String |
| `atom()` | String |
| `list()` | Array |
| `%{String.Chars.t() => _}` | Object |
You may also implement the `JSON.Encoder` protocol for custom data structures.
## Decoding
Elixir built-in data structures are decoded from JSON as follows:
| **JSON** | **Elixir** |
|----------|------------------------|
| Number | `integer() \| float()` |
| Boolean | `true \| false` |
| Null | `nil` |
| String | `binary()` |
| Object | `%{binary() => _}` |
"""
@moduledoc since: "1.18.0"
@type encoder :: (term(), encoder() -> iodata())
@type decode_error_reason ::
{:unexpected_end, non_neg_integer()}
| {:invalid_byte, non_neg_integer(), byte()}
| {:unexpected_sequence, non_neg_integer(), binary()}
@doc ~S"""
Decodes the given JSON.
Returns `{:ok, decoded}` or `{:error, reason}`.
## Examples
iex> JSON.decode("[null,123,\"string\",{\"key\":\"value\"}]")
{:ok, [nil, 123, "string", %{"key" => "value"}]}
## Error reasons
The error tuple will have one of the following reasons.
* `{:unexpected_end, offset}` if `binary` contains incomplete JSON value
* `{:invalid_byte, offset, byte}` if `binary` contains unexpected byte or invalid UTF-8 byte
* `{:unexpected_sequence, offset, bytes}` if `binary` contains invalid UTF-8 escape
"""
@spec decode(binary()) :: {:ok, term()} | {:error, decode_error_reason()}
def decode(binary) when is_binary(binary) do
with {decoded, :ok, rest} <- decode(binary, :ok, []) do
if rest == "" do
{:ok, decoded}
else
{:error, {:invalid_byte, byte_size(binary) - byte_size(rest), :binary.at(rest, 0)}}
end
end
end
@doc ~S"""
Decodes the given JSON with the given decoders.
Returns `{decoded, acc, rest}` or `{:error, reason}`.
See `decode/1` for the error reasons.
## Decoders
All decoders are optional. If not provided, they will fall back to
implementations used by the `decode/1` function:
* for `array_start`: `fn _ -> [] end`
* for `array_push`: `fn elem, acc -> [elem | acc] end`
* for `array_finish`: `fn acc, old_acc -> {Enum.reverse(acc), old_acc} end`
* for `object_start`: `fn _ -> [] end`
* for `object_push`: `fn key, value, acc -> [{key, value} | acc] end`
* for `object_finish`: `fn acc, old_acc -> {Map.new(acc), old_acc} end`
* for `float`: `&String.to_float/1`
* for `integer`: `&String.to_integer/1`
* for `string`: `&Function.identity/1`
* for `null`: the atom `nil`
For streaming decoding, see Erlang's `:json` module.
"""
@spec decode(binary(), term(), keyword()) ::
{term(), term(), binary()} | {:error, decode_error_reason()}
def decode(binary, acc, decoders) when is_binary(binary) and is_list(decoders) do
decoders = Keyword.put_new(decoders, :null, nil)
try do
:elixir_json.decode(binary, acc, Map.new(decoders))
catch
:error, :unexpected_end ->
{:error, {:unexpected_end, byte_size(binary)}}
:error, {:invalid_byte, byte} ->
{:error, {:invalid_byte, offset(__STACKTRACE__), byte}}
:error, {:unexpected_sequence, bytes} ->
{:error, {:unexpected_sequence, offset(__STACKTRACE__), bytes}}
end
end
defp offset(stacktrace) do
with [{_, _, _, opts} | _] <- stacktrace,
%{cause: %{position: position}} <- opts[:error_info] do
position
else
_ -> 0
end
end
@doc ~S"""
Decodes the given JSON but raises an exception in case of errors.
Returns the decoded content. See `decode/1` for possible errors.
## Examples
iex> JSON.decode!("[null,123,\"string\",{\"key\":\"value\"}]")
[nil, 123, "string", %{"key" => "value"}]
"""
@spec decode!(binary()) :: term()
def decode!(binary) when is_binary(binary) do
case decode(binary) do
{:ok, decoded} ->
decoded
{:error, {:unexpected_end, offset}} ->
raise JSON.DecodeError,
message: "unexpected end of JSON binary at position (byte offset) #{offset}",
data: binary,
offset: offset
{:error, {:invalid_byte, offset, byte}} ->
raise JSON.DecodeError,
message: "invalid byte #{byte} at position (byte offset) #{offset}",
data: binary,
offset: offset
{:error, {:unexpected_sequence, offset, bytes}} ->
raise JSON.DecodeError,
message: "unexpected sequence #{inspect(bytes)} at position (byte offset) #{offset}",
data: binary,
offset: offset
end
end
@doc ~S"""
Encodes the given term to JSON as a binary.
The second argument is a function that is recursively
invoked to encode a term.
> #### IO and performance {: .tip}
>
> If you need to encode data to be sent over the network
> or written to the filesystem, consider using the more
> efficient `encode_to_iodata!/2`.
## Examples
iex> JSON.encode!([123, "string", %{key: "value"}])
"[123,\"string\",{\"key\":\"value\"}]"
"""
@spec encode!(term(), encoder()) :: binary()
def encode!(term, encoder \\ &protocol_encode/2) do
IO.iodata_to_binary(encoder.(term, encoder))
end
@doc ~S"""
Encodes the given term to JSON as an iodata.
This is the most efficient format if the JSON is going to be
used for IO purposes.
The second argument is a function that is recursively
invoked to encode a term.
## Examples
iex> data = JSON.encode_to_iodata!([123, "string", %{key: "value"}])
iex> IO.iodata_to_binary(data)
"[123,\"string\",{\"key\":\"value\"}]"
"""
@spec encode_to_iodata!(term(), encoder()) :: iodata()
def encode_to_iodata!(term, encoder \\ &protocol_encode/2) do
encoder.(term, encoder)
end
@doc """
This is the default encode implementation passed to `encode!/1`.
This function is most typically passed as second argument to
`encode!/2` and `encode_to_iodata!/2`. The default implementation
is an optimized dispatch to the `JSON.Encoder` protocol.
"""
@spec protocol_encode(term(), encoder()) :: iodata()
def protocol_encode(value, encoder) when is_atom(value) do
case value do
nil -> "null"
true -> "true"
false -> "false"
_ -> encoder.(Atom.to_string(value), encoder)
end
end
def protocol_encode(value, _encoder) when is_binary(value),
do: :elixir_json.encode_binary(value)
def protocol_encode(value, _encoder) when is_integer(value),
do: :elixir_json.encode_integer(value)
def protocol_encode(value, _encoder) when is_float(value),
do: :elixir_json.encode_float(value)
def protocol_encode(value, encoder) when is_list(value),
do: :elixir_json.encode_list(value, encoder)
def protocol_encode(%{} = value, encoder) when not is_map_key(value, :__struct__),
do: JSON.Encoder.Map.encode(value, encoder)
def protocol_encode(value, encoder),
do: JSON.Encoder.encode(value, encoder)
end
+354 -605
View File
File diff suppressed because it is too large Load Diff
+13 -35
View File
@@ -9,7 +9,6 @@ defmodule Kernel.CLI do
compile: [],
no_halt: false,
compiler_options: [],
warnings_as_errors: false,
errors: [],
verbose_compile: false,
profile: nil,
@@ -196,7 +195,7 @@ defmodule Kernel.CLI do
end
@elixir_internals [:elixir, :elixir_aliases, :elixir_expand, :elixir_compiler, :elixir_module] ++
[:elixir_clauses, :elixir_lexical, :elixir_def, :elixir_map] ++
[:elixir_clauses, :elixir_lexical, :elixir_def, :elixir_map, :elixir_locals] ++
[:elixir_erl, :elixir_erl_clauses, :elixir_erl_compiler, :elixir_erl_pass] ++
[Kernel.ErrorHandler, Module.ParallelChecker]
@@ -295,16 +294,6 @@ defmodule Kernel.CLI do
parse_argv(t, %{config | commands: [{:parallel_require, h} | config.commands]})
end
defp parse_argv([~c"--color" | t], config) do
Application.put_env(:elixir, :ansi_enabled, true)
parse_argv(t, config)
end
defp parse_argv([~c"--no-color" | t], config) do
Application.put_env(:elixir, :ansi_enabled, false)
parse_argv(t, config)
end
## Compiler
defp parse_argv([~c"-o", h | t], %{mode: :elixirc} = config) do
@@ -326,7 +315,8 @@ defmodule Kernel.CLI do
end
defp parse_argv([~c"--warnings-as-errors" | t], %{mode: :elixirc} = config) do
parse_argv(t, %{config | warnings_as_errors: true})
compiler_options = [{:warnings_as_errors, true} | config.compiler_options]
parse_argv(t, %{config | compiler_options: compiler_options})
end
defp parse_argv([~c"--verbose" | t], %{mode: :elixirc} = config) do
@@ -337,8 +327,6 @@ defmodule Kernel.CLI do
parse_argv(t, %{config | profile: :time})
end
## IEx
defp parse_argv([~c"--dbg", backend | t], %{mode: :iex} = config) do
case backend do
~c"pry" ->
@@ -356,21 +344,6 @@ defmodule Kernel.CLI do
defp parse_argv([~c"--dot-iex", _ | t], %{mode: :iex} = config), do: parse_argv(t, config)
defp parse_argv([~c"--remsh", _ | t], %{mode: :iex} = config), do: parse_argv(t, config)
## Erlang flags
defp parse_argv([~c"--boot", _ | t], config), do: parse_argv(t, config)
defp parse_argv([~c"--boot-var", _, _ | t], config), do: parse_argv(t, config)
defp parse_argv([~c"--cookie", _ | t], config), do: parse_argv(t, config)
defp parse_argv([~c"--hidden" | t], config), do: parse_argv(t, config)
defp parse_argv([~c"--erl-config", _ | t], config), do: parse_argv(t, config)
defp parse_argv([~c"--logger-otp-reports", _ | t], config), do: parse_argv(t, config)
defp parse_argv([~c"--logger-sasl-reports", _ | t], config), do: parse_argv(t, config)
defp parse_argv([~c"--name", _ | t], config), do: parse_argv(t, config)
defp parse_argv([~c"--sname", _ | t], config), do: parse_argv(t, config)
defp parse_argv([~c"--vm-args", _ | t], config), do: parse_argv(t, config)
defp parse_argv([~c"--erl", _ | t], config), do: parse_argv(t, config)
defp parse_argv([~c"--pipe-to", _, _ | t], config), do: parse_argv(t, config)
## Fallback
defp parse_argv([h | t], %{mode: :elixirc} = config) do
@@ -489,7 +462,8 @@ defmodule Kernel.CLI do
defp process_command({:compile, patterns}, config) do
# If ensuring the dir returns an error no files will be found.
_ = :filelib.ensure_path(config.output)
# TODO: Use :filelib.ensure_path/1 once we require Erlang/OTP 25+
_ = :filelib.ensure_dir(:filename.join(config.output, "."))
case filter_multiple_patterns(patterns) do
{:ok, []} ->
@@ -509,11 +483,15 @@ defmodule Kernel.CLI do
]
end
output = IO.chardata_to_string(config.output)
profile_opts =
if config.profile do
[profile: config.profile]
else
[]
end
opts =
verbose_opts ++
[profile: config.profile, warnings_as_errors: config.warnings_as_errors]
output = IO.chardata_to_string(config.output)
opts = verbose_opts ++ profile_opts
case Kernel.ParallelCompiler.compile_to_path(files, output, opts) do
{:ok, _, _} -> :ok
+26 -41
View File
@@ -8,7 +8,6 @@ defmodule Kernel.LexicalTracker do
@moduledoc false
@timeout :infinity
@behaviour :gen_server
@warn_key 0
@doc """
Returns all references in this lexical scope.
@@ -17,24 +16,6 @@ defmodule Kernel.LexicalTracker do
:gen_server.call(pid, :references, @timeout)
end
@doc """
Invoked during module expansion to annotate an alias
must be warned if unused.
"""
def warn_alias(pid, meta, alias, module) do
:gen_server.cast(pid, {:warn_alias, alias, meta})
module
end
@doc """
Invoked during module expansion to annotate an import
must be warned if unused.
"""
def warn_import(pid, module) do
:gen_server.cast(pid, {:warn_import, module})
module
end
# Internal API
# Starts the tracker and returns its PID.
@@ -58,6 +39,11 @@ defmodule Kernel.LexicalTracker do
:gen_server.cast(pid, {:add_import, module, fas, meta, warn})
end
@doc false
def add_alias(pid, module, meta, warn) when is_atom(module) do
:gen_server.cast(pid, {:add_alias, module, meta, warn})
end
@doc false
def remote_dispatch(pid, module, mode) when is_atom(module) do
:gen_server.cast(pid, {:remote_dispatch, module, mode})
@@ -107,12 +93,16 @@ defmodule Kernel.LexicalTracker do
@doc false
def collect_unused_imports(pid) do
:gen_server.call(pid, :unused_imports, @timeout)
unused(pid, :unused_imports)
end
@doc false
def collect_unused_aliases(pid) do
:gen_server.call(pid, :unused_aliases, @timeout)
unused(pid, :unused_aliases)
end
defp unused(pid, tag) do
:gen_server.call(pid, tag, @timeout)
end
# Callbacks
@@ -133,17 +123,11 @@ defmodule Kernel.LexicalTracker do
@doc false
def handle_call(:unused_aliases, _from, state) do
aliases = for {alias, meta} when is_list(meta) <- state.aliases, do: {alias, meta}
{:reply, Enum.sort(aliases), state}
{:reply, Enum.sort(state.aliases), state}
end
def handle_call(:unused_imports, _from, state) do
imports =
for {module, %{@warn_key => _} = map} <- state.imports do
{module, Map.delete(map, @warn_key)}
end
{:reply, Enum.sort(imports), state}
{:reply, Enum.sort(state.imports), state}
end
def handle_call(:references, _from, state) do
@@ -187,8 +171,8 @@ defmodule Kernel.LexicalTracker do
{:noreply, %{state | imports: imports, references: references}}
end
def handle_cast({:alias_dispatch, module}, state) do
{:noreply, put_in(state.aliases[module], :used)}
def handle_cast({:alias_dispatch, module}, %{aliases: aliases} = state) do
{:noreply, %{state | aliases: Map.delete(aliases, module)}}
end
def handle_cast({:import_quoted, module, function, arities}, state) do
@@ -226,19 +210,20 @@ defmodule Kernel.LexicalTracker do
end
def handle_cast({:add_import, module, fas, meta, warn}, state) do
keys = if warn, do: [@warn_key, module | fas], else: [module | fas]
{:noreply, put_in(state.imports[module], Map.from_keys(keys, meta))}
end
def handle_cast({:warn_alias, alias, meta}, %{aliases: aliases} = state) do
case aliases do
%{^alias => :used} -> {:noreply, state}
%{} -> {:noreply, %{state | aliases: Map.put(aliases, alias, meta)}}
if warn do
imports = for module_or_fa <- [module | fas], do: {module_or_fa, meta}, into: %{}
{:noreply, put_in(state.imports[module], imports)}
else
{:noreply, state}
end
end
def handle_cast({:warn_import, module}, state) do
{:noreply, put_in(state.imports[module][@warn_key], true)}
def handle_cast({:add_alias, module, meta, warn}, state) do
if warn do
{:noreply, put_in(state.aliases[module], meta)}
else
{:noreply, state}
end
end
@doc false
+118 -217
View File
@@ -14,40 +14,34 @@ defmodule Kernel.ParallelCompiler do
@doc """
Starts a task for parallel compilation.
"""
# TODO: Deprecate this on Elixir v1.20.
@doc deprecated: "Use `pmap/2` instead"
def async(fun) when is_function(fun, 0) do
{ref, task} = inner_async(fun)
send(task.pid, ref)
task
end
defp inner_async(fun) do
If you have a file that needs to compile other modules in parallel,
the spawned processes need to be aware of the compiler environment.
This function allows a developer to create a task that is aware of
those environments.
See `Task.async/1` for more information. The task spawned must be
always awaited on by calling `Task.await/1`
"""
@doc since: "1.6.0"
def async(fun) when is_function(fun, 0) do
case :erlang.get(:elixir_compiler_info) do
{compiler_pid, file_pid} ->
ref = make_ref()
{compiler, _} ->
file = :erlang.get(:elixir_compiler_file)
dest = :erlang.get(:elixir_compiler_dest)
{:error_handler, error_handler} = :erlang.process_info(self(), :error_handler)
{_parent, cache} = Module.ParallelChecker.get()
{_parent, checker} = Module.ParallelChecker.get()
task =
Task.async(fn ->
Module.ParallelChecker.put(compiler_pid, cache)
:erlang.put(:elixir_compiler_info, {compiler_pid, file_pid})
:erlang.put(:elixir_compiler_file, file)
dest != :undefined and :erlang.put(:elixir_compiler_dest, dest)
:erlang.process_flag(:error_handler, error_handler)
receive do
^ref -> fun.()
end
end)
send(compiler_pid, {:async, task.pid})
{ref, task}
Task.async(fn ->
send(compiler, {:async, self()})
Module.ParallelChecker.put(compiler, checker)
:erlang.put(:elixir_compiler_info, {compiler, self()})
:erlang.put(:elixir_compiler_file, file)
dest != :undefined and :erlang.put(:elixir_compiler_dest, dest)
:erlang.process_flag(:error_handler, error_handler)
fun.()
end)
:undefined ->
raise ArgumentError,
@@ -56,52 +50,6 @@ defmodule Kernel.ParallelCompiler do
end
end
@doc """
Perform parallel compilation of `collection` with `fun`.
If you have a file that needs to compile other modules in parallel,
the spawned processes need to be aware of the compiler environment.
This function allows a developer to perform such tasks.
"""
@doc since: "1.16.0"
def pmap(collection, fun) when is_function(fun, 1) do
ref = make_ref()
# We spawn a series of tasks for parallel processing.
# The tasks are waiting until we give the go ahead.
refs_tasks =
Enum.map(collection, fn item ->
inner_async(fn -> fun.(item) end)
end)
# Notify the compiler we are waiting on the tasks.
{compiler_pid, file_pid} = :erlang.get(:elixir_compiler_info)
defining = :elixir_module.compiler_modules()
on = Enum.map(refs_tasks, fn {_ref, %{pid: pid}} -> pid end)
send(compiler_pid, {:waiting, :pmap, self(), ref, file_pid, on, defining, :raise})
# Now we allow the tasks to run. This step is not strictly
# necessary but it makes compilation more deterministic by
# only allowing tasks to run once we are waiting.
tasks =
Enum.map(refs_tasks, fn {ref, task} ->
send(task.pid, ref)
task
end)
# Await tasks and notify the compiler they are done. We could
# have the tasks report directly to the compiler, which in turn
# would notify us, but that would require reimplementing await_many,
# and copying the results across boundaries, so we don't.
res = Task.await_many(tasks, :infinity)
send(compiler_pid, {:available, :pmap, on})
# Only run once the compiler lets us, to avoid unbounded parallelism.
receive do
{^ref, _result} -> res
end
end
@doc """
Compiles the given files.
@@ -158,10 +106,6 @@ defmodule Kernel.ParallelCompiler do
* `:return_diagnostics` (since v1.15.0) - returns maps with information instead of
a list of warnings and returns diagnostics as maps instead of tuples
* `:max_concurrency` - the maximum number of files to compile in parallel.
Setting this option to 1 will compile files sequentially.
Defaults to the number of schedulers online, or at least 2.
"""
@doc since: "1.6.0"
@spec compile([Path.t()], keyword()) ::
@@ -208,10 +152,6 @@ defmodule Kernel.ParallelCompiler do
* `:each_module` - for each module compiled, invokes the callback passing
the file, module and the module bytecode
* `:max_concurrency` - the maximum number of files to compile in parallel.
Setting this option to 1 will compile files sequentially.
Defaults to the number of schedulers online, or at least 2.
"""
@doc since: "1.6.0"
@spec require([Path.t()], keyword()) ::
@@ -247,19 +187,36 @@ defmodule Kernel.ParallelCompiler do
defp spawn_workers(files, output, options) do
{:module, _} = :code.ensure_loaded(Kernel.ErrorHandler)
schedulers =
Keyword.get_lazy(options, :max_concurrency, fn ->
max(:erlang.system_info(:schedulers_online), 2)
end)
{:ok, cache} = Module.ParallelChecker.start_link(schedulers)
schedulers = max(:erlang.system_info(:schedulers_online), 2)
{:ok, checker} = Module.ParallelChecker.start_link(schedulers)
{status, modules_or_errors, info} =
try do
spawn_workers(schedulers, cache, files, output, options)
outcome = spawn_workers(schedulers, checker, files, output, options)
{outcome, Code.get_compiler_option(:warnings_as_errors)}
else
{{:ok, _, %{runtime_warnings: r_warnings, compile_warnings: c_warnings} = info}, true}
when r_warnings != [] or c_warnings != [] ->
message =
"Compilation failed due to warnings while using the --warnings-as-errors option"
IO.puts(:stderr, message)
errors = Enum.map(r_warnings ++ c_warnings, &Map.replace!(&1, :severity, :error))
{:error, errors, %{info | runtime_warnings: [], compile_warnings: []}}
{{:ok, outcome, info}, _} ->
beam_timestamp = Keyword.get(options, :beam_timestamp)
{:ok, write_module_binaries(outcome, output, beam_timestamp), info}
{{:error, errors, info}, true} ->
%{runtime_warnings: r_warnings, compile_warnings: c_warnings} = info
info = %{info | runtime_warnings: [], compile_warnings: []}
{:error, c_warnings ++ r_warnings ++ errors, info}
{{:error, errors, info}, _} ->
{:error, errors, info}
after
Module.ParallelChecker.stop(cache)
Module.ParallelChecker.stop(checker)
end
# TODO: Require this to be set from Elixir v1.19
@@ -281,7 +238,6 @@ defmodule Kernel.ParallelCompiler do
{outcome, state} =
spawn_workers(files, %{}, %{}, [], %{}, [], [], %{
beam_timestamp: Keyword.get(options, :beam_timestamp),
dest: Keyword.get(options, :dest),
each_cycle: Keyword.get(options, :each_cycle, fn -> {:runtime, [], []} end),
each_file: Keyword.get(options, :each_file, fn _, _ -> :ok end) |> each_file(),
@@ -339,40 +295,38 @@ defmodule Kernel.ParallelCompiler do
## Verification
defp verify_modules(result, compile_warnings, dependent_modules, state) do
modules = write_module_binaries(result, state.output, state.beam_timestamp)
runtime_warnings = maybe_check_modules(result, dependent_modules, state)
info = %{compile_warnings: Enum.reverse(compile_warnings), runtime_warnings: runtime_warnings}
{{:ok, modules, info}, state}
{{:ok, result, info}, state}
end
defp maybe_check_modules(result, runtime_modules, state) do
%{profile: profile, checker: checker} = state
compiled_modules =
for {{:module, module}, binary} when is_binary(binary) <- result,
do: module
profile(
state,
fn ->
num_modules = length(compiled_modules) + length(runtime_modules)
"group pass check of #{num_modules} modules"
end,
fn -> Module.ParallelChecker.verify(state.checker, runtime_modules) end
)
profile_checker(profile, compiled_modules, runtime_modules, fn ->
Module.ParallelChecker.verify(checker, runtime_modules)
end)
end
defp profile_init(:time), do: {:time, System.monotonic_time(), 0}
defp profile_init(nil), do: :none
defp profile(%{profile: :none}, _what, fun), do: fun.()
defp profile(%{profile: {:time, _, _}}, what, fun) do
defp profile_checker({:time, _, _}, compiled_modules, runtime_modules, fun) do
{time, result} = :timer.tc(fun)
time = div(time, 1000)
what = if is_binary(what), do: what, else: what.()
IO.puts(:stderr, "[profile] Finished #{what} in #{time}ms")
num_modules = length(compiled_modules) + length(runtime_modules)
IO.puts(:stderr, "[profile] Finished group pass check of #{num_modules} modules in #{time}ms")
result
end
defp profile_checker(:none, _compiled_modules, _runtime_modules, fun) do
fun.()
end
## Compiler worker spawning
# We already have n=schedulers currently running, don't spawn new ones
@@ -408,13 +362,13 @@ defmodule Kernel.ParallelCompiler do
end
defp spawn_workers([file | queue], spawned, waiting, files, result, warnings, errors, state) do
%{output: output, dest: dest, checker: cache} = state
%{output: output, dest: dest, checker: checker} = state
parent = self()
file = Path.expand(file)
{pid, ref} =
:erlang.spawn_monitor(fn ->
Module.ParallelChecker.put(parent, cache)
Module.ParallelChecker.put(parent, checker)
:erlang.put(:elixir_compiler_info, {parent, self()})
:erlang.put(:elixir_compiler_file, file)
@@ -450,25 +404,16 @@ defmodule Kernel.ParallelCompiler do
# No more queue, nothing waiting, this cycle is done
defp spawn_workers([], spawned, waiting, files, result, warnings, errors, state)
when map_size(spawned) == 0 and map_size(waiting) == 0 do
# Print any spurious error that we may have found
Enum.map(errors, fn {diagnostic, read_snippet} ->
:elixir_errors.print_diagnostic(diagnostic, read_snippet)
end)
[] = errors
[] = files
cycle_return =
profile(state, "cycle resolution", fn -> each_cycle_return(state.each_cycle.()) end)
cycle_return = each_cycle_return(state.each_cycle.())
state = cycle_timing(result, state)
case cycle_return do
{:runtime, dependent_modules, extra_warnings} ->
:elixir_code_server.cast(:purge_compiler_modules)
verify_modules(result, extra_warnings ++ warnings, dependent_modules, state)
{:compile, [], extra_warnings} ->
:elixir_code_server.cast(:purge_compiler_modules)
verify_modules(result, extra_warnings ++ warnings, [], state)
{:compile, more, extra_warnings} ->
@@ -503,22 +448,19 @@ defmodule Kernel.ParallelCompiler do
# Finally, note there is no difference between hard and raise, the
# difference is where the raise is happening, inside the compiler
# or in the caller.
deadlocked =
profile(state, "deadlock resolution", fn ->
waiting_list = Map.to_list(waiting)
waiting_list = Map.to_list(waiting)
deadlocked(waiting_list, :soft, false) ||
deadlocked(waiting_list, :soft, true) ||
deadlocked(waiting_list, :hard, false) ||
without_definition(waiting_list, files)
end)
deadlocked =
deadlocked(waiting_list, :soft, false) ||
deadlocked(waiting_list, :soft, true) ||
deadlocked(waiting_list, :hard, false) ||
without_definition(waiting_list, files)
if deadlocked do
spawn_workers(deadlocked, spawned, waiting, files, result, warnings, errors, state)
else
return_error(warnings, errors, state, fn ->
handle_deadlock(waiting, files)
end)
deadlock_errors = handle_deadlock(waiting, files)
{return_error(deadlock_errors ++ errors, warnings), state}
end
end
@@ -586,7 +528,7 @@ defmodule Kernel.ParallelCompiler do
nilify_empty_or_sort(
for %{pid: file_pid} <- files,
{pid, {_, _, ^file_pid, on, _, _}} <- waiting_list,
is_atom(on) and not defining?(on, waiting_list),
not defining?(on, waiting_list),
do: {pid, :not_found}
)
end
@@ -594,7 +536,7 @@ defmodule Kernel.ParallelCompiler do
defp deadlocked(waiting_list, type, defining?) do
nilify_empty_or_sort(
for {pid, {_, _, _, on, _, ^type}} <- waiting_list,
is_atom(on) and defining?(on, waiting_list) == defining?,
defining?(on, waiting_list) == defining?,
do: {pid, :deadlock}
)
end
@@ -616,8 +558,8 @@ defmodule Kernel.ParallelCompiler do
new_spawned = Map.put(spawned, ref, pid)
wait_for_messages(queue, new_spawned, waiting, files, result, warnings, errors, state)
{:available, kind, on} ->
{available, result} = update_result(result, kind, on, :done)
{:available, kind, module} ->
{available, result} = update_result(result, kind, module, true)
spawn_workers(
available ++ queue,
@@ -635,6 +577,7 @@ defmodule Kernel.ParallelCompiler do
# Release the module loader which is waiting for an ack
send(child, {ref, :ack})
{available, result} = update_result(result, :module, module, binary)
spawn_workers(
@@ -653,7 +596,7 @@ defmodule Kernel.ParallelCompiler do
send(child, {ref, :not_found})
spawn_workers(queue, spawned, waiting, files, result, warnings, errors, state)
{:waiting, kind, child_pid, ref, file_pid, on, defining, deadlock} ->
{:waiting, kind, child_pid, ref, file_pid, on, defining, deadlock?} ->
# If we already got what we were waiting for, do not put it on waiting.
# If we're waiting on ourselves, send :found so that we can crash with
# a better error.
@@ -664,7 +607,7 @@ defmodule Kernel.ParallelCompiler do
send(child_pid, {ref, :found})
{waiting, files, result}
else
waiting = Map.put(waiting, child_pid, {kind, ref, file_pid, on, defining, deadlock})
waiting = Map.put(waiting, child_pid, {kind, ref, file_pid, on, defining, deadlock?})
files = update_timing(files, file_pid, :compiling)
result = Map.put(result, {kind, on}, [child_pid | available_or_pending])
{waiting, files, result}
@@ -688,13 +631,12 @@ defmodule Kernel.ParallelCompiler do
state = %{state | timer_ref: timer_ref}
spawn_workers(queue, spawned, waiting, files, result, warnings, errors, state)
{:diagnostic, %{severity: :warning, file: file} = diagnostic, read_snippet} ->
:elixir_errors.print_diagnostic(diagnostic, read_snippet)
warnings = [%{diagnostic | file: file && Path.absname(file)} | warnings]
{:diagnostic, %{severity: :warning} = diagnostic} ->
warnings = [Module.ParallelChecker.format_diagnostic_file(diagnostic) | warnings]
wait_for_messages(queue, spawned, waiting, files, result, warnings, errors, state)
{:diagnostic, %{severity: :error} = diagnostic, read_snippet} ->
errors = [{diagnostic, read_snippet} | errors]
{:diagnostic, %{severity: :error} = diagnostic} ->
errors = [Module.ParallelChecker.format_diagnostic_file(diagnostic) | errors]
wait_for_messages(queue, spawned, waiting, files, result, warnings, errors, state)
{:file_ok, child_pid, ref, file, lexical} ->
@@ -714,13 +656,10 @@ defmodule Kernel.ParallelCompiler do
spawn_workers(queue, new_spawned, waiting, new_files, result, warnings, errors, state)
{:file_error, child_pid, file, {kind, reason, stack}} ->
print_error(file, kind, reason, stack)
{_file, _new_spawned, new_files} = discard_file_pid(spawned, files, child_pid)
terminate(new_files)
return_error(warnings, errors, state, fn ->
print_error(file, kind, reason, stack)
[to_error(file, kind, reason, stack)]
end)
{return_error([to_error(file, kind, reason, stack) | errors], warnings), state}
{:DOWN, ref, :process, pid, reason} when is_map_key(spawned, ref) ->
# async spawned processes have no file, so we always have to delete the ref directly
@@ -729,30 +668,18 @@ defmodule Kernel.ParallelCompiler do
{file, spawned, files} = discard_file_pid(spawned, files, pid)
if file do
print_error(file.file, :exit, reason, [])
terminate(files)
return_error(warnings, errors, state, fn ->
print_error(file.file, :exit, reason, [])
[to_error(file.file, :exit, reason, [])]
end)
{return_error([to_error(file.file, :exit, reason, []) | errors], warnings), state}
else
wait_for_messages(queue, spawned, waiting, files, result, warnings, errors, state)
end
end
end
defp return_error(warnings, errors, state, fun) do
# Also prune compiler modules in case of errors
:elixir_code_server.cast(:purge_compiler_modules)
errors =
Enum.map(errors, fn {%{file: file} = diagnostic, read_snippet} ->
:elixir_errors.print_diagnostic(diagnostic, read_snippet)
%{diagnostic | file: file && Path.absname(file)}
end)
defp return_error(errors, warnings) do
info = %{compile_warnings: Enum.reverse(warnings), runtime_warnings: []}
{{:error, Enum.reverse(errors, fun.()), info}, state}
{:error, Enum.reverse(errors), info}
end
defp update_result(result, kind, module, value) do
@@ -882,16 +809,12 @@ defmodule Kernel.ParallelCompiler do
)
for {file, _, description, stacktrace} <- deadlock do
file = Path.absname(file)
%{
severity: :error,
file: file,
source: file,
file: Path.absname(file),
position: nil,
message: description,
stacktrace: stacktrace,
span: nil
stacktrace: stacktrace
}
end
end
@@ -915,64 +838,42 @@ defmodule Kernel.ParallelCompiler do
])
end
defp to_error(source, kind, reason, stack) do
{file, line, span} = get_snippet_info(source, reason, stack)
source = Path.absname(source)
defp to_error(file, kind, reason, stack) do
line = get_line(file, reason, stack)
file = Path.absname(file)
message = :unicode.characters_to_binary(Kernel.CLI.format_error(kind, reason, stack))
%{
file: file || source,
source: source,
position: line || 0,
message: message,
severity: :error,
stacktrace: stack,
span: span,
details: {kind, reason}
}
%{file: file, position: line || 0, message: message, severity: :error, stacktrace: stack}
end
defp get_snippet_info(
_file,
%{file: file, line: line, column: column, end_line: end_line, end_column: end_column},
_stack
)
when is_integer(line) and line > 0 and is_integer(column) and column >= 0 and
is_integer(end_line) and end_line > 0 and is_integer(end_column) and end_column >= 0 do
{Path.absname(file), {line, column}, {end_line, end_column}}
end
defp get_snippet_info(_file, %{file: file, line: line, column: column}, _stack)
defp get_line(_file, %{line: line, column: column}, _stack)
when is_integer(line) and line > 0 and is_integer(column) and column >= 0 do
{Path.absname(file), {line, column}, nil}
{line, column}
end
defp get_snippet_info(_file, %{line: line}, _stack) when is_integer(line) and line > 0 do
{nil, line, nil}
defp get_line(_file, %{line: line}, _stack) when is_integer(line) and line > 0 do
line
end
defp get_snippet_info(file, :undef, [{_, _, _, []}, {_, _, _, info} | _]) do
get_snippet_info_from_stacktrace_info(info, file)
end
defp get_snippet_info(file, _reason, [{_, _, _, [file: expanding]}, {_, _, _, info} | _])
when expanding in [~c"expanding macro", ~c"expanding struct"] do
get_snippet_info_from_stacktrace_info(info, file)
end
defp get_snippet_info(file, _reason, [{_, _, _, info} | _]) do
get_snippet_info_from_stacktrace_info(info, file)
end
defp get_snippet_info(_, _, _) do
{nil, nil, nil}
end
defp get_snippet_info_from_stacktrace_info(info, file) do
defp get_line(file, :undef, [{_, _, _, []}, {_, _, _, info} | _]) do
if Keyword.get(info, :file) == to_charlist(Path.relative_to_cwd(file)) do
{nil, Keyword.get(info, :line), nil}
else
{nil, nil, nil}
Keyword.get(info, :line)
end
end
defp get_line(file, _reason, [{_, _, _, [file: expanding]}, {_, _, _, info} | _])
when expanding in [~c"expanding macro", ~c"expanding struct"] do
if Keyword.get(info, :file) == to_charlist(Path.relative_to_cwd(file)) do
Keyword.get(info, :line)
end
end
defp get_line(file, _reason, [{_, _, _, info} | _]) do
if Keyword.get(info, :file) == to_charlist(Path.relative_to_cwd(file)) do
Keyword.get(info, :line)
end
end
defp get_line(_, _, _) do
nil
end
end
+46 -85
View File
@@ -112,24 +112,26 @@ defmodule Kernel.SpecialForms do
%User{age: age} = user
An update operation specific for structs is also available:
%User{user | age: 28}
The advantage of structs is that they validate that the given
keys are part of the defined struct. The example below will fail
because there is no key `:full_name` in the `User` struct:
%User{full_name: "john doe"}
An update operation specific for structs is also available:
The syntax above will guarantee the given keys are valid at
compilation time and it will guarantee at runtime the given
argument is a struct, failing with `BadStructError` otherwise.
%User{user | age: 28}
Once again, the syntax above will guarantee the given keys
are valid at compilation time and it will guarantee at runtime
the given argument is a struct, failing with `BadStructError`
otherwise. The map update syntax can also be used for updating
structs, and it is useful when you want to update any struct,
regardless of their name, as long as they have matching fields:
%{user | age: 28}
Although structs are maps, by default structs do not implement
any of the protocols implemented for maps. Check
`Kernel.defprotocol/2` for more information on how structs
can be used with protocols for polymorphic dispatch. Also
see `Kernel.struct/2` and `Kernel.struct!/2` for examples on
how to create and update structs dynamically.
## Pattern matching on struct names
@@ -185,23 +187,20 @@ defmodule Kernel.SpecialForms do
iex> <<0, "foo">>
<<0, 102, 111, 111>>
You can use one of `utf8` (the default), `utf16`, and `utf32` to
control how the string is encoded:
iex> <<"foo"::utf16>>
<<0, 102, 0, 111, 0, 111>>
Which is equivalent to writing:
iex> <<?f::utf16, ?o::utf16, ?o::utf16>>
<<0, 102, 0, 111, 0, 111>>
At runtime, binaries need to be explicitly tagged as `binary`:
Binaries need to be explicitly tagged as `binary`:
iex> rest = "oo"
iex> <<102, rest::binary>>
"foo"
The `utf8`, `utf16`, and `utf32` types are for Unicode code points. They
can also be applied to literal strings and charlists:
iex> <<"foo"::utf16>>
<<0, 102, 0, 111, 0, 111>>
iex> <<"foo"::utf32>>
<<0, 0, 0, 102, 0, 0, 0, 111, 0, 0, 0, 111>>
Otherwise we get an `ArgumentError` when constructing the binary:
rest = "oo"
@@ -295,7 +294,7 @@ defmodule Kernel.SpecialForms do
`unsigned` (default) | `integer`
`little` | `integer`, `float`, `utf16`, `utf32`
`big` (default) | `integer`, `float`, `utf16`, `utf32`
`native` | `integer`, `float`, `utf16`, `utf32`
`native` | `integer`, `utf16`, `utf32`
### Sign
@@ -347,7 +346,7 @@ defmodule Kernel.SpecialForms do
Or as a part of function definitions to pattern match:
defmodule ImageType do
defmodule ImageTyper do
@png_signature <<137::size(8), 80::size(8), 78::size(8), 71::size(8),
13::size(8), 10::size(8), 26::size(8), 10::size(8)>>
@jpg_signature <<255::size(8), 216::size(8)>>
@@ -548,7 +547,7 @@ defmodule Kernel.SpecialForms do
Elixir won't emit any warnings though, since the alias
was not explicitly defined.
Both warning behaviors could be changed by explicitly
Both warning behaviours could be changed by explicitly
setting the `:warn` option to `true` or `false`.
"""
@@ -672,7 +671,7 @@ defmodule Kernel.SpecialForms do
Elixir won't emit any warnings though, since the import
was not explicitly defined.
Both warning behaviors could be changed by explicitly
Both warning behaviours could be changed by explicitly
setting the `:warn` option to `true` or `false`.
## Ambiguous function/macro names
@@ -886,7 +885,7 @@ defmodule Kernel.SpecialForms do
At first, there is nothing in this example that actually reveals it is a
macro. But what is happening is that, at compilation time, `squared(5)`
becomes `5 * 5`. The argument `5` is duplicated in the produced code, we
can see this behavior in practice though because our macro actually has
can see this behaviour in practice though because our macro actually has
a bug:
import Math
@@ -913,7 +912,7 @@ defmodule Kernel.SpecialForms do
my_number.() * my_number.()
Which invokes the function twice, explaining why we get the printed value
twice! In the majority of the cases, this is actually unexpected behavior,
twice! In the majority of the cases, this is actually unexpected behaviour,
and that's why one of the first things you need to keep in mind when it
comes to macros is to **not unquote the same value more than once**.
@@ -1024,10 +1023,9 @@ defmodule Kernel.SpecialForms do
end
end
require Hygiene
Hygiene.write()
Hygiene.read()
** (CompileError) undefined variable "a" (context Hygiene)
** (RuntimeError) undefined variable a or undefined function a/0
For such, you can explicitly pass the current module scope as
argument:
@@ -1046,7 +1044,6 @@ defmodule Kernel.SpecialForms do
end
end
require ContextHygiene
ContextHygiene.write()
ContextHygiene.read()
#=> 1
@@ -1442,7 +1439,7 @@ defmodule Kernel.SpecialForms do
[elixir: :prolog]
Given the grandparents of Erlang and Prolog were nil, those values were
filtered out. If you don't want this behavior, a simple option is to
filtered out. If you don't want this behaviour, a simple option is to
move the filter inside the do-block:
iex> languages = [elixir: :erlang, erlang: :prolog, prolog: nil]
@@ -1492,9 +1489,7 @@ defmodule Kernel.SpecialForms do
## The `:reduce` option
*Available since Elixir v1.8*.
While the `:into` option allows us to customize the comprehension behavior
While the `:into` option allows us to customize the comprehension behaviour
to a given data type, such as putting all of the values inside a map or inside
a binary, it is not always enough.
@@ -1539,13 +1534,13 @@ defmodule Kernel.SpecialForms do
@doc """
Combine matching clauses.
One of the ways to understand `with` is to show which code
One of the ways to understand with is to show which code
patterns it improves. Imagine you have a map where the fields
`width` and `height` are optional and you want to compute its
area, as `{:ok, area}` or return `:error`. We could implement
this function as:
def area(opts) do
def area(map) do
case Map.fetch(opts, :width) do
{:ok, width} ->
case Map.fetch(opts, :height) do
@@ -1564,7 +1559,7 @@ defmodule Kernel.SpecialForms do
While the code above works, it is quite verbose. Using `with`,
we could rewrite it as:
def area(opts) do
def area(map) do
with {:ok, width} <- Map.fetch(opts, :width),
{:ok, height} <- Map.fetch(opts, :height) do
{:ok, width * height}
@@ -1622,7 +1617,7 @@ defmodule Kernel.SpecialForms do
iex> width
nil
The behavior of any expression in a clause is the same as if it was
The behaviour of any expression in a clause is the same as if it was
written outside of `with`. For example, `=` will raise a `MatchError`
instead of returning the non-matched value:
@@ -1661,7 +1656,7 @@ defmodule Kernel.SpecialForms do
...> end
{:error, :wrong_data}
The `else` block works like a `case`: it can have multiple clauses,
The `else` block works like a `case` clause: it can have multiple clauses,
and the first match will be used. Variables bound inside `with` (such as
`width` in this example) are not available in the `else` block.
@@ -1754,15 +1749,6 @@ defmodule Kernel.SpecialForms do
"""
defmacro unquote(:__block__)(args), do: error!([args])
@doc """
Internal special form for cursor position.
This is the special form used whenever we need to represent
the cursor position in Elixir's AST. See `Code.Fragment` for
more information.
"""
defmacro unquote(:__cursor__)(args), do: error!([args])
@doc """
Capture operator. Captures or creates an anonymous function.
@@ -1786,15 +1772,6 @@ defmodule Kernel.SpecialForms do
&local_function/1
Note that `&local_function/1` creates a local capture, but
`&__MODULE__.local_function/1` or `&imported_function/1` create a remote
capture. For more information, refer to the ["Functions" section in the Erlang Reference Manual](https://www.erlang.org/doc/system/eff_guide_functions.html#function-calls).
Whether a capture is local or remote has implications when using hot code
reloading: local captures dispatch to the version of the module that existed
at the time they were created, while remote captures dispatch to the current
version of the module.
See also `Function.capture/3`.
## Anonymous functions
@@ -1894,11 +1871,6 @@ defmodule Kernel.SpecialForms do
@doc ~S"""
Matches the given expression against the given clauses.
`case/2` relies on pattern matching and guards to choose
which clause to execute. If your logic cannot be expressed
within patterns and guards, consider using `if/2` or `cond/1`
instead.
## Examples
case File.read(file) do
@@ -1929,9 +1901,6 @@ defmodule Kernel.SpecialForms do
end
#=> "This clause would match any value (x = 10)"
If you find yourself nesting `case` expressions inside
`case` expressions, consider using `with/1`.
## Variable handling
Note that variables bound in a clause do not leak to the outer context:
@@ -1991,20 +1960,17 @@ defmodule Kernel.SpecialForms do
Evaluates the expression corresponding to the first clause that
evaluates to a truthy value.
## Examples
The following example has a single clause that always evaluates
to true:
cond do
hd([1, 2, 3]) ->
"1 is considered as true"
end
#=> "1 is considered as true"
If all clauses evaluate to `nil` or `false`, `cond` raises an error.
Raises an error if all conditions evaluate to `nil` or `false`.
For this reason, it may be necessary to add a final always-truthy condition
(anything non-`false` and non-`nil`), which will always match:
(anything non-`false` and non-`nil`), which will always match.
## Examples
cond do
1 + 1 == 1 ->
@@ -2016,9 +1982,6 @@ defmodule Kernel.SpecialForms do
end
#=> "This will"
If your `cond` has two clauses, and the last one falls back to
`true`, you may consider using `if/2` instead.
"""
defmacro cond(clauses), do: error!([clauses])
@@ -2332,13 +2295,11 @@ defmodule Kernel.SpecialForms do
defmacro try(args), do: error!([args])
@doc """
Checks if there is a message matching any of the given clauses in the current
process mailbox.
Checks if there is a message matching the given clauses
in the current process mailbox.
If there is no matching message, the current process waits until a matching
message arrives or until after a given timeout value.
Any new and existing messages that do not match will remain in the mailbox.
In case there is no such message, the current process hangs
until a message arrives or waits until a given timeout value.
## Examples
@@ -2351,8 +2312,8 @@ defmodule Kernel.SpecialForms do
IO.puts(:stderr, "Unexpected message received")
end
An optional `after` clause can be given in case no matching message is
received during the given timeout period, specified in milliseconds:
An optional `after` clause can be given in case the message was not
received after the given timeout period, specified in milliseconds:
receive do
{:selector, number, name} when is_integer(number) ->
+85 -104
View File
@@ -118,7 +118,7 @@ defmodule Kernel.Typespec do
case spec_to_signature(expr) do
{name, arity} ->
# store doc only once in case callback has multiple clauses
if not :ets.member(set, {kind, name, arity}) do
unless :ets.member(set, {kind, name, arity}) do
{line, doc} = get_doc_info(set, :doc, line)
store_doc(set, kind, name, arity, line, :doc, doc, %{})
end
@@ -138,7 +138,7 @@ defmodule Kernel.Typespec do
case type_to_signature(expr) do
{name, arity} = signature when signature in @reserved_signatures ->
compile_error(
:elixir_module.get_cached_env(pos),
:elixir_locals.get_cached_env(pos),
"type #{name}/#{arity} is a reserved type and it cannot be defined"
)
@@ -247,7 +247,7 @@ defmodule Kernel.Typespec do
defp collect_defined_type_pairs(type_typespecs) do
fun = fn {_kind, expr, pos}, type_pairs ->
%{file: file, line: line} = env = :elixir_module.get_cached_env(pos)
%{file: file, line: line} = env = :elixir_locals.get_cached_env(pos)
case type_to_signature(expr) do
{name, arity} = type_pair ->
@@ -290,9 +290,8 @@ defmodule Kernel.Typespec do
:lists.filter(fun, types)
end
defp translate_type({kind, {:"::", _, [{name, meta, args}, definition]}, pos}, state)
when is_list(meta) do
caller = :elixir_module.get_cached_env(pos)
defp translate_type({kind, {:"::", _, [{name, _, args}, definition]}, pos}, state) do
caller = :elixir_locals.get_cached_env(pos)
state = clean_local_state(state)
args =
@@ -321,7 +320,7 @@ defmodule Kernel.Typespec do
invalid_args = :lists.filter(&(not valid_variable_ast?(&1)), args)
if invalid_args != [] do
unless invalid_args == [] do
invalid_args = :lists.join(", ", :lists.map(&Macro.to_string/1, invalid_args))
message =
@@ -336,7 +335,7 @@ defmodule Kernel.Typespec do
IO.warn(message, caller)
end
{{kind, {name, arity}, meta, type, export}, state}
{{kind, {name, arity}, caller.line, type, export}, state}
end
defp valid_variable_ast?({variable_name, _, context})
@@ -349,17 +348,17 @@ defmodule Kernel.Typespec do
defp underspecified?(_kind, _arity, _spec), do: false
defp translate_spec({kind, {:when, _meta, [spec, guard]}, pos}, state) do
caller = :elixir_module.get_cached_env(pos)
caller = :elixir_locals.get_cached_env(pos)
translate_spec(kind, spec, guard, caller, state)
end
defp translate_spec({kind, spec, pos}, state) do
caller = :elixir_module.get_cached_env(pos)
caller = :elixir_locals.get_cached_env(pos)
translate_spec(kind, spec, [], caller, state)
end
defp translate_spec(kind, {:"::", _, [{name, meta, args}, return]}, guard, caller, state)
when is_atom(name) and name != :"::" and is_list(meta) and (is_list(args) or is_atom(args)) do
defp translate_spec(kind, {:"::", meta, [{name, _, args}, return]}, guard, caller, state)
when is_atom(name) and name != :"::" do
translate_spec(kind, meta, name, args, return, guard, caller, state)
end
@@ -381,28 +380,28 @@ defmodule Kernel.Typespec do
ensure_no_defaults!(args)
state = clean_local_state(state)
if not Keyword.keyword?(guard) do
unless Keyword.keyword?(guard) do
error = "expected keywords as guard in type specification, got: #{Macro.to_string(guard)}"
compile_error(caller, error)
end
location = location(meta)
line = line(meta)
vars = Keyword.keys(guard)
{args, state} = :lists.mapfoldl(&typespec(&1, vars, caller, &2), state, args)
{return, state} = typespec(return, vars, caller, state)
spec = {:type, location, :fun, [{:type, location, :product, args}, return]}
spec = {:type, line, :fun, [{:type, line, :product, args}, return]}
{spec, state} =
case guard_to_constraints(guard, vars, meta, caller, state) do
{[], state} -> {spec, state}
{constraints, state} -> {{:type, location, :bounded_fun, [spec, constraints]}, state}
{constraints, state} -> {{:type, line, :bounded_fun, [spec, constraints]}, state}
end
ensure_no_unused_local_vars!(caller, state.local_vars)
arity = length(args)
{{kind, {name, arity}, meta, spec}, state}
{{kind, {name, arity}, caller.line, spec}, state}
end
# TODO: Remove char_list type by v2.0
@@ -438,7 +437,7 @@ defmodule Kernel.Typespec do
defp ensure_not_default(_), do: :ok
defp guard_to_constraints(guard, vars, meta, caller, state) do
location = location(meta)
line = line(meta)
fun = fn
{_name, {:var, _, context}}, {constraints, state} when is_atom(context) ->
@@ -446,9 +445,9 @@ defmodule Kernel.Typespec do
{name, type}, {constraints, state} ->
{spec, state} = typespec(type, vars, caller, state)
constraint = [{:atom, location, :is_subtype}, [{:var, location, name}, spec]]
constraint = [{:atom, line, :is_subtype}, [{:var, line, name}, spec]]
state = update_local_vars(state, name)
{[{:type, location, :constraint, constraint} | constraints], state}
{[{:type, line, :constraint, constraint} | constraints], state}
end
{constraints, state} = :lists.foldl(fun, {[], state}, guard)
@@ -457,27 +456,21 @@ defmodule Kernel.Typespec do
## To typespec conversion
defp location(meta) do
line = Keyword.get(meta, :line, 0)
if column = Keyword.get(meta, :column) do
{line, column}
else
line
end
defp line(meta) do
Keyword.get(meta, :line, 0)
end
# Handle unions
defp typespec({:|, meta, [_, _]} = exprs, vars, caller, state) do
exprs = collect_union(exprs)
{union, state} = :lists.mapfoldl(&typespec(&1, vars, caller, &2), state, exprs)
{{:type, location(meta), :union, union}, state}
{{:type, line(meta), :union, union}, state}
end
# Handle binaries
defp typespec({:<<>>, meta, []}, _, _, state) do
location = location(meta)
{{:type, location, :binary, [{:integer, location, 0}, {:integer, location, 0}]}, state}
line = line(meta)
{{:type, line, :binary, [{:integer, line, 0}, {:integer, line, 0}]}, state}
end
defp typespec(
@@ -487,36 +480,14 @@ defmodule Kernel.Typespec do
state
)
when is_atom(ctx1) and is_atom(ctx2) and unit in 1..256 do
location = location(meta)
{{:type, location, :binary, [{:integer, location, 0}, {:integer, location(unit_meta), unit}]},
state}
end
defp typespec(
{:<<>>, meta, [{:"::", _, [{:_, _, ctx1}, {:*, prod_meta, [size, unit]}]}]},
_,
_,
state
)
when is_atom(ctx1) and is_integer(size) and size >= 0 and unit in 1..256 do
location = location(meta)
prod_location = location(prod_meta)
{{:type, location, :binary,
[
{:op, prod_location, :*, {:integer, prod_location, size},
{:integer, prod_location, unit}},
{:integer, location, 0}
]}, state}
line = line(meta)
{{:type, line, :binary, [{:integer, line, 0}, {:integer, line(unit_meta), unit}]}, state}
end
defp typespec({:<<>>, meta, [{:"::", size_meta, [{:_, _, ctx}, size]}]}, _, _, state)
when is_atom(ctx) and is_integer(size) and size >= 0 do
location = location(meta)
{{:type, location, :binary, [{:integer, location(size_meta), size}, {:integer, location, 0}]},
state}
line = line(meta)
{{:type, line, :binary, [{:integer, line(size_meta), size}, {:integer, line, 0}]}, state}
end
defp typespec(
@@ -534,8 +505,8 @@ defmodule Kernel.Typespec do
)
when is_atom(ctx1) and is_atom(ctx2) and is_atom(ctx3) and is_integer(size) and
size >= 0 and unit in 1..256 do
args = [{:integer, location(size_meta), size}, {:integer, location(unit_meta), unit}]
{{:type, location(meta), :binary, args}, state}
args = [{:integer, line(size_meta), size}, {:integer, line(unit_meta), unit}]
{{:type, line(meta), :binary, args}, state}
end
defp typespec({:<<>>, _meta, _args}, _vars, caller, _state) do
@@ -548,7 +519,7 @@ defmodule Kernel.Typespec do
## Handle maps and structs
defp typespec({:map, meta, args}, _vars, _caller, state) when args == [] or is_atom(args) do
{{:type, location(meta), :map, :any}, state}
{{:type, line(meta), :map, :any}, state}
end
defp typespec({:%{}, meta, fields} = map, vars, caller, state) do
@@ -556,17 +527,17 @@ defmodule Kernel.Typespec do
{{:required, meta2, [k]}, v}, state ->
{arg1, state} = typespec(k, vars, caller, state)
{arg2, state} = typespec(v, vars, caller, state)
{{:type, location(meta2), :map_field_exact, [arg1, arg2]}, state}
{{:type, line(meta2), :map_field_exact, [arg1, arg2]}, state}
{{:optional, meta2, [k]}, v}, state ->
{arg1, state} = typespec(k, vars, caller, state)
{arg2, state} = typespec(v, vars, caller, state)
{{:type, location(meta2), :map_field_assoc, [arg1, arg2]}, state}
{{:type, line(meta2), :map_field_assoc, [arg1, arg2]}, state}
{k, v}, state ->
{arg1, state} = typespec(k, vars, caller, state)
{arg2, state} = typespec(v, vars, caller, state)
{{:type, location(meta), :map_field_exact, [arg1, arg2]}, state}
{{:type, line(meta), :map_field_exact, [arg1, arg2]}, state}
{:|, _, [_, _]}, _state ->
error =
@@ -580,29 +551,33 @@ defmodule Kernel.Typespec do
end
{fields, state} = :lists.mapfoldl(fun, state, fields)
{{:type, location(meta), :map, fields}, state}
{{:type, line(meta), :map, fields}, state}
end
defp typespec({:%, _, [name, {:%{}, meta, fields}]} = node, vars, caller, state) do
case Macro.expand(name, %{caller | function: {:__info__, 1}}) do
module when is_atom(module) ->
struct_info = Macro.struct_info!(module, caller)
struct =
module
|> Macro.struct!(caller)
|> Map.delete(:__struct__)
|> Map.to_list()
if not Keyword.keyword?(fields) do
unless Keyword.keyword?(fields) do
compile_error(caller, "expected key-value pairs in struct #{Macro.to_string(name)}")
end
types =
:lists.map(
fn %{field: field} ->
default_type = if field == :__exception__, do: true, else: quote(do: term())
{field, Keyword.get(fields, field, default_type)}
fn
{:__exception__ = field, true} -> {field, Keyword.get(fields, field, true)}
{field, _} -> {field, Keyword.get(fields, field, quote(do: term()))}
end,
struct_info
:lists.sort(struct)
)
fun = fn {field, _} ->
if not Enum.any?(struct_info, &(&1.field == field)) do
unless Keyword.has_key?(struct, field) do
compile_error(
caller,
"undefined field #{inspect(field)} on struct #{inspect(module)}"
@@ -611,7 +586,7 @@ defmodule Kernel.Typespec do
end
:lists.foreach(fun, fields)
typespec({:%{}, meta, [__struct__: module] ++ :lists.usort(types)}, vars, caller, state)
typespec({:%{}, meta, [__struct__: module] ++ types}, vars, caller, state)
_ ->
compile_error(
@@ -645,7 +620,7 @@ defmodule Kernel.Typespec do
)
fun = fn {field, _} ->
if not Keyword.has_key?(fields, field) do
unless Keyword.has_key?(fields, field) do
compile_error(caller, "undefined field #{field} on record #{inspect(tag)}")
end
end
@@ -669,7 +644,7 @@ defmodule Kernel.Typespec do
{right, state} = typespec(right, vars, caller, state)
:ok = validate_range(left, right, caller)
{{:type, location(meta), :range, [left, right]}, state}
{{:type, line(meta), :range, [left, right]}, state}
end
# Handle special forms
@@ -686,7 +661,14 @@ defmodule Kernel.Typespec do
when is_list(args) do
{args, state} = fn_args(meta, args, vars, caller, state)
{spec, state} = typespec(return, vars, caller, state)
{{:type, location(meta), :fun, [args, spec]}, state}
fun_args =
case [args, spec] do
[{:type, _, :any}, {:type, _, :any, []}] -> []
pair -> pair
end
{{:type, line(meta), :fun, fun_args}, state}
end
# Handle type operator
@@ -709,10 +691,10 @@ defmodule Kernel.Typespec do
# This may be generating an invalid typespec but we need to generate it
# to avoid breaking existing code that was valid but only broke Dialyzer
{right, state} = typespec(expr, vars, caller, state)
{{:ann_type, location(meta), [{:var, location(var_meta), var_name}, right]}, state}
{{:ann_type, line(meta), [{:var, line(var_meta), var_name}, right]}, state}
{right, state} ->
{{:ann_type, location(meta), [{:var, location(var_meta), var_name}, right]}, state}
{{:ann_type, line(meta), [{:var, line(var_meta), var_name}, right]}, state}
end
end
@@ -741,13 +723,13 @@ defmodule Kernel.Typespec do
{left, state} = typespec(left, vars, caller, state)
state = %{state | undefined_type_error_enabled?: true}
{right, state} = typespec(right, vars, caller, state)
{{:ann_type, location(meta), [left, right]}, state}
{{:ann_type, line(meta), [left, right]}, state}
end
# Handle unary ops
defp typespec({op, meta, [integer]}, _, _, state) when op in [:+, :-] and is_integer(integer) do
location = location(meta)
{{:op, location, op, {:integer, location, integer}}, state}
line = line(meta)
{{:op, line, op, {:integer, line, integer}}, state}
end
# Handle remote calls in the form of @module_attribute.type.
@@ -762,7 +744,7 @@ defmodule Kernel.Typespec do
) do
remote = Module.get_attribute(caller.module, attr)
if not (is_atom(remote) and remote != nil) do
unless is_atom(remote) and remote != nil do
message =
"invalid remote in typespec: #{Macro.to_string(orig)} (@#{attr} is #{inspect(remote)})"
@@ -796,12 +778,12 @@ defmodule Kernel.Typespec do
# Handle tuples
defp typespec({:tuple, meta, []}, _vars, _caller, state) do
{{:type, location(meta), :tuple, :any}, state}
{{:type, line(meta), :tuple, :any}, state}
end
defp typespec({:{}, meta, t}, vars, caller, state) when is_list(t) do
{args, state} = :lists.mapfoldl(&typespec(&1, vars, caller, &2), state, t)
{{:type, location(meta), :tuple, args}, state}
{{:type, line(meta), :tuple, args}, state}
end
defp typespec({left, right}, vars, caller, state) do
@@ -817,7 +799,7 @@ defmodule Kernel.Typespec do
defp typespec({name, meta, atom}, vars, caller, state) when is_atom(atom) do
if :lists.member(name, vars) do
state = update_local_vars(state, name)
{{:var, location(meta), name}, state}
{{:var, line(meta), name}, state}
else
typespec({name, meta, []}, vars, caller, state)
end
@@ -832,7 +814,7 @@ defmodule Kernel.Typespec do
IO.warn(warning, caller)
{args, state} = :lists.mapfoldl(&typespec(&1, vars, caller, &2), state, args)
{{:type, location(meta), :string, args}, state}
{{:type, line(meta), :string, args}, state}
end
defp typespec({:nonempty_string, meta, args}, vars, caller, state) do
@@ -843,7 +825,7 @@ defmodule Kernel.Typespec do
IO.warn(warning, caller)
{args, state} = :lists.mapfoldl(&typespec(&1, vars, caller, &2), state, args)
{{:type, location(meta), :nonempty_string, args}, state}
{{:type, line(meta), :nonempty_string, args}, state}
end
defp typespec({type, _meta, []}, vars, caller, state) when type in [:charlist, :char_list] do
@@ -873,7 +855,7 @@ defmodule Kernel.Typespec do
defp typespec({:fun, meta, args}, vars, caller, state) do
{args, state} = :lists.mapfoldl(&typespec(&1, vars, caller, &2), state, args)
{{:type, location(meta), :fun, args}, state}
{{:type, line(meta), :fun, args}, state}
end
defp typespec({:..., _meta, _args}, _vars, caller, _state) do
@@ -890,7 +872,7 @@ defmodule Kernel.Typespec do
case :erl_internal.is_type(name, arity) do
true ->
{{:type, location(meta), name, args}, state}
{{:type, line(meta), name, args}, state}
false ->
if state.undefined_type_error_enabled? and
@@ -908,7 +890,7 @@ defmodule Kernel.Typespec do
%{state | used_type_pairs: [{name, arity} | state.used_type_pairs]}
end
{{:user_type, location(meta), name, args}, state}
{{:user_type, line(meta), name, args}, state}
end
end
@@ -925,11 +907,11 @@ defmodule Kernel.Typespec do
typespec({nil, [], []}, vars, caller, state)
end
defp typespec([{:..., _, _}], vars, caller, state) do
defp typespec([{:..., _, atom}], vars, caller, state) when is_atom(atom) do
typespec({:nonempty_list, [], []}, vars, caller, state)
end
defp typespec([spec, {:..., _, _}], vars, caller, state) do
defp typespec([spec, {:..., _, atom}], vars, caller, state) when is_atom(atom) do
typespec({:nonempty_list, [], [spec]}, vars, caller, state)
end
@@ -956,20 +938,19 @@ defmodule Kernel.Typespec do
## Helpers
# This is a modified backport of Macro.expand/2 because we want to expand
# This is a backport of Macro.expand/2 because we want to expand
# aliases but we don't them to become compile-time references.
defp expand_remote({:__aliases__, meta, list} = alias, env) do
case :elixir_aliases.expand_or_concat(meta, list, env, true) do
defp expand_remote({:__aliases__, _, _} = alias, env) do
case :elixir_aliases.expand(alias, env) do
receiver when is_atom(receiver) ->
receiver
[head | tail] ->
case Macro.expand(head, env) do
head when is_atom(head) ->
:elixir_aliases.concat([head | tail])
aliases ->
aliases = :lists.map(&Macro.expand_once(&1, env), aliases)
_ ->
compile_error(env, "unexpected expression in typespec: #{Macro.to_string(alias)}")
case :lists.all(&is_atom/1, aliases) do
true -> :elixir_aliases.concat(aliases)
false -> alias
end
end
end
@@ -982,7 +963,7 @@ defmodule Kernel.Typespec do
defp remote_type({remote, meta, name, args}, vars, caller, state) do
{args, state} = :lists.mapfoldl(&typespec(&1, vars, caller, &2), state, args)
{{:remote_type, location(meta), [remote, name, args]}, state}
{{:remote_type, line(meta), [remote, name, args]}, state}
end
defp collect_union({:|, _, [a, b]}), do: [a | collect_union(b)]
@@ -1015,16 +996,16 @@ defmodule Kernel.Typespec do
end
defp fn_args(meta, [{:..., _, _}], _vars, _caller, state) do
{{:type, location(meta), :any}, state}
{{:type, line(meta), :any}, state}
end
defp fn_args(meta, args, vars, caller, state) do
{args, state} = :lists.mapfoldl(&typespec(&1, vars, caller, &2), state, args)
{{:type, location(meta), :product, args}, state}
{{:type, line(meta), :product, args}, state}
end
defp variable({name, meta, args}) when is_atom(name) and is_atom(args) do
{:var, location(meta), name}
{:var, line(meta), name}
end
defp variable(expr), do: expr
+30 -33
View File
@@ -36,14 +36,14 @@ defmodule Kernel.Utils do
if is_list(funs) do
IO.warn(
"passing a list to Kernel.defdelegate/2 is deprecated, please define each delegate separately",
env
Macro.Env.stacktrace(env)
)
end
if Keyword.has_key?(opts, :append_first) do
IO.warn(
"Kernel.defdelegate/2 :append_first option is deprecated",
env
Macro.Env.stacktrace(env)
)
end
@@ -100,10 +100,10 @@ defmodule Kernel.Utils do
@doc """
Callback for defstruct.
"""
def defstruct(module, fields, bootstrapped?, env) do
def defstruct(module, fields, bootstrapped?) do
{set, bag} = :elixir_module.data_tables(module)
if :ets.member(set, {:elixir, :struct}) do
if :ets.member(set, :__struct__) do
raise ArgumentError,
"defstruct has already been called for " <>
"#{Kernel.inspect(module)}, defstruct can only be called once per module"
@@ -119,10 +119,8 @@ defmodule Kernel.Utils do
mapper = fn
{key, val} when is_atom(key) ->
key == :__struct__ and raise(ArgumentError, "cannot set :__struct__ in struct definition")
try do
:elixir_quote.escape(val, :none, false)
:elixir_quote.escape(val, false, :none)
rescue
e in [ArgumentError] ->
raise ArgumentError, "invalid value for struct field #{key}, " <> Exception.message(e)
@@ -131,7 +129,6 @@ defmodule Kernel.Utils do
end
key when is_atom(key) ->
key == :__struct__ and raise(ArgumentError, "cannot set :__struct__ in struct definition")
{key, nil}
other ->
@@ -155,7 +152,7 @@ defmodule Kernel.Utils do
end
# TODO: Make it raise on v2.0
warn_on_duplicate_struct_key(:lists.keysort(1, fields), env)
warn_on_duplicate_struct_key(:lists.keysort(1, fields))
foreach = fn
key when is_atom(key) ->
@@ -166,24 +163,23 @@ defmodule Kernel.Utils do
end
:lists.foreach(foreach, enforce_keys)
struct = :maps.from_list([__struct__: module] ++ fields)
escaped_struct = :elixir_quote.escape(struct, :none, false)
struct = :maps.put(:__struct__, module, :maps.from_list(fields))
body =
case bootstrapped? do
true ->
case enforce_keys do
[] ->
quote line: 0, generated: true do
Enum.reduce(kv, unquote(escaped_struct), fn {key, val}, map ->
quote do
Enum.reduce(kv, @__struct__, fn {key, val}, map ->
%{map | key => val}
end)
end
_ ->
quote line: 0, generated: true do
quote do
{map, keys} =
Enum.reduce(kv, {unquote(escaped_struct), unquote(enforce_keys)}, fn
Enum.reduce(kv, {@__struct__, unquote(enforce_keys)}, fn
{key, val}, {map, keys} ->
{%{map | key => val}, List.delete(keys, key)}
end)
@@ -201,10 +197,10 @@ defmodule Kernel.Utils do
end
false ->
quote line: 0, generated: true do
quote do
:lists.foldl(
fn {key, val}, acc -> %{acc | key => val} end,
unquote(escaped_struct),
@__struct__,
kv
)
end
@@ -212,13 +208,17 @@ defmodule Kernel.Utils do
case enforce_keys -- :maps.keys(struct) do
[] ->
# The __struct__ field is used for expansion and for loading remote structs
:ets.insert(set, {:__struct__, struct, nil, []})
# Store all field metadata to go into __info__(:struct)
mapper = fn {key, val} ->
%{field: key, default: val}
%{field: key, default: val, required: :lists.member(key, enforce_keys)}
end
:ets.insert(set, {{:elixir, :struct}, :lists.map(mapper, fields)})
derive = :lists.map(fn {_, value} -> value end, :ets.take(bag, {:accumulate, :derive}))
{struct, :lists.reverse(derive), escaped_struct, quote(do: kv), body}
{struct, :lists.reverse(derive), quote(do: kv), body}
error_keys ->
raise ArgumentError,
@@ -227,17 +227,17 @@ defmodule Kernel.Utils do
end
end
defp warn_on_duplicate_struct_key([], _) do
defp warn_on_duplicate_struct_key([]) do
:ok
end
defp warn_on_duplicate_struct_key([{key, _} | [{key, _} | _] = rest], env) do
IO.warn("duplicate key #{inspect(key)} found in struct", env)
warn_on_duplicate_struct_key(rest, env)
defp warn_on_duplicate_struct_key([{key, _} | [{key, _} | _] = rest]) do
IO.warn("duplicate key #{inspect(key)} found in struct")
warn_on_duplicate_struct_key(rest)
end
defp warn_on_duplicate_struct_key([_ | rest], env) do
warn_on_duplicate_struct_key(rest, env)
defp warn_on_duplicate_struct_key([_ | rest]) do
warn_on_duplicate_struct_key(rest)
end
@doc """
@@ -289,7 +289,7 @@ defmodule Kernel.Utils do
macro.
Secondly, if the expression is being used outside of a guard, we want to unquote
`value`, but only once, and then reuse the unquoted form throughout the expression.
`value`, but only once, and then re-use the unquoted form throughout the expression.
This helper does exactly that: takes the AST for an expression and a list of
variable references it should be aware of, and rewrites it into a new expression
@@ -330,11 +330,8 @@ 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, env.module), generated: true))
true -> unquote(literal_quote(unquote_every_ref(expr, vars)))
false -> unquote(literal_quote(unquote_refs_once(expr, vars, env.module)))
end
end
end
@@ -399,8 +396,8 @@ defmodule Kernel.Utils do
end
end
defp literal_quote(ast, args) do
{:quote, [], [args, [do: ast]]}
defp literal_quote(ast) do
{:quote, [], [[do: ast]]}
end
defp literal_unquote(ast) do
+4 -41
View File
@@ -5,9 +5,7 @@ defmodule Keyword do
The first element of these tuples is known as the *key*, and it must be an atom.
The second element, known as the *value*, can be any term.
Keywords are mostly used to work with optional values. For a general introduction
to keywords and how they compare with maps, see our [Keyword and Maps](keywords-and-maps.md)
guide.
Keywords are mostly used to work with optional values.
## Examples
@@ -102,9 +100,6 @@ defmodule Keyword do
@type key :: atom
@type value :: any
@typedoc since: "1.17.0"
@type default :: any
@type t :: [{key, value}]
@type t(value) :: [{key, value}]
@@ -115,8 +110,6 @@ defmodule Keyword do
iex> Keyword.from_keys([:foo, :bar, :baz], :atom)
[foo: :atom, bar: :atom, baz: :atom]
iex> Keyword.from_keys([], :atom)
[]
"""
@doc since: "1.14.0"
@@ -391,7 +384,7 @@ defmodule Keyword do
3
"""
@spec get(t, key, default) :: value | default
@spec get(t, key, value) :: value
def get(keywords, key, default \\ nil) when is_list(keywords) and is_atom(key) do
case :lists.keyfind(key, 1, keywords) do
{^key, value} -> value
@@ -960,36 +953,6 @@ defmodule Keyword do
:lists.sort(left) === :lists.sort(right)
end
@doc """
Intersects two keyword lists, returning a keyword with the common keys.
By default, it returns the values of the intersected keys in `keyword2`.
The keys are returned in the order found in `keyword1`.
## Examples
iex> Keyword.intersect([a: 1, b: 2], [b: "b", c: "c"])
[b: "b"]
iex> Keyword.intersect([a: 1, b: 2], [b: 2, c: 3], fn _k, v1, v2 ->
...> v1 + v2
...> end)
[b: 4]
"""
@doc since: "1.17.0"
@spec intersect(keyword, keyword, (key, value, value -> value)) :: keyword
def intersect(keyword1, keyword2, fun \\ fn _key, _v1, v2 -> v2 end)
def intersect([{k, v1} | keyword1], keyword2, fun) do
case :lists.keyfind(k, 1, keyword2) do
{_, v2} -> [{k, fun.(k, v1, v2)} | intersect(keyword1, keyword2, fun)]
false -> intersect(keyword1, keyword2, fun)
end
end
def intersect([], _keyword2, _fun), do: []
@doc """
Merges two keyword lists into one.
@@ -1322,7 +1285,7 @@ defmodule Keyword do
{1, []}
"""
@spec pop(t, key, default) :: {value | default, t}
@spec pop(t, key, value) :: {value, t}
def pop(keywords, key, default \\ nil) when is_list(keywords) and is_atom(key) do
case fetch(keywords, key) do
{:ok, value} -> {value, delete(keywords, key)}
@@ -1438,7 +1401,7 @@ defmodule Keyword do
{1, [a: 2]}
"""
@spec pop_first(t, key, default) :: {value | default, t}
@spec pop_first(t, key, value) :: {value, t}
def pop_first(keywords, key, default \\ nil) when is_list(keywords) and is_atom(key) do
case :lists.keytake(key, 1, keywords) do
{:value, {^key, value}, rest} -> {value, rest}
+55 -66
View File
@@ -57,8 +57,8 @@ defmodule List do
iex> list ++ [4] # slow
[1, 2, 3, 4]
Most of the functions in this module work in linear time. This means that
the time it takes to perform an operation grows at the same rate as the
Most of the functions in this module work in linear time. This means that,
that the time it takes to perform an operation grows at the same rate as the
length of the list. For example `length/1` and `last/1` will run in linear
time because they need to iterate through every element of the list, but
`first/1` will run in constant time because it only needs the first element.
@@ -107,7 +107,7 @@ defmodule List do
charlists in IEx when you encounter them, which shows you the type, description
and also the raw representation in one single summary.
The rationale behind this behavior is to better support
The rationale behind this behaviour is to better support
Erlang libraries which may return text as charlists
instead of Elixir strings. In Erlang, charlists are the default
way of handling strings, while in Elixir it's binaries. One
@@ -633,39 +633,27 @@ defmodule List do
[other]
end
@deprecated "Use Enum.zip/1 instead"
# We keep the old implementation because it also supported lists
# of tuples, even though this was not included in its @spec.
@doc """
Zips corresponding elements from each list in `list_of_lists`.
The zipping finishes as soon as any list terminates.
## Examples
iex> List.zip([[1, 2], [3, 4], [5, 6]])
[{1, 3, 5}, {2, 4, 6}]
iex> List.zip([[1, 2], [3], [5, 6]])
[{1, 3, 5}]
"""
@spec zip([list]) :: [tuple]
def zip([]), do: []
def zip(list_of_lists) when is_list(list_of_lists), do: do_zip(list_of_lists, [])
defp do_zip(list, acc) do
converter = fn x, acc -> do_zip_each(to_list(x), acc) end
case :lists.mapfoldl(converter, [], list) do
{_, nil} ->
:lists.reverse(acc)
{mlist, heads} ->
do_zip(mlist, [to_tuple(:lists.reverse(heads)) | acc])
end
def zip(list_of_lists) when is_list(list_of_lists) do
do_zip(list_of_lists, [])
end
defp do_zip_each(_, nil) do
{nil, nil}
end
defp do_zip_each([head | tail], acc) do
{tail, [head | acc]}
end
defp do_zip_each([], _) do
{nil, nil}
end
defp to_list(tuple) when is_tuple(tuple), do: Tuple.to_list(tuple)
defp to_list(list) when is_list(list), do: list
@doc ~S"""
Checks if `list` is a charlist made only of printable ASCII characters.
@@ -907,7 +895,7 @@ defmodule List do
end
@doc """
Returns `true` if `list` starts with the given `prefix` list, otherwise returns `false`.
Returns `true` if `list` starts with the given `prefix` list; otherwise returns `false`.
If `prefix` is an empty list, it returns `true`.
@@ -936,38 +924,10 @@ defmodule List do
def starts_with?(list, []) when is_list(list), do: true
def starts_with?(list, [_ | _]) when is_list(list), do: false
@doc """
Returns `true` if `list` ends with the given `suffix` list, otherwise returns `false`.
If `suffix` is an empty list, it returns `true`.
### Examples
iex> List.ends_with?([1, 2, 3], [2, 3])
true
iex> List.ends_with?([1, 2], [1, 2, 3])
false
iex> List.ends_with?([:alpha], [])
true
iex> List.ends_with?([], [:alpha])
false
"""
@doc since: "1.18.0"
@spec ends_with?(nonempty_list, nonempty_list) :: boolean
@spec ends_with?(list, []) :: true
@spec ends_with?([], nonempty_list) :: false
def ends_with?(list, suffix) do
:lists.suffix(suffix, list)
end
@doc """
Converts a charlist to an atom.
Elixir supports conversions from charlists which contain any Unicode
Elixir supports conversions from charlists which contains any Unicode
code point.
Inlined by the compiler.
@@ -989,7 +949,7 @@ defmodule List do
@doc """
Converts a charlist to an existing atom.
Elixir supports conversions from charlists which contain any Unicode
Elixir supports conversions from charlists which contains any Unicode
code point. Raises an `ArgumentError` if the atom does not exist.
Inlined by the compiler.
@@ -1133,7 +1093,7 @@ defmodule List do
Please check the given list or call inspect/1 to get the list representation, got:
#{inspect(list, charlists: :as_lists)}
#{inspect(list)}
"""
else
result when is_binary(result) ->
@@ -1185,7 +1145,7 @@ defmodule List do
Please check the given list or call inspect/1 to get the list representation, got:
#{inspect(list, charlists: :as_lists)}
#{inspect(list)}
"""
else
result when is_list(result) ->
@@ -1208,7 +1168,7 @@ defmodule List do
An *edit script* is a keyword list. Each key describes the "editing action" to
take in order to bring `list1` closer to being equal to `list2`; a key can be
`:eq`, `:ins`, or `:del`. Each value is a sublist of either `list1` or `list2`
that should be inserted (if the corresponding key is `:ins`), deleted (if the
that should be inserted (if the corresponding key `:ins`), deleted (if the
corresponding key is `:del`), or left alone (if the corresponding key is
`:eq`) in `list1` in order to be closer to `list2`.
@@ -1412,4 +1372,33 @@ defmodule List do
defp do_pop_at([head | tail], index, default, acc) do
do_pop_at(tail, index - 1, default, [head | acc])
end
# zip
defp do_zip(list, acc) do
converter = fn x, acc -> do_zip_each(to_list(x), acc) end
case :lists.mapfoldl(converter, [], list) do
{_, nil} ->
:lists.reverse(acc)
{mlist, heads} ->
do_zip(mlist, [to_tuple(:lists.reverse(heads)) | acc])
end
end
defp do_zip_each(_, nil) do
{nil, nil}
end
defp do_zip_each([head | tail], acc) do
{tail, [head | acc]}
end
defp do_zip_each([], _) do
{nil, nil}
end
defp to_list(tuple) when is_tuple(tuple), do: Tuple.to_list(tuple)
defp to_list(list) when is_list(list), do: list
end
+1 -1
View File
@@ -58,6 +58,6 @@ end
defimpl List.Chars, for: Float do
def to_charlist(term) do
Float.to_charlist(term)
:io_lib_format.fwrite_g(term)
end
end
+202 -346
View File
@@ -61,6 +61,75 @@ defmodule Macro do
> The functions in this module do not evaluate code. In fact,
> evaluating code from macros is often an anti-pattern. For code
> evaluation, see the `Code` module.
## Custom Sigils
Macros are also commonly used to implement custom sigils.
Sigils start with `~` and are followed by one lowercase letter or by one
or more uppercase letters, and then a separator
(see the [Syntax Reference](syntax-reference.md)). One example is
`~D[2020-10-13]` to define a date.
To create a custom sigil, define a macro with the name `sigil_{identifier}`
that takes two arguments. The first argument will be the string, the second
will be a charlist containing any modifiers. If the sigil is lower case
(such as `sigil_x`) then the string argument will allow interpolation.
If the sigil is one or more upper case letters (such as `sigil_X` and
`sigil_EXAMPLE`) then the string will not be interpolated.
Valid modifiers are ASCII letters and digits. Any other character will
cause a syntax error.
Single-letter sigils are typically reserved to the language. Multi-letter
sigils are uppercased and extensively used by the community to embed
alternative markups and data-types within Elixir source code.
The module containing the custom sigil must be imported before the sigil
syntax can be used.
### Examples
As an example, let's define a sigil `~x` and sigil `~X` which
return its contents as a string. However, if the `r` modifier
is given, it reverses the string instead:
defmodule MySigils do
defmacro sigil_x(term, [?r]) do
quote do
unquote(term) |> String.reverse()
end
end
defmacro sigil_x(term, _modifiers) do
term
end
defmacro sigil_X(term, [?r]) do
quote do
unquote(term) |> String.reverse()
end
end
defmacro sigil_X(term, _modifiers) do
term
end
end
import MySigils
~x(with #{"inter" <> "polation"})
#=> "with interpolation"
~x(with #{"inter" <> "polation"})r
#=> "noitalopretni htiw"
~X(without #{"interpolation"})
#=> "without \#{"interpolation"}"
~X(without #{"interpolation"})r
#=> "}\"noitalopretni\"{# tuohtiw"
"""
alias Code.Identifier
@@ -112,12 +181,6 @@ defmodule Macro do
the compiler, each variable is identified by the combination of either
`name` and `metadata[:counter]`, or `name` and `context`.
* `:from_brackets` - Used to determine whether a call to `Access.get/3` is from
bracket syntax.
* `:from_interpolation` - Used to determine whether a call to `Kernel.to_string/1` is
from interpolation.
* `:generated` - Whether the code should be considered as generated by
the compiler or not. This means the compiler and tools like Dialyzer may not
emit certain warnings.
@@ -129,46 +192,30 @@ defmodule Macro do
* `:keep` - Used by `quote/2` with the option `location: :keep` to annotate
the file and the line number of the quoted source.
* `:line` - The line number of the AST node. Note line information is discarded
from quoted code but can be enabled back via the `:line` option.
* `:line` - The line number of the AST node.
* `:from_brackets` - Used to determine whether a call to `Access.get/3` is from
bracket syntax or a function call.
The following metadata keys are enabled by `Code.string_to_quoted/2`:
* `:closing` - contains metadata about the closing pair, such as a `}`
in a tuple or in a map, or such as the closing `)` in a function call
with parens (when `:token_metadata` is true). If the function call
has a do-end block attached to it, its metadata is found under the
`:do` and `:end` metadata
* `:column` - the column number of the AST node (when `:columns` is true).
Note column information is always discarded from quoted code.
with parens. The `:closing` does not delimit the end of expression if
there are `:do` and `:end` metadata (when `:token_metadata` is true)
* `:column` - the column number of the AST node (when `:columns` is true)
* `:delimiter` - contains the opening delimiter for sigils, strings,
and charlists as a string (such as `"{"`, `"/"`, `"'"`, and the like)
* `:format` - set to `:keyword` when an atom is defined as a keyword
* `:do` - contains metadata about the `do` location in a function call with
`do`-`end` blocks (when `:token_metadata` is true)
* `:end` - contains metadata about the `end` location in a function call with
`do`-`end` blocks (when `:token_metadata` is true)
* `:end_of_expression` - denotes when the end of expression effectively
happens (when `:token_metadata` is true). This is only available for
expressions inside "blocks of code", which are either direct children
of a `__block__` or the right side of `->`. The last expression of the
block does not have metadata if it is not followed by an end of line
character (either a newline or `;`). This entry may appear multiple times
in the same metadata if the expression is surround by parens
* `:format` - set to `:keyword` when an atom is defined as a keyword.
It may also be set to `:atom` to distinguish `nil`, `false`, and `true`
happens. Available for all expressions except the last one inside a
`__block__` (when `:token_metadata` is true)
* `:indentation` - indentation of a sigil heredoc
* `:parens` - denotes a node was surrounded by parens for grouping.
This entry may appear multiple times in the same metadata if
multiple pairs are used for grouping
The following metadata keys are private:
* `:alias` - Used for alias hygiene.
@@ -377,7 +424,7 @@ defmodule Macro do
Note the arguments are not unique. If you later on want
to access the same variables, you can invoke this function
with the same inputs. Use `generate_unique_arguments/2` to
generate unique arguments that can't be overridden.
generate a unique arguments that can't be overridden.
## Examples
@@ -391,12 +438,10 @@ defmodule Macro do
def generate_arguments(amount, context), do: generate_arguments(amount, context, &var/2)
@doc """
Returns the path to the node in `ast` for which `fun` returns a truthy value.
Returns the path to the node in `ast` which `fun` returns true.
The path is a list, starting with the node in which `fun` returns
a truthy value, followed by all of its parents.
Returns `nil` if `fun` returns only falsy values.
true, followed by all of its parents.
Computing the path can be an efficient operation when you want
to find a particular node in the AST within its context and then
@@ -407,9 +452,6 @@ defmodule Macro do
iex> Macro.path(quote(do: [1, 2, 3]), & &1 == 3)
[3, [1, 2, 3]]
iex> Macro.path(quote(do: [1, 2]), & &1 == 5)
nil
iex> Macro.path(quote(do: Foo.bar(3)), & &1 == 3)
[3, quote(do: Foo.bar(3))]
@@ -424,7 +466,6 @@ defmodule Macro do
"""
@doc since: "1.14.0"
@spec path(t, (t -> as_boolean(term))) :: [t] | nil
def path(ast, fun) when is_function(fun, 1) do
path(ast, [], fun)
end
@@ -501,8 +542,10 @@ defmodule Macro do
"""
@doc since: "1.11.3"
@spec generate_unique_arguments(0, context :: atom) :: []
@spec generate_unique_arguments(pos_integer, context) ::
[{atom, [counter: integer], context}, ...]
@spec generate_unique_arguments(pos_integer, context) :: [
{atom, [counter: integer], context},
...
]
when context: atom
def generate_unique_arguments(amount, context),
do: generate_arguments(amount, context, &unique_var/2)
@@ -782,23 +825,23 @@ defmodule Macro do
## Options
* `:unquote` - when `true`, this function leaves `unquote/1` and
`unquote_splicing/1` expressions unescaped, effectively unquoting
* `:unquote` - when true, this function leaves `unquote/1` and
`unquote_splicing/1` statements unescaped, effectively unquoting
the contents on escape. This option is useful only when escaping
ASTs which may have quoted fragments in them. Defaults to `false`.
ASTs which may have quoted fragments in them. Defaults to false.
* `:prune_metadata` - when `true`, removes most metadata from escaped AST
* `:prune_metadata` - when true, removes metadata from escaped AST
nodes. Note this option changes the semantics of escaped code and
it should only be used when escaping ASTs. Defaults to `false`.
it should only be used when escaping ASTs. Defaults to false.
As an example for `:prune_metadata`, `ExUnit` stores the AST of every
assertion, so when an assertion fails we can show code snippets to users.
Without this option, each time the test module is compiled, we would get a
different MD5 of the module bytecode, because the AST contains metadata,
As an example, `ExUnit` stores the AST of every assertion, so when
an assertion fails we can show code snippets to users. Without this
option, each time the test module is compiled, we get a different
MD5 of the module bytecode, because the AST contains metadata,
such as counters, specific to the compilation environment. By pruning
the metadata, we ensure that the module is deterministic and reduce
the amount of data `ExUnit` needs to keep around. Only the minimal
amount of metadata is kept, such as `:line`, `:no_parens` and `:delimiter`.
amount of metadata is kept, such as `:line` and `:no_parens`.
## Comparison to `quote/2`
@@ -820,8 +863,7 @@ defmodule Macro do
iex> value = {:a, :b, :c}
iex> quote do: unquote(value)
** (ArgumentError) tried to unquote invalid AST: {:a, :b, :c}
Did you forget to escape term using Macro.escape/1?
{:a, :b, :c}
`escape/2` is used to escape *values* (either directly passed or variable
bound), while `quote/2` produces syntax trees for
@@ -834,36 +876,30 @@ defmodule Macro do
:elixir_quote.escape(expr, kind, unquote)
end
# TODO: Deprecate me on Elixir v1.22
@doc deprecated: "Use Macro.struct_info!/2 instead"
def struct!(module, env) when is_atom(module) do
pairs =
for %{field: field, default: default} <- struct_info!(module, env), do: {field, default}
:maps.from_list([__struct__: module] ++ pairs)
end
@doc """
Extracts the struct information (equivalent to calling
`module.__info__(:struct)`).
Expands the struct given by `module` in the given `env`.
This is useful when a struct needs to be expanded at
compilation time and the struct being expanded may or may
not have been compiled. This function is also capable of
expanding structs defined under the module being compiled.
Calling this function also adds an export dependency on the
given struct.
It will raise `ArgumentError` if the struct is not available.
It will raise `CompileError` if the struct is not available.
From Elixir v1.12, calling this function also adds an export
dependency on the given struct.
"""
@doc since: "1.18.0"
@spec struct_info!(module(), Macro.Env.t()) ::
[%{field: atom(), required: boolean(), default: term()}]
def struct_info!(module, env) when is_atom(module) do
case :elixir_map.maybe_load_struct_info([line: env.line], module, [], true, env) do
{:ok, info} -> info
{:error, desc} -> raise ArgumentError, List.to_string(:elixir_map.format_error(desc))
end
@doc since: "1.8.0"
@spec struct!(module, Macro.Env.t()) ::
%{required(:__struct__) => module, optional(atom) => any}
when module: module()
def struct!(module, env) when is_atom(module) do
if module == env.module do
Module.get_attribute(module, :__struct__)
end ||
case :elixir_map.maybe_load_struct([line: env.line], module, [], [], env) do
{:ok, struct} -> struct
{:error, desc} -> raise ArgumentError, List.to_string(:elixir_map.format_error(desc))
end
end
@doc """
@@ -908,8 +944,8 @@ defmodule Macro do
defp find_invalid(bin) when is_binary(bin), do: nil
defp find_invalid(fun) when is_function(fun) do
if Function.info(fun, :env) != {:env, []} or
Function.info(fun, :type) != {:type, :external} do
unless Function.info(fun, :env) == {:env, []} and
Function.info(fun, :type) == {:type, :external} do
{:error, fun}
end
end
@@ -1138,10 +1174,6 @@ defmodule Macro do
`Code.quoted_to_algebra/2` as a lower level function
with more control around formatting.
If the AST contains invalid nodes, this function will
attempt to inspect them, to aid debugging, although
the elements won't be formatted accordingly.
## Examples
iex> Macro.to_string(quote(do: foo.bar(1, 2, 3)))
@@ -1149,10 +1181,9 @@ defmodule Macro do
"""
@spec to_string(t()) :: String.t()
# TODO: Allow line_length to be configurable on v1.17
def to_string(tree) do
doc =
Inspect.Algebra.format(Code.quoted_to_algebra(tree, migrate_charlists_as_sigils: true), 98)
doc = Inspect.Algebra.format(Code.quoted_to_algebra(tree), 98)
IO.iodata_to_binary(doc)
end
@@ -1501,7 +1532,7 @@ defmodule Macro do
:error
end
defp op_call({:..//, _, [left, middle, right]} = ast, fun) do
defp op_call({:"..//", _, [left, middle, right]} = ast, fun) do
left = op_to_string(left, fun, :.., :left)
middle = op_to_string(middle, fun, :.., :right)
right = op_to_string(right, fun, :"//", :right)
@@ -1727,18 +1758,6 @@ defmodule Macro do
end
end
@doc """
Applies a `mod`, `function`, and `args` at compile-time in `caller`.
This is used when you want to programmatically invoke a macro at
compile-time.
"""
@doc since: "1.16.0"
def compile_apply(mod, fun, args, caller) do
:elixir_env.trace({:remote_macro, [], mod, fun, length(args)}, caller)
Kernel.apply(mod, fun, args)
end
@doc """
Receives an AST node and expands it once.
@@ -1751,9 +1770,7 @@ defmodule Macro do
If the expression cannot be expanded, it returns the expression
itself. This function does not traverse the AST, only the root
node is expanded. The expansion happens as if it was expanded by
the Elixir compiler and therefore compilation tracers will be invoked
and deprecation warnings will be emitted during the expansion.
node is expanded.
`expand_once/2` performs the expansion just once. Check `expand/2`
to perform expansion until the node can no longer be expanded.
@@ -1824,23 +1841,23 @@ defmodule Macro do
elem(do_expand_once(ast, env), 0)
end
defp do_expand_once({:__aliases__, meta, list} = alias, env) do
case :elixir_aliases.expand_or_concat(meta, list, env, true) do
defp do_expand_once({:__aliases__, meta, _} = original, env) do
case :elixir_aliases.expand_or_concat(original, env) do
receiver when is_atom(receiver) ->
:elixir_env.trace({:alias_reference, meta, receiver}, env)
{receiver, true}
[head | tail] ->
{head, _} = do_expand_once(head, env)
aliases ->
aliases = :lists.map(&elem(do_expand_once(&1, env), 0), aliases)
case is_atom(head) do
case :lists.all(&is_atom/1, aliases) do
true ->
receiver = :elixir_aliases.concat([head | tail])
receiver = :elixir_aliases.concat(aliases)
:elixir_env.trace({:alias_reference, meta, receiver}, env)
{receiver, true}
false ->
{alias, false}
{original, false}
end
end
end
@@ -1851,13 +1868,13 @@ 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) and env.context != :match do
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) and env.context != :match do
when is_atom(atom) and is_atom(field) do
if Map.has_key?(env, field) do
{maybe_escape_map(Map.get(env, field)), true}
else
@@ -1865,39 +1882,56 @@ defmodule Macro do
end
end
defp do_expand_once({name, meta, context} = original, _env)
when is_atom(name) and is_list(meta) and is_atom(context) do
defp do_expand_once({atom, meta, context} = original, _env)
when is_atom(atom) and is_list(meta) and is_atom(context) do
{original, false}
end
defp do_expand_once({name, meta, args} = original, env)
when is_atom(name) and is_list(args) and is_list(meta) do
defp do_expand_once({atom, meta, args} = original, env)
when is_atom(atom) and is_list(args) and is_list(meta) do
arity = length(args)
case Macro.Env.expand_import(env, meta, name, arity) do
{:macro, _receiver, expander} ->
# We don't want the line to propagate yet, but generated might!
{expander.(Keyword.take(meta, [:generated]), args), true}
if special_form?(atom, arity) do
{original, false}
else
module = env.module
{:function, Kernel, op} when op in [:+, :-] and arity == 1 ->
case expand_once(hd(args), env) do
integer when is_integer(integer) -> {apply(Kernel, op, [integer]), true}
_ -> {original, false}
extra =
if function_exported?(module, :__info__, 1) do
[{module, module.__info__(:macros)}]
else
[]
end
{:function, _receiver, _name} ->
{original, false}
s = :elixir_env.env_to_ex(env)
{:error, :not_found} ->
{original, false}
expand =
:elixir_dispatch.expand_import(meta, {atom, length(args)}, args, s, env, extra, true)
{:error, other} ->
:elixir_errors.file_error(meta, env, :elixir_dispatch, {:import, other, name, arity})
case expand do
{:ok, receiver, quoted} ->
next = :elixir_module.next_counter(module)
# We don't want the line to propagate yet, but generated might!
meta = Keyword.take(meta, [:generated])
{:elixir_quote.linify_with_context_counter(meta, {receiver, next}, quoted), true}
{:ok, Kernel, op, [arg]} when op in [:+, :-] ->
case expand_once(arg, env) do
integer when is_integer(integer) -> {apply(Kernel, op, [integer]), true}
_ -> {original, false}
end
{:ok, _receiver, _name, _args} ->
{original, false}
:error ->
{original, false}
end
end
end
# Expand possible macro require invocation
defp do_expand_once({{:., _, [left, name]}, meta, args} = original, env) when is_atom(name) do
defp do_expand_once({{:., _, [left, right]}, meta, args} = original, env) when is_atom(right) do
{receiver, _} = do_expand_once(left, env)
case is_atom(receiver) do
@@ -1905,10 +1939,16 @@ defmodule Macro do
{original, false}
true ->
case Macro.Env.expand_require(env, meta, receiver, name, length(args)) do
{:macro, _receiver, expander} ->
s = :elixir_env.env_to_ex(env)
name_arity = {right, length(args)}
expand = :elixir_dispatch.expand_require(meta, receiver, name_arity, args, s, env)
case expand do
{:ok, receiver, quoted} ->
next = :elixir_module.next_counter(env.module)
# We don't want the line to propagate yet, but generated might!
{expander.(Keyword.take(meta, [:generated]), args), true}
meta = Keyword.take(meta, [:generated])
{:elixir_quote.linify_with_context_counter(meta, {receiver, next}, quoted), true}
:error ->
{original, false}
@@ -1952,7 +1992,7 @@ defmodule Macro do
@spec operator?(name :: atom(), arity()) :: boolean()
def operator?(name, arity)
def operator?(:..//, 3),
def operator?(:"..//", 3),
do: true
# Code.Identifier treats :// as a binary operator for precedence
@@ -1963,8 +2003,8 @@ defmodule Macro do
def operator?(name, 1) when is_atom(name),
do: Identifier.unary_op(name) != :error
def operator?(:.., 0), do: true
def operator?(:..., 0), do: true
def operator?(:.., 0),
do: true
def operator?(name, arity) when is_atom(name) and is_integer(arity), do: false
@@ -2155,8 +2195,9 @@ defmodule Macro do
@doc """
Converts the given argument to a string with the underscore-slash format.
The argument must either be an atom, representing an Elixir module,
or a string representing a module without the `Elixir.` prefix.
The argument must either be an atom or a string.
If an atom is given, it is assumed to be an Elixir module,
so it is converted to a string and then processed.
This function was designed to format language identifiers/tokens with the underscore-slash format,
that's why it belongs to the `Macro` module. Do not use it as a general
@@ -2190,7 +2231,7 @@ defmodule Macro do
"Foo.Bar"
"""
@spec underscore(atom() | String.t()) :: String.t()
@spec underscore(module() | atom() | String.t()) :: String.t()
def underscore(atom_or_string)
def underscore(atom) when is_atom(atom) do
@@ -2281,7 +2322,7 @@ defmodule Macro do
## Atom handling
@doc """
Classifies an `atom` based on its possible AST placement.
Classifies a runtime `atom` based on its possible AST placement.
It returns one of the following atoms:
@@ -2334,15 +2375,6 @@ defmodule Macro do
as a key (`:key`), or as the function name of a remote call
(`:remote_call`).
## Options
* `:escape` - a two-arity function used to escape a quoted
atom content, if necessary. The function receives the atom
content as string and a quote delimiter character, which
should always be escaped. By default the content is escaped
such that the inspected sequence would be parsed as the
given atom.
## Examples
### As a literal
@@ -2391,14 +2423,14 @@ defmodule Macro do
"""
@doc since: "1.14.0"
@spec inspect_atom(:literal | :key | :remote_call, atom, keyword) :: binary
def inspect_atom(source_format, atom, opts \\ [])
@spec inspect_atom(:literal | :key | :remote_call, atom) :: binary
def inspect_atom(source_format, atom)
def inspect_atom(:literal, atom, _opts) when is_nil(atom) or is_boolean(atom) do
def inspect_atom(:literal, atom) when is_nil(atom) or is_boolean(atom) do
Atom.to_string(atom)
end
def inspect_atom(:literal, atom, opts) when is_atom(atom) do
def inspect_atom(:literal, atom) when is_atom(atom) do
binary = Atom.to_string(atom)
case classify_atom(atom) do
@@ -2410,7 +2442,7 @@ defmodule Macro do
end
:quoted ->
escaped = inspect_atom_escape(opts, binary, ?")
{escaped, _} = Code.Identifier.escape(binary, ?")
IO.iodata_to_binary([?:, ?", escaped, ?"])
_ ->
@@ -2418,7 +2450,7 @@ defmodule Macro do
end
end
def inspect_atom(:key, atom, opts) when is_atom(atom) do
def inspect_atom(:key, atom) when is_atom(atom) do
binary = Atom.to_string(atom)
case classify_atom(atom) do
@@ -2426,7 +2458,7 @@ defmodule Macro do
IO.iodata_to_binary([?", binary, ?", ?:])
:quoted ->
escaped = inspect_atom_escape(opts, binary, ?")
{escaped, _} = Code.Identifier.escape(binary, ?")
IO.iodata_to_binary([?", escaped, ?", ?:])
_ ->
@@ -2434,7 +2466,7 @@ defmodule Macro do
end
end
def inspect_atom(:remote_call, atom, opts) when is_atom(atom) do
def inspect_atom(:remote_call, atom) when is_atom(atom) do
binary = Atom.to_string(atom)
case inner_classify(atom) do
@@ -2446,22 +2478,13 @@ defmodule Macro do
if type in [:not_callable, :alias] do
binary
else
inspect_atom_escape(opts, binary, ?")
elem(Code.Identifier.escape(binary, ?"), 0)
end
IO.iodata_to_binary([?", escaped, ?"])
end
end
defp inspect_atom_escape(opts, string, char) do
if escape = opts[:escape] do
escape.(string, char)
else
{escaped, _} = Code.Identifier.escape(string, char)
escaped
end
end
# Classifies the given atom into one of the following categories:
#
# * `:alias` - a valid Elixir alias, like `Foo`, `Foo.Bar` and so on
@@ -2488,7 +2511,7 @@ defmodule Macro do
#
defp inner_classify(atom) when is_atom(atom) do
cond do
atom in [:%, :%{}, :{}, :<<>>, :..., :.., :., :..//, :->] ->
atom in [:%, :%{}, :{}, :<<>>, :..., :.., :., :"..//", :->] ->
:not_callable
# <|>, ^^^, and ~~~ are deprecated
@@ -2586,13 +2609,13 @@ defmodule Macro do
header = dbg_format_header(env)
quote do
to_debug = unquote(dbg_ast_to_debuggable(code, env))
to_debug = unquote(dbg_ast_to_debuggable(code))
unquote(__MODULE__).__dbg__(unquote(header), to_debug, unquote(options))
end
end
# Pipelines.
defp dbg_ast_to_debuggable({:|>, _meta, _args} = pipe_ast, _env) do
defp dbg_ast_to_debuggable({:|>, _meta, _args} = pipe_ast) do
value_var = unique_var(:value, __MODULE__)
values_acc_var = unique_var(:values, __MODULE__)
@@ -2625,7 +2648,7 @@ defmodule Macro do
dbg_decomposed_binary_operators = [:&&, :||, :and, :or]
# Logic operators.
defp dbg_ast_to_debuggable({op, _meta, [_left, _right]} = ast, _env)
defp dbg_ast_to_debuggable({op, _meta, [_left, _right]} = ast)
when op in unquote(dbg_decomposed_binary_operators) do
acc_var = unique_var(:acc, __MODULE__)
result_var = unique_var(:result, __MODULE__)
@@ -2633,22 +2656,11 @@ defmodule Macro do
quote do
unquote(acc_var) = []
unquote(dbg_boolean_tree(ast, acc_var, result_var))
{:logic_op, Enum.reverse(unquote(acc_var)), unquote(result_var)}
{:logic_op, Enum.reverse(unquote(acc_var))}
end
end
defp dbg_ast_to_debuggable({:__block__, _meta, exprs} = ast, _env) when exprs != [] do
acc_var = unique_var(:acc, __MODULE__)
result_var = unique_var(:result, __MODULE__)
quote do
unquote(acc_var) = []
unquote(dbg_block(ast, acc_var, result_var))
{:block, Enum.reverse(unquote(acc_var)), unquote(result_var)}
end
end
defp dbg_ast_to_debuggable({:case, _meta, [expr, [do: clauses]]} = ast, _env) do
defp dbg_ast_to_debuggable({:case, _meta, [expr, [do: clauses]]} = ast) do
clauses_returning_index =
Enum.with_index(clauses, fn {:->, meta, [left, right]}, index ->
{:->, meta, [left, {right, index}]}
@@ -2666,7 +2678,7 @@ defmodule Macro do
end
end
defp dbg_ast_to_debuggable({:cond, _meta, [[do: clauses]]} = ast, _env) do
defp dbg_ast_to_debuggable({:cond, _meta, [[do: clauses]]} = ast) do
modified_clauses =
Enum.with_index(clauses, fn {:->, _meta, [[left], right]}, index ->
hd(
@@ -2687,104 +2699,8 @@ defmodule Macro do
end
end
defp dbg_ast_to_debuggable({:if, meta, [condition_ast, clauses]} = ast, env) do
case Macro.Env.lookup_import(env, {:if, 2}) do
[macro: Kernel] ->
condition_result_var = unique_var(:condition_result, __MODULE__)
quote do
unquote(condition_result_var) = unquote(condition_ast)
result = unquote({:if, meta, [condition_result_var, clauses]})
{:if, unquote(escape(ast)), unquote(escape(condition_ast)),
unquote(condition_result_var), result}
end
_ ->
quote do: {:value, unquote(escape(ast)), unquote(ast)}
end
end
defp dbg_ast_to_debuggable({:with, meta, args} = ast, _env) do
{opts, clauses} = List.pop_at(args, -1)
acc_var = unique_var(:acc, __MODULE__)
modified_clauses =
Enum.flat_map(clauses, fn
# We only detail assignments and pattern-matching clauses that
# can be helpful to understand how the result is constructed.
{:<-, _meta, [left, right]} ->
modified_left =
case left do
{:when, meta, [pattern, guard]} -> {:when, meta, [{pattern, quote(do: _)}, guard]}
pattern -> {pattern, quote(do: _)}
end
quote do
[
value = unquote(right),
unquote(acc_var) = [{unquote(escape(right)), value} | unquote(acc_var)],
unquote(modified_left) <- {value, unquote(acc_var)}
]
end
{:=, _meta, [left, right]} ->
quote do
[
value = unquote(right),
unquote(acc_var) = [{unquote(escape(right)), value} | unquote(acc_var)],
unquote(left) = value
]
end
# Other expressions like side effects are omitted.
expr ->
[expr]
end)
modified_opts =
Enum.map(opts, fn
{:do, do_block} ->
{:do, {do_block, acc_var}}
{:else, else_block} ->
clauses =
Enum.map(else_block, fn
{:->, meta, [[{:when, meta2, [pattern, guard]}], right]} ->
{:->, meta, [[{:when, meta2, [{pattern, acc_var}, guard]}], {right, acc_var}]}
{:->, meta, [[left], right]} ->
{:->, meta, [[{left, acc_var}], {right, acc_var}]}
invalid ->
invalid
end)
error_clause =
quote generated: true do
{other, _acc} -> raise WithClauseError, term: other
end
{:else, clauses ++ error_clause}
invalid ->
invalid
end)
modified_with_ast = {:with, meta, modified_clauses ++ [modified_opts]}
quote do
unquote(acc_var) = []
{value, acc} = unquote(modified_with_ast)
{:with, unquote(escape(ast)), Enum.reverse(acc), value}
end
end
# Any other AST.
defp dbg_ast_to_debuggable(ast, _env) do
defp dbg_ast_to_debuggable(ast) do
quote do: {:value, unquote(escape(ast)), unquote(ast)}
end
@@ -2815,18 +2731,6 @@ defmodule Macro do
end
end
defp dbg_block({:__block__, meta, exprs}, acc_var, result_var) do
modified_exprs =
Enum.map(exprs, fn expr ->
quote do
unquote(result_var) = unquote(expr)
unquote(acc_var) = [{unquote(escape(expr)), unquote(result_var)} | unquote(acc_var)]
end
end)
{:__block__, meta, modified_exprs}
end
# Made public to be called from Macro.dbg/3, so that we generate as little code
# as possible and call out into a function as soon as we can.
@doc false
@@ -2864,27 +2768,15 @@ defmodule Macro do
{[first_formatted | rest_formatted], result}
end
defp dbg_format_ast_to_debug({:logic_op, components, value}, options) do
defp dbg_format_ast_to_debug({:logic_op, components}, options) do
{_ast, final_value} = List.last(components)
formatted =
Enum.map(components, fn {ast, value} ->
[dbg_format_ast(to_string_with_colors(ast, options)), " ", inspect(value, options), ?\n]
end)
{formatted, value}
end
defp dbg_format_ast_to_debug({:block, components, value}, options) do
formatted =
[
dbg_maybe_underline("Code block", options),
":\n(\n",
Enum.map(components, fn {ast, value} ->
[" ", dbg_format_ast_with_value(ast, value, options)]
end),
")\n"
]
{formatted, value}
{formatted, final_value}
end
defp dbg_format_ast_to_debug({:case, ast, expr_value, clause_index, value}, options) do
@@ -2920,42 +2812,6 @@ defmodule Macro do
{formatted, value}
end
defp dbg_format_ast_to_debug(
{:if, ast, condition_ast, condition_result, result},
options
) do
formatted = [
dbg_maybe_underline("If condition", options),
":\n",
dbg_format_ast_with_value(condition_ast, condition_result, options),
?\n,
dbg_maybe_underline("If expression", options),
":\n",
dbg_format_ast_with_value(ast, result, options)
]
{formatted, result}
end
defp dbg_format_ast_to_debug({:with, ast, clauses, result}, options) do
formatted_clauses =
Enum.map(clauses, fn {clause_ast, clause_result} ->
dbg_format_ast_with_value(clause_ast, clause_result, options)
end)
formatted = [
dbg_maybe_underline("With clauses", options),
":\n",
formatted_clauses,
?\n,
dbg_maybe_underline("With expression", options),
":\n",
dbg_format_ast_with_value(ast, result, options)
]
{formatted, result}
end
defp dbg_format_ast_to_debug({:value, code_ast, value}, options) do
{dbg_format_ast_with_value(code_ast, value, options), value}
end
+42 -366
View File
@@ -6,25 +6,20 @@ defmodule Macro.Env do
`__ENV__/0`. Inside macros, the caller environment can be
accessed as `__CALLER__/0`.
The majority of the functions in this module are provided
for low-level tools, which need to integrate with the Elixir
compiler, such as language servers and embedded languages.
For regular usage in Elixir code and macros, you must use
the `Macro` module instead. In particular, avoid modifying
the `Macro.Env` struct directly and prefer to use high-level
constructs, such as a `import`, `aliases`, and so forth to
build your own environment. For example, to build a custom
environment, you can define a function such as:
An instance of `Macro.Env` must not be modified by hand. If you need to
create a custom environment to pass to `Code.eval_quoted/3`, use the
following trick:
def make_custom_env do
import SomeModule, only: [some_function: 2], warn: false
alias A.B.C, warn: false
import SomeModule, only: [some_function: 2]
alias A.B.C
__ENV__
end
## Struct fields
You may then call `make_custom_env()` to get a struct with the desired
imports and aliases included.
The `Macro.Env` struct contains the following fields:
It contains the following fields:
* `context` - the context of the environment; it can be `nil`
(default context), `:guard` (inside a guard) or `:match` (inside a match)
@@ -102,10 +97,8 @@ defmodule Macro.Env do
]
# Define the __struct__ callbacks by hand for bootstrap reasons.
{_struct, [], escaped_struct, kv, body} =
Kernel.Utils.defstruct(__MODULE__, fields, false, __ENV__)
def __struct__(), do: unquote(escaped_struct)
{struct, [], kv, body} = Kernel.Utils.defstruct(__MODULE__, fields, false)
def __struct__(), do: unquote(:elixir_quote.escape(struct, false, :none))
def __struct__(unquote(kv)), do: unquote(body)
@doc """
@@ -170,13 +163,39 @@ defmodule Macro.Env do
[file: file, line: line]
end
# TODO: Deprecate on Elixir 1.21 in favor of expand_alias/4
@doc false
@doc """
Fetches the alias for the given atom.
Returns `{:ok, alias}` if the alias exists, `:error`
otherwise.
## Examples
iex> alias Foo.Bar, as: Baz
iex> Baz
Foo.Bar
iex> Macro.Env.fetch_alias(__ENV__, :Baz)
{:ok, Foo.Bar}
iex> Macro.Env.fetch_alias(__ENV__, :Unknown)
:error
"""
@doc since: "1.13.0"
@spec fetch_alias(t, atom) :: {:ok, atom} | :error
def fetch_alias(%{__struct__: Macro.Env, aliases: aliases}, atom) when is_atom(atom),
do: Keyword.fetch(aliases, :"Elixir.#{atom}")
# TODO: Deprecate on Elixir 1.21 in favor of expand_alias/4
@doc false
@doc """
Fetches the macro alias for the given atom.
Returns `{:ok, macro_alias}` if the alias exists, `:error`
otherwise.
A macro alias is only used inside quoted expansion. See
`fetch_alias/2` for a more general example.
"""
@doc since: "1.13.0"
@spec fetch_macro_alias(t, atom) :: {:ok, atom} | :error
def fetch_macro_alias(%{__struct__: Macro.Env, macro_aliases: aliases}, atom)
when is_atom(atom),
do: Keyword.fetch(aliases, :"Elixir.#{atom}")
@@ -189,14 +208,6 @@ defmodule Macro.Env do
`{:function | :macro, module}`. The elements in the list
are in no particular order and the order is not guaranteed.
> #### Use only for introspection {: .warning}
>
> This function does not emit compiler tracing events,
> which may block the compiler from correctly tracking
> dependencies. Use this function for reflection purposes
> but to do not use it to expand imports into qualified
> calls. Instead, use `expand_import/5`.
## Examples
iex> Macro.Env.lookup_import(__ENV__, {:duplicate, 2})
@@ -214,8 +225,6 @@ defmodule Macro.Env do
"""
@doc since: "1.13.0"
@spec lookup_import(t, name_arity) :: [{:function | :macro, module}]
def lookup_import(env, name_arity)
def lookup_import(
%{__struct__: Macro.Env, functions: functions, macros: macros},
{name, arity} = pair
@@ -247,14 +256,12 @@ defmodule Macro.Env do
"""
@doc since: "1.15.0"
@spec lookup_alias_as(t, atom) :: [atom]
def lookup_alias_as(env, atom)
def lookup_alias_as(%{__struct__: Macro.Env, aliases: aliases}, atom) when is_atom(atom) do
for {name, ^atom} <- aliases, do: name
end
@doc """
Returns `true` if the given module has been required.
Returns true if the given module has been required.
## Examples
@@ -269,8 +276,6 @@ defmodule Macro.Env do
"""
@doc since: "1.13.0"
@spec required?(t, module) :: boolean
def required?(env, module)
def required?(%{__struct__: Macro.Env, requires: requires}, mod) when is_atom(mod),
do: mod in requires
@@ -288,337 +293,8 @@ defmodule Macro.Env do
%{env | tracers: [tracer | tracers]}
end
trace_option = """
`:trace` - when set to `false`, it disables compilation tracers and
lexical tracker. This option must only be used by language servers and
other tools that need to introspect code without affecting how it is compiled.
Disabling tracer inside macros or regular code expansion is extremely
discouraged as it blocks the compiler from accurately tracking dependencies\
"""
@doc """
Defines the given `module` as required in the environment.
It does not check or assert the module is available.
This is used by tools which need to mimic the Elixir compiler.
The appropriate `:require` compiler tracing event will be emitted.
## Additional options
It accepts the same options as `Kernel.SpecialForm.require/2` plus:
* #{trace_option}
## Examples
iex> env = __ENV__
iex> Macro.Env.required?(env, Integer)
false
iex> {:ok, env} = Macro.Env.define_require(env, [line: 10], Integer)
iex> Macro.Env.required?(env, Integer)
true
If the `:as` option is given, it will also define an alias:
iex> env = __ENV__
iex> {:ok, env} = Macro.Env.define_require(env, [line: 10], Foo.Bar, as: Baz)
iex> Macro.Env.expand_alias(env, [], [:Baz])
{:alias, Foo.Bar}
"""
@doc since: "1.17.0"
@spec define_require(t, Macro.metadata(), module) :: {:ok, t}
def define_require(env, meta, module, opts \\ [])
when is_list(meta) and is_atom(module) and is_list(opts) do
{trace, opts} = Keyword.pop(opts, :trace, true)
env = :elixir_aliases.require(meta, module, opts, env, trace)
result = :elixir_aliases.alias(meta, module, false, opts, env, trace)
maybe_define_error(result, :elixir_aliases)
end
@doc """
Defines the given `module` as imported in the environment.
It assumes `module` is available. This is used by tools which
need to mimic the Elixir compiler. The appropriate `:import`
compiler tracing event will be emitted.
## Additional options
It accepts the same options as `Kernel.SpecialForm.import/2` plus:
* `:emit_warnings` - emit warnings found when defining imports
* #{trace_option}
* `:info_callback` - a function to use instead of `c:Module.__info__/1`.
The function will be invoked with `:functions` or `:macros` argument.
It has to return a list of `{function, arity}` key value pairs.
If it fails, it defaults to using module metadata based on `module_info/1`.
## Examples
iex> env = __ENV__
iex> Macro.Env.lookup_import(env, {:flatten, 1})
[]
iex> {:ok, env} = Macro.Env.define_import(env, [line: 10], List)
iex> Macro.Env.lookup_import(env, {:flatten, 1})
[{:function, List}]
It accepts the same options as `Kernel.SpecialForm.import/2`:
iex> env = __ENV__
iex> Macro.Env.lookup_import(env, {:is_odd, 1})
[]
iex> {:ok, env} = Macro.Env.define_import(env, [line: 10], Integer, only: :macros)
iex> Macro.Env.lookup_import(env, {:is_odd, 1})
[{:macro, Integer}]
## Info callback override
iex> env = __ENV__
iex> Macro.Env.lookup_import(env, {:flatten, 1})
[]
iex> {:ok, env} = Macro.Env.define_import(env, [line: 10], SomeModule, [info_callback: fn :functions -> [{:flatten, 1}]; :macros -> [{:some, 2}]; end])
iex> Macro.Env.lookup_import(env, {:flatten, 1})
[{:function, SomeModule}]
iex> Macro.Env.lookup_import(env, {:some, 2})
[{:macro, SomeModule}]
"""
@doc since: "1.17.0"
@spec define_import(t, Macro.metadata(), module, keyword) :: {:ok, t} | {:error, String.t()}
def define_import(env, meta, module, opts \\ [])
when is_list(meta) and is_atom(module) and is_list(opts) do
{trace, opts} = Keyword.pop(opts, :trace, true)
{warnings, opts} = Keyword.pop(opts, :emit_warnings, true)
{info_callback, opts} = Keyword.pop(opts, :info_callback, &module.__info__/1)
result = :elixir_import.import(meta, module, opts, env, warnings, trace, info_callback)
maybe_define_error(result, :elixir_import)
end
@doc """
Defines the given `as` an alias to `module` in the environment.
This is used by tools which need to mimic the Elixir compiler.
The appropriate `:alias` compiler tracing event will be emitted.
## Additional options
It accepts the same options as `Kernel.SpecialForm.alias/2` plus:
* #{trace_option}
## Examples
iex> env = __ENV__
iex> Macro.Env.expand_alias(env, [], [:Baz])
:error
iex> {:ok, env} = Macro.Env.define_alias(env, [line: 10], Foo.Bar, as: Baz)
iex> Macro.Env.expand_alias(env, [], [:Baz])
{:alias, Foo.Bar}
iex> Macro.Env.expand_alias(env, [], [:Baz, :Bat])
{:alias, Foo.Bar.Bat}
If no `:as` option is given, the alias will be inferred from the module:
iex> env = __ENV__
iex> {:ok, env} = Macro.Env.define_alias(env, [line: 10], Foo.Bar)
iex> Macro.Env.expand_alias(env, [], [:Bar])
{:alias, Foo.Bar}
If it is not possible to infer one, an error is returned:
iex> Macro.Env.define_alias(__ENV__, [line: 10], :an_atom)
{:error,
"alias cannot be inferred automatically for module: :an_atom, " <>
"please use the :as option. Implicit aliasing is only supported with Elixir modules"}
"""
@doc since: "1.17.0"
@spec define_alias(t, Macro.metadata(), module, keyword) :: {:ok, t} | {:error, String.t()}
def define_alias(env, meta, module, opts \\ [])
when is_list(meta) and is_atom(module) and is_list(opts) do
{trace, opts} = Keyword.pop(opts, :trace, true)
result = :elixir_aliases.alias(meta, module, true, opts, env, trace)
maybe_define_error(result, :elixir_aliases)
end
defp maybe_define_error({:ok, _info, env}, _mod),
do: {:ok, env}
defp maybe_define_error({:error, reason}, mod),
do: {:error, Kernel.to_string(mod.format_error(reason))}
@doc """
Expands an alias given by the alias segments.
It returns `{:alias, alias}` if the segments is a list
of atoms and an alias was found. Returns `:error` otherwise.
This expansion may emit the `:alias_expansion` trace event
but it does not emit the `:alias_reference` one.
## Options
* #{trace_option}
## Examples
iex> alias List, as: MyList
iex> Macro.Env.expand_alias(__ENV__, [], [:MyList])
{:alias, List}
iex> Macro.Env.expand_alias(__ENV__, [], [:MyList, :Nested])
{:alias, List.Nested}
If there is no alias or the alias starts with `Elixir.`
(which disables aliasing), then `:error` is returned:
iex> alias List, as: MyList
iex> Macro.Env.expand_alias(__ENV__, [], [:Elixir, MyList])
:error
iex> Macro.Env.expand_alias(__ENV__, [], [:AnotherList])
:error
"""
@doc since: "1.17.0"
@spec expand_alias(t, keyword, [atom()], keyword) ::
{:alias, atom()} | :error
def expand_alias(env, meta, list, opts \\ [])
when is_list(meta) and is_list(list) and is_list(opts) do
trace = Keyword.get(opts, :trace, true)
case :elixir_aliases.expand(meta, list, env, trace) do
atom when is_atom(atom) -> {:alias, atom}
[_ | _] -> :error
end
end
@doc """
Expands an import given by `name` and `arity`.
If the import points to a macro, it returns a tuple
with the module and a function that expands the macro.
The function expects the metadata to be attached to the
expansion and the arguments of the macro.
If the import points to a function, it returns a tuple
with the module and the function name.
If any import is found, the appropriate compiler tracing
event will be emitted.
Otherwise returns `{:error, reason}`.
## Options
* `:allow_locals` - when set to `false`, it does not attempt to capture
local macros defined in the current module in `env`
* `:check_deprecations` - when set to `false`, does not check for deprecations
when expanding macros
* #{trace_option}
"""
@doc since: "1.17.0"
@spec expand_import(t, keyword, atom(), arity(), keyword) ::
{:macro, module(), (Macro.metadata(), args :: [Macro.t()] -> Macro.t())}
| {:function, module(), atom()}
| {:error, :not_found | {:conflict, module()} | {:ambiguous, [module()]}}
def expand_import(env, meta, name, arity, opts \\ [])
when is_list(meta) and is_atom(name) and is_integer(arity) and is_list(opts) do
case :elixir_import.special_form(name, arity) do
true ->
{:error, :not_found}
false ->
allow_locals = Keyword.get(opts, :allow_locals, true)
trace = Keyword.get(opts, :trace, true)
module = env.module
extra =
case allow_locals and function_exported?(module, :__info__, 1) do
true -> [{module, module.__info__(:macros)}]
false -> []
end
case :elixir_dispatch.expand_import(meta, name, arity, env, extra, allow_locals, trace) do
{:macro, receiver, expander} ->
{:macro, receiver, wrap_expansion(receiver, expander, meta, name, arity, env, opts)}
{:function, receiver, name} ->
{:function, receiver, name}
error ->
{:error, error}
end
end
end
@doc """
Expands a require given by `module`, `name`, and `arity`.
If the require points to a macro and the module has been
required, it returns a tuple with the module and a function
that expands the macro. The function expects the metadata
to be attached to the expansion and the arguments of the macro.
The appropriate `:remote_macro` compiler tracing event will
be emitted if a macro is found (note a `:remote_function`
event is not emitted in `:error` cases).
Otherwise returns `:error`.
## Options
* `:check_deprecations` - when set to `false`, does not check for deprecations
when expanding macros
* #{trace_option}
"""
@doc since: "1.17.0"
@spec expand_require(t, keyword, module(), atom(), arity(), keyword) ::
{:macro, module(), (Macro.metadata(), args :: [Macro.t()] -> Macro.t())}
| :error
def expand_require(env, meta, module, name, arity, opts \\ [])
when is_list(meta) and is_atom(module) and is_atom(name) and is_integer(arity) and
is_list(opts) do
trace = Keyword.get(opts, :trace, true)
case :elixir_dispatch.expand_require(meta, module, name, arity, env, trace) do
{:macro, receiver, expander} ->
{:macro, receiver, wrap_expansion(receiver, expander, meta, name, arity, env, opts)}
:error ->
:error
end
end
defp wrap_expansion(receiver, expander, meta, name, arity, env, opts) do
fn expansion_meta, args ->
if Keyword.get(opts, :check_deprecations, true) do
:elixir_dispatch.check_deprecated(:macro, meta, receiver, name, arity, env)
end
quoted = expander.(args, env)
next = :elixir_module.next_counter(env.module)
:elixir_quote.linify_with_context_counter(expansion_meta, {receiver, next}, quoted)
end
end
@doc """
Returns an environment in the guard context.
"""
@doc since: "1.17.0"
@spec to_guard(t) :: t
def to_guard(%{__struct__: Macro.Env} = env) do
%{env | context: :guard}
end
@doc """
Returns an environment in the match context.
Returns a `Macro.Env` in the match context.
"""
@spec to_match(t) :: t
def to_match(%{__struct__: Macro.Env} = env) do
+19 -16
View File
@@ -14,7 +14,7 @@ defmodule Map do
in the example above has a different order than the map that was created).
Maps do not impose any restriction on the key type: anything can be a key in a
map. As a key-value structure, maps do not allow duplicate keys. Keys are
map. As a key-value structure, maps do not allow duplicated keys. Keys are
compared using the exact-equality operator (`===/2`). If colliding keys are defined
in a map literal, the last one prevails.
@@ -96,11 +96,11 @@ defmodule Map do
iex> %{map | one: "one"}
%{one: "one", two: 2}
Or any other keys:
Or any other key:
iex> other_map = %{"three" => 3, "four" => 4, "five" => 5}
iex> %{other_map | "three" => "three", "four" => "four"}
%{"five" => 5, "four" => "four", "three" => "three"}
iex> other_map = %{"three" => 3, "four" => 4}
iex> %{other_map | "three" => "three"}
%{"four" => 4, "three" => "three"}
When a key that does not exist in the map is updated a `KeyError` exception will be raised:
@@ -128,8 +128,6 @@ defmodule Map do
@doc """
Builds a map from the given `keys` and the fixed `value`.
Inlined by the compiler.
## Examples
iex> Map.from_keys([1, 2, 3], :number)
@@ -138,7 +136,9 @@ defmodule Map do
"""
@doc since: "1.14.0"
@spec from_keys([key], value) :: map
defdelegate from_keys(keys, value), to: :maps
def from_keys(keys, value) do
:maps.from_keys(keys, value)
end
@doc """
Returns all keys from `map`.
@@ -147,7 +147,7 @@ defmodule Map do
## Examples
Map.keys(%{a: 1, b: 2})
iex> Map.keys(%{a: 1, b: 2})
[:a, :b]
"""
@@ -161,7 +161,7 @@ defmodule Map do
## Examples
Map.values(%{a: 1, b: 2})
iex> Map.values(%{a: 1, b: 2})
[1, 2]
"""
@@ -709,10 +709,10 @@ defmodule Map do
end
@doc """
Removes and returns the value associated with `key` in `map` alongside
the updated map, or raises if `key` is not present.
Removes the value associated with `key` in `map` and returns the value
and the updated map, or it raises if `key` is not present.
Behaves the same as `pop/3` but raises a `KeyError` exception if `key` is not present in `map`.
Behaves the same as `pop/3` but raises if `key` is not present in `map`.
## Examples
@@ -1011,8 +1011,9 @@ defmodule Map do
@doc """
Converts a `struct` to map.
It accepts a struct and simply removes the `__struct__` field
from the given struct.
It accepts the struct module or a struct itself and
simply removes the `__struct__` field from the given struct
or from a new struct generated from the given module.
## Example
@@ -1020,13 +1021,15 @@ defmodule Map do
defstruct [:name]
end
Map.from_struct(User)
#=> %{name: nil}
Map.from_struct(%User{name: "john"})
#=> %{name: "john"}
"""
@spec from_struct(atom | struct) :: map
def from_struct(struct) when is_atom(struct) do
IO.warn("Map.from_struct/1 with a module is deprecated, please pass a struct instead")
delete(struct.__struct__(), :__struct__)
end
+1 -1
View File
@@ -401,7 +401,7 @@ defmodule MapSet do
"""
@doc since: "1.15.0"
@spec split_with(MapSet.t(), (term() -> as_boolean(term))) :: {MapSet.t(), MapSet.t()}
@spec split_with(MapSet.t(), (any() -> as_boolean(term))) :: {MapSet.t(), MapSet.t()}
def split_with(%MapSet{map: map}, fun) when is_function(fun, 1) do
{while_true, while_false} = Map.split_with(map, fn {key, _} -> fun.(key) end)
{%MapSet{map: while_true}, %MapSet{map: while_false}}
+82 -174
View File
@@ -270,9 +270,6 @@ defmodule Module do
a dependency on it, causing the module to be recompiled as soon
as the file is added.
For more control over when a module is recompiled, see
[`__mix_recompile__?/0`](`m:Mix.Tasks.Compile.Elixir#module-__mix_recompile__-0`).
### `@file`
Changes the filename used in stacktraces for the function or macro that
@@ -286,24 +283,6 @@ defmodule Module do
end
end
Note that this is only valid for exceptions/diagnostics that come from the
definition inner scope (which includes its patterns and guards). For example:
defmodule MyModule do # <---- module definition
@file "hello.ex"
defp unused(a) do # <---- function definition
"world" # <---- function scope
end
@file "bye.ex"
def unused(_), do: true
end
If you run this code with the second "unused" definition commented, you will
see that `hello.ex` is used as the stacktrace when reporting warnings, but if
you uncomment it you'll see that the error will not mention `bye.ex`, because
it's a module-level error rather than an expression-level error.
### `@moduledoc`
Provides documentation for the current module.
@@ -326,21 +305,6 @@ defmodule Module do
Once this module is compiled, this information becomes available via
the `Code.fetch_docs/1` function.
### `@nifs` (since v1.16.0)
A list of functions and their arities which will be overridden
by a native implementation (NIF).
defmodule MyLibrary.MyModule do
@nifs [foo: 1, bar: 2]
def foo(arg1), do: :erlang.nif_error(:not_loaded)
def bar(arg1, arg2), do: :erlang.nif_error(:not_loaded)
end
See the Erlang documentation for more information:
https://www.erlang.org/doc/man/erl_nif
### `@on_definition`
A hook that will be invoked when each function or macro in the current
@@ -444,8 +408,7 @@ defmodule Module do
* `@typep` - defines a private type to be used in `@spec`
* `@opaque` - defines an opaque type to be used in `@spec`
* `@spec` - provides a specification for a function
* `@callback` - provides a specification for a behaviour callback (and generates
a `behaviour_info/1` function in the module, see below)
* `@callback` - provides a specification for a behaviour callback
* `@macrocallback` - provides a specification for a macro behaviour callback
* `@optional_callbacks` - specifies which behaviour callbacks and macro
behaviour callbacks are optional
@@ -542,9 +505,7 @@ defmodule Module do
undefined functions, deprecations, etc. A module is always verified after
it is compiled. In Mix projects, a module is also verified when any of its
runtime dependencies change. Therefore this is useful to perform verification
of the current module while avoiding compile-time dependencies. Given the
callback is invoked under different scenarios, Elixir provides no guarantees
of when in the compilation cycle nor in which process the callback runs.
of the current module while avoiding compile-time dependencies.
Accepts a module or a `{module, function_name}` tuple. The function
must take one argument: the module name. When just a module is provided,
@@ -594,78 +555,6 @@ defmodule Module do
`@compile {:no_warn_undefined, {Mod, fun, arity}}` - does not warn if
the given module or the given `Mod.fun/arity` are not defined
## Generated functions
Sometimes the compiler will generate public functions within modules. These
are documented below.
### `behaviour_info/1`
This function is generated for modules that define a behaviour, that is,
that have one or more `@callback` definitions. The signature for this function,
expressed as a spec, is:
@spec behaviour_info(:callbacks) :: [function_info]
when function_info: {function_name :: atom(), arity :: non_neg_integer()}
@spec behaviour_info(:optional_callbacks) :: [function_info]
when function_info: {function_name :: atom(), arity :: non_neg_integer()}
`behaviour_info(:callbacks)` includes optional callbacks.
For example:
iex> Enum.sort(GenServer.behaviour_info(:callbacks))
[
code_change: 3,
format_status: 1,
format_status: 2,
handle_call: 3,
handle_cast: 2,
handle_continue: 2,
handle_info: 2,
init: 1,
terminate: 2
]
### `module_info/0`
This function is generated for all modules. It returns all the attributes
returned by `module_info/1` (see below), but as a single keyword list. See also the
[Erlang documentation](https://www.erlang.org/doc/system/modules.html#module_info-0).
### `module_info/1`
This function is generated for all modules and returns
information about the module. The signature for this function,
expressed as a spec, is:
@spec module_info(:module) :: module() # Returns the module itself
@spec module_info(:attributes) :: keyword()
@spec module_info(:compile) :: keyword()
@spec module_info(:md5) :: binary()
@spec module_info(:nifs) :: module()
@spec module_info(:exports) :: [function_info]
when function_info: {function_name :: atom(), arity :: non_neg_integer()}
@spec module_info(:functions) :: [function_info]
when function_info: {function_name :: atom(), arity :: non_neg_integer()}
For example:
iex> URI.module_info(:module)
URI
iex> {:decode_www_form, 1} in URI.module_info(:exports)
true
For more information about `module_info/1`, also check out the [Erlang
documentation](https://www.erlang.org/doc/system/modules.html#module_info-1).
### `__info__/1`
This function is generated for all modules. It's similar to `module_info/1` but
includes some additional Elixir-specific information, such as struct and macro
information. For documentation, see `c:Module.__info__/1`.
'''
@type definition :: {atom, arity}
@@ -704,8 +593,7 @@ defmodule Module do
@callback __info__(:macros) :: keyword()
@callback __info__(:md5) :: binary()
@callback __info__(:module) :: module()
@callback __info__(:struct) ::
list(%{required(:field) => atom(), optional(:default) => term()}) | nil
@callback __info__(:struct) :: list(%{field: atom(), required: boolean()}) | nil
@doc """
Returns information about module attributes used by Elixir.
@@ -723,7 +611,6 @@ defmodule Module do
"""
@doc since: "1.12.0"
@spec reserved_attributes() :: map
def reserved_attributes() do
%{
after_compile: %{
@@ -746,10 +633,6 @@ defmodule Module do
doc:
"If set to `true` generates a default protocol implementation for all types (inside `defprotocol`)."
},
undefined_impl_description: %{
doc:
"A string with additional description to be used on `Protocol.UndefinedError` when looking up the implementation fails."
},
for: %{
doc:
"The current module/type a protocol implementation is being defined for (inside `defimpl`)."
@@ -834,7 +717,50 @@ defmodule Module do
:elixir_module.is_open(module)
end
@deprecated "Use Code.eval_quoted/3 instead"
@doc """
Evaluates the quoted contents in the given module's context.
A list of environment options can also be given as argument.
See `Code.eval_string/3` for more information.
Raises an error if the module was already compiled.
## Examples
defmodule Foo do
contents =
quote do
def sum(a, b), do: a + b
end
Module.eval_quoted(__MODULE__, contents)
end
Foo.sum(1, 2)
#=> 3
For convenience, you can pass any `Macro.Env` struct, such
as `__ENV__/0`, as the first argument or as options. Both
the module and all options will be automatically extracted
from the environment:
defmodule Foo do
contents =
quote do
def sum(a, b), do: a + b
end
Module.eval_quoted(__ENV__, contents)
end
Foo.sum(1, 2)
#=> 3
Note that if you pass a `Macro.Env` struct as first argument
while also passing `opts`, they will be merged with `opts`
having precedence.
"""
@spec eval_quoted(module | Macro.Env.t(), Macro.t(), list, keyword | Macro.Env.t()) :: term
def eval_quoted(module_or_env, quoted, binding \\ [], opts \\ [])
def eval_quoted(%Macro.Env{} = env, quoted, binding, opts)
@@ -865,8 +791,7 @@ defmodule Module do
the given quoted expressions.
The line where the module is defined and its file **must**
be passed as options. See `Code.env_for_eval/1` for a complete
list of options.
be passed as options.
It returns a tuple of shape `{:module, module, binary, term}`
where `module` is the module name, `binary` is the module
@@ -910,7 +835,7 @@ defmodule Module do
end
def create(module, quoted, opts) when is_atom(module) and is_list(opts) do
if not Keyword.has_key?(opts, :file) do
unless Keyword.has_key?(opts, :file) do
raise ArgumentError, "expected :file to be given as option"
end
@@ -921,7 +846,7 @@ defmodule Module do
defp create(meta, module, quoted, env_or_opts) do
next = :elixir_module.next_counter(nil)
quoted = :elixir_quote.linify_with_context_counter(meta, {module, next}, quoted)
:elixir_module.compile(meta, module, quoted, [], false, :elixir.env_for_eval(env_or_opts))
:elixir_module.compile(module, quoted, [], false, :elixir.env_for_eval(env_or_opts))
end
@doc """
@@ -946,8 +871,7 @@ defmodule Module do
@doc """
Concatenates two aliases and returns a new alias.
It handles binaries and atoms. If one of the aliases
is nil, it is discarded.
It handles binaries and atoms.
## Examples
@@ -957,9 +881,6 @@ defmodule Module do
iex> Module.concat(Foo, "Bar")
Foo.Bar
iex> Module.concat(Foo, nil)
Foo
"""
@spec concat(binary | atom, binary | atom) :: atom
def concat(left, right)
@@ -1181,7 +1102,7 @@ defmodule Module do
Use `Kernel.function_exported?/3` and `Kernel.macro_exported?/3` to check for
public functions and macros respectively in compiled modules.
Note that `defines?` returns `false` for functions and macros that have
Note that `defines?` returns false for functions and macros that have
been defined but then marked as overridable and no other implementation
has been provided. You can check the overridable status by calling
`overridable?/2`.
@@ -1416,15 +1337,23 @@ defmodule Module do
@doc """
Deletes a definition from a module.
It returns `true` if the definition exists and it was removed,
otherwise it returns `false`.
It returns true if the definition exists and it was removed,
otherwise it returns false.
"""
@doc since: "1.12.0"
@spec delete_definition(module, definition) :: boolean()
def delete_definition(module, {name, arity})
when is_atom(module) and is_atom(name) and is_integer(arity) do
assert_not_readonly!(__ENV__.function, module)
:elixir_def.take_definition(module, {name, arity}) != false
case :elixir_def.take_definition(module, {name, arity}) do
false ->
false
_ ->
:elixir_locals.yank({name, arity}, module)
true
end
end
@doc """
@@ -1453,7 +1382,8 @@ defmodule Module do
"overridable because it was not defined"
clause ->
:elixir_overridable.record_overridable(module, tuple, clause)
neighbours = :elixir_locals.yank(tuple, module)
:elixir_overridable.record_overridable(module, tuple, clause, neighbours)
end
other ->
@@ -1477,7 +1407,7 @@ defmodule Module do
"to defoverridable/1 because #{error_explanation}"
end
behaviour_callbacks = Module.Behaviour.callbacks(behaviour)
behaviour_callbacks = Module.Types.Behaviour.callbacks(behaviour)
tuples =
for definition <- definitions_in(module),
@@ -1514,7 +1444,7 @@ defmodule Module do
Returns `true` if `tuple` in `module` was marked as overridable
at some point.
Note `overridable?/2` returns `true` even if the definition was
Note `overridable?/2` returns true even if the definition was
already overridden. You can use `defines?/2` to see if a definition
exists or one is pending.
"""
@@ -1677,11 +1607,11 @@ defmodule Module do
case :ets.lookup(set, key) do
[{_, _, :accumulate, traces}] ->
trace_attribute(true, module, traces, set, key, nil, [])
trace_attribute(true, module, traces, set, key, [])
reverse_values(:ets.take(bag, {:accumulate, key}), [])
[{_, value, _, traces}] ->
trace_attribute(module, key, nil, traces)
trace_attribute(module, traces)
:ets.delete(set, key)
value
@@ -1944,7 +1874,7 @@ defmodule Module do
_ ->
message =
case :ets.lookup(set, {:elixir, :struct}) do
case :ets.lookup(set, :__struct__) do
[] ->
"warning: module attribute @derive was set but never used (it must come before defstruct)"
@@ -1994,19 +1924,16 @@ defmodule Module do
{set, bag} = data_tables_for(module)
case :ets.lookup(set, key) do
[{_, _, :unset, _}] ->
default
[{_, _, :accumulate, traces}] ->
trace_attribute(trace?, module, traces, set, key, caller_line, [])
trace_attribute(trace?, module, traces, set, key, [])
lookup_accumulate_attribute(bag, key, default, last_accumulated?)
[{_, value, warn_line, traces}] when is_integer(warn_line) ->
trace_attribute(trace?, module, traces, set, key, caller_line, [{3, :used}])
trace_attribute(trace?, module, traces, set, key, [{3, :used}])
value
[{_, value, _, traces}] ->
trace_attribute(trace?, module, traces, set, key, caller_line, [])
trace_attribute(trace?, module, traces, set, key, [])
value
[] when is_integer(caller_line) ->
@@ -2034,15 +1961,12 @@ defmodule Module do
end
end
defp trace_attribute(module, key, caller_line, traces) do
defp trace_attribute(module, traces) do
:lists.foreach(
fn {line, lexical_tracker, tracers, aliases} ->
line = caller_line || line
env = %{
Macro.Env.__struct__()
| line: line,
file: "@#{key}",
lexical_tracker: lexical_tracker,
module: module,
tracers: tracers
@@ -2059,10 +1983,10 @@ defmodule Module do
)
end
defp trace_attribute(trace?, module, traces, set, key, caller_line, updates) do
defp trace_attribute(trace?, module, traces, set, key, updates) do
updates =
if trace? and traces != [] do
trace_attribute(module, key, caller_line, traces)
trace_attribute(module, traces)
updates ++ [{4, []}]
else
updates
@@ -2175,7 +2099,8 @@ defmodule Module do
end
defp attribute_stack(module, line) do
struct!(Macro.Env, module: module, file: :elixir_module.file(module), line: line)
file = String.to_charlist(Path.relative_to_cwd(:elixir_module.file(module)))
[{module, :__MODULE__, 0, file: file, line: line}]
end
## Helpers
@@ -2265,16 +2190,6 @@ defmodule Module do
end
end
defp preprocess_attribute(:nifs, value) do
if not function_arity_list?(value) do
raise ArgumentError,
"@nifs is a built-in module attribute for specifying a list " <>
"of functions and their arities that are NIFs, got: #{inspect(value)}"
end
value
end
defp preprocess_attribute(:dialyzer, value) do
# From https://github.com/erlang/otp/blob/master/lib/stdlib/src/erl_lint.erl
:lists.foreach(
@@ -2293,22 +2208,17 @@ defmodule Module do
value
end
defp function_arity_list?(fun_arities) do
is_list(fun_arities) and
defp valid_dialyzer_attribute?({key, fun_arities}) when is_atom(key) do
(key == :nowarn_function or valid_dialyzer_attribute?(key)) and
:lists.all(
fn
{fun, arity} when is_atom(fun) and is_integer(arity) -> true
_ -> false
end,
fun_arities
List.wrap(fun_arities)
)
end
defp valid_dialyzer_attribute?({key, fun_arities}) when is_atom(key) do
(key == :nowarn_function or valid_dialyzer_attribute?(key)) and
function_arity_list?(List.wrap(fun_arities))
end
defp valid_dialyzer_attribute?(attr) do
:lists.member(
attr,
@@ -2316,9 +2226,7 @@ defmodule Module do
[:no_match, :no_opaque, :no_fail_call, :no_contracts] ++
[:no_behaviours, :no_undefined_callbacks, :unmatched_returns] ++
[:error_handling, :race_conditions, :no_missing_calls] ++
[:specdiffs, :overspecs, :underspecs, :unknown, :no_underspecs] ++
[:extra_return, :no_extra_return, :no_missing_return] ++
[:missing_return, :no_unknown]
[:specdiffs, :overspecs, :underspecs, :unknown, :no_underspecs]
)
end
+253
View File
@@ -0,0 +1,253 @@
# This is an Elixir module responsible for tracking
# calls in order to extract Elixir modules' behaviour
# during compilation time.
#
# ## Implementation
#
# The implementation uses ETS to track all dependencies
# resembling a graph. The keys and what they point to are:
#
# * `:reattach` points to `{name, arity}`
# * `{:local, {name, arity}}` points to `{{name, arity}, line, macro_dispatch?}`
# * `{:import, {name, arity}}` points to `Module`
#
# This is built on top of the internal module tables.
defmodule Module.LocalsTracker do
@moduledoc false
@defmacros [:defmacro, :defmacrop]
@doc """
Adds and tracks defaults for a definition into the tracker.
"""
def add_defaults({_set, bag}, kind, {name, arity} = pair, defaults, meta) do
for i <- :lists.seq(arity - defaults, arity - 1) do
put_edge(bag, {:local, {name, i}}, {pair, get_line(meta), kind in @defmacros})
end
:ok
end
@doc """
Adds a local dispatch from-to the given target.
"""
def add_local({_set, bag}, from, to, meta, macro_dispatch?)
when is_tuple(from) and is_tuple(to) and is_boolean(macro_dispatch?) do
put_edge(bag, {:local, from}, {to, get_line(meta), macro_dispatch?})
:ok
end
@doc """
Adds an import dispatch to the given target.
"""
def add_import({set, _bag}, function, module, imported)
when is_tuple(function) and is_atom(module) do
put_edge(set, {:import, imported}, module)
:ok
end
@doc """
Yanks a local node. Returns its in and out vertices in a tuple.
"""
def yank({_set, bag}, local) do
:lists.usort(take_out_neighbours(bag, {:local, local}))
end
@doc """
Reattach a previously yanked node.
"""
def reattach({_set, bag}, tuple, kind, function, out_neighbours, meta) do
for out_neighbour <- out_neighbours do
put_edge(bag, {:local, function}, out_neighbour)
end
# Make a call from the old function to the new one
if function != tuple do
put_edge(bag, {:local, function}, {tuple, get_line(meta), kind in @defmacros})
end
# Finally marked the new one as reattached
put_edge(bag, :reattach, tuple)
:ok
end
@doc """
Collect all conflicting imports with the given functions
"""
def collect_imports_conflicts({set, _bag}, all_defined) do
for {pair, _, meta, _} <- all_defined, n = out_neighbour(set, {:import, pair}) do
{meta, {n, pair}}
end
end
@doc """
Collect all unused definitions based on the private
given, also accounting the expected number of default
clauses a private function have.
"""
def collect_unused_locals({_set, bag}, all_defined, private) do
reachable =
Enum.reduce(all_defined, %{}, fn {pair, kind, _, _}, acc ->
if kind in [:def, :defmacro] do
reachable_from(bag, pair, acc)
else
acc
end
end)
reattached = :lists.usort(out_neighbours(bag, :reattach))
{unreachable(reachable, reattached, private), collect_warnings(reachable, private)}
end
@doc """
Collect undefined functions based on local calls and existing definitions.
"""
def collect_undefined_locals({set, bag}, all_defined) do
undefined =
for {pair, _, meta, _} <- all_defined,
{local, line, macro_dispatch?} <- out_neighbours(bag, {:local, pair}),
error = undefined_local_error(set, local, macro_dispatch?),
do: {pair, build_meta(line, meta), local, error}
:lists.usort(undefined)
end
defp undefined_local_error(set, local, true) do
case :ets.member(set, {:def, local}) do
true -> false
false -> :undefined_function
end
end
defp undefined_local_error(set, local, false) do
try do
if :ets.lookup_element(set, {:def, local}, 2) in @defmacros do
:incorrect_dispatch
else
false
end
catch
_, _ -> :undefined_function
end
end
defp unreachable(reachable, reattached, private) do
for {tuple, kind, _, _} <- private,
not reachable?(tuple, kind, reachable, reattached),
do: tuple
end
defp reachable?(tuple, :defmacrop, reachable, reattached) do
# All private macros are unreachable unless they have been
# reattached and they are reachable.
:lists.member(tuple, reattached) and Map.has_key?(reachable, tuple)
end
defp reachable?(tuple, :defp, reachable, _reattached) do
Map.has_key?(reachable, tuple)
end
defp collect_warnings(reachable, private) do
:lists.foldl(&collect_warnings(&1, &2, reachable), [], private)
end
defp collect_warnings({_, _, false, _}, acc, _reachable) do
acc
end
defp collect_warnings({tuple, kind, meta, 0}, acc, reachable) do
if Map.has_key?(reachable, tuple) do
acc
else
[{meta, {:unused_def, tuple, kind}} | acc]
end
end
defp collect_warnings({tuple, kind, meta, default}, acc, reachable) when default > 0 do
{name, arity} = tuple
min = arity - default
max = arity
case min_reachable_default(max, min, :none, name, reachable) do
:none -> [{meta, {:unused_def, tuple, kind}} | acc]
^min -> acc
^max -> [{meta, {:unused_args, tuple}} | acc]
diff -> [{meta, {:unused_args, tuple, diff}} | acc]
end
end
defp min_reachable_default(max, min, last, name, reachable) when max >= min do
case Map.has_key?(reachable, {name, max}) do
true -> min_reachable_default(max - 1, min, max, name, reachable)
false -> min_reachable_default(max - 1, min, last, name, reachable)
end
end
defp min_reachable_default(_max, _min, last, _name, _reachable) do
last
end
@doc """
Returns all local nodes reachable from `vertex`.
By default, all public functions are reachable.
A private function is only reachable if it has
a public function that it invokes directly.
"""
def reachable_from({_, bag}, local) do
bag
|> reachable_from(local, %{})
|> Map.keys()
end
defp reachable_from(bag, local, vertices) do
vertices = Map.put(vertices, local, true)
Enum.reduce(out_neighbours(bag, {:local, local}), vertices, fn {local, _line, _}, acc ->
case acc do
%{^local => true} -> acc
_ -> reachable_from(bag, local, acc)
end
end)
end
defp get_line(meta), do: Keyword.get(meta, :line)
defp build_meta(nil, _meta), do: []
# We need to transform any file annotation in the function
# definition into a keep annotation that is used by the
# error handling system in order to respect line/file.
defp build_meta(line, meta) do
case Keyword.get(meta, :file) do
{file, _} -> [keep: {file, line}]
_ -> [line: line]
end
end
## Lightweight digraph implementation
defp put_edge(d, from, to) do
:ets.insert(d, {from, to})
end
defp out_neighbour(d, from) do
try do
:ets.lookup_element(d, from, 2)
catch
:error, :badarg -> nil
end
end
defp out_neighbours(d, from) do
try do
:ets.lookup_element(d, from, 2)
catch
:error, :badarg -> []
end
end
defp take_out_neighbours(d, from) do
Keyword.values(:ets.take(d, from))
end
end
+218 -215
View File
@@ -5,19 +5,20 @@ defmodule Module.ParallelChecker do
@type cache() :: {pid(), :ets.tid()}
@type warning() :: term()
@type kind() :: :def | :defmacro
@type mode() :: :elixir | :erlang
@doc """
Initializes the parallel checker process.
"""
def start_link(schedulers \\ nil) do
:proc_lib.start_link(__MODULE__, :init, [schedulers])
:gen_server.start_link(__MODULE__, schedulers, [])
end
@doc """
Stops the parallel checker process.
"""
def stop({checker, _table}) do
def stop(checker) do
send(checker, {__MODULE__, :stop})
:ok
end
@@ -28,30 +29,26 @@ defmodule Module.ParallelChecker do
def get do
case :erlang.get(:elixir_checker_info) do
{parent, nil} ->
{:ok, cache} = start_link()
put(parent, cache)
{parent, cache}
{:ok, checker} = start_link()
put(parent, checker)
{parent, checker}
{parent, cache} ->
{parent, cache}
{parent, checker} ->
{parent, checker}
end
end
@doc """
Stores the parallel checker information.
"""
def put(pid, {checker, table}) when is_pid(pid) do
:erlang.put(:elixir_checker_info, {pid, {checker, table}})
def put(pid, checker) when is_pid(pid) and is_pid(checker) do
:erlang.put(:elixir_checker_info, {pid, checker})
end
@doc """
Spawns a process that runs the parallel checker.
"""
def spawn({pid, {checker, table}}, module, module_map, log?) do
inner_spawn(pid, checker, table, module, cache_from_module_map(table, module_map), log?)
end
defp inner_spawn(pid, checker, table, module, info, log?) do
def spawn({pid, checker}, module, info, log?) do
ref = make_ref()
spawned =
@@ -59,25 +56,20 @@ defmodule Module.ParallelChecker do
mon_ref = Process.monitor(pid)
receive do
{^ref, :cache} ->
{^ref, :cache, ets} ->
Process.link(pid)
module_tuple =
cond do
is_tuple(info) ->
info
is_binary(info) ->
with {:ok, binary} <- File.read(info),
{: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
cache_from_module_map(table, module_map)
else
_ -> nil
end
module_map =
if is_map(info) do
info
else
case File.read(info) do
{:ok, binary} -> maybe_module_map(binary, module)
{:error, _} -> nil
end
end
module_map && cache_from_module_map(ets, module_map)
send(checker, {ref, :cached})
receive do
@@ -86,8 +78,8 @@ defmodule Module.ParallelChecker do
:erlang.put(:elixir_compiler_info, {pid, self()})
warnings =
if module_tuple do
check_module(module_tuple, {checker, table}, log?)
if module_map do
check_module(module_map, {checker, ets}, log?)
else
[]
end
@@ -121,12 +113,12 @@ defmodule Module.ParallelChecker do
case :erlang.get(:elixir_checker_info) do
{_, nil} -> :ok
{_, cache} -> verify(cache, [])
{_, checker} -> verify(checker, [])
end
result
after
{_, cache} = :erlang.get(:elixir_checker_info)
{_, checker} = :erlang.get(:elixir_checker_info)
if previous != :undefined do
:erlang.put(:elixir_checker_info, previous)
@@ -134,7 +126,7 @@ defmodule Module.ParallelChecker do
:erlang.erase(:elixir_checker_info)
end
cache && stop(cache)
checker && stop(checker)
end
_ ->
@@ -149,13 +141,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(cache(), [{module(), Path.t()}]) :: [warning()]
def verify({checker, table}, runtime_files) do
@spec verify(pid(), [{module(), Path.t()}]) :: [warning()]
def verify(checker, runtime_files) do
value = :erlang.get(:elixir_code_diagnostics)
log? = not match?({_, false}, value)
for {module, file} <- runtime_files do
inner_spawn(self(), checker, table, module, file, log?)
spawn({self(), checker}, module, file, log?)
end
count = :gen_server.call(checker, :start, :infinity)
@@ -163,7 +155,8 @@ defmodule Module.ParallelChecker do
case :erlang.get(:elixir_code_diagnostics) do
:undefined -> :ok
{tail, log?} -> :erlang.put(:elixir_code_diagnostics, {diagnostics ++ tail, log?})
{tail, true} -> :erlang.put(:elixir_code_diagnostics, {diagnostics ++ tail, true})
{tail, false} -> :erlang.put(:elixir_code_diagnostics, {diagnostics ++ tail, false})
end
diagnostics
@@ -175,9 +168,8 @@ defmodule Module.ParallelChecker do
defp collect_results(count, diagnostics) do
receive do
{:diagnostic, %{file: file} = diagnostic, read_snippet} ->
:elixir_errors.print_diagnostic(diagnostic, read_snippet)
diagnostic = %{diagnostic | file: file && Path.absname(file)}
{:diagnostic, diagnostic} ->
diagnostic = format_diagnostic_file(diagnostic)
collect_results(count, [diagnostic | diagnostics])
{__MODULE__, _module, new_diagnostics} ->
@@ -189,8 +181,20 @@ defmodule Module.ParallelChecker do
Test cache.
"""
def test_cache do
{:ok, cache} = start_link()
cache
{:ok, checker} = start_link()
{checker, :gen_server.call(checker, :ets, :infinity)}
end
@doc """
Preloads a module into the cache. Call this function before any other
cache lookups for the module.
"""
@spec preload_module(cache(), module()) :: :ok
def preload_module({server, ets}, module) do
case :ets.lookup(ets, {:cached, module}) do
[{_key, _}] -> :ok
[] -> cache_module({server, ets}, module)
end
end
@doc """
@@ -199,81 +203,74 @@ defmodule Module.ParallelChecker do
or if the function does not exist return `{:error, :function}`.
"""
@spec fetch_export(cache(), module(), atom(), arity()) ::
{:ok, mode(), binary() | nil, {:infer, [term()]} | :none}
| {:error, :function | :module}
def fetch_export({checker, table}, module, fun, arity) do
case :ets.lookup(table, module) do
[] ->
cache_module({checker, table}, module)
fetch_export({checker, table}, module, fun, arity)
{:ok, mode(), kind(), binary() | nil} | {:error, :function | :module}
def fetch_export({_server, ets}, module, fun, arity) do
case :ets.lookup(ets, {:cached, module}) do
[{_key, false}] ->
{:error, :module}
[{_key, mode}] ->
case :ets.lookup(table, {module, {fun, arity}}) do
[{_key, reason, signature}] -> {:ok, mode, reason, signature}
case :ets.lookup(ets, {:export, module, {fun, arity}}) do
[{_key, kind, reason}] -> {:ok, mode, kind, reason}
[] -> {:error, :function}
end
end
end
## Module checking
defp check_module(module_tuple, cache, log?) do
{module, file, line, definitions, no_warn_undefined, behaviours, impls, after_verify} =
module_tuple
behaviour_warnings =
Module.Behaviour.check_behaviours_and_impls(
module,
file,
line,
behaviours,
impls,
definitions
)
diagnostics =
module
|> Module.Types.warnings(file, definitions, no_warn_undefined, cache)
|> Kernel.++(behaviour_warnings)
|> group_warnings()
|> emit_warnings(file, log?)
Enum.each(after_verify, fn {verify_mod, verify_fun} ->
apply(verify_mod, verify_fun, [module])
end)
diagnostics
@doc """
Returns all exported functions and macros for the given module from
the cache.
"""
@spec all_exports(cache(), module()) :: [{atom(), arity()}]
def all_exports({_server, ets}, module) do
# This is only called after we get a deprecation notice
# so we can assume it's a cached module
ets
|> :ets.match({{:export, module, :"$1"}, :_, :_})
|> Enum.flat_map(& &1)
|> Enum.sort()
end
defp module_map_to_module_tuple(module_map) do
## Module checking
defp check_module(module_map, cache, log?) do
%{
module: module,
file: file,
line: line,
compile_opts: compile_opts,
definitions: definitions,
attributes: attributes,
impls: impls,
after_verify: after_verify
uses_behaviours: uses_behaviours,
impls: impls
} = module_map
# TODO: Match on anno directly in Elixir v1.22+
line =
case module_map do
%{anno: anno} -> :erl_anno.line(anno)
%{line: line} -> line
end
behaviours = for {:behaviour, module} <- attributes, do: module
no_warn_undefined =
compile_opts
|> extract_no_warn_undefined()
|> merge_compiler_no_warn_undefined()
{module, file, line, definitions, no_warn_undefined, behaviours, impls, after_verify}
behaviour_warnings =
Module.Types.Behaviour.check_behaviours_and_impls(
module,
file,
line,
uses_behaviours,
impls,
definitions
)
warnings =
module
|> Module.Types.warnings(file, definitions, no_warn_undefined, cache)
|> Kernel.++(behaviour_warnings)
|> group_warnings()
|> emit_warnings(log?)
module_map
|> Map.get(:after_verify, [])
|> Enum.each(fn {verify_mod, verify_fun} -> apply(verify_mod, verify_fun, [module]) end)
warnings
end
defp extract_no_warn_undefined(compile_opts) do
@@ -291,125 +288,112 @@ defmodule Module.ParallelChecker do
end
end
@doc false
def format_diagnostic_file(%{file: file} = diagnostic) do
%{diagnostic | file: file && Path.absname(file)}
end
## Warning helpers
defp group_warnings(warnings) do
{ungrouped, grouped} =
Enum.reduce(warnings, {[], %{}}, fn {module, warning, location}, {ungrouped, grouped} ->
%{message: _} = diagnostic = module.format_diagnostic(warning)
if Map.get(diagnostic, :group, false) do
locations = MapSet.new([location])
grouped =
Map.update(grouped, warning, {locations, diagnostic}, fn
{locations, diagnostic} -> {MapSet.put(locations, location), diagnostic}
end)
{ungrouped, grouped}
else
{[{[location], diagnostic} | ungrouped], grouped}
end
end)
grouped =
Enum.map(grouped, fn {_warning, {locations, diagnostic}} ->
{Enum.sort(locations), diagnostic}
end)
Enum.sort(ungrouped ++ grouped)
warnings
|> Enum.reduce(%{}, fn {module, warning, location}, acc ->
locations = MapSet.new([location])
Map.update(acc, {module, warning}, locations, &MapSet.put(&1, location))
end)
|> Enum.map(fn {{module, warning}, locations} -> {module, warning, Enum.sort(locations)} end)
|> Enum.sort()
end
defp emit_warnings(warnings, file, log?) do
Enum.flat_map(warnings, fn {locations, diagnostic} ->
diagnostics = Enum.map(locations, &to_diagnostic(diagnostic, file, &1))
log? and print_diagnostics(diagnostics)
defp emit_warnings(warnings, log?) do
Enum.flat_map(warnings, fn {module, warning, locations} ->
message = module.format_warning(warning)
diagnostics = Enum.map(locations, &to_diagnostic(message, &1))
log? and :elixir_errors.print_warning([message, ?\n, format_stacktraces(diagnostics)])
diagnostics
end)
end
defp print_diagnostics([diagnostic]) do
:elixir_errors.print_diagnostic(diagnostic, true)
defp format_stacktraces([diagnostic]) do
format_diagnostic_stacktrace(diagnostic)
end
defp print_diagnostics(diagnostics) do
:elixir_errors.print_diagnostics(diagnostics)
defp format_stacktraces(diagnostics) do
[
"Invalid call found at #{length(diagnostics)} locations:\n",
Enum.map(diagnostics, &format_diagnostic_stacktrace/1)
]
end
defp to_diagnostic(diagnostic, source, {file, position, mfa}) when is_list(position) do
file = Path.absname(file)
defp format_diagnostic_stacktrace(%{stacktrace: [stacktrace]}) do
[" ", Exception.format_stacktrace_entry(stacktrace), ?\n]
end
defp to_diagnostic(message, {file, line, mfa}) do
%{
severity: :warning,
source: source,
file: file,
position: position_to_tuple(position),
stacktrace: [to_stacktrace(file, position, mfa)],
span: nil
position: line,
message: IO.iodata_to_binary(message),
stacktrace: [to_stacktrace(file, line, mfa)]
}
|> Map.merge(diagnostic)
end
defp position_to_tuple(position) do
case position[:column] do
nil -> position[:line] || 0
col -> {position[:line], col}
end
end
defp to_stacktrace(file, line, {module, fun, arity}),
do: {module, fun, arity, location(file, line)}
defp to_stacktrace(file, pos, {module, fun, arity}),
do: {module, fun, arity, location(file, pos)}
defp to_stacktrace(file, line, nil),
do: {:elixir_compiler, :__FILE__, 1, location(file, line)}
defp to_stacktrace(file, pos, nil),
do: {:elixir_compiler, :__FILE__, 1, location(file, pos)}
defp to_stacktrace(file, line, module),
do: {module, :__MODULE__, 0, location(file, line)}
defp to_stacktrace(file, pos, module),
do: {module, :__MODULE__, 0, location(file, pos)}
defp location(file, position) do
[{:file, String.to_charlist(Path.relative_to_cwd(file))} | position]
defp location(file, line) do
[file: String.to_charlist(Path.relative_to_cwd(file)), line: line]
end
## Cache
defp cache_module({checker, table}, module) do
if lock(checker, module) do
object_code = :code.get_object_code(module)
defp cache_module({server, ets}, module) do
if lock(server, module) do
cache_from_chunk(ets, module) || cache_from_info(ets, module)
unlock(server, module)
end
end
# The chunk has more information, so that's our preference
with {^module, binary, _filename} <- object_code,
{:ok, {^module, [{~c"ExCk", chunk}]}} <- :beam_lib.chunks(binary, [~c"ExCk"]),
{:elixir_checker_v1, contents} <- :erlang.binary_to_term(chunk) do
cache_chunk(table, module, contents.exports)
else
_ ->
# Otherwise, if the module is loaded, use its info
case :erlang.module_loaded(module) do
true ->
{mode, exports} = info_exports(module)
deprecated = info_deprecated(module)
cache_info(table, module, exports, deprecated, %{}, mode)
defp cache_from_chunk(ets, module) do
with {^module, binary, _filename} <- :code.get_object_code(module),
{: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
else
_ -> false
end
end
false ->
# Or load exports from chunk
with {^module, binary, _filename} <- object_code,
{:ok, {^module, [exports: exports]}} <- :beam_lib.chunks(binary, [:exports]) do
cache_info(table, module, exports, %{}, %{}, :erlang)
else
_ ->
:ets.insert(table, {module, false})
end
end
end
unlock(checker, module)
defp cache_from_info(ets, module) do
if Code.ensure_loaded?(module) do
{mode, exports} = info_exports(module)
deprecated = info_deprecated(module)
cache_info(ets, module, exports, deprecated, mode)
else
:ets.insert(ets, {{:cached, module}, false})
end
end
defp info_exports(module) do
{:elixir, behaviour_exports(module) ++ [{:__info__, 1} | module.__info__(:functions)]}
map =
Map.new(
[{{:__info__, 1}, :def}] ++
behaviour_exports(module) ++
Enum.map(module.__info__(:macros), &{&1, :defmacro}) ++
Enum.map(module.__info__(:functions), &{&1, :def})
)
{:elixir, map}
rescue
_ -> {:erlang, module.module_info(:exports)}
_ -> {:erlang, Map.new(Enum.map(module.module_info(:exports), &{&1, :def}))}
end
defp info_deprecated(module) do
@@ -418,55 +402,70 @@ defmodule Module.ParallelChecker do
_ -> %{}
end
defp cache_from_module_map(table, map) do
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
exports =
[{:__info__, 1}] ++
[{{:__info__, 1}, :def}] ++
behaviour_exports(map) ++
for({function, :def, _meta, _clauses} <- map.definitions, do: function)
definitions_to_exports(map.definitions)
cache_info(table, map.module, exports, Map.new(map.deprecated), map.signatures, :elixir)
module_map_to_module_tuple(map)
deprecated = Map.new(map.deprecated)
cache_info(ets, map.module, exports, deprecated, :elixir)
end
defp cache_info(table, module, exports, deprecated, sigs, mode) do
Enum.each(exports, fn fa ->
reason = Map.get(deprecated, fa)
:ets.insert(table, {{module, fa}, reason, Map.get(sigs, fa, :none)})
defp cache_info(ets, module, exports, deprecated, mode) do
Enum.each(exports, fn {{fun, arity}, kind} ->
reason = Map.get(deprecated, {fun, arity})
:ets.insert(ets, {{:export, module, {fun, arity}}, kind, reason})
{{fun, arity}, kind}
end)
:ets.insert(table, {module, mode})
:ets.insert(ets, {{:cached, module}, mode})
end
defp cache_chunk(table, module, exports) do
Enum.each(exports, fn {{fun, arity}, info} ->
sig =
case info do
%{sig: {:strong, _, _} = sig} -> sig
%{sig: {:infer, _} = sig} -> sig
_ -> :none
end
defp cache_chunk(ets, module, exports) do
Enum.each(exports, fn {{fun, arity}, %{kind: kind, deprecated_reason: reason}} ->
:ets.insert(ets, {{:export, module, {fun, arity}}, kind, reason})
:ets.insert(
table,
{{module, {fun, arity}}, Map.get(info, :deprecated), sig}
)
{{fun, arity}, kind}
end)
:ets.insert(table, {{module, {:__info__, 1}}, nil, :none})
:ets.insert(table, {module, :elixir})
:ets.insert(ets, {{:export, module, {:__info__, 1}}, :def, nil})
:ets.insert(ets, {{:cached, module}, :elixir})
end
defp behaviour_exports(%{defines_behaviour: true}), do: [{:behaviour_info, 1}]
defp behaviour_exports(%{defines_behaviour: true}), do: [{{:behaviour_info, 1}, :def}]
defp behaviour_exports(%{defines_behaviour: false}), do: []
defp behaviour_exports(module) when is_atom(module) do
if function_exported?(module, :behaviour_info, 1) do
[{:behaviour_info, 1}]
[{{:behaviour_info, 1}, :def}]
else
[]
end
end
defp definitions_to_exports(definitions) do
Enum.flat_map(definitions, fn {function, kind, _meta, _clauses} ->
if kind in [:def, :defmacro] do
[{function, kind}]
else
[]
end
end)
end
defp lock(server, module) do
:gen_server.call(server, {:lock, module}, :infinity)
end
@@ -482,22 +481,22 @@ defmodule Module.ParallelChecker do
## Server callbacks
def init(schedulers) do
table = :ets.new(__MODULE__, [:set, :public, {:read_concurrency, true}])
:proc_lib.init_ack({:ok, {self(), table}})
ets = :ets.new(__MODULE__, [:set, :public, {:read_concurrency, true}])
state = %{
ets: ets,
waiting: %{},
modules: [],
spawned: 0,
schedulers: schedulers || max(:erlang.system_info(:schedulers_online), 2)
}
:gen_server.enter_loop(__MODULE__, [], state)
{:ok, state}
end
def handle_call(:start, _from, %{modules: modules} = state) do
def handle_call(:start, _from, %{ets: ets, modules: modules} = state) do
for {pid, ref} <- modules do
send(pid, {ref, :cache})
send(pid, {ref, :cache, ets})
end
for {_pid, ref} <- modules do
@@ -509,10 +508,14 @@ defmodule Module.ParallelChecker do
{:reply, length(modules), run_checkers(state)}
end
def handle_call(:ets, _from, state) do
{:reply, state.ets, state}
end
def handle_call({:lock, module}, from, %{waiting: waiting} = state) do
case waiting do
%{^module => from_list} ->
waiting = Map.put(state.waiting, module, [from | from_list])
%{^module => froms} ->
waiting = Map.put(state.waiting, module, [from | froms])
{:noreply, %{state | waiting: waiting}}
%{} ->
@@ -522,8 +525,8 @@ defmodule Module.ParallelChecker do
end
def handle_call({:unlock, module}, _from, %{waiting: waiting} = state) do
from_list = Map.fetch!(waiting, module)
Enum.each(from_list, &:gen_server.reply(&1, false))
froms = Map.fetch!(waiting, module)
Enum.each(froms, &:gen_server.reply(&1, false))
waiting = Map.delete(waiting, module)
{:reply, :ok, %{state | waiting: waiting}}
end
+459 -331
View File
@@ -1,304 +1,55 @@
defmodule Module.Types do
@moduledoc false
alias Module.Types.{Descr, Expr, Pattern, Helpers}
# The mode controls what happens on function application when
# there are gradual arguments. Non-gradual arguments always
# perform subtyping and return its output (OUT).
#
# * :strict - Requires types signatures (not implemented).
# * Strong arrows with gradual performs subtyping and returns OUT
# * Weak arrows with gradual performs subtyping and returns OUT
#
# * :static - Type signatures have been given.
# * Strong arrows with gradual performs compatibility and returns OUT
# * Weak arrows with gradual performs compatibility and returns dynamic()
#
# * :dynamic - Type signatures have not been given.
# * Strong arrows with gradual performs compatibility and returns dynamic(OUT)
# * Weak arrows with gradual performs compatibility and returns dynamic()
#
# * :infer - Same as :dynamic but skips remote calls.
#
# * :traversal - Focused mostly on traversing AST, skips most type system
# operations. Used by macros and when skipping inference.
#
# The mode may also control exhaustiveness checks in the future (to be decided).
# We may also want for applications with subtyping in dynamic mode to always
# intersect with dynamic, but this mode may be too lax (to be decided based on
# feedback).
@modes [:static, :dynamic, :infer, :traversal]
# These functions are not inferred because they are added/managed by the compiler
@no_infer [__protocol__: 1, behaviour_info: 1]
@doc false
def infer(module, file, defs, private, used_private, env, {_, cache}) do
infer_signatures? = :elixir_config.get(:infer_signatures) and cache != nil
finder =
fn fun_arity ->
case :lists.keyfind(fun_arity, 1, defs) do
{_, kind, _, _} = clause -> {infer_mode(kind, infer_signatures?), clause}
false -> false
end
end
handler = fn meta, fun_arity, stack, context ->
case local_handler(meta, fun_arity, stack, context, finder) do
false ->
undefined_function!(:undefined_function, meta, fun_arity, stack, env)
false
{kind, _, _} = triplet ->
if (kind == :defmacro or kind == :defmacrop) and not Keyword.has_key?(meta, :super) do
undefined_function!(:incorrect_dispatch, meta, fun_arity, stack, env)
false
else
triplet
end
end
end
stack = stack(:infer, file, module, {:__info__, 1}, env, cache, handler)
{types, %{local_sigs: reachable_sigs} = context} =
for {fun_arity, kind, meta, _clauses} = def <- defs,
kind in [:def, :defmacro],
reduce: {[], context()} do
{types, context} ->
finder = fn _ -> {infer_mode(kind, infer_signatures?), def} end
{_kind, inferred, context} = local_handler(meta, fun_arity, stack, context, finder)
if infer_signatures? and kind == :def and fun_arity not in @no_infer do
{[{fun_arity, inferred} | types], context}
else
{types, context}
end
end
# Now traverse all used privates to find any other private that have been used by them.
context =
%{local_sigs: used_sigs} =
for fun_arity <- used_private, reduce: context do
context ->
{_kind, _inferred, context} = local_handler([], fun_arity, stack, context, finder)
context
end
{unreachable, _context} =
Enum.reduce(private, {[], context}, fn
{fun_arity, kind, _meta, _defaults} = info, {unreachable, context} ->
warn_unused_def(info, used_sigs, env)
# Find anything undefined within unused functions
{_kind, _inferred, context} = local_handler([], fun_arity, stack, context, finder)
# defp is reachable if used, defmacrop only if directly invoked
private_sigs = if kind == :defp, do: used_sigs, else: reachable_sigs
if is_map_key(private_sigs, fun_arity) do
{unreachable, context}
else
{[fun_arity | unreachable], context}
end
end)
{Map.new(types), unreachable}
defmodule Error do
defexception [:message]
end
defp infer_mode(kind, infer_signatures?) do
if infer_signatures? and kind in [:def, :defp], do: :infer, else: :traversal
end
defp undefined_function!(reason, meta, {fun, arity}, stack, env) do
env = %{env | function: stack.function, file: stack.file}
tuple = {reason, {fun, arity}, stack.module}
:elixir_errors.module_error(Helpers.with_span(meta, fun), env, __MODULE__, tuple)
end
defp warn_unused_def({_fun_arity, _kind, false, _}, _used, _env) do
:ok
end
defp warn_unused_def({fun_arity, kind, meta, 0}, used, env) do
case is_map_key(used, fun_arity) do
true -> :ok
false -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_def, fun_arity, kind})
end
:ok
end
defp warn_unused_def({tuple, kind, meta, default}, used, env) when default > 0 do
{name, arity} = tuple
min = arity - default
max = arity
case min_reachable_default(max, min, :none, name, used) do
:none -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_def, tuple, kind})
^min -> :ok
^max -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_args, tuple})
diff -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_args, tuple, diff})
end
:ok
end
defp min_reachable_default(max, min, last, name, used) when max >= min do
fun_arity = {name, max}
case is_map_key(used, fun_arity) do
true -> min_reachable_default(max - 1, min, max, name, used)
false -> min_reachable_default(max - 1, min, last, name, used)
end
end
defp min_reachable_default(_max, _min, last, _name, _used) do
last
end
import Module.Types.Helpers
alias Module.Types.{Expr, Pattern, Unify}
@doc false
def warnings(module, file, defs, no_warn_undefined, cache) do
finder = fn fun_arity ->
case :lists.keyfind(fun_arity, 1, defs) do
{_, _, _, _} = clause -> {:dynamic, clause}
false -> false
end
end
stack = stack()
handler = &local_handler(&1, &2, &3, &4, finder)
stack = stack(:dynamic, file, module, {:__info__, 1}, no_warn_undefined, cache, handler)
Enum.flat_map(defs, fn {{fun, arity} = function, kind, meta, clauses} ->
context = context(with_file_meta(meta, file), module, function, no_warn_undefined, cache)
context =
Enum.reduce(defs, context(), fn {fun_arity, _kind, meta, _clauses} = def, context ->
finder = fn _ -> {:dynamic, def} end
{_kind, _inferred, context} = local_handler(meta, fun_arity, stack, context, finder)
context
end)
Enum.flat_map(clauses, fn {_meta, args, guards, body} ->
def_expr = {kind, meta, [guards_to_expr(guards, {fun, [], args})]}
context = warn_unused_clauses(defs, stack, context)
context.warnings
end
try do
warnings_from_clause(args, guards, body, def_expr, stack, context)
rescue
e ->
def_expr = {kind, meta, [guards_to_expr(guards, {fun, [], args}), [do: body]]}
defp warn_unused_clauses(defs, stack, context) do
for {fun_arity, pending} <- context.local_used,
pending != [],
{_fun_arity, kind, meta, clauses} = List.keyfind(defs, fun_arity, 0),
not Keyword.get(meta, :from_super, false),
reduce: context do
context ->
{_kind, _inferred, mapping} = Map.fetch!(context.local_sigs, fun_arity)
error =
Error.exception("""
found error while checking types for #{Exception.format_mfa(module, fun, arity)}:
clauses_indexes =
for type_index <- pending, {clause_index, ^type_index} <- mapping, do: clause_index
#{Exception.format_banner(:error, e, __STACKTRACE__)}\
Enum.reduce(clauses_indexes, context, fn clause_index, context ->
{meta, _args, _guards, _body} = Enum.fetch!(clauses, clause_index)
stack = %{stack | function: fun_arity}
Helpers.warn(__MODULE__, {:unused_clause, kind, fun_arity}, meta, stack, context)
end)
end
end
The exception happened while checking this code:
defp local_handler(_meta, fun_arity, stack, context, finder) do
case context.local_sigs do
%{^fun_arity => {kind, inferred, _mapping}} ->
{kind, inferred, context}
#{Macro.to_string(def_expr)}
%{^fun_arity => kind} when is_atom(kind) ->
{kind, :none, context}
In case it is a bug, please report it at: https://github.com/elixir-lang/elixir/issues
""")
local_sigs ->
case finder.(fun_arity) do
{mode, {fun_arity, kind, meta, clauses}} ->
context = put_in(context.local_sigs, Map.put(local_sigs, fun_arity, kind))
{inferred, mapping, context} =
local_handler(fun_arity, kind, meta, clauses, mode, stack, context)
context =
update_in(context.local_sigs, &Map.put(&1, fun_arity, {kind, inferred, mapping}))
{kind, inferred, context}
false ->
false
reraise error, __STACKTRACE__
end
end
end
defp local_handler({fun, arity} = fun_arity, kind, meta, clauses, mode, stack, context) do
expected = List.duplicate(Descr.dynamic(), arity)
stack = stack |> fresh_stack(mode, fun_arity) |> with_file_meta(meta)
{_, _, mapping, clauses_types, clauses_context} =
Enum.reduce(clauses, {0, 0, [], [], context}, fn
{meta, args, guards, body}, {index, total, mapping, inferred, context} ->
context = fresh_context(context)
try do
{args_types, context} =
Pattern.of_head(args, guards, expected, :default, meta, stack, context)
{return_type, context} =
Expr.of_expr(body, stack, context)
{type_index, inferred} =
add_inferred(inferred, args_types, return_type, total - 1, [])
if type_index == -1 do
{index + 1, total + 1, [{index, total} | mapping], inferred, context}
else
{index + 1, total, [{index, type_index} | mapping], inferred, context}
end
rescue
e ->
internal_error!(e, __STACKTRACE__, kind, meta, fun, args, guards, body, stack)
end
end)
inferred = {:infer, Enum.reverse(clauses_types)}
{inferred, mapping, restore_context(context, clauses_context)}
end)
end
# We check for term equality of types as an optimization
# to reduce the amount of check we do at runtime.
defp add_inferred([{args, existing_return} | tail], args, return, index, acc),
do: {index, Enum.reverse(acc, [{args, Descr.union(existing_return, return)} | tail])}
defp add_inferred([head | tail], args, return, index, acc),
do: add_inferred(tail, args, return, index - 1, [head | acc])
defp add_inferred([], args, return, -1, acc),
do: {-1, [{args, return} | Enum.reverse(acc)]}
defp with_file_meta(stack, meta) do
defp with_file_meta(meta, file) do
case Keyword.fetch(meta, :file) do
{:ok, {meta_file, _}} -> %{stack | file: meta_file}
:error -> stack
{:ok, {meta_file, _}} -> meta_file
:error -> file
end
end
defp internal_error!(e, trace, kind, meta, fun, args, guards, body, stack) do
def_expr = {kind, meta, [guards_to_expr(guards, {fun, [], args}), [do: body]]}
exception =
RuntimeError.exception("""
found error while checking types for #{Exception.format_mfa(stack.module, fun, length(args))}:
#{Exception.format_banner(:error, e, trace)}\
The exception happened while checking this code:
#{Macro.to_string(def_expr)}
Please report this bug at: https://github.com/elixir-lang/elixir/issues
""")
reraise exception, trace
end
defp guards_to_expr([], left) do
left
end
@@ -307,94 +58,471 @@ defmodule Module.Types do
guards_to_expr(guards, {:when, [], [left, guard]})
end
defp warnings_from_clause(args, guards, body, def_expr, stack, context) do
head_stack = Unify.push_expr_stack(def_expr, stack)
with {:ok, _types, context} <- Pattern.of_head(args, guards, head_stack, context),
{:ok, _type, context} <- Expr.of_expr(body, :dynamic, stack, context) do
context.warnings
else
{:error, {type, error, context}} ->
[error_to_warning(type, error, context) | context.warnings]
end
end
@doc false
def stack(mode, file, module, function, no_warn_undefined, cache, handler)
when mode in @modes do
def context(file, module, function, no_warn_undefined, cache) do
%{
# The fallback meta used for literals in patterns and guards
meta: [],
# File of module
file: file,
# Module of definitions
module: module,
# Current function
function: function,
# List of calls to not warn on as undefined or :all or Macro.Env indicating limited remotes
# List of calls to not warn on as undefined
no_warn_undefined: no_warn_undefined,
# A tuple with cache information (may be nil)
# A list of cached modules received from the parallel compiler
cache: cache,
# The mode to be used, see the @modes attribute
mode: mode,
# The function for handling local calls
local_handler: handler
# Expression variable to type variable
vars: %{},
# Type variable to expression variable
types_to_vars: %{},
# Type variable to type
types: %{},
# Trace of all variables that have been refined to a type,
# including the type they were refined to, why, and where
traces: %{},
# Counter to give type variables unique names
counter: 0,
# Track if a variable was inferred from a type guard function such is_tuple/1
# or a guard function that fails such as elem/2, possible values are:
# `:guarded` when `is_tuple(x)`
# `:guarded` when `is_tuple and elem(x, 0)`
# `:fail` when `elem(x, 0)`
guard_sources: %{},
# A list with all warnings from the running the code
warnings: []
}
end
@doc false
def context() do
def stack() do
%{
# A list of all warnings found so far
warnings: [],
# All vars and their types
vars: %{},
# Variables and arguments from patterns
pattern_info: nil,
# If type checking has found an error/failure
failed: false,
# Local signatures used by local handler
local_sigs: %{},
# Track which clauses have been used across private local calls
local_used: %{}
# Stack of variables we have refined during unification,
# used for creating relevant traces
unify_stack: [],
# Last expression we have recursed through during inference,
# used for tracing
last_expr: nil,
# When false do not add a trace when a type variable is refined,
# useful when merging contexts where the variables already have traces
trace: true,
# There are two factors that control how we track guards.
#
# * consider_type_guards?: if type guards should be considered.
# This applies only at the root and root-based "and" and "or" nodes.
#
# * keep_guarded? - if a guarded clause should remain as guarded
# even on failure. Used on the right side of and.
#
type_guards: {_consider_type_guards? = true, _keep_guarded? = false},
# Context used to determine if unification is bi-directional, :expr
# is directional, :pattern is bi-directional
context: nil
}
end
defp fresh_stack(stack, mode, function) when mode in @modes do
%{stack | mode: mode, function: function}
## ERROR TO WARNING
# 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
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)
error = {:unable_apply, mfa, args, expected, signature, {location, stack.last_expr, traces}}
{Module.Types, error, location}
end
defp fresh_context(context) do
%{context | vars: %{}, failed: false}
def error_to_warning(:unable_unify, {left, right, stack}, context) do
{fun, arity} = context.function
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)
error = {:unable_unify, left, right, {location, stack.last_expr, traces}}
{Module.Types, error, location}
end
defp restore_context(%{vars: vars, failed: failed}, later_context) do
%{later_context | vars: vars, failed: failed}
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
# in the stack since we get related variables anyway?
stack =
stack.unify_stack
|> Enum.flat_map(&[&1 | related_variables(&1, context.types)])
|> Enum.uniq()
Enum.flat_map(stack, fn var_index ->
with %{^var_index => traces} <- context.traces,
%{^var_index => expr_var} <- context.types_to_vars do
Enum.map(traces, &tag_trace(expr_var, &1, context))
else
_other -> []
end
end)
end
## Diagnostics
defp related_variables(var, types) do
Enum.flat_map(types, fn
{related_var, {:var, ^var}} ->
[related_var | related_variables(related_var, types)]
def format_diagnostic({:unused_clause, kind, {fun, arity}}) do
%{
message: "this clause of #{kind} #{fun}/#{arity} is never used"
}
_ ->
[]
end)
end
## Module errors
# Tag if trace is for a concrete type or type variable
defp tag_trace(var, {type, expr, location}, context) do
with {:var, var_index} <- type,
%{^var_index => expr_var} <- context.types_to_vars do
{:var, var, expr_var, expr, location}
else
_ -> {:type, var, type, expr, location}
end
end
def format_error({:unused_args, {name, arity}}),
do: "default values for the optional arguments in #{name}/#{arity} are never used"
defp lift_all_types(types, traces, context) do
trace_types = for({:type, _, type, _, _} <- traces, do: type)
{types, lift_context} = Unify.lift_types(types, context)
{trace_types, _lift_context} = Unify.lift_types(trace_types, lift_context)
def format_error({:unused_args, {name, arity}, count}) when arity - count == 1,
do: "the default value for the last optional argument in #{name}/#{arity} is never used"
{traces, []} =
Enum.map_reduce(traces, trace_types, fn
{:type, var, _, expr, location}, [type | acc] -> {{:type, var, type, expr, location}, acc}
other, acc -> {other, acc}
end)
def format_error({:unused_args, {name, arity}, count}),
do:
"the default values for the last #{arity - count} optional arguments in #{name}/#{arity} are never used"
{types, traces}
end
def format_error({:unused_def, {name, arity}, :defp}),
do: "function #{name}/#{arity} is unused"
## FORMAT WARNINGS
def format_error({:unused_def, {name, arity}, :defmacrop}),
do: "macro #{name}/#{arity} is unused"
def format_warning({:unable_apply, mfa, args, expected, signature, {location, expr, traces}}) do
{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)
def format_error({:undefined_function, {f, a}, _})
when {f, a} in [__info__: 1, behaviour_info: 1, module_info: 1, module_info: 0],
do:
"undefined function #{f}/#{a} (this function is auto-generated by the compiler and must always be called as a remote, as in __MODULE__.#{f}/#{a})"
clauses =
Enum.map(signature, fn {ins, out} ->
{_, _, ins} = erl_to_ex(original_module, original_function, ins, [])
def format_error({:undefined_function, {f, a}, module}),
do:
"undefined function #{f}/#{a} (expected #{inspect(module)} to define such a function or for it to be imported, but none are available)"
{:fun, [{ins, out}]}
|> Unify.format_type(false)
|> IO.iodata_to_binary()
|> binary_slice(1..-2//1)
end)
def format_error({:incorrect_dispatch, {f, a}, _module}),
do: "cannot invoke macro #{f}/#{a} before its definition"
[
"expected #{format_mfa} to have signature:\n\n ",
Enum.map_join(args, ", ", &Unify.format_type(&1, false)),
" -> #{Unify.format_type(expected, false)}",
"\n\nbut it has signature:\n\n ",
indent(Enum.join(clauses, "\n")),
"\n\n",
format_expr(expr, location),
traces,
"Conflict found at"
]
end
def format_warning({:unable_unify, left, right, {location, expr, traces}}) do
if map_type?(left) and map_type?(right) and match?({:ok, _, _}, missing_field(left, right)) do
{:ok, atom, known_atoms} = missing_field(left, right)
# Drop the last trace which is the expression map.foo
traces = Enum.drop(traces, 1)
{traces, hints} = format_traces(traces, [left, right], true)
[
"undefined field \"#{atom}\" ",
format_expr(expr, location),
"expected one of the following fields: ",
Enum.join(Enum.sort(known_atoms), ", "),
"\n\n",
traces,
format_message_hints(hints),
"Conflict found at"
]
else
simplify_left? = simplify_type?(left, right)
simplify_right? = simplify_type?(right, left)
{traces, hints} = format_traces(traces, [left, right], simplify_left? or simplify_right?)
[
"incompatible types:\n\n ",
Unify.format_type(left, simplify_left?),
" !~ ",
Unify.format_type(right, simplify_right?),
"\n\n",
format_expr(expr, location),
traces,
format_message_hints(hints),
"Conflict found at"
]
end
end
defp missing_field(
{:map, [{:required, {:atom, atom} = type, _}, {:optional, :dynamic, :dynamic}]},
{:map, fields}
) do
matched_missing_field(fields, type, atom)
end
defp missing_field(
{:map, fields},
{:map, [{:required, {:atom, atom} = type, _}, {:optional, :dynamic, :dynamic}]}
) do
matched_missing_field(fields, type, atom)
end
defp missing_field(_, _), do: :error
defp matched_missing_field(fields, type, atom) do
if List.keymember?(fields, type, 1) do
:error
else
known_atoms = for {_, {:atom, atom}, _} <- fields, do: atom
{:ok, atom, known_atoms}
end
end
defp format_traces([], _types, _simplify?) do
{[], []}
end
defp format_traces(traces, types, simplify?) do
traces
|> Enum.uniq()
|> Enum.reverse()
|> Enum.map_reduce([], fn
{:type, var, type, expr, location}, hints ->
{hint, hints} = format_type_hint(type, types, expr, hints)
trace = [
"where \"",
Macro.to_string(var),
"\" was given the type ",
Unify.format_type(type, simplify?),
hint,
" in:\n\n # ",
format_location(location),
" ",
indent(expr_to_string(expr)),
"\n\n"
]
{trace, hints}
{:var, var1, var2, expr, location}, hints ->
trace = [
"where \"",
Macro.to_string(var1),
"\" was given the same type as \"",
Macro.to_string(var2),
"\" in:\n\n # ",
format_location(location),
" ",
indent(expr_to_string(expr)),
"\n\n"
]
{trace, hints}
end)
end
defp format_location({file, line, _mfa}) do
format_location({file, line})
end
defp format_location({file, line}) do
file = Path.relative_to_cwd(file)
line = if line, do: [Integer.to_string(line)], else: []
[file, ?:, line, ?\n]
end
defp simplify_type?(type, other) do
map_like_type?(type) and not map_like_type?(other)
end
## EXPRESSION FORMATTING
defp format_expr(nil, _location) do
[]
end
defp format_expr(expr, location) do
[
"in expression:\n\n # ",
format_location(location),
" ",
indent(expr_to_string(expr)),
"\n\n"
]
end
@doc false
def expr_to_string(expr) do
expr
|> reverse_rewrite()
|> Macro.to_string()
end
defp reverse_rewrite(guard) do
Macro.prewalk(guard, fn
{{:., _, [mod, fun]}, meta, args} -> erl_to_ex(mod, fun, args, meta)
other -> other
end)
end
defp erl_to_ex(mod, fun, args, meta) do
case :elixir_rewrite.erl_to_ex(mod, fun, args) do
{Kernel, fun, args} -> {fun, meta, args}
{mod, fun, args} -> {{:., [], [mod, fun]}, meta, args}
end
end
## Hints
defp format_message_hints(hints) do
hints
|> Enum.uniq()
|> Enum.reverse()
|> Enum.map(&[format_message_hint(&1), "\n"])
end
defp format_message_hint(:inferred_dot) do
"""
HINT: "var.field" (without parentheses) implies "var" is a map() while \
"var.fun()" (with parentheses) implies "var" is an atom()
"""
end
defp format_message_hint(:inferred_bitstring_spec) do
"""
HINT: all expressions given to binaries are assumed to be of type \
integer() unless said otherwise. For example, <<expr>> assumes "expr" \
is an integer. Pass a modifier, such as <<expr::float>> or <<expr::binary>>, \
to change the default behaviour.
"""
end
defp format_message_hint({:sized_and_unsize_tuples, {size, var}}) do
"""
HINT: use pattern matching or "is_tuple(#{Macro.to_string(var)}) and \
tuple_size(#{Macro.to_string(var)}) == #{size}" to guard a sized tuple.
"""
end
defp format_type_hint(type, types, expr, hints) do
case format_type_hint(type, types, expr) do
{message, hint} -> {message, [hint | hints]}
:error -> {[], hints}
end
end
defp format_type_hint(type, types, expr) do
cond do
dynamic_map_dot?(type, expr) ->
{" (due to calling var.field)", :inferred_dot}
dynamic_remote_call?(type, expr) ->
{" (due to calling var.fun())", :inferred_dot}
inferred_bitstring_spec?(type, expr) ->
{[], :inferred_bitstring_spec}
message = sized_and_unsize_tuples(expr, types) ->
{[], {:sized_and_unsize_tuples, message}}
true ->
:error
end
end
defp dynamic_map_dot?(type, expr) do
with true <- map_type?(type),
{{:., _meta1, [_map, _field]}, meta2, []} <- expr,
true <- Keyword.get(meta2, :no_parens, false) do
true
else
_ -> false
end
end
defp dynamic_remote_call?(type, expr) do
with true <- atom_type?(type),
{{:., _meta1, [_module, _field]}, meta2, []} <- expr,
false <- Keyword.get(meta2, :no_parens, false) do
true
else
_ -> false
end
end
defp inferred_bitstring_spec?(type, expr) do
with true <- integer_type?(type),
{:<<>>, _, args} <- expr,
true <- Enum.any?(args, &match?({:"::", [{:inferred_bitstring_spec, true} | _], _}, &1)) do
true
else
_ -> false
end
end
defp sized_and_unsize_tuples({{:., _, [:erlang, :is_tuple]}, _, [var]}, types) do
case Enum.find(types, &match?({:tuple, _, _}, &1)) do
{:tuple, size, _} ->
{size, var}
nil ->
nil
end
end
defp sized_and_unsize_tuples(_expr, _types) do
nil
end
## Formatting helpers
defp indent(string) do
String.replace(string, "\n", "\n ")
end
defp map_type?({:map, _}), do: true
defp map_type?(_other), do: false
defp map_like_type?({:map, _}), do: true
defp map_like_type?({:union, union}), do: Enum.any?(union, &map_like_type?/1)
defp map_like_type?(_other), do: false
defp atom_type?(:atom), do: true
defp atom_type?({:atom, _}), do: false
defp atom_type?({:union, union}), do: Enum.all?(union, &atom_type?/1)
defp atom_type?(_other), do: false
defp integer_type?(:integer), do: true
defp integer_type?(_other), do: false
defp call_to_mfa({{:., _, [mod, fun]}, _, args}), do: {mod, fun, length(args)}
defp call_to_mfa({fun, _, args}) when is_atom(fun), do: {Kernel, fun, length(args)}
end
File diff suppressed because it is too large Load Diff
@@ -1,10 +1,10 @@
defmodule Module.Behaviour do
defmodule Module.Types.Behaviour do
# Checking functionality for @behaviours and @impl
@moduledoc false
@doc false
def callbacks(behaviour) do
for callback <- behaviour.behaviour_info(:callbacks) do
for callback <- behaviour_info(behaviour, :callbacks) do
{pair, _kind} = normalize_macro_or_function_callback(callback)
pair
end
@@ -33,14 +33,15 @@ defmodule Module.Behaviour do
module: module,
file: file,
line: line,
# Map containing the callbacks to be implemented
callbacks: %{},
# list of warnings {message, env}
warnings: []
}
end
defp warn(context, warning, meta \\ []) do
meta = Keyword.put_new(meta, :line, context.line)
location = {meta[:file] || context.file, meta, context.module}
location = {meta[:file] || context.file, meta[:line] || context.line, context.module}
update_in(context.warnings, &[{__MODULE__, warning, location} | &1])
end
@@ -55,8 +56,8 @@ defmodule Module.Behaviour do
warn(context, {:module_does_not_define_behaviour, context.module, behaviour})
true ->
optional_callbacks = behaviour.behaviour_info(:optional_callbacks)
callbacks = behaviour.behaviour_info(:callbacks)
optional_callbacks = behaviour_info(behaviour, :optional_callbacks)
callbacks = behaviour_info(behaviour, :callbacks)
Enum.reduce(callbacks, context, &add_callback(&2, &1, behaviour, optional_callbacks))
end
end)
@@ -67,7 +68,7 @@ defmodule Module.Behaviour do
context =
case context.callbacks do
%{^callback => [{_kind, conflict, _optional?} | _]} ->
%{^callback => {_kind, conflict, _optional?}} ->
warn(
context,
{:duplicate_behaviour, context.module, behaviour, conflict, kind, callback}
@@ -77,15 +78,11 @@ defmodule Module.Behaviour do
context
end
new_callback = {kind, behaviour, original in optional_callbacks}
callbacks = context.callbacks[callback] || []
put_in(context.callbacks[callback], [new_callback | callbacks])
put_in(context.callbacks[callback], {kind, behaviour, original in optional_callbacks})
end
defp check_callbacks(context, all_definitions) do
for {callback, callbacks} <- context.callbacks,
{kind, behaviour, optional?} <- callbacks,
for {callback, {kind, behaviour, optional?}} <- context.callbacks,
reduce: context do
context ->
case :lists.keyfind(callback, 1, all_definitions) do
@@ -135,14 +132,14 @@ defmodule Module.Behaviour do
{:error, message} ->
warning = message |> Tuple.insert_at(1, kind) |> Tuple.insert_at(1, fa)
context = warn(context, warning, line: line, file: file)
context = warn(context, warning, %{line: line, file: file})
{context, impl_contexts}
end
end)
end
defp impl_behaviours({function, arity}, defaults, kind, value, behaviours, callbacks) do
impls = for n <- (arity - defaults)..arity, do: {function, n}
impls = for n <- arity..(arity - defaults), do: {function, n}
impl_behaviours(impls, kind, value, behaviours, callbacks)
end
@@ -188,10 +185,10 @@ defmodule Module.Behaviour do
end
defp behaviour_callbacks_for_impls([fa | tail], behaviour, callbacks) do
with list when is_list(list) <- callbacks[fa],
true <- Enum.any?(list, &match?({_, ^behaviour, _}, &1)) do
[{fa, behaviour} | behaviour_callbacks_for_impls(tail, behaviour, callbacks)]
else
case callbacks[fa] do
{_, ^behaviour, _} ->
[{fa, behaviour} | behaviour_callbacks_for_impls(tail, behaviour, callbacks)]
_ ->
behaviour_callbacks_for_impls(tail, behaviour, callbacks)
end
@@ -203,12 +200,8 @@ defmodule Module.Behaviour do
defp callbacks_for_impls([fa | tail], callbacks) do
case callbacks[fa] do
list when is_list(list) ->
Enum.map(list, fn {_, behaviour, _} -> {fa, behaviour} end) ++
callbacks_for_impls(tail, callbacks)
nil ->
callbacks_for_impls(tail, callbacks)
{_, behaviour, _} -> [{fa, behaviour} | callbacks_for_impls(tail, callbacks)]
nil -> callbacks_for_impls(tail, callbacks)
end
end
@@ -220,14 +213,11 @@ defmodule Module.Behaviour do
defp warn_missing_impls(context, impl_contexts, defs) do
for {pair, kind, meta, _clauses} <- defs, kind in [:def, :defmacro], reduce: context do
context ->
with {:ok, callbacks} <- Map.fetch(context.callbacks, pair),
{_, behaviour, _} <-
Enum.find(callbacks, fn {_, behaviour, _} ->
missing_impl_in_context?(meta, behaviour, impl_contexts)
end) do
warn(context, {:missing_impl, pair, kind, behaviour},
with {:ok, {_, behaviour, _}} <- Map.fetch(context.callbacks, pair),
true <- missing_impl_in_context?(meta, behaviour, impl_contexts) do
warn(context, {:missing_impl, pair, kind, behaviour}, %{
line: :elixir_utils.get_line(meta)
)
})
else
_ -> context
end
@@ -257,13 +247,20 @@ defmodule Module.Behaviour do
defp known_callbacks(callbacks) do
formatted_callbacks =
for {{name, arity}, list} <- callbacks, {kind, module, _} <- list do
for {{name, arity}, {kind, module, _}} <- callbacks do
"\n * " <> Exception.format_mfa(module, name, arity) <> " (#{format_definition(kind)})"
end
". The known callbacks are:\n#{formatted_callbacks}\n"
end
defp behaviour_info(module, key) do
case module.behaviour_info(key) do
list when is_list(list) -> list
:undefined -> []
end
end
defp normalize_macro_or_function_callback({function_name, arity}) do
case :erlang.atom_to_list(function_name) do
# Macros are always provided one extra argument in behaviour_info/1
@@ -275,11 +272,7 @@ defmodule Module.Behaviour do
end
end
def format_diagnostic(warning) do
%{message: IO.iodata_to_binary(format_warning(warning))}
end
defp format_warning({:undefined_behaviour, module, behaviour}) do
def format_warning({:undefined_behaviour, module, behaviour}) do
[
"@behaviour ",
inspect(behaviour),
@@ -289,14 +282,14 @@ defmodule Module.Behaviour do
]
end
defp format_warning({:module_does_not_define_behaviour, module, behaviour}) do
def format_warning({:module_does_not_define_behaviour, module, behaviour}) do
["module ", inspect(behaviour), " is not a behaviour (in module ", inspect(module), ")"]
end
defp format_warning({:duplicate_behaviour, module, behaviour, conflict, kind, callback})
when conflict == behaviour do
def format_warning({:duplicate_behaviour, module, behaviour, conflict, kind, callback})
when conflict == behaviour do
[
"the behaviour ",
"the behavior ",
inspect(behaviour),
" has been declared twice (conflict in ",
format_definition(kind, callback),
@@ -306,11 +299,11 @@ defmodule Module.Behaviour do
]
end
defp format_warning({:duplicate_behaviour, module, behaviour, conflict, kind, callback}) do
def format_warning({:duplicate_behaviour, module, behaviour, conflict, kind, callback}) do
[
"conflicting behaviours found. Callback ",
"conflicting behaviours found. ",
format_definition(kind, callback),
" is defined by both ",
" is required by ",
inspect(conflict),
" and ",
inspect(behaviour),
@@ -320,7 +313,7 @@ defmodule Module.Behaviour do
]
end
defp format_warning({:missing_callback, module, callback, kind, behaviour}) do
def format_warning({:missing_callback, module, callback, kind, behaviour}) do
[
format_callback(callback, kind, behaviour),
" is not implemented (in module ",
@@ -329,7 +322,7 @@ defmodule Module.Behaviour do
]
end
defp format_warning({:callback_mismatch, module, callback, kind, wrong_kind, behaviour}) do
def format_warning({:callback_mismatch, module, callback, kind, wrong_kind, behaviour}) do
[
format_callback(callback, kind, behaviour),
" was implemented as \"",
@@ -342,14 +335,14 @@ defmodule Module.Behaviour do
]
end
defp format_warning({:private_function, callback, kind}) do
def format_warning({:private_function, callback, kind}) do
[
format_definition(kind, callback),
" is private, @impl attribute is always discarded for private functions/macros"
]
end
defp format_warning({:no_behaviours, callback, kind, value}) do
def format_warning({:no_behaviours, callback, kind, value}) do
[
"got \"@impl ",
inspect(value),
@@ -359,7 +352,7 @@ defmodule Module.Behaviour do
]
end
defp format_warning({:impl_not_defined, callback, kind, {_fa, behaviour}}) do
def format_warning({:impl_not_defined, callback, kind, {_fa, behaviour}}) do
[
"got \"@impl false\" for ",
format_definition(kind, callback),
@@ -368,7 +361,7 @@ defmodule Module.Behaviour do
]
end
defp format_warning({:impl_defined, callback, kind, callbacks}) do
def format_warning({:impl_defined, callback, kind, callbacks}) do
[
"got \"@impl true\" for ",
format_definition(kind, callback),
@@ -377,7 +370,7 @@ defmodule Module.Behaviour do
]
end
defp format_warning({:behaviour_not_declared, callback, kind, behaviour}) do
def format_warning({:behaviour_not_declared, callback, kind, behaviour}) do
[
"got \"@impl ",
inspect(behaviour),
@@ -387,7 +380,7 @@ defmodule Module.Behaviour do
]
end
defp format_warning({:behaviour_not_defined, callback, kind, behaviour, callbacks}) do
def format_warning({:behaviour_not_defined, callback, kind, behaviour, callbacks}) do
[
"got \"@impl ",
inspect(behaviour),
@@ -398,7 +391,7 @@ defmodule Module.Behaviour do
]
end
defp format_warning({:missing_impl, callback, kind, behaviour}) do
def format_warning({:missing_impl, callback, kind, behaviour}) do
[
"module attribute @impl was not set for ",
format_definition(kind, callback),
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+129 -212
View File
@@ -2,8 +2,6 @@ defmodule Module.Types.Helpers do
# AST and enumeration helpers.
@moduledoc false
## AST helpers
@doc """
Guard function to check if an AST node is a variable.
"""
@@ -17,20 +15,9 @@ defmodule Module.Types.Helpers do
end
@doc """
Unpacks a list into head elements and tail.
Returns unique identifier for the current assignment of the variable.
"""
def unpack_list([{:|, _, [head, tail]}], acc), do: {Enum.reverse([head | acc]), tail}
def unpack_list([head], acc), do: {Enum.reverse([head | acc]), []}
def unpack_list([head | tail], acc), do: unpack_list(tail, [head | acc])
@doc """
Unpacks a match into several matches.
"""
def unpack_match({:=, _, [left, right]}, acc),
do: unpack_match(left, unpack_match(right, acc))
def unpack_match(node, acc),
do: [node | acc]
def var_name({_name, meta, _context}), do: Keyword.fetch!(meta, :version)
@doc """
Returns the AST metadata.
@@ -39,241 +26,171 @@ defmodule Module.Types.Helpers do
def get_meta(_other), do: []
@doc """
Attaches span information.
Like `Enum.reduce/3` but only continues while `fun` returns `{:ok, acc}`
and stops on `{:error, reason}`.
"""
def with_span(meta, name) do
:elixir_env.calculate_span(meta, name)
def reduce_ok(list, acc, fun) do
do_reduce_ok(list, acc, fun)
end
## Warnings
defp do_reduce_ok([head | tail], acc, fun) do
case fun.(head, acc) do
{:ok, acc} ->
do_reduce_ok(tail, acc, fun)
@doc """
Formatted hints in typing errors.
"""
def format_hints(hints) do
hints
|> Enum.uniq()
|> Enum.map(fn
:inferred_bitstring_spec ->
"""
#{hint()} all expressions given to binaries are assumed to be of type \
integer() unless said otherwise. For example, <<expr>> assumes "expr" \
is an integer. Pass a modifier, such as <<expr::float>> or <<expr::binary>>, \
to change the default behavior.
"""
:dot ->
"""
#{hint()} "var.field" (without parentheses) means "var" is a map() while \
"var.fun()" (with parentheses) means "var" is an atom()
"""
:anonymous_rescue ->
"""
#{hint()} when you rescue without specifying exception names, \
the variable is assigned a type of a struct but all of its fields are unknown. \
If you are trying to access an exception's :message key, either specify the \
exception names or use `Exception.message/1`.
"""
end)
end
defp hint, do: :elixir_errors.prefix(:hint)
@doc """
Collect traces from variables in expression.
This information is exposed to language servers and
therefore must remain backwards compatible.
"""
def collect_traces(expr, %{vars: vars}) do
{_, versions} =
Macro.prewalk(expr, %{}, fn
{var_name, meta, var_context}, versions when is_atom(var_name) and is_atom(var_context) ->
version = meta[:version]
case vars do
%{^version => %{off_traces: off_traces, name: name, context: context}} ->
{:ok,
Map.put(versions, version, %{
type: :variable,
name: name,
context: context,
traces: collect_var_traces(off_traces)
})}
_ ->
{:ok, versions}
end
node, versions ->
{node, versions}
end)
versions
|> Map.values()
|> Enum.sort_by(& &1.name)
end
defp collect_var_traces(traces) do
traces
|> Enum.reject(fn {_expr, _file, type, _formatter} ->
# As an otimization do not care about dynamic terms
type == %{dynamic: :term}
end)
|> case do
[] -> traces
filtered -> filtered
{:error, reason} ->
{:error, reason}
end
|> Enum.reverse()
|> Enum.map(fn {expr, file, type, formatter} ->
meta = get_meta(expr)
{formatted_expr, formatter_hints} =
case formatter do
:default -> {expr_to_string(expr), []}
formatter -> formatter.(expr)
end
# This information is exposed to language servers and
# therefore must remain backwards compatible.
%{
file: file,
meta: meta,
formatted_expr: formatted_expr,
formatted_hints: format_hints(formatter_hints ++ expr_hints(expr)),
formatted_type: Module.Types.Descr.to_quoted_string(type, collapse_structs: true)
}
end)
|> Enum.sort_by(&{&1.meta[:line], &1.meta[:column]})
|> Enum.dedup()
end
defp do_reduce_ok([], acc, _fun), do: {:ok, acc}
@doc """
Format previously collected traces.
Like `Enum.unzip/1` but only continues while `fun` returns `{:ok, elem1, elem2}`
and stops on `{:error, reason}`.
"""
def format_traces(traces) do
Enum.map(traces, &format_trace/1)
def unzip_ok(list) do
do_unzip_ok(list, [], [])
end
defp format_trace(%{type: :variable, name: name, context: context, traces: traces}) do
traces =
for trace <- traces do
location =
trace.file
|> Path.relative_to_cwd()
|> Exception.format_file_line(trace.meta[:line], trace.meta[:column])
|> String.replace_suffix(":", "")
[
"""
# type: #{indent(trace.formatted_type, 4)}
# from: #{location}
\
""",
indent(trace.formatted_expr, 4),
?\n,
trace.formatted_hints
]
end
type_or_types = pluralize(traces, "type", "types")
["\nwhere #{format_var(name, context)} was given the #{type_or_types}:\n" | traces]
defp do_unzip_ok([{:ok, head1, head2} | tail], acc1, acc2) do
do_unzip_ok(tail, [head1 | acc1], [head2 | acc2])
end
defp do_unzip_ok([{:error, reason} | _tail], _acc1, _acc2), do: {:error, reason}
defp do_unzip_ok([], acc1, acc2), do: {:ok, Enum.reverse(acc1), Enum.reverse(acc2)}
@doc """
Formats a var for pretty printing.
Like `Enum.map/2` but only continues while `fun` returns `{:ok, elem}`
and stops on `{:error, reason}`.
"""
def format_var({var, _, context}), do: format_var(var, context)
def format_var(var, nil), do: "\"#{var}\""
def format_var(var, context), do: "\"#{var}\" (context #{inspect(context)})"
def map_ok(list, fun) do
do_map_ok(list, [], fun)
end
defp pluralize([_], singular, _plural), do: singular
defp pluralize(_, _singular, plural), do: plural
defp do_map_ok([head | tail], acc, fun) do
case fun.(head) do
{:ok, elem} ->
do_map_ok(tail, [elem | acc], fun)
defp expr_hints({:<<>>, [inferred_bitstring_spec: true] ++ _meta, _}),
do: [:inferred_bitstring_spec]
{:error, reason} ->
{:error, reason}
end
end
defp expr_hints(_), do: []
defp do_map_ok([], acc, _fun), do: {:ok, Enum.reverse(acc)}
@doc """
Converts the given expression to a string,
translating inlined Erlang calls back to Elixir.
Like `Enum.map_reduce/3` but only continues while `fun` returns `{:ok, elem, acc}`
and stops on `{:error, reason}`.
"""
def expr_to_string(expr) do
expr
|> Macro.prewalk(fn
{{:., _, [mod, fun]}, meta, args} -> erl_to_ex(mod, fun, args, meta)
other -> other
end)
|> Macro.to_string()
def map_reduce_ok(list, acc, fun) do
do_map_reduce_ok(list, {[], acc}, fun)
end
defp erl_to_ex(
:erlang,
:error,
[expr, :none, [error_info: {:%{}, _, [module: Exception]}]],
meta
) do
{:raise, meta, [expr]}
defp do_map_reduce_ok([head | tail], {list, acc}, fun) do
case fun.(head, acc) do
{:ok, elem, acc} ->
do_map_reduce_ok(tail, {[elem | list], acc}, fun)
{:error, reason} ->
{:error, reason}
end
end
defp erl_to_ex(mod, fun, args, meta) do
case :elixir_rewrite.erl_to_ex(mod, fun, args) do
{Kernel, fun, args, _} -> {fun, meta, args}
{mod, fun, args, _} -> {{:., [], [mod, fun]}, meta, args}
defp do_map_reduce_ok([], {list, acc}, _fun), do: {:ok, Enum.reverse(list), acc}
@doc """
Like `Enum.flat_map/2` but only continues while `fun` returns `{:ok, list}`
and stops on `{:error, reason}`.
"""
def flat_map_ok(list, fun) do
do_flat_map_ok(list, [], fun)
end
defp do_flat_map_ok([head | tail], acc, fun) do
case fun.(head) do
{:ok, elem} ->
do_flat_map_ok(tail, [elem | acc], fun)
{:error, reason} ->
{:error, reason}
end
end
defp do_flat_map_ok([], acc, _fun), do: {:ok, Enum.reverse(Enum.concat(acc))}
@doc """
Like `Enum.flat_map_reduce/3` but only continues while `fun` returns `{:ok, list, acc}`
and stops on `{:error, reason}`.
"""
def flat_map_reduce_ok(list, acc, fun) do
do_flat_map_reduce_ok(list, {[], acc}, fun)
end
defp do_flat_map_reduce_ok([head | tail], {list, acc}, fun) do
case fun.(head, acc) do
{:ok, elems, acc} ->
do_flat_map_reduce_ok(tail, {[elems | list], acc}, fun)
{:error, reason} ->
{:error, reason}
end
end
defp do_flat_map_reduce_ok([], {list, acc}, _fun),
do: {:ok, Enum.reverse(Enum.concat(list)), acc}
@doc """
Given a list of `[{:ok, term()} | {:error, term()}]` it returns a list of
errors `{:error, [term()]}` in case of at least one error or `{:ok, [term()]}`
if there are no errors.
"""
def oks_or_errors(list) do
case Enum.split_with(list, &match?({:ok, _}, &1)) do
{oks, []} -> {:ok, Enum.map(oks, fn {:ok, ok} -> ok end)}
{_oks, errors} -> {:error, Enum.map(errors, fn {:error, error} -> error end)}
end
end
@doc """
Indents new lines.
Combines a list of guard expressions `when x when y when z` to an expression
combined with `or`, `x or y or z`.
"""
def indent(content, count) do
String.replace(content, "\n", "\n" <> String.duplicate(" ", count))
# TODO: Remove this and let multiple when be treated as multiple clauses,
# meaning they will be intersection types
def guards_to_or([]) do
[]
end
def guards_to_or(guards) do
Enum.reduce(guards, fn guard, acc -> {{:., [], [:erlang, :orelse]}, [], [guard, acc]} end)
end
@doc """
Emits a warning.
Like `Enum.zip/1` but will zip multiple lists together instead of only two.
"""
def warn(module, warning, meta, stack, context) do
if Keyword.get(meta, :generated, false) do
context
else
effectively_warn(module, warning, meta, stack, context)
end
def zip_many(lists) do
zip_many(lists, [], [[]])
end
defp effectively_warn(module, warning, meta, stack, context) do
{fun, arity} = stack.function
location = {stack.file, meta, {stack.module, fun, arity}}
%{context | warnings: [{module, warning, location} | context.warnings]}
defp zip_many([], [], [[] | acc]) do
map_reverse(acc, [], &Enum.reverse/1)
end
@doc """
Emits an error.
In practice an error is a warning that halts other errors from being collected.
"""
def error(module, warning, meta, stack, context) do
case context do
%{failed: true} ->
context
%{failed: false} ->
if Keyword.get(meta, :generated, false) do
context
else
effectively_warn(module, warning, meta, stack, %{context | failed: true})
end
end
defp zip_many([], remain, [last | acc]) do
zip_many(Enum.reverse(remain), [], [[] | [last | acc]])
end
@doc """
The type to return when there is an error.
"""
def error_type, do: Module.Types.Descr.dynamic()
defp zip_many([[] | _], remain, [last | acc]) do
zip_many(Enum.reverse(remain), [], [last | acc])
end
defp zip_many([[elem | list1] | list2], remain, [last | acc]) do
zip_many(list2, [list1 | remain], [[elem | last] | acc])
end
defp map_reverse([], acc, _fun), do: acc
defp map_reverse([head | tail], acc, fun), do: map_reverse(tail, [fun.(head) | acc], fun)
end
+290 -440
View File
@@ -2,248 +2,137 @@ defmodule Module.Types.Of do
# Typing functionality shared between Expr and Pattern.
# Generic AST and Enum helpers go to Module.Types.Helpers.
@moduledoc false
import Module.Types.{Helpers, Descr}
@prefix quote(do: ...)
@suffix quote(do: ...)
@integer_or_float union(integer(), float())
@integer_or_binary union(integer(), binary())
@integer integer()
@float float()
@binary binary()
alias Module.ParallelChecker
## Variables
import Module.Types.Helpers
import Module.Types.Unify
# There are important assumptions on how we work with maps.
#
# First, the keys in the map must be ordered by subtyping.
#
# Second, optional keys must be a superset of the required
# keys, i.e. %{required(atom) => integer, optional(:foo) => :bar}
# is forbidden.
#
# Third, in order to preserve co/contra-variance, a supertype
# must satisfy its subtypes. I.e. %{foo: :bar, atom() => :baz}
# is forbidden, it must be %{foo: :bar, atom() => :baz | :bar}.
#
# Once we support user declared maps, we need to validate these
# assumptions.
@doc """
Fetches the type of a defined variable.
Handles open maps (with dynamic => dynamic).
"""
def var({_name, meta, _context}, context) do
version = Keyword.fetch!(meta, :version)
%{vars: %{^version => %{type: type}}} = context
type
end
@doc """
Marks a variable with error.
"""
def error_var(var, context) do
{var_name, meta, var_context} = var
version = Keyword.fetch!(meta, :version)
data = %{
type: error_type(),
name: var_name,
context: var_context,
off_traces: []
}
put_in(context.vars[version], data)
end
@doc """
Refines the type of a variable.
"""
def refine_var(var, type, expr, formatter \\ :default, stack, context) do
{var_name, meta, var_context} = var
version = Keyword.fetch!(meta, :version)
case context.vars do
%{^version => %{type: old_type, off_traces: off_traces} = data} ->
new_type = intersection(type, old_type)
data = %{
data
| type: new_type,
off_traces: new_trace(expr, type, formatter, stack, off_traces)
}
context = put_in(context.vars[version], data)
# We need to return error otherwise it leads to cascading errors
if empty?(new_type) do
{:error, error_type(),
error({:refine_var, old_type, type, var, context}, meta, stack, context)}
else
{:ok, new_type, context}
def open_map(args, stack, context, of_fun) do
with {:ok, pairs, context} <- map_pairs(args, stack, context, of_fun) do
# If we match on a map such as %{"foo" => "bar"}, we cannot
# assert that %{binary() => binary()}, since we are matching
# only a single binary of infinite possible values. Therefore,
# the correct would be to match it to %{binary() => binary() | var}.
#
# We can skip this in two cases:
#
# 1. If the key is a singleton, then we know that it has no
# other value than the current one
#
# 2. If the value is a variable, then there is no benefit in
# creating another variable, so we can skip it
#
# For now, we skip generating the var itself and introduce
# :dynamic instead.
pairs =
for {key, value} <- pairs, not has_unbound_var?(key, context) do
if singleton?(key, context) or match?({:var, _}, value) do
{key, value}
else
{key, to_union([value, :dynamic], context)}
end
end
%{} ->
data = %{
type: type,
name: var_name,
context: var_context,
off_traces: new_trace(expr, type, formatter, stack, [])
}
context = put_in(context.vars[version], data)
{:ok, type, context}
end
end
defp new_trace(nil, _type, _formatter, _stack, traces),
do: traces
defp new_trace(expr, type, formatter, stack, traces),
do: [{expr, stack.file, type, formatter} | traces]
## Map/structs
@doc """
Handles fetching a map key.
"""
def map_fetch(expr, type, field, stack, context) when is_atom(field) do
case map_fetch(type, field) do
{_optional?, value_type} ->
{value_type, context}
reason ->
{error_type(), error({reason, expr, type, field, context}, elem(expr, 1), stack, context)}
triplets = pairs_to_unions(pairs, [], context) ++ [{:optional, :dynamic, :dynamic}]
{:ok, {:map, triplets}, context}
end
end
@doc """
Builds a closed map.
Handles closed maps (without dynamic => dynamic).
"""
def closed_map(pairs, stack, context, of_fun) do
permutate_map(pairs, stack, context, of_fun, fn fallback, _keys, pairs ->
# TODO: Use the fallback type to actually indicate if open or closed.
if fallback == none(), do: closed_map(pairs), else: open_map(pairs)
def closed_map(args, stack, context, of_fun) do
with {:ok, pairs, context} <- map_pairs(args, stack, context, of_fun) do
{:ok, {:map, closed_to_unions(pairs, context)}, context}
end
end
defp map_pairs(pairs, stack, context, of_fun) do
map_reduce_ok(pairs, context, fn {key, value}, context ->
with {:ok, key_type, context} <- of_fun.(key, :dynamic, stack, context),
{:ok, value_type, context} <- of_fun.(value, :dynamic, stack, context),
do: {:ok, {key_type, value_type}, context}
end)
end
@doc """
Builds permutation of maps according to the given keys.
"""
def permutate_map(pairs, %{mode: :traversal} = stack, context, of_fun, _of_map) do
context =
Enum.reduce(pairs, context, fn {key, value}, context ->
{_, context} = of_fun.(key, stack, context)
{_, context} = of_fun.(value, stack, context)
context
end)
defp closed_to_unions([{key, value}], _context), do: [{:required, key, value}]
{dynamic(), context}
defp closed_to_unions(pairs, context) do
case Enum.split_with(pairs, fn {key, _value} -> has_unbound_var?(key, context) end) do
{[], pairs} -> pairs_to_unions(pairs, [], context)
{[_ | _], pairs} -> pairs_to_unions([{:dynamic, :dynamic} | pairs], [], context)
end
end
def permutate_map(pairs, stack, context, of_fun, of_map) do
{dynamic?, fallback, single, multiple, assert, context} =
Enum.reduce(pairs, {false, none(), [], [], [], context}, fn
{key, value}, {dynamic?, fallback, single, multiple, assert, context} ->
{dynamic_key?, keys, context} = finite_key_type(key, stack, context, of_fun)
{value_type, context} = of_fun.(value, stack, context)
dynamic? = dynamic? or dynamic_key? or gradual?(value_type)
defp pairs_to_unions([{key, value} | ahead], behind, context) do
{matched_ahead, values} = find_matching_values(ahead, key, [], [])
case keys do
:none ->
fallback = union(fallback, value_type)
# In case nothing matches, use the original ahead
ahead = matched_ahead || ahead
{fallback, assert} =
Enum.reduce(single, {fallback, assert}, fn {key, type}, {fallback, assert} ->
{union(fallback, type), [key | assert]}
end)
all_values =
[value | values] ++
find_subtype_values(ahead, key, context) ++
find_subtype_values(behind, key, context)
{fallback, assert} =
Enum.reduce(multiple, {fallback, assert}, fn {keys, type}, {fallback, assert} ->
{union(fallback, type), keys ++ assert}
end)
pairs_to_unions(ahead, [{key, to_union(all_values, context)} | behind], context)
end
{dynamic?, fallback, [], [], assert, context}
defp pairs_to_unions([], acc, context) do
acc
|> Enum.sort(&subtype?(elem(&1, 0), elem(&2, 0), context))
|> Enum.map(fn {key, value} -> {:required, key, value} end)
end
[key] when multiple == [] ->
{dynamic?, fallback, [{key, value_type} | single], multiple, assert, context}
defp find_subtype_values(pairs, key, context) do
for {pair_key, pair_value} <- pairs, subtype?(pair_key, key, context), do: pair_value
end
keys ->
{dynamic?, fallback, single, [{keys, value_type} | multiple], assert, context}
end
end)
defp find_matching_values([{key, value} | ahead], key, acc, values) do
find_matching_values(ahead, key, acc, [value | values])
end
map =
case Enum.reverse(multiple) do
[] ->
of_map.(fallback, Enum.uniq(assert), Enum.reverse(single))
defp find_matching_values([{_, _} = pair | ahead], key, acc, values) do
find_matching_values(ahead, key, [pair | acc], values)
end
[{keys, type} | tail] ->
for key <- keys, t <- cartesian_map(tail) do
of_map.(fallback, Enum.uniq(assert), Enum.reverse(single, [{key, type} | t]))
end
|> Enum.reduce(&union/2)
defp find_matching_values([], _key, acc, [_ | _] = values), do: {Enum.reverse(acc), values}
defp find_matching_values([], _key, _acc, []), do: {nil, []}
@doc """
Handles structs.
"""
def struct(struct, meta, context) do
context = remote(struct, :__struct__, 0, meta, context)
entries =
for key <- Map.keys(struct.__struct__()), key != :__struct__ do
{:required, {:atom, key}, :dynamic}
end
if dynamic?, do: {dynamic(map), context}, else: {map, context}
end
defp finite_key_type(key, _stack, context, _of_fun) when is_atom(key) do
{false, [key], context}
end
defp finite_key_type(key, stack, context, of_fun) do
{key_type, context} = of_fun.(key, stack, context)
case atom_fetch(key_type) do
{:finite, list} -> {gradual?(key_type), list, context}
_ -> {gradual?(key_type), :none, context}
end
end
defp cartesian_map(lists) do
case lists do
[] ->
[[]]
[{keys, type} | tail] ->
for key <- keys, t <- cartesian_map(tail), do: [{key, type} | t]
end
end
@doc """
Handles instantiation of a new struct.
"""
def struct_instance(struct, args, meta, stack, context, of_fun)
when is_atom(struct) do
# The compiler has already checked the keys are atoms and which ones are required.
{args_types, context} =
Enum.map_reduce(args, context, fn {key, value}, context when is_atom(key) ->
{type, context} = of_fun.(value, stack, context)
{{key, type}, context}
end)
{info, context} = struct_info(struct, meta, stack, context)
{struct_type(struct, info, args_types), context}
end
@doc """
Returns `__info__(:struct)` information about a struct.
"""
def struct_info(struct, meta, stack, context) do
case stack.no_warn_undefined do
%Macro.Env{} = env ->
case :elixir_map.maybe_load_struct_info(meta, struct, [], false, env) do
{:ok, info} -> {info, context}
{:error, desc} -> raise ArgumentError, List.to_string(:elixir_map.format_error(desc))
end
_ ->
# Fetch the signature to validate for warnings.
{_, context} = Module.Types.Apply.signature(struct, :__struct__, 0, meta, stack, context)
info =
struct.__info__(:struct) ||
raise "expected #{inspect(struct)} to return struct metadata, but got none"
{info, context}
end
end
@doc """
Builds a type from the struct info.
"""
def struct_type(struct, info, args_types \\ []) do
term = term()
pairs = for %{field: field} <- info, do: {field, term}
pairs = [{:__struct__, atom([struct])} | pairs]
pairs = if args_types == [], do: pairs, else: pairs ++ args_types
closed_map(pairs)
{:ok, {:map, [{:required, {:atom, :__struct__}, {:atom, struct}} | entries]}, context}
end
## Binary
@@ -254,262 +143,223 @@ defmodule Module.Types.Of do
In the stack, we add nodes such as <<expr>>, <<..., expr>>, etc,
based on the position of the expression within the binary.
"""
def binary([], _kind, _stack, context) do
context
def binary([], _stack, context, _of_fun) do
{:ok, context}
end
def binary([head], kind, stack, context) do
binary_segment(head, kind, [head], stack, context)
def binary([head], stack, context, of_fun) do
head_stack = push_expr_stack({:<<>>, get_meta(head), [head]}, stack)
binary_segment(head, head_stack, context, of_fun)
end
def binary([head | tail], kind, stack, context) do
context = binary_segment(head, kind, [head, @suffix], stack, context)
binary_many(tail, kind, stack, context)
end
def binary([head | tail], stack, context, of_fun) do
head_stack = push_expr_stack({:<<>>, get_meta(head), [head, @suffix]}, stack)
defp binary_many([last], kind, stack, context) do
binary_segment(last, kind, [@prefix, last], stack, context)
end
defp binary_many([head | tail], kind, stack, context) do
context = binary_segment(head, kind, [@prefix, head, @suffix], stack, context)
binary_many(tail, kind, stack, context)
end
# If the segment is a literal, the compiler has already checked its validity,
# so we just skip it.
defp binary_segment({:"::", _meta, [left, _right]}, _kind, _args, _stack, context)
when is_binary(left) or is_number(left) do
context
end
defp binary_segment({:"::", meta, [left, right]}, kind, args, stack, context) do
type = specifier_type(kind, right)
expr = {:<<>>, meta, args}
{_type, context} =
case kind do
:match ->
Module.Types.Pattern.of_match_var(left, type, expr, stack, context)
:guard ->
Module.Types.Pattern.of_guard(left, type, expr, stack, context)
:expr ->
{actual, context} = Module.Types.Expr.of_expr(left, stack, context)
intersect(actual, type, expr, stack, context)
end
specifier_size(kind, right, stack, context)
end
defp specifier_type(kind, {:-, _, [left, _right]}), do: specifier_type(kind, left)
defp specifier_type(:match, {:utf8, _, _}), do: @integer
defp specifier_type(:match, {:utf16, _, _}), do: @integer
defp specifier_type(:match, {:utf32, _, _}), do: @integer
defp specifier_type(:match, {:float, _, _}), do: @float
defp specifier_type(_kind, {:float, _, _}), do: @integer_or_float
defp specifier_type(_kind, {:utf8, _, _}), do: @integer_or_binary
defp specifier_type(_kind, {:utf16, _, _}), do: @integer_or_binary
defp specifier_type(_kind, {:utf32, _, _}), do: @integer_or_binary
defp specifier_type(_kind, {:integer, _, _}), do: @integer
defp specifier_type(_kind, {:bits, _, _}), do: @binary
defp specifier_type(_kind, {:bitstring, _, _}), do: @binary
defp specifier_type(_kind, {:bytes, _, _}), do: @binary
defp specifier_type(_kind, {:binary, _, _}), do: @binary
defp specifier_type(_kind, _specifier), do: @integer
defp specifier_size(kind, {:-, _, [left, right]}, stack, context) do
specifier_size(kind, right, stack, specifier_size(kind, left, stack, context))
end
defp specifier_size(:expr, {:size, _, [arg]} = expr, stack, context)
when not is_integer(arg) do
{actual, context} = Module.Types.Expr.of_expr(arg, stack, context)
{_, context} = intersect(actual, integer(), expr, stack, context)
context
end
defp specifier_size(_pattern_or_guard, {:size, _, [arg]} = expr, stack, context)
when not is_integer(arg) do
{_type, context} = Module.Types.Pattern.of_guard(arg, integer(), expr, stack, context)
context
end
defp specifier_size(_kind, _specifier, _stack, context) do
context
end
## Modules
@doc """
Returns modules in a type.
The call information is used on report reporting.
"""
def modules(type, fun, arity, hints \\ [], expr, meta, stack, context) do
case atom_fetch(type) do
{_, mods} ->
{mods, context}
:error ->
warning = {:badmodule, expr, type, fun, arity, hints, context}
{[], error(warning, meta, stack, context)}
case binary_segment(head, head_stack, context, of_fun) do
{:ok, context} -> binary_many(tail, stack, context, of_fun)
{:error, reason} -> {:error, reason}
end
end
## Warning helpers
defp binary_many([last], stack, context, of_fun) do
last_stack = push_expr_stack({:<<>>, get_meta(last), [@prefix, last]}, stack)
binary_segment(last, last_stack, context, of_fun)
end
defp binary_many([head | tail], stack, context, of_fun) do
head_stack = push_expr_stack({:<<>>, get_meta(head), [@prefix, head, @suffix]}, stack)
case binary_segment(head, head_stack, context, of_fun) do
{:ok, context} -> binary_many(tail, stack, context, of_fun)
{:error, reason} -> {:error, reason}
end
end
defp binary_segment({:"::", _meta, [expr, specifiers]}, stack, context, of_fun) do
expected_type =
collect_binary_specifier(specifiers, &binary_type(stack.context, &1)) || :integer
utf? = collect_binary_specifier(specifiers, &utf_type?/1)
float? = collect_binary_specifier(specifiers, &float_type?/1)
# Special case utf and float specifiers because they can be two types as literals
# but only a specific type as a variable in a pattern
cond do
stack.context == :pattern and utf? and is_binary(expr) ->
{:ok, context}
stack.context == :pattern and float? and is_integer(expr) ->
{:ok, context}
true ->
with {:ok, type, context} <- of_fun.(expr, expected_type, stack, context),
{:ok, _type, context} <- unify(type, expected_type, stack, context),
do: {:ok, context}
end
end
# Collect binary type specifiers,
# from `<<pattern::integer-size(10)>>` collect `integer`
defp collect_binary_specifier({:-, _meta, [left, right]}, fun) do
collect_binary_specifier(left, fun) || collect_binary_specifier(right, fun)
end
defp collect_binary_specifier(other, fun) do
fun.(other)
end
defp binary_type(:expr, {:float, _, _}), do: {:union, [:integer, :float]}
defp binary_type(:expr, {:utf8, _, _}), do: {:union, [:integer, :binary]}
defp binary_type(:expr, {:utf16, _, _}), do: {:union, [:integer, :binary]}
defp binary_type(:expr, {:utf32, _, _}), do: {:union, [:integer, :binary]}
defp binary_type(:pattern, {:utf8, _, _}), do: :integer
defp binary_type(:pattern, {:utf16, _, _}), do: :integer
defp binary_type(:pattern, {:utf32, _, _}), do: :integer
defp binary_type(:pattern, {:float, _, _}), do: :float
defp binary_type(_context, {:integer, _, _}), do: :integer
defp binary_type(_context, {:bits, _, _}), do: :binary
defp binary_type(_context, {:bitstring, _, _}), do: :binary
defp binary_type(_context, {:bytes, _, _}), do: :binary
defp binary_type(_context, {:binary, _, _}), do: :binary
defp binary_type(_context, _specifier), do: nil
defp utf_type?({specifier, _, _}), do: specifier in [:utf8, :utf16, :utf32]
defp utf_type?(_), do: false
defp float_type?({:float, _, _}), do: true
defp float_type?(_), do: false
## Remote
@doc """
Intersects two types and emit an incompatible error if empty.
Handles remote calls.
"""
def intersect(actual, expected, expr, stack, context) do
type = intersection(actual, expected)
if empty?(type) do
{error_type(), incompatible_error(expr, expected, actual, stack, context)}
def remote(module, fun, arity, meta, context) when is_atom(module) do
# TODO: In the future we may want to warn for modules defined
# in the local context
if Keyword.get(meta, :context_module, false) do
context
else
{type, context}
ParallelChecker.preload_module(context.cache, module)
check_export(module, fun, arity, meta, context)
end
end
@doc """
Emits incompatible types warning for the given expression.
def remote(_module, _fun, _arity, _meta, context), do: context
This is a generic warning for when the expected/actual types
themselves may come from several different circumstances.
"""
def incompatible_error(expr, expected_type, actual_type, stack, context) do
meta = get_meta(expr) || stack.meta
hints = if meta[:inferred_bitstring_spec], do: [:inferred_bitstring_spec], else: []
warning = {:incompatible, expr, expected_type, actual_type, hints, context}
error(warning, meta, stack, context)
defp check_export(module, fun, arity, meta, context) do
case ParallelChecker.fetch_export(context.cache, module, fun, arity) do
{:ok, mode, :def, reason} ->
check_deprecated(mode, module, fun, arity, reason, meta, context)
{:ok, mode, :defmacro, reason} ->
context = warn(meta, context, {:unrequired_module, module, fun, arity})
check_deprecated(mode, module, fun, arity, reason, meta, context)
{:error, :module} ->
if warn_undefined?(module, fun, arity, context) do
warn(meta, context, {:undefined_module, module, fun, arity})
else
context
end
{:error, :function} ->
if warn_undefined?(module, fun, arity, context) do
exports = ParallelChecker.all_exports(context.cache, module)
warn(meta, context, {:undefined_function, module, fun, arity, exports})
else
context
end
end
end
defp error(warning, meta, stack, context) do
error(__MODULE__, warning, meta, stack, context)
defp check_deprecated(:elixir, module, fun, arity, reason, meta, context) do
if reason do
warn(meta, context, {:deprecated, module, fun, arity, reason})
else
context
end
end
defp check_deprecated(:erlang, module, fun, arity, _reason, meta, context) do
case :otp_internal.obsolete(module, fun, arity) do
{:deprecated, string} when is_list(string) ->
reason = string |> List.to_string() |> String.capitalize()
warn(meta, context, {:deprecated, module, fun, arity, reason})
{:deprecated, string, removal} when is_list(string) and is_list(removal) ->
reason = string |> List.to_string() |> String.capitalize()
reason = "It will be removed in #{removal}. #{reason}"
warn(meta, context, {:deprecated, module, fun, arity, reason})
_ ->
context
end
end
# The protocol code dispatches to unknown modules, so we ignore them here.
#
# try do
# SomeProtocol.Atom.__impl__
# rescue
# ...
# end
#
# But for protocols we don't want to traverse the protocol code anyway.
# TODO: remove this clause once we no longer traverse the protocol code.
defp warn_undefined?(_module, :__impl__, 1, _context), do: false
defp warn_undefined?(_module, :module_info, 0, _context), do: false
defp warn_undefined?(_module, :module_info, 1, _context), do: false
defp warn_undefined?(:erlang, :orelse, 2, _context), do: false
defp warn_undefined?(:erlang, :andalso, 2, _context), do: false
defp warn_undefined?(_, _, _, %{no_warn_undefined: :all}) do
false
end
defp warn_undefined?(module, fun, arity, context) do
not Enum.any?(context.no_warn_undefined, &(&1 == module or &1 == {module, fun, arity}))
end
defp warn(meta, context, warning) do
{fun, arity} = context.function
location = {context.file, meta[:line] || 0, {context.module, fun, arity}}
%{context | warnings: [{__MODULE__, warning, location} | context.warnings]}
end
## Warning formatting
def format_diagnostic({:refine_var, old_type, new_type, var, context}) do
traces = collect_traces(var, context)
%{
details: %{typing_traces: traces},
message:
IO.iodata_to_binary([
"""
incompatible types assigned to #{format_var(var)}:
#{to_quoted_string(old_type)} !~ #{to_quoted_string(new_type)}
""",
format_traces(traces)
])
}
def format_warning({:undefined_module, module, fun, arity}) do
[
Exception.format_mfa(module, fun, arity),
" is undefined (module ",
inspect(module),
" is not available or is yet to be defined)"
]
end
def format_diagnostic({:incompatible, expr, expected_type, actual_type, hints, context}) do
traces = collect_traces(expr, context)
%{
details: %{typing_traces: traces},
message:
IO.iodata_to_binary([
"""
incompatible types in expression:
#{expr_to_string(expr) |> indent(4)}
got type:
#{to_quoted_string(actual_type) |> indent(4)}
but expected type:
#{to_quoted_string(expected_type) |> indent(4)}
""",
format_traces(traces),
format_hints(hints)
])
}
def format_warning({:undefined_function, module, fun, arity, exports}) do
[
Exception.format_mfa(module, fun, arity),
" is undefined or private",
UndefinedFunctionError.hint_for_loaded_module(module, fun, arity, exports)
]
end
def format_diagnostic({:badmodule, expr, type, fun, arity, hints, context}) do
traces = collect_traces(expr, context)
%{
details: %{typing_traces: traces},
message:
IO.iodata_to_binary([
"""
expected a module (an atom) when invoking #{fun}/#{arity} in expression:
#{expr_to_string(expr) |> indent(4)}
""",
empty_if(dot_var?(expr), """
but got type:
#{to_quoted_string(type) |> indent(4)}
"""),
format_traces(traces),
format_hints(hints)
])
}
def format_warning({:deprecated, module, fun, arity, reason}) do
[
Exception.format_mfa(module, fun, arity),
" is deprecated. ",
reason
]
end
def format_diagnostic({:badmap, expr, type, key, context}) do
traces = collect_traces(expr, context)
%{
details: %{typing_traces: traces},
message:
IO.iodata_to_binary([
"""
expected a map or struct when accessing .#{key} in expression:
#{expr_to_string(expr) |> indent(4)}
""",
empty_if(dot_var?(expr), """
but got type:
#{to_quoted_string(type) |> indent(4)}
"""),
format_traces(traces),
format_hints([:dot])
])
}
end
def format_diagnostic({:badkey, expr, type, key, context}) do
traces = collect_traces(expr, context)
%{
details: %{typing_traces: traces},
span: expr |> get_meta() |> :elixir_env.calculate_span(key) |> Keyword.get(:span),
message:
IO.iodata_to_binary([
"""
unknown key .#{key} in expression:
#{expr_to_string(expr) |> indent(4)}
the given type does not have the given key:
#{to_quoted_string(type) |> indent(4)}
""",
format_traces(traces)
])
}
end
defp dot_var?(expr) do
match?({{:., _, [var, _fun]}, _, _args} when is_var(var), expr)
end
defp empty_if(condition, content) do
if condition, do: "", else: content
def format_warning({:unrequired_module, module, fun, arity}) do
[
"you must require ",
inspect(module),
" before invoking the macro ",
Exception.format_mfa(module, fun, arity)
]
end
end
File diff suppressed because it is too large Load Diff
+992
View File
@@ -0,0 +1,992 @@
defmodule Module.Types.Unify do
@moduledoc false
import Module.Types.Helpers
# Those are the simple types known to the system:
#
# :dynamic
# {:var, var}
# {:atom, atom} < :atom
# :integer
# :float
# :binary
# :pid
# :port
# :reference
#
# Those are the composite types:
#
# {:list, type}
# {:tuple, size, [type]} < :tuple
# {:union, [type]}
# {:map, [{:required | :optional, key_type, value_type}]}
# {:fun, [{params, return}]}
#
# Once new types are added, they should be considered in:
#
# * unify (all)
# * format_type (all)
# * subtype? (subtypes only)
# * recursive_type? (composite only)
# * collect_var_indexes (composite only)
# * lift_types (composite only)
# * flatten_union (composite only)
# * walk (composite only)
#
@doc """
Unifies two types and returns the unified type and an updated typing context
or an error in case of a typing conflict.
"""
def unify(same, same, _stack, context) do
{:ok, same, context}
end
def unify({:var, var}, type, stack, context) do
unify_var(var, type, stack, context, _var_source = true)
end
def unify(type, {:var, var}, stack, context) do
unify_var(var, type, stack, context, _var_source = false)
end
def unify({:tuple, n, sources}, {:tuple, n, targets}, stack, context) do
result =
map_reduce_ok(Enum.zip(sources, targets), context, fn {source, target}, context ->
unify(source, target, stack, context)
end)
case result do
{:ok, types, context} -> {:ok, {:tuple, n, types}, context}
{:error, reason} -> {:error, reason}
end
end
def unify({:list, source}, {:list, target}, stack, context) do
case unify(source, target, stack, context) do
{:ok, type, context} -> {:ok, {:list, type}, context}
{:error, reason} -> {:error, reason}
end
end
def unify({:map, source_pairs}, {:map, target_pairs}, stack, context) do
unify_maps(source_pairs, target_pairs, stack, context)
end
def unify(source, :dynamic, _stack, context) do
{:ok, source, context}
end
def unify(:dynamic, target, _stack, context) do
{:ok, target, context}
end
def unify({:union, types}, target, stack, context) do
unify_result =
map_reduce_ok(types, context, fn type, context ->
unify(type, target, stack, context)
end)
case unify_result do
{:ok, types, context} -> {:ok, to_union(types, context), context}
{:error, context} -> {:error, context}
end
end
def unify(source, target, stack, context) do
cond do
# TODO: This condition exists to handle unions with unbound vars.
match?({:union, _}, target) and has_unbound_var?(target, context) ->
{:ok, source, context}
subtype?(source, target, context) ->
{:ok, source, context}
true ->
error(:unable_unify, {source, target, stack}, context)
end
end
def unify_var(var, :dynamic, _stack, context, _var_source?) do
{:ok, {:var, var}, context}
end
def unify_var(var, type, stack, context, var_source?) do
case context.types do
%{^var => :unbound} ->
context = refine_var!(var, type, stack, context)
stack = push_unify_stack(var, stack)
if recursive_type?(type, [], context) do
if var_source? do
error(:unable_unify, {{:var, var}, type, stack}, context)
else
error(:unable_unify, {type, {:var, var}, stack}, context)
end
else
{:ok, {:var, var}, context}
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"
context =
if variable_expanded?(var, stack, context) do
context
else
trace_var(var, type, stack, context)
end
stack = push_unify_stack(var, stack)
unify_result =
if var_source? do
unify(var_type, type, stack, context)
else
unify(type, var_type, stack, context)
end
case unify_result do
{:ok, {:var, ^var}, context} ->
{:ok, {:var, var}, context}
{:ok, res_type, context} ->
context = refine_var!(var, res_type, stack, context)
{:ok, {:var, var}, context}
{:error, reason} ->
{:error, reason}
end
end
end
# * All required keys on each side need to match to the other side.
# * All optional keys on each side that do not match must be discarded.
def unify_maps(source_pairs, target_pairs, stack, context) do
{source_required, source_optional} = split_pairs(source_pairs)
{target_required, target_optional} = split_pairs(target_pairs)
with {:ok, source_required_pairs, context} <-
unify_source_required(source_required, target_pairs, stack, context),
{:ok, target_required_pairs, context} <-
unify_target_required(target_required, source_pairs, stack, context),
{:ok, source_optional_pairs, context} <-
unify_source_optional(source_optional, target_optional, stack, context),
{:ok, target_optional_pairs, context} <-
unify_target_optional(target_optional, source_optional, stack, context) do
# Remove duplicate pairs from matching in both left and right directions
pairs =
Enum.uniq(
source_required_pairs ++
target_required_pairs ++
source_optional_pairs ++
target_optional_pairs
)
{:ok, {:map, pairs}, context}
else
{:error, :unify} ->
error(:unable_unify, {{:map, source_pairs}, {:map, target_pairs}, stack}, context)
{:error, context} ->
{:error, context}
end
end
def unify_source_required(source_required, target_pairs, stack, context) do
map_reduce_ok(source_required, context, fn {source_key, source_value}, context ->
Enum.find_value(target_pairs, fn {target_kind, target_key, target_value} ->
with {:ok, key, context} <- unify(source_key, target_key, stack, context) do
case unify(source_value, target_value, stack, context) do
{:ok, value, context} ->
{:ok, {:required, key, value}, context}
{:error, _reason} ->
source_map = {:map, [{:required, source_key, source_value}]}
target_map = {:map, [{target_kind, target_key, target_value}]}
error(:unable_unify, {source_map, target_map, stack}, context)
end
else
{:error, _reason} -> nil
end
end) || {:error, :unify}
end)
end
def unify_target_required(target_required, source_pairs, stack, context) do
map_reduce_ok(target_required, context, fn {target_key, target_value}, context ->
Enum.find_value(source_pairs, fn {source_kind, source_key, source_value} ->
with {:ok, key, context} <- unify(source_key, target_key, stack, context) do
case unify(source_value, target_value, stack, context) do
{:ok, value, context} ->
{:ok, {:required, key, value}, context}
{:error, _reason} ->
source_map = {:map, [{source_kind, source_key, source_value}]}
target_map = {:map, [{:required, target_key, target_value}]}
error(:unable_unify, {source_map, target_map, stack}, context)
end
else
{:error, _reason} -> nil
end
end) || {:error, :unify}
end)
end
def unify_source_optional(source_optional, target_optional, stack, context) do
flat_map_reduce_ok(source_optional, context, fn {source_key, source_value}, context ->
Enum.find_value(target_optional, fn {target_key, target_value} ->
with {:ok, key, context} <- unify(source_key, target_key, stack, context) do
case unify(source_value, target_value, stack, context) do
{:ok, value, context} ->
{:ok, [{:optional, key, value}], context}
{:error, _reason} ->
source_map = {:map, [{:optional, source_key, source_value}]}
target_map = {:map, [{:optional, target_key, target_value}]}
error(:unable_unify, {source_map, target_map, stack}, context)
end
else
_ -> nil
end
end) || {:ok, [], context}
end)
end
def unify_target_optional(target_optional, source_optional, stack, context) do
flat_map_reduce_ok(target_optional, context, fn {target_key, target_value}, context ->
Enum.find_value(source_optional, fn {source_key, source_value} ->
with {:ok, key, context} <- unify(source_key, target_key, stack, context) do
case unify(source_value, target_value, stack, context) do
{:ok, value, context} ->
{:ok, [{:optional, key, value}], context}
{:error, _reason} ->
source_map = {:map, [{:optional, source_key, source_value}]}
target_map = {:map, [{:optional, target_key, target_value}]}
error(:unable_unify, {source_map, target_map, stack}, context)
end
else
_ -> nil
end
end) || {:ok, [], context}
end)
end
defp split_pairs(pairs) do
{required, optional} =
Enum.split_with(pairs, fn {kind, _key, _value} -> kind == :required end)
required = Enum.map(required, fn {_kind, key, value} -> {key, value} end)
optional = Enum.map(optional, fn {_kind, key, value} -> {key, value} end)
{required, optional}
end
def error(type, reason, context), do: {:error, {type, reason, context}}
@doc """
Push expression to stack.
The expression stack is used to give the context where a type variable
was refined when show a type conflict error.
"""
def push_expr_stack(expr, stack) do
%{stack | last_expr: expr}
end
@doc """
Gets a variable.
"""
def get_var!(var, context) do
Map.fetch!(context.vars, var_name(var))
end
@doc """
Adds a variable to the typing context and returns its type variable.
If the variable has already been added, return the existing type variable.
"""
def new_var(var, context) do
var_name = var_name(var)
case context.vars do
%{^var_name => type} ->
{type, context}
%{} ->
type = {:var, context.counter}
vars = Map.put(context.vars, var_name, type)
types_to_vars = Map.put(context.types_to_vars, context.counter, var)
types = Map.put(context.types, context.counter, :unbound)
traces = Map.put(context.traces, context.counter, [])
context = %{
context
| vars: vars,
types_to_vars: types_to_vars,
types: types,
traces: traces,
counter: context.counter + 1
}
{type, context}
end
end
@doc """
Adds an internal variable to the typing context and returns its type variable.
An internal variable is used to help unify complex expressions,
it does not belong to a specific AST expression.
"""
def add_var(context) do
type = {:var, context.counter}
types = Map.put(context.types, context.counter, :unbound)
traces = Map.put(context.traces, context.counter, [])
context = %{
context
| types: types,
traces: traces,
counter: context.counter + 1
}
{type, context}
end
@doc """
Maybe resolves a variable.
"""
def resolve_var({:var, var}, context) do
case context.types do
%{^var => :unbound} -> {:var, var}
%{^var => type} -> resolve_var(type, context)
end
end
def resolve_var(other, _context), do: other
# Check unify stack to see if variable was already expanded
defp variable_expanded?(var, stack, context) do
Enum.any?(stack.unify_stack, &variable_same?(var, &1, context))
end
defp variable_same?(left, right, context) do
case context.types do
%{^left => {:var, new_left}} ->
variable_same?(new_left, right, context)
%{^right => {:var, new_right}} ->
variable_same?(left, new_right, context)
%{} ->
false
end
end
defp push_unify_stack(var, stack) do
%{stack | unify_stack: [var | stack.unify_stack]}
end
@doc """
Restores the variable information from the old context into new context.
"""
def restore_var!(var, new_context, old_context) do
%{^var => type} = old_context.types
%{^var => trace} = old_context.traces
types = Map.put(new_context.types, var, type)
traces = Map.put(new_context.traces, var, trace)
%{new_context | types: types, traces: traces}
end
@doc """
Set the type for a variable and add trace.
"""
def refine_var!(var, type, stack, context) do
types = Map.put(context.types, var, type)
context = %{context | types: types}
trace_var(var, type, stack, context)
end
@doc """
Remove type variable and all its traces.
"""
def remove_var(var, context) do
types = Map.delete(context.types, var)
traces = Map.delete(context.traces, var)
%{context | types: types, traces: traces}
end
defp trace_var(var, type, %{trace: true, last_expr: last_expr} = _stack, context) do
line = get_meta(last_expr)[:line]
trace = {type, last_expr, {context.file, line}}
traces = Map.update!(context.traces, var, &[trace | &1])
%{context | traces: traces}
end
defp trace_var(_var, _type, %{trace: false} = _stack, context) do
context
end
# Check if a variable is recursive and incompatible with itself
# Bad: `{var} = var`
# Good: `x = y; y = z; z = x`
defp recursive_type?({:var, var} = parent, parents, context) do
case context.types do
%{^var => :unbound} ->
false
%{^var => type} ->
if type in parents do
not Enum.all?(parents, &match?({:var, _}, &1))
else
recursive_type?(type, [parent | parents], context)
end
end
end
defp recursive_type?({:list, type} = parent, parents, context) do
recursive_type?(type, [parent | parents], context)
end
defp recursive_type?({:union, types} = parent, parents, context) do
Enum.any?(types, &recursive_type?(&1, [parent | parents], context))
end
defp recursive_type?({:tuple, _, types} = parent, parents, context) do
Enum.any?(types, &recursive_type?(&1, [parent | parents], context))
end
defp recursive_type?({:map, pairs} = parent, parents, context) do
Enum.any?(pairs, fn {_kind, key, value} ->
recursive_type?(key, [parent | parents], context) or
recursive_type?(value, [parent | parents], context)
end)
end
defp recursive_type?({:fun, clauses}, parents, context) do
Enum.any?(clauses, fn {args, return} ->
Enum.any?([return | args], &recursive_type?(&1, [clauses | parents], context))
end)
end
defp recursive_type?(_other, _parents, _context) do
false
end
@doc """
Collects all type vars recursively.
"""
def collect_var_indexes(type, context, acc \\ %{}) do
{_type, indexes} =
walk(type, acc, fn
{:var, var}, acc ->
case acc do
%{^var => _} ->
{{:var, var}, acc}
%{} ->
case context.types do
%{^var => :unbound} ->
{{:var, var}, Map.put(acc, var, true)}
%{^var => type} ->
{{:var, var}, collect_var_indexes(type, context, Map.put(acc, var, true))}
end
end
other, acc ->
{other, acc}
end)
indexes
end
@doc """
Checks if the type has a type var.
"""
def has_unbound_var?(type, context) do
walk(type, :ok, fn
{:var, var}, acc ->
case context.types do
%{^var => :unbound} ->
throw(:has_unbound_var?)
%{^var => type} ->
has_unbound_var?(type, context)
{{:var, var}, acc}
end
other, acc ->
{other, acc}
end)
false
catch
:throw, :has_unbound_var? -> true
end
@doc """
Returns true if it is a singleton type.
Only atoms are singleton types. Unbound vars are not
considered singleton types.
"""
def singleton?({:var, var}, context) do
case context.types do
%{^var => :unbound} -> false
%{^var => type} -> singleton?(type, context)
end
end
def singleton?({:atom, _}, _context), do: true
def singleton?(_type, _context), do: false
@doc """
Checks if the first argument is a subtype of the second argument.
This function assumes that:
* unbound variables are not subtype of anything
* dynamic is not considered a subtype of all other types but the top type.
This allows this function can be used for ordering, in other cases, you
may need to check for both sides
"""
def subtype?(type, type, _context), do: true
def subtype?({:var, var}, other, context) do
case context.types do
%{^var => :unbound} -> false
%{^var => type} -> subtype?(type, other, context)
end
end
def subtype?(other, {:var, var}, context) do
case context.types do
%{^var => :unbound} -> false
%{^var => type} -> subtype?(other, type, context)
end
end
def subtype?(_, :dynamic, _context), do: true
def subtype?({:atom, atom}, :atom, _context) when is_atom(atom), do: true
# Composite
def subtype?({:tuple, _, _}, :tuple, _context), do: true
def subtype?({:tuple, n, left_types}, {:tuple, n, right_types}, context) do
left_types
|> Enum.zip(right_types)
|> Enum.all?(fn {left, right} -> subtype?(left, right, context) end)
end
def subtype?({:map, left_pairs}, {:map, right_pairs}, context) do
Enum.all?(left_pairs, fn
{:required, left_key, left_value} ->
Enum.any?(right_pairs, fn {_, right_key, right_value} ->
subtype?(left_key, right_key, context) and subtype?(left_value, right_value, context)
end)
{:optional, _, _} ->
true
end)
end
def subtype?({:list, left}, {:list, right}, context) do
subtype?(left, right, context)
end
def subtype?({:union, left_types}, {:union, _} = right_union, context) do
Enum.all?(left_types, &subtype?(&1, right_union, context))
end
def subtype?(left, {:union, right_types}, context) do
Enum.any?(right_types, &subtype?(left, &1, context))
end
def subtype?({:union, left_types}, right, context) do
Enum.all?(left_types, &subtype?(&1, right, context))
end
def subtype?(_left, _right, _context), do: false
@doc """
Returns a "simplified" union using `subtype?/3` to remove redundant types.
Due to limitations in `subtype?/3` some overlapping types may still be
included. For example unions with overlapping non-concrete types such as
`{boolean()} | {atom()}` will not be merged or types with variables that
are distinct but equivalent such as `a | b when a ~ b`.
"""
def to_union([type], _context), do: type
def to_union(types, context) when types != [] do
case unique_super_types(unnest_unions(types), context) do
[type] -> type
types -> {:union, types}
end
end
defp unnest_unions(types) do
Enum.flat_map(types, fn
{:union, types} -> unnest_unions(types)
type -> [type]
end)
end
# Filter subtypes
#
# `boolean() | atom()` => `atom()`
# `:foo | atom()` => `atom()`
#
# Does not merge `true | false` => `boolean()`
defp unique_super_types([type | types], context) do
types = Enum.reject(types, &subtype?(&1, type, context))
if Enum.any?(types, &subtype?(type, &1, context)) do
unique_super_types(types, context)
else
[type | unique_super_types(types, context)]
end
end
defp unique_super_types([], _context) do
[]
end
## Type lifting
@doc """
Lifts type variables to their inferred types from the context.
"""
def lift_types(types, %{lifted_types: _} = context) do
Enum.map_reduce(types, context, &lift_type/2)
end
def lift_types(types, context) do
context = %{
types: context.types,
lifted_types: %{},
lifted_counter: 0
}
Enum.map_reduce(types, context, &lift_type/2)
end
# Lift type variable to its inferred (hopefully concrete) types from the context
defp lift_type({:var, var}, context) do
case context.lifted_types do
%{^var => lifted_var} ->
{{:var, lifted_var}, context}
%{} ->
case context.types do
%{^var => :unbound} ->
new_lifted_var(var, context)
%{^var => type} ->
if recursive_type?(type, [], context) do
new_lifted_var(var, context)
else
# Remove visited types to avoid infinite loops
# then restore after we are done recursing on vars
types = context.types
context = put_in(context.types[var], :unbound)
{type, context} = lift_type(type, context)
{type, %{context | types: types}}
end
%{} ->
new_lifted_var(var, context)
end
end
end
defp lift_type({:union, types}, context) do
{types, context} = Enum.map_reduce(types, context, &lift_type/2)
{{:union, types}, context}
end
defp lift_type({:tuple, n, types}, context) do
{types, context} = Enum.map_reduce(types, context, &lift_type/2)
{{:tuple, n, types}, context}
end
defp lift_type({:map, pairs}, context) do
{pairs, context} =
Enum.map_reduce(pairs, context, fn {kind, key, value}, context ->
{key, context} = lift_type(key, context)
{value, context} = lift_type(value, context)
{{kind, key, value}, context}
end)
{{:map, pairs}, context}
end
defp lift_type({:list, type}, context) do
{type, context} = lift_type(type, context)
{{:list, type}, context}
end
defp lift_type({:fun, clauses}, context) do
clauses =
Enum.map_reduce(clauses, context, fn {args, return}, context ->
{[return | args], context} = Enum.map_reduce([return | args], context, &lift_type/2)
{{args, return}, context}
end)
{{:fun, clauses}, context}
end
defp lift_type(other, context) do
{other, context}
end
defp new_lifted_var(original_var, context) do
types = Map.put(context.lifted_types, original_var, context.lifted_counter)
counter = context.lifted_counter + 1
type = {:var, context.lifted_counter}
context = %{context | lifted_types: types, lifted_counter: counter}
{type, context}
end
# TODO: Figure out function expansion
@doc """
Expand unions so that all unions are at the top level.
{integer() | float()} => {integer()} | {float()}
"""
def flatten_union({:union, types}, context) do
Enum.flat_map(types, &flatten_union(&1, context))
end
def flatten_union(type, context) do
List.wrap(do_flatten_union(type, context))
end
def do_flatten_union({:tuple, num, types}, context) do
flatten_union_tuple(types, num, context, [])
end
def do_flatten_union({:list, type}, context) do
case do_flatten_union(type, context) do
{:union, union_types} -> Enum.map(union_types, &{:list, &1})
_type -> [{:list, type}]
end
end
def do_flatten_union({:map, pairs}, context) do
flatten_union_map(pairs, context, [])
end
def do_flatten_union({:var, var}, context) do
if looping_var?(var, context, []) do
{:var, var}
else
case context.types do
%{^var => :unbound} -> {:var, var}
%{^var => {:union, types}} -> Enum.map(types, &do_flatten_union(&1, context))
%{^var => type} -> do_flatten_union(type, context)
end
end
end
def do_flatten_union(type, _context) do
type
end
defp flatten_union_tuple([type | types], num, context, acc) do
case do_flatten_union(type, context) do
{:union, union_types} ->
Enum.flat_map(union_types, &flatten_union_tuple(types, num, context, [&1 | acc]))
type ->
flatten_union_tuple(types, num, context, [type | acc])
end
end
defp flatten_union_tuple([], num, _context, acc) do
[{:tuple, num, Enum.reverse(acc)}]
end
defp flatten_union_map([{kind, key, value} | pairs], context, acc) do
case do_flatten_union(key, context) do
{:union, union_types} ->
Enum.flat_map(union_types, &flatten_union_map_value(kind, &1, value, pairs, context, acc))
type ->
flatten_union_map_value(kind, type, value, pairs, context, acc)
end
end
defp flatten_union_map([], _context, acc) do
[{:map, Enum.reverse(acc)}]
end
defp flatten_union_map_value(kind, key, value, pairs, context, acc) do
case do_flatten_union(value, context) do
{:union, union_types} ->
Enum.flat_map(union_types, &flatten_union_map(pairs, context, [{kind, key, &1} | acc]))
value ->
flatten_union_map(pairs, context, [{kind, key, value} | acc])
end
end
defp looping_var?(var, context, parents) do
case context.types do
%{^var => :unbound} ->
false
%{^var => {:var, type}} ->
if var in parents do
true
else
looping_var?(type, context, [var | parents])
end
%{^var => _type} ->
false
end
end
@doc """
Formats types.
The second argument says when complex types such as maps and
structs should be simplified and not shown.
"""
def format_type({:map, pairs}, true) do
case List.keyfind(pairs, {:atom, :__struct__}, 1) do
{:required, {:atom, :__struct__}, {:atom, struct}} ->
["%", inspect(struct), "{}"]
_ ->
"map()"
end
end
def format_type({:union, types}, simplify?) do
types
|> Enum.map(&format_type(&1, simplify?))
|> Enum.intersperse(" | ")
end
def format_type({:tuple, _, types}, simplify?) do
format =
types
|> Enum.map(&format_type(&1, simplify?))
|> Enum.intersperse(", ")
["{", format, "}"]
end
def format_type({:list, type}, simplify?) do
["[", format_type(type, simplify?), "]"]
end
def format_type({:map, pairs}, false) do
case List.keytake(pairs, {:atom, :__struct__}, 1) do
{{:required, {:atom, :__struct__}, {:atom, struct}}, pairs} ->
["%", inspect(struct), "{", format_map_pairs(pairs), "}"]
_ ->
["%{", format_map_pairs(pairs), "}"]
end
end
def format_type({:atom, literal}, _simplify?) do
inspect(literal)
end
def format_type({:var, index}, _simplify?) do
["var", Integer.to_string(index + 1)]
end
def format_type({:fun, clauses}, simplify?) do
format =
Enum.map(clauses, fn {params, return} ->
params = Enum.intersperse(Enum.map(params, &format_type(&1, simplify?)), ", ")
params = if params == [], do: params, else: [params, " "]
return = format_type(return, simplify?)
[params, "-> ", return]
end)
["(", Enum.intersperse(format, "; "), ")"]
end
def format_type(atom, _simplify?) when is_atom(atom) do
[Atom.to_string(atom), "()"]
end
defp format_map_pairs(pairs) do
{atoms, others} = Enum.split_with(pairs, &match?({:required, {:atom, _}, _}, &1))
{required, optional} = Enum.split_with(others, &match?({:required, _, _}, &1))
(atoms ++ required ++ optional)
|> Enum.map(fn
{:required, {:atom, atom}, right} ->
[Atom.to_string(atom), ": ", format_type(right, false)]
{:required, left, right} ->
[format_type(left, false), " => ", format_type(right, false)]
{:optional, left, right} ->
["optional(", format_type(left, false), ") => ", format_type(right, false)]
end)
|> Enum.intersperse(", ")
end
@doc """
Performs a depth-first, pre-order traversal of the type tree using an accumulator.
"""
def walk({:map, pairs}, acc, fun) do
{pairs, acc} =
Enum.map_reduce(pairs, acc, fn {kind, key, value}, acc ->
{key, acc} = walk(key, acc, fun)
{value, acc} = walk(value, acc, fun)
{{kind, key, value}, acc}
end)
fun.({:map, pairs}, acc)
end
def walk({:union, types}, acc, fun) do
{types, acc} = Enum.map_reduce(types, acc, &walk(&1, &2, fun))
fun.({:union, types}, acc)
end
def walk({:tuple, num, types}, acc, fun) do
{types, acc} = Enum.map_reduce(types, acc, &walk(&1, &2, fun))
fun.({:tuple, num, types}, acc)
end
def walk({:list, type}, acc, fun) do
{type, acc} = walk(type, acc, fun)
fun.({:list, type}, acc)
end
def walk({:fun, clauses}, acc, fun) do
{clauses, acc} =
Enum.map_reduce(clauses, acc, fn {params, return}, acc ->
{params, acc} = Enum.map_reduce(params, acc, &walk(&1, &2, fun))
{return, acc} = walk(return, acc, fun)
{{params, return}, acc}
end)
fun.({:fun, clauses}, acc)
end
def walk(type, acc, fun) do
fun.(type, acc)
end
end
+1 -1
View File
@@ -219,7 +219,7 @@ defmodule Node do
If `node` does not exist, a useless PID is returned.
For the list of available options, see `:erlang.spawn_opt/5`.
For the list of available options, see `:erlang.spawn/4`.
Inlined by the compiler.
"""
+26 -41
View File
@@ -38,12 +38,6 @@ defmodule OptionParser do
]
defmodule ParseError do
@moduledoc """
An exception raised when parsing option fails.
For example, see `OptionParser.parse!/2`.
"""
defexception [:message]
end
@@ -134,7 +128,7 @@ defmodule OptionParser do
Switches can be specified with modifiers, which change how
they behave. The following modifiers are supported:
* `:keep` - keeps duplicate elements instead of overriding them;
* `:keep` - keeps duplicated elements instead of overriding them;
works with all types except `:count`. Specifying `switch_name: :keep`
assumes the type of `:switch_name` will be `:string`.
@@ -152,47 +146,44 @@ defmodule OptionParser do
### Parsing unknown switches
When the `:switches` option is given, `OptionParser` will attempt to parse
unknown switches.
Switches without an argument will be set to `true`:
unknown switches:
iex> OptionParser.parse(["--debug"], switches: [key: :string])
{[debug: true], [], []}
Even though we haven't specified `--debug` in the list of switches, it is part
of the returned options. The same happens for switches followed by another switch:
iex> OptionParser.parse(["--debug", "--ok"], switches: [])
{[debug: true, ok: true], [], []}
Switches followed by a value will be assigned the value, as a string:
of the returned options. This would also work:
iex> OptionParser.parse(["--debug", "value"], switches: [key: :string])
{[debug: "value"], [], []}
Since we cannot assert the type of the switch value, it is preferred to use the
`:strict` option that accepts only known switches and always verify their types.
Switches followed by a value will be assigned the value, as a string. Switches
without an argument will be set automatically to `true`. Since we cannot assert
the type of the switch value, it is preferred to use the `:strict` option that
accepts only known switches and always verify their types.
If you do want to parse unknown switches, remember that Elixir converts switches
to atoms. Since atoms are not garbage-collected, to avoid creating new ones,
OptionParser by default only parses switches that translate to existing atoms.
The code below discards the `--option-parser-example` switch because the
`:option_parser_example` atom is never used anywhere:
to atoms. Since atoms are not garbage-collected, OptionParser will only parse
switches that translate to atoms used by the runtime to avoid leaking atoms.
For instance, the code below will discard the `--option-parser-example` switch
because the `:option_parser_example` atom is never used anywhere:
iex> OptionParser.parse(["--option-parser-example"], switches: [])
{[], [], []}
OptionParser.parse(["--option-parser-example"], switches: [debug: :boolean])
# The :option_parser_example atom is not used anywhere below
If a switch corresponds to an existing Elixir atom, whether from your
code, a dependency or from Elixir itself, it will be accepted. However,
it is best to not rely on external code, and always define the atoms
you want to parse in the same module that calls `OptionParser` itself,
as direct arguments to the `:switches` or `:strict` options.
However, the code below would work as long as `:option_parser_example` atom is
used at some point later (or earlier) **in the same module**. For example:
If you would like to parse all switches, regardless if they exist or not,
you can force creation of atoms by passing `allow_nonexistent_atoms: true`
as option. Use this option with care. It is only useful when you are building
command-line applications that receive dynamically-named arguments and must
be avoided in long-running systems.
{opts, _, _} = OptionParser.parse(["--option-parser-example"], switches: [debug: :boolean])
# ... then somewhere in the same module you access it ...
opts[:option_parser_example]
In other words, Elixir will only parse options that are used by the runtime,
ignoring all others. If you would like to parse all switches, regardless if
they exist or not, you can force creation of atoms by passing
`allow_nonexistent_atoms: true` as option. Use this option with care. It is
only useful when you are building command-line applications that receive
dynamically-named arguments and must be avoided in long-running systems.
## Aliases
@@ -865,13 +856,7 @@ defmodule OptionParser do
defp format_error({option, nil}, opts, types) do
if type = get_type(option, opts, types) do
if String.contains?(option, "_") do
msg = "#{option} : Unknown option"
msg <> ". Did you mean #{String.replace(option, "_", "-")}?"
else
"#{option} : Missing argument of type #{type}"
end
"#{option} : Missing argument of type #{type}"
else
msg = "#{option} : Unknown option"
+51 -118
View File
@@ -13,8 +13,6 @@ defmodule PartitionSupervisor do
`name` is the name of the `PartitionSupervisor` and key is used
for routing.
This module was introduced in Elixir v1.14.0.
## Simple Example
Let's start with an example which is not useful per se, but shows how the
@@ -31,7 +29,7 @@ defmodule PartitionSupervisor do
end
def init(args) do
IO.inspect([__MODULE__, " got args ", args, " in ", self()])
IO.inspect [__MODULE__, " got args ", args, " in ", self()]
{:ok, _initial_state = []}
end
@@ -41,7 +39,7 @@ defmodule PartitionSupervisor do
def handle_call({:collect, msg}, _from, state) do
new_state = [msg | state]
IO.inspect(["current messages:", new_state, " in process", self()])
IO.inspect ["current messages:", new_state, " in process", self()]
{:reply, :ok, new_state}
end
end
@@ -96,7 +94,7 @@ defmodule PartitionSupervisor do
DynamicSupervisor.start_child(MyApp.DynamicSupervisor, {Agent, fn -> %{} end})
You can start the dynamic supervisors under a `PartitionSupervisor`:
You can do start the dynamic supervisors under a `PartitionSupervisor`:
children = [
{PartitionSupervisor,
@@ -136,8 +134,6 @@ defmodule PartitionSupervisor do
you can use `GenServer.whereis({:via, PartitionSupervisor, {name, key}})`.
"""
@moduledoc since: "1.14.0"
@behaviour Supervisor
@registry PartitionSupervisor.Registry
@@ -145,13 +141,12 @@ defmodule PartitionSupervisor do
@typedoc """
The name of the `PartitionSupervisor`.
"""
@typedoc since: "1.14.0"
@type name :: atom() | {:via, module(), term()}
@doc false
def child_spec(opts) when is_list(opts) do
id =
case Keyword.get(opts, :name, PartitionSupervisor) do
case Keyword.get(opts, :name, DynamicSupervisor) do
name when is_atom(name) -> name
{:via, _module, name} -> name
end
@@ -228,26 +223,26 @@ defmodule PartitionSupervisor do
def start_link(opts) when is_list(opts) do
name = opts[:name]
if !name do
unless name do
raise ArgumentError, "the :name option must be given to PartitionSupervisor"
end
{child_spec, opts} = Keyword.pop(opts, :child_spec)
if !child_spec do
unless child_spec do
raise ArgumentError, "the :child_spec option must be given to PartitionSupervisor"
end
{partitions, opts} = Keyword.pop(opts, :partitions, System.schedulers_online())
if not (is_integer(partitions) and partitions >= 1) do
unless is_integer(partitions) and partitions >= 1 do
raise ArgumentError,
"the :partitions option must be a positive integer, got: #{inspect(partitions)}"
end
{with_arguments, opts} = Keyword.pop(opts, :with_arguments, fn args, _partition -> args end)
if not is_function(with_arguments, 2) do
unless is_function(with_arguments, 2) do
raise ArgumentError,
"the :with_arguments option must be a function that receives two arguments, " <>
"the current call arguments and the partition, got: #{inspect(with_arguments)}"
@@ -260,17 +255,17 @@ defmodule PartitionSupervisor do
for partition <- 0..(partitions - 1) do
args = with_arguments.(args, partition)
if not is_list(args) do
unless is_list(args) do
raise "the call to the function in :with_arguments must return a list, got: #{inspect(args)}"
end
start = {__MODULE__, :start_child, [mod, fun, args, partition]}
start = {__MODULE__, :start_child, [mod, fun, args, name, partition]}
Map.merge(map, %{id: partition, start: start, modules: modules})
end
auto_shutdown = Keyword.get(opts, :auto_shutdown, :never)
if auto_shutdown != :never do
unless auto_shutdown == :never do
raise ArgumentError,
"the :auto_shutdown option must be :never, got: #{inspect(auto_shutdown)}"
end
@@ -282,14 +277,14 @@ defmodule PartitionSupervisor do
end
@doc false
def start_child(mod, fun, args, partition) do
def start_child(mod, fun, args, name, partition) do
case apply(mod, fun, args) do
{:ok, pid} ->
register_child(partition, pid)
register_child(name, partition, pid)
{:ok, pid}
{:ok, pid, info} ->
register_child(partition, pid)
register_child(name, partition, pid)
{:ok, pid, info}
other ->
@@ -297,97 +292,33 @@ defmodule PartitionSupervisor do
end
end
defp register_child(partition, pid) do
:ets.insert(Process.get(:ets_table), {partition, pid})
defp register_child(name, partition, pid) when is_atom(name) do
:ets.insert(name, {partition, pid})
end
defp register_child({:via, _, _}, partition, pid) do
Registry.register(@registry, {self(), partition}, pid)
end
@impl true
def init({name, partitions, children, init_opts}) do
table = init_table(name)
:ets.insert(table, {:partitions, partitions, partitions})
Process.put(:ets_table, table)
init_partitions(name, partitions)
Supervisor.init(children, Keyword.put_new(init_opts, :strategy, :one_for_one))
end
defp init_table(name) when is_atom(name) do
:ets.new(name, [:set, :named_table, :public, read_concurrency: true])
defp init_partitions(name, partitions) when is_atom(name) do
:ets.new(name, [:set, :named_table, :protected, read_concurrency: true])
:ets.insert(name, {:partitions, partitions})
end
defp init_table({:via, _, _}) do
table = :ets.new(__MODULE__, [:set, :public, read_concurrency: true])
ensure_registry()
Registry.register(@registry, self(), table)
table
end
defp init_partitions({:via, _, _}, partitions) do
child_spec = {Registry, keys: :unique, name: @registry}
defp ensure_registry do
if Process.whereis(@registry) == nil do
Supervisor.start_child(:elixir_sup, {Registry, keys: :unique, name: @registry})
unless Process.whereis(@registry) do
Supervisor.start_child(:elixir_sup, child_spec)
end
end
@doc """
Resizes the number of partitions in the PartitionSupervisor.
This is done by starting or stopping a given number of
partitions in the supervisor. All of the child specifications
are kept in the `PartitionSupervisor` itself.
The final number of partitions cannot be less than zero and
cannot be more than the number of partitions the supervisor
started with.
"""
@doc since: "1.18.0"
@spec resize!(name(), non_neg_integer()) :: non_neg_integer()
def resize!(name, partitions) when is_integer(partitions) do
supervisor =
GenServer.whereis(name) || exit({:noproc, {__MODULE__, :resize!, [name, partitions]}})
table = table(name)
ensure_registry()
Registry.lock(@registry, supervisor, fn ->
case :ets.lookup(table, :partitions) do
[{:partitions, _current, max}] when partitions not in 0..max//1 ->
raise ArgumentError,
"the number of partitions to resize to must be a number between 0 and #{max}, got: #{partitions}"
[{:partitions, current, max}] when partitions > current ->
for id <- current..(partitions - 1) do
case Supervisor.restart_child(supervisor, id) do
{:ok, _} ->
:ok
{:ok, _, _} ->
:ok
{:error, reason} ->
raise "cannot restart partition #{id} of PartitionSupervisor #{inspect(name)} due to reason #{inspect(reason)}"
end
end
:ets.insert(table, {:partitions, partitions, max})
current
[{:partitions, current, max}] when partitions < current ->
:ets.insert(table, {:partitions, partitions, max})
for id <- partitions..(current - 1) do
case Supervisor.terminate_child(supervisor, id) do
:ok ->
:ok
{:error, reason} ->
raise "cannot terminate partition #{id} of PartitionSupervisor #{inspect(name)} due to reason #{inspect(reason)}"
end
end
current
[{:partitions, current, _max}] ->
current
end
end)
Registry.register(@registry, self(), partitions)
end
@doc """
@@ -396,27 +327,24 @@ defmodule PartitionSupervisor do
@doc since: "1.14.0"
@spec partitions(name()) :: pos_integer()
def partitions(name) do
name |> table() |> partitions(name)
{_name, partitions} = name_partitions(name)
partitions
end
defp partitions(table, name) do
# For whereis_name, we want to lookup on GenServer.whereis/1
# just once, so we lookup the name and partitions together.
defp name_partitions(name) when is_atom(name) do
try do
:ets.lookup_element(table, :partitions, 2)
{name, :ets.lookup_element(name, :partitions, 2)}
rescue
_ -> exit({:noproc, {__MODULE__, :partitions, [name]}})
end
end
defp table(name) when is_atom(name) do
name
end
# For whereis_name, we want to lookup on GenServer.whereis/1
# just once, so we lookup the name and partitions together.
defp table(name) when is_tuple(name) do
defp name_partitions(name) when is_tuple(name) do
with pid when is_pid(pid) <- GenServer.whereis(name),
[{_, table}] <- Registry.lookup(@registry, pid) do
table
[name_partitions] <- Registry.lookup(@registry, pid) do
name_partitions
else
_ -> exit({:noproc, {__MODULE__, :partitions, [name]}})
end
@@ -441,7 +369,7 @@ defmodule PartitionSupervisor do
@doc since: "1.14.0"
@spec which_children(name()) :: [
# Inlining [module()] | :dynamic here because :supervisor.modules() is not exported
{integer(), pid | :restarting, :worker | :supervisor, [module()] | :dynamic}
{:undefined, pid | :restarting, :worker | :supervisor, [module()] | :dynamic}
]
def which_children(name) when is_atom(name) or elem(name, 0) == :via do
Supervisor.which_children(name)
@@ -495,17 +423,22 @@ defmodule PartitionSupervisor do
@doc false
def whereis_name({name, key}) when is_atom(name) or is_tuple(name) do
table = table(name)
partitions = partitions(table, name)
if partitions == 0 do
raise ArgumentError, "PartitionSupervisor #{inspect(name)} has zero partitions"
end
{name, partitions} = name_partitions(name)
partition =
if is_integer(key), do: rem(abs(key), partitions), else: :erlang.phash2(key, partitions)
:ets.lookup_element(table, partition, 2)
whereis_name(name, partition)
end
defp whereis_name(name, partition) when is_atom(name) do
:ets.lookup_element(name, partition, 2)
end
defp whereis_name(name, partition) when is_pid(name) do
@registry
|> Registry.values({name, partition}, name)
|> List.first(:undefined)
end
@doc false
+94 -201
View File
@@ -44,17 +44,17 @@ defmodule Path do
"""
@spec absname(t) :: binary
def absname(path) do
absname(path, &File.cwd!/0)
absname(path, File.cwd!())
end
@doc """
Builds a path from `relative_to` to `path`.
If `path` is already an absolute path, `relative_to` is ignored. See also
`relative_to/3`. `relative_to` is either a path or an anonymous function,
which is invoked only when necessary, that returns a path.
`relative_to/2`.
Unlike `expand/2`, no attempt is made to resolve `..`, `.` or `~`.
Unlike `expand/2`, no attempt is made to
resolve `..`, `.` or `~`.
## Examples
@@ -64,37 +64,20 @@ defmodule Path do
iex> Path.absname("../x", "bar")
"bar/../x"
iex> Path.absname("foo", fn -> "lazy" end)
"lazy/foo"
"""
@spec absname(t, t | (-> t)) :: binary
@spec absname(t, t) :: binary
def absname(path, relative_to) do
path = IO.chardata_to_string(path)
case type(path) do
:relative ->
relative_to =
if is_function(relative_to, 0) do
relative_to.()
else
relative_to
end
absname_join([relative_to, path])
:absolute ->
absname_join([path])
:volumerelative ->
relative_to =
if is_function(relative_to, 0) do
relative_to.()
else
relative_to
end
|> IO.chardata_to_string()
relative_to = IO.chardata_to_string(relative_to)
absname_vr(split(path), split(relative_to), relative_to)
end
end
@@ -172,21 +155,15 @@ defmodule Path do
Converts the path to an absolute one, expanding
any `.` and `..` components and a leading `~`.
If a relative path is provided it is expanded relatively to
the current working directory.
## Examples
Path.expand("/foo/bar/../baz")
#=> "/foo/baz"
Path.expand("foo/bar/../baz")
#=> "$PWD/foo/baz"
"""
@spec expand(t) :: binary
def expand(path) do
expand_dot(absname(expand_home(path), &File.cwd!/0))
expand_dot(absname(expand_home(path), File.cwd!()))
end
@doc """
@@ -215,7 +192,7 @@ defmodule Path do
"""
@spec expand(t, t) :: binary
def expand(path, relative_to) do
expand_dot(absname(absname(expand_home(path), expand_home(relative_to)), &File.cwd!/0))
expand_dot(absname(absname(expand_home(path), expand_home(relative_to)), File.cwd!()))
end
@doc """
@@ -249,8 +226,6 @@ defmodule Path do
@doc """
Forces the path to be a relative path.
If an absolute path is given, it is stripped from its root component.
## Examples
### Unix-like operating systems
@@ -267,12 +242,6 @@ defmodule Path do
Path.relative("/bar/foo.ex") #=> "bar/foo.ex"
"""
# Note this function does not expand paths because the behavior
# is ambiguous. If we expand it before converting to relative, then
# "/usr/../../foo" means "/foo". If we expand it after, it means "../foo".
# We could expand only relative paths but it is best to say it never
# expands and then provide a `Path.expand_relative` function (or an
# option) if desired.
@spec relative(t) :: binary
def relative(name) do
relative(name, major_os_type())
@@ -328,143 +297,52 @@ defmodule Path do
defp win32_pathtype(relative), do: {:relative, relative}
@doc """
Returns the direct relative path from `path` in relation to `cwd`.
Returns the direct relative path from `path` in relation to `from`.
In other words, this function attempts to return a path such that
`Path.expand(result, cwd)` points to `path`. This function aims
to return a relative path whenever possible, but that's not guaranteed:
In other words, this function tries to strip the `from` prefix from `path`.
* If both paths are relative, a relative path is always returned
This function does not query the file system, so it assumes
no symlinks between the paths.
* If both paths are absolute, a relative path may be returned if
they share a common prefix. You can pass the `:force` option to
force this function to traverse up, but even then a relative
path is not guaranteed (for example, if the absolute paths
belong to different drives on Windows)
* If a mixture of paths are given, the result will always match
the given `path` (the first argument)
This function expands `.` and `..` entries without traversing the
file system, so it assumes no symlinks between the paths. See
`safe_relative_to/2` for a safer alternative.
## Options
* `:force` - (boolean since v1.16.0) if `true` forces a relative
path to be returned by traversing the path up. Except if the paths
are in different volumes on Windows. Defaults to `false`.
In case a direct relative path cannot be found, it returns
the original path.
## Examples
### With relative `cwd`
iex> Path.relative_to("/usr/local/foo", "/usr/local")
"foo"
If both paths are relative, a minimum path is computed:
iex> Path.relative_to("/usr/local/foo", "/")
"usr/local/foo"
Path.relative_to("tmp/foo/bar", "tmp") #=> "foo/bar"
Path.relative_to("tmp/foo/bar", "tmp/foo") #=> "bar"
Path.relative_to("tmp/foo/bar", "tmp/bat") #=> "../foo/bar"
iex> Path.relative_to("/usr/local/foo", "/etc")
"/usr/local/foo"
If an absolute path is given with relative `cwd`, it is returned as:
Path.relative_to("/usr/foo/bar", "tmp/bat") #=> "/usr/foo/bar"
### With absolute `cwd`
If both paths are absolute, a relative is computed if possible,
without traversing up:
Path.relative_to("/usr/local/foo", "/usr/local") #=> "foo"
Path.relative_to("/usr/local/foo", "/") #=> "usr/local/foo"
Path.relative_to("/usr/local/foo", "/etc") #=> "/usr/local/foo"
Path.relative_to("/usr/local/foo", "/usr/local/foo") #=> "."
Path.relative_to("/usr/local/../foo", "/usr/foo") #=> "."
Path.relative_to("/usr/local/../foo/bar", "/usr/foo") #=> "bar"
If `:force` is set to `true` paths are traversed up:
Path.relative_to("/usr", "/usr/local", force: true) #=> ".."
Path.relative_to("/usr/foo", "/usr/local", force: true) #=> "../foo"
Path.relative_to("/usr/../foo/bar", "/etc/foo", force: true) #=> "../../foo/bar"
If a relative path is given, it is assumed to be relative to the
given path, so the path is returned with "." and ".." expanded:
Path.relative_to(".", "/usr/local") #=> "."
Path.relative_to("foo", "/usr/local") #=> "foo"
Path.relative_to("foo/../bar", "/usr/local") #=> "bar"
Path.relative_to("foo/..", "/usr/local") #=> "."
Path.relative_to("../foo", "/usr/local") #=> "../foo"
iex> Path.relative_to("/usr/local/foo", "/usr/local/foo")
"."
"""
@spec relative_to(t, t, keyword) :: binary
def relative_to(path, cwd, opts \\ []) when is_list(opts) do
os_type = major_os_type()
split_path = split(path)
split_cwd = split(cwd)
force = Keyword.get(opts, :force, false)
case {split_absolute?(split_path, os_type), split_absolute?(split_cwd, os_type)} do
{true, true} ->
split_path = expand_split(split_path)
split_cwd = expand_split(split_cwd)
case force do
true -> relative_to_forced(split_path, split_cwd, split_path)
false -> relative_to_unforced(split_path, split_cwd, split_path)
end
{false, false} ->
split_path = expand_relative(split_path, [], [])
split_cwd = expand_relative(split_cwd, [], [])
relative_to_forced(split_path, split_cwd, [])
{_, _} ->
join(expand_relative(split_path, [], []))
end
@spec relative_to(t, t) :: binary
def relative_to(path, from) do
path = IO.chardata_to_string(path)
relative_to(split(path), split(from), path)
end
defp relative_to_unforced(path, path, _original), do: "."
defp relative_to_unforced([h | t1], [h | t2], original),
do: relative_to_unforced(t1, t2, original)
defp relative_to_unforced([_ | _] = l1, [], _original), do: join(l1)
defp relative_to_unforced(_, _, original), do: join(original)
defp relative_to_forced(path, path, _original), do: "."
defp relative_to_forced(["."], _path, _original), do: "."
defp relative_to_forced(path, ["."], _original), do: join(path)
defp relative_to_forced([h | t1], [h | t2], original), do: relative_to_forced(t1, t2, original)
# this should only happen if we have two paths on different drives on windows
defp relative_to_forced(original, _, original), do: join(original)
defp relative_to_forced(l1, l2, _original) do
base = List.duplicate("..", length(l2))
join(base ++ l1)
defp relative_to(path, path, _original) do
"."
end
defp expand_relative([".." | t], [_ | acc], up), do: expand_relative(t, acc, up)
defp expand_relative([".." | t], acc, up), do: expand_relative(t, acc, [".." | up])
defp expand_relative(["." | t], acc, up), do: expand_relative(t, acc, up)
defp expand_relative([h | t], acc, up), do: expand_relative(t, [h | acc], up)
defp expand_relative([], [], []), do: ["."]
defp expand_relative([], acc, up), do: up ++ :lists.reverse(acc)
defp relative_to([h | t1], [h | t2], original) do
relative_to(t1, t2, original)
end
defp expand_split([head | tail]), do: expand_split(tail, [head])
defp expand_split([".." | t], [_, last | acc]), do: expand_split(t, [last | acc])
defp expand_split([".." | t], acc), do: expand_split(t, acc)
defp expand_split(["." | t], acc), do: expand_split(t, acc)
defp expand_split([h | t], acc), do: expand_split(t, [h | acc])
defp expand_split([], acc), do: :lists.reverse(acc)
defp relative_to([_ | _] = l1, [], _original) do
join(l1)
end
defp split_absolute?(split, :win32), do: win32_split_absolute?(split)
defp split_absolute?(split, _), do: match?(["/" | _], split)
defp win32_split_absolute?(["//" | _]), do: true
defp win32_split_absolute?([<<_, ":/">> | _]), do: true
defp win32_split_absolute?(_), do: false
defp relative_to(_, _, original) do
original
end
@doc """
Convenience to get the path relative to the current working
@@ -472,13 +350,11 @@ defmodule Path do
If, for some reason, the current working directory
cannot be retrieved, this function returns the given `path`.
Check `relative_to/3` for the supported options.
"""
@spec relative_to_cwd(t, keyword) :: binary
def relative_to_cwd(path, opts \\ []) when is_list(opts) do
@spec relative_to_cwd(t) :: binary
def relative_to_cwd(path) do
case :file.get_cwd() do
{:ok, base} -> relative_to(path, IO.chardata_to_string(base), opts)
{:ok, base} -> relative_to(path, IO.chardata_to_string(base))
_ -> path
end
end
@@ -738,7 +614,7 @@ defmodule Path do
end
end
@doc ~S"""
@doc """
Traverses paths according to the given `glob` expression and returns a
list of matches.
@@ -770,9 +646,9 @@ defmodule Path do
You may call `Path.expand/1` to normalize the path before invoking
this function.
A character preceded by `\\` loses its special meaning.
Note that `\\` must be written as `\\\\` in a string literal.
For example, `"\\\\?*"` will match any filename starting with `?.`.
A character preceded by \ loses its special meaning.
Note that \ must be written as \\ in a string literal.
For example, "\\?*" will match any filename starting with ?.
By default, the patterns `*` and `?` do not match files starting
with a dot `.`. See the `:match_dot` option in the "Options" section
@@ -842,18 +718,21 @@ defmodule Path do
end
end
# expands dots in an absolute path represented as a string
defp expand_dot(path) do
[head | tail] = :binary.split(path, "/", [:global])
IO.iodata_to_binary(expand_dot(tail, [head <> "/"]))
end
# expand_dot the given path by expanding "..", "." and "~".
defp expand_dot(<<"/", rest::binary>>), do: "/" <> do_expand_dot(rest)
defp expand_dot([".." | t], [_, _ | acc]), do: expand_dot(t, acc)
defp expand_dot([".." | t], acc), do: expand_dot(t, acc)
defp expand_dot(["." | t], acc), do: expand_dot(t, acc)
defp expand_dot([h | t], acc), do: expand_dot(t, ["/", h | acc])
defp expand_dot([], ["/", head | acc]), do: :lists.reverse([head | acc])
defp expand_dot([], acc), do: :lists.reverse(acc)
defp expand_dot(<<letter, ":/", rest::binary>>) when letter in ?a..?z,
do: <<letter, ":/">> <> do_expand_dot(rest)
defp expand_dot(path), do: do_expand_dot(path)
defp do_expand_dot(path), do: do_expand_dot(:binary.split(path, "/", [:global]), [])
defp do_expand_dot([".." | t], [_, _ | acc]), do: do_expand_dot(t, acc)
defp do_expand_dot([".." | t], []), do: do_expand_dot(t, [])
defp do_expand_dot(["." | t], acc), do: do_expand_dot(t, acc)
defp do_expand_dot([h | t], acc), do: do_expand_dot(t, ["/", h | acc])
defp do_expand_dot([], []), do: ""
defp do_expand_dot([], ["/" | acc]), do: IO.iodata_to_binary(:lists.reverse(acc))
defp major_os_type do
:os.type() |> elem(0)
@@ -862,18 +741,6 @@ defmodule Path do
@doc """
Returns a relative path that is protected from directory-traversal attacks.
See `safe_relative/2` for a non-deprecated version of this API.
"""
# TODO: Deprecate me on Elixir v1.19
@doc since: "1.14.0", deprecated: "Use safe_relative/2 instead"
@spec safe_relative_to(t, t) :: {:ok, binary} | :error
def safe_relative_to(path, cwd) do
safe_relative(path, cwd)
end
@doc """
Returns a relative path that is protected from directory-traversal attacks.
The given relative path is sanitized by eliminating `..` and `.` components.
This function checks that, after expanding those components, the path is still "safe".
@@ -881,32 +748,29 @@ defmodule Path do
* The path is not relative, such as `"/foo/bar"`.
* A `..` component would make it so that the path would traverse up above
* A `..` component would make it so that the path would travers up above
the root of `relative_to`.
* A symbolic link in the path points to something above the root of `relative_to`.
## Examples
iex> Path.safe_relative("foo")
{:ok, "foo"}
iex> Path.safe_relative("deps/my_dep/app.beam")
iex> Path.safe_relative_to("deps/my_dep/app.beam", "deps")
{:ok, "deps/my_dep/app.beam"}
iex> Path.safe_relative("deps/my_dep/./build/../app.beam", File.cwd!())
iex> Path.safe_relative_to("deps/my_dep/./build/../app.beam", "deps")
{:ok, "deps/my_dep/app.beam"}
iex> Path.safe_relative("my_dep/../..")
iex> Path.safe_relative_to("my_dep/../..", "deps")
:error
iex> Path.safe_relative("/usr/local", File.cwd!())
iex> Path.safe_relative_to("/usr/local", ".")
:error
"""
@doc since: "1.14.0"
@spec safe_relative(t, t) :: {:ok, binary} | :error
def safe_relative(path, relative_to \\ File.cwd!()) do
@spec safe_relative_to(t, t) :: {:ok, binary} | :error
def safe_relative_to(path, relative_to) do
path = IO.chardata_to_string(path)
case :filelib.safe_relative_path(path, relative_to) do
@@ -914,4 +778,33 @@ defmodule Path do
relative_path -> {:ok, IO.chardata_to_string(relative_path)}
end
end
@doc """
Returns a path relative to the current working directory that is
protected from directory-traversal attacks.
Same as `safe_relative_to/2` with the current working directory as
the second argument. If there is an issue retrieving the current working
directory, this function raises an error.
## Examples
iex> Path.safe_relative("foo")
{:ok, "foo"}
iex> Path.safe_relative("foo/../bar")
{:ok, "bar"}
iex> Path.safe_relative("foo/../..")
:error
iex> Path.safe_relative("/usr/local")
:error
"""
@doc since: "1.14.0"
@spec safe_relative(t) :: {:ok, binary} | :error
def safe_relative(path) do
safe_relative_to(path, File.cwd!())
end
end
+1 -1
View File
@@ -16,7 +16,7 @@ defmodule Port do
iex> send(port, {self(), :close})
:ok
iex> flush()
{#Port<0.1444>, :closed}
{#Port<0.1464>, :closed}
:ok
In the example above, we have created a new port that executes the
+8 -69
View File
@@ -88,9 +88,6 @@ defmodule Process do
`false`.
`pid` must refer to a process running on the local node or `ArgumentError` is raised.
To check whether a process on any node is alive you can use the [`:erpc`](`:erpc`) module.
:erpc.call(node(pid), Process, :alive?, [pid])
Inlined by the compiler.
"""
@@ -198,7 +195,7 @@ defmodule Process do
@doc """
Sends an exit signal with the given `reason` to `pid`.
The following behavior applies if `reason` is any term except `:normal`
The following behaviour applies if `reason` is any term except `:normal`
or `:kill`:
1. If `pid` is not trapping exits, `pid` will exit with the given
@@ -219,16 +216,6 @@ defmodule Process do
Inlined by the compiler.
> #### Differences to `Kernel.exit/1` {: .info }
>
> The functions `Kernel.exit/1` and `Process.exit/2` are
> named similarly but provide very different functionalities. The
> `Kernel:exit/1` function should be used when the intent is to stop the current
> process while `Process.exit/2` should be used when the intent is to send an
> exit signal to another process. Note also that `Kernel.exit/1` can be caught
> with `try/1` while `Process.exit/2` can only be handled by trapping exits and
> when the signal is different than `:kill`.
## Examples
Process.exit(pid, :kill)
@@ -246,12 +233,6 @@ defmodule Process do
the current process will sleep forever, and not
consume or reply to messages.
> #### Sleeping limit {: .info }
>
> Before Elixir v1.18, `sleep/1` did not accept integer timeout values greater
> than `16#ffffffff`, that is, `2^32-1`. Since Elixir v1.18, arbitrarily-high integer
> values are accepted.
**Use this function with extreme care**. For almost all situations
where you would use `sleep/1` in Elixir, there is likely a
more correct, faster and precise way of achieving the same with
@@ -318,15 +299,7 @@ defmodule Process do
end
"""
# Max value for a receive's after clause
@max_receive_after 0xFFFFFFFF
@spec sleep(timeout) :: :ok
def sleep(timeout) when is_integer(timeout) and timeout > @max_receive_after do
receive after: (@max_receive_after -> sleep(timeout - @max_receive_after))
end
def sleep(timeout)
when is_integer(timeout) and timeout >= 0
when timeout == :infinity do
@@ -531,7 +504,7 @@ defmodule Process do
If the process is already dead when calling `Process.monitor/1`, a
`:DOWN` message is delivered immediately.
See ["The need for monitoring"](genservers.md#the-need-for-monitoring)
See ["The need for monitoring"](https://elixir-lang.org/getting-started/mix-otp/genserver.html#the-need-for-monitoring)
for an example. See `:erlang.monitor/2` for more information.
Inlined by the compiler.
@@ -579,15 +552,10 @@ defmodule Process do
send(pid, {:ping, ref_and_alias})
receive do: (msg -> msg)
receive do: msg -> msg
#=> :pong
ref_and_alias = Process.monitor(pid, alias: :reply_demonitor)
#=> #Reference<0.906660723.3006791681.40191>
send(pid, {:ping, ref_and_alias})
receive do: (msg -> msg)
receive do: msg -> msg
#=> {:DOWN, #Reference<0.906660723.3006791681.40191>, :process, #PID<0.118.0>, :noproc}
"""
@@ -656,7 +624,7 @@ defmodule Process do
exits with a reason other than `:normal` (which is also the exit reason used
when a process finishes its job) and `pid1` is not trapping exits (see
`flag/2`), then `pid1` will exit with the same reason as `pid2` and in turn
emit an exit signal to all its other linked processes. The behavior when
emit an exit signal to all its other linked processes. The behaviour when
`pid1` is trapping exits is described in `exit/2`.
See `:erlang.link/1` for more information.
@@ -683,7 +651,7 @@ defmodule Process do
defdelegate unlink(pid_or_port), to: :erlang
@doc """
Registers the given `pid_or_port` under the given `name` on the local node.
Registers the given `pid_or_port` under the given `name`.
`name` must be an atom and can then be used instead of the
PID/port identifier when sending messages with `Kernel.send/2`.
@@ -871,17 +839,13 @@ defmodule Process do
nilify(:erlang.process_info(pid))
end
@type process_info_item :: atom | {:dictionary, term}
@type process_info_result_item :: {process_info_item, term}
@doc """
Returns information about the process identified by `pid`,
or returns `nil` if the process is not alive.
See `:erlang.process_info/2` for more information.
"""
@spec info(pid, process_info_item) :: process_info_result_item | nil
@spec info(pid, [process_info_item]) :: [process_info_result_item] | nil
@spec info(pid, atom | [atom]) :: {atom, term} | [{atom, term}] | nil
def info(pid, spec)
def info(pid, :registered_name) do
@@ -892,7 +856,7 @@ defmodule Process do
end
end
def info(pid, spec) do
def info(pid, spec) when is_atom(spec) or is_list(spec) do
nilify(:erlang.process_info(pid, spec))
end
@@ -976,31 +940,6 @@ defmodule Process do
@spec unalias(alias) :: boolean
defdelegate unalias(alias), to: :erlang
@doc """
Add a descriptive term to the current process.
The term does not need to be unique, and in Erlang/OTP 27+ will be shown in
Observer and in crash logs.
This label may be useful for identifying a process as one of multiple in a
given role, such as `:queue_worker` or `{:live_chat, user_id}`.
## Examples
Process.set_label(:worker)
#=> :ok
Process.set_label({:any, "term"})
#=> :ok
"""
@doc since: "1.17.0"
@spec set_label(term()) :: :ok
def set_label(label) do
# TODO: switch to `:proc_lib.set_label/2` when we require Erlang/OTP 27+
Process.put(:"$process_label", label)
# mimic return value of `:proc_lib.set_label/2`
:ok
end
@compile {:inline, nilify: 1}
defp nilify(:undefined), do: nil
defp nilify(other), do: other

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