Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
327063cc84 | ||
|
|
795d0a9583 | ||
|
|
1150ebad03 | ||
|
|
18feec4cd8 | ||
|
|
7d5920206b | ||
|
|
a694447601 | ||
|
|
19ae388dda | ||
|
|
6b5edea5f1 | ||
|
|
b736c23af9 | ||
|
|
63f0000ace | ||
|
|
5e8b187d08 | ||
|
|
aea1a47b8d | ||
|
|
eeba8e992d | ||
|
|
dd42dd8620 | ||
|
|
ed1ddfd338 | ||
|
|
d17df054e2 | ||
|
|
41f6ee3f2e | ||
|
|
9d31768f42 | ||
|
|
558e0ba6a6 | ||
|
|
ab1e3448f0 | ||
|
|
17cd103a28 | ||
|
|
60e6ceae10 | ||
|
|
f64ccd0871 | ||
|
|
6c463b2310 | ||
|
|
e310dd91d6 | ||
|
|
29ef147a22 | ||
|
|
74ab38439e | ||
|
|
5654752026 | ||
|
|
e78538a42b | ||
|
|
079b92e98e | ||
|
|
325877a224 | ||
|
|
097c4cf672 | ||
|
|
fe16fd3442 | ||
|
|
e520e8ce96 | ||
|
|
dd004539c6 | ||
|
|
2512cbaff0 | ||
|
|
a99a9cf47a | ||
|
|
677ad618e9 | ||
|
|
ff309747b7 | ||
|
|
e5ec5ce7f6 | ||
|
|
f3e669c471 | ||
|
|
bf915aa2e7 | ||
|
|
ef3e2796ef | ||
|
|
d8cc841ab0 | ||
|
|
7098ac8a16 | ||
|
|
e0658bb55a | ||
|
|
cf8c28c34b | ||
|
|
60dcd14390 | ||
|
|
8344e218a6 | ||
|
|
740b2d74df | ||
|
|
da0189e641 | ||
|
|
ad778a6f29 | ||
|
|
c35edd1ee8 | ||
|
|
bb8ad4406b | ||
|
|
111b48dcf4 | ||
|
|
6dccefe687 | ||
|
|
9ea950c9af | ||
|
|
7d100fff9f | ||
|
|
6b69c7f5ac | ||
|
|
691d402341 | ||
|
|
b96211a325 | ||
|
|
17b9ba61d9 | ||
|
|
f83b4e4e84 | ||
|
|
4de331597a | ||
|
|
6e295998ef | ||
|
|
f2dae095f8 | ||
|
|
6f5715fc30 | ||
|
|
1ece71a831 | ||
|
|
197351dd73 | ||
|
|
0d671dafea | ||
|
|
9ae7c39125 | ||
|
|
ba8fb4dff1 | ||
|
|
194661197e | ||
|
|
298acd1cdb | ||
|
|
97c608c346 | ||
|
|
f84bc19bf2 | ||
|
|
91375778cb | ||
|
|
38ddc98fef | ||
|
|
1b7c73273d | ||
|
|
5bd5e75c3b | ||
|
|
1cba584ac8 | ||
|
|
dfafea092e | ||
|
|
0ae4bfb222 | ||
|
|
7db4413476 | ||
|
|
4d52d18ef7 | ||
|
|
39ff86c551 | ||
|
|
12fdd8ea9b | ||
|
|
28f8eba9a2 | ||
|
|
b3b14202d9 | ||
|
|
ffa3ef1ccf | ||
|
|
d13f4155af | ||
|
|
d23e42e27f | ||
|
|
872efb180d | ||
|
|
2e61f32b6d | ||
|
|
7abb3bdddc | ||
|
|
67fd9824cc | ||
|
|
1878e30bdb | ||
|
|
88bbffd614 | ||
|
|
a50a2fd983 | ||
|
|
c950a57a3e | ||
|
|
80404da8f6 | ||
|
|
010d516b11 | ||
|
|
fb42733873 | ||
|
|
1070b2f434 | ||
|
|
9675e2285a | ||
|
|
a09ddbb0a6 | ||
|
|
81a12b7774 | ||
|
|
88ba7766e0 | ||
|
|
6475917b3f | ||
|
|
5b6e04cb4d | ||
|
|
2bfb14751f | ||
|
|
b1998960bb | ||
|
|
c4dcbf379f | ||
|
|
3b5bd39743 | ||
|
|
4e4cde1118 | ||
|
|
e1db6d8831 | ||
|
|
db8a1cdc7d | ||
|
|
d25aacb207 | ||
|
|
106539b5d2 | ||
|
|
d716bc2703 | ||
|
|
aa0dcb9a70 | ||
|
|
01366ef526 | ||
|
|
6145599638 | ||
|
|
abd4f54d78 | ||
|
|
adff7f6d34 | ||
|
|
2364f04991 | ||
|
|
46a5c54844 | ||
|
|
2caacae2e4 | ||
|
|
1ff5e0c88b | ||
|
|
c150876e5f | ||
|
|
758da1e311 | ||
|
|
250b48aa3b | ||
|
|
0a9ee79fbb | ||
|
|
58b8b93ee6 | ||
|
|
aef8673666 | ||
|
|
901ec18aa7 | ||
|
|
56b62b64cb | ||
|
|
abbf9061ea | ||
|
|
8f28bd9c3f | ||
|
|
68782004a7 | ||
|
|
0a146d10cc | ||
|
|
beaa33d088 | ||
|
|
2a94668e6e | ||
|
|
56e6494b36 | ||
|
|
033a177078 | ||
|
|
b9fffa3b41 | ||
|
|
7be85838fa | ||
|
|
37f8832229 | ||
|
|
7ebf5c3032 | ||
|
|
7d4d42097d | ||
|
|
33e1b570bf | ||
|
|
8920cc1432 | ||
|
|
9b480f985b | ||
|
|
daab3f80a0 | ||
|
|
ec8782a486 | ||
|
|
6745f775b2 | ||
|
|
7923e4f383 | ||
|
|
d280846843 | ||
|
|
178b654a1b | ||
|
|
3a516a3f2b | ||
|
|
d5316d558f | ||
|
|
a7216decad | ||
|
|
8d545610d2 | ||
|
|
23c27b629d | ||
|
|
0220d81513 | ||
|
|
aabe5dad76 | ||
|
|
741757f2a7 | ||
|
|
f5e543576e | ||
|
|
e6b641a075 | ||
|
|
3822609621 | ||
|
|
0f65cb065a | ||
|
|
ae19236e08 | ||
|
|
f422b77aaa | ||
|
|
43b3e94506 | ||
|
|
2d86cb0027 | ||
|
|
792d4cc631 | ||
|
|
f8016bca48 | ||
|
|
82818f7126 | ||
|
|
5c8f9aac64 | ||
|
|
1a8bad1bf2 | ||
|
|
4b795871b6 | ||
|
|
3036401c7c | ||
|
|
8bbba572de | ||
|
|
968b51319e | ||
|
|
900f25f832 |
+2
-1
@@ -17,5 +17,6 @@
|
||||
|
||||
# Float tests
|
||||
float_assert: 1
|
||||
]
|
||||
],
|
||||
normalize_bitstring_modifiers: false
|
||||
]
|
||||
|
||||
@@ -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@v4
|
||||
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
|
||||
@@ -3,12 +3,12 @@ name: CI for Markdown content
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
- 'main'
|
||||
paths:
|
||||
- "lib/**/*.md"
|
||||
- 'lib/**/*.md'
|
||||
pull_request:
|
||||
paths:
|
||||
- "lib/**/*.md"
|
||||
- 'lib/**/*.md'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
- name: Check out the repository
|
||||
@@ -27,8 +27,7 @@ jobs:
|
||||
fetch-depth: 10
|
||||
|
||||
- name: Run markdownlint
|
||||
uses: DavidAnson/markdownlint-cli2-action@v18.0.0
|
||||
uses: DavidAnson/markdownlint-cli2-action@v13.0.0
|
||||
with:
|
||||
globs: |
|
||||
lib/elixir/pages/**/*.md
|
||||
README.md
|
||||
|
||||
+10
-14
@@ -11,6 +11,7 @@ on:
|
||||
env:
|
||||
ELIXIR_ASSERT_TIMEOUT: 2000
|
||||
ELIXIRC_OPTS: "--warnings-as-errors"
|
||||
ERLC_OPTS: "warnings_as_errors"
|
||||
LANG: C.UTF-8
|
||||
|
||||
permissions:
|
||||
@@ -18,24 +19,22 @@ 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_latest: true
|
||||
- otp_version: "27.0"
|
||||
- otp_version: "26.0"
|
||||
otp_latest: true
|
||||
- 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
|
||||
with:
|
||||
@@ -43,9 +42,6 @@ jobs:
|
||||
- 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,19 +66,19 @@ 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.0"]
|
||||
runs-on: windows-2019
|
||||
steps:
|
||||
- name: Configure Git
|
||||
run: git config --global core.autocrlf input
|
||||
@@ -109,7 +105,7 @@ jobs:
|
||||
|
||||
check_posix_compliant:
|
||||
name: Check POSIX-compliant
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
|
||||
@@ -10,7 +10,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
notify:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ubuntu-20.04
|
||||
name: Notify
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -18,8 +18,8 @@ jobs:
|
||||
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 }}
|
||||
|
||||
+20
-308
@@ -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
|
||||
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
|
||||
|
||||
@@ -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,6 +34,8 @@ runs:
|
||||
export ELIXIR_ZIP=$PWD/elixir-otp-${{ inputs.otp }}.zip
|
||||
(cd lib/elixir/scripts/windows_installer && ./build.sh)
|
||||
mv lib/elixir/scripts/windows_installer/tmp/elixir-otp-${{ inputs.otp }}.exe .
|
||||
shasum -a 1 elixir-otp-${{ inputs.otp }}.exe > elixir-otp-${{ inputs.otp }}.exe.sha1sum
|
||||
shasum -a 256 elixir-otp-${{ inputs.otp }}.exe > elixir-otp-${{ inputs.otp }}.exe.sha256sum
|
||||
- name: Get ExDoc ref
|
||||
if: ${{ inputs.build_docs }}
|
||||
shell: bash
|
||||
@@ -42,7 +46,7 @@ runs:
|
||||
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
|
||||
- uses: actions/checkout@v3
|
||||
if: ${{ inputs.build_docs }}
|
||||
with:
|
||||
repository: elixir-lang/ex_doc
|
||||
@@ -60,5 +64,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
|
||||
|
||||
@@ -17,19 +17,14 @@
|
||||
"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"
|
||||
|
||||
+171
-414
@@ -1,505 +1,262 @@
|
||||
# Changelog for Elixir v1.18
|
||||
# Changelog for Elixir v1.16
|
||||
|
||||
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.
|
||||
## Code snippets in diagnostics
|
||||
|
||||
## Type system improvements
|
||||
Elixir v1.15 introduced a new compiler diagnostic format and the ability to print multiple error diagnostics per compilation (in addition to multiple warnings).
|
||||
|
||||
The most exciting change in Elixir v1.18 is type checking of function calls, alongside gradual inference of patterns and return types. To understand how this will impact your programs, consider the following code in "lib/user.ex":
|
||||
|
||||
```elixir
|
||||
defmodule User do
|
||||
defstruct [:age, :car_choice]
|
||||
|
||||
def drive(%User{age: age, car_choice: car}, 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
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
```elixir
|
||||
User.drive({:ok, %User{}}, car_choices)
|
||||
```
|
||||
|
||||
Here is the warning:
|
||||
With Elixir v1.16, we also include code snippets in exceptions and diagnostics raised by the compiler. For example, a syntax error now includes a pointer to where the error happened:
|
||||
|
||||
```
|
||||
warning: incompatible types given to User.drive/2:
|
||||
|
||||
User.drive({:ok, %User{age: nil, car_choice: nil}}, car_choices)
|
||||
|
||||
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:
|
||||
** (SyntaxError) invalid syntax found on lib/my_app.ex:1:17:
|
||||
error: syntax error before: '*'
|
||||
│
|
||||
22 │ User.drive({:ok, %User{}}, car_choices)
|
||||
│ ~
|
||||
1 │ [1, 2, 3, 4, 5, *]
|
||||
│ ^
|
||||
│
|
||||
└─ lib/foo.ex:22:10: Example.run/0
|
||||
└─ lib/my_app.ex:1:17
|
||||
```
|
||||
|
||||
> 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:
|
||||
For mismatched delimiters, it now shows both delimiters:
|
||||
|
||||
```
|
||||
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:
|
||||
** (MismatchedDelimiterError) mismatched delimiter found on lib/my_app.ex:1:18:
|
||||
error: unexpected token: )
|
||||
│
|
||||
26 │ :error -> Logger.error("User cannot drive")
|
||||
│ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
1 │ [1, 2, 3, 4, 5, 6)
|
||||
│ │ └ mismatched closing delimiter (expected "]")
|
||||
│ └ unclosed delimiter
|
||||
│
|
||||
└─ lib/foo.ex:26: Example.run/0
|
||||
└─ lib/my_app.ex:1:18
|
||||
```
|
||||
|
||||
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).
|
||||
For unclosed delimiters, it now shows where the unclosed delimiter starts:
|
||||
|
||||
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
|
||||
```
|
||||
** (TokenMissingError) token missing on lib/my_app:8:23:
|
||||
error: missing terminator: )
|
||||
│
|
||||
1 │ my_numbers = (1, 2, 3, 4, 5, 6
|
||||
│ └ unclosed delimiter
|
||||
...
|
||||
8 │ IO.inspect(my_numbers)
|
||||
│ └ missing closing delimiter (expected ")")
|
||||
│
|
||||
└─ lib/my_app:8:23
|
||||
```
|
||||
|
||||
ExUnit parameterizes whole test modules. If your modules are configured to run concurrently, as above, so will the parameterized ones.
|
||||
Errors and warnings diagnostics also include code snippets. When possible, we will show precise spans, such as on undefined variables:
|
||||
|
||||
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 ...
|
||||
```
|
||||
error: undefined variable "unknown_var"
|
||||
│
|
||||
5 │ a - unknown_var
|
||||
│ ^^^^^^^^^^^
|
||||
│
|
||||
└─ lib/sample.ex:5:9: Sample.foo/1
|
||||
```
|
||||
|
||||
### Decoding
|
||||
Otherwise the whole line is underlined:
|
||||
|
||||
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
|
||||
```
|
||||
error: function names should start with lowercase characters or underscore, invalid name CamelCase
|
||||
│
|
||||
3 │ def CamelCase do
|
||||
│ ^^^^^^^^^^^^^^^^
|
||||
│
|
||||
└─ lib/sample.ex:3
|
||||
```
|
||||
|
||||
You must rewrite it to:
|
||||
A huge thank you to Vinícius Müller for working on the new diagnostics.
|
||||
|
||||
```elixir
|
||||
def some_fun, do: ~r/foo/
|
||||
```
|
||||
## Revamped documentation
|
||||
|
||||
### 1. Enhancements
|
||||
Elixir's Getting Started guides have been made part of the Elixir repository and incorporated into ExDoc. This was an opportunity to revisit and unify all official guides and references.
|
||||
|
||||
#### IEx
|
||||
We have also incorporated and extended the work on [Understanding Code Smells in Elixir Functional Language](https://github.com/lucasvegi/Elixir-Code-Smells/blob/main/etc/2023-emse-code-smells-elixir.pdf), by Lucas Vegi and Marco Tulio Valente, from [ASERG/DCC/UFMG](http://aserg.labsoft.dcc.ufmg.br/), into the official document in the form of anti-patterns. The anti-patterns are divided into four categories: code-related, design-related, process-related, and meta-programming. Our goal is to give all developers examples of potential anti-patterns, with context and examples on how to improve their codebases.
|
||||
|
||||
* [IEx.Helpers] Add `IEx.Helpers.process_info/1` which prints process information
|
||||
Another [ExDoc](https://github.com/elixir-lang/ex_doc) feature we have incorporated in this release is the addition of cheatsheets, starting with [a cheatsheet for the Enum module](https://hexdocs.pm/elixir/main/enum-cheat.html). If you would like to contribute future cheatsheets to Elixir itself, feel free to start a discussion with an issue.
|
||||
|
||||
#### Mix
|
||||
Finally, we have started enriching our documentation with [Mermaid.js](https://mermaid.js.org/) diagrams. You can find examples in the [GenServer](https://hexdocs.pm/elixir/main/GenServer.html) and [Supervisor](https://hexdocs.pm/elixir/main/Supervisor.html) docs.
|
||||
|
||||
* [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
|
||||
## v1.16.3 (2024-05-21)
|
||||
|
||||
### 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
|
||||
* [Code.Formatter] Add brackets around keyword lists when formatting the left-hand side of `when`
|
||||
* [Kernel] Only infer size in pinned variable in binary strings when needed
|
||||
* [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.16.2 (2024-03-10)
|
||||
|
||||
### 1. Enhancements
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [JSON] Encode any JSON key to string
|
||||
* [Kernel] Allow `<<_::3*8>>` in typespecs
|
||||
* [Code] Emit `:defmodule` tracing event on module definition
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix loadpaths] Support `--no-listeners` option
|
||||
* [Mix] Add `Mix.install_project_dir/0`
|
||||
* [Mix] Add environment variable for reusing `Mix.install/2` installation
|
||||
* [Mix.SCM] Add `Mix.SCM.delete/1`
|
||||
|
||||
### 2. Bug fixes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [CLI] Fix `--no-color` not setting `:ansi_enabled` to false
|
||||
* [Protocol] Return correct implementation for an invalid struct pointing to `nil`
|
||||
* [Stream] Do not raise when `Stream.cycle/1` is explicitly halted
|
||||
|
||||
#### ExUnit
|
||||
|
||||
* [ExUnit.Diff] Fix regression when diffing nested improper lists
|
||||
* [Code] Fix charlist formatting issue when a single-quoted charlist escapes a double-quote character
|
||||
* [Path] Fix regression on how `Path.relative_to/2` dealt with "." as input
|
||||
|
||||
#### IEx
|
||||
|
||||
* [IEx.Autocomplete] Fix autocomplete crash when expanding struct with `__MODULE__`
|
||||
* [IEx.Helpers] Do not purge on `recompile` if IEx is not running
|
||||
* [IEx.Helpers] Discard mermaid fenced blocks from ansi docs
|
||||
|
||||
## v1.18.2 (2025-01-22)
|
||||
#### ExUnit
|
||||
|
||||
* [ExUnit] Properly compared pinned values when building diffs
|
||||
|
||||
## v1.16.1 (2024-01-31)
|
||||
|
||||
### 1. Bug fixes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Code] Fix `Code.quoted_to_algebra/2` for operator with :do key as operand
|
||||
* [Kernel.ParallelCompiler] Do not crash parallel compiler when it receives diagnostics from additional code evaluation
|
||||
* [Kernel.ParallelCompiler] Always log errors at the end of compilation
|
||||
* [String] Fix `String.capitalize/1` with a single codepoint
|
||||
|
||||
#### IEx
|
||||
|
||||
* [IEx] Fix autocompletion of function signatures on Erlang/OTP 26
|
||||
* [IEx] Do not assume `$HOME` is set
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix deps.compile] Handle compilation of rebar3 dependencies when rebar3 is on a path with spaces on Unix
|
||||
* [mix test] Properly resolve relative paths when running tests from individual files
|
||||
* [mix test] Properly resolve Windows paths when running tests from individual files
|
||||
|
||||
## v1.16.0 (2023-12-22)
|
||||
|
||||
### 1. Enhancements
|
||||
|
||||
#### EEx
|
||||
|
||||
* [EEx] Include relative file information in diagnostics
|
||||
|
||||
#### 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
|
||||
* [Code] Add `:emit_warnings` for `Code.string_to_quoted/2`
|
||||
* [Code] Automatically include columns in parsing options
|
||||
* [Code] Introduce `MismatchedDelimiterError` for handling mismatched delimiter exceptions
|
||||
* [Code.Fragment] Handle anonymous calls in fragments
|
||||
* [Code.Formatter] Trim trailing whitespace on heredocs with `\r\n`
|
||||
* [File] Add `:offset` option to `File.stream!/2`
|
||||
* [Kernel] Auto infer size of matched variable in bitstrings
|
||||
* [Kernel] Preserve column information when translating typespecs
|
||||
* [Kernel] Suggest module names based on suffix and casing errors when the module does not exist in `UndefinedFunctionError`
|
||||
* [Kernel.ParallelCompiler] Introduce `Kernel.ParallelCompiler.pmap/2` to compile multiple additional entries in parallel
|
||||
* [Kernel.SpecialForms] Warn if `True`/`False`/`Nil` are used as aliases and there is no such alias
|
||||
* [Macro] Add `Macro.compile_apply/4`
|
||||
* [Module] Add support for `@nifs` annotation from Erlang/OTP 25
|
||||
* [Module] Add support for missing `@dialyzer` configuration
|
||||
* [String] Update to Unicode 15.1.0
|
||||
* [String] Add `String.replace_invalid/2`
|
||||
* [Task] Add `:limit` option to `Task.yield_many/2`
|
||||
|
||||
#### Logger
|
||||
|
||||
* [Logger] Add `Logger.levels/0`
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix] Print compilation lock waiting message to stderr
|
||||
* [mix] Add an environment variable to optionally disable compilation locking
|
||||
* [mix] Add `MIX_PROFILE` to profile a list of comma separated tasks
|
||||
* [mix archive.install] Support `--sparse` option
|
||||
* [mix compile.app] Warn if both `:applications` and `:extra_applications` are used
|
||||
* [mix compile.elixir] Pass original exception down to diagnostic `:details` when possible
|
||||
* [mix compile.elixir] Optimize scenario where there are thousands of files in `lib/` and one of them is changed
|
||||
* [mix deps.clean] Emit a warning instead of crashing when a dependency cannot be removed
|
||||
* [mix escript.build] Escripts now strip .beam files by default, which leads to smaller escripts. However, if you are using escripts to access Elixir docs or compile Elixir code, documentation and deprecation metadata is no longer available. Set `strip_beams: false` in your escript configuration in your `mix.exs` to keep all metadata
|
||||
* [mix escript.install] Support `--sparse` option
|
||||
* [mix release] Include `include/` directory in releases
|
||||
* [mix test] Allow testing multiple file:line at once, such as `mix test test/foo_test.exs:13 test/bar_test.exs:27`
|
||||
|
||||
### 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
|
||||
* [Code] Keep quotes for atom keys in formatter
|
||||
* [Code.Fragment] Fix crash in `Code.Fragment.surround_context/2` when matching on `->`
|
||||
* [IO] Raise when using `IO.binwrite/2` on terminated device (mirroring `IO.write/2`)
|
||||
* [Kernel] Do not expand aliases recursively (the alias stored in Macro.Env is already expanded)
|
||||
* [Kernel] Ensure `dbg` module is a compile-time dependency
|
||||
* [Kernel] Warn when a private function or macro uses `unquote/1` and the function/macro itself is unused
|
||||
* [Kernel] Re-enabled compiler optimizations for top level functions in scripts (disabled in v1.14.0 but shouldn't impact most programs)
|
||||
* [Kernel] Do not define an alias for nested modules starting with `Elixir.` in their definition
|
||||
* [Kernel.ParallelCompiler] Consider a module has been defined in `@after_compile` callbacks to avoid deadlocks
|
||||
* [Macro] Address exception on `Macro.to_string/1` for certain ASTs
|
||||
* [Path] Lazily evaluate `File.cwd!/0` in `Path.expand/1` and `Path.absname/1`
|
||||
* [Path] Ensure `Path.relative_to/2` returns a relative path when the given argument does not share a common prefix with `cwd`
|
||||
|
||||
#### ExUnit
|
||||
|
||||
* [ExUnit] Fix crash when diffing bitstring specifiers
|
||||
* [ExUnit] Raise on incorrectly dedented doctests
|
||||
|
||||
#### IEx
|
||||
|
||||
* [IEx.Autocomplete] Fix crashing when autocompleting structs with runtime values
|
||||
* [IEx.Pry] Fix prying functions with only literals in their body
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix] Track compilation locks per user to avoid permission errors
|
||||
* [mix deps.update] Ensure Git dependencies can be upgraded by doing so against the origin
|
||||
|
||||
## v1.18.1 (2024-12-24)
|
||||
|
||||
### 1. Enhancements
|
||||
|
||||
* [Kernel] Do not emit type violation warnings when comparing or matching against literals
|
||||
* [Kernel] Do not validate clauses of private overridable functions
|
||||
|
||||
### 2. Bug fixes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Code.Fragment] Ensure `Code.Fragment.container_cursor_to_quoted/2` with `:trailing_fragment` parses expressions that were supported in previous versions
|
||||
* [Kernel] Do not crash when typing violation is detected on dynamic dispatch
|
||||
* [Kernel] Properly annotate the source for warnings emitted by the compiler with the `@file` annotation
|
||||
* [Kernel] Properly annotate the source for warnings emitted by the type system with the `@file` annotation
|
||||
* [Kernel] Remove `:no_parens` metadata when using capture with arity on all cases
|
||||
* [Kernel] Ensure diagnostic traces are kept backwards compatible
|
||||
|
||||
#### ExUnit
|
||||
|
||||
* [ExUnit.Case] Ensure async groups do not run concurrenly while the test suite is still loading
|
||||
* [ExUnit.Case] Ensure `--repeat-until-failure` can be combined with groups
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix compile.elixir] Store compilation results if compilation fails due to `--warnings-as-errors`
|
||||
* [mix deps.loadpaths] Add build lock
|
||||
* [mix escript.build] Ensure build succeeds when protocol consolidation is disabled
|
||||
* [Mix.Shell] Ensure encoding is properly respected on Windows and Unix systems
|
||||
|
||||
## v1.18.0 (2024-12-19)
|
||||
|
||||
### 1. Enhancements
|
||||
|
||||
#### 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
|
||||
|
||||
#### 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
|
||||
|
||||
#### 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
|
||||
|
||||
#### 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
|
||||
|
||||
### 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`
|
||||
|
||||
#### ExUnit
|
||||
|
||||
* [ExUnit.Assertions] Raise if guards are used in `assert/1` with `=`
|
||||
* [ExUnit.Assertions] Format inserted/deleted maps in list assertions
|
||||
|
||||
#### IEx
|
||||
|
||||
* [IEx.Helpers] `IEx.Helpers.recompile/0` will reload modules changed by other operating system processes
|
||||
|
||||
#### 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 archive.install] Restore code paths after `mix archive.install`
|
||||
* [mix compile] Ensure files with duplicate modules are recompiled whenever any of the files change
|
||||
* [mix compile] Update Mix compiler diagnostics documentation and typespecs to match the Elixir compiler behaviour where both lines and columns start from one (before it inaccurately said that columns started from zero)
|
||||
* [mix escript.install] Restore code paths after `mix escript.install`
|
||||
|
||||
### 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
|
||||
|
||||
### 4. Hard deprecations
|
||||
|
||||
#### 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
|
||||
* [File] Deprecate `File.stream!(file, options, line_or_bytes)` in favor of keeping the options as last argument, as in `File.stream!(file, line_or_bytes, options)`
|
||||
* [Kernel.ParallelCompiler] Deprecate `Kernel.ParallelCompiler.async/1` in favor of `Kernel.ParallelCompiler.pmap/2`
|
||||
* [Path] Deprecate `Path.safe_relative_to/2` in favor of `Path.safe_relative/2`
|
||||
|
||||
#### 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 compile] Returning a four-element tuple as a position in `Mix.Task.Compiler.Diagnostic`
|
||||
|
||||
### 5. Potential breaking changes
|
||||
### 4. Hard deprecations
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Kernel] Using unquote is deprecated when quote is used inside a pattern or guard
|
||||
* [Date] Deprecate inferring a range with negative step, call `Date.range/3` with a negative step instead
|
||||
* [Enum] Deprecate passing a range with negative step on `Enum.slice/2`, give `first..last//1` instead
|
||||
* [Kernel] `~R/.../` is deprecated in favor of `~r/.../`. This is because `~R/.../` still allowed escape codes, which did not fit the definition of uppercase sigils
|
||||
* [String] Deprecate passing a range with negative step on `String.slice/2`, give `first..last//1` instead
|
||||
|
||||
## v1.17
|
||||
#### ExUnit
|
||||
|
||||
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).
|
||||
* [ExUnit.Formatter] Deprecate `format_time/2`, use `format_times/1` instead
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix compile.leex] Require `:leex` to be added as a compiler to run the `leex` compiler
|
||||
* [mix compile.yecc] Require `:yecc` to be added as a compiler to run the `yecc` compiler
|
||||
|
||||
## v1.15
|
||||
|
||||
The CHANGELOG for v1.15 releases can be found [in the v1.15 branch](https://github.com/elixir-lang/elixir/blob/v1.15/CHANGELOG.md).
|
||||
|
||||
@@ -5,7 +5,7 @@ MAN_PREFIX ?= $(SHARE_PREFIX)/man
|
||||
# CANONICAL := main/
|
||||
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 +17,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
|
||||
@@ -65,21 +65,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 +83,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 +144,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 +154,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 +173,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" $(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 +306,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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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">
|
||||
|
||||
[](https://github.com/elixir-lang/elixir/actions/workflows/ci.yml?query=branch%3Amain)
|
||||
[](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.
|
||||
@@ -103,35 +99,33 @@ 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`.
|
||||
Additionally, you may choose to run the test suite with `make clean 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 +133,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 +156,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 +166,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 +176,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,8 +196,8 @@ 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:
|
||||
|
||||
+7
-9
@@ -2,23 +2,21 @@
|
||||
|
||||
## 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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
+3
-3
@@ -6,11 +6,11 @@ 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.16 | Bug fixes and security patches
|
||||
1.15 | Security patches only
|
||||
1.14 | Security patches only
|
||||
1.13 | Security patches only
|
||||
1.12 | Security patches only
|
||||
|
||||
## Announcements
|
||||
|
||||
|
||||
+8
-5
@@ -1,7 +1,7 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
ELIXIR_VERSION=1.18.5
|
||||
ELIXIR_VERSION=1.16.3
|
||||
|
||||
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=""
|
||||
|
||||
+70
-28
@@ -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.16.3
|
||||
|
||||
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,25 +97,48 @@ 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)
|
||||
@@ -126,19 +152,35 @@ if ""==!par:--sname=! (set "parsErlang=!parsErlang! -sname "%~1""
|
||||
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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
+2
-2
@@ -317,8 +317,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
|
||||
|
||||
|
||||
+14
-27
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
|
||||
|
||||
@@ -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 %>")
|
||||
|
||||
@@ -534,16 +543,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 ->
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
@@ -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
|
||||
@@ -673,6 +673,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 +796,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 +817,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 +850,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 +889,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) ::
|
||||
|
||||
@@ -67,7 +67,7 @@ defmodule Atom do
|
||||
## Examples
|
||||
|
||||
iex> Atom.to_charlist(:"An atom")
|
||||
~c"An atom"
|
||||
'An atom'
|
||||
|
||||
"""
|
||||
@spec to_charlist(atom) :: charlist
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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).
|
||||
"""
|
||||
@@ -690,8 +687,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 +703,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 +757,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 +998,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 +1083,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: ""
|
||||
|
||||
@@ -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?
|
||||
@@ -88,14 +83,14 @@ 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
|
||||
explicitly say which occurence 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
|
||||
@@ -175,9 +170,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()
|
||||
@@ -902,7 +894,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 +1022,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
|
||||
@@ -1349,11 +1338,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",
|
||||
@@ -1510,11 +1494,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"}
|
||||
@@ -1626,8 +1605,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 +1633,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 +1669,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 +1925,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) <> "]"
|
||||
|
||||
_ ->
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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,23 @@ 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
|
||||
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
|
||||
|
||||
@@ -566,83 +571,6 @@ defmodule NaiveDateTime do
|
||||
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 +682,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 +916,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 +924,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 +943,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 +976,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 +999,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.
|
||||
|
||||
@@ -1260,10 +1145,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,
|
||||
@@ -1465,15 +1346,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: ""
|
||||
|
||||
+28
-117
@@ -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 """
|
||||
@@ -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 """
|
||||
|
||||
+63
-135
@@ -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.
|
||||
|
||||
@@ -164,7 +159,7 @@ defmodule Code do
|
||||
`{: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`
|
||||
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
|
||||
@@ -185,7 +180,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
|
||||
|
||||
@@ -249,8 +244,8 @@ defmodule Code do
|
||||
: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 +507,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
|
||||
@@ -659,17 +659,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 +682,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 +714,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 +798,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 +813,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 +859,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 +936,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 +950,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 +981,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
|
||||
@@ -1107,8 +1074,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)
|
||||
@@ -1163,7 +1128,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,7 +1149,7 @@ 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.
|
||||
|
||||
@@ -1253,7 +1218,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 +1241,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 +1526,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 +1556,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 +1584,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 +1602,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
|
||||
@@ -1682,9 +1631,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 +1653,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 +1682,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 +1721,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 +1965,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
|
||||
@@ -2097,11 +2032,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,
|
||||
@@ -2172,9 +2103,6 @@ defmodule Code do
|
||||
|
||||
{:error, :beam_lib, {:file_error, _, :enoent}} ->
|
||||
{:error, :module_not_found}
|
||||
|
||||
{:error, :beam_lib, _} ->
|
||||
{:error, :invalid_beam}
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -16,7 +16,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 +31,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 +182,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 +208,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 +266,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 +290,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,
|
||||
@@ -377,11 +366,6 @@ 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['''] ->
|
||||
@@ -407,7 +391,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 +399,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 +416,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 +430,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 +447,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 +470,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 +479,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 +499,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 +517,20 @@ 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.18 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) ->
|
||||
iodata =
|
||||
if Macro.classify_atom(atom) in [:identifier, :unquoted] do
|
||||
[Atom.to_string(atom), ?:]
|
||||
else
|
||||
[?", atom |> Atom.to_string() |> String.replace("\"", "\\\""), ?", ?:]
|
||||
end
|
||||
|
||||
{iodata |> IO.iodata_to_binary() |> string() |> color(:atom, state.inspect_opts),
|
||||
state}
|
||||
|
||||
{{:., _, [:erlang, :binary_to_atom]}, _, [{:<<>>, _, entries}, :utf8]} ->
|
||||
interpolation_to_algebra(entries, @double_quote, state, "\"", "\":")
|
||||
@@ -610,11 +554,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 +625,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 +657,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 +694,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 +708,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 +718,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 +731,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 +745,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 +777,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 +946,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 +993,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 +1061,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 +1424,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 +1502,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 +1535,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 +1546,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 +1557,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.18 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 +1579,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 +1592,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 +1609,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 +1617,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
|
||||
@@ -2251,15 +2196,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 +2235,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 +2376,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.
|
||||
@@ -2486,7 +2421,7 @@ defmodule Code.Formatter do
|
||||
end
|
||||
|
||||
defp get_charlist_quotes(:heredoc, state) do
|
||||
if state.migrate_charlists_as_sigils do
|
||||
if state.normalize_charlists_as_sigils do
|
||||
{@sigil_c_heredoc, @double_heredoc}
|
||||
else
|
||||
{@single_heredoc, @single_heredoc}
|
||||
@@ -2495,7 +2430,7 @@ defmodule Code.Formatter do
|
||||
|
||||
defp get_charlist_quotes({:regular, chunks}, state) do
|
||||
cond do
|
||||
!state.migrate_charlists_as_sigils -> {@single_quote, @single_quote}
|
||||
!state.normalize_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
|
||||
@@ -2504,46 +2439,4 @@ defmodule Code.Formatter do
|
||||
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
|
||||
|
||||
+14
-162
@@ -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
|
||||
|
||||
@@ -320,9 +320,6 @@ defmodule Code.Fragment do
|
||||
_ ->
|
||||
{{:local_or_var, acc}, count}
|
||||
end
|
||||
|
||||
{:capture_arg, acc, count} ->
|
||||
{{:capture_arg, acc}, count}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -365,14 +362,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,10 +481,10 @@ 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
|
||||
@@ -558,7 +547,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 +562,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,9 +590,7 @@ 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}
|
||||
@@ -653,16 +640,7 @@ defmodule Code.Fragment do
|
||||
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
|
||||
@@ -678,9 +656,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 +683,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 +715,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)
|
||||
|
||||
@@ -1099,13 +1061,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 +1083,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, emit_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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
@@ -347,14 +356,14 @@ defmodule Code.Normalizer do
|
||||
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]}, _} | _], last) ->
|
||||
# def foo do :ok end
|
||||
# def foo, do: :ok
|
||||
normalize_kw_blocks(form, meta, args, state)
|
||||
|
||||
match?([{:do, _} | _], last) and Keyword.keyword?(last) ->
|
||||
# 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)
|
||||
|
||||
@@ -364,20 +373,11 @@ defmodule Code.Normalizer do
|
||||
|
||||
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
|
||||
|
||||
[] ->
|
||||
@@ -391,12 +391,6 @@ defmodule Code.Normalizer do
|
||||
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)
|
||||
|
||||
@@ -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
|
||||
@@ -283,6 +288,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 +317,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 +338,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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -41,8 +41,7 @@ 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`
|
||||
|
||||
@@ -83,9 +82,9 @@ 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`
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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}
|
||||
|
||||
+122
-251
@@ -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.
|
||||
@@ -269,12 +269,12 @@ defmodule Enum do
|
||||
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
|
||||
@@ -837,7 +837,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]
|
||||
@@ -1096,10 +1096,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
|
||||
@@ -1257,14 +1253,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 +1262,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 +1269,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 +1428,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])
|
||||
@@ -1543,13 +1528,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 +1578,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 +1591,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 +1636,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 +1651,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 +2362,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 +2386,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)
|
||||
@@ -2427,17 +2413,13 @@ defmodule Enum do
|
||||
{:ok, count, fun} when is_function(fun, 1) ->
|
||||
slice_list(fun.(enumerable), random_count(count), 1, 1)
|
||||
|
||||
# TODO: Deprecate me in Elixir v1.18.
|
||||
{:ok, count, fun} when is_function(fun, 2) ->
|
||||
fun.(random_count(count), 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)
|
||||
|
||||
{:error, _} ->
|
||||
take_random(enumerable, 1)
|
||||
end
|
||||
@@ -2717,11 +2699,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 +2733,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 +2750,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 +2790,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 +2902,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 +2916,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`.
|
||||
|
||||
@@ -3353,7 +3332,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 +3437,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 +3463,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 +3484,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 +3588,101 @@ 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_index(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_index(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]
|
||||
|
||||
defp random_index(0), do: 0
|
||||
defp random_index(idx), do: :rand.uniform(idx + 1) - 1
|
||||
|
||||
@doc """
|
||||
Takes the elements from the beginning of the `enumerable` while `fun` returns
|
||||
a truthy value.
|
||||
@@ -3837,9 +3730,6 @@ defmodule Enum do
|
||||
@doc """
|
||||
Enumerates the `enumerable`, removing all duplicate elements.
|
||||
|
||||
The first occurrence of each element is kept and all following
|
||||
duplicates are removed. The overall order is preserved.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Enum.uniq([1, 2, 3, 3, 2, 1])
|
||||
@@ -3865,8 +3755,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
|
||||
|
||||
@@ -3934,13 +3823,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 +3873,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 +3880,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 +4000,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 +4024,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 +4034,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)
|
||||
|
||||
@@ -4175,7 +4059,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
|
||||
@@ -4440,9 +4324,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 +4436,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 +4473,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 +4484,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 +4546,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 +4561,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 +4739,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: []
|
||||
|
||||
+110
-188
@@ -1,18 +1,11 @@
|
||||
defmodule Exception do
|
||||
@moduledoc """
|
||||
Functions for dealing with throw/catch/exit and exceptions.
|
||||
Functions to format throw/catch/exit and exceptions.
|
||||
|
||||
This module also defines the behaviour required by custom
|
||||
exceptions. To define your own, see `defexception/1`.
|
||||
Note that stacktraces in Elixir are only available inside
|
||||
catch and rescue by using the `__STACKTRACE__/0` variable.
|
||||
|
||||
## Formatting functions
|
||||
|
||||
Several functions in this module help format exceptions.
|
||||
Some of these functions expect the stacktrace as argument.
|
||||
The stacktrace is typically available inside catch and
|
||||
rescue by using the `__STACKTRACE__/0` variable.
|
||||
|
||||
Do not rely on the particular format returned by the
|
||||
Do not rely on the particular format returned by the `format*`
|
||||
functions in this module. They may be changed in future releases
|
||||
in order to better suit Elixir's tool chain. In other words,
|
||||
by using the functions in this module it is guaranteed you will
|
||||
@@ -49,7 +42,7 @@ defmodule Exception do
|
||||
@doc """
|
||||
Receives the exception struct and must return its message.
|
||||
|
||||
Many exceptions have a message field which by default is accessed
|
||||
Most commonly exceptions have a message field which by default is accessed
|
||||
by this function. However, if an exception does not have a message field,
|
||||
this function must be explicitly implemented.
|
||||
"""
|
||||
@@ -346,7 +339,7 @@ defmodule Exception do
|
||||
defp rewrite_arg(arg) do
|
||||
Macro.prewalk(arg, fn
|
||||
{:%{}, meta, [__struct__: Range, first: first, last: last, step: step]} ->
|
||||
{:..//, meta, [first, last, step]}
|
||||
{:"..//", meta, [first, last, step]}
|
||||
|
||||
other ->
|
||||
other
|
||||
@@ -407,8 +400,8 @@ defmodule Exception do
|
||||
|
||||
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}
|
||||
{Kernel, fun, args} -> {fun, meta, args}
|
||||
{mod, fun, args} -> {{:., [], [mod, fun]}, meta, args}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1048,6 +1041,44 @@ defmodule ArgumentError do
|
||||
"""
|
||||
|
||||
defexception message: "argument error"
|
||||
|
||||
@impl true
|
||||
def blame(
|
||||
exception,
|
||||
[{:erlang, :apply, [module, function, args], _} | _] = stacktrace
|
||||
) do
|
||||
message =
|
||||
cond do
|
||||
not proper_list?(args) ->
|
||||
"you attempted to apply a function named #{inspect(function)} on module #{inspect(module)} " <>
|
||||
"with arguments #{inspect(args)}. Arguments (the third argument of apply) must always be a proper list"
|
||||
|
||||
# Note that args may be an empty list even if they were supplied
|
||||
not is_atom(module) and is_atom(function) and args == [] ->
|
||||
"you attempted to apply a function named #{inspect(function)} on #{inspect(module)}. " <>
|
||||
"If you are using Kernel.apply/3, make sure the module is an atom. " <>
|
||||
"If you are using the dot syntax, such as module.function(), " <>
|
||||
"make sure the left-hand side of the dot is a module atom"
|
||||
|
||||
not is_atom(module) ->
|
||||
"you attempted to apply a function on #{inspect(module)}. " <>
|
||||
"Modules (the first argument of apply) must always be an atom"
|
||||
|
||||
not is_atom(function) ->
|
||||
"you attempted to apply a function named #{inspect(function)} on module #{inspect(module)}. " <>
|
||||
"However, #{inspect(function)} is not a valid function name. Function names (the second argument " <>
|
||||
"of apply) must always be an atom"
|
||||
end
|
||||
|
||||
{%{exception | message: message}, stacktrace}
|
||||
end
|
||||
|
||||
def blame(exception, stacktrace) do
|
||||
{exception, stacktrace}
|
||||
end
|
||||
|
||||
defp proper_list?(list) when length(list) >= 0, do: true
|
||||
defp proper_list?(_), do: false
|
||||
end
|
||||
|
||||
defmodule ArithmeticError do
|
||||
@@ -1212,7 +1243,7 @@ defmodule SyntaxError do
|
||||
})
|
||||
when not is_nil(snippet) and not is_nil(column) do
|
||||
snippet =
|
||||
:elixir_errors.format_snippet(:error, {line, column}, file, description, snippet, %{})
|
||||
:elixir_errors.format_snippet({line, column}, file, description, snippet, :error, [], nil)
|
||||
|
||||
format_message(file, line, column, snippet)
|
||||
end
|
||||
@@ -1225,7 +1256,7 @@ defmodule SyntaxError do
|
||||
description: description
|
||||
}) do
|
||||
snippet =
|
||||
:elixir_errors.format_snippet(:error, {line, column}, file, description, nil, %{})
|
||||
:elixir_errors.format_snippet({line, column}, file, description, nil, :error, [], nil)
|
||||
|
||||
padded = " " <> String.replace(snippet, "\n", "\n ")
|
||||
format_message(file, line, column, padded)
|
||||
@@ -1316,7 +1347,7 @@ defmodule TokenMissingError do
|
||||
description: description
|
||||
}) do
|
||||
snippet =
|
||||
:elixir_errors.format_snippet(:error, {line, column}, file, description, snippet, %{})
|
||||
:elixir_errors.format_snippet({line, column}, file, description, snippet, :error, [], nil)
|
||||
|
||||
format_message(file, line, column, snippet)
|
||||
end
|
||||
@@ -1581,15 +1612,13 @@ defmodule UndefinedFunctionError do
|
||||
|
||||
"""
|
||||
|
||||
@function_threshold 0.77
|
||||
@max_suggestions 5
|
||||
defexception [:module, :function, :arity, :reason, :message]
|
||||
|
||||
@impl true
|
||||
def message(%{message: nil} = exception) do
|
||||
%{reason: reason, module: module, function: function, arity: arity} = exception
|
||||
{message, hint_type} = message(reason, module, function, arity)
|
||||
message <> if(hint_type == :suggest_module, do: hint_for_missing_alias(module), else: "")
|
||||
{message, _hint_type} = message(reason, module, function, arity)
|
||||
message
|
||||
end
|
||||
|
||||
def message(%{message: message}) do
|
||||
@@ -1650,25 +1679,12 @@ defmodule UndefinedFunctionError do
|
||||
end
|
||||
|
||||
defp hint(module, function, arity, :suggest_function) do
|
||||
hint_for_behaviour(module, function, arity) <>
|
||||
hint_for_loaded_module(module, function, arity)
|
||||
behaviour_hint(module, function, arity) <>
|
||||
hint_for_loaded_module(module, function, arity, nil)
|
||||
end
|
||||
|
||||
@max_suggestions 5
|
||||
defp hint(module, function, arity, :suggest_module) do
|
||||
hint_for_missing_module(module, function, arity)
|
||||
end
|
||||
|
||||
defp hint_for_missing_alias(module) do
|
||||
with "Elixir." <> rest <- Atom.to_string(module),
|
||||
false <- rest =~ "." do
|
||||
". Make sure the module name is correct and has been specified in full (or that an alias has been defined)"
|
||||
else
|
||||
_ -> ""
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def hint_for_missing_module(module, function, arity) do
|
||||
downcased_module = downcase_module_name(module)
|
||||
stripped_module = module |> Atom.to_string() |> String.replace_leading("Elixir.", "")
|
||||
|
||||
@@ -1689,9 +1705,9 @@ defmodule UndefinedFunctionError do
|
||||
["\n * ", Exception.format_mfa(module, function, arity)]
|
||||
end)
|
||||
|
||||
". Did you mean:\n#{suggestions}\n"
|
||||
IO.iodata_to_binary([". Did you mean:\n", suggestions, "\n"])
|
||||
else
|
||||
hint_for_missing_alias(module)
|
||||
""
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1708,7 +1724,7 @@ defmodule UndefinedFunctionError do
|
||||
end
|
||||
|
||||
@doc false
|
||||
def hint_for_loaded_module(module, function, arity) do
|
||||
def hint_for_loaded_module(module, function, arity, exports) do
|
||||
cond do
|
||||
macro_exported?(module, function, arity) ->
|
||||
". However, there is a macro with the same name and arity. " <>
|
||||
@@ -1718,7 +1734,7 @@ defmodule UndefinedFunctionError do
|
||||
", #{message}"
|
||||
|
||||
true ->
|
||||
IO.iodata_to_binary(did_you_mean(module, function))
|
||||
IO.iodata_to_binary(did_you_mean(module, function, exports))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1729,39 +1745,11 @@ defmodule UndefinedFunctionError do
|
||||
end
|
||||
end
|
||||
|
||||
defp hint_for_behaviour(module, function, arity) do
|
||||
case behaviours_for(module) do
|
||||
[] ->
|
||||
""
|
||||
@function_threshold 0.77
|
||||
@max_suggestions 5
|
||||
|
||||
behaviours ->
|
||||
case Enum.find(behaviours, &expects_callback?(&1, function, arity)) do
|
||||
nil -> ""
|
||||
behaviour -> ", but the behaviour #{inspect(behaviour)} expects it to be present"
|
||||
end
|
||||
end
|
||||
rescue
|
||||
# In case the module was removed while we are computing this
|
||||
UndefinedFunctionError -> ""
|
||||
end
|
||||
|
||||
defp behaviours_for(module) do
|
||||
:attributes
|
||||
|> module.module_info()
|
||||
|> Keyword.get(:behaviour, [])
|
||||
end
|
||||
|
||||
defp expects_callback?(behaviour, function, arity) do
|
||||
callbacks =
|
||||
behaviour.behaviour_info(:callbacks) -- behaviour.behaviour_info(:optional_callbacks)
|
||||
|
||||
Enum.member?(callbacks, {function, arity})
|
||||
end
|
||||
|
||||
## Shared helpers across hints
|
||||
|
||||
defp did_you_mean(module, function) do
|
||||
exports = exports_for(module)
|
||||
defp did_you_mean(module, function, exports) do
|
||||
exports = exports || exports_for(module)
|
||||
|
||||
result =
|
||||
case Keyword.take(exports, [function]) do
|
||||
@@ -1791,6 +1779,35 @@ defmodule UndefinedFunctionError do
|
||||
[" * ", Macro.inspect_atom(:remote_call, fun), ?/, Integer.to_string(arity), ?\n]
|
||||
end
|
||||
|
||||
defp behaviour_hint(module, function, arity) do
|
||||
case behaviours_for(module) do
|
||||
[] ->
|
||||
""
|
||||
|
||||
behaviours ->
|
||||
case Enum.find(behaviours, &expects_callback?(&1, function, arity)) do
|
||||
nil -> ""
|
||||
behaviour -> ", but the behaviour #{inspect(behaviour)} expects it to be present"
|
||||
end
|
||||
end
|
||||
rescue
|
||||
# In case the module was removed while we are computing this
|
||||
UndefinedFunctionError -> ""
|
||||
end
|
||||
|
||||
defp behaviours_for(module) do
|
||||
:attributes
|
||||
|> module.module_info()
|
||||
|> Keyword.get(:behaviour, [])
|
||||
end
|
||||
|
||||
defp expects_callback?(behaviour, function, arity) do
|
||||
callbacks =
|
||||
behaviour.behaviour_info(:callbacks) -- behaviour.behaviour_info(:optional_callbacks)
|
||||
|
||||
Enum.member?(callbacks, {function, arity})
|
||||
end
|
||||
|
||||
defp exports_for(module) do
|
||||
if function_exported?(module, :__info__, 1) do
|
||||
module.__info__(:macros) ++ module.__info__(:functions)
|
||||
@@ -1799,7 +1816,8 @@ defmodule UndefinedFunctionError do
|
||||
end
|
||||
rescue
|
||||
# In case the module was removed while we are computing this
|
||||
UndefinedFunctionError -> []
|
||||
UndefinedFunctionError ->
|
||||
[]
|
||||
end
|
||||
|
||||
defp deprecated_functions_for(module) do
|
||||
@@ -1810,7 +1828,8 @@ defmodule UndefinedFunctionError do
|
||||
end
|
||||
rescue
|
||||
# In case the module was removed while we are computing this
|
||||
UndefinedFunctionError -> []
|
||||
UndefinedFunctionError ->
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1997,23 +2016,8 @@ defmodule Protocol.UndefinedError do
|
||||
|
||||
@impl true
|
||||
def message(%{protocol: protocol, value: value, description: description}) do
|
||||
inspected =
|
||||
value
|
||||
|> inspect(pretty: true)
|
||||
# Indent only lines with contents on them
|
||||
|> String.replace(~r/^(?=.+)/m, " ")
|
||||
|
||||
"protocol #{inspect(protocol)} not implemented for type " <>
|
||||
value_type(value) <>
|
||||
maybe_description(description) <>
|
||||
maybe_available(protocol) <>
|
||||
"""
|
||||
|
||||
|
||||
Got value:
|
||||
|
||||
#{inspected}
|
||||
"""
|
||||
"protocol #{inspect(protocol)} not implemented for #{inspect(value)} of type " <>
|
||||
value_type(value) <> maybe_description(description) <> maybe_available(protocol)
|
||||
end
|
||||
|
||||
defp value_type(%{__struct__: struct}), do: "#{inspect(struct)} (a struct)"
|
||||
@@ -2070,17 +2074,10 @@ defmodule KeyError do
|
||||
defp message(key, term) do
|
||||
message = "key #{inspect(key)} not found"
|
||||
|
||||
cond do
|
||||
term == nil ->
|
||||
message
|
||||
|
||||
is_atom(term) and is_atom(key) ->
|
||||
message <>
|
||||
" in: #{inspect(term)} (if instead you want to invoke #{inspect(term)}.#{key}(), " <>
|
||||
"make sure to add parentheses after the function name)"
|
||||
|
||||
true ->
|
||||
message <> " in: #{inspect(term, pretty: true, limit: :infinity)}"
|
||||
if term != nil do
|
||||
message <> " in: #{inspect(term, pretty: true, limit: :infinity)}"
|
||||
else
|
||||
message
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2089,6 +2086,11 @@ defmodule KeyError do
|
||||
{exception, stacktrace}
|
||||
end
|
||||
|
||||
def blame(exception = %{term: nil}, stacktrace) do
|
||||
message = message(exception.key, exception.term)
|
||||
{%{exception | message: message}, stacktrace}
|
||||
end
|
||||
|
||||
def blame(exception, stacktrace) do
|
||||
%{term: term, key: key} = exception
|
||||
message = message(key, term)
|
||||
@@ -2158,56 +2160,6 @@ defmodule UnicodeConversionError do
|
||||
end
|
||||
end
|
||||
|
||||
defmodule MissingApplicationsError do
|
||||
@moduledoc """
|
||||
An exception that is raised when an application depends on one or more
|
||||
missing applications.
|
||||
|
||||
This exception is used by Mix and other tools. It can also be used by library authors
|
||||
when their library only requires an external application (like a dependency) for a subset
|
||||
of features.
|
||||
|
||||
The fields of this exception are public. See `t:t/0`.
|
||||
|
||||
*Available since v1.18.0.*
|
||||
|
||||
## Examples
|
||||
|
||||
unless Application.spec(:plug, :vsn) do
|
||||
raise MissingApplicationsError,
|
||||
description: "application :plug is required for testing Plug-related functionality",
|
||||
apps: [{:plug, "~> 1.0"}]
|
||||
end
|
||||
|
||||
"""
|
||||
|
||||
@moduledoc since: "1.18.0"
|
||||
|
||||
@type t() :: %__MODULE__{
|
||||
apps: [{Application.app(), Version.requirement()}, ...],
|
||||
description: String.t()
|
||||
}
|
||||
|
||||
defexception apps: [], description: "missing applications found"
|
||||
|
||||
@impl true
|
||||
def message(%__MODULE__{apps: apps, description: description}) do
|
||||
# We explicitly format these as tuples so that they're easier to copy-paste
|
||||
# into dependencies.
|
||||
formatted_apps =
|
||||
Enum.map(apps, fn {app_name, requirement} ->
|
||||
~s(\n {#{inspect(app_name)}, "#{requirement}"})
|
||||
end)
|
||||
|
||||
"""
|
||||
#{description}
|
||||
|
||||
To address this, include these applications as your dependencies:
|
||||
#{formatted_apps}\
|
||||
"""
|
||||
end
|
||||
end
|
||||
|
||||
defmodule Enum.OutOfBoundsError do
|
||||
@moduledoc """
|
||||
An exception that is raised when a function expects an enumerable to have
|
||||
@@ -2355,39 +2307,9 @@ defmodule ErlangError do
|
||||
|
||||
@doc false
|
||||
def normalize(:badarg, stacktrace) do
|
||||
case stacktrace do
|
||||
[{:erlang, :apply, [module, function, args], _} | _] when not is_atom(module) ->
|
||||
message =
|
||||
cond do
|
||||
is_map(module) and is_atom(function) and is_map_key(module, function) ->
|
||||
"you attempted to apply a function named #{inspect(function)} on a map/struct. " <>
|
||||
"If you are using Kernel.apply/3, make sure the module is an atom. " <>
|
||||
if is_function(module[function]) do
|
||||
"If you are trying to invoke an anonymous function in a map/struct, " <>
|
||||
"add a dot between the function name and the parenthesis: map.#{function}.()"
|
||||
else
|
||||
"If you are using the dot syntax, ensure there are no parentheses " <>
|
||||
"after the field name, such as map.#{function}"
|
||||
end
|
||||
|
||||
is_atom(function) and args == [] ->
|
||||
"you attempted to apply a function named #{inspect(function)} on #{inspect(module)}. " <>
|
||||
"If you are using Kernel.apply/3, make sure the module is an atom. " <>
|
||||
"If you are using the dot syntax, such as module.function(), " <>
|
||||
"make sure the left-hand side of the dot is an atom representing a module"
|
||||
|
||||
true ->
|
||||
"you attempted to apply a function on #{inspect(module)}. " <>
|
||||
"Modules (the first argument of apply) must always be an atom"
|
||||
end
|
||||
|
||||
%ArgumentError{message: message}
|
||||
|
||||
_ ->
|
||||
case error_info(:badarg, stacktrace, "errors were found at the given arguments") do
|
||||
{:ok, reason, details} -> %ArgumentError{message: reason <> details}
|
||||
:error -> %ArgumentError{}
|
||||
end
|
||||
case error_info(:badarg, stacktrace, "errors were found at the given arguments") do
|
||||
{:ok, reason, details} -> %ArgumentError{message: reason <> details}
|
||||
:error -> %ArgumentError{}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2530,7 +2452,7 @@ defmodule ErlangError do
|
||||
{:ok, reason, IO.iodata_to_binary([":\n\n" | Enum.map(args_errors, &arg_error/1)])}
|
||||
|
||||
general = extra[:general] ->
|
||||
{:ok, reason, ": " <> IO.chardata_to_string(general)}
|
||||
{:ok, reason, ": " <> general}
|
||||
|
||||
true ->
|
||||
:error
|
||||
|
||||
+29
-44
@@ -101,7 +101,6 @@ defmodule File do
|
||||
|
||||
@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
|
||||
@@ -759,7 +758,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 +822,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 +895,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 +904,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 +1363,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 +1389,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 +1404,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 +1446,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 +1467,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 +1502,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 +1524,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 +1544,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} ->
|
||||
|
||||
+13
-13
@@ -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 """
|
||||
|
||||
+21
-21
@@ -139,8 +139,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")
|
||||
@@ -212,7 +210,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 +218,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
|
||||
|
||||
@@ -256,7 +254,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 +262,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.
|
||||
@@ -309,7 +307,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 +315,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.
|
||||
|
||||
@@ -577,23 +575,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 +614,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 +621,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
|
||||
|
||||
@@ -22,7 +22,7 @@ defmodule GenEvent do
|
||||
This approach has some shortcomings (it provides no back-pressure 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
|
||||
@@ -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
|
||||
|
||||
@@ -400,7 +400,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 +415,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)
|
||||
@@ -523,8 +523,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 +759,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 +783,6 @@ defmodule GenServer do
|
||||
handle_info: 2,
|
||||
handle_cast: 2,
|
||||
handle_call: 3,
|
||||
format_status: 1,
|
||||
format_status: 2,
|
||||
handle_continue: 2
|
||||
|
||||
@@ -845,7 +827,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 +927,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 +1000,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 +1010,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 +1102,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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+112
-158
@@ -139,85 +139,6 @@ defprotocol Inspect do
|
||||
# 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 +155,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 +175,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 +186,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 +228,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 +259,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 +281,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 +301,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 +322,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 +339,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 +349,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,47 +387,33 @@ 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, ?/>>)
|
||||
@@ -562,7 +449,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 +534,73 @@ defimpl Inspect, for: Reference do
|
||||
end
|
||||
|
||||
defimpl Inspect, for: Any do
|
||||
defmacro __deriving__(module, struct, options) do
|
||||
fields = Enum.sort(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 == 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))
|
||||
|
||||
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 +622,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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -333,7 +333,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 +438,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 +455,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 +525,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 +588,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 +604,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 +918,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 +929,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 +973,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 +995,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 +1062,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 +1073,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 +1103,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 +1136,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
@@ -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
|
||||
|
||||
+17
-40
@@ -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
|
||||
|
||||
@@ -296,8 +282,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!")
|
||||
@@ -394,7 +378,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 +408,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 +424,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 +556,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:
|
||||
@@ -606,9 +587,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.
|
||||
@@ -661,17 +641,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.
|
||||
|
||||
@@ -268,7 +268,7 @@ 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.
|
||||
@@ -299,7 +299,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]
|
||||
|
||||
"""
|
||||
|
||||
@@ -221,6 +221,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 +354,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 +365,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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
+267
-547
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -18,36 +18,23 @@ defmodule Kernel.ParallelCompiler do
|
||||
# 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
|
||||
case :erlang.get(:elixir_compiler_info) do
|
||||
{compiler_pid, file_pid} ->
|
||||
ref = make_ref()
|
||||
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_pid, {:async, self()})
|
||||
Module.ParallelChecker.put(compiler_pid, checker)
|
||||
: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)
|
||||
fun.()
|
||||
end)
|
||||
|
||||
:undefined ->
|
||||
raise ArgumentError,
|
||||
@@ -65,29 +52,41 @@ defmodule Kernel.ParallelCompiler do
|
||||
"""
|
||||
@doc since: "1.16.0"
|
||||
def pmap(collection, fun) when is_function(fun, 1) do
|
||||
parent = self()
|
||||
ref = make_ref()
|
||||
|
||||
# We spawn a series of tasks for parallel processing.
|
||||
# The tasks are waiting until we give the go ahead.
|
||||
refs_tasks =
|
||||
# The tasks notify themselves to the compiler.
|
||||
tasks =
|
||||
Enum.map(collection, fn item ->
|
||||
inner_async(fn -> fun.(item) end)
|
||||
async(fn ->
|
||||
send(parent, {ref, self()})
|
||||
|
||||
receive do
|
||||
^ref -> fun.(item)
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
# Then the tasks notify us. This is important because if
|
||||
# we wait before the tasks notify the compiler, we may be
|
||||
# released as there is nothing else running.
|
||||
on =
|
||||
for %{pid: pid} <- tasks do
|
||||
receive do
|
||||
{^ref, ^pid} -> pid
|
||||
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)
|
||||
Enum.each(on, &send(&1, ref))
|
||||
|
||||
# Await tasks and notify the compiler they are done. We could
|
||||
# have the tasks report directly to the compiler, which in turn
|
||||
@@ -158,10 +157,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 +203,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 +238,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 +289,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 +346,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 +413,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)
|
||||
|
||||
@@ -456,19 +461,14 @@ defmodule Kernel.ParallelCompiler do
|
||||
end)
|
||||
|
||||
[] = 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,15 +503,13 @@ 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)
|
||||
@@ -742,9 +740,6 @@ defmodule Kernel.ParallelCompiler do
|
||||
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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -548,7 +550,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 +674,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 +888,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 +915,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**.
|
||||
|
||||
@@ -1046,7 +1048,7 @@ defmodule Kernel.SpecialForms do
|
||||
end
|
||||
end
|
||||
|
||||
require ContextHygiene
|
||||
require Hygiene
|
||||
ContextHygiene.write()
|
||||
ContextHygiene.read()
|
||||
#=> 1
|
||||
@@ -1442,7 +1444,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 +1494,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,7 +1539,7 @@ 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
|
||||
@@ -1622,7 +1622,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 +1661,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 +1754,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 +1777,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 +1876,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 +1906,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 +1965,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 +1987,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 +2300,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 +2317,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) ->
|
||||
|
||||
@@ -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,7 +380,7 @@ 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
|
||||
@@ -402,7 +401,7 @@ defmodule Kernel.Typespec do
|
||||
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
|
||||
@@ -493,24 +492,6 @@ defmodule Kernel.Typespec do
|
||||
state}
|
||||
end
|
||||
|
||||
defp typespec(
|
||||
{:<<>>, meta, [{:"::", _, [{:_, _, ctx1}, {:*, prod_meta, [size, unit]}]}]},
|
||||
_,
|
||||
_,
|
||||
state
|
||||
)
|
||||
when is_atom(ctx1) and is_integer(size) and size >= 0 and unit in 1..256 do
|
||||
location = location(meta)
|
||||
prod_location = location(prod_meta)
|
||||
|
||||
{{:type, location, :binary,
|
||||
[
|
||||
{:op, prod_location, :*, {:integer, prod_location, size},
|
||||
{:integer, prod_location, unit}},
|
||||
{:integer, location, 0}
|
||||
]}, state}
|
||||
end
|
||||
|
||||
defp typespec({:<<>>, meta, [{:"::", size_meta, [{:_, _, ctx}, size]}]}, _, _, state)
|
||||
when is_atom(ctx) and is_integer(size) and size >= 0 do
|
||||
location = location(meta)
|
||||
@@ -586,23 +567,27 @@ defmodule Kernel.Typespec do
|
||||
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 +596,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 +630,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
|
||||
@@ -686,7 +671,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, location(meta), :fun, fun_args}, state}
|
||||
end
|
||||
|
||||
# Handle type operator
|
||||
@@ -762,7 +754,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)})"
|
||||
|
||||
@@ -925,11 +917,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 +948,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
|
||||
|
||||
@@ -103,7 +103,7 @@ defmodule Kernel.Utils do
|
||||
def defstruct(module, fields, bootstrapped?, env) 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 ->
|
||||
@@ -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,
|
||||
@@ -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
|
||||
|
||||
@@ -6,7 +6,7 @@ defmodule Keyword do
|
||||
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)
|
||||
to keywords and how the compare with maps, see our [Keyword and Maps](keywords-and-maps.md)
|
||||
guide.
|
||||
|
||||
## Examples
|
||||
@@ -102,9 +102,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}]
|
||||
|
||||
@@ -391,7 +388,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 +957,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 +1289,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 +1405,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}
|
||||
|
||||
+50
-61
@@ -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,34 +924,6 @@ 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.
|
||||
|
||||
@@ -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) ->
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+126
-306
@@ -146,6 +146,8 @@ defmodule Macro do
|
||||
* `:delimiter` - contains the opening delimiter for sigils, strings,
|
||||
and charlists as a string (such as `"{"`, `"/"`, `"'"`, and the like)
|
||||
|
||||
* `:format` - set to `:keyword` when an atom is defined as a keyword
|
||||
|
||||
* `:do` - contains metadata about the `do` location in a function call with
|
||||
`do`-`end` blocks (when `:token_metadata` is true)
|
||||
|
||||
@@ -154,21 +156,12 @@ defmodule Macro do
|
||||
|
||||
* `: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`
|
||||
direct children of a `__block__`, and it is either the location of a
|
||||
newline or of the `;` character. The last expression of `__block__`
|
||||
does not have this metadata.
|
||||
|
||||
* `: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 +370,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
|
||||
|
||||
@@ -501,8 +494,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 +777,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 +815,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 +828,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 +896,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 +1126,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 +1133,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 +1484,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)
|
||||
@@ -1730,7 +1713,7 @@ defmodule Macro do
|
||||
@doc """
|
||||
Applies a `mod`, `function`, and `args` at compile-time in `caller`.
|
||||
|
||||
This is used when you want to programmatically invoke a macro at
|
||||
This is used when you want to programatically invoke a macro at
|
||||
compile-time.
|
||||
"""
|
||||
@doc since: "1.16.0"
|
||||
@@ -1751,9 +1734,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 +1805,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 +1832,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 +1846,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 +1903,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 +1956,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 +1967,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 +2159,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 +2195,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
|
||||
@@ -2334,15 +2339,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 +2387,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 +2406,7 @@ defmodule Macro do
|
||||
end
|
||||
|
||||
:quoted ->
|
||||
escaped = inspect_atom_escape(opts, binary, ?")
|
||||
{escaped, _} = Code.Identifier.escape(binary, ?")
|
||||
IO.iodata_to_binary([?:, ?", escaped, ?"])
|
||||
|
||||
_ ->
|
||||
@@ -2418,7 +2414,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 +2422,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 +2430,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 +2442,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 +2475,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 +2573,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 +2612,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 +2620,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 +2642,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 +2663,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 +2695,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 +2732,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 +2776,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
|
||||
|
||||
+43
-357
@@ -6,15 +6,9 @@ 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
|
||||
@@ -22,9 +16,10 @@ defmodule Macro.Env do
|
||||
__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, __ENV__)
|
||||
def __struct__(), do: unquote(:elixir_quote.escape(struct, false, :none))
|
||||
def __struct__(unquote(kv)), do: unquote(body)
|
||||
|
||||
@doc """
|
||||
@@ -170,13 +163,43 @@ 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(env, atom)
|
||||
|
||||
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(env, atom)
|
||||
|
||||
def fetch_macro_alias(%{__struct__: Macro.Env, macro_aliases: aliases}, atom)
|
||||
when is_atom(atom),
|
||||
do: Keyword.fetch(aliases, :"Elixir.#{atom}")
|
||||
@@ -189,14 +212,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})
|
||||
@@ -288,337 +303,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
|
||||
|
||||
+16
-13
@@ -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`.
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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}}
|
||||
|
||||
+73
-115
@@ -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
|
||||
@@ -444,8 +441,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 +538,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 +588,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 +626,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.
|
||||
@@ -746,10 +667,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 +751,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 +825,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 +869,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 +880,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 +905,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 +915,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)
|
||||
@@ -1424,7 +1379,15 @@ defmodule Module do
|
||||
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 +1416,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 +1441,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),
|
||||
@@ -1677,11 +1641,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 +1908,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 +1958,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 +1995,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 +2017,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
|
||||
@@ -2266,7 +2224,7 @@ defmodule Module do
|
||||
end
|
||||
|
||||
defp preprocess_attribute(:nifs, value) do
|
||||
if not function_arity_list?(value) do
|
||||
unless 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)}"
|
||||
|
||||
@@ -0,0 +1,258 @@
|
||||
# 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_position(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, file) do
|
||||
undefined =
|
||||
for {pair, _, meta, _} <- all_defined,
|
||||
{{local_name, _} = local, position, macro_dispatch?} <-
|
||||
out_neighbours(bag, {:local, pair}),
|
||||
error = undefined_local_error(set, local, macro_dispatch?) do
|
||||
file =
|
||||
case Keyword.get(meta, :file) do
|
||||
{keep_file, _keep_line} -> keep_file
|
||||
nil -> file
|
||||
end
|
||||
|
||||
meta = build_meta(position, local_name)
|
||||
{pair, meta, file, local, error}
|
||||
end
|
||||
|
||||
: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 get_position(meta), do: {get_line(meta), meta[:column]}
|
||||
|
||||
defp build_meta(position, function_name) do
|
||||
case position do
|
||||
{line, nil} -> [line: line]
|
||||
{line, col} -> :elixir_env.calculate_span([line: line, column: col], function_name)
|
||||
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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -294,60 +291,42 @@ defmodule Module.ParallelChecker do
|
||||
## 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 print_warning(message, diagnostics)
|
||||
diagnostics
|
||||
end)
|
||||
end
|
||||
|
||||
defp print_diagnostics([diagnostic]) do
|
||||
:elixir_errors.print_diagnostic(diagnostic, true)
|
||||
defp print_warning(message, [diagnostic]) do
|
||||
:elixir_errors.print_warning(message, diagnostic)
|
||||
end
|
||||
|
||||
defp print_diagnostics(diagnostics) do
|
||||
:elixir_errors.print_diagnostics(diagnostics)
|
||||
defp print_warning(message, grouped_warnings) do
|
||||
:elixir_errors.print_warning_group(message, grouped_warnings)
|
||||
end
|
||||
|
||||
defp to_diagnostic(diagnostic, source, {file, position, mfa}) when is_list(position) do
|
||||
file = Path.absname(file)
|
||||
|
||||
defp to_diagnostic(message, {file, position, mfa}) when is_list(position) do
|
||||
%{
|
||||
severity: :warning,
|
||||
source: source,
|
||||
source: file,
|
||||
file: file,
|
||||
position: position_to_tuple(position),
|
||||
message: IO.iodata_to_binary(message),
|
||||
stacktrace: [to_stacktrace(file, position, mfa)],
|
||||
span: nil
|
||||
}
|
||||
|> Map.merge(diagnostic)
|
||||
end
|
||||
|
||||
defp position_to_tuple(position) do
|
||||
@@ -372,15 +351,15 @@ defmodule Module.ParallelChecker do
|
||||
|
||||
## Cache
|
||||
|
||||
defp cache_module({checker, table}, module) do
|
||||
if lock(checker, module) do
|
||||
defp cache_module({server, ets}, module) do
|
||||
if lock(server, module) do
|
||||
object_code = :code.get_object_code(module)
|
||||
|
||||
# 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)
|
||||
cache_chunk(ets, module, contents.exports)
|
||||
else
|
||||
_ ->
|
||||
# Otherwise, if the module is loaded, use its info
|
||||
@@ -388,28 +367,37 @@ defmodule Module.ParallelChecker do
|
||||
true ->
|
||||
{mode, exports} = info_exports(module)
|
||||
deprecated = info_deprecated(module)
|
||||
cache_info(table, module, exports, deprecated, %{}, mode)
|
||||
cache_info(ets, module, exports, deprecated, mode)
|
||||
|
||||
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)
|
||||
exports = Map.new(Enum.map(exports, &{&1, :def}))
|
||||
cache_info(ets, module, exports, %{}, :erlang)
|
||||
else
|
||||
_ ->
|
||||
:ets.insert(table, {module, false})
|
||||
:ets.insert(ets, {{:cached, module}, false})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
unlock(checker, module)
|
||||
unlock(server, module)
|
||||
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 +406,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 +485,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 +512,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 +529,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
|
||||
|
||||
+463
-331
@@ -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,475 @@ 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_position(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_position(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_position(stack) do
|
||||
get_meta(stack.last_expr)
|
||||
end
|
||||
|
||||
## Diagnostics
|
||||
# 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()
|
||||
|
||||
def format_diagnostic({:unused_clause, kind, {fun, arity}}) do
|
||||
%{
|
||||
message: "this clause of #{kind} #{fun}/#{arity} is never used"
|
||||
}
|
||||
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
|
||||
|
||||
## Module errors
|
||||
defp related_variables(var, types) do
|
||||
Enum.flat_map(types, fn
|
||||
{related_var, {:var, ^var}} ->
|
||||
[related_var | related_variables(related_var, types)]
|
||||
|
||||
def format_error({:unused_args, {name, arity}}),
|
||||
do: "default values for the optional arguments in #{name}/#{arity} are never used"
|
||||
_ ->
|
||||
[]
|
||||
end)
|
||||
end
|
||||
|
||||
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"
|
||||
# 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}, count}),
|
||||
do:
|
||||
"the default values for the last #{arity - count} 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_def, {name, arity}, :defp}),
|
||||
do: "function #{name}/#{arity} is unused"
|
||||
{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_def, {name, arity}, :defmacrop}),
|
||||
do: "macro #{name}/#{arity} is unused"
|
||||
{types, traces}
|
||||
end
|
||||
|
||||
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})"
|
||||
## FORMAT WARNINGS
|
||||
|
||||
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)"
|
||||
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({:incorrect_dispatch, {f, a}, _module}),
|
||||
do: "cannot invoke macro #{f}/#{a} before its definition"
|
||||
clauses =
|
||||
Enum.map(signature, fn {ins, out} ->
|
||||
{_, _, ins} = erl_to_ex(original_module, original_function, ins, [])
|
||||
|
||||
{:fun, [{ins, out}]}
|
||||
|> Unify.format_type(false)
|
||||
|> IO.iodata_to_binary()
|
||||
|> binary_slice(1..-2//1)
|
||||
end)
|
||||
|
||||
[
|
||||
"expected #{format_mfa} to have signature:\n\n ",
|
||||
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, position, _mfa}) do
|
||||
format_location({file, position[: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 hint, do: :elixir_errors.prefix(:hint)
|
||||
|
||||
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,7 +33,9 @@ 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
|
||||
@@ -55,8 +57,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)
|
||||
@@ -142,7 +144,7 @@ defmodule Module.Behaviour do
|
||||
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
|
||||
|
||||
@@ -264,6 +266,13 @@ defmodule Module.Behaviour do
|
||||
". 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 +284,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,12 +294,12 @@ 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 ",
|
||||
inspect(behaviour),
|
||||
@@ -306,7 +311,7 @@ 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 ",
|
||||
format_definition(kind, callback),
|
||||
@@ -320,7 +325,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 +334,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 +347,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 +364,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 +373,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 +382,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 +392,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 +403,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
+443
-662
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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.titlecase()
|
||||
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.titlecase()
|
||||
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, {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
@@ -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
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
@@ -152,47 +152,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 +862,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"
|
||||
|
||||
|
||||
@@ -96,7 +96,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,
|
||||
@@ -151,7 +151,7 @@ defmodule PartitionSupervisor do
|
||||
@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 +228,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 +260,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 +282,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 +297,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 +332,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 +374,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 +428,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
|
||||
|
||||
+4
-13
@@ -64,9 +64,6 @@ 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
|
||||
def absname(path, relative_to) do
|
||||
@@ -172,17 +169,11 @@ 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
|
||||
@@ -267,7 +258,7 @@ defmodule Path do
|
||||
Path.relative("/bar/foo.ex") #=> "bar/foo.ex"
|
||||
|
||||
"""
|
||||
# Note this function does not expand paths because the behavior
|
||||
# Note this function does not expand paths because the behaviour
|
||||
# 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
|
||||
@@ -884,7 +875,7 @@ defmodule Path do
|
||||
* A `..` component would make it so that the path would traverse up above
|
||||
the root of `relative_to`.
|
||||
|
||||
* A symbolic link in the path points to something above the root of `relative_to`.
|
||||
* A symbolic link in the path points to something above the root of `cwd`.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -906,10 +897,10 @@ defmodule Path do
|
||||
"""
|
||||
@doc since: "1.14.0"
|
||||
@spec safe_relative(t, t) :: {:ok, binary} | :error
|
||||
def safe_relative(path, relative_to \\ File.cwd!()) do
|
||||
def safe_relative(path, cwd \\ File.cwd!()) do
|
||||
path = IO.chardata_to_string(path)
|
||||
|
||||
case :filelib.safe_relative_path(path, relative_to) do
|
||||
case :filelib.safe_relative_path(path, cwd) do
|
||||
:unsafe -> :error
|
||||
relative_path -> {:ok, IO.chardata_to_string(relative_path)}
|
||||
end
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
+132
-139
@@ -6,7 +6,7 @@ defmodule Protocol do
|
||||
implementations. A protocol is defined with `Kernel.defprotocol/2`
|
||||
and its implementations with `Kernel.defimpl/3`.
|
||||
|
||||
## Example
|
||||
## A real case
|
||||
|
||||
In Elixir, we have two nouns for checking how many items there
|
||||
are in a data structure: `length` and `size`. `length` means the
|
||||
@@ -102,7 +102,8 @@ defmodule Protocol do
|
||||
invoking the protocol will raise unless it is configured to
|
||||
fall back to `Any`. Conveniences for building implementations
|
||||
on top of existing ones are also available, look at `defstruct/1`
|
||||
for more information about deriving protocols.
|
||||
for more information about deriving
|
||||
protocols.
|
||||
|
||||
## Fallback to `Any`
|
||||
|
||||
@@ -125,8 +126,8 @@ defmodule Protocol do
|
||||
Although the implementation above is arguably not a reasonable
|
||||
one. For example, it makes no sense to say a PID or an integer
|
||||
have a size of `0`. That's one of the reasons why `@fallback_to_any`
|
||||
is an opt-in behavior. For the majority of protocols, raising
|
||||
an error when a protocol is not implemented is the proper behavior.
|
||||
is an opt-in behaviour. For the majority of protocols, raising
|
||||
an error when a protocol is not implemented is the proper behaviour.
|
||||
|
||||
## Multiple implementations
|
||||
|
||||
@@ -164,16 +165,56 @@ defmodule Protocol do
|
||||
The `@spec` above expresses that all types allowed to implement the
|
||||
given protocol are valid argument types for the given function.
|
||||
|
||||
## Configuration
|
||||
## Reflection
|
||||
|
||||
The following module attributes are available to configure a protocol:
|
||||
Any protocol module contains three extra functions:
|
||||
|
||||
* `@fallback_to_any` - when true, enables protocol dispatch to
|
||||
fallback to any
|
||||
* `__protocol__/1` - returns the protocol information. The function takes
|
||||
one of the following atoms:
|
||||
|
||||
* `@undefined_impl_description` - a string with additional description
|
||||
to be used on `Protocol.UndefinedError` when looking up the implementation
|
||||
fails. This option is only applied if `@fallback_to_any` is not set to true
|
||||
* `:consolidated?` - returns whether the protocol is consolidated
|
||||
|
||||
* `:functions` - returns a keyword list of protocol functions and their arities
|
||||
|
||||
* `:impls` - if consolidated, returns `{:consolidated, modules}` with the list of modules
|
||||
implementing the protocol, otherwise `:not_consolidated`
|
||||
|
||||
* `:module` - the protocol module atom name
|
||||
|
||||
* `impl_for/1` - returns the module that implements the protocol for the given argument,
|
||||
`nil` otherwise
|
||||
|
||||
* `impl_for!/1` - same as above but raises `Protocol.UndefinedError` if an implementation is
|
||||
not found
|
||||
|
||||
For example, for the `Enumerable` protocol we have:
|
||||
|
||||
iex> Enumerable.__protocol__(:functions)
|
||||
[count: 1, member?: 2, reduce: 3, slice: 1]
|
||||
|
||||
iex> Enumerable.impl_for([])
|
||||
Enumerable.List
|
||||
|
||||
iex> Enumerable.impl_for(42)
|
||||
nil
|
||||
|
||||
In addition, every protocol implementation module contains the `__impl__/1`
|
||||
function. The function takes one of the following atoms:
|
||||
|
||||
* `:for` - returns the module responsible for the data structure of the
|
||||
protocol implementation
|
||||
|
||||
* `:protocol` - returns the protocol module for which this implementation
|
||||
is provided
|
||||
|
||||
For example, the module implementing the `Enumerable` protocol for lists is
|
||||
`Enumerable.List`. Therefore, we can invoke `__impl__/1` on this module:
|
||||
|
||||
iex(1)> Enumerable.List.__impl__(:for)
|
||||
List
|
||||
|
||||
iex(2)> Enumerable.List.__impl__(:protocol)
|
||||
Enumerable
|
||||
|
||||
## Consolidation
|
||||
|
||||
@@ -181,8 +222,7 @@ defmodule Protocol do
|
||||
are known up-front, typically after all Elixir code in a project is compiled,
|
||||
Elixir provides a feature called *protocol consolidation*. Consolidation directly
|
||||
links protocols to their implementations in a way that invoking a function from a
|
||||
consolidated protocol is equivalent to invoking two remote functions - one to
|
||||
identify the correct implementation, and another to call the implementation.
|
||||
consolidated protocol is equivalent to invoking two remote functions.
|
||||
|
||||
Protocol consolidation is applied by default to all Mix projects during compilation.
|
||||
This may be an issue during test. For instance, if you want to implement a protocol
|
||||
@@ -228,41 +268,6 @@ defmodule Protocol do
|
||||
globally set.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
A function available in all protocol definitions that returns protocol metadata.
|
||||
"""
|
||||
@callback __protocol__(:consolidated?) :: boolean()
|
||||
@callback __protocol__(:functions) :: [{atom(), arity()}]
|
||||
@callback __protocol__(:impls) :: {:consolidated, [module()]} | :not_consolidated
|
||||
@callback __protocol__(:module) :: module()
|
||||
|
||||
@doc """
|
||||
A function available in all protocol definitions that returns the implementation
|
||||
for the given `term` or nil.
|
||||
|
||||
If `@fallback_to_any` is true, `nil` is never returned.
|
||||
"""
|
||||
@callback impl_for(term) :: module() | nil
|
||||
|
||||
@doc """
|
||||
A function available in all protocol definitions that returns the implementation
|
||||
for the given `term` or raises.
|
||||
|
||||
If `@fallback_to_any` is true, it never raises.
|
||||
"""
|
||||
@callback impl_for!(term) :: module()
|
||||
|
||||
@doc """
|
||||
An optional callback to be implemented by protocol authors for custom deriving.
|
||||
|
||||
It must return a quoted expression that implements the protocol for the given module.
|
||||
|
||||
See `Protocol.derive/3` for an example.
|
||||
"""
|
||||
@macrocallback __deriving__(module(), term()) :: Macro.t()
|
||||
|
||||
@optional_callbacks __deriving__: 2
|
||||
|
||||
@doc false
|
||||
defmacro def(signature)
|
||||
|
||||
@@ -347,7 +352,7 @@ defmodule Protocol do
|
||||
end
|
||||
|
||||
defp assert_impl!(protocol, base, extra) do
|
||||
impl = Protocol.__concat__(protocol, base)
|
||||
impl = Module.concat(protocol, base)
|
||||
|
||||
try do
|
||||
Code.ensure_compiled!(impl)
|
||||
@@ -375,50 +380,52 @@ defmodule Protocol do
|
||||
@doc """
|
||||
Derives the `protocol` for `module` with the given options.
|
||||
|
||||
Every time you derive a protocol, Elixir will verify if the protocol
|
||||
has implemented the `c:Protocol.__deriving__/2` callback. If so,
|
||||
the callback will be invoked and it should define the implementation
|
||||
module. Otherwise an implementation that simply points to the `Any`
|
||||
implementation is automatically derived.
|
||||
If your implementation passes options or if you are generating
|
||||
custom code based on the struct, you will also need to implement
|
||||
a macro defined as `__deriving__(module, struct, options)`
|
||||
to get the options that were passed.
|
||||
|
||||
## Examples
|
||||
|
||||
defprotocol Derivable do
|
||||
@impl true
|
||||
defmacro __deriving__(module, options) do
|
||||
# If you need to load struct metadata, you may call:
|
||||
# struct_info = Macro.struct_info!(module, __CALLER__)
|
||||
def ok(arg)
|
||||
end
|
||||
|
||||
defimpl Derivable, for: Any do
|
||||
defmacro __deriving__(module, struct, options) do
|
||||
quote do
|
||||
defimpl Derivable, for: unquote(module) do
|
||||
def ok(arg) do
|
||||
{:ok, arg, unquote(options)}
|
||||
{:ok, arg, unquote(Macro.escape(struct)), unquote(options)}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def ok(arg)
|
||||
def ok(arg) do
|
||||
{:ok, arg}
|
||||
end
|
||||
end
|
||||
|
||||
Once the protocol is defined, there are two ways it can be
|
||||
derived. The first is by using the `@derive` module attribute
|
||||
by the time you define the struct:
|
||||
|
||||
defmodule ImplStruct do
|
||||
@derive [Derivable]
|
||||
defstruct a: 0, b: 0
|
||||
end
|
||||
|
||||
Derivable.ok(%ImplStruct{})
|
||||
#=> {:ok, %ImplStruct{a: 0, b: 0}, []}
|
||||
#=> {:ok, %ImplStruct{a: 0, b: 0}, %ImplStruct{a: 0, b: 0}, []}
|
||||
|
||||
If the struct has already been defined, you can call this macro:
|
||||
Explicit derivations can now be called via `__deriving__/3`:
|
||||
|
||||
# Explicitly derived via `__deriving__/3`
|
||||
Derivable.ok(%ImplStruct{a: 1, b: 1})
|
||||
#=> {:ok, %ImplStruct{a: 1, b: 1}, %ImplStruct{a: 0, b: 0}, []}
|
||||
|
||||
# Explicitly derived by API via `__deriving__/3`
|
||||
require Protocol
|
||||
Protocol.derive(Derivable, ImplStruct, :oops)
|
||||
Derivable.ok(%ImplStruct{a: 1, b: 1})
|
||||
#=> {:ok, %ImplStruct{a: 1, b: 1}, :oops}
|
||||
#=> {:ok, %ImplStruct{a: 1, b: 1}, %ImplStruct{a: 0, b: 0}, :oops}
|
||||
|
||||
"""
|
||||
defmacro derive(protocol, module, options \\ []) do
|
||||
@@ -490,8 +497,7 @@ defmodule Protocol do
|
||||
|
||||
defp extract_matching_by_attribute(paths, prefix, callback) do
|
||||
for path <- paths,
|
||||
# Do not use protocols as they may be consolidating
|
||||
path = if(is_list(path), do: path, else: String.to_charlist(path)),
|
||||
path = to_charlist(path),
|
||||
file <- list_dir(path),
|
||||
mod = extract_from_file(path, file, prefix, callback),
|
||||
do: mod
|
||||
@@ -678,7 +684,7 @@ defmodule Protocol do
|
||||
end
|
||||
|
||||
defp load_impl(protocol, for) do
|
||||
Protocol.__concat__(protocol, for)
|
||||
Module.concat(protocol, for)
|
||||
end
|
||||
|
||||
# Finally compile the module and emit its bytecode.
|
||||
@@ -693,14 +699,25 @@ defmodule Protocol do
|
||||
def __protocol__(name, do: block) do
|
||||
quote do
|
||||
defmodule unquote(name) do
|
||||
@behaviour Protocol
|
||||
@before_compile Protocol
|
||||
|
||||
# We don't allow function definition inside protocols
|
||||
import Kernel,
|
||||
except: [def: 1, def: 2, defdelegate: 2, defguard: 1, defguardp: 1]
|
||||
except: [
|
||||
def: 1,
|
||||
def: 2,
|
||||
defp: 1,
|
||||
defp: 2,
|
||||
defdelegate: 2,
|
||||
defguard: 1,
|
||||
defguardp: 1,
|
||||
defmacro: 1,
|
||||
defmacro: 2,
|
||||
defmacrop: 1,
|
||||
defmacrop: 2
|
||||
]
|
||||
|
||||
# Import the new `def` that is used by protocols
|
||||
# Import the new dsl that holds the new def
|
||||
import Protocol, only: [def: 1]
|
||||
|
||||
# Compile with debug info for consolidation
|
||||
@@ -719,17 +736,19 @@ defmodule Protocol do
|
||||
end
|
||||
end
|
||||
|
||||
defp callback_ast_to_fa({_kind, {:"::", meta, [{name, _, args}, _return]}, _pos}) do
|
||||
defp callback_ast_to_fa({kind, {:"::", meta, [{name, _, args}, _return]}, _pos})
|
||||
when kind in [:callback, :macrocallback] do
|
||||
[{{name, length(List.wrap(args))}, meta}]
|
||||
end
|
||||
|
||||
defp callback_ast_to_fa(
|
||||
{_kind, {:when, _, [{:"::", meta, [{name, _, args}, _return]}, _vars]}, _pos}
|
||||
) do
|
||||
{kind, {:when, _, [{:"::", meta, [{name, _, args}, _return]}, _vars]}, _pos}
|
||||
)
|
||||
when kind in [:callback, :macrocallback] do
|
||||
[{{name, length(List.wrap(args))}, meta}]
|
||||
end
|
||||
|
||||
defp callback_ast_to_fa({_kind, _clause, _pos}) do
|
||||
defp callback_ast_to_fa({kind, _, _pos}) when kind in [:callback, :macrocallback] do
|
||||
[]
|
||||
end
|
||||
|
||||
@@ -747,10 +766,13 @@ defmodule Protocol do
|
||||
end
|
||||
|
||||
defp warn(message, env, line) when is_integer(line) do
|
||||
IO.warn(message, %{env | line: line})
|
||||
stacktrace = :maps.update(:line, line, env) |> Macro.Env.stacktrace()
|
||||
IO.warn(message, stacktrace)
|
||||
end
|
||||
|
||||
def __before_compile__(env) do
|
||||
callback_metas = callback_metas(env.module, :callback)
|
||||
callbacks = :maps.keys(callback_metas)
|
||||
functions = Module.get_attribute(env.module, :__functions__)
|
||||
|
||||
if functions == [] do
|
||||
@@ -761,11 +783,8 @@ defmodule Protocol do
|
||||
)
|
||||
end
|
||||
|
||||
callback_metas = callback_metas(env.module, :callback)
|
||||
callbacks = :maps.keys(callback_metas)
|
||||
|
||||
# TODO: Convert the following warnings into errors in future Elixir versions
|
||||
:lists.foreach(
|
||||
:lists.map(
|
||||
fn {name, arity} = fa ->
|
||||
warn(
|
||||
"cannot define @callback #{name}/#{arity} inside protocol, use def/1 to outline your protocol definition",
|
||||
@@ -780,7 +799,7 @@ defmodule Protocol do
|
||||
macrocallback_metas = callback_metas(env.module, :macrocallback)
|
||||
macrocallbacks = :maps.keys(macrocallback_metas)
|
||||
|
||||
:lists.foreach(
|
||||
:lists.map(
|
||||
fn {name, arity} = fa ->
|
||||
warn(
|
||||
"cannot define @macrocallback #{name}/#{arity} inside protocol, use def/1 to outline your protocol definition",
|
||||
@@ -807,7 +826,7 @@ defmodule Protocol do
|
||||
quote bind_quoted: [built_in: __built_in__()] do
|
||||
any_impl_for =
|
||||
if @fallback_to_any do
|
||||
__MODULE__.Any
|
||||
quote do: unquote(__MODULE__.Any)
|
||||
else
|
||||
nil
|
||||
end
|
||||
@@ -831,7 +850,7 @@ defmodule Protocol do
|
||||
# Define the implementation for built-ins
|
||||
:lists.foreach(
|
||||
fn {guard, mod} ->
|
||||
target = Protocol.__concat__(__MODULE__, mod)
|
||||
target = Module.concat(__MODULE__, mod)
|
||||
|
||||
Kernel.def impl_for(data) when :erlang.unquote(guard)(data) do
|
||||
case Code.ensure_compiled(unquote(target)) do
|
||||
@@ -853,9 +872,6 @@ defmodule Protocol do
|
||||
unquote(any_impl_for)
|
||||
end
|
||||
|
||||
undefined_impl_description =
|
||||
Module.get_attribute(__MODULE__, :undefined_impl_description, "")
|
||||
|
||||
@doc false
|
||||
@spec impl_for!(term) :: atom
|
||||
if any_impl_for do
|
||||
@@ -864,27 +880,23 @@ defmodule Protocol do
|
||||
end
|
||||
else
|
||||
Kernel.def impl_for!(data) do
|
||||
impl_for(data) ||
|
||||
raise(Protocol.UndefinedError,
|
||||
protocol: __MODULE__,
|
||||
value: data,
|
||||
description: unquote(undefined_impl_description)
|
||||
)
|
||||
impl_for(data) || raise(Protocol.UndefinedError, protocol: __MODULE__, value: data)
|
||||
end
|
||||
end
|
||||
|
||||
# Internal handler for Structs
|
||||
Kernel.defp struct_impl_for(struct) do
|
||||
case Code.ensure_compiled(Protocol.__concat__(__MODULE__, struct)) do
|
||||
{:module, module} -> module
|
||||
{:error, _} -> unquote(any_impl_for)
|
||||
end
|
||||
target =
|
||||
case Code.ensure_compiled(Module.concat(__MODULE__, struct)) do
|
||||
{:module, module} -> module
|
||||
{:error, _} -> unquote(any_impl_for)
|
||||
end
|
||||
end
|
||||
|
||||
# Inline struct implementation for performance
|
||||
@compile {:inline, struct_impl_for: 1}
|
||||
|
||||
if not Module.defines_type?(__MODULE__, {:t, 0}) do
|
||||
unless Module.defines_type?(__MODULE__, {:t, 0}) do
|
||||
@typedoc """
|
||||
All the types that implement this protocol.
|
||||
"""
|
||||
@@ -898,9 +910,9 @@ defmodule Protocol do
|
||||
|
||||
@doc false
|
||||
@spec __protocol__(:module) :: __MODULE__
|
||||
@spec __protocol__(:functions) :: [{atom(), arity()}]
|
||||
@spec __protocol__(:consolidated?) :: boolean()
|
||||
@spec __protocol__(:impls) :: :not_consolidated | {:consolidated, [module()]}
|
||||
@spec __protocol__(:functions) :: unquote(Protocol.__functions_spec__(@__functions__))
|
||||
@spec __protocol__(:consolidated?) :: boolean
|
||||
@spec __protocol__(:impls) :: :not_consolidated | {:consolidated, [module]}
|
||||
Kernel.def(__protocol__(:module), do: __MODULE__)
|
||||
Kernel.def(__protocol__(:functions), do: unquote(:lists.sort(@__functions__)))
|
||||
Kernel.def(__protocol__(:consolidated?), do: false)
|
||||
@@ -908,6 +920,12 @@ defmodule Protocol do
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def __functions_spec__([]), do: []
|
||||
|
||||
def __functions_spec__([head | tail]),
|
||||
do: [:lists.foldl(&{:|, [], [&1, &2]}, head, tail), quote(do: ...)]
|
||||
|
||||
@doc false
|
||||
def __impl__(protocol, opts, do_block, env) do
|
||||
opts = Keyword.merge(opts, do_block)
|
||||
@@ -948,13 +966,12 @@ defmodule Protocol do
|
||||
quote do
|
||||
protocol = unquote(protocol)
|
||||
for = unquote(for)
|
||||
name = Protocol.__concat__(protocol, for)
|
||||
name = Module.concat(protocol, for)
|
||||
|
||||
Protocol.assert_protocol!(protocol)
|
||||
Protocol.__ensure_defimpl__(protocol, for, __ENV__)
|
||||
|
||||
defmodule name do
|
||||
@moduledoc false
|
||||
@behaviour protocol
|
||||
@protocol protocol
|
||||
@for for
|
||||
@@ -971,12 +988,14 @@ defmodule Protocol do
|
||||
|
||||
@doc false
|
||||
def __derive__(derives, for, %Macro.Env{} = env) when is_atom(for) do
|
||||
struct = Macro.struct!(for, env)
|
||||
|
||||
foreach = fn
|
||||
proto when is_atom(proto) ->
|
||||
derive(proto, for, [], env)
|
||||
derive(proto, for, struct, [], env)
|
||||
|
||||
{proto, opts} when is_atom(proto) ->
|
||||
derive(proto, for, opts, env)
|
||||
derive(proto, for, struct, opts, env)
|
||||
end
|
||||
|
||||
:lists.foreach(foreach, :lists.flatten(derives))
|
||||
@@ -984,30 +1003,21 @@ defmodule Protocol do
|
||||
:ok
|
||||
end
|
||||
|
||||
defp derive(protocol, for, opts, env) do
|
||||
defp derive(protocol, for, struct, opts, env) do
|
||||
extra = ", cannot derive #{inspect(protocol)} for #{inspect(for)}"
|
||||
assert_protocol!(protocol, extra)
|
||||
|
||||
{mod, args} =
|
||||
if macro_exported?(protocol, :__deriving__, 2) do
|
||||
{protocol, [for, opts]}
|
||||
else
|
||||
# TODO: Deprecate this on Elixir v1.22+
|
||||
assert_impl!(protocol, Any, extra)
|
||||
{Protocol.__concat__(protocol, Any), [for, Macro.struct!(for, env), opts]}
|
||||
end
|
||||
__ensure_defimpl__(protocol, for, env)
|
||||
assert_impl!(protocol, Any, extra)
|
||||
|
||||
# Clean up variables from eval context
|
||||
env = :elixir_env.reset_vars(env)
|
||||
args = [for, struct, opts]
|
||||
impl = Module.concat(protocol, Any)
|
||||
|
||||
:elixir_module.expand_callback(env.line, mod, :__deriving__, args, env, fn mod, fun, args ->
|
||||
:elixir_module.expand_callback(env.line, impl, :__deriving__, args, env, fn mod, fun, args ->
|
||||
if function_exported?(mod, fun, length(args)) do
|
||||
apply(mod, fun, args)
|
||||
else
|
||||
__ensure_defimpl__(protocol, for, env)
|
||||
assert_impl!(protocol, Any, extra)
|
||||
impl = Protocol.__concat__(protocol, Any)
|
||||
|
||||
funs =
|
||||
for {fun, arity} <- protocol.__protocol__(:functions) do
|
||||
args = Macro.generate_arguments(arity, nil)
|
||||
@@ -1031,11 +1041,7 @@ defmodule Protocol do
|
||||
def __impl__(:for), do: unquote(for)
|
||||
end
|
||||
|
||||
Module.create(
|
||||
Protocol.__concat__(protocol, for),
|
||||
[quoted | funs],
|
||||
Macro.Env.location(env)
|
||||
)
|
||||
Module.create(Module.concat(protocol, for), [quoted | funs], Macro.Env.location(env))
|
||||
end
|
||||
end)
|
||||
end
|
||||
@@ -1074,17 +1080,4 @@ defmodule Protocol do
|
||||
is_reference: Reference
|
||||
]
|
||||
end
|
||||
|
||||
@doc false
|
||||
def __concat__(left, right) do
|
||||
String.to_atom(
|
||||
ensure_prefix(Atom.to_string(left)) <> "." <> remove_prefix(Atom.to_string(right))
|
||||
)
|
||||
end
|
||||
|
||||
defp ensure_prefix("Elixir." <> _ = left), do: left
|
||||
defp ensure_prefix(left), do: "Elixir." <> left
|
||||
|
||||
defp remove_prefix("Elixir." <> right), do: right
|
||||
defp remove_prefix(right), do: right
|
||||
end
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user