Compare commits
489
Commits
v1.19.2
...
v1.20.0-rc.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3bb5f92fa9 | ||
|
|
cb472c10f3 | ||
|
|
e5dc69398e | ||
|
|
fd6c6af81d | ||
|
|
a9a0969c28 | ||
|
|
dd284bbf9b | ||
|
|
bc1a862c93 | ||
|
|
ae383313f6 | ||
|
|
f00cb3a9a0 | ||
|
|
9af054f9bc | ||
|
|
61f55052fe | ||
|
|
d8322e3f76 | ||
|
|
f2ff3a7a86 | ||
|
|
7f5e4e3bb2 | ||
|
|
bfa154e70b | ||
|
|
e586ed519e | ||
|
|
8767c5b530 | ||
|
|
7c13e99412 | ||
|
|
d8ed2b9ae4 | ||
|
|
d653d1522a | ||
|
|
1752058872 | ||
|
|
0f9d919074 | ||
|
|
45de6e5dc3 | ||
|
|
15f2165903 | ||
|
|
2a2be0acde | ||
|
|
583737d77d | ||
|
|
205a1ecb4e | ||
|
|
9721759d35 | ||
|
|
6bace04dc9 | ||
|
|
4ee0241a5e | ||
|
|
1b234cadb1 | ||
|
|
99eae9536b | ||
|
|
7fb01d3f47 | ||
|
|
386c5d9d72 | ||
|
|
ad7559f6de | ||
|
|
98155171a2 | ||
|
|
5abc024203 | ||
|
|
ea59662fa0 | ||
|
|
be0f3ae3e9 | ||
|
|
aa222426d0 | ||
|
|
caea8bcfa3 | ||
|
|
a4e7a2d19d | ||
|
|
ad67ac0a1a | ||
|
|
88cbabfd84 | ||
|
|
0f67706cf9 | ||
|
|
5b4ee56d7e | ||
|
|
c35f651309 | ||
|
|
f1bbb2cd32 | ||
|
|
3884e7a91c | ||
|
|
94ba62178b | ||
|
|
a077a6e3fb | ||
|
|
fff97fd3cc | ||
|
|
53a567556a | ||
|
|
56259933ea | ||
|
|
19c628ae23 | ||
|
|
5f616e254f | ||
|
|
ee87c1c8a9 | ||
|
|
6d433580f3 | ||
|
|
3c8fe8606c | ||
|
|
8d8111af07 | ||
|
|
fc747ffbe3 | ||
|
|
e6f90bd55c | ||
|
|
7a01f5bab2 | ||
|
|
f7904a4aa6 | ||
|
|
6948d08d51 | ||
|
|
0473fec367 | ||
|
|
2cd8a57b37 | ||
|
|
07889d8c52 | ||
|
|
1445e8d021 | ||
|
|
1921765c78 | ||
|
|
0e3d22fd79 | ||
|
|
f9ebeaa5a7 | ||
|
|
6ce5044521 | ||
|
|
5f60c39d66 | ||
|
|
9d8df88f80 | ||
|
|
743fa8ada5 | ||
|
|
995f7fc2c4 | ||
|
|
67431fc48a | ||
|
|
ec267745e6 | ||
|
|
bb9721f27c | ||
|
|
68207c0186 | ||
|
|
14bd4b5401 | ||
|
|
0f00cb0113 | ||
|
|
e622a26420 | ||
|
|
72435c44a5 | ||
|
|
743fea7a11 | ||
|
|
67baf686eb | ||
|
|
297bd84ac2 | ||
|
|
e609d29bc6 | ||
|
|
0f3fdcd84f | ||
|
|
2d327241de | ||
|
|
f58e02adf1 | ||
|
|
5c586dae68 | ||
|
|
af3624c823 | ||
|
|
bd909cb658 | ||
|
|
aee1747c5d | ||
|
|
fabbf5e9c9 | ||
|
|
0435602337 | ||
|
|
b60e424204 | ||
|
|
70e23a407e | ||
|
|
a9aac5216f | ||
|
|
d831a00063 | ||
|
|
a09fa1075a | ||
|
|
0b3dc130cd | ||
|
|
62d3e61e89 | ||
|
|
de32dff024 | ||
|
|
a6ba0cb329 | ||
|
|
18c81d66e2 | ||
|
|
7fbf48ccc7 | ||
|
|
77ef258ee6 | ||
|
|
ab843d0342 | ||
|
|
79c28420d4 | ||
|
|
0e3465d1fc | ||
|
|
ee75b22a0c | ||
|
|
1110a9511f | ||
|
|
5f608c4ad3 | ||
|
|
c80a3b1a0e | ||
|
|
b79b08d819 | ||
|
|
34004a2b90 | ||
|
|
09fc63d475 | ||
|
|
e1ff7819b8 | ||
|
|
82983cf979 | ||
|
|
904adda55f | ||
|
|
8fc181fb27 | ||
|
|
d30b22f5d4 | ||
|
|
030660aaf7 | ||
|
|
fc07f452b3 | ||
|
|
4c10b43e3d | ||
|
|
08b559fe96 | ||
|
|
1cadefffc7 | ||
|
|
ac44e723ba | ||
|
|
2ac361a4a4 | ||
|
|
509e715f60 | ||
|
|
a6951d0448 | ||
|
|
3c5e968a54 | ||
|
|
cad69f790f | ||
|
|
1aa188f115 | ||
|
|
41f9bcf621 | ||
|
|
2b63546b19 | ||
|
|
a13f38edbe | ||
|
|
31c9c84ebe | ||
|
|
284b190bb0 | ||
|
|
b58cdd823c | ||
|
|
e66a799b2f | ||
|
|
bd4c232a28 | ||
|
|
521b16ce21 | ||
|
|
3172f7487d | ||
|
|
6b519ae782 | ||
|
|
6710d7e197 | ||
|
|
7ad99b5590 | ||
|
|
ff2b73861d | ||
|
|
e2c4c07b9a | ||
|
|
2acf5804d3 | ||
|
|
1da7feb387 | ||
|
|
31ec8cc703 | ||
|
|
abd73a841f | ||
|
|
0b4d0f360d | ||
|
|
10bc090ac1 | ||
|
|
68ebb78d66 | ||
|
|
66165e6b0f | ||
|
|
bf2298e050 | ||
|
|
75c5aa0a6b | ||
|
|
19dcd95a36 | ||
|
|
da1481e433 | ||
|
|
c4c5c060d6 | ||
|
|
1b215b7a36 | ||
|
|
81ef21272d | ||
|
|
7da1b76b6b | ||
|
|
f52f395fcf | ||
|
|
e55388d6e6 | ||
|
|
912f3d546a | ||
|
|
7eea067534 | ||
|
|
dc3986833e | ||
|
|
a2f21c91a4 | ||
|
|
17e244c0a5 | ||
|
|
2219d74c38 | ||
|
|
0055f2fe53 | ||
|
|
522aebdf74 | ||
|
|
1fa63ffe7a | ||
|
|
16862cef79 | ||
|
|
ae752c7bb0 | ||
|
|
f9745a25cf | ||
|
|
1b92f3ac87 | ||
|
|
9ec2eb41ae | ||
|
|
47aab3b37c | ||
|
|
7ef4a3be97 | ||
|
|
d56546d81e | ||
|
|
e56b730c8c | ||
|
|
98b35eaa53 | ||
|
|
5732283ada | ||
|
|
46807cfcdf | ||
|
|
7989e87716 | ||
|
|
ef52e13938 | ||
|
|
8fb1ddf060 | ||
|
|
51e25fbf4a | ||
|
|
cf454cd1f1 | ||
|
|
f12ff1d7db | ||
|
|
c7a9d8544a | ||
|
|
c80fc6da1a | ||
|
|
cbb508600c | ||
|
|
f248994830 | ||
|
|
b45853f043 | ||
|
|
6f26bb0b05 | ||
|
|
ee469e56e6 | ||
|
|
0126f1f1a3 | ||
|
|
edb9431df6 | ||
|
|
71a8f53521 | ||
|
|
541f641a92 | ||
|
|
c0933fbbac | ||
|
|
06619c2fcc | ||
|
|
a0eb603091 | ||
|
|
0df110c9c0 | ||
|
|
22635e6bda | ||
|
|
4a8d4f29ae | ||
|
|
6460c642c3 | ||
|
|
31905ca136 | ||
|
|
3c1514aef6 | ||
|
|
6402967c4b | ||
|
|
757ab8c456 | ||
|
|
0fac5d5f4a | ||
|
|
21450cf838 | ||
|
|
c18f53ed93 | ||
|
|
9a1c722737 | ||
|
|
8a513afc60 | ||
|
|
01d9c505bb | ||
|
|
5ca2c15c75 | ||
|
|
3a290c30c1 | ||
|
|
7fa1914e88 | ||
|
|
aef3dcecd1 | ||
|
|
25771dd347 | ||
|
|
e309da46cb | ||
|
|
e5ace9bba6 | ||
|
|
1a2c0a24e7 | ||
|
|
bd3d68cccc | ||
|
|
668fb69a47 | ||
|
|
bb262d8302 | ||
|
|
bd3b2f6e00 | ||
|
|
58c45612dc | ||
|
|
e9370bb023 | ||
|
|
f175f5f3fa | ||
|
|
a7258fe26d | ||
|
|
a8000f211a | ||
|
|
5bcfbd38be | ||
|
|
720d77361c | ||
|
|
cf7d329915 | ||
|
|
7eb5b3ae17 | ||
|
|
7a1dde6034 | ||
|
|
260149951e | ||
|
|
dc6647c0e9 | ||
|
|
d18088a3b4 | ||
|
|
22b47d7f48 | ||
|
|
e1214a64c9 | ||
|
|
5d29d61890 | ||
|
|
faa8d4722e | ||
|
|
809b035dcc | ||
|
|
50a6f0f5e6 | ||
|
|
e1772ef73a | ||
|
|
4e867b3775 | ||
|
|
3992ad4ade | ||
|
|
d11a84dfd6 | ||
|
|
9cc24c9d92 | ||
|
|
43aeded377 | ||
|
|
c265744b9c | ||
|
|
77f30d6321 | ||
|
|
d596853211 | ||
|
|
640275b8ba | ||
|
|
53fb96c808 | ||
|
|
504c680870 | ||
|
|
f4aa384d1b | ||
|
|
7612567830 | ||
|
|
b727a502d6 | ||
|
|
33467dad7b | ||
|
|
a4bbb96d7a | ||
|
|
0f597b7327 | ||
|
|
b2c1c3958b | ||
|
|
d792fe55f7 | ||
|
|
06ef46ceb5 | ||
|
|
da20a70810 | ||
|
|
e07f0117bd | ||
|
|
36b0a69d3c | ||
|
|
977efeac99 | ||
|
|
e7c121609e | ||
|
|
fecb22116c | ||
|
|
e4d0a0d252 | ||
|
|
92a28606ac | ||
|
|
e3cd399505 | ||
|
|
96fe37e587 | ||
|
|
874a390881 | ||
|
|
781d500246 | ||
|
|
aec930cd21 | ||
|
|
c6d3e48474 | ||
|
|
7afabc99cb | ||
|
|
3ccc915e66 | ||
|
|
b4cab3641a | ||
|
|
20564ba6d9 | ||
|
|
f6c62669cd | ||
|
|
184c7717f6 | ||
|
|
2b57a97d0a | ||
|
|
26612e59c8 | ||
|
|
f646c89315 | ||
|
|
898e61a843 | ||
|
|
16fb81545d | ||
|
|
3146daf162 | ||
|
|
5a2a5ae96c | ||
|
|
0685a35c63 | ||
|
|
f310ed9d82 | ||
|
|
bba5542393 | ||
|
|
c9b529a43c | ||
|
|
81cd55beef | ||
|
|
2b424ca674 | ||
|
|
f089571351 | ||
|
|
811767fa6c | ||
|
|
20c7d07345 | ||
|
|
9a8794183e | ||
|
|
514bc86a71 | ||
|
|
29e8883134 | ||
|
|
2ee1d0eb78 | ||
|
|
2e7ee76ac2 | ||
|
|
6fccf34e6b | ||
|
|
2474303f93 | ||
|
|
2e55f40713 | ||
|
|
479849475e | ||
|
|
7d521de63a | ||
|
|
2dc3d2713c | ||
|
|
59c0a38bb1 | ||
|
|
1be424db50 | ||
|
|
3ae49eb4ec | ||
|
|
175f54869b | ||
|
|
b9387d34d8 | ||
|
|
95265ba1c2 | ||
|
|
0dd3985f16 | ||
|
|
f9dbdb88c9 | ||
|
|
e2f6bbafaa | ||
|
|
469a1cc05e | ||
|
|
4008142018 | ||
|
|
56333d451e | ||
|
|
21c76b58ba | ||
|
|
a5bdf8a881 | ||
|
|
bedb6777c1 | ||
|
|
6a7511f9af | ||
|
|
78fb312013 | ||
|
|
90e1826c7e | ||
|
|
02968a46ff | ||
|
|
33ee657a7f | ||
|
|
b87a9fa35b | ||
|
|
827e65a5cb | ||
|
|
54321de136 | ||
|
|
6ee313a835 | ||
|
|
c5f1c64be3 | ||
|
|
848fc1d6df | ||
|
|
65ff52aadf | ||
|
|
032d5660f3 | ||
|
|
dd45d9666f | ||
|
|
9ae79d1385 | ||
|
|
cff61f2404 | ||
|
|
7a8dc78593 | ||
|
|
eee42e4448 | ||
|
|
53be491e9f | ||
|
|
5a8ba81b05 | ||
|
|
06fc222e53 | ||
|
|
32a5c97d5a | ||
|
|
86d5b3a293 | ||
|
|
c7b2d6b6e5 | ||
|
|
9f8bacdb5e | ||
|
|
927f91c9fe | ||
|
|
6ea5654438 | ||
|
|
d4ba7ee92c | ||
|
|
ff9608fc22 | ||
|
|
d21a13b915 | ||
|
|
f0595a4799 | ||
|
|
c7a54aeac0 | ||
|
|
1a855ae1ba | ||
|
|
03522aaeb2 | ||
|
|
f93919f54a | ||
|
|
7d50f43217 | ||
|
|
1f786273e6 | ||
|
|
67042e8770 | ||
|
|
04f1b7ccae | ||
|
|
75a3272036 | ||
|
|
2cfd329eea | ||
|
|
06ff083e48 | ||
|
|
cacdbf65a6 | ||
|
|
b5885a6f33 | ||
|
|
2a71415c65 | ||
|
|
374f6e342a | ||
|
|
9b34ec2295 | ||
|
|
841f426a4c | ||
|
|
2367300c45 | ||
|
|
290b5d63f8 | ||
|
|
b2d548af0e | ||
|
|
23776d9e8f | ||
|
|
74df71078d | ||
|
|
8b1922150b | ||
|
|
8bdd277b0e | ||
|
|
4fc85f3d8f | ||
|
|
01b7dd32e8 | ||
|
|
f5b65f5d86 | ||
|
|
0705e3f663 | ||
|
|
a8c8535f8f | ||
|
|
494855aa26 | ||
|
|
4b48982da1 | ||
|
|
8344634bb6 | ||
|
|
d4b0b0ee9b | ||
|
|
c6ab1dbbae | ||
|
|
20cac294e9 | ||
|
|
f4fdf64b61 | ||
|
|
20daffb630 | ||
|
|
a4f40f7622 | ||
|
|
79a6a57234 | ||
|
|
fb8f6900fa | ||
|
|
d5460361b2 | ||
|
|
419ce18d2c | ||
|
|
4b09a08042 | ||
|
|
ce64fc364d | ||
|
|
be9352ed70 | ||
|
|
2f2368a2d4 | ||
|
|
bbe1709dca | ||
|
|
c1c8ebaa8f | ||
|
|
2350e83f5b | ||
|
|
677e1ec8b1 | ||
|
|
67593e1c9a | ||
|
|
a6844bf725 | ||
|
|
802ecdd4a0 | ||
|
|
36eb9daf1a | ||
|
|
c710b2afb7 | ||
|
|
352f2723a9 | ||
|
|
8bc49af49d | ||
|
|
e55c61ffaf | ||
|
|
fa51593cc6 | ||
|
|
f2804b96db | ||
|
|
29ee3a2ad8 | ||
|
|
0be3e70037 | ||
|
|
e78f105f23 | ||
|
|
1fb7ca159c | ||
|
|
785ffcc7cd | ||
|
|
cfc43b37d4 | ||
|
|
57c254f725 | ||
|
|
efcd164e38 | ||
|
|
b29c83a42f | ||
|
|
d7bdea55de | ||
|
|
824fc3bc24 | ||
|
|
3e373e8a59 | ||
|
|
0253f614b7 | ||
|
|
b26ae516ac | ||
|
|
cf8689e498 | ||
|
|
dc1890b879 | ||
|
|
e3f7759fde | ||
|
|
2fe44b747d | ||
|
|
32ed2c38b2 | ||
|
|
ad524f528f | ||
|
|
237263c446 | ||
|
|
a5d2aa88f3 | ||
|
|
b188f4d33d | ||
|
|
3482e4e740 | ||
|
|
8172ff0775 | ||
|
|
274169f628 | ||
|
|
08d3865fad | ||
|
|
1753c81f9e | ||
|
|
54489ef86f | ||
|
|
0487e4dd8a | ||
|
|
54515bf48a | ||
|
|
9d4c91be2a | ||
|
|
cc9a5e7682 | ||
|
|
b2587e8633 | ||
|
|
6ca0ad84b7 | ||
|
|
66753a868b | ||
|
|
972f9e46ed | ||
|
|
f07bc7b3d5 | ||
|
|
dc4c1cf402 | ||
|
|
e27cd15715 | ||
|
|
d59f49aa54 | ||
|
|
7ad145082b | ||
|
|
fb9a9e97d8 | ||
|
|
5f2633a305 | ||
|
|
e04444c42e | ||
|
|
7da4d6570b | ||
|
|
5946e8bb9d | ||
|
|
46b5fabc63 | ||
|
|
bff9f3ebbc | ||
|
|
ff4adca18a | ||
|
|
954133d64c | ||
|
|
319360ce9f | ||
|
|
7ef6905484 | ||
|
|
52495ba8bc | ||
|
|
514fbbaf52 | ||
|
|
34cf44587c | ||
|
|
57ed90d5b7 | ||
|
|
09dcbf7566 | ||
|
|
c001cbdd62 |
@@ -1,40 +0,0 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
||||
|
||||
name: CI for Markdown content
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
paths:
|
||||
- "lib/**/*.md"
|
||||
pull_request:
|
||||
paths:
|
||||
- "lib/**/*.md"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Lint Markdown content
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check out the repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 10
|
||||
|
||||
- name: Run markdownlint
|
||||
uses: DavidAnson/markdownlint-cli2-action@992badcdf24e3b8eb7e87ff9287fe931bcb00c6e # v20.0.0
|
||||
with:
|
||||
globs: |
|
||||
lib/elixir/pages/**/*.md
|
||||
README.md
|
||||
+57
-78
@@ -1,15 +1,13 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
||||
# SPDX-FileCopyrightText: 2012 Plataformatec
|
||||
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
paths-ignore:
|
||||
- "lib/**/*.md"
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- "lib/**/*.md"
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
ELIXIR_ASSERT_TIMEOUT: 2000
|
||||
@@ -21,19 +19,19 @@ permissions:
|
||||
|
||||
jobs:
|
||||
test_linux:
|
||||
name: Ubuntu 24.04, Erlang/OTP ${{ matrix.otp_version }}${{ matrix.deterministic && ' (deterministic)' || '' }}${{ matrix.coverage && ' (coverage)' || '' }}
|
||||
name: Ubuntu 24.04, OTP ${{ matrix.otp_version }}${{ matrix.deterministic && ' (deterministic)' || '' }}${{ matrix.coverage && ' (coverage)' || '' }}
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- otp_version: "28.0"
|
||||
- otp_version: "28.1"
|
||||
deterministic: true
|
||||
- otp_version: "28.0"
|
||||
- otp_version: "28.1"
|
||||
erlc_opts: "warnings_as_errors"
|
||||
docs: true
|
||||
coverage: true
|
||||
- otp_version: "28.0"
|
||||
otp_latest: true
|
||||
erlc_opts: "warnings_as_errors"
|
||||
- otp_version: "27.3"
|
||||
erlc_opts: "warnings_as_errors"
|
||||
- otp_version: "27.0"
|
||||
@@ -43,43 +41,46 @@ jobs:
|
||||
development: true
|
||||
- otp_version: maint
|
||||
development: true
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
# Earlier Erlang/OTP versions ignored compiler directives
|
||||
# when using warnings_as_errors. So we only set ERLC_OPTS
|
||||
# from Erlang/OTP 27+.
|
||||
env:
|
||||
ERLC_OPTS: ${{ matrix.erlc_opts || '' }}
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 50
|
||||
- uses: erlef/setup-beam@8aa8a857c6be0daae6e97272bb299d5b942675a4 # v1.19.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- uses: erlef/setup-beam@e6d7c94229049569db56a7ad5a540c051a010af9 # v1.20.4
|
||||
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
|
||||
echo "$PWD/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Build info
|
||||
run: bin/elixir --version
|
||||
|
||||
- name: Check format
|
||||
run: make test_formatted && echo "All Elixir source code files are properly formatted."
|
||||
|
||||
- name: Erlang test suite
|
||||
run: make test_erlang
|
||||
continue-on-error: ${{ matrix.development }}
|
||||
continue-on-error: ${{ matrix.development == true }}
|
||||
|
||||
- name: Elixir test suite
|
||||
run: make test_elixir
|
||||
continue-on-error: ${{ matrix.development }}
|
||||
continue-on-error: ${{ matrix.development == true }}
|
||||
env:
|
||||
COVER: "${{ matrix.coverage }}"
|
||||
- name: "Calculate Coverage"
|
||||
run: make cover | tee "$GITHUB_STEP_SUMMARY"
|
||||
if: "${{ matrix.coverage }}"
|
||||
|
||||
- name: Build docs (ExDoc main)
|
||||
if: ${{ matrix.otp_latest }}
|
||||
if: ${{ matrix.docs }}
|
||||
run: |
|
||||
cd ..
|
||||
git clone https://github.com/elixir-lang/ex_doc.git --depth 1
|
||||
@@ -88,87 +89,65 @@ jobs:
|
||||
cd ../elixir/
|
||||
git fetch --tags
|
||||
DOCS_OPTIONS="--warnings-as-errors" 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
|
||||
|
||||
- name: "Calculate Coverage"
|
||||
if: ${{ matrix.coverage }}
|
||||
run: make cover | tee "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: "Upload Coverage Artifact"
|
||||
if: "${{ matrix.coverage }}"
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
if: ${{ matrix.coverage }}
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: TestCoverage
|
||||
path: cover/*
|
||||
|
||||
- name: Check reproducible builds
|
||||
if: ${{ matrix.deterministic }}
|
||||
run: taskset 1 make check_reproducible
|
||||
|
||||
- name: Check git is not required
|
||||
if: ${{ matrix.deterministic }}
|
||||
run: |
|
||||
rm -rf .git
|
||||
cd lib/elixir
|
||||
elixirc --ignore-module-conflict -o ebin "lib/**/*.ex"
|
||||
|
||||
test_windows:
|
||||
name: Windows Server 2019, Erlang/OTP ${{ matrix.otp_version }}
|
||||
name: Windows Server 2022, OTP ${{ matrix.otp_version }}
|
||||
runs-on: windows-2022
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
otp_version: ["26.2", "27.3", "28.0"]
|
||||
runs-on: windows-2022
|
||||
otp_version:
|
||||
- "28.1"
|
||||
- "27.3"
|
||||
- "26.2"
|
||||
|
||||
steps:
|
||||
- name: Configure Git
|
||||
run: git config --global core.autocrlf input
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 50
|
||||
- uses: erlef/setup-beam@8aa8a857c6be0daae6e97272bb299d5b942675a4 # v1.19.0
|
||||
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- uses: erlef/setup-beam@e6d7c94229049569db56a7ad5a540c051a010af9 # v1.20.4
|
||||
with:
|
||||
otp-version: ${{ matrix.otp_version }}
|
||||
|
||||
- name: Compile Elixir
|
||||
run: |
|
||||
Remove-Item -Recurse -Force '.git'
|
||||
make compile
|
||||
|
||||
- name: Build info
|
||||
run: bin/elixir --version
|
||||
|
||||
- name: Check format
|
||||
run: make test_formatted && echo "All Elixir source code files are properly formatted."
|
||||
|
||||
- name: Erlang test suite
|
||||
run: make test_erlang
|
||||
|
||||
- name: Elixir test suite
|
||||
run: |
|
||||
Remove-Item 'c:/Windows/System32/drivers/etc/hosts'
|
||||
make test_elixir
|
||||
|
||||
check_posix_compliant:
|
||||
name: Check POSIX-compliant
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 50
|
||||
- name: Install Shellcheck
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install -y shellcheck
|
||||
- name: Check POSIX-compliant
|
||||
run: |
|
||||
shellcheck -e SC2039,2086 bin/elixir && echo "bin/elixir is POSIX compliant"
|
||||
shellcheck bin/elixirc && echo "bin/elixirc is POSIX compliant"
|
||||
shellcheck bin/iex && echo "bin/iex is POSIX compliant"
|
||||
|
||||
license_compliance:
|
||||
name: Check Licence Compliance
|
||||
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- name: Use HTTPS instead of SSH for Git cloning
|
||||
id: git-config
|
||||
shell: bash
|
||||
run: git config --global url.https://github.com/.insteadOf ssh://git@github.com/
|
||||
|
||||
- name: Checkout project
|
||||
id: checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: "Run OSS Review Toolkit"
|
||||
id: ort
|
||||
uses: ./.github/workflows/ort
|
||||
with:
|
||||
upload-reports: true
|
||||
fail-on-violation: true
|
||||
report-formats: "WebApp"
|
||||
version: "${{ github.sha }}"
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
||||
|
||||
name: License Compliance
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
LANG: C.UTF-8
|
||||
|
||||
jobs:
|
||||
license_compliance:
|
||||
name: Check License Compliance
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
steps:
|
||||
- name: Use HTTPS instead of SSH for Git cloning
|
||||
id: git-config
|
||||
shell: bash
|
||||
run: git config --global url.https://github.com/.insteadOf ssh://git@github.com/
|
||||
|
||||
- name: Checkout project
|
||||
id: checkout
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Run OSS Review Toolkit
|
||||
id: ort
|
||||
uses: ./.github/workflows/ort
|
||||
with:
|
||||
upload-reports: true
|
||||
fail-on-violation: true
|
||||
report-formats: "WebApp"
|
||||
version: "${{ github.sha }}"
|
||||
@@ -0,0 +1,39 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
||||
|
||||
name: Markdown Content
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
|
||||
paths: &paths-filter
|
||||
- "**/*.md"
|
||||
- .github/workflows/markdown.yml
|
||||
- .markdownlint-cli2.jsonc
|
||||
|
||||
pull_request:
|
||||
paths: *paths-filter
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
LANG: C.UTF-8
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Lint Markdown content
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Run markdownlint-cli2
|
||||
uses: DavidAnson/markdownlint-cli2-action@07035fd053f7be764496c0f8d8f9f41f98305101 # v22.0.0
|
||||
@@ -62,6 +62,16 @@ runs:
|
||||
# Override Default Evaluator Rules
|
||||
cp .ort/config/evaluator.rules.kts "$HOME/.ort/config/evaluator.rules.kts"
|
||||
|
||||
# Add Package Configurations
|
||||
mkdir -p "$HOME/.ort/config/package-configurations/SpdxDocumentFile/The Elixir Team"
|
||||
for FILE in .ort/package-configurations/*.yml; do
|
||||
COMPONENT="$(basename "$FILE")"
|
||||
cp "$FILE" "$HOME/.ort/config/package-configurations/SpdxDocumentFile/The Elixir Team/$COMPONENT"
|
||||
sed -i -E \
|
||||
"s/(\"SpdxDocumentFile:The Elixir Team:.+:)\"/\1${ELIXIR_VERSION}\"/" \
|
||||
"$HOME/.ort/config/package-configurations/SpdxDocumentFile/The Elixir Team/$COMPONENT"
|
||||
done
|
||||
|
||||
# Set Version in SPDX & Config
|
||||
sed -i "s/# elixir-version-insert/versionInfo: '${ELIXIR_VERSION}'/" project.spdx.yml
|
||||
sed -i -E "s/(\"SpdxDocumentFile:The Elixir Team:.+:)\"/\1${ELIXIR_VERSION}\"/" .ort.yml
|
||||
@@ -80,7 +90,7 @@ runs:
|
||||
id: ort
|
||||
uses: oss-review-toolkit/ort-ci-github-action@1805edcf1f4f55f35ae6e4d2d9795ccfb29b6021 # v1.1.0
|
||||
with:
|
||||
image: ghcr.io/oss-review-toolkit/ort-minimal:54.0.0
|
||||
image: ghcr.io/oss-review-toolkit/ort-minimal:65.0.0
|
||||
run: >-
|
||||
labels,
|
||||
cache-dependencies,
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
||||
# SPDX-FileCopyrightText: 2012 Plataformatec
|
||||
|
||||
name: POSIX Compliance
|
||||
|
||||
on:
|
||||
push:
|
||||
paths: &paths-filter
|
||||
- .github/workflows/posix_compliance.yml
|
||||
- bin/elixir
|
||||
- bin/elixirc
|
||||
- bin/iex
|
||||
|
||||
pull_request:
|
||||
paths: *paths-filter
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
env:
|
||||
LANG: C.UTF-8
|
||||
|
||||
jobs:
|
||||
check_posix_compliance:
|
||||
name: Check POSIX compliance
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: Install ShellCheck
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install -y shellcheck
|
||||
|
||||
- name: Run ShellCheck on bin/ dir
|
||||
run: |
|
||||
shellcheck -e SC2039,2086 bin/elixir && \
|
||||
echo "bin/elixir is POSIX compliant"
|
||||
|
||||
shellcheck bin/elixirc && \
|
||||
echo "bin/elixirc is POSIX compliant"
|
||||
|
||||
shellcheck bin/iex && \
|
||||
echo "bin/iex is POSIX compliant"
|
||||
@@ -1,16 +1,19 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
||||
|
||||
name: Release
|
||||
name: Releases
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- v*.*
|
||||
|
||||
tags:
|
||||
- v*
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
ELIXIR_OPTS: "--warnings-as-errors"
|
||||
LANG: C.UTF-8
|
||||
@@ -20,11 +23,15 @@ permissions:
|
||||
|
||||
jobs:
|
||||
create_draft_release:
|
||||
runs-on: ubuntu-22.04
|
||||
name: Create draft release
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
steps:
|
||||
- name: Create draft release
|
||||
if: github.ref_type != 'branch'
|
||||
@@ -36,10 +43,8 @@ jobs:
|
||||
--draft \
|
||||
${{ github.ref_name }}
|
||||
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
if: github.ref_type == 'branch'
|
||||
with:
|
||||
fetch-depth: 50
|
||||
|
||||
- name: Update ${{ github.ref_name }}-latest
|
||||
if: github.ref_type == 'branch'
|
||||
@@ -58,7 +63,8 @@ jobs:
|
||||
git push origin $ref_name --force
|
||||
|
||||
build:
|
||||
name: "Build Elixir"
|
||||
name: Ubuntu 24.04, OTP ${{ matrix.otp_version }}${{ matrix.build_docs && ' (build docs)' || '' }}
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
strategy:
|
||||
fail-fast: true
|
||||
@@ -66,18 +72,16 @@ jobs:
|
||||
include:
|
||||
- otp: 26
|
||||
otp_version: "26.0"
|
||||
|
||||
- otp: 27
|
||||
otp_version: "27.0"
|
||||
|
||||
- otp: 28
|
||||
otp_version: "28.0"
|
||||
build_docs: build_docs
|
||||
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 50
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: "Build Release"
|
||||
uses: ./.github/workflows/release_pre_built
|
||||
@@ -92,27 +96,29 @@ jobs:
|
||||
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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
- name: "Upload Linux release artifacts"
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: build-linux-elixir-otp-${{ matrix.otp }}
|
||||
path: elixir-otp-${{ matrix.otp }}.zip
|
||||
|
||||
- name: "Upload windows release artifacts"
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
- name: "Upload Windows release artifacts"
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: build-windows-elixir-otp-${{ matrix.otp }}
|
||||
path: elixir-otp-${{ matrix.otp }}.exe
|
||||
|
||||
- name: "Upload doc artifacts"
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
if: matrix.build_docs
|
||||
with:
|
||||
name: Docs
|
||||
path: Docs.zip*
|
||||
|
||||
sign:
|
||||
name: Sign files, ${{ matrix.flavor == 'windows' && 'Windows' || matrix.flavor == 'linux' && 'Linux' || matrix.flavor }}, OTP ${{ matrix.otp }}
|
||||
needs: [build]
|
||||
environment: release
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
@@ -122,34 +128,33 @@ jobs:
|
||||
env:
|
||||
RELEASE_FILE: elixir-otp-${{ matrix.otp }}.${{ matrix.flavor == 'linux' && 'zip' || 'exe' }}
|
||||
|
||||
runs-on: ${{ matrix.flavor == 'linux' && 'ubuntu-22.04' || 'windows-2022' }}
|
||||
runs-on: ${{ matrix.flavor == 'linux' && 'ubuntu-24.04' || 'windows-2022' }}
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: "Download build"
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
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: github.repository == 'elixir-lang/elixir' && matrix.flavor == 'windows'
|
||||
uses: azure/trusted-signing-action@1d365fec12862c4aa68fcac418143d73f0cea293 # v0.5.11
|
||||
if: ${{ matrix.flavor == 'windows' && vars.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
|
||||
with:
|
||||
azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
azure-client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
# AZURE_TENANT_ID and AZURE_CLIENT_ID should stay the same,
|
||||
# but AZURE_CLIENT_SECRET has expiration date. When it expires go to
|
||||
# App Registrations / <app> / Certificates & secrets,
|
||||
# click (+) New client secret, note the "Value" (not "Secret ID")
|
||||
# and update it:
|
||||
#
|
||||
# $ gh --repo elixir-lang/elixir secret set AZURE_CLIENT_SECRET
|
||||
azure-client-secret: ${{ secrets.AZURE_CLIENT_SECRET }}
|
||||
endpoint: https://eus.codesigning.azure.net/
|
||||
trusted-signing-account-name: trusted-signing-elixir
|
||||
certificate-profile-name: Elixir
|
||||
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
|
||||
@@ -173,17 +178,15 @@ jobs:
|
||||
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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
- name: "Upload Linux release artifacts"
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: sign-${{ matrix.flavor }}-elixir-otp-${{ matrix.otp }}
|
||||
path: ${{ env.RELEASE_FILE }}*
|
||||
|
||||
sbom:
|
||||
name: Generate SBoM
|
||||
|
||||
needs: [build, sign]
|
||||
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
permissions:
|
||||
@@ -199,11 +202,11 @@ jobs:
|
||||
|
||||
- name: Checkout project
|
||||
id: checkout
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- name: "Download Build Artifacts"
|
||||
id: download-build-artifacts
|
||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
with:
|
||||
pattern: "{sign-*-elixir-otp-*,Docs}"
|
||||
merge-multiple: true
|
||||
@@ -218,7 +221,7 @@ jobs:
|
||||
|
||||
- name: Attest Distribution Assets with SBoM
|
||||
id: attest-sbom
|
||||
uses: actions/attest-sbom@115c3be05ff3974bcbd596578934b3f9ce39bf68 # v2.2.0
|
||||
uses: actions/attest-sbom@4651f806c01d8637787e274ac3bdf724ef169f34 # v3.0.0
|
||||
with:
|
||||
subject-path: |
|
||||
/tmp/build-artifacts/{elixir-otp-*.*,Docs.zip}
|
||||
@@ -246,7 +249,7 @@ jobs:
|
||||
ATTESTATION: "${{ steps.attest-sbom.outputs.bundle-path }}"
|
||||
|
||||
- name: "Assemble Release SBoM Artifacts"
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: "SBoM"
|
||||
path: |
|
||||
@@ -256,25 +259,26 @@ jobs:
|
||||
${{ steps.ort.outputs.results-sbom-spdx-json-path }}
|
||||
|
||||
- name: "Assemble Distribution Attestations"
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
with:
|
||||
name: "Attestations"
|
||||
path: "attestations/*.sigstore"
|
||||
|
||||
upload-release:
|
||||
name: Upload release
|
||||
needs: [create_draft_release, build, sign, sbom]
|
||||
runs-on: ubuntu-22.04
|
||||
runs-on: ubuntu-24.04
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
with:
|
||||
pattern: "{sign-*-elixir-otp-*,Docs,SBoM,Attestations}"
|
||||
merge-multiple: true
|
||||
|
||||
- name: Upload Pre-built
|
||||
- name: Upload Pre-build
|
||||
shell: bash
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -301,20 +305,26 @@ jobs:
|
||||
bom.*
|
||||
|
||||
upload-builds-hex-pm:
|
||||
name: Upload builds to hex.pm
|
||||
runs-on: ubuntu-24.04
|
||||
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: ${{ 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 }}
|
||||
OTP_GENERIC_VERSION: "25"
|
||||
AWS_REGION: ${{ vars.HEX_AWS_REGION }}
|
||||
AWS_S3_BUCKET: ${{ vars.HEX_AWS_S3_BUCKET }}
|
||||
|
||||
steps:
|
||||
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
||||
- 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@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
|
||||
with:
|
||||
pattern: "{sign-*-elixir-otp-*,Docs}"
|
||||
merge-multiple: true
|
||||
@@ -327,6 +337,8 @@ jobs:
|
||||
run: |
|
||||
ref_name=${{ github.ref_name }}
|
||||
|
||||
oldest_otp=$(find . -type f -name 'elixir-otp-*.zip' | sed -r 's/^.*elixir-otp-([[:digit:]]+)\.zip$/\1/' | sort -n | head -n 1)
|
||||
|
||||
for zip in $(find . -type f -name 'elixir-otp-*.zip' | sed 's/^\.\///'); do
|
||||
dest=${zip/elixir/${ref_name}}
|
||||
surrogate_key=${dest/.zip$/}
|
||||
@@ -336,7 +348,7 @@ jobs:
|
||||
--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
|
||||
if [ "$zip" == "elixir-otp-${oldest_otp}.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\"}"
|
||||
@@ -369,6 +381,8 @@ jobs:
|
||||
date="$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
|
||||
ref_name=${{ github.ref_name }}
|
||||
|
||||
oldest_otp=$(find . -name 'elixir-otp-*.zip.sha256sum' | sed -r 's/^.*elixir-otp-([[:digit:]]+)\.zip\.sha256sum$/\1/' | sort -n | head -n 1)
|
||||
|
||||
aws s3 cp "s3://${AWS_S3_BUCKET}/builds/elixir/builds.txt" builds.txt || true
|
||||
touch builds.txt
|
||||
|
||||
@@ -379,7 +393,7 @@ jobs:
|
||||
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
|
||||
if [ "${otp_version}" == "otp-${oldest_otp}" ]; then
|
||||
sed -i "/^${ref_name} /d" builds.txt
|
||||
echo -e "${ref_name} ${{ github.sha }} ${date} ${build_sha256} \n$(cat builds.txt)" > builds.txt
|
||||
fi
|
||||
@@ -418,3 +432,8 @@ jobs:
|
||||
for key in $(cat purge_keys.txt); do
|
||||
purge "${key}"
|
||||
done
|
||||
|
||||
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 }}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
||||
|
||||
name: Notify
|
||||
name: Release Notifications
|
||||
|
||||
on:
|
||||
release:
|
||||
@@ -15,14 +15,15 @@ jobs:
|
||||
notify:
|
||||
runs-on: ubuntu-latest
|
||||
name: Notify
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 50
|
||||
- uses: erlef/setup-beam@8aa8a857c6be0daae6e97272bb299d5b942675a4 # v1.19.0
|
||||
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||
|
||||
- uses: erlef/setup-beam@e6d7c94229049569db56a7ad5a540c051a010af9 # v1.20.4
|
||||
with:
|
||||
otp-version: "27.3"
|
||||
elixir-version: "1.18.3"
|
||||
|
||||
- name: Run Elixir script
|
||||
env:
|
||||
ELIXIR_FORUM_TOKEN: ${{ secrets.ELIXIR_FORUM_TOKEN }}
|
||||
@@ -1,33 +1,41 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
||||
|
||||
name: "Release pre built"
|
||||
description: "Builds elixir release, ExDoc and generates docs"
|
||||
name: Release Pre-build
|
||||
description: "Builds Elixir release, ExDoc and generates docs"
|
||||
|
||||
inputs:
|
||||
otp:
|
||||
description: "The major OTP version"
|
||||
|
||||
otp_version:
|
||||
description: "The exact OTP version (major.minor[.patch])"
|
||||
|
||||
build_docs:
|
||||
description: "If docs have to be built or not"
|
||||
description: "Whether docs have to be built"
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
|
||||
steps:
|
||||
- uses: erlef/setup-beam@5304e04ea2b355f03681464e683d92e3b2f18451 # v1.18.2
|
||||
with:
|
||||
otp-version: ${{ inputs.otp_version }}
|
||||
version-type: strict
|
||||
|
||||
- name: Build Elixir Release
|
||||
shell: bash
|
||||
run: |
|
||||
make Precompiled.zip
|
||||
mv Precompiled.zip elixir-otp-${{ inputs.otp }}.zip
|
||||
echo "$PWD/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Install NSIS
|
||||
shell: bash
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install -y nsis
|
||||
|
||||
- name: Build Elixir Windows Installer
|
||||
shell: bash
|
||||
run: |
|
||||
|
||||
+3
-2
@@ -10,9 +10,10 @@
|
||||
/lib/elixir/test/ebin/
|
||||
/man/elixir.1
|
||||
/man/iex.1
|
||||
/Docs-v*.zip
|
||||
/Precompiled-v*.zip
|
||||
/Docs.zip
|
||||
/Precompiled.zip
|
||||
/.eunit
|
||||
.elixir.plt
|
||||
erl_crash.dump
|
||||
/cover/
|
||||
.tool-versions
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2021 The Elixir Team
|
||||
{
|
||||
"globs": [
|
||||
"**/*.md"
|
||||
],
|
||||
"ignores": [
|
||||
".git/**"
|
||||
],
|
||||
"gitignore": true,
|
||||
"config": {
|
||||
// Consecutive header levels (h1 -> h2 -> h3).
|
||||
"MD001": false,
|
||||
// Header style. We use #s.
|
||||
"MD003": {
|
||||
"style": "atx"
|
||||
},
|
||||
// Style of unordered lists..
|
||||
"MD007": {
|
||||
"indent": 2,
|
||||
"start_indented": true
|
||||
},
|
||||
// Line length. Who cares.
|
||||
"MD013": false,
|
||||
// This warns if you have "console" or "shell" code blocks with a dollar sign $ that
|
||||
// don't show output. We use those a lot, so this is fine for us.
|
||||
"MD014": false,
|
||||
// Multiple headings with the same content.
|
||||
"MD024": {
|
||||
// Duplication is allowed for headings with different parents.
|
||||
"siblings_only": true
|
||||
},
|
||||
// Trailing punctuation in heading.
|
||||
// Some headers finish with ! because it refers to a function name. Therefore we remove ! from
|
||||
// the default values.
|
||||
"MD026": {
|
||||
"punctuation": ".,;:。,;:!"
|
||||
},
|
||||
// Allow empty line between block quotes. Used by contiguous admonition blocks.
|
||||
"MD028": false,
|
||||
// Allowed HTML inline elements.
|
||||
"MD033": {
|
||||
"allowed_elements": [
|
||||
"h1",
|
||||
"a",
|
||||
"br",
|
||||
"img",
|
||||
"picture",
|
||||
"source",
|
||||
"noscript",
|
||||
"p",
|
||||
"script"
|
||||
]
|
||||
},
|
||||
// This warns if you have spaces in code blocks. Sometimes, that's fine.
|
||||
"MD038": false,
|
||||
// Code block style. We don't care if it's fenced or indented.
|
||||
"MD046": false,
|
||||
// Our tables are too large to align.
|
||||
"MD060": false
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2021 The Elixir Team
|
||||
|
||||
{
|
||||
// Consecutive header levels (h1 -> h2 -> h3). We don't care about this.
|
||||
"MD001": false,
|
||||
// Header style. We use #s.
|
||||
"MD003": {
|
||||
"style": "atx"
|
||||
},
|
||||
// Style of unordered lists..
|
||||
"MD007": {
|
||||
"indent": 2,
|
||||
"start_indented": true
|
||||
},
|
||||
// Line length. Who cares.
|
||||
"MD013": false,
|
||||
// This warns if you have "console" or "shell" code blocks with a dollar sign $ that
|
||||
// don't show output. We use those a lot, so this is fine for us.
|
||||
"MD014": false,
|
||||
// Multiple headings with the same content. That's fine.
|
||||
"MD024": false,
|
||||
// Some headers finish with ! because it refers to a function name
|
||||
"MD026": false,
|
||||
// Allow empty line between block quotes. Used by contiguous admonition blocks.
|
||||
"MD028": false,
|
||||
// Allowed HTML inline elements.
|
||||
"MD033": {
|
||||
"allowed_elements": [
|
||||
"h1",
|
||||
"a",
|
||||
"br",
|
||||
"img",
|
||||
"picture",
|
||||
"source",
|
||||
"noscript",
|
||||
"p",
|
||||
"script"
|
||||
]
|
||||
},
|
||||
// This warns if you have spaces in code blocks. Sometimes, that's fine.
|
||||
"MD038": false,
|
||||
// Code block style. We don't care if it's fenced or indented.
|
||||
"MD046": false
|
||||
}
|
||||
@@ -3,18 +3,6 @@
|
||||
|
||||
excludes:
|
||||
paths:
|
||||
- pattern: "lib/elixir/pages/**/*"
|
||||
reason: "DOCUMENTATION_OF"
|
||||
comment: "Documentation"
|
||||
- pattern: "lib/elixir/scripts/**/*"
|
||||
reason: "BUILD_TOOL_OF"
|
||||
comment: "Build Tool"
|
||||
- pattern: "lib/ex_unit/examples/**/*"
|
||||
reason: "EXAMPLE_OF"
|
||||
comment: "Example"
|
||||
- pattern: "lib/*/test/**/*"
|
||||
reason: "TEST_OF"
|
||||
comment: "Tests"
|
||||
- pattern: "man/*"
|
||||
reason: "DOCUMENTATION_OF"
|
||||
comment: "Documentation"
|
||||
@@ -25,8 +13,64 @@ excludes:
|
||||
reason: "BUILD_TOOL_OF"
|
||||
comment: "Documentation"
|
||||
|
||||
# Unfortunately we'll have to repeat all package level excludes here
|
||||
# Make sure to keep them in sync with the package configuration in
|
||||
# .ort/package-configurations
|
||||
- pattern: "lib/*/pages/**/*"
|
||||
reason: "DOCUMENTATION_OF"
|
||||
comment: "Documentation"
|
||||
- pattern: "lib/*/test/**/*"
|
||||
reason: "TEST_OF"
|
||||
comment: "Tests"
|
||||
- pattern: "lib/*/scripts/**/*"
|
||||
reason: "BUILD_TOOL_OF"
|
||||
comment: "Build Tool"
|
||||
- pattern: "lib/*/examples/**/*"
|
||||
reason: "EXAMPLE_OF"
|
||||
comment: "Example"
|
||||
|
||||
curations:
|
||||
license_findings:
|
||||
# Version File
|
||||
- path: "VERSION"
|
||||
reason: "NOT_DETECTED"
|
||||
comment: "Apply Trademark Policy to VERSION file"
|
||||
detected_license: "NONE"
|
||||
concluded_license: "Apache-2.0"
|
||||
|
||||
# Wrongly Identified
|
||||
- path: ".gitignore"
|
||||
reason: "INCORRECT"
|
||||
comment: "Ignored by ScanCode"
|
||||
detected_license: "NONE"
|
||||
concluded_license: "Apache-2.0"
|
||||
- path: ".gitattributes"
|
||||
reason: "INCORRECT"
|
||||
comment: "Ignored by ScanCode"
|
||||
detected_license: "NONE"
|
||||
concluded_license: "Apache-2.0"
|
||||
- path: "CONTRIBUTING.md"
|
||||
reason: "INCORRECT"
|
||||
comment: "Wrongly identified TSL license"
|
||||
detected_license: "Apache-2.0 OR NOASSERTION OR LicenseRef-scancode-tsl-2020"
|
||||
concluded_license: "Apache-2.0"
|
||||
- path: "OPEN_SOURCE_POLICY.md"
|
||||
reason: "INCORRECT"
|
||||
comment: "Wrongly identified NOASSERTION"
|
||||
detected_license: "NOASSERTION"
|
||||
concluded_license: "Apache-2.0"
|
||||
|
||||
# Unfortunately we'll have to repeat all package level license curations here
|
||||
# Make sure to keep them in sync with the package configuration in
|
||||
# .ort/package-configurations
|
||||
|
||||
# Test Fixtures
|
||||
- path: "lib/*/test/fixtures/**/*"
|
||||
reason: "NOT_DETECTED"
|
||||
comment: "Apply default license to test fixtures"
|
||||
detected_license: "NONE"
|
||||
concluded_license: "Apache-2.0"
|
||||
|
||||
# Logos
|
||||
- path: "lib/elixir/pages/images/logo.png"
|
||||
reason: "NOT_DETECTED"
|
||||
@@ -39,13 +83,6 @@ curations:
|
||||
detected_license: "NONE"
|
||||
concluded_license: "LicenseRef-elixir-trademark-policy"
|
||||
|
||||
# Version File
|
||||
- path: "VERSION"
|
||||
reason: "NOT_DETECTED"
|
||||
comment: "Apply Trademark Policy to VERSION file"
|
||||
detected_license: "NONE"
|
||||
concluded_license: "Apache-2.0"
|
||||
|
||||
# Documentation Images
|
||||
- path: "lib/elixir/pages/images/**/*.png"
|
||||
reason: "NOT_DETECTED"
|
||||
@@ -54,26 +91,11 @@ curations:
|
||||
concluded_license: "Apache-2.0"
|
||||
|
||||
# Test Fixtures
|
||||
- path: "lib/eex/test/fixtures/**/*"
|
||||
reason: "NOT_DETECTED"
|
||||
comment: "Apply default license to test fixtures"
|
||||
detected_license: "NONE"
|
||||
concluded_license: "Apache-2.0"
|
||||
- path: "lib/elixir/test/elixir/fixtures/**/*"
|
||||
reason: "NOT_DETECTED"
|
||||
comment: "Apply default license to test fixtures"
|
||||
detected_license: "NONE"
|
||||
concluded_license: "Apache-2.0"
|
||||
- path: "lib/ex_unit/test/fixtures/**/*"
|
||||
reason: "NOT_DETECTED"
|
||||
comment: "Apply default license to test fixtures"
|
||||
detected_license: "NONE"
|
||||
concluded_license: "Apache-2.0"
|
||||
- path: "lib/mix/test/fixtures/**/*"
|
||||
reason: "NOT_DETECTED"
|
||||
comment: "Apply default license to test fixtures"
|
||||
detected_license: "NONE"
|
||||
concluded_license: "Apache-2.0"
|
||||
|
||||
# Unicode
|
||||
- path: "lib/elixir/unicode/*.txt"
|
||||
@@ -89,57 +111,8 @@ curations:
|
||||
The guide mentions multiple licenses for users to choose from.
|
||||
It however is not licensed itself by the mentioned licenses.
|
||||
concluded_license: "Apache-2.0"
|
||||
- path: ".gitignore"
|
||||
reason: "INCORRECT"
|
||||
comment: "Ignored by ScanCode"
|
||||
detected_license: "NONE"
|
||||
concluded_license: "Apache-2.0"
|
||||
- path: ".gitattributes"
|
||||
reason: "INCORRECT"
|
||||
comment: "Ignored by ScanCode"
|
||||
detected_license: "NONE"
|
||||
concluded_license: "Apache-2.0"
|
||||
- path: "lib/elixir/scripts/windows_installer/.gitignore"
|
||||
reason: "INCORRECT"
|
||||
comment: "Ignored by ScanCode"
|
||||
detected_license: "NONE"
|
||||
concluded_license: "Apache-2.0"
|
||||
- path: "CONTRIBUTING.md"
|
||||
reason: "INCORRECT"
|
||||
comment: "Wrongly identified TSL license"
|
||||
detected_license: "Apache-2.0 OR NOASSERTION OR LicenseRef-scancode-tsl-2020"
|
||||
concluded_license: "Apache-2.0"
|
||||
- path: "OPEN_SOURCE_POLICY.md"
|
||||
reason: "INCORRECT"
|
||||
comment: "Wrongly identified NOASSERTION"
|
||||
detected_license: "NOASSERTION"
|
||||
concluded_license: "Apache-2.0"
|
||||
|
||||
packages:
|
||||
- id: "SpdxDocumentFile:The Elixir Team:elixir-lang:"
|
||||
curations:
|
||||
concluded_license: "Apache-2.0 AND LicenseRef-scancode-unicode"
|
||||
- id: "SpdxDocumentFile:The Elixir Team:eex:"
|
||||
curations:
|
||||
concluded_license: "Apache-2.0"
|
||||
is_metadata_only: true
|
||||
- id: "SpdxDocumentFile:The Elixir Team:elixir:"
|
||||
curations:
|
||||
concluded_license: "Apache-2.0 AND LicenseRef-scancode-unicode"
|
||||
is_metadata_only: true
|
||||
- id: "SpdxDocumentFile:The Elixir Team:exunit:"
|
||||
curations:
|
||||
concluded_license: "Apache-2.0"
|
||||
is_metadata_only: true
|
||||
- id: "SpdxDocumentFile:The Elixir Team:iex:"
|
||||
curations:
|
||||
concluded_license: "Apache-2.0"
|
||||
is_metadata_only: true
|
||||
- id: "SpdxDocumentFile:The Elixir Team:logger:"
|
||||
curations:
|
||||
concluded_license: "Apache-2.0"
|
||||
is_metadata_only: true
|
||||
- id: "SpdxDocumentFile:The Elixir Team:mix:"
|
||||
curations:
|
||||
concluded_license: "Apache-2.0"
|
||||
is_metadata_only: true
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
ort:
|
||||
enableRepositoryPackageCurations: true
|
||||
enableRepositoryPackageConfigurations: true
|
||||
|
||||
scanner:
|
||||
skipConcluded: false
|
||||
@@ -11,4 +12,10 @@ ort:
|
||||
analyzer:
|
||||
allowDynamicVersions: true
|
||||
enabledPackageManagers: [SpdxDocumentFile]
|
||||
skipExcluded: true
|
||||
|
||||
reporter:
|
||||
reporters:
|
||||
SpdxDocument:
|
||||
options:
|
||||
creationInfoOrganization: The Elixir Team
|
||||
documentName: "Elixir Source SPDX Document"
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
||||
|
||||
id: "SpdxDocumentFile:The Elixir Team:eex:"
|
||||
path_excludes:
|
||||
- pattern: "lib/eex/test/**/*"
|
||||
reason: "TEST_OF"
|
||||
comment: "Tests"
|
||||
license_finding_curations:
|
||||
# Test Fixtures
|
||||
- path: "lib/eex/test/fixtures/**/*"
|
||||
reason: "NOT_DETECTED"
|
||||
comment: "Apply default license to test fixtures"
|
||||
detected_license: "NONE"
|
||||
concluded_license: "Apache-2.0"
|
||||
@@ -0,0 +1,60 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
||||
|
||||
id: "SpdxDocumentFile:The Elixir Team:elixir:"
|
||||
path_excludes:
|
||||
- pattern: "lib/elixir/pages/**/*"
|
||||
reason: "DOCUMENTATION_OF"
|
||||
comment: "Documentation"
|
||||
- pattern: "lib/elixir/scripts/**/*"
|
||||
reason: "BUILD_TOOL_OF"
|
||||
comment: "Build Tool"
|
||||
- pattern: "lib/elixir/test/**/*"
|
||||
reason: "TEST_OF"
|
||||
comment: "Tests"
|
||||
license_finding_curations:
|
||||
# Logos
|
||||
- path: "lib/elixir/pages/images/logo.png"
|
||||
reason: "NOT_DETECTED"
|
||||
comment: "Apply Trademark Policy to Elixir Logo"
|
||||
detected_license: "NONE"
|
||||
concluded_license: "LicenseRef-elixir-trademark-policy"
|
||||
- path: "lib/elixir/scripts/windows_installer/assets/Elixir.ico"
|
||||
reason: "NOT_DETECTED"
|
||||
comment: "Apply Trademark Policy to Elixir Logo"
|
||||
detected_license: "NONE"
|
||||
concluded_license: "LicenseRef-elixir-trademark-policy"
|
||||
|
||||
# Documentation Images
|
||||
- path: "lib/elixir/pages/images/**/*.png"
|
||||
reason: "NOT_DETECTED"
|
||||
comment: "Apply default license to all images"
|
||||
detected_license: "NONE"
|
||||
concluded_license: "Apache-2.0"
|
||||
|
||||
# Test Fixtures
|
||||
- path: "lib/elixir/test/elixir/fixtures/**/*"
|
||||
reason: "NOT_DETECTED"
|
||||
comment: "Apply default license to test fixtures"
|
||||
detected_license: "NONE"
|
||||
concluded_license: "Apache-2.0"
|
||||
|
||||
# Unicode
|
||||
- path: "lib/elixir/unicode/*.txt"
|
||||
reason: "NOT_DETECTED"
|
||||
comment: "Apply default license to unicode files"
|
||||
detected_license: "NONE"
|
||||
concluded_license: "LicenseRef-scancode-unicode"
|
||||
|
||||
# Wrongly Identified
|
||||
- path: "lib/elixir/pages/references/library-guidelines.md"
|
||||
reason: "INCORRECT"
|
||||
comment: |
|
||||
The guide mentions multiple licenses for users to choose from.
|
||||
It however is not licensed itself by the mentioned licenses.
|
||||
concluded_license: "Apache-2.0"
|
||||
- path: "lib/elixir/scripts/windows_installer/.gitignore"
|
||||
reason: "INCORRECT"
|
||||
comment: "Ignored by ScanCode"
|
||||
detected_license: "NONE"
|
||||
concluded_license: "Apache-2.0"
|
||||
@@ -0,0 +1,18 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
||||
|
||||
id: "SpdxDocumentFile:The Elixir Team:exunit:"
|
||||
path_excludes:
|
||||
- pattern: "lib/ex_unit/examples/**/*"
|
||||
reason: "EXAMPLE_OF"
|
||||
comment: "Example"
|
||||
- pattern: "lib/ex_unit/test/**/*"
|
||||
reason: "TEST_OF"
|
||||
comment: "Tests"
|
||||
license_finding_curations:
|
||||
# Test Fixtures
|
||||
- path: "lib/ex_unit/test/fixtures/**/*"
|
||||
reason: "NOT_DETECTED"
|
||||
comment: "Apply default license to test fixtures"
|
||||
detected_license: "NONE"
|
||||
concluded_license: "Apache-2.0"
|
||||
@@ -0,0 +1,8 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
||||
|
||||
id: "SpdxDocumentFile:The Elixir Team:logger:"
|
||||
path_excludes:
|
||||
- pattern: "lib/logger/test/**/*"
|
||||
reason: "TEST_OF"
|
||||
comment: "Tests"
|
||||
@@ -0,0 +1,15 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
||||
|
||||
id: "SpdxDocumentFile:The Elixir Team:mix:"
|
||||
path_excludes:
|
||||
- pattern: "lib/mix/test/**/*"
|
||||
reason: "TEST_OF"
|
||||
comment: "Tests"
|
||||
license_finding_curations:
|
||||
# Test Fixtures
|
||||
- path: "lib/mix/test/fixtures/**/*"
|
||||
reason: "NOT_DETECTED"
|
||||
comment: "Apply default license to test fixtures"
|
||||
detected_license: "NONE"
|
||||
concluded_license: "Apache-2.0"
|
||||
+136
-246
@@ -4,11 +4,13 @@
|
||||
SPDX-FileCopyrightText: 2012 Plataformatec
|
||||
-->
|
||||
|
||||
# Changelog for Elixir v1.19
|
||||
# Changelog for Elixir v1.20
|
||||
|
||||
## Type system improvements
|
||||
|
||||
### More type inference
|
||||
This release includes type inference of all constructs.
|
||||
|
||||
### Type inference of function calls
|
||||
|
||||
Elixir now performs inference of whole functions. The best way to show the new capabilities are with examples. Take the following code:
|
||||
|
||||
@@ -30,294 +32,182 @@ end
|
||||
|
||||
Even though the `+` operator works with both integers and floats, Elixir infers that `a` and `b` must be both integers, as the result of `+` is given to a function that expects an integer. The inferred type information is then used during type checking to find possible typing errors.
|
||||
|
||||
### Type checking of protocol dispatch and implementations
|
||||
### Type inference of guards
|
||||
|
||||
This release also adds type checking when dispatching and implementing protocols.
|
||||
|
||||
For example, string interpolation in Elixir uses the `String.Chars` protocol. If you pass a value that does not implement said protocol, Elixir will now emit a warning accordingly.
|
||||
|
||||
Here is an example passing a range, which cannot be converted into a string, to an interpolation:
|
||||
This release also performs inference of guards! Let's see some examples:
|
||||
|
||||
```elixir
|
||||
defmodule Example do
|
||||
def my_code(first..last//step = range) do
|
||||
"hello #{range}"
|
||||
def example(x, y) when is_list(x) and is_integer(y)
|
||||
```
|
||||
|
||||
The code above correctly infers `x` is a list and `y` is an integer.
|
||||
|
||||
```elixir
|
||||
def example({:ok, x} = y) when is_binary(x) or is_integer(x)
|
||||
```
|
||||
|
||||
The one above infers x is a binary or an integer, and `y` is a two element tuple with `:ok` as first element and a binary or integer as second.
|
||||
|
||||
```elixir
|
||||
def example(x) when is_map_key(x, :foo)
|
||||
```
|
||||
|
||||
The code above infers `x` is a map which has the `:foo` key, represented as `%{..., foo: dynamic()}`. Remember the leading `...` indicates the map may have other keys.
|
||||
|
||||
```elixir
|
||||
def example(x) when not is_map_key(x, :foo)
|
||||
```
|
||||
|
||||
And the code above infers `x` does not have the `:foo` key (hence `x.foo` will raise a typing violation), which has the type: `%{..., foo: not_set()}`.
|
||||
|
||||
You can also have expressions that assert on the size of data structures:
|
||||
|
||||
```elixir
|
||||
def example(x) when tuple_size(x) < 3
|
||||
```
|
||||
|
||||
Elixir will correctly track the tuple has at most two elements, and therefore accessing `elem(x, 3)` will emit a typing violation. In other words, Elixir can look at complex guards, infer types, and use this information to find bugs in our code, without a need to introduce type signatures (yet).
|
||||
|
||||
### Complete typing of maps keys
|
||||
|
||||
Maps were one of the first data-structures we implemented within the Elixir type system however, up to this point, they only supported atom keys. If they had additional keys, those keys were simply marked as `dynamic()`.
|
||||
|
||||
As of Elixir v1.20, we can track all possible domains as map keys. For example, the map:
|
||||
|
||||
```elixir
|
||||
%{123 => "hello", 456.0 => :ok}
|
||||
```
|
||||
|
||||
will have the type:
|
||||
|
||||
```elixir
|
||||
%{integer() => binary(), float() => :ok}
|
||||
```
|
||||
|
||||
It is also possible to mix domain keys, as above, with atom keys, yielding the following:
|
||||
|
||||
```elixir
|
||||
%{integer() => integer(), root: integer()}
|
||||
```
|
||||
|
||||
This system is an implementation of [Typing Records, Maps, and Structs, by Giuseppe Castagna (2023)](https://www.irif.fr/~gc/papers/icfp23.pdf).
|
||||
|
||||
### Typing of map operations
|
||||
|
||||
We have typed the majority of the functions in the `Map` module, allowing the type system to track how keys are added, updated, and removed across all possible key types.
|
||||
|
||||
For example, imagine we are calling the following `Map` functions with a variable `map`, which we don't know the exact shape of, and an atom key:
|
||||
|
||||
```elixir
|
||||
Map.put(map, :key, 123)
|
||||
#=> returns type %{..., key: integer()}
|
||||
|
||||
Map.delete(map, :key)
|
||||
#=> returns type %{..., key: not_set()}
|
||||
```
|
||||
|
||||
As you can see, we track when keys are set and also when they are removed.
|
||||
|
||||
Some operations, like `Map.replace/3`, only replace the key if it exists, and that is also propagated by the type system:
|
||||
|
||||
```elixir
|
||||
Map.replace(map, :key, 123)
|
||||
#=> returns type %{..., key: if_set(integer())}
|
||||
```
|
||||
|
||||
In other words, if the key exists, it would have been replaced by an integer value. Furthermore, whenever calling a function in the `Map` module and the given key is statically proven to never exist in the map, an error is emitted.
|
||||
|
||||
By combining full type inference with bang operations like `Map.fetch!/2`, `Map.pop!/2`, `Map.replace!/3`, and `Map.update!/3`, Elixir is able to propagate information about the desired keys. Take this module:
|
||||
|
||||
```elixir
|
||||
defmodule User do
|
||||
def name(map), do: Map.fetch!(map, :name)
|
||||
end
|
||||
|
||||
defmodule CallsUser do
|
||||
def calls_name do
|
||||
User.name(%{})
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
the above emits the following warnings:
|
||||
The code above has a type violation, which is now caught by the type system:
|
||||
|
||||
```
|
||||
warning: incompatible value given to string interpolation:
|
||||
```text
|
||||
warning: incompatible types given to User.name/1:
|
||||
|
||||
data
|
||||
|
||||
it has type:
|
||||
|
||||
%Range{first: term(), last: term(), step: term()}
|
||||
|
||||
but expected a type that implements the String.Chars protocol, it must be one of:
|
||||
|
||||
dynamic(
|
||||
%Date{} or %DateTime{} or %NaiveDateTime{} or %Time{} or %URI{} or %Version{} or
|
||||
%Version.Requirement{}
|
||||
) or atom() or binary() or float() or integer() or list(term())
|
||||
```
|
||||
|
||||
Warnings are also emitted if you pass a data type that does not implement the `Enumerable` protocol as a generator to for-comprehensions:
|
||||
|
||||
```elixir
|
||||
defmodule Example do
|
||||
def my_code(%Date{} = date) do
|
||||
for(x <- date, do: x)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
will emit:
|
||||
|
||||
```
|
||||
warning: incompatible value given to for-comprehension:
|
||||
|
||||
x <- date
|
||||
|
||||
it has type:
|
||||
|
||||
%Date{year: term(), month: term(), day: term(), calendar: term()}
|
||||
|
||||
but expected a type that implements the Enumerable protocol, it must be one of:
|
||||
|
||||
dynamic(
|
||||
%Date.Range{} or %File.Stream{} or %GenEvent.Stream{} or %HashDict{} or %HashSet{} or
|
||||
%IO.Stream{} or %MapSet{} or %Range{} or %Stream{}
|
||||
) or fun() or list(term()) or non_struct_map()
|
||||
```
|
||||
|
||||
### Type checking and inference of anonymous functions
|
||||
|
||||
Elixir v1.19 can now type infer and type check anonymous functions. Here is a trivial example:
|
||||
|
||||
```elixir
|
||||
defmodule Example do
|
||||
def run do
|
||||
fun = fn %{} -> :map end
|
||||
fun.("hello")
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
The example above has an obvious typing violation, as the anonymous function expects a map but a string is given. With Elixir v1.19, the following warning is now printed:
|
||||
|
||||
```
|
||||
warning: incompatible types given on function application:
|
||||
|
||||
fun.("hello")
|
||||
User.name(%{})
|
||||
|
||||
given types:
|
||||
|
||||
binary()
|
||||
%{name: not_set()}
|
||||
|
||||
but function has type:
|
||||
but expected one of:
|
||||
|
||||
(dynamic(map()) -> :map)
|
||||
dynamic(%{..., name: term()})
|
||||
|
||||
typing violation found at:
|
||||
│
|
||||
6 │ fun.("hello")
|
||||
│ ~
|
||||
16 │ User.name(%{})
|
||||
│ ~
|
||||
│
|
||||
└─ mod.exs:6:8: Example.run/0
|
||||
└─ lib/calls_user.ex:7:5: CallsUser.calls_name/0
|
||||
```
|
||||
|
||||
Function captures, such as `&String.to_integer/1`, will also propagate the type as of Elixir v1.19, arising more opportunity for Elixir's type system to catch bugs in our programs.
|
||||
|
||||
### Acknowledgements
|
||||
|
||||
The type system was made possible thanks to a partnership between [CNRS](https://www.cnrs.fr/) and [Remote](https://remote.com/). The development work is currently sponsored by [Fresha](https://www.fresha.com/), [Starfish*](https://starfish.team/), and [Dashbit](https://dashbit.co/).
|
||||
The type system was made possible thanks to a partnership between [CNRS](https://www.cnrs.fr/) and [Remote](https://remote.com/). The development work is currently sponsored by [Fresha](https://www.fresha.com/) and [Tidewave](https://tidewave.ai/).
|
||||
|
||||
## Faster compile times in large projects
|
||||
## v1.20.0-rc.1 (2026-01-13)
|
||||
|
||||
This release includes two compiler improvements that can lead up to 4x faster builds in large codebases.
|
||||
### 1. Bug fixes
|
||||
|
||||
While Elixir has always compiled the given files in project or a dependency in parallel, the compiler would sometimes be unable to use all of the machine resources efficiently. This release addresses two common limitations, delivering performance improvements that scale with codebase size and available CPU cores.
|
||||
#### Elixir
|
||||
|
||||
### Code loading bottlenecks
|
||||
* [Kernel] Improve the performance of the type system when working with large unions of open maps
|
||||
* [Kernel] Do not crash on map types with struct keys when performing type operations
|
||||
* [Kernel] Mark the outcome of bitstring types as dynamic
|
||||
* [Kernel] `<<expr::bitstring>>` will have type `binary` instead of `bitstring` if `expr` is a binary
|
||||
* [Kernel] Do not crash on conditional variables when calling a function on a module which is represented by a variable
|
||||
|
||||
Prior to this release, Elixir would load modules as soon as they were defined. However, because the Erlang part of code loading happens within a single process (the code server), this would make it a bottleneck, reducing the amount of parallelization, especially on large projects.
|
||||
|
||||
This release makes it so modules are loaded lazily. This reduces the pressure on the code server, making compilation up to 2x faster for large projects, and also reduces the overall amount of work done during compilation.
|
||||
|
||||
Implementation wise, [the parallel compiler already acts as a mechanism to resolve modules during compilation](https://elixir-lang.org/blog/2012/04/24/a-peek-inside-elixir-s-parallel-compiler/), so we built on that. By making sure the compiler controls both module compilation and module loading, it can also better guarantee deterministic builds.
|
||||
|
||||
The only potential regression in this approach happens if you have a module, which is used at compile time and defines an `@on_load` callback (typically used for [NIFs](https://www.erlang.org/doc/system/nif.html)) that invokes another modules within the same project. For example:
|
||||
|
||||
```elixir
|
||||
defmodule MyLib.SomeModule do
|
||||
@on_load :init
|
||||
|
||||
def init do
|
||||
MyLib.AnotherModule.do_something()
|
||||
end
|
||||
|
||||
def something_else do
|
||||
...
|
||||
end
|
||||
end
|
||||
|
||||
MyLib.SomeModule.something_else()
|
||||
```
|
||||
|
||||
The reason this fails is because `@on_load` callbacks are invoked within the code server and therefore they have limited ability to load additional modules. It is generally advisable to limit invocation of external modules during `@on_load` callbacks but, in case it is strictly necessary, you can set `@compile {:autoload, true}` in the invoked module to address this issue in a forward and backwards compatible manner.
|
||||
|
||||
### Parallel compilation of dependencies
|
||||
|
||||
This release introduces a variable called `MIX_OS_DEPS_COMPILE_PARTITION_COUNT`, which instructs `mix deps.compile` to compile dependencies in parallel.
|
||||
|
||||
While fetching dependencies and compiling individual Elixir dependencies already happened in parallel, there were pathological cases where performance would be left on the table, such as compiling dependencies with native code or dependencies where one or two large file would take over most of the compilation time.
|
||||
|
||||
By setting `MIX_OS_DEPS_COMPILE_PARTITION_COUNT` to a number greater than 1, Mix will now compile multiple dependencies at the same time, using separate OS processes. Empirical testing shows that setting it to half of the number of cores on your machine is enough to maximize resource usage. The exact speed up will depend on the number of dependencies and the number of machine cores, although some reports mention up to 4x faster compilation times. If you plan to enable it on CI or build servers, keep in mind it will most likely have a direct impact on memory usage too.
|
||||
|
||||
## Improved pretty printing algorithm
|
||||
|
||||
Elixir v1.19 ships with a new pretty printing implementation that tracks limits as a whole, instead of per depth. Previous versions would track limits per depth. For example, if you had a list of lists of 4 elements and a limit of 5, it would be pretty printed as follows:
|
||||
|
||||
```elixir
|
||||
[
|
||||
[1, 2, 3],
|
||||
[1, 2, ...],
|
||||
[1, ...],
|
||||
[...],
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
This allows for more information to be shown at different nesting levels, which is useful for complex data structures. But it led to some pathological cases where the `limit` option had little effect on actually filtering the amount of data shown. The new implementation decouples the limit handling from depth, decreasing it as it goes. Therefore, the list above with the same limit in Elixir v1.19 is now printed as:
|
||||
|
||||
```elixir
|
||||
[
|
||||
[1, 2, 3],
|
||||
...
|
||||
]
|
||||
```
|
||||
|
||||
The outer list is the first element, the first nested list is the second, followed by three numbers, reaching the limit. This gives developers more precise control over pretty printing.
|
||||
|
||||
Given this may reduce the amount of data printed by default, the default limit has also been increased from 50 to 100. We may further increase it in upcoming releases based on community feedback.
|
||||
|
||||
## OpenChain certification
|
||||
|
||||
Elixir v1.19 is also our first release following OpenChain compliance, [as previously announced](https://elixir-lang.org/blog/2025/02/26/elixir-openchain-certification/). In a nutshell:
|
||||
|
||||
* Elixir releases now include a Source SBoM in CycloneDX 1.6 or later and SPDX 2.3 or later formats.
|
||||
* Each release is attested along with the Source SBoM.
|
||||
|
||||
These additions offer greater transparency into the components and licenses of each release, supporting more rigorous supply chain requirements.
|
||||
|
||||
This work was performed by Jonatan Männchen and sponsored by the Erlang Ecosystem Foundation.
|
||||
|
||||
## v1.19.0-dev
|
||||
## v1.20.0-rc.0 (2026-01-09)
|
||||
|
||||
### 1. Enhancements
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Access] Add `Access.values/0` for traversing maps and keyword lists values
|
||||
* [Base] Add functions to verify if an encoding is valid, such as `valid16?`, `valid64?`, and so forth
|
||||
* [Calendar] Support 2-arity options for `Calendar.strftime/3` which receives the whole data type
|
||||
* [Code] Add `:migrate_call_parens_on_pipe` formatter option
|
||||
* [Code] Add `:indentation` option to `Code.string_to_quoted/2`
|
||||
* [Code.Fragment] Preserve more block content around cursor in `container_cursor_to_quoted`
|
||||
* [Code.Fragment] Add `:block_keyword_or_binary_operator` to `Code.Fragment` for more precise suggestions after operators and closing terminators
|
||||
* [Code.Fragment] Add `Code.Fragment.lines/1`
|
||||
* [Enum] Provide more information on `Enum.OutOfBoundsError`
|
||||
* [Inspect] Allow `optional: :all` when deriving Inspect
|
||||
* [Inspect.Algebra] Add optimistic/pessimistic groups as a simplified implementation of `next_break_fits`
|
||||
* [IO.ANSI] Add ANSI codes to turn off conceal and crossed_out
|
||||
* [Kernel] Allow controlling which applications are used during inference
|
||||
* [Kernel] Support `min/2` and `max/2` as guards
|
||||
* [Kernel.ParallelCompiler] Add `each_long_verification_threshold` which invokes a callback when type checking a module takes too long
|
||||
* [Kernel.ParallelCompiler] Include lines in `== Compilation error in file ... ==` slogans
|
||||
* [Macro] Print debugging results from `Macro.dbg/3` as they happen, instead of once at the end
|
||||
* [Module] Do not automatically load modules after their compilation, guaranteeing a more consistent compile time experience and drastically improving compilation times
|
||||
* [Protocol] Type checking of protocols dispatch and implementations
|
||||
* [Regex] Add `Regex.to_embed/2` which returns an embeddable representation of regex in another regex
|
||||
* [String] Add `String.count/2` to count occurrences of a pattern
|
||||
* [Calendar] Optimize `date_from_iso_days` by using the Neri-Schneider algorithm
|
||||
* [Enum] Add `Enum.min_max` sorter
|
||||
* [Integer] Add `Integer.ceil_div/2`
|
||||
* [IO] Add `IO.iodata_empty?/1`
|
||||
* [File] Skip device, named pipes, etc in `File.cp_r/3` instead of erroring with reason `:eio`
|
||||
* [Kernel] Print intermediate results of `dbg` for pipes
|
||||
* [Kernel] Warn on unused requires
|
||||
* [Regex] Add `Regex.import/1` to import regexes defined with `/E`
|
||||
|
||||
#### ExUnit
|
||||
|
||||
* [ExUnit.CaptureLog] Parallelize log dispatch when multiple processes are capturing log
|
||||
* [ExUnit.Case] Add `:test_group` to the test context
|
||||
* [ExUnit.Doctest] Support ellipsis in doctest exceptions to match the remaining of the exception
|
||||
* [ExUnit.Doctest] Add `:inspect_opts` option for doctest
|
||||
|
||||
#### IEx
|
||||
|
||||
* [IEx] Support multi-line prompts (due to this feature, `:continuation_prompt` and `:alive_continuation_prompt` are no longer supported as IEx configuration)
|
||||
* [IEx.Autocomplete] Functions annotated with `@doc group: "Name"` metadata will appear within their own groups in autocompletion
|
||||
* [ExUnit.CaptureLog] Add `:formatter` option for custom log formatting
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix] Add support for `MIX_PROFILE_FLAGS` to configure `MIX_PROFILE`
|
||||
* [mix compile] Debug the compiler and type checker PID when `MIX_DEBUG=1` and compilation/verification thresholds are met
|
||||
* [mix compile] Add `Mix.Tasks.Compiler.reenable/1`
|
||||
* [mix deps.compile] Support `MIX_OS_DEPS_COMPILE_PARTITION_COUNT` for compiling deps concurrently across multiple operating system processes
|
||||
* [mix help] Add `mix help Mod`, `mix help :mod`, `mix help Mod.fun` and `mix help Mod.fun/arity`
|
||||
* [mix test] Allow to distinguish the exit status between warnings as errors and test failures
|
||||
* [mix xref graph] Add support for `--format json`
|
||||
* [mix xref graph] Emit a warning if `--source` is part of a cycle
|
||||
* [M ix.Task.Compiler] Add `Mix.Task.Compiler.run/2`
|
||||
* [mix deps] Support filtering `mix deps` output
|
||||
* [mix compile] Enforce `:elixirc_paths` to be a list of strings to avoid paths from being discarded (the only documented type was lists of strings)
|
||||
* [mix test] Add `mix test --dry-run`
|
||||
|
||||
### 2. Bug fixes
|
||||
### 2. Hard deprecations
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [DateTime] Do not truncate microseconds regardless of precision in `DateTime.diff/3`
|
||||
* [File] Properly handle permissions errors cascading from parent in `File.mkdir_p/1`
|
||||
* [Kernel] `not_a_map.key` now raises `BadMapError` for consistency with other map operations
|
||||
* [Regex] Fix `Regex.split/2` returning too many results when the chunk being split on was empty (which can happen when using features such as `/K`)
|
||||
* [Stream] Ensure `Stream.transform/5` respects suspend command when its inner stream halts
|
||||
* [URI] Several fixes to `URI.merge/2` related to trailing slashes, trailing dots, and hostless base URIs
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix cmd] Preserve argument quoting in subcommands
|
||||
* [mix format] Ensure the formatter does not go over the specified limit in certain corner cases
|
||||
* [mix release] Fix `RELEASE_SYS_CONFIG` for Windows 11
|
||||
* [mix test] Preserve files with no longer filter on `mix test`
|
||||
* [mix xref graph] Provide more consistent output by considering strong connected components only when computing graphs
|
||||
|
||||
### 3. Soft deprecations (no warnings emitted)
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Inspect.Algebra] `next_break_fits` is deprecated in favor of `optimistic`/`pessimistic` groups
|
||||
* [Node] `Node.start/2-3` is deprecated in favor of `Node.start/2` with a keyword list
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix compile] `--no-protocol-consolidation` is deprecated in favor of `--no-consolidate-protocols` for consistency with `mix.exs` configuration
|
||||
* [mix compile.protocols] Protocol consolidation is now part of `compile.elixir` and has no effect
|
||||
|
||||
### 4. Hard deprecations
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Code] The `on_undefined_variable: :warn` is deprecated. Relying on undefined variables becoming function calls will not be supported in the future
|
||||
* [File] Passing a callback as third argument to `File.cp/3` is deprecated, pass it as a `on_conflict: callback` option instead
|
||||
* [File] Passing a callback as third argument to `File.cp_r/3` is deprecated, pass it as a `on_conflict: callback` option instead
|
||||
* [Kernel] The struct update syntax, such as `%URI{uri | path: "/foo/bar"}` is deprecated in favor of pattern matching on the struct when the variable is defined and then using the map update syntax `%{uri | path: "/foo/bar"}`. Thanks to the type system, pattern matching on structs can find more errors, more reliably
|
||||
* [Kernel.ParallelCompiler] Passing `return_diagnostics: true` as an option is required on `compile`, `compile_to_path` and `require`
|
||||
* [File] `File.stream!(path, modes, lines_or_bytes)` is deprecated in favor of `File.stream!(path, lines_or_bytes, modes)`
|
||||
* [Kernel] Matching on the size inside a bit pattern now requires the pin operator for consistency, such as `<<x::size(^existing_var)>>`
|
||||
* [Kernel.ParallelCompiler] `Kernel.ParallelCompiler.async/1` is deprecated in favor of `Kernel.ParallelCompiler.pmap/2`, which is more performant and addresses known limitations
|
||||
|
||||
#### Logger
|
||||
|
||||
* [Logger] The `:backends` configuration is deprecated, either set the `:default_handler` to false or start backends in your application start callback
|
||||
* [Logger] `Logger.*_backend` functions are deprecated in favor of handlers. If you really want to keep on using backends, see the `:logger_backends` package
|
||||
* [Logger] `Logger.enable/1` and `Logger.disable/1` have been deprecated in favor of `Logger.put_process_level/2` and `Logger.delete_process_level/1`
|
||||
|
||||
#### Mix
|
||||
## v1.19
|
||||
|
||||
* [mix] The `:default_task`, `:preferred_cli_env`, and `:preferred_cli_target` configuration inside `def project` in your `mix.exs` has been deprecated in favor of `:default_task`, `:preferred_envs` and `:preferred_targets` inside the `def cli` function
|
||||
* [mix do] Using commas as task separator in `mix do` (such as `mix do foo, bar`) is deprecated, use `+` instead (as in `mix do foo + bar`)
|
||||
|
||||
## v1.18
|
||||
|
||||
The CHANGELOG for v1.18 releases can be found [in the v1.18 branch](https://github.com/elixir-lang/elixir/blob/v1.18/CHANGELOG.md).
|
||||
The CHANGELOG for v1.19 releases can be found [in the v1.19 branch](https://github.com/elixir-lang/elixir/blob/v1.19/CHANGELOG.md).
|
||||
|
||||
+5
-5
@@ -6,7 +6,7 @@
|
||||
|
||||
# Code of Conduct
|
||||
|
||||
Contact: elixir-lang-conduct@googlegroups.com
|
||||
Contact: <elixir-lang-conduct@googlegroups.com>
|
||||
|
||||
## Why have a Code of Conduct?
|
||||
|
||||
@@ -51,15 +51,15 @@ If you participate in or contribute to the Elixir ecosystem in any way, you are
|
||||
|
||||
Explicit enforcement of the Code of Conduct applies to the official mediums operated by the Elixir project:
|
||||
|
||||
* The [official GitHub projects][1] and code reviews.
|
||||
* The official elixir-lang mailing lists.
|
||||
* The **[#elixir][2]** IRC channel on [Libera.Chat][3].
|
||||
* The [official GitHub projects][1] and code reviews.
|
||||
* The official elixir-lang mailing lists.
|
||||
* The **[#elixir][2]** IRC channel on [Libera.Chat][3].
|
||||
|
||||
Other Elixir activities (such as conferences, meetups, and unofficial forums) are encouraged to adopt this Code of Conduct. Such groups must provide their own contact information.
|
||||
|
||||
Project maintainers may block, remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct.
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by emailing: elixir-lang-conduct@googlegroups.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. **All reports will be kept confidential**.
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by emailing: <elixir-lang-conduct@googlegroups.com>. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. **All reports will be kept confidential**.
|
||||
|
||||
**The goal of the Code of Conduct is to resolve conflicts in the most harmonious way possible**. We hope that in most cases issues may be resolved through polite discussion and mutual agreement. Bannings and other forceful measures are to be employed only as a last resort. **Do not** post about the issue publicly or try to rally sentiment against a particular individual or group.
|
||||
|
||||
|
||||
+37
-29
@@ -78,6 +78,7 @@ introduced behavior, especially for bug fixes and major changes:
|
||||
*fails* before your change and *passes* afterward. This makes it easier to
|
||||
confirm that the fix addresses the underlying issue and helps prevent
|
||||
regressions in the future.
|
||||
|
||||
* **New Features or Major Changes:** If you are adding a new feature or making
|
||||
major changes to existing functionality, please add tests that cover the
|
||||
major parts of that functionality. Aim to have the best code coverage possible.
|
||||
@@ -88,7 +89,9 @@ 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)
|
||||
|
||||
## Reviewing changes
|
||||
@@ -121,30 +124,34 @@ Please review our [Open Source Policy](OPEN_SOURCE_POLICY.md) for complete
|
||||
guidelines on licensing and compliance. Below is a summary of the key points
|
||||
affecting **all external contributors**:
|
||||
|
||||
- Accepted Licenses: Any code contributed must be licensed under the
|
||||
`Apache-2.0` license.
|
||||
- SPDX License Headers: With the exception of approved test fixture files,
|
||||
all new or modified files in a pull request must include correct SPDX
|
||||
headers. If you are creating a new file under the `Apache-2.0` license, for
|
||||
instance, please use:
|
||||
|
||||
* Accepted Licenses: Any code contributed must be licensed under the
|
||||
`Apache-2.0` license.
|
||||
|
||||
* SPDX License Headers: With the exception of approved test fixture files,
|
||||
all new or modified files in a pull request must include correct SPDX
|
||||
headers. If you are creating a new file under the `Apache-2.0` license, for
|
||||
instance, please use:
|
||||
|
||||
```elixir
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
||||
```
|
||||
|
||||
- No Executable Binaries: Contributions must **not** include any executable
|
||||
binary files. If you require an exception (for example, certain test artifacts),
|
||||
please see the policy on how to request approval and document exceptions.
|
||||
- Preserving Copyright and License Info: If you copy code from elsewhere,
|
||||
ensure that **all original copyright and license notices remain intact**. If
|
||||
they are missing or incomplete, you must add them.
|
||||
- Failure to Comply: Pull requests that do not meet these licensing and
|
||||
compliance standards will be rejected or require modifications before merging.
|
||||
- Developer Certificate of Origin: All contributions are subject to the
|
||||
Developer Certificate of Origin.
|
||||
|
||||
```
|
||||
* No Executable Binaries: Contributions must **not** include any executable
|
||||
binary files. If you require an exception (for example, certain test artifacts),
|
||||
please see the policy on how to request approval and document exceptions.
|
||||
|
||||
* Preserving Copyright and License Info: If you copy code from elsewhere,
|
||||
ensure that **all original copyright and license notices remain intact**. If
|
||||
they are missing or incomplete, you must add them.
|
||||
|
||||
* Failure to Comply: Pull requests that do not meet these licensing and
|
||||
compliance standards will be rejected or require modifications before merging.
|
||||
|
||||
* Developer Certificate of Origin: All contributions are subject to the
|
||||
Developer Certificate of Origin.
|
||||
|
||||
```text
|
||||
By making a contribution to this project, I certify that:
|
||||
|
||||
(a) The contribution was created in whole or in part by me and I
|
||||
@@ -171,25 +178,26 @@ affecting **all external contributors**:
|
||||
involved.
|
||||
```
|
||||
|
||||
See http://developercertificate.org/ for a copy of the Developer Certificate
|
||||
See <https://developercertificate.org/> for a copy of the Developer Certificate
|
||||
of Origin license.
|
||||
|
||||
## Building documentation
|
||||
|
||||
Building the documentation requires that [ExDoc](https://github.com/elixir-lang/ex_doc)
|
||||
is installed and built alongside Elixir:
|
||||
is installed and built alongside Elixir.
|
||||
|
||||
After cloning and compiling Elixir, run:
|
||||
|
||||
```sh
|
||||
# After cloning and compiling Elixir, in its parent directory:
|
||||
git clone https://github.com/elixir-lang/ex_doc.git
|
||||
cd ex_doc && ../elixir/bin/elixir ../elixir/bin/mix do deps.get + compile
|
||||
```
|
||||
elixir_dir=$(pwd)
|
||||
cd .. && git clone https://github.com/elixir-lang/ex_doc.git
|
||||
cd ex_doc && "${elixir_dir}/bin/elixir" "${elixir_dir}/bin/mix" do deps.get + compile
|
||||
|
||||
Now go back to Elixir's root directory and run:
|
||||
# Now we will go back to Elixir's root directory,
|
||||
cd "${elixir_dir}"
|
||||
|
||||
```sh
|
||||
make docs # to generate HTML pages
|
||||
make docs DOCS_FORMAT=epub # to generate EPUB documents
|
||||
# and generate HTML and EPUB documents:
|
||||
make docs
|
||||
```
|
||||
|
||||
This will produce documentation sets for `elixir`, `eex`, `ex_unit`, `iex`, `logger`,
|
||||
|
||||
@@ -107,8 +107,10 @@ $(KERNEL): lib/elixir/src/* lib/elixir/lib/*.ex lib/elixir/lib/*/*.ex lib/elixir
|
||||
fi
|
||||
@ echo "==> elixir (compile)";
|
||||
$(Q) cd lib/elixir && ../../$(ELIXIRC_MIN_SIG) "lib/**/*.ex" -o ebin;
|
||||
$(Q) $(GENERATE_APP) $(VERSION)
|
||||
$(Q) bin/elixir lib/elixir/scripts/infer.exs;
|
||||
|
||||
$(APP): lib/elixir/src/elixir.app.src lib/elixir/ebin VERSION $(GENERATE_APP)
|
||||
$(APP): lib/elixir/src/elixir.app.src $(GENERATE_APP)
|
||||
$(Q) $(GENERATE_APP) $(VERSION)
|
||||
|
||||
unicode: $(UNICODE)
|
||||
|
||||
+10
-13
@@ -20,7 +20,7 @@ ensuring that Elixir remains a trusted and innovative open source project.
|
||||
## 2. Scope
|
||||
|
||||
This policy applies to the Elixir Programming language, located at
|
||||
https://github.com/elixir-lang/elixir. It covers every file, and contribution
|
||||
<https://github.com/elixir-lang/elixir>. It covers every file, and contribution
|
||||
made, including documentation and any associated assets.
|
||||
|
||||
## 3. Licensing
|
||||
@@ -29,18 +29,19 @@ All code released by the Elixir team is licensed under the
|
||||
[Apache-2.0](./LICENSES/Apache-2.0.txt) license. Additionally, the following
|
||||
licenses are recognized as permissible in this project:
|
||||
|
||||
- The Unicode license, as documented at
|
||||
[LicenseRef-scancode-unicode](./LICENSES/LicenseRef-scancode-unicode.txt)
|
||||
- The Elixir Trademark Policy, as documented at
|
||||
[LicenseRef-elixir-trademark-policy](./LICENSES/LicenseRef-elixir-trademark-policy.txt)
|
||||
- The Unicode license, as documented at
|
||||
[LicenseRef-scancode-unicode](./LICENSES/LicenseRef-scancode-unicode.txt)
|
||||
|
||||
- The Elixir Trademark Policy, as documented at
|
||||
[LicenseRef-elixir-trademark-policy](./LICENSES/LicenseRef-elixir-trademark-policy.txt)
|
||||
|
||||
These licenses are considered acceptable for any files or code that form part of
|
||||
an Elixir repository. If a contribution requires a different license, it must
|
||||
either be rejected or prompt an update to this policy.
|
||||
|
||||
## 4. Contributing to Elixir Projects
|
||||
## 4. Contributing to the Elixir repository
|
||||
|
||||
Any code contributed to Elixir repositories must fall under one of the accepted
|
||||
Any code contributed to the Elixir repository must fall under one of the accepted
|
||||
licenses (Apache-2.0, Unicode, or Elixir Trademark). Contributions under any
|
||||
other license will be rejected unless this policy is formally revised to include
|
||||
that license. All files except those specifically exempted (e.g., certain test
|
||||
@@ -51,13 +52,9 @@ configuration and undergo review.
|
||||
|
||||
Contributions must not introduce executable binary files into the codebase.
|
||||
|
||||
Every Elixir project within the organization will have an automated GitHub
|
||||
Action to enforce these rules. This mechanism aids in detecting non-compliant
|
||||
licenses or files early in the review process.
|
||||
|
||||
## 5. Preservation of Copyright and License Information
|
||||
|
||||
Any third-party code incorporated into Elixir projects must retain original
|
||||
Any third-party code incorporated into the Elixir repository must retain original
|
||||
copyright and license headers. If no such headers exist in the source, they must
|
||||
be added. This practice ensures that original authors receive proper credit and
|
||||
that the licensing lineage is preserved.
|
||||
@@ -165,4 +162,4 @@ necessary, by the EEF CISO. Any significant changes will be communicated to
|
||||
contributors and made publicly available.
|
||||
|
||||
*Effective Date: 2025-02-20*
|
||||
*Last Reviewed: 2025-02-20*
|
||||
*Last Reviewed: 2025-11-20*
|
||||
|
||||
@@ -59,7 +59,7 @@ Our *actionable item policy* has some important consequences, such as:
|
||||
comment and we can always reopen the issue.
|
||||
|
||||
By keeping the overall issues tracker tidy and organized, the community
|
||||
can easily peak at what is coming in new releases and also get involved
|
||||
can easily peek at what is coming in new releases and also get involved
|
||||
by commenting on existing issues and submitting pull requests. Please
|
||||
remember to keep the tone positive and be kind! For more information,
|
||||
see the [Code of Conduct][1].
|
||||
|
||||
+4
-10
@@ -8,15 +8,9 @@
|
||||
|
||||
## Shipping a new version
|
||||
|
||||
1. Update version in /VERSION, bin/elixir, bin/elixir.bat, and bin/elixir.ps1
|
||||
1. Update version in /VERSION, bin/elixir, and bin/elixir.bat
|
||||
|
||||
2. Ensure /CHANGELOG.md is updated, versioned and add the current date
|
||||
- If this release addresses any publicly known security vulnerabilities with
|
||||
assigned CVEs, add a "Security" section to `CHANGELOG.md`. For example:
|
||||
```md
|
||||
## Security
|
||||
- Fixed CVE-2025-00000: Description of the vulnerability
|
||||
```
|
||||
|
||||
3. Update "Compatibility and Deprecations" if a new OTP version is supported
|
||||
|
||||
@@ -30,11 +24,11 @@
|
||||
|
||||
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 (usually before first rc)
|
||||
|
||||
### In the new branch
|
||||
|
||||
1. Comment the `CANONICAL=` in /Makefile
|
||||
1. Comment out `CANONICAL := main/` in /Makefile
|
||||
|
||||
2. Update tables in /SECURITY.md and "Compatibility and Deprecations"
|
||||
|
||||
@@ -42,7 +36,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
|
||||
|
||||
|
||||
+4
-4
@@ -12,16 +12,16 @@ Elixir applies bug fixes only to the latest minor branch. Security patches are a
|
||||
|
||||
Elixir version | Support
|
||||
:------------- | :-----------------------------
|
||||
1.19 | Development
|
||||
1.18 | Bug fixes and security patches
|
||||
1.20 | Development
|
||||
1.19 | Bug fixes and security patches
|
||||
1.18 | Security patches only
|
||||
1.17 | Security patches only
|
||||
1.16 | Security patches only
|
||||
1.15 | Security patches only
|
||||
1.14 | Security patches only
|
||||
|
||||
## Announcements
|
||||
|
||||
New releases are announced in the read-only [announcements mailing list](https://groups.google.com/group/elixir-lang-ann). You can subscribe by sending an email to elixir-lang-ann+subscribe@googlegroups.com and replying to the confirmation email. Security notifications [will be tagged with `[security]`](https://groups.google.com/forum/#!searchin/elixir-lang-ann/%5Bsecurity%5D%7Csort:date).
|
||||
New releases are announced in the read-only [announcements mailing list](https://groups.google.com/group/elixir-lang-ann). You can subscribe by sending an email to <elixir-lang-ann+subscribe@googlegroups.com> and replying to the confirmation email. Security notifications [will be tagged with `[security]`](https://groups.google.com/forum/#!searchin/elixir-lang-ann/%5Bsecurity%5D%7Csort:date).
|
||||
|
||||
You may also see [all releases](https://github.com/elixir-lang/elixir/releases) and [consult all disclosed vulnerabilities](https://github.com/elixir-lang/elixir/security) on GitHub.
|
||||
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
|
||||
set -e
|
||||
|
||||
ELIXIR_VERSION=1.19.0-dev
|
||||
ELIXIR_VERSION=1.20.0-rc.1
|
||||
|
||||
if [ $# -eq 0 ] || { [ $# -eq 1 ] && { [ "$1" = "--help" ] || [ "$1" = "-h" ]; }; }; then
|
||||
cat <<USAGE >&2
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
:: SPDX-FileCopyrightText: 2021 The Elixir Team
|
||||
:: SPDX-FileCopyrightText: 2012 Plataformatec
|
||||
|
||||
set ELIXIR_VERSION=1.19.0-dev
|
||||
set ELIXIR_VERSION=1.20.0-rc.1
|
||||
|
||||
if ""%1""=="""" if ""%2""=="""" goto documentation
|
||||
if /I ""%1""==""--help"" if ""%2""=="""" goto documentation
|
||||
|
||||
+23
-6
@@ -118,6 +118,19 @@ defmodule EEx do
|
||||
| {:expr | :start_expr | :middle_expr | :end_expr, marker, charlist, metadata}
|
||||
| {:eof, metadata}
|
||||
|
||||
@type tokenize_opt ::
|
||||
{:file, binary()}
|
||||
| {:line, line}
|
||||
| {:column, column}
|
||||
| {:indentation, non_neg_integer}
|
||||
| {:trim, boolean()}
|
||||
|
||||
@type compile_opt ::
|
||||
tokenize_opt
|
||||
| {:engine, module()}
|
||||
| {:parser_options, Code.parser_opts()}
|
||||
| {atom(), term()}
|
||||
|
||||
@doc """
|
||||
Generates a function definition from the given string.
|
||||
|
||||
@@ -128,6 +141,7 @@ defmodule EEx do
|
||||
template.
|
||||
|
||||
The supported `options` are described [in the module docs](#module-options).
|
||||
Additional options are passed to the underlying engine.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -220,9 +234,11 @@ defmodule EEx do
|
||||
"3"
|
||||
|
||||
"""
|
||||
@spec compile_string(String.t(), keyword) :: Macro.t()
|
||||
@spec compile_string(String.t(), [compile_opt]) :: Macro.t()
|
||||
def compile_string(source, options \\ []) when is_binary(source) and is_list(options) do
|
||||
case tokenize(source, options) do
|
||||
tokenize_opts = Keyword.take(options, [:file, :line, :column, :indentation, :trim])
|
||||
|
||||
case tokenize(source, tokenize_opts) do
|
||||
{:ok, tokens} ->
|
||||
EEx.Compiler.compile(tokens, source, options)
|
||||
|
||||
@@ -259,7 +275,7 @@ defmodule EEx do
|
||||
#=> "3"
|
||||
|
||||
"""
|
||||
@spec compile_file(Path.t(), keyword) :: Macro.t()
|
||||
@spec compile_file(Path.t(), [compile_opt]) :: Macro.t()
|
||||
def compile_file(filename, options \\ []) when is_list(options) do
|
||||
filename = IO.chardata_to_string(filename)
|
||||
options = Keyword.merge([file: filename, line: 1], options)
|
||||
@@ -277,7 +293,7 @@ defmodule EEx do
|
||||
"foo baz"
|
||||
|
||||
"""
|
||||
@spec eval_string(String.t(), keyword, keyword) :: String.t()
|
||||
@spec eval_string(String.t(), keyword, [compile_opt]) :: term()
|
||||
def eval_string(source, bindings \\ [], options \\ [])
|
||||
when is_binary(source) and is_list(bindings) and is_list(options) do
|
||||
compiled = compile_string(source, options)
|
||||
@@ -299,7 +315,7 @@ defmodule EEx do
|
||||
#=> "foo baz"
|
||||
|
||||
"""
|
||||
@spec eval_file(Path.t(), keyword, keyword) :: String.t()
|
||||
@spec eval_file(Path.t(), keyword, [compile_opt]) :: String.t()
|
||||
def eval_file(filename, bindings \\ [], options \\ [])
|
||||
when is_list(bindings) and is_list(options) do
|
||||
filename = IO.chardata_to_string(filename)
|
||||
@@ -339,7 +355,7 @@ defmodule EEx do
|
||||
Note new tokens may be added in the future.
|
||||
"""
|
||||
@doc since: "1.14.0"
|
||||
@spec tokenize([char()] | String.t(), opts :: keyword) ::
|
||||
@spec tokenize([char()] | String.t(), [tokenize_opt]) ::
|
||||
{:ok, [token()]} | {:error, String.t(), metadata()}
|
||||
def tokenize(contents, opts \\ []) do
|
||||
EEx.Compiler.tokenize(contents, opts)
|
||||
@@ -348,6 +364,7 @@ defmodule EEx do
|
||||
### Helpers
|
||||
|
||||
defp do_eval(compiled, bindings, options) do
|
||||
options = Keyword.take(options, [:file, :line, :module, :prune_binding])
|
||||
{result, _} = Code.eval_quoted(compiled, bindings, options)
|
||||
result
|
||||
end
|
||||
|
||||
@@ -96,7 +96,7 @@ defmodule EEx.Compiler do
|
||||
"unexpected beginning of EEx tag \"<%#{marker}\" on \"<%#{marker}#{expr}%>\", " <>
|
||||
"please remove \"#{marker}\""
|
||||
|
||||
:elixir_errors.erl_warn({line, column}, state.file, message)
|
||||
IO.warn(message, file: state.file, line: line, column: column)
|
||||
~c""
|
||||
else
|
||||
marker
|
||||
@@ -373,7 +373,7 @@ defmodule EEx.Compiler 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)
|
||||
IO.warn(message, file: state.file, line: meta.line, column: meta.column)
|
||||
end
|
||||
|
||||
buffer = state.engine.handle_expr(buffer, IO.chardata_to_string(mark), contents)
|
||||
|
||||
@@ -17,6 +17,10 @@ defmodule EEx.Engine do
|
||||
@doc """
|
||||
Called at the beginning of every template.
|
||||
|
||||
It receives the options during compilation, including the
|
||||
ones managed by EEx, such as `:line` and `:file`, as well
|
||||
as custom engine options.
|
||||
|
||||
It must return the initial state.
|
||||
"""
|
||||
@callback init(opts :: keyword) :: state
|
||||
|
||||
@@ -873,11 +873,6 @@ defmodule Access do
|
||||
...> end)
|
||||
{[], [%{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.filter(5)])
|
||||
** (FunctionClauseError) no function clause matching in Access.filter/1
|
||||
|
||||
An error is raised if the accessed structure is not a list:
|
||||
|
||||
iex> get_in(%{}, [Access.filter(fn a -> a == 10 end)])
|
||||
@@ -891,7 +886,7 @@ defmodule Access do
|
||||
end
|
||||
|
||||
defp filter(:get, data, func, next) when is_list(data) do
|
||||
data |> Enum.filter(func) |> Enum.map(next)
|
||||
for elem <- data, func.(elem), do: next.(elem)
|
||||
end
|
||||
|
||||
defp filter(:get_and_update, data, func, next) when is_list(data) do
|
||||
@@ -1154,11 +1149,6 @@ defmodule Access do
|
||||
...> 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)])
|
||||
|
||||
@@ -507,7 +507,7 @@ defmodule Application do
|
||||
of all loaded applications. Returns `nil` if
|
||||
the module is not listed in any application spec.
|
||||
"""
|
||||
@spec get_application(atom) :: atom | nil
|
||||
@spec get_application(module) :: app | nil
|
||||
def get_application(module) when is_atom(module) do
|
||||
case :application.get_application(module) do
|
||||
{:ok, app} -> app
|
||||
@@ -696,7 +696,7 @@ defmodule Application do
|
||||
config :my_app, Databases.RepoTwo,
|
||||
# Another database configuration (for the same OTP app)
|
||||
ip: "localhost",
|
||||
port: 20717
|
||||
port: 20_717
|
||||
|
||||
config :my_app, my_app_databases: [Databases.RepoOne, Databases.RepoTwo]
|
||||
|
||||
@@ -814,7 +814,7 @@ defmodule Application do
|
||||
stick after the application is loaded and also on application reload.
|
||||
"""
|
||||
@spec put_env(app, key, value, timeout: timeout, persistent: boolean) :: :ok
|
||||
def put_env(app, key, value, opts \\ []) when is_atom(app) do
|
||||
def put_env(app, key, value, opts \\ []) when is_atom(app) and is_list(opts) do
|
||||
maybe_warn_on_app_env_key(app, key)
|
||||
:application.set_env(app, key, value, opts)
|
||||
end
|
||||
@@ -856,7 +856,7 @@ defmodule Application do
|
||||
It receives the same options as `put_env/4`. Returns `:ok`.
|
||||
"""
|
||||
@spec delete_env(app, key, timeout: timeout, persistent: boolean) :: :ok
|
||||
def delete_env(app, key, opts \\ []) when is_atom(app) do
|
||||
def delete_env(app, key, opts \\ []) when is_atom(app) and is_list(opts) do
|
||||
maybe_warn_on_app_env_key(app, key)
|
||||
:application.unset_env(app, key, opts)
|
||||
end
|
||||
@@ -903,13 +903,13 @@ defmodule Application do
|
||||
@doc """
|
||||
Ensures the given `app` or `apps` and their child applications are started.
|
||||
|
||||
The second argument is either the `t:restart_type/1` (for consistency with
|
||||
The second argument is either the `t:restart_type/0` (for consistency with
|
||||
`start/2`) or a keyword list.
|
||||
|
||||
## Options
|
||||
|
||||
* `:type` - if the application should be started `:temporary` (default),
|
||||
`:permanent`, or `:transient`. See `t:restart_type/1` for more information.
|
||||
`:permanent`, or `:transient`. See `t:restart_type/0` for more information.
|
||||
|
||||
* `:mode` - (since v1.15.0) if the applications should be started serially
|
||||
(`:serial`, default) or concurrently (`:concurrent`).
|
||||
@@ -921,11 +921,11 @@ defmodule Application do
|
||||
{:ok, [app]} | {:error, term}
|
||||
def ensure_all_started(app_or_apps, type_or_opts \\ [])
|
||||
|
||||
def ensure_all_started(app, type) when is_atom(type) do
|
||||
ensure_all_started(app, type: type)
|
||||
def ensure_all_started(app_or_apps, type) when is_atom(type) do
|
||||
ensure_all_started(app_or_apps, type: type)
|
||||
end
|
||||
|
||||
def ensure_all_started(app, opts) when is_atom(app) do
|
||||
def ensure_all_started(app, opts) when is_atom(app) and is_list(opts) do
|
||||
ensure_all_started([app], opts)
|
||||
end
|
||||
|
||||
@@ -1056,7 +1056,8 @@ defmodule Application do
|
||||
Returns a list with information about the applications which are currently running.
|
||||
"""
|
||||
@spec started_applications(timeout) :: [{app, description :: charlist(), vsn :: charlist()}]
|
||||
def started_applications(timeout \\ 5000) do
|
||||
def started_applications(timeout \\ 5000)
|
||||
when timeout == :infinity or (is_integer(timeout) and timeout >= 0) do
|
||||
:application.which_applications(timeout)
|
||||
end
|
||||
|
||||
|
||||
@@ -162,6 +162,22 @@ defmodule Calendar do
|
||||
"""
|
||||
@type time_zone_database :: module()
|
||||
|
||||
@typedoc """
|
||||
Options for formatting dates and times with `strftime/3`.
|
||||
"""
|
||||
@type strftime_opts :: [
|
||||
preferred_datetime: String.t(),
|
||||
preferred_date: String.t(),
|
||||
preferred_time: String.t(),
|
||||
am_pm_names: (:am | :pm -> String.t()) | (:am | :pm, map() -> String.t()),
|
||||
month_names: (pos_integer() -> String.t()) | (pos_integer(), map() -> String.t()),
|
||||
abbreviated_month_names:
|
||||
(pos_integer() -> String.t()) | (pos_integer(), map() -> String.t()),
|
||||
day_of_week_names: (pos_integer() -> String.t()) | (pos_integer(), map() -> String.t()),
|
||||
abbreviated_day_of_week_names:
|
||||
(pos_integer() -> String.t()) | (pos_integer(), map() -> String.t())
|
||||
]
|
||||
|
||||
@doc """
|
||||
Returns how many days there are in the given month of the given year.
|
||||
"""
|
||||
@@ -617,7 +633,7 @@ defmodule Calendar do
|
||||
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
@spec strftime(map(), String.t(), keyword()) :: String.t()
|
||||
@spec strftime(map(), String.t(), strftime_opts()) :: String.t()
|
||||
def strftime(date_or_time_or_datetime, string_format, user_options \\ [])
|
||||
when is_map(date_or_time_or_datetime) and is_binary(string_format) do
|
||||
parse(
|
||||
|
||||
@@ -53,7 +53,7 @@ defmodule Date do
|
||||
iex> Date.diff(~D[2010-04-17], ~D[1970-01-01])
|
||||
14716
|
||||
|
||||
iex> Date.add(~D[1970-01-01], 14716)
|
||||
iex> Date.add(~D[1970-01-01], 14_716)
|
||||
~D[2010-04-17]
|
||||
|
||||
iex> Date.shift(~D[1970-01-01], year: 40, month: 3, week: 2, day: 2)
|
||||
@@ -321,7 +321,7 @@ defmodule Date do
|
||||
@doc """
|
||||
Converts the given date to a string according to its calendar.
|
||||
|
||||
### Examples
|
||||
## Examples
|
||||
|
||||
iex> Date.to_string(~D[2000-02-28])
|
||||
"2000-02-28"
|
||||
@@ -399,7 +399,7 @@ defmodule Date do
|
||||
or other calendars in which the days also start at midnight.
|
||||
Attempting to convert dates from other calendars will raise an `ArgumentError`.
|
||||
|
||||
### Examples
|
||||
## Examples
|
||||
|
||||
iex> Date.to_iso8601(~D[2000-02-28])
|
||||
"2000-02-28"
|
||||
@@ -633,7 +633,7 @@ defmodule Date do
|
||||
## Examples
|
||||
|
||||
Imagine someone implements `Calendar.Holocene`, a calendar based on the
|
||||
Gregorian calendar that adds exactly 10,000 years to the current Gregorian
|
||||
Gregorian calendar that adds exactly 10 000 years to the current Gregorian
|
||||
year:
|
||||
|
||||
iex> Date.convert(~D[2000-01-01], Calendar.Holocene)
|
||||
@@ -667,7 +667,7 @@ defmodule Date do
|
||||
## Examples
|
||||
|
||||
Imagine someone implements `Calendar.Holocene`, a calendar based on the
|
||||
Gregorian calendar that adds exactly 10,000 years to the current Gregorian
|
||||
Gregorian calendar that adds exactly 10 000 years to the current Gregorian
|
||||
year:
|
||||
|
||||
iex> Date.convert!(~D[2000-01-01], Calendar.Holocene)
|
||||
@@ -691,10 +691,15 @@ defmodule Date do
|
||||
@doc """
|
||||
Adds the number of days to the given `date`.
|
||||
|
||||
The days are counted as Gregorian days. The date is returned in the same
|
||||
calendar as it was given in.
|
||||
> #### Prefer `shift/2` {: .info}
|
||||
>
|
||||
> Prefer `shift/2` over `add/2`, as it offers a more ergonomic API.
|
||||
>
|
||||
> `add/2` always considers a day to be measured according to the
|
||||
> `Calendar.ISO`.
|
||||
|
||||
To shift a date by a `Duration` and according to its underlying calendar, use `Date.shift/2`.
|
||||
The days are counted as Gregorian days, independent of the underlying
|
||||
calendar. The date is returned in the same calendar as it was given in.
|
||||
|
||||
## Examples
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ defmodule Date.Range do
|
||||
[date_from_iso_days(current, calendar)]
|
||||
end
|
||||
|
||||
defp slice(current, step, remaining, calendar) do
|
||||
defp slice(current, step, remaining, calendar) when remaining > 1 do
|
||||
[
|
||||
date_from_iso_days(current, calendar)
|
||||
| slice(current + step, step, remaining - 1, calendar)
|
||||
|
||||
@@ -1046,7 +1046,7 @@ defmodule DateTime do
|
||||
its abbreviation, which means information is lost when converting to such
|
||||
format.
|
||||
|
||||
### Examples
|
||||
## Examples
|
||||
|
||||
iex> dt = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "CET",
|
||||
...> hour: 23, minute: 0, second: 7, microsecond: {0, 0},
|
||||
@@ -1390,7 +1390,7 @@ defmodule DateTime do
|
||||
custom (but relatively common) representation which appends the time
|
||||
zone abbreviation and full name to the datetime.
|
||||
|
||||
### Examples
|
||||
## Examples
|
||||
|
||||
iex> dt = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "CET",
|
||||
...> hour: 23, minute: 0, second: 7, microsecond: {0, 0},
|
||||
@@ -1611,32 +1611,45 @@ defmodule DateTime do
|
||||
@doc """
|
||||
Adds a specified amount of time to a `DateTime`.
|
||||
|
||||
> #### Prefer `shift/2` {: .info}
|
||||
>
|
||||
> Prefer `shift/2` over `add/3`, as it offers a more ergonomic API.
|
||||
>
|
||||
> `add/3` provides a lower-level API which only supports fixed units
|
||||
> such as `:hour` and `:second`, but not `:month` (as the exact length
|
||||
> of a month depends on the current month). `add/3` always considers
|
||||
> the unit to be computed according to the `Calendar.ISO`.
|
||||
|
||||
Accepts an `amount_to_add` in any `unit`. `unit` can be `:day`,
|
||||
`:hour`, `:minute`, `:second` or any subsecond precision from
|
||||
`t:System.time_unit/0`. It defaults to `:second`. Negative values
|
||||
will move backwards in time.
|
||||
|
||||
This function always considers the unit to be computed according
|
||||
to the `Calendar.ISO`.
|
||||
`t:System.time_unit/0` for convenience but ultimately they are
|
||||
all converted to microseconds. Negative values will move backwards
|
||||
in time and the default precision is `:second`.
|
||||
|
||||
This function relies on a contiguous representation of time,
|
||||
ignoring the wall time and timezone changes. For example, if you add
|
||||
one day when there are summer time/daylight saving time changes,
|
||||
it will also change the time forward or backward by one hour,
|
||||
so the elapsed time is precisely 24 hours. Similarly, adding just
|
||||
a few seconds to a datetime just before "spring forward" can cause
|
||||
wall time to increase by more than an hour.
|
||||
ignoring timezone changes. For example, if you add one day when there
|
||||
are summer time/daylight saving time changes, it will also change the
|
||||
time forward or backward by one hour, so the elapsed time is precisely
|
||||
24 hours. Similarly, adding just a few seconds to a datetime just before
|
||||
"spring forward" can cause wall time to increase by more than an hour.
|
||||
|
||||
While this means this function is precise in terms of elapsed time,
|
||||
its result may be misleading in certain use cases. For example, if a
|
||||
its result may be confusing in certain use cases. For example, if a
|
||||
user requests a meeting to happen every day at 15:00 and you use this
|
||||
function to compute all future meetings by adding day after day, this
|
||||
function may change the meeting time to 14:00 or 16:00 if there are
|
||||
changes to the current timezone. Computing of recurring datetimes is
|
||||
not currently supported in Elixir's standard library but it is available
|
||||
by third-party libraries.
|
||||
changes to the current timezone.
|
||||
|
||||
### Examples
|
||||
In case you don't want these changes to happen automatically or you
|
||||
want to surface time zone conflicts to the user, you can add to
|
||||
the datetime as a naive datetime and then use `from_naive/2`:
|
||||
|
||||
dt |> NaiveDateTime.add(1, :day) |> DateTime.from_naive(dt.time_zone)
|
||||
|
||||
The above will surface time jumps and ambiguous datetimes, allowing you
|
||||
to deal with them accordingly.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> dt = DateTime.from_naive!(~N[2018-11-15 10:00:00], "Europe/Copenhagen", FakeTimeZoneDatabase)
|
||||
iex> dt |> DateTime.add(3600, :second, FakeTimeZoneDatabase)
|
||||
@@ -1664,8 +1677,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(
|
||||
@@ -1739,7 +1750,7 @@ defmodule DateTime do
|
||||
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"
|
||||
Consequently, time zones that observe "Daylight Saving Time"
|
||||
or other changes, across summer/winter time will add/remove hours
|
||||
from the resulting datetime:
|
||||
|
||||
@@ -1751,12 +1762,22 @@ defmodule DateTime do
|
||||
DateTime.shift(dt, hour: 2)
|
||||
#=> #DateTime<2018-11-04 01:00:00-08:00 PST America/Los_Angeles>
|
||||
|
||||
Although the first example shows a difference of 2 hours when
|
||||
comparing the wall clocks of the given datetime with the returned one,
|
||||
due to the "spring forward" time jump, the actual elapsed time is
|
||||
still exactly of 1 hour.
|
||||
|
||||
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)
|
||||
|
||||
The above will surface time jumps and ambiguous datetimes, allowing you
|
||||
to deal with them accordingly.
|
||||
|
||||
## ISO calendar considerations
|
||||
|
||||
When using the default ISO calendar, durations are collapsed and
|
||||
applied in the order of months, then seconds and microseconds:
|
||||
|
||||
@@ -1922,7 +1943,7 @@ defmodule DateTime do
|
||||
## Examples
|
||||
|
||||
Imagine someone implements `Calendar.Holocene`, a calendar based on the
|
||||
Gregorian calendar that adds exactly 10,000 years to the current Gregorian
|
||||
Gregorian calendar that adds exactly 10 000 years to the current Gregorian
|
||||
year:
|
||||
|
||||
iex> dt1 = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "AMT",
|
||||
@@ -1969,7 +1990,7 @@ defmodule DateTime do
|
||||
## Examples
|
||||
|
||||
Imagine someone implements `Calendar.Holocene`, a calendar based on the
|
||||
Gregorian calendar that adds exactly 10,000 years to the current Gregorian
|
||||
Gregorian calendar that adds exactly 10 000 years to the current Gregorian
|
||||
year:
|
||||
|
||||
iex> dt1 = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "AMT",
|
||||
|
||||
@@ -161,6 +161,22 @@ defmodule Duration do
|
||||
"""
|
||||
@type duration :: t | [unit_pair]
|
||||
|
||||
@typedoc """
|
||||
Options for `Duration.to_string/2`.
|
||||
"""
|
||||
@type to_string_opts :: [
|
||||
units: [
|
||||
year: String.t(),
|
||||
month: String.t(),
|
||||
week: String.t(),
|
||||
day: String.t(),
|
||||
hour: String.t(),
|
||||
minute: String.t(),
|
||||
second: String.t()
|
||||
],
|
||||
separator: String.t()
|
||||
]
|
||||
|
||||
@microseconds_per_second 1_000_000
|
||||
|
||||
@doc """
|
||||
@@ -436,6 +452,7 @@ defmodule Duration do
|
||||
|
||||
"""
|
||||
@doc since: "1.18.0"
|
||||
@spec to_string(t, to_string_opts) :: String.t()
|
||||
def to_string(%Duration{} = duration, opts \\ []) do
|
||||
units = Keyword.get(opts, :units, [])
|
||||
separator = Keyword.get(opts, :separator, " ")
|
||||
|
||||
+78
-151
@@ -182,7 +182,7 @@ defmodule Calendar.ISO do
|
||||
|
||||
@type day_of_year :: 1..366
|
||||
@type quarter_of_year :: 1..4
|
||||
@type year_of_era :: {1..10000, era}
|
||||
@type year_of_era :: {1..10_000, era}
|
||||
|
||||
@seconds_per_minute 60
|
||||
@seconds_per_hour 60 * 60
|
||||
@@ -196,14 +196,28 @@ defmodule Calendar.ISO do
|
||||
@ext_date_sep ?-
|
||||
@ext_time_sep ?:
|
||||
|
||||
@days_per_nonleap_year 365
|
||||
@days_per_leap_year 366
|
||||
|
||||
# The ISO epoch starts, in this implementation,
|
||||
# with ~D[0000-01-01]. Era "1" starts
|
||||
# on ~D[0001-01-01] which is 366 days later.
|
||||
@iso_epoch 366
|
||||
|
||||
# Constants for date calculations using 400-year era cycles.
|
||||
# The algorithm uses a March-based year where March 1 is day 0.
|
||||
# Reference: Neri C, Schneider L. "Euclidean Affine Functions and
|
||||
# their Application to Calendar Algorithms". Softw Pract Exper. 2022.
|
||||
@days_per_year 365
|
||||
@years_per_era 400
|
||||
@days_per_era @years_per_era * @days_per_year + 97
|
||||
@days_per_4_years 4 * @days_per_year
|
||||
@days_per_100_years 100 * @days_per_year + 24
|
||||
@march_1_offset 31 + 29
|
||||
@unix_epoch_days 719_528
|
||||
|
||||
# Month calculation constants: in a March-based year, each 5-month
|
||||
# cycle has exactly 153 days (31+30+31+30+31 or 31+30+31+30+31).
|
||||
@days_per_5_months 153
|
||||
@months_per_cycle 5
|
||||
|
||||
[match_basic_date, match_ext_date, guard_date, read_date] =
|
||||
quote do
|
||||
[
|
||||
@@ -652,7 +666,7 @@ defmodule Calendar.ISO do
|
||||
day_fraction = time_to_day_fraction(hour, minute, second, {0, 0})
|
||||
|
||||
{{year, month, day}, {hour, minute, second, _}} =
|
||||
case add_day_fraction_to_iso_days({0, day_fraction}, -offset, 86400) do
|
||||
case add_day_fraction_to_iso_days({0, day_fraction}, -offset, 86_400) do
|
||||
{0, day_fraction} ->
|
||||
{{year, month, day}, time_from_day_fraction(day_fraction)}
|
||||
|
||||
@@ -784,13 +798,13 @@ defmodule Calendar.ISO do
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Calendar.ISO.naive_datetime_from_iso_days({0, {0, 86400}})
|
||||
iex> Calendar.ISO.naive_datetime_from_iso_days({0, {0, 86_400}})
|
||||
{0, 1, 1, 0, 0, 0, {0, 6}}
|
||||
iex> Calendar.ISO.naive_datetime_from_iso_days({730_485, {0, 86400}})
|
||||
iex> Calendar.ISO.naive_datetime_from_iso_days({730_485, {0, 86_400}})
|
||||
{2000, 1, 1, 0, 0, 0, {0, 6}}
|
||||
iex> Calendar.ISO.naive_datetime_from_iso_days({730_485, {43200, 86400}})
|
||||
iex> Calendar.ISO.naive_datetime_from_iso_days({730_485, {43_200, 86_400}})
|
||||
{2000, 1, 1, 12, 0, 0, {0, 6}}
|
||||
iex> Calendar.ISO.naive_datetime_from_iso_days({-365, {0, 86400000000}})
|
||||
iex> Calendar.ISO.naive_datetime_from_iso_days({-365, {0, 86_400_000_000}})
|
||||
{-1, 1, 1, 0, 0, 0, {0, 6}}
|
||||
|
||||
"""
|
||||
@@ -878,28 +892,49 @@ defmodule Calendar.ISO do
|
||||
|
||||
# Converts year, month, day to count of days since 0000-01-01.
|
||||
@doc false
|
||||
def date_to_iso_days(0, 1, 1) do
|
||||
0
|
||||
end
|
||||
|
||||
def date_to_iso_days(1970, 1, 1) do
|
||||
719_528
|
||||
end
|
||||
def date_to_iso_days(0, 1, 1), do: 0
|
||||
def date_to_iso_days(1970, 1, 1), do: @unix_epoch_days
|
||||
|
||||
def date_to_iso_days(year, month, day) do
|
||||
ensure_day_in_month!(year, month, day)
|
||||
|
||||
days_in_previous_years(year) + days_before_month(month) + leap_day_offset(year, month) + day -
|
||||
1
|
||||
y = if month <= 2, do: year - 1, else: year
|
||||
era = if y >= 0, do: div(y, @years_per_era), else: div(y - 399, @years_per_era)
|
||||
year_of_era = y - era * @years_per_era
|
||||
month_prime = if month > 2, do: month - 3, else: month + 9
|
||||
day_of_year = div(@days_per_5_months * month_prime + 2, @months_per_cycle) + day - 1
|
||||
|
||||
day_of_era =
|
||||
@days_per_year * year_of_era + div(year_of_era, 4) - div(year_of_era, 100) + day_of_year
|
||||
|
||||
era * @days_per_era + day_of_era + @march_1_offset
|
||||
end
|
||||
|
||||
# Converts count of days since 0000-01-01 to {year, month, day} tuple.
|
||||
@doc false
|
||||
def date_from_iso_days(days) 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)
|
||||
{year, month, day_in_month + 1}
|
||||
z = days - @march_1_offset
|
||||
era = if z >= 0, do: div(z, @days_per_era), else: div(z - @days_per_era + 1, @days_per_era)
|
||||
day_of_era = z - era * @days_per_era
|
||||
|
||||
year_of_era =
|
||||
div(
|
||||
day_of_era - div(day_of_era, @days_per_4_years) + div(day_of_era, @days_per_100_years) -
|
||||
div(day_of_era, @days_per_era - 1),
|
||||
@days_per_year
|
||||
)
|
||||
|
||||
day_of_year =
|
||||
day_of_era -
|
||||
(@days_per_year * year_of_era + div(year_of_era, 4) - div(year_of_era, 100))
|
||||
|
||||
month_prime = div(@months_per_cycle * day_of_year + 2, @days_per_5_months)
|
||||
day = day_of_year - div(@days_per_5_months * month_prime + 2, @months_per_cycle) + 1
|
||||
month = if month_prime < 10, do: month_prime + 3, else: month_prime - 9
|
||||
year = year_of_era + era * @years_per_era
|
||||
year = if month <= 2, do: year + 1, else: year
|
||||
|
||||
{year, month, day}
|
||||
end
|
||||
|
||||
defp div_rem(int1, int2) do
|
||||
@@ -913,6 +948,9 @@ defmodule Calendar.ISO do
|
||||
end
|
||||
end
|
||||
|
||||
defp floor_div_positive_divisor(int1, int2) when int1 >= 0, do: div(int1, int2)
|
||||
defp floor_div_positive_divisor(int1, int2), do: -div(-int1 - 1, int2) - 1
|
||||
|
||||
@doc """
|
||||
Returns how many days there are in the given year-month.
|
||||
|
||||
@@ -1133,7 +1171,7 @@ defmodule Calendar.ISO do
|
||||
|
||||
"""
|
||||
@doc since: "1.8.0"
|
||||
@spec year_of_era(year) :: {1..10000, era}
|
||||
@spec year_of_era(year) :: {1..10_000, era}
|
||||
def year_of_era(year) when is_year_CE(year), do: {year, 1}
|
||||
def year_of_era(year) when is_year_BCE(year), do: {abs(year) + 1, 0}
|
||||
|
||||
@@ -1159,7 +1197,7 @@ defmodule Calendar.ISO do
|
||||
"""
|
||||
@doc since: "1.13.0"
|
||||
@impl true
|
||||
@spec year_of_era(year, month, day) :: {1..10000, era}
|
||||
@spec year_of_era(year, month, day) :: {1..10_000, era}
|
||||
def year_of_era(year, _month, _day), do: year_of_era(year)
|
||||
|
||||
@doc """
|
||||
@@ -1704,11 +1742,11 @@ defmodule Calendar.ISO do
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Calendar.ISO.iso_days_to_beginning_of_day({0, {0, 86400000000}})
|
||||
iex> Calendar.ISO.iso_days_to_beginning_of_day({0, {0, 86_400_000_000}})
|
||||
{0, {0, 86400000000}}
|
||||
iex> Calendar.ISO.iso_days_to_beginning_of_day({730485, {43200000000, 86400000000}})
|
||||
iex> Calendar.ISO.iso_days_to_beginning_of_day({730_485, {43_200_000_000, 86_400_000_000}})
|
||||
{730485, {0, 86400000000}}
|
||||
iex> Calendar.ISO.iso_days_to_beginning_of_day({730485, {46800000000, 86400000000}})
|
||||
iex> Calendar.ISO.iso_days_to_beginning_of_day({730_485, {46_800_000_000, 86_400_000_000}})
|
||||
{730485, {0, 86400000000}}
|
||||
|
||||
"""
|
||||
@@ -1724,11 +1762,11 @@ defmodule Calendar.ISO do
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Calendar.ISO.iso_days_to_end_of_day({0, {0, 86400000000}})
|
||||
iex> Calendar.ISO.iso_days_to_end_of_day({0, {0, 86_400_000_000}})
|
||||
{0, {86399999999, 86400000000}}
|
||||
iex> Calendar.ISO.iso_days_to_end_of_day({730485, {43200000000, 86400000000}})
|
||||
iex> Calendar.ISO.iso_days_to_end_of_day({730_485, {43_200_000_000, 86_400_000_000}})
|
||||
{730485, {86399999999, 86400000000}}
|
||||
iex> Calendar.ISO.iso_days_to_end_of_day({730485, {46800000000, 86400000000}})
|
||||
iex> Calendar.ISO.iso_days_to_end_of_day({730_485, {46_800_000_000, 86_400_000_000}})
|
||||
{730485, {86399999999, 86400000000}}
|
||||
|
||||
"""
|
||||
@@ -1848,7 +1886,7 @@ defmodule Calendar.ISO 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_year = floor_div_positive_divisor(total_months, months_in_year)
|
||||
|
||||
new_month =
|
||||
case rem(total_months, months_in_year) + 1 do
|
||||
@@ -1888,7 +1926,7 @@ defmodule Calendar.ISO do
|
||||
|
||||
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)
|
||||
ppd = System.convert_time_unit(86_400, :second, unit)
|
||||
add_day_fraction_to_iso_days(iso_days, value, ppd)
|
||||
end
|
||||
|
||||
@@ -1937,7 +1975,7 @@ defmodule Calendar.ISO do
|
||||
}) do
|
||||
[
|
||||
month: year * 12 + month,
|
||||
second: week * 7 * 86400 + day * 86400 + hour * 3600 + minute * 60 + second,
|
||||
second: week * 7 * 86_400 + day * 86_400 + hour * 3600 + minute * 60 + second,
|
||||
microsecond: microsecond
|
||||
]
|
||||
end
|
||||
@@ -1971,7 +2009,7 @@ defmodule Calendar.ISO do
|
||||
|
||||
if total in @unix_range_microseconds do
|
||||
microseconds = Integer.mod(total, @microseconds_per_second)
|
||||
seconds = @unix_epoch + Integer.floor_div(total, @microseconds_per_second)
|
||||
seconds = @unix_epoch + floor_div_positive_divisor(total, @microseconds_per_second)
|
||||
precision = precision_for_unit(unit)
|
||||
{date, time} = iso_seconds_to_datetime(seconds)
|
||||
{:ok, date, time, {microseconds, precision}}
|
||||
@@ -2093,9 +2131,12 @@ defmodule Calendar.ISO do
|
||||
end
|
||||
end
|
||||
|
||||
# Note that this function does not add the extra leap day for a leap year.
|
||||
# If you want to add that leap day when appropriate,
|
||||
# add the result of leap_day_offset/2 to the result of days_before_month/1.
|
||||
defp leap_day_offset(_year, month) when month < 3, do: 0
|
||||
|
||||
defp leap_day_offset(year, _month) do
|
||||
if leap_year?(year), do: 1, else: 0
|
||||
end
|
||||
|
||||
defp days_before_month(1), do: 0
|
||||
defp days_before_month(2), do: 31
|
||||
defp days_before_month(3), do: 59
|
||||
@@ -2109,120 +2150,6 @@ defmodule Calendar.ISO do
|
||||
defp days_before_month(11), do: 304
|
||||
defp days_before_month(12), do: 334
|
||||
|
||||
defp leap_day_offset(_year, month) when month < 3, do: 0
|
||||
|
||||
defp leap_day_offset(year, _month) do
|
||||
if leap_year?(year), do: 1, else: 0
|
||||
end
|
||||
|
||||
defp days_to_year(days) when days < 0 do
|
||||
year_estimate = -div(-days, @days_per_nonleap_year) - 1
|
||||
|
||||
{year, days_before_year} =
|
||||
days_to_year(year_estimate, days, days_to_end_of_epoch(year_estimate))
|
||||
|
||||
leap_year_pad = if leap_year?(year), do: 1, else: 0
|
||||
{year, leap_year_pad + @days_per_nonleap_year + days - days_before_year}
|
||||
end
|
||||
|
||||
defp days_to_year(days) do
|
||||
year_estimate = div(days, @days_per_nonleap_year)
|
||||
|
||||
{year, days_before_year} =
|
||||
days_to_year(year_estimate, days, days_in_previous_years(year_estimate))
|
||||
|
||||
{year, days - days_before_year}
|
||||
end
|
||||
|
||||
defp days_to_year(year, days1, days2) when year < 0 and days1 >= days2 do
|
||||
days_to_year(year + 1, days1, days_to_end_of_epoch(year + 1))
|
||||
end
|
||||
|
||||
defp days_to_year(year, days1, days2) when year >= 0 and days1 < days2 do
|
||||
days_to_year(year - 1, days1, days_in_previous_years(year - 1))
|
||||
end
|
||||
|
||||
defp days_to_year(year, _days1, days2) do
|
||||
{year, days2}
|
||||
end
|
||||
|
||||
defp days_to_end_of_epoch(year) when year < 0 do
|
||||
previous_year = year + 1
|
||||
|
||||
div(previous_year, 4) - div(previous_year, 100) + div(previous_year, 400) +
|
||||
previous_year * @days_per_nonleap_year
|
||||
end
|
||||
|
||||
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
|
||||
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 +
|
||||
@days_per_leap_year
|
||||
end
|
||||
|
||||
# Note that 0 is the first day of the month.
|
||||
defp year_day_to_year_date(_extra_day, day_of_year) when day_of_year < 31 do
|
||||
{1, day_of_year}
|
||||
end
|
||||
|
||||
defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 59 + extra_day do
|
||||
{2, day_of_year - 31}
|
||||
end
|
||||
|
||||
defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 90 + extra_day do
|
||||
{3, day_of_year - (59 + extra_day)}
|
||||
end
|
||||
|
||||
defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 120 + extra_day do
|
||||
{4, day_of_year - (90 + extra_day)}
|
||||
end
|
||||
|
||||
defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 151 + extra_day do
|
||||
{5, day_of_year - (120 + extra_day)}
|
||||
end
|
||||
|
||||
defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 181 + extra_day do
|
||||
{6, day_of_year - (151 + extra_day)}
|
||||
end
|
||||
|
||||
defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 212 + extra_day do
|
||||
{7, day_of_year - (181 + extra_day)}
|
||||
end
|
||||
|
||||
defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 243 + extra_day do
|
||||
{8, day_of_year - (212 + extra_day)}
|
||||
end
|
||||
|
||||
defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 273 + extra_day do
|
||||
{9, day_of_year - (243 + extra_day)}
|
||||
end
|
||||
|
||||
defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 304 + extra_day do
|
||||
{10, day_of_year - (273 + extra_day)}
|
||||
end
|
||||
|
||||
defp year_day_to_year_date(extra_day, day_of_year) when day_of_year < 334 + extra_day do
|
||||
{11, day_of_year - (304 + extra_day)}
|
||||
end
|
||||
|
||||
defp year_day_to_year_date(extra_day, day_of_year) do
|
||||
{12, day_of_year - (334 + extra_day)}
|
||||
end
|
||||
|
||||
defp iso_seconds_to_datetime(seconds) do
|
||||
{days, rest_seconds} = div_rem(seconds, @seconds_per_day)
|
||||
|
||||
|
||||
@@ -391,13 +391,20 @@ defmodule NaiveDateTime do
|
||||
@doc """
|
||||
Adds a specified amount of time to a `NaiveDateTime`.
|
||||
|
||||
> #### Prefer `shift/2` {: .info}
|
||||
>
|
||||
> Prefer `shift/2` over `add/3`, as it offers a more ergonomic API.
|
||||
>
|
||||
> `add/3` provides a lower-level API which only supports fixed units
|
||||
> such as `:hour` and `:second`, but not `:month` (as the exact length
|
||||
> of a month depends on the current month). `add/3` always considers
|
||||
> the unit to be computed according to the `Calendar.ISO`.
|
||||
|
||||
Accepts an `amount_to_add` in any `unit`. `unit` can be `:day`,
|
||||
`:hour`, `:minute`, `:second` or any subsecond precision from
|
||||
`t:System.time_unit/0`. It defaults to `:second`. Negative values
|
||||
will move backwards in time.
|
||||
|
||||
This function always consider the unit to be computed according
|
||||
to the `Calendar.ISO`.
|
||||
`t:System.time_unit/0` for convenience but ultimately they are
|
||||
all converted to microseconds. Negative values will move backwards
|
||||
in time and the default precision is `:second`.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -447,8 +454,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
|
||||
@@ -761,7 +766,7 @@ defmodule NaiveDateTime do
|
||||
For readability, this function follows the RFC3339 suggestion of removing
|
||||
the "T" separator between the date and time components.
|
||||
|
||||
### Examples
|
||||
## Examples
|
||||
|
||||
iex> NaiveDateTime.to_string(~N[2000-02-28 23:00:13])
|
||||
"2000-02-28 23:00:13"
|
||||
@@ -908,7 +913,7 @@ defmodule NaiveDateTime do
|
||||
Only supports converting naive datetimes which are in the ISO calendar,
|
||||
attempting to convert naive datetimes from other calendars will raise.
|
||||
|
||||
### Examples
|
||||
## Examples
|
||||
|
||||
iex> NaiveDateTime.to_iso8601(~N[2000-02-28 23:00:13])
|
||||
"2000-02-28T23:00:13"
|
||||
@@ -1261,7 +1266,7 @@ defmodule NaiveDateTime do
|
||||
## Examples
|
||||
|
||||
Imagine someone implements `Calendar.Holocene`, a calendar based on the
|
||||
Gregorian calendar that adds exactly 10,000 years to the current Gregorian
|
||||
Gregorian calendar that adds exactly 10 000 years to the current Gregorian
|
||||
year:
|
||||
|
||||
iex> NaiveDateTime.convert(~N[2000-01-01 13:30:15], Calendar.Holocene)
|
||||
@@ -1327,7 +1332,7 @@ defmodule NaiveDateTime do
|
||||
## Examples
|
||||
|
||||
Imagine someone implements `Calendar.Holocene`, a calendar based on the
|
||||
Gregorian calendar that adds exactly 10,000 years to the current Gregorian
|
||||
Gregorian calendar that adds exactly 10 000 years to the current Gregorian
|
||||
year:
|
||||
|
||||
iex> NaiveDateTime.convert!(~N[2000-01-01 13:30:15], Calendar.Holocene)
|
||||
|
||||
@@ -225,7 +225,7 @@ defmodule Time do
|
||||
@doc """
|
||||
Converts the given `time` to a string.
|
||||
|
||||
### Examples
|
||||
## Examples
|
||||
|
||||
iex> Time.to_string(~T[23:00:00])
|
||||
"23:00:00"
|
||||
@@ -334,7 +334,7 @@ defmodule Time do
|
||||
format, for human readability. It also supports the "basic" format through
|
||||
passing the `:basic` option.
|
||||
|
||||
### Examples
|
||||
## Examples
|
||||
|
||||
iex> Time.to_iso8601(~T[23:00:13])
|
||||
"23:00:13"
|
||||
@@ -505,13 +505,18 @@ defmodule Time do
|
||||
@doc """
|
||||
Adds the `amount_to_add` of `unit`s to the given `time`.
|
||||
|
||||
> #### Prefer `shift/2` {: .info}
|
||||
>
|
||||
> Prefer `shift/2` over `add/3`, as it offers a more ergonomic API.
|
||||
>
|
||||
> `add/3` always considers the unit to be computed according to
|
||||
> the `Calendar.ISO`.
|
||||
|
||||
Accepts an `amount_to_add` in any `unit`. `unit` can be
|
||||
`:hour`, `:minute`, `:second` or any subsecond precision from
|
||||
`t:System.time_unit/0`. It defaults to `:second`. Negative values
|
||||
will move backwards in time.
|
||||
|
||||
This function always consider the unit to be computed according
|
||||
to the `Calendar.ISO`.
|
||||
`t:System.time_unit/0` for convenience but ultimately they are
|
||||
all converted to microseconds. Negative values will move backwards
|
||||
in time and the default precision is `:second`.
|
||||
|
||||
Note the result value represents the time of day, meaning that it is cyclic,
|
||||
for instance, it will never go over 24 hours for the ISO calendar.
|
||||
@@ -549,8 +554,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
|
||||
@@ -781,7 +784,7 @@ defmodule Time do
|
||||
## Examples
|
||||
|
||||
Imagine someone implements `Calendar.Holocene`, a calendar based on the
|
||||
Gregorian calendar that adds exactly 10,000 years to the current Gregorian
|
||||
Gregorian calendar that adds exactly 10 000 years to the current Gregorian
|
||||
year:
|
||||
|
||||
iex> Time.convert(~T[13:30:15], Calendar.Holocene)
|
||||
@@ -837,7 +840,7 @@ defmodule Time do
|
||||
## Examples
|
||||
|
||||
Imagine someone implements `Calendar.Holocene`, a calendar based on the
|
||||
Gregorian calendar that adds exactly 10,000 years to the current Gregorian
|
||||
Gregorian calendar that adds exactly 10 000 years to the current Gregorian
|
||||
year:
|
||||
|
||||
iex> Time.convert!(~T[13:30:15], Calendar.Holocene)
|
||||
|
||||
+90
-35
@@ -248,6 +248,58 @@ defmodule Code do
|
||||
"""
|
||||
@type position() :: line() | {line :: pos_integer(), column :: pos_integer()}
|
||||
|
||||
@typedoc """
|
||||
Options for code formatting functions.
|
||||
"""
|
||||
@type format_opt ::
|
||||
{:file, binary()}
|
||||
| {:line, pos_integer()}
|
||||
| {:line_length, pos_integer()}
|
||||
| {:locals_without_parens, keyword()}
|
||||
| {:force_do_end_blocks, boolean()}
|
||||
| {:migrate, boolean()}
|
||||
| {:migrate_bitstring_modifiers, boolean()}
|
||||
| {:migrate_call_parens_on_pipe, boolean()}
|
||||
| {:migrate_charlists_as_sigils, boolean()}
|
||||
| {:migrate_unless, boolean()}
|
||||
| {atom(), term()}
|
||||
|
||||
@typedoc """
|
||||
Options for `quoted_to_algebra/2`.
|
||||
"""
|
||||
@type quoted_to_algebra_opt ::
|
||||
{:line, pos_integer() | nil}
|
||||
| {:escape, boolean()}
|
||||
| {:locals_without_parens, keyword()}
|
||||
| {:comments, [term()]}
|
||||
|
||||
@typedoc """
|
||||
Options for parsing functions that convert strings to quoted expressions.
|
||||
"""
|
||||
@type parser_opts :: [
|
||||
file: binary(),
|
||||
line: pos_integer(),
|
||||
column: pos_integer(),
|
||||
indentation: non_neg_integer(),
|
||||
columns: boolean(),
|
||||
unescape: boolean(),
|
||||
existing_atoms_only: boolean(),
|
||||
token_metadata: boolean(),
|
||||
literal_encoder: (term(), Macro.metadata() -> term()),
|
||||
static_atoms_encoder: (atom() -> term()),
|
||||
emit_warnings: boolean()
|
||||
]
|
||||
|
||||
@typedoc """
|
||||
Options for environment evaluation functions like eval_string/3 and eval_quoted/3.
|
||||
"""
|
||||
@type env_eval_opts :: [
|
||||
file: binary(),
|
||||
line: pos_integer(),
|
||||
module: module(),
|
||||
prune_binding: boolean()
|
||||
]
|
||||
|
||||
@boolean_compiler_options [
|
||||
:docs,
|
||||
:debug_info,
|
||||
@@ -552,7 +604,7 @@ defmodule Code do
|
||||
all imports, requires and aliases defined in the current environment
|
||||
will be automatically carried over:
|
||||
|
||||
iex> require Integer
|
||||
iex> require Integer, warn: false
|
||||
iex> {result, binding} = Code.eval_string("if Integer.is_odd(a), do: a + b", [a: 1, b: 2], __ENV__)
|
||||
iex> result
|
||||
3
|
||||
@@ -560,7 +612,7 @@ defmodule Code do
|
||||
[a: 1, b: 2]
|
||||
|
||||
"""
|
||||
@spec eval_string(List.Chars.t(), binding, Macro.Env.t() | keyword) :: {term, binding}
|
||||
@spec eval_string(List.Chars.t(), binding, Macro.Env.t() | env_eval_opts) :: {term, binding}
|
||||
def eval_string(string, binding \\ [], opts \\ [])
|
||||
|
||||
def eval_string(string, binding, %Macro.Env{} = env) do
|
||||
@@ -615,7 +667,8 @@ defmodule Code do
|
||||
|
||||
"""
|
||||
@doc since: "1.15.0"
|
||||
@spec with_diagnostics(keyword(), (-> result)) :: {result, [diagnostic(:warning | :error)]}
|
||||
@spec with_diagnostics([log: boolean()], (-> result)) ::
|
||||
{result, [diagnostic(:warning | :error)]}
|
||||
when result: term()
|
||||
def with_diagnostics(opts \\ [], fun) do
|
||||
value = :erlang.get(:elixir_code_diagnostics)
|
||||
@@ -648,7 +701,7 @@ defmodule Code do
|
||||
Defaults to `true`.
|
||||
"""
|
||||
@doc since: "1.15.0"
|
||||
@spec print_diagnostic(diagnostic(:warning | :error), keyword()) :: :ok
|
||||
@spec print_diagnostic(diagnostic(:warning | :error), snippet: boolean()) :: :ok
|
||||
def print_diagnostic(diagnostic, opts \\ []) do
|
||||
read_snippet? = Keyword.get(opts, :snippet, true)
|
||||
:elixir_errors.print_diagnostic(diagnostic, read_snippet?)
|
||||
@@ -672,7 +725,7 @@ defmodule Code do
|
||||
* `: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
|
||||
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
|
||||
|
||||
@@ -1035,9 +1088,9 @@ defmodule Code do
|
||||
address the deprecation warnings.
|
||||
"""
|
||||
@doc since: "1.6.0"
|
||||
@spec format_string!(binary, keyword) :: iodata
|
||||
@spec format_string!(binary, [format_opt]) :: iodata
|
||||
def format_string!(string, opts \\ []) when is_binary(string) and is_list(opts) do
|
||||
line_length = Keyword.get(opts, :line_length, 98)
|
||||
{line_length, opts} = Keyword.pop(opts, :line_length, 98)
|
||||
|
||||
to_quoted_opts =
|
||||
[
|
||||
@@ -1060,7 +1113,7 @@ defmodule Code do
|
||||
available options.
|
||||
"""
|
||||
@doc since: "1.6.0"
|
||||
@spec format_file!(binary, keyword) :: iodata
|
||||
@spec format_file!(binary, [format_opt]) :: iodata
|
||||
def format_file!(file, opts \\ []) when is_binary(file) and is_list(opts) do
|
||||
string = File.read!(file)
|
||||
formatted = format_string!(string, [file: file, line: 1] ++ opts)
|
||||
@@ -1098,7 +1151,7 @@ defmodule Code do
|
||||
[a: 1, b: 2]
|
||||
|
||||
"""
|
||||
@spec eval_quoted(Macro.t(), binding, Macro.Env.t() | keyword) :: {term, binding}
|
||||
@spec eval_quoted(Macro.t(), binding, Macro.Env.t() | env_eval_opts) :: {term, binding}
|
||||
def eval_quoted(quoted, binding \\ [], env_or_opts \\ []) do
|
||||
{value, binding, _env} =
|
||||
eval_verify(:eval_quoted, [quoted, binding, env_for_eval(env_or_opts)])
|
||||
@@ -1129,8 +1182,15 @@ defmodule Code do
|
||||
* `:line` - the line on which the script starts
|
||||
|
||||
* `:module` - the module to run the environment on
|
||||
|
||||
* `:prune_binding` - (since v1.14.2) prune binding to keep only
|
||||
variables read or written by the evaluated code. Note that
|
||||
variables used by modules are always pruned, even if later used
|
||||
by the modules. You can submit to the `:on_module` tracer event
|
||||
and access the variables used by the module from its environment.
|
||||
"""
|
||||
@doc since: "1.14.0"
|
||||
@spec env_for_eval(Macro.Env.t() | env_eval_opts) :: Macro.Env.t()
|
||||
def env_for_eval(env_or_opts), do: :elixir.env_for_eval(env_or_opts)
|
||||
|
||||
@doc """
|
||||
@@ -1144,15 +1204,11 @@ defmodule Code do
|
||||
|
||||
## Options
|
||||
|
||||
* `:prune_binding` - (since v1.14.2) prune binding to keep only
|
||||
variables read or written by the evaluated code. Note that
|
||||
variables used by modules are always pruned, even if later used
|
||||
by the modules. You can submit to the `:on_module` tracer event
|
||||
and access the variables used by the module from its environment.
|
||||
It accepts the same options as `env_for_eval/1`.
|
||||
|
||||
"""
|
||||
@doc since: "1.14.0"
|
||||
@spec eval_quoted_with_env(Macro.t(), binding, Macro.Env.t(), keyword) ::
|
||||
@spec eval_quoted_with_env(Macro.t(), binding, Macro.Env.t(), env_eval_opts) ::
|
||||
{term, binding, Macro.Env.t()}
|
||||
def eval_quoted_with_env(quoted, binding, %Macro.Env{} = env, opts \\ [])
|
||||
when is_list(binding) do
|
||||
@@ -1171,14 +1227,14 @@ defmodule Code do
|
||||
Defaults to `"nofile"`.
|
||||
|
||||
* `:line` - the starting line of the string being parsed.
|
||||
Defaults to 1.
|
||||
Defaults to `1`.
|
||||
|
||||
* `:column` - (since v1.11.0) the starting column of the string being parsed.
|
||||
Defaults to 1.
|
||||
Defaults to `1`.
|
||||
|
||||
* `:indentation` - (since v1.19.0) the indentation for the string being parsed.
|
||||
This is useful when the code parsed is embedded within another document.
|
||||
Defaults to 0.
|
||||
Defaults to `0`.
|
||||
|
||||
* `:columns` - when `true`, attach a `:column` key to the quoted
|
||||
metadata. Defaults to `false`.
|
||||
@@ -1263,7 +1319,7 @@ defmodule Code do
|
||||
{:error, {[line: 1, column: 4], "syntax error before: ", "\"3\""}}
|
||||
|
||||
"""
|
||||
@spec string_to_quoted(List.Chars.t(), keyword) ::
|
||||
@spec string_to_quoted(List.Chars.t(), parser_opts) ::
|
||||
{:ok, Macro.t()} | {:error, {location :: keyword, binary | {binary, binary}, binary}}
|
||||
def string_to_quoted(string, opts \\ []) when is_list(opts) do
|
||||
file = Keyword.get(opts, :file, "nofile")
|
||||
@@ -1290,7 +1346,7 @@ defmodule Code do
|
||||
|
||||
Check `string_to_quoted/2` for options information.
|
||||
"""
|
||||
@spec string_to_quoted!(List.Chars.t(), keyword) :: Macro.t()
|
||||
@spec string_to_quoted!(List.Chars.t(), parser_opts) :: Macro.t()
|
||||
def string_to_quoted!(string, opts \\ []) when is_list(opts) do
|
||||
file = Keyword.get(opts, :file, "nofile")
|
||||
line = Keyword.get(opts, :line, 1)
|
||||
@@ -1341,7 +1397,7 @@ defmodule Code do
|
||||
|
||||
"""
|
||||
@doc since: "1.13.0"
|
||||
@spec string_to_quoted_with_comments(List.Chars.t(), keyword) ::
|
||||
@spec string_to_quoted_with_comments(List.Chars.t(), parser_opts) ::
|
||||
{:ok, Macro.t(), list(map())} | {:error, {location :: keyword, term, term}}
|
||||
def string_to_quoted_with_comments(string, opts \\ []) when is_list(opts) do
|
||||
charlist = to_charlist(string)
|
||||
@@ -1371,7 +1427,7 @@ defmodule Code do
|
||||
Check `string_to_quoted/2` for options information.
|
||||
"""
|
||||
@doc since: "1.13.0"
|
||||
@spec string_to_quoted_with_comments!(List.Chars.t(), keyword) :: {Macro.t(), list(map())}
|
||||
@spec string_to_quoted_with_comments!(List.Chars.t(), parser_opts) :: {Macro.t(), list(map())}
|
||||
def string_to_quoted_with_comments!(string, opts \\ []) do
|
||||
charlist = to_charlist(string)
|
||||
|
||||
@@ -1456,6 +1512,9 @@ defmodule Code do
|
||||
|
||||
## Options
|
||||
|
||||
This function accepts all options supported by `format_string!/2` for controlling
|
||||
code formatting, plus these additional options:
|
||||
|
||||
* `:comments` - the list of comments associated with the quoted expression.
|
||||
Defaults to `[]`. It is recommended that both `:token_metadata` and
|
||||
`:literal_encoder` options are given to `string_to_quoted_with_comments/2`
|
||||
@@ -1466,17 +1525,13 @@ defmodule Code do
|
||||
`string_to_quoted/2`, setting this option to `false` will prevent it from
|
||||
escaping the sequences twice. Defaults to `true`.
|
||||
|
||||
* `:locals_without_parens` - a keyword list of name and arity
|
||||
pairs that should be kept without parens whenever possible.
|
||||
The arity may be the atom `:*`, which implies all arities of
|
||||
that name. The formatter already includes a list of functions
|
||||
and this option augments this list.
|
||||
|
||||
* `:syntax_colors` - a keyword list of colors the output is colorized.
|
||||
See `Inspect.Opts` for more information.
|
||||
See `format_string!/2` for the full list of formatting options including
|
||||
`:file`, `:line`, `:line_length`, `:locals_without_parens`, `:force_do_end_blocks`,
|
||||
`:syntax_colors`, and all migration options like `:migrate_charlists_as_sigils`.
|
||||
"""
|
||||
@doc since: "1.13.0"
|
||||
@spec quoted_to_algebra(Macro.t(), keyword) :: Inspect.Algebra.t()
|
||||
@spec quoted_to_algebra(Macro.t(), [format_opt() | quoted_to_algebra_opt()]) ::
|
||||
Inspect.Algebra.t()
|
||||
def quoted_to_algebra(quoted, opts \\ []) do
|
||||
quoted
|
||||
|> Code.Normalizer.normalize(opts)
|
||||
@@ -1686,10 +1741,10 @@ defmodule Code do
|
||||
module. Type checking will be executed regardless of the value of this option.
|
||||
Defaults to `true`, which is equivalent to setting it to `[:elixir]` only.
|
||||
|
||||
When setting this option, we recommend running `mix clean` so the current module
|
||||
may be compiled from scratch. `mix test` automatically disables this option via
|
||||
the `:test_elixirc_options` project configuration, as there is typically no need
|
||||
to infer signatures for test files.
|
||||
When setting this option, we recommend running `mix clean` so the modules can be
|
||||
recompiled with the new behaviour. `mix test` automatically disables this option
|
||||
via the `:test_elixirc_options` project configuration, as there is typically no
|
||||
need to 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
|
||||
|
||||
@@ -158,6 +158,7 @@ defmodule Code.Formatter do
|
||||
@doc """
|
||||
Converts the quoted expression into an algebra document.
|
||||
"""
|
||||
@spec to_algebra(Macro.t(), keyword()) :: Inspect.Algebra.t()
|
||||
def to_algebra(quoted, opts \\ []) do
|
||||
comments = Keyword.get(opts, :comments, [])
|
||||
|
||||
|
||||
@@ -11,6 +11,26 @@ defmodule Code.Fragment do
|
||||
|
||||
@type position :: {line :: pos_integer(), column :: pos_integer()}
|
||||
|
||||
@typedoc """
|
||||
Options for cursor context functions.
|
||||
|
||||
Currently, these options are not used but reserved for future extensibility.
|
||||
"""
|
||||
@type cursor_opts :: []
|
||||
|
||||
@typedoc """
|
||||
Options for converting code fragments to quoted expressions.
|
||||
"""
|
||||
@type container_cursor_to_quoted_opts :: [
|
||||
file: String.t(),
|
||||
line: pos_integer(),
|
||||
column: pos_integer(),
|
||||
columns: boolean(),
|
||||
token_metadata: boolean(),
|
||||
literal_encoder: (term(), Macro.metadata() -> term()),
|
||||
trailing_fragment: String.t()
|
||||
]
|
||||
|
||||
@doc ~S"""
|
||||
Returns the list of lines in the given string, preserving their line endings.
|
||||
|
||||
@@ -172,7 +192,7 @@ defmodule Code.Fragment do
|
||||
references, and more.
|
||||
"""
|
||||
@doc since: "1.13.0"
|
||||
@spec cursor_context(List.Chars.t(), keyword()) ::
|
||||
@spec cursor_context(List.Chars.t(), cursor_opts()) ::
|
||||
{:alias, charlist}
|
||||
| {:alias, inside_alias, charlist}
|
||||
| {:block_keyword_or_binary_operator, charlist}
|
||||
@@ -282,7 +302,8 @@ defmodule Code.Fragment do
|
||||
{{:local_or_var, acc}, count} -> {{:local_arity, acc}, count}
|
||||
{{:dot, base, acc}, count} -> {{:dot_arity, base, acc}, count}
|
||||
{{:operator, acc}, count} -> {{:operator_arity, acc}, count}
|
||||
{_, _} -> {:none, 0}
|
||||
{{:sigil, _}, _} -> {:none, 0}
|
||||
{_, _} -> {{:operator, ~c"/"}, 1}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -315,7 +336,7 @@ defmodule Code.Fragment do
|
||||
end
|
||||
|
||||
defp identifier_to_cursor_context([?., ?., ?: | _], n, _), do: {{:unquoted_atom, ~c".."}, n + 3}
|
||||
defp identifier_to_cursor_context([?., ?., ?. | _], n, _), do: {{:local_or_var, ~c"..."}, n + 3}
|
||||
defp identifier_to_cursor_context([?., ?., ?. | _], n, _), do: {{:operator, ~c"..."}, n + 3}
|
||||
defp identifier_to_cursor_context([?., ?: | _], n, _), do: {{:unquoted_atom, ~c"."}, n + 2}
|
||||
defp identifier_to_cursor_context([?., ?. | _], n, _), do: {{:operator, ~c".."}, n + 2}
|
||||
|
||||
@@ -662,7 +683,7 @@ defmodule Code.Fragment do
|
||||
of examples and their return values.
|
||||
"""
|
||||
@doc since: "1.13.0"
|
||||
@spec surround_context(List.Chars.t(), position(), keyword()) ::
|
||||
@spec surround_context(List.Chars.t(), position(), cursor_opts()) ::
|
||||
%{begin: position, end: position, context: context} | :none
|
||||
when context:
|
||||
{:alias, charlist}
|
||||
@@ -771,6 +792,12 @@ defmodule Code.Fragment do
|
||||
{{:local_or_var, acc}, offset} ->
|
||||
build_surround({:local_or_var, acc}, reversed, line, offset)
|
||||
|
||||
{{:block_keyword_or_binary_operator, acc}, offset} when acc in @textual_operators ->
|
||||
build_surround({:operator, acc}, reversed, line, offset)
|
||||
|
||||
{{:block_keyword_or_binary_operator, acc}, offset} when acc in @keywords ->
|
||||
build_surround({:keyword, acc}, reversed, line, offset)
|
||||
|
||||
{{:module_attribute, ~c""}, offset} ->
|
||||
build_surround({:operator, ~c"@"}, reversed, line, offset)
|
||||
|
||||
@@ -1187,10 +1214,10 @@ defmodule Code.Fragment do
|
||||
Defaults to `"nofile"`.
|
||||
|
||||
* `:line` - the starting line of the string being parsed.
|
||||
Defaults to 1.
|
||||
Defaults to `1`.
|
||||
|
||||
* `:column` - the starting column of the string being parsed.
|
||||
Defaults to 1.
|
||||
Defaults to `1`.
|
||||
|
||||
* `:columns` - when `true`, attach a `:column` key to the quoted
|
||||
metadata. Defaults to `false`.
|
||||
@@ -1209,7 +1236,7 @@ defmodule Code.Fragment do
|
||||
|
||||
"""
|
||||
@doc since: "1.13.0"
|
||||
@spec container_cursor_to_quoted(List.Chars.t(), keyword()) ::
|
||||
@spec container_cursor_to_quoted(List.Chars.t(), container_cursor_to_quoted_opts()) ::
|
||||
{: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)
|
||||
@@ -1305,7 +1332,7 @@ defmodule Code.Fragment do
|
||||
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 drop_tokens([], _counter), do: []
|
||||
|
||||
defp maybe_missing_stab?([{:after, _} | _], _stab_choice?), do: true
|
||||
defp maybe_missing_stab?([{:do, _} | _], _stab_choice?), do: true
|
||||
|
||||
@@ -14,6 +14,7 @@ defmodule Code.Normalizer do
|
||||
Wraps literals in the quoted expression to conform to the AST format expected
|
||||
by the formatter.
|
||||
"""
|
||||
@spec normalize(Macro.t(), keyword()) :: Macro.t()
|
||||
def normalize(quoted, opts \\ []) do
|
||||
line = Keyword.get(opts, :line, nil)
|
||||
escape = Keyword.get(opts, :escape, true)
|
||||
|
||||
@@ -98,6 +98,12 @@ defmodule Config do
|
||||
(assembled with `mix release`).
|
||||
"""
|
||||
|
||||
@type config_opts :: [
|
||||
imports: [Path.t()] | :disabled,
|
||||
env: atom(),
|
||||
target: atom()
|
||||
]
|
||||
|
||||
@opts_key {__MODULE__, :opts}
|
||||
@config_key {__MODULE__, :config}
|
||||
@imports_key {__MODULE__, :imports}
|
||||
@@ -306,7 +312,7 @@ defmodule Config do
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec __eval__!(Path.t(), binary(), keyword) :: {keyword, [Path.t()] | :disabled}
|
||||
@spec __eval__!(Path.t(), binary(), config_opts) :: {keyword, [Path.t()] | :disabled}
|
||||
def __eval__!(file, content, opts \\ []) when is_binary(file) and is_list(opts) do
|
||||
env = Keyword.get(opts, :env)
|
||||
target = Keyword.get(opts, :target)
|
||||
|
||||
@@ -111,6 +111,16 @@ defmodule Config.Provider do
|
||||
"""
|
||||
@type config_path :: {:system, binary(), binary()} | binary()
|
||||
|
||||
@typedoc """
|
||||
Options for `init/3`.
|
||||
"""
|
||||
@type init_opts :: [
|
||||
extra_config: config(),
|
||||
prune_runtime_sys_config_after_boot: boolean(),
|
||||
reboot_system_after_config: boolean(),
|
||||
validate_compile_env: [{atom(), [atom()], term()}]
|
||||
]
|
||||
|
||||
@doc """
|
||||
Invoked when initializing a config provider.
|
||||
|
||||
@@ -196,6 +206,7 @@ defmodule Config.Provider do
|
||||
@reboot_mode_key :config_provider_reboot_mode
|
||||
|
||||
@doc false
|
||||
@spec init([{module(), term()}], config_path(), init_opts()) :: config()
|
||||
def init(providers, config_path, opts \\ []) when is_list(providers) and is_list(opts) do
|
||||
validate_config_path!(config_path)
|
||||
providers = for {provider, init} <- providers, do: {provider, provider.init(init)}
|
||||
|
||||
@@ -46,6 +46,12 @@ defmodule Config.Reader do
|
||||
|
||||
@behaviour Config.Provider
|
||||
|
||||
@type config_opts :: [
|
||||
imports: [Path.t()] | :disabled,
|
||||
env: atom(),
|
||||
target: atom()
|
||||
]
|
||||
|
||||
@impl true
|
||||
def init(opts) when is_list(opts) do
|
||||
{path, opts} = Keyword.pop!(opts, :path)
|
||||
@@ -68,7 +74,7 @@ defmodule Config.Reader do
|
||||
Accepts the same options as `read!/2`.
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
@spec eval!(Path.t(), binary, keyword) :: keyword
|
||||
@spec eval!(Path.t(), binary, config_opts) :: keyword
|
||||
def eval!(file, contents, opts \\ [])
|
||||
when is_binary(file) and is_binary(contents) and is_list(opts) do
|
||||
Config.__eval__!(Path.expand(file), contents, opts) |> elem(0)
|
||||
@@ -90,7 +96,7 @@ defmodule Config.Reader do
|
||||
|
||||
"""
|
||||
@doc since: "1.9.0"
|
||||
@spec read!(Path.t(), keyword) :: keyword
|
||||
@spec read!(Path.t(), config_opts) :: keyword
|
||||
def read!(file, opts \\ []) when is_binary(file) and is_list(opts) do
|
||||
file = Path.expand(file)
|
||||
Config.__eval__!(file, File.read!(file), opts) |> elem(0)
|
||||
@@ -104,7 +110,7 @@ defmodule Config.Reader do
|
||||
option cannot be disabled in `read_imports!/2`.
|
||||
"""
|
||||
@doc since: "1.9.0"
|
||||
@spec read_imports!(Path.t(), keyword) :: {keyword, [Path.t()]}
|
||||
@spec read_imports!(Path.t(), config_opts) :: {keyword, [Path.t()]}
|
||||
def read_imports!(file, opts \\ []) when is_binary(file) and is_list(opts) do
|
||||
if opts[:imports] == :disabled do
|
||||
raise ArgumentError, ":imports must be a list of paths"
|
||||
|
||||
@@ -16,7 +16,7 @@ defmodule DynamicSupervisor do
|
||||
|
||||
## Examples
|
||||
|
||||
A dynamic supervisor is started with no children and often a name:
|
||||
A dynamic supervisor is started with no children and often with a name:
|
||||
|
||||
children = [
|
||||
{DynamicSupervisor, name: MyApp.DynamicSupervisor, strategy: :one_for_one}
|
||||
@@ -137,67 +137,6 @@ defmodule DynamicSupervisor do
|
||||
|
||||
A supervisor is bound to the same name registration rules as a `GenServer`.
|
||||
Read more about these rules in the documentation for `GenServer`.
|
||||
|
||||
## Migrating from Supervisor's :simple_one_for_one
|
||||
|
||||
In case you were using the deprecated `:simple_one_for_one` strategy from
|
||||
the `Supervisor` module, you can migrate to the `DynamicSupervisor` in
|
||||
few steps.
|
||||
|
||||
Imagine the given "old" code:
|
||||
|
||||
defmodule MySupervisor do
|
||||
use Supervisor
|
||||
|
||||
def start_link(init_arg) do
|
||||
Supervisor.start_link(__MODULE__, init_arg, name: __MODULE__)
|
||||
end
|
||||
|
||||
def start_child(foo, bar, baz) do
|
||||
# This will start child by calling MyWorker.start_link(init_arg, foo, bar, baz)
|
||||
Supervisor.start_child(__MODULE__, [foo, bar, baz])
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init(init_arg) do
|
||||
children = [
|
||||
# Or the deprecated: worker(MyWorker, [init_arg])
|
||||
%{id: MyWorker, start: {MyWorker, :start_link, [init_arg]}}
|
||||
]
|
||||
|
||||
Supervisor.init(children, strategy: :simple_one_for_one)
|
||||
end
|
||||
end
|
||||
|
||||
It can be upgraded to the DynamicSupervisor like this:
|
||||
|
||||
defmodule MySupervisor do
|
||||
use DynamicSupervisor
|
||||
|
||||
def start_link(init_arg) do
|
||||
DynamicSupervisor.start_link(__MODULE__, init_arg, name: __MODULE__)
|
||||
end
|
||||
|
||||
def start_child(foo, bar, baz) do
|
||||
# If MyWorker is not using the new child specs, we need to pass a map:
|
||||
# spec = %{id: MyWorker, start: {MyWorker, :start_link, [foo, bar, baz]}}
|
||||
spec = {MyWorker, foo: foo, bar: bar, baz: baz}
|
||||
DynamicSupervisor.start_child(__MODULE__, spec)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init(init_arg) do
|
||||
DynamicSupervisor.init(
|
||||
strategy: :one_for_one,
|
||||
extra_arguments: [init_arg]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
The difference is that the `DynamicSupervisor` expects the child specification
|
||||
at the moment `start_child/2` is called, and no longer on the init callback.
|
||||
If there are any initial arguments given on initialization, such as `[initial_arg]`,
|
||||
it can be given in the `:extra_arguments` flag on `DynamicSupervisor.init/1`.
|
||||
"""
|
||||
|
||||
@behaviour GenServer
|
||||
@@ -233,9 +172,10 @@ defmodule DynamicSupervisor do
|
||||
@typedoc """
|
||||
Return values of `start_child` functions.
|
||||
|
||||
Unlike `Supervisor`, this module ignores the child spec ids, so
|
||||
`{:error, {:already_started, pid}}` is not returned for child specs given with the same id.
|
||||
`{:error, {:already_started, pid}}` is returned however if a duplicate name is used when using
|
||||
Unlike `Supervisor`, this module ignores the child spec ids,
|
||||
so `{:error, {:already_started, pid}}` is not returned for child specs
|
||||
given with the same id. `{:error, {:already_started, pid}}` is returned
|
||||
however if a duplicate name is used when using
|
||||
[name registration](`m:GenServer#module-name-registration`).
|
||||
"""
|
||||
@type on_start_child ::
|
||||
@@ -266,6 +206,7 @@ defmodule DynamicSupervisor do
|
||||
See `Supervisor` for more information about child specifications.
|
||||
"""
|
||||
@doc since: "1.6.1"
|
||||
@spec child_spec([init_option() | GenServer.option()]) :: Supervisor.child_spec()
|
||||
def child_spec(options) when is_list(options) do
|
||||
id =
|
||||
case Keyword.get(options, :name, DynamicSupervisor) do
|
||||
@@ -415,6 +356,10 @@ defmodule DynamicSupervisor do
|
||||
`{:error, {:already_started, pid}}` is returned however if a duplicate name is
|
||||
used when using [name registration](`m:GenServer#module-name-registration`).
|
||||
|
||||
This function will block the `DynamicSupervisor` until the child initializes.
|
||||
When starting too many processes dynamically, you may want to use a
|
||||
`PartitionSupervisor` to split the work across multiple processes.
|
||||
|
||||
If the child process start function returns `{:ok, child}` or `{:ok, child,
|
||||
info}`, then child specification and PID are added to the supervisor and
|
||||
this function returns the same value.
|
||||
@@ -518,6 +463,14 @@ defmodule DynamicSupervisor do
|
||||
@doc """
|
||||
Terminates the given child identified by `pid`.
|
||||
|
||||
This function will block the `DynamicSupervisor` until the child
|
||||
terminates, which may take an arbitrary amount of time if the child
|
||||
is trapping exits and implements its own terminate callback.
|
||||
For this reason, it is often better to ask the child process
|
||||
itself to terminate, often by declaring in its child spec it has
|
||||
a restart strategy of `:transient` (or `:temporary`) and then
|
||||
sending it a message to stop with reason `:shutdown`.
|
||||
|
||||
If successful, this function returns `:ok`. If there is no process with
|
||||
the given PID, this function returns `{:error, :not_found}`.
|
||||
"""
|
||||
@@ -528,11 +481,11 @@ defmodule DynamicSupervisor do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a list with information about all children.
|
||||
Returns a list with information about all children of the given supervisor.
|
||||
|
||||
Note that calling this function when supervising a large number
|
||||
of children under low memory conditions can cause an out of memory
|
||||
exception.
|
||||
of children under low memory conditions can bring the system down due to an
|
||||
out of memory error.
|
||||
|
||||
This function returns a list of tuples containing:
|
||||
|
||||
|
||||
+144
-35
@@ -39,6 +39,20 @@ defprotocol Enumerable do
|
||||
`reduce/3` function. All other functions exist as optimizations paths
|
||||
for data structures that can implement certain properties in better
|
||||
than linear time.
|
||||
|
||||
## Default implementation for lists
|
||||
|
||||
Sometimes you may want to implement this protocol for a list contained
|
||||
in struct. This can be done by delegating to the `Enumerable.List` module
|
||||
in the `reduce/3` implementation and providing a straight-forward
|
||||
implementation for the remaining ones:
|
||||
|
||||
defimpl Enumerable, for: CustomStruct do
|
||||
def count(struct), do: {:ok, length(struct.items)}
|
||||
def member?(struct, value), do: {:ok, value in struct.items}
|
||||
def slice(struct), do: {:error, __MODULE__}
|
||||
def reduce(struct, acc, fun), do: Enumerable.List.reduce(struct.items, acc, fun)
|
||||
end
|
||||
"""
|
||||
|
||||
@typedoc """
|
||||
@@ -766,6 +780,10 @@ defmodule Enum do
|
||||
end
|
||||
end
|
||||
|
||||
def count_until(_enumerable, limit) when is_integer(limit) do
|
||||
raise ArgumentError, "expected limit to be greater than 0, got: #{limit}"
|
||||
end
|
||||
|
||||
@doc """
|
||||
Counts the elements in the enumerable for which `fun` returns a truthy value, stopping at `limit`.
|
||||
|
||||
@@ -787,6 +805,10 @@ defmodule Enum do
|
||||
end
|
||||
end
|
||||
|
||||
def count_until(_enumerable, _fun, limit) when is_integer(limit) do
|
||||
raise ArgumentError, "expected limit to be greater than 0, got: #{limit}"
|
||||
end
|
||||
|
||||
@doc """
|
||||
Enumerates the `enumerable`, returning a list where all consecutive
|
||||
duplicate elements are collapsed to a single element.
|
||||
@@ -951,8 +973,8 @@ defmodule Enum do
|
||||
## Examples
|
||||
|
||||
Enum.each(["some", "example"], fn x -> IO.puts(x) end)
|
||||
"some"
|
||||
"example"
|
||||
some
|
||||
example
|
||||
#=> :ok
|
||||
|
||||
"""
|
||||
@@ -1214,7 +1236,7 @@ defmodule Enum do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Maps the given `fun` over `enumerable` and flattens the result.
|
||||
Maps the given `fun` over `enumerable` and flattens the result only one level deep.
|
||||
|
||||
This function returns a new enumerable built by appending the result of invoking `fun`
|
||||
on each element of `enumerable` together; conceptually, this is similar to a
|
||||
@@ -1262,13 +1284,16 @@ defmodule Enum do
|
||||
defp flat_reverse([], acc), do: acc
|
||||
|
||||
@doc """
|
||||
Maps and reduces an `enumerable`, flattening the given results (only one level deep).
|
||||
Maps and reduces an `enumerable`, flattening the results only one level deep.
|
||||
|
||||
It expects an accumulator and a function that receives each enumerable
|
||||
element, and must return a tuple containing a new enumerable (often a list)
|
||||
with the new accumulator or a tuple with `:halt` as first element and
|
||||
the accumulator as second.
|
||||
|
||||
Returns a 2-element tuple where the first element is the results flattened one level deep and
|
||||
the second element is the last accumulator.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> enumerable = 1..100
|
||||
@@ -1493,6 +1518,14 @@ defmodule Enum do
|
||||
to_list(enumerable)
|
||||
end
|
||||
|
||||
def into(enumerable, collectable) when is_struct(collectable, MapSet) do
|
||||
if MapSet.size(collectable) == 0 do
|
||||
MapSet.new(enumerable)
|
||||
else
|
||||
MapSet.new(enumerable) |> MapSet.union(collectable)
|
||||
end
|
||||
end
|
||||
|
||||
def into(%_{} = enumerable, collectable) do
|
||||
into_protocol(enumerable, collectable)
|
||||
end
|
||||
@@ -1569,8 +1602,12 @@ defmodule Enum do
|
||||
map(enumerable, transform)
|
||||
end
|
||||
|
||||
def into(%_{} = enumerable, collectable, transform) do
|
||||
into_protocol(enumerable, collectable, transform)
|
||||
def into(enumerable, collectable, transform) when is_struct(collectable, MapSet) do
|
||||
if MapSet.size(collectable) == 0 do
|
||||
MapSet.new(enumerable, transform)
|
||||
else
|
||||
MapSet.new(enumerable, transform) |> MapSet.union(collectable)
|
||||
end
|
||||
end
|
||||
|
||||
def into(enumerable, %_{} = collectable, transform) do
|
||||
@@ -1842,7 +1879,7 @@ defmodule Enum do
|
||||
Returns the maximal element in the `enumerable` according
|
||||
to Erlang's term ordering.
|
||||
|
||||
By default, the comparison is done with the `>=` sorter function.
|
||||
By default, the comparison is done with the [`>=`](`>=/2`) sorter function.
|
||||
If multiple elements are considered maximal, the first one that
|
||||
was found is returned. If you want the last element considered
|
||||
maximal to be returned, the sorter function should not return true
|
||||
@@ -1909,7 +1946,7 @@ defmodule Enum do
|
||||
Returns the maximal element in the `enumerable` as calculated
|
||||
by the given `fun`.
|
||||
|
||||
By default, the comparison is done with the `>=` sorter function.
|
||||
By default, the comparison is done with the [`>=`](`>=/2`) sorter function.
|
||||
If multiple elements are considered maximal, the first one that
|
||||
was found is returned. If you want the last element considered
|
||||
maximal to be returned, the sorter function should not return true
|
||||
@@ -2022,7 +2059,7 @@ defmodule Enum do
|
||||
Returns the minimal element in the `enumerable` according
|
||||
to Erlang's term ordering.
|
||||
|
||||
By default, the comparison is done with the `<=` sorter function.
|
||||
By default, the comparison is done with the [`<=`](`<=/2`) sorter function.
|
||||
If multiple elements are considered minimal, the first one that
|
||||
was found is returned. If you want the last element considered
|
||||
minimal to be returned, the sorter function should not return true
|
||||
@@ -2089,7 +2126,7 @@ defmodule Enum do
|
||||
Returns the minimal element in the `enumerable` as calculated
|
||||
by the given `fun`.
|
||||
|
||||
By default, the comparison is done with the `<=` sorter function.
|
||||
By default, the comparison is done with the [`<=`](`<=/2`) sorter function.
|
||||
If multiple elements are considered minimal, the first one that
|
||||
was found is returned. If you want the last element considered
|
||||
minimal to be returned, the sorter function should not return true
|
||||
@@ -2143,28 +2180,60 @@ defmodule Enum do
|
||||
|
||||
@doc """
|
||||
Returns a tuple with the minimal and the maximal elements in the
|
||||
enumerable according to Erlang's term ordering.
|
||||
enumerable.
|
||||
|
||||
If multiple elements are considered maximal or minimal, the first one
|
||||
that was found is returned.
|
||||
|
||||
Calls the provided `empty_fallback` function and returns its value if
|
||||
`enumerable` is empty. The default `empty_fallback` raises `Enum.EmptyError`.
|
||||
By default, the comparison is done with the [`<`](`</2`) sorter function,
|
||||
as the function must not return true for equal elements.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Enum.min_max([2, 3, 1])
|
||||
{1, 3}
|
||||
|
||||
iex> Enum.min_max(["foo", "bar", "baz"])
|
||||
{"bar", "foo"}
|
||||
|
||||
iex> Enum.min_max([], fn -> {nil, nil} end)
|
||||
{nil, nil}
|
||||
|
||||
"""
|
||||
@spec min_max(t, (-> empty_result)) :: {element, element} | empty_result
|
||||
when empty_result: any
|
||||
def min_max(enumerable, empty_fallback \\ fn -> raise Enum.EmptyError end)
|
||||
The fact this function uses Erlang's term ordering means that the
|
||||
comparison is structural and not semantic. Therefore, if you want
|
||||
to compare structs, most structs provide a "compare" function, such as
|
||||
`Date.compare/2`, which receives two structs and returns `:lt` (less-than),
|
||||
`:eq` (equal to), and `:gt` (greater-than). If you pass a module as the
|
||||
sorting function, Elixir will automatically use the `compare/2` function
|
||||
of said module:
|
||||
|
||||
def min_max(first..last//step = range, empty_fallback) when is_function(empty_fallback, 0) do
|
||||
iex> dates = [
|
||||
...> ~D[2019-01-01],
|
||||
...> ~D[2020-01-01],
|
||||
...> ~D[2018-01-01]
|
||||
...> ]
|
||||
iex> Enum.min_max(dates, Date)
|
||||
{~D[2018-01-01], ~D[2020-01-01]}
|
||||
|
||||
You can also pass a custom sorting function:
|
||||
|
||||
iex> Enum.min_max([2, 3, 1], &>/2)
|
||||
{3, 1}
|
||||
|
||||
Finally, if you don't want to raise on empty enumerables, you can pass
|
||||
the empty fallback:
|
||||
|
||||
iex> Enum.min_max([], fn -> nil end)
|
||||
nil
|
||||
|
||||
"""
|
||||
@spec min_max(t, (element, element -> boolean) | module()) :: {element, element}
|
||||
@spec min_max(t, (-> empty_result)) :: {element, element} | empty_result when empty_result: any
|
||||
@spec min_max(t, (element, element -> boolean) | module(), (-> empty_result)) ::
|
||||
{element, element} | empty_result
|
||||
when empty_result: any
|
||||
|
||||
def min_max(enumerable, sorter_or_empty_fallback \\ fn -> raise Enum.EmptyError end)
|
||||
|
||||
def min_max(first..last//step = range, empty_fallback)
|
||||
when is_function(empty_fallback, 0) do
|
||||
case Range.size(range) do
|
||||
0 ->
|
||||
empty_fallback.()
|
||||
@@ -2175,11 +2244,39 @@ defmodule Enum do
|
||||
end
|
||||
end
|
||||
|
||||
def min_max(enumerable, empty_fallback) when is_function(empty_fallback, 0) do
|
||||
def min_max(enumerable, empty_fallback)
|
||||
when is_function(empty_fallback, 0) do
|
||||
min_max(enumerable, &</2, empty_fallback)
|
||||
end
|
||||
|
||||
def min_max(enumerable, sorter) when is_atom(sorter) do
|
||||
min_max(enumerable, min_max_sort_fun(sorter))
|
||||
end
|
||||
|
||||
def min_max(enumerable, sorter) when is_function(sorter, 2) do
|
||||
min_max(enumerable, sorter, fn -> raise Enum.EmptyError end)
|
||||
end
|
||||
|
||||
def min_max(enumerable, sorter, empty_fallback)
|
||||
when is_atom(sorter) and is_function(empty_fallback, 0) do
|
||||
min_max(enumerable, min_max_sort_fun(sorter), empty_fallback)
|
||||
end
|
||||
|
||||
def min_max(enumerable, sorter, empty_fallback)
|
||||
when is_function(sorter, 2) and is_function(empty_fallback, 0) do
|
||||
first_fun = &[&1 | &1]
|
||||
|
||||
reduce_fun = fn entry, [min | max] ->
|
||||
[Kernel.min(min, entry) | Kernel.max(max, entry)]
|
||||
reduce_fun = fn entry, [min | max] = acc ->
|
||||
cond do
|
||||
sorter.(entry, min) ->
|
||||
[entry | max]
|
||||
|
||||
sorter.(max, entry) ->
|
||||
[min | entry]
|
||||
|
||||
true ->
|
||||
acc
|
||||
end
|
||||
end
|
||||
|
||||
case reduce_by(enumerable, first_fun, reduce_fun) do
|
||||
@@ -2200,8 +2297,8 @@ defmodule Enum do
|
||||
Returns a tuple with the minimal and the maximal elements in the
|
||||
enumerable as calculated by the given function.
|
||||
|
||||
If multiple elements are considered maximal or minimal, the first one
|
||||
that was found is returned.
|
||||
By default, the comparison is done with the [`<`](`</2`) sorter function,
|
||||
as the function must not return `true` for equal elements.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -2259,7 +2356,7 @@ defmodule Enum do
|
||||
|
||||
def min_max_by(enumerable, fun, sorter, empty_fallback)
|
||||
when is_function(fun, 1) and is_atom(sorter) and is_function(empty_fallback, 0) do
|
||||
min_max_by(enumerable, fun, min_max_by_sort_fun(sorter), empty_fallback)
|
||||
min_max_by(enumerable, fun, min_max_sort_fun(sorter), empty_fallback)
|
||||
end
|
||||
|
||||
def min_max_by(enumerable, fun, sorter, empty_fallback)
|
||||
@@ -2290,7 +2387,7 @@ defmodule Enum do
|
||||
end
|
||||
end
|
||||
|
||||
defp min_max_by_sort_fun(module) when is_atom(module), do: &(module.compare(&1, &2) == :lt)
|
||||
defp min_max_sort_fun(module) when is_atom(module), do: &(module.compare(&1, &2) == :lt)
|
||||
|
||||
@doc """
|
||||
Splits the `enumerable` in two lists according to the given function `fun`.
|
||||
@@ -3611,9 +3708,14 @@ defmodule Enum do
|
||||
end
|
||||
|
||||
def take(enumerable, amount) when is_integer(amount) and amount < 0 do
|
||||
{count, fun} = slice_count_and_fun(enumerable, 1)
|
||||
first = Kernel.max(amount + count, 0)
|
||||
fun.(first, count - first, 1)
|
||||
case slice_count_and_fun(enumerable, 1) do
|
||||
{0, _fun} ->
|
||||
[]
|
||||
|
||||
{count, fun} ->
|
||||
first = Kernel.max(amount + count, 0)
|
||||
fun.(first, count - first, 1)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -4089,6 +4191,11 @@ defmodule Enum do
|
||||
iex> Enum.zip_with([[1, 2], [3, 4]], fn [x, y] -> x + y end)
|
||||
[4, 6]
|
||||
|
||||
`zip_with/2` can be used to transpose lists of lists:
|
||||
|
||||
iex> Enum.zip_with([[1, 2,], [3, 4]], & &1)
|
||||
[[1, 3], [2, 4]]
|
||||
|
||||
"""
|
||||
@doc since: "1.12.0"
|
||||
@spec zip_with(t, ([term] -> term)) :: [term]
|
||||
@@ -5006,8 +5113,7 @@ end
|
||||
defimpl Enumerable, for: List do
|
||||
def count(list), do: {:ok, length(list)}
|
||||
|
||||
def member?([], _value), do: {:ok, false}
|
||||
def member?(_list, _value), do: {:error, __MODULE__}
|
||||
def member?(list, value), do: {:ok, :lists.member(value, list)}
|
||||
|
||||
def slice([]), do: {:ok, 0, fn _, _, _ -> [] end}
|
||||
def slice(_list), do: {:error, __MODULE__}
|
||||
@@ -5118,6 +5224,9 @@ defimpl Enumerable, for: Range do
|
||||
slice(Map.put(range, :step, step))
|
||||
end
|
||||
|
||||
defp slice(_current, _step, 0), do: []
|
||||
defp slice(current, step, remaining), do: [current | slice(current + step, step, remaining - 1)]
|
||||
defp slice(current, _step, 1), do: [current]
|
||||
|
||||
defp slice(current, step, remaining) when remaining > 1 do
|
||||
[current | slice(current + step, step, remaining - 1)]
|
||||
end
|
||||
end
|
||||
|
||||
+18
-36
@@ -188,13 +188,12 @@ defmodule Exception do
|
||||
term
|
||||
|> inspect(pretty: true)
|
||||
|> String.split("\n")
|
||||
|> Enum.map(fn
|
||||
|> Enum.map_intersperse("\n", fn
|
||||
"" -> ""
|
||||
line -> " " <> line
|
||||
end)
|
||||
|> Enum.join("\n")
|
||||
|
||||
message <> "\n\n" <> inspected
|
||||
IO.iodata_to_binary([message, "\n\n", inspected, "\n"])
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -288,10 +287,10 @@ defmodule Exception do
|
||||
end
|
||||
end
|
||||
|
||||
defp is_map_node?({:is_map, _, [_]}), do: true
|
||||
defp is_map_node?(_), do: false
|
||||
defp is_map_key_node?({:is_map_key, _, [_, _]}), do: true
|
||||
defp is_map_key_node?(_), do: false
|
||||
defp map_node?({:is_map, _, [_]}), do: true
|
||||
defp map_node?(_), do: false
|
||||
defp map_key_node?({:is_map_key, _, [_, _]}), do: true
|
||||
defp map_key_node?(_), do: false
|
||||
|
||||
defp struct_validation_node?(
|
||||
{:is_atom, _, [{{:., [], [:erlang, :map_get]}, _, [:__struct__, _]}]}
|
||||
@@ -305,16 +304,16 @@ defmodule Exception do
|
||||
|
||||
defp struct_validation_node?(_), do: false
|
||||
|
||||
defp is_struct_macro?(
|
||||
defp struct_macro?(
|
||||
{:and, _,
|
||||
[
|
||||
{:and, _, [%{node: node_1 = {_, _, [arg]}}, %{node: node_2 = {_, _, [arg, _]}}]},
|
||||
%{node: node_3 = {_, _, [{_, _, [_, arg]}]}}
|
||||
]}
|
||||
),
|
||||
do: is_map_node?(node_1) and is_map_key_node?(node_2) and struct_validation_node?(node_3)
|
||||
do: map_node?(node_1) and map_key_node?(node_2) and struct_validation_node?(node_3)
|
||||
|
||||
defp is_struct_macro?(
|
||||
defp struct_macro?(
|
||||
{:and, _,
|
||||
[
|
||||
{:and, _,
|
||||
@@ -329,12 +328,12 @@ defmodule Exception do
|
||||
%{node: node_3 = {_, _, [{_, _, [_, arg]}, _]}}
|
||||
]}
|
||||
),
|
||||
do: is_map_node?(node_1) and is_map_key_node?(node_2) and struct_validation_node?(node_3)
|
||||
do: map_node?(node_1) and map_key_node?(node_2) and struct_validation_node?(node_3)
|
||||
|
||||
defp is_struct_macro?(_), do: false
|
||||
defp struct_macro?(_), do: false
|
||||
|
||||
defp translate_guard(guard) do
|
||||
if is_struct_macro?(guard) do
|
||||
if struct_macro?(guard) do
|
||||
undo_is_struct_guard(guard)
|
||||
else
|
||||
guard
|
||||
@@ -1389,6 +1388,7 @@ defmodule CompileError do
|
||||
* `:file` (`t:Path.t/0` or `nil`) - the file where the error occurred, or `nil` if
|
||||
the error occurred in code that did not come from a file
|
||||
* `:line` (`t:non_neg_integer/0`) - the line where the error occurred
|
||||
* `:description` (`t:String.t/0`) - a description of the compile error
|
||||
|
||||
This is mostly raised by Elixir tooling when compiling and evaluating code.
|
||||
"""
|
||||
@@ -1457,20 +1457,6 @@ defmodule BadFunctionError do
|
||||
end
|
||||
end
|
||||
|
||||
defmodule BadStructError do
|
||||
@moduledoc deprecated:
|
||||
"This exception is deprecated alongside the struct update syntax that raises it"
|
||||
defexception [:struct, :term]
|
||||
|
||||
@impl true
|
||||
def message(exception) do
|
||||
Exception.__format_message_with_term__(
|
||||
"expected a struct named #{inspect(exception.struct)}, got:",
|
||||
exception.term
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
defmodule BadMapError do
|
||||
@moduledoc """
|
||||
An exception raised when a map is expected, but something else was given.
|
||||
@@ -1912,7 +1898,7 @@ defmodule UndefinedFunctionError do
|
||||
end
|
||||
|
||||
defp format_fa({_dist, fun, arity}) do
|
||||
[" * ", Macro.inspect_atom(:remote_call, fun), ?/, Integer.to_string(arity), ?\n]
|
||||
[" * ", Macro.inspect_atom(:remote_call, fun), ?/, Integer.to_string(arity), ?\n]
|
||||
end
|
||||
|
||||
defp exports_for(module) do
|
||||
@@ -1944,8 +1930,8 @@ defmodule FunctionClauseError do
|
||||
|
||||
For example:
|
||||
|
||||
iex> URI.parse(:wrong_argument)
|
||||
** (FunctionClauseError) no function clause matching in URI.parse/1
|
||||
iex> List.duplicate(:ok, -3)
|
||||
** (FunctionClauseError) no function clause matching in List.duplicate/2
|
||||
|
||||
The following fields of this exception are public and can be accessed freely:
|
||||
|
||||
@@ -2244,7 +2230,7 @@ defmodule KeyError do
|
||||
|
||||
case suggestions do
|
||||
[] -> []
|
||||
suggestions -> ["\n\nDid you mean:\n\n" | format_suggestions(suggestions)]
|
||||
suggestions -> ["\nDid you mean:\n\n" | format_suggestions(suggestions)]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2253,7 +2239,7 @@ defmodule KeyError do
|
||||
|> Enum.sort(&(elem(&1, 0) >= elem(&2, 0)))
|
||||
|> Enum.take(@max_suggestions)
|
||||
|> Enum.sort(&(elem(&1, 1) <= elem(&2, 1)))
|
||||
|> Enum.map(fn {_, key} -> [" * ", inspect(key), ?\n] end)
|
||||
|> Enum.map(fn {_, key} -> [" * ", inspect(key), ?\n] end)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2605,10 +2591,6 @@ defmodule ErlangError do
|
||||
%BadFunctionError{term: term}
|
||||
end
|
||||
|
||||
def normalize({:badstruct, struct, term}, _stacktrace) do
|
||||
%BadStructError{struct: struct, term: term}
|
||||
end
|
||||
|
||||
def normalize({:badmatch, term}, _stacktrace) do
|
||||
%MatchError{term: term}
|
||||
end
|
||||
|
||||
+21
-8
@@ -317,7 +317,7 @@ defmodule File do
|
||||
directories of `path`
|
||||
* `:enospc` - there is no space left on the device
|
||||
* `:enotdir` - a component of `path` is not a directory
|
||||
* `:eperm` - missed required permisions
|
||||
* `:eperm` - missed required permissions
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -694,7 +694,7 @@ defmodule File do
|
||||
File.touch("/fakedir/b.txt", {{2018, 1, 30}, {13, 59, 59}})
|
||||
{:error, :enoent}
|
||||
|
||||
File.touch("/tmp/a.txt", 1544519753)
|
||||
File.touch("/tmp/a.txt", 1_544_519_753)
|
||||
#=> :ok
|
||||
|
||||
"""
|
||||
@@ -706,7 +706,7 @@ defmodule File do
|
||||
path = IO.chardata_to_string(path)
|
||||
|
||||
with {:error, :enoent} <- :elixir_utils.change_universal_time(path, time),
|
||||
:ok <- write(path, "", [:append]),
|
||||
:ok <- write(path, "", [:raw, :append]),
|
||||
do: :elixir_utils.change_universal_time(path, time)
|
||||
end
|
||||
|
||||
@@ -714,7 +714,7 @@ defmodule File do
|
||||
path = IO.chardata_to_string(path)
|
||||
|
||||
with {:error, :enoent} <- :elixir_utils.change_posix_time(path, time),
|
||||
:ok <- write(path, "", [:append]),
|
||||
:ok <- write(path, "", [:raw, :append]),
|
||||
do: :elixir_utils.change_posix_time(path, time)
|
||||
end
|
||||
|
||||
@@ -733,7 +733,7 @@ defmodule File do
|
||||
File.touch!("/fakedir/b.txt", {{2018, 1, 30}, {13, 59, 59}})
|
||||
** (File.Error) could not touch "/fakedir/b.txt": no such file or directory
|
||||
|
||||
File.touch!("/tmp/a.txt", 1544519753)
|
||||
File.touch!("/tmp/a.txt", 1_544_519_753)
|
||||
|
||||
"""
|
||||
@spec touch!(Path.t(), erlang_time() | posix_time()) :: :ok
|
||||
@@ -1114,6 +1114,8 @@ defmodule File do
|
||||
explicitly disallow this behavior. If `source` is a `file` and `destination`
|
||||
is a directory, `{:error, :eisdir}` will be returned.
|
||||
|
||||
Special files such as device files, sockets, and named pipes are not copied.
|
||||
|
||||
## Options
|
||||
|
||||
* `:on_conflict` - (since v1.14.0) Invoked when a file already exists in the destination.
|
||||
@@ -1224,7 +1226,13 @@ defmodule File do
|
||||
defp do_cp_r(src, dest, on_conflict, dereference?, acc) when is_list(acc) do
|
||||
case :elixir_utils.read_link_type(src) do
|
||||
{:ok, :regular} ->
|
||||
do_cp_file(src, dest, on_conflict, acc)
|
||||
case do_cp_file(src, dest, on_conflict, acc) do
|
||||
# we don't have a way to make a distinction between a non-existing src
|
||||
# or dest being a non-existing dir in the case of :enoent,
|
||||
# but we already know that src exists here.
|
||||
{:error, :enoent, _} -> {:error, :enoent, dest}
|
||||
other -> other
|
||||
end
|
||||
|
||||
{:ok, :symlink} ->
|
||||
case :file.read_link(src) do
|
||||
@@ -1256,7 +1264,7 @@ defmodule File do
|
||||
end
|
||||
|
||||
{:ok, _} ->
|
||||
{:error, :eio, src}
|
||||
acc
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason, src}
|
||||
@@ -2159,7 +2167,12 @@ defmodule File do
|
||||
def stream!(path, line_or_bytes, modes)
|
||||
|
||||
def stream!(path, modes, line_or_bytes) when is_list(modes) do
|
||||
# TODO: Deprecate this on Elixir v1.20
|
||||
# TODO: Remove me on Elixir 2.0
|
||||
IO.warn(
|
||||
"File.stream!(path, modes, line_or_byte) is deprecated, " <>
|
||||
"invoke File.stream!(path, line_or_bytes, modes) instead"
|
||||
)
|
||||
|
||||
stream!(path, line_or_bytes, modes)
|
||||
end
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ defmodule Float do
|
||||
|
||||
To learn more about floating-point arithmetic visit:
|
||||
|
||||
* [0.30000000000000004.com](http://0.30000000000000004.com/)
|
||||
* [0.30000000000000004.com](https://0.30000000000000004.com/)
|
||||
* [What Every Programmer Should Know About Floating-Point Arithmetic](https://floating-point-gui.de/)
|
||||
|
||||
"""
|
||||
|
||||
@@ -349,6 +349,41 @@ defmodule GenServer do
|
||||
message arriving, `handle_info/2` is called with `:timeout` as the first
|
||||
argument.
|
||||
|
||||
For example:
|
||||
|
||||
defmodule Counter do
|
||||
use GenServer
|
||||
|
||||
@timeout to_timeout(second: 5)
|
||||
|
||||
@impl true
|
||||
def init(count) do
|
||||
{:ok, count, @timeout}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_call(:increment, _from, count) do
|
||||
new_count = count + 1
|
||||
{:reply, new_count, new_count, @timeout}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_info(:timeout, count) do
|
||||
{:stop, :normal, count}
|
||||
end
|
||||
end
|
||||
|
||||
A `Counter` server will exit with `:normal` if there are no messages in 5 seconds
|
||||
after the initialization or after the last `:increment` call:
|
||||
|
||||
{:ok, counter_pid} = GenServer.start(Counter, 50)
|
||||
GenServer.call(counter_pid, :increment)
|
||||
#=> 51
|
||||
|
||||
# After 5 seconds
|
||||
Process.alive?(counter_pid)
|
||||
#=> false
|
||||
|
||||
## When (not) to use a GenServer
|
||||
|
||||
So far, we have learned that a `GenServer` can be used as a supervised process
|
||||
@@ -488,7 +523,7 @@ defmodule GenServer do
|
||||
* [GenServer - Elixir's Getting Started Guide](genservers.md)
|
||||
* [`:gen_server` module documentation](`:gen_server`)
|
||||
* [gen_server Behaviour - OTP Design Principles](https://www.erlang.org/doc/design_principles/gen_server_concepts.html)
|
||||
* [Clients and Servers - Learn You Some Erlang for Great Good!](http://learnyousomeerlang.com/clients-and-servers)
|
||||
* [Clients and Servers - Learn You Some Erlang for Great Good!](https://learnyousomeerlang.com/clients-and-servers)
|
||||
|
||||
"""
|
||||
|
||||
|
||||
@@ -275,8 +275,6 @@ defprotocol Inspect do
|
||||
end
|
||||
|
||||
defimpl Inspect, for: Atom do
|
||||
require Macro
|
||||
|
||||
def inspect(atom, opts) do
|
||||
color_doc(Macro.inspect_atom(:literal, atom), color_key(atom), opts)
|
||||
end
|
||||
@@ -569,6 +567,7 @@ defimpl Inspect, for: Regex do
|
||||
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([:export | t], acc), do: translate_options(t, [?E | acc])
|
||||
defp translate_options([], acc), do: acc
|
||||
defp translate_options(_t, _acc), do: :error
|
||||
|
||||
@@ -662,36 +661,14 @@ end
|
||||
|
||||
defimpl Inspect, for: Any do
|
||||
def inspect(%module{} = struct, opts) do
|
||||
try do
|
||||
module.__info__(:struct)
|
||||
rescue
|
||||
_ -> Inspect.Map.inspect_as_map(struct, opts)
|
||||
else
|
||||
info ->
|
||||
if valid_struct?(info, struct) do
|
||||
info =
|
||||
for %{field: field} = map <- info,
|
||||
field != :__exception__,
|
||||
do: map
|
||||
info =
|
||||
for %{field: field} = map <- module.__info__(:struct),
|
||||
field != :__exception__,
|
||||
do: map
|
||||
|
||||
Inspect.Map.inspect_as_struct(struct, Macro.inspect_atom(:literal, module), info, opts)
|
||||
else
|
||||
Inspect.Map.inspect_as_map(struct, opts)
|
||||
end
|
||||
end
|
||||
Inspect.Map.inspect_as_struct(struct, Macro.inspect_atom(:literal, module), info, opts)
|
||||
end
|
||||
|
||||
defp valid_struct?(info, struct), do: valid_struct?(info, struct, map_size(struct) - 1)
|
||||
|
||||
defp valid_struct?([%{field: field} | info], struct, count) when is_map_key(struct, field),
|
||||
do: valid_struct?(info, struct, count - 1)
|
||||
|
||||
defp valid_struct?([], _struct, 0),
|
||||
do: true
|
||||
|
||||
defp valid_struct?(_fields, _struct, _count),
|
||||
do: false
|
||||
|
||||
def inspect_as_struct(map, name, infos, opts) do
|
||||
open = color_doc("#" <> name <> "<", :map, opts)
|
||||
sep = color_doc(",", :map, opts)
|
||||
|
||||
@@ -46,7 +46,7 @@ defmodule Inspect.Opts do
|
||||
* `:limit` - limits the number of items that are inspected for tuples,
|
||||
bitstrings, maps, lists and any other collection of items, with the exception of
|
||||
printable strings and printable charlists which use the `:printable_limit` option.
|
||||
It accepts a positive integer or `:infinity`. It defaults to 100 since
|
||||
It accepts a positive integer or `:infinity`. It defaults to `100` since
|
||||
`Elixir v1.19.0`, as it has better defaults to deal with nested collections.
|
||||
|
||||
* `:pretty` - if set to `true` enables pretty printing. Defaults to `false`.
|
||||
@@ -115,11 +115,28 @@ defmodule Inspect.Opts do
|
||||
width: non_neg_integer | :infinity
|
||||
}
|
||||
|
||||
@typedoc """
|
||||
Options for building an `Inspect.Opts` struct with `new/1`.
|
||||
"""
|
||||
@type new_opt ::
|
||||
{:base, :decimal | :binary | :hex | :octal}
|
||||
| {:binaries, :infer | :as_binaries | :as_strings}
|
||||
| {:charlists, :infer | :as_lists | :as_charlists}
|
||||
| {:custom_options, keyword}
|
||||
| {:inspect_fun, (any, t -> Inspect.Algebra.t())}
|
||||
| {:limit, non_neg_integer | :infinity}
|
||||
| {:pretty, boolean}
|
||||
| {:printable_limit, non_neg_integer | :infinity}
|
||||
| {:safe, boolean}
|
||||
| {:structs, boolean}
|
||||
| {:syntax_colors, [{color_key, IO.ANSI.ansidata()}]}
|
||||
| {:width, non_neg_integer | :infinity}
|
||||
|
||||
@doc """
|
||||
Builds an `Inspect.Opts` struct.
|
||||
"""
|
||||
@doc since: "1.13.0"
|
||||
@spec new(keyword()) :: t
|
||||
@spec new([new_opt()]) :: t
|
||||
def new(opts) do
|
||||
struct(%Inspect.Opts{inspect_fun: default_inspect_fun()}, opts)
|
||||
end
|
||||
@@ -324,6 +341,14 @@ defmodule Inspect.Algebra do
|
||||
quote do: {:doc_color, unquote(doc), unquote(color)}
|
||||
end
|
||||
|
||||
@typedoc """
|
||||
Options for container documents.
|
||||
"""
|
||||
@type container_opts :: [
|
||||
separator: String.t(),
|
||||
break: :strict | :flex | :maybe
|
||||
]
|
||||
|
||||
@docs [
|
||||
:doc_break,
|
||||
:doc_collapse,
|
||||
@@ -371,7 +396,7 @@ defmodule Inspect.Algebra do
|
||||
def to_doc_with_opts(term, opts)
|
||||
|
||||
def to_doc_with_opts(%_{} = struct, %Inspect.Opts{inspect_fun: fun} = opts) do
|
||||
if opts.structs do
|
||||
if opts.structs and valid_struct?(struct) do
|
||||
try do
|
||||
fun.(struct, opts)
|
||||
rescue
|
||||
@@ -428,6 +453,26 @@ defmodule Inspect.Algebra do
|
||||
fun.(arg, opts) |> pack_opts(opts)
|
||||
end
|
||||
|
||||
defp valid_struct?(%module{} = struct) do
|
||||
try do
|
||||
module.__info__(:struct)
|
||||
rescue
|
||||
_ -> false
|
||||
else
|
||||
info ->
|
||||
valid_struct?(info, struct, map_size(struct) - 1)
|
||||
end
|
||||
end
|
||||
|
||||
defp valid_struct?([%{field: field} | info], struct, count) when is_map_key(struct, field),
|
||||
do: valid_struct?(info, struct, count - 1)
|
||||
|
||||
defp valid_struct?([], _struct, 0),
|
||||
do: true
|
||||
|
||||
defp valid_struct?(_fields, _struct, _count),
|
||||
do: false
|
||||
|
||||
defp pack_opts({_doc, %Inspect.Opts{}} = doc_opts, _opts), do: doc_opts
|
||||
defp pack_opts(doc, opts), do: {doc, opts}
|
||||
|
||||
@@ -440,7 +485,14 @@ defmodule Inspect.Algebra do
|
||||
updated options from inspection.
|
||||
"""
|
||||
@doc since: "1.6.0"
|
||||
@spec container_doc(t, [term], t, Inspect.Opts.t(), (term, Inspect.Opts.t() -> t), keyword()) ::
|
||||
@spec container_doc(
|
||||
t,
|
||||
[term],
|
||||
t,
|
||||
Inspect.Opts.t(),
|
||||
(term, Inspect.Opts.t() -> t),
|
||||
container_opts()
|
||||
) ::
|
||||
t
|
||||
def container_doc(left, collection, right, inspect_opts, fun, opts \\ []) do
|
||||
container_doc_with_opts(left, collection, right, inspect_opts, fun, opts) |> elem(0)
|
||||
@@ -496,7 +548,7 @@ defmodule Inspect.Algebra do
|
||||
t,
|
||||
Inspect.Opts.t(),
|
||||
(term, Inspect.Opts.t() -> t),
|
||||
keyword()
|
||||
container_opts()
|
||||
) ::
|
||||
{t, Inspect.Opts.t()}
|
||||
def container_doc_with_opts(left, collection, right, inspect_opts, fun, opts \\ [])
|
||||
|
||||
@@ -172,6 +172,35 @@ defmodule Integer do
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Performs a ceiled integer division.
|
||||
|
||||
Raises an `ArithmeticError` exception if one of the arguments is not an
|
||||
integer, or when the `divisor` is `0`.
|
||||
|
||||
This function performs a *ceiled* integer division, which means that
|
||||
the result will always be rounded towards positive infinity.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Integer.ceil_div(5, 2)
|
||||
3
|
||||
iex> Integer.ceil_div(6, -4)
|
||||
-1
|
||||
iex> Integer.ceil_div(-99, 2)
|
||||
-49
|
||||
|
||||
"""
|
||||
@doc since: "1.20.0"
|
||||
@spec ceil_div(integer, neg_integer | pos_integer) :: integer
|
||||
def ceil_div(dividend, divisor) do
|
||||
if not :erlang.xor(dividend < 0, divisor < 0) and rem(dividend, divisor) != 0 do
|
||||
div(dividend, divisor) + 1
|
||||
else
|
||||
div(dividend, divisor)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the ordered digits for the given `integer`.
|
||||
|
||||
|
||||
+61
-14
@@ -128,6 +128,22 @@ defmodule IO do
|
||||
@type nodata :: {:error, term} | :eof
|
||||
@type chardata :: String.t() | maybe_improper_list(char | chardata, String.t() | [])
|
||||
|
||||
@type inspect_opts :: [Inspect.Opts.new_opt() | {:label, term}]
|
||||
|
||||
@typedoc """
|
||||
Stacktrace information as keyword options for `warn/2`.
|
||||
|
||||
At least `:file` is required. Other options are optional and used
|
||||
to provide more precise location information.
|
||||
"""
|
||||
@type warn_stacktrace_opts :: [
|
||||
file: String.t(),
|
||||
line: pos_integer(),
|
||||
column: pos_integer(),
|
||||
module: module(),
|
||||
function: {atom(), arity()}
|
||||
]
|
||||
|
||||
defguardp is_device(term) when is_atom(term) or is_pid(term)
|
||||
defguardp is_iodata(data) when is_list(data) or is_binary(data)
|
||||
|
||||
@@ -346,7 +362,10 @@ defmodule IO do
|
||||
#=> my_app.ex:4: MyApp.main/1
|
||||
|
||||
"""
|
||||
@spec warn(chardata | String.Chars.t(), Exception.stacktrace() | keyword() | Macro.Env.t()) ::
|
||||
@spec warn(
|
||||
chardata | String.Chars.t(),
|
||||
Exception.stacktrace() | warn_stacktrace_opts() | Macro.Env.t()
|
||||
) ::
|
||||
:ok
|
||||
def warn(message, stacktrace_info)
|
||||
|
||||
@@ -448,13 +467,15 @@ defmodule IO do
|
||||
|
||||
## Examples
|
||||
|
||||
The following code:
|
||||
|
||||
IO.inspect(<<0, 1, 2>>, width: 40)
|
||||
|
||||
Prints:
|
||||
|
||||
<<0, 1, 2>>
|
||||
|
||||
We can use the `:label` option to decorate the output:
|
||||
You can use the `:label` option to decorate the output:
|
||||
|
||||
IO.inspect(1..100, label: "a wonderful range")
|
||||
|
||||
@@ -462,21 +483,23 @@ defmodule IO do
|
||||
|
||||
a wonderful range: 1..100
|
||||
|
||||
The `:label` option is especially useful with pipelines:
|
||||
Inspect truncates large inputs by default. The `:printable_limit` controls
|
||||
the limit for strings and other string-like constructs (such as charlists):
|
||||
|
||||
[1, 2, 3]
|
||||
|> IO.inspect(label: "before")
|
||||
|> Enum.map(&(&1 * 2))
|
||||
|> IO.inspect(label: "after")
|
||||
|> Enum.sum()
|
||||
"abc"
|
||||
|> String.duplicate(9001)
|
||||
|> IO.inspect(printable_limit: :infinity)
|
||||
|
||||
Prints:
|
||||
For containers such as lists, maps, and tuples, the number of entries
|
||||
is managed by the `:limit` option:
|
||||
|
||||
before: [1, 2, 3]
|
||||
after: [2, 4, 6]
|
||||
1..100
|
||||
|> Enum.map(& {&1, &1})
|
||||
|> Enum.into(%{})
|
||||
|> IO.inspect(limit: :infinity)
|
||||
|
||||
"""
|
||||
@spec inspect(item, keyword) :: item when item: var
|
||||
@spec inspect(item, inspect_opts) :: item when item: var
|
||||
def inspect(item, opts \\ []) do
|
||||
inspect(:stdio, item, opts)
|
||||
end
|
||||
@@ -486,9 +509,10 @@ defmodule IO do
|
||||
|
||||
See `inspect/2` for a full list of options.
|
||||
"""
|
||||
@spec inspect(device, item, keyword) :: item when item: var
|
||||
@spec inspect(device, item, inspect_opts) :: item when item: var
|
||||
def inspect(device, item, opts) when is_device(device) and is_list(opts) do
|
||||
label = if label = opts[:label], do: [to_chardata(label), ": "], else: []
|
||||
{label, opts} = Keyword.pop(opts, :label)
|
||||
label = if label, do: [to_chardata(label), ": "], else: []
|
||||
opts = Inspect.Opts.new(opts)
|
||||
doc = Inspect.Algebra.group(Inspect.Algebra.to_doc(item, opts))
|
||||
chardata = Inspect.Algebra.format(doc, opts.width)
|
||||
@@ -772,6 +796,29 @@ defmodule IO do
|
||||
:erlang.iolist_size(iodata)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if an IO data (the length is zero).
|
||||
|
||||
For more information about IO data, see the ["IO data"](#module-io-data)
|
||||
section in the module documentation.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> IO.iodata_empty?([])
|
||||
true
|
||||
iex> IO.iodata_empty?([""])
|
||||
true
|
||||
iex> IO.iodata_empty?([1, 2 | <<3, 4>>])
|
||||
false
|
||||
|
||||
"""
|
||||
@doc since: "1.20.0"
|
||||
@spec iodata_empty?(iodata) :: boolean
|
||||
def iodata_empty?(""), do: true
|
||||
def iodata_empty?([]), do: true
|
||||
def iodata_empty?([head | tail]), do: iodata_empty?(head) and iodata_empty?(tail)
|
||||
def iodata_empty?(_), do: false
|
||||
|
||||
@doc false
|
||||
def each_stream(device, line_or_codepoints) do
|
||||
case read(device, line_or_codepoints) do
|
||||
|
||||
@@ -5,6 +5,20 @@
|
||||
defmodule IO.ANSI.Docs do
|
||||
@moduledoc false
|
||||
|
||||
@type print_opts :: [
|
||||
enabled: boolean(),
|
||||
doc_bold: [IO.ANSI.ansicode()],
|
||||
doc_code: [IO.ANSI.ansicode()],
|
||||
doc_headings: [IO.ANSI.ansicode()],
|
||||
doc_metadata: [IO.ANSI.ansicode()],
|
||||
doc_quote: [IO.ANSI.ansicode()],
|
||||
doc_inline_code: [IO.ANSI.ansicode()],
|
||||
doc_table_heading: [IO.ANSI.ansicode()],
|
||||
doc_title: [IO.ANSI.ansicode()],
|
||||
doc_underline: [IO.ANSI.ansicode()],
|
||||
width: pos_integer()
|
||||
]
|
||||
|
||||
@bullet_text_unicode "• "
|
||||
@bullet_text_ascii "* "
|
||||
@bullets [?*, ?-, ?+]
|
||||
@@ -30,7 +44,7 @@ defmodule IO.ANSI.Docs do
|
||||
Values for the color settings are strings with
|
||||
comma-separated ANSI values.
|
||||
"""
|
||||
@spec default_options() :: keyword
|
||||
@spec default_options() :: print_opts
|
||||
def default_options do
|
||||
[
|
||||
enabled: true,
|
||||
@@ -52,7 +66,7 @@ defmodule IO.ANSI.Docs do
|
||||
|
||||
See `default_options/0` for docs on the supported options.
|
||||
"""
|
||||
@spec print_headings([String.t()], keyword) :: :ok
|
||||
@spec print_headings([String.t()], print_opts) :: :ok
|
||||
def print_headings(headings, options \\ []) do
|
||||
# It's possible for some of the headings to contain newline characters (`\n`), so in order to prevent it from
|
||||
# breaking the output from `print_headings/2`, as `print_headings/2` tries to pad the whole heading, we first split
|
||||
@@ -77,7 +91,7 @@ defmodule IO.ANSI.Docs do
|
||||
|
||||
See `default_options/0` for docs on the supported options.
|
||||
"""
|
||||
@spec print_metadata(map, keyword) :: :ok
|
||||
@spec print_metadata(map, print_opts) :: :ok
|
||||
def print_metadata(metadata, options \\ []) when is_map(metadata) do
|
||||
options = Keyword.merge(default_options(), options)
|
||||
print_each_metadata(metadata, options) && IO.write("\n")
|
||||
@@ -115,7 +129,7 @@ defmodule IO.ANSI.Docs do
|
||||
|
||||
It takes a set of `options` defined in `default_options/0`.
|
||||
"""
|
||||
@spec print(term(), String.t(), keyword) :: :ok
|
||||
@spec print(term(), String.t(), print_opts) :: :ok
|
||||
def print(doc, format, options \\ [])
|
||||
|
||||
def print(doc, "text/markdown", options) when is_binary(doc) and is_list(options) do
|
||||
|
||||
+29
-8
@@ -8,24 +8,29 @@ defprotocol JSON.Encoder do
|
||||
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: [....]}
|
||||
@derive {JSON.Encoder, only: [...]}
|
||||
defstruct ...
|
||||
|
||||
It is also possible to encode all fields or skip some fields via the
|
||||
`:except` option:
|
||||
Additionally, you can exclude specific fields using the `:except` option or
|
||||
encode all fields by omitting both options entirely, but these should be used
|
||||
with caution:
|
||||
|
||||
@derive {JSON.Encoder, except: [...]}
|
||||
defstruct ...
|
||||
|
||||
@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.
|
||||
> Prefer using `:only` to avoid accidentally leaking private information when
|
||||
> new fields are added. Other approaches should be used with auction.
|
||||
|
||||
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:
|
||||
You can also use `Protocol.derive/3` if you don't own the struct that you want
|
||||
to encode to JSON:
|
||||
|
||||
Protocol.derive(JSON.Encoder, NameOfTheStruct, only: [...])
|
||||
Protocol.derive(JSON.Encoder, NameOfTheStruct, except: [...])
|
||||
Protocol.derive(JSON.Encoder, NameOfTheStruct)
|
||||
|
||||
"""
|
||||
@@ -328,6 +333,22 @@ defmodule JSON do
|
||||
| {:invalid_byte, non_neg_integer(), byte()}
|
||||
| {:unexpected_sequence, non_neg_integer(), binary()}
|
||||
|
||||
@typedoc """
|
||||
Decoders for customizing JSON decoding behavior.
|
||||
"""
|
||||
@type decoders :: [
|
||||
array_start: (term() -> term()),
|
||||
array_push: (term(), term() -> term()),
|
||||
array_finish: (term(), term() -> {term(), term()}),
|
||||
object_start: (term() -> term()),
|
||||
object_push: (term(), term(), term() -> term()),
|
||||
object_finish: (term(), term() -> {term(), term()}),
|
||||
float: (String.t() -> term()),
|
||||
integer: (String.t() -> term()),
|
||||
string: (String.t() -> term()),
|
||||
null: term()
|
||||
]
|
||||
|
||||
@doc ~S"""
|
||||
Decodes the given JSON.
|
||||
|
||||
@@ -381,7 +402,7 @@ defmodule JSON do
|
||||
|
||||
For streaming decoding, see Erlang's [`:json`](`:json`) module.
|
||||
"""
|
||||
@spec decode(binary(), term(), keyword()) ::
|
||||
@spec decode(binary(), term(), decoders()) ::
|
||||
{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)
|
||||
|
||||
+50
-49
@@ -231,7 +231,7 @@ defmodule Kernel do
|
||||
|
||||
Finally, note there is an overall structural sorting order, called
|
||||
"Term Ordering", defined below. This order is provided for reference
|
||||
purposes, it is not required by Elixir developers to know it by heart.
|
||||
purposes, it is not required for Elixir developers to know it by heart.
|
||||
|
||||
### Term ordering
|
||||
|
||||
@@ -1999,6 +1999,12 @@ defmodule Kernel do
|
||||
{:case, extra ++ meta, args}
|
||||
end
|
||||
|
||||
defp x_is_false_or_nil do
|
||||
quote generated: true do
|
||||
:erlang.orelse(:erlang."=:="(x, false), :erlang."=:="(x, nil))
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Strictly boolean "or" operator.
|
||||
|
||||
@@ -2101,7 +2107,7 @@ defmodule Kernel do
|
||||
[optimize_boolean: true, type_check: :expr],
|
||||
quote do
|
||||
case unquote(value) do
|
||||
x when :"Elixir.Kernel".in(x, [false, nil]) -> false
|
||||
x when unquote(x_is_false_or_nil()) -> false
|
||||
_ -> true
|
||||
end
|
||||
end
|
||||
@@ -2115,7 +2121,7 @@ defmodule Kernel do
|
||||
[optimize_boolean: true, type_check: :expr],
|
||||
quote do
|
||||
case unquote(value) do
|
||||
x when :"Elixir.Kernel".in(x, [false, nil]) -> true
|
||||
x when unquote(x_is_false_or_nil()) -> true
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
@@ -2456,7 +2462,7 @@ defmodule Kernel do
|
||||
See the "Deriving" section of the documentation of the `Inspect`
|
||||
protocol for more information.
|
||||
"""
|
||||
@spec inspect(Inspect.t(), keyword) :: String.t()
|
||||
@spec inspect(Inspect.t(), [Inspect.Opts.new_opt()]) :: String.t()
|
||||
def inspect(term, opts \\ []) when is_list(opts) do
|
||||
opts = Inspect.Opts.new(opts)
|
||||
|
||||
@@ -2815,7 +2821,7 @@ defmodule Kernel do
|
||||
This is most commonly used in pipelines, using the `|>/2` operator, allowing you
|
||||
to pipe a value to a function outside of its first argument.
|
||||
|
||||
### Examples
|
||||
## Examples
|
||||
|
||||
iex> 1 |> then(fn x -> x * 2 end)
|
||||
2
|
||||
@@ -3524,8 +3530,7 @@ defmodule Kernel do
|
||||
end
|
||||
|
||||
@doc """
|
||||
A convenience macro that checks if the right side (an expression) matches the
|
||||
left side (a pattern).
|
||||
A convenience macro that checks if the result of `expression` matches `pattern`.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -3603,7 +3608,7 @@ defmodule Kernel do
|
||||
#=> (MatchError) no match of right hand side value: %{x: 1, y: 2}
|
||||
|
||||
"""
|
||||
defmacro match?(pattern, expr) do
|
||||
defmacro match?(pattern, expression) do
|
||||
success =
|
||||
quote do
|
||||
unquote(pattern) -> true
|
||||
@@ -3614,7 +3619,7 @@ defmodule Kernel do
|
||||
_ -> false
|
||||
end
|
||||
|
||||
{:case, [], [expr, [do: success ++ failure]]}
|
||||
{:case, [], [expression, [do: success ++ failure]]}
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -3814,19 +3819,6 @@ defmodule Kernel do
|
||||
{_, doc} when doc_attr? ->
|
||||
do_at_escape(name, doc)
|
||||
|
||||
%{__struct__: Regex, source: source, opts: opts} = regex ->
|
||||
# TODO: Remove this in Elixir v2.0
|
||||
IO.warn(
|
||||
"storing and reading regexes from module attributes is deprecated, " <>
|
||||
"inline the regex inside the function definition instead",
|
||||
env
|
||||
)
|
||||
|
||||
case :erlang.system_info(:otp_release) < [?2, ?8] do
|
||||
true -> do_at_escape(name, regex)
|
||||
false -> quote(do: Regex.compile!(unquote(source), unquote(opts)))
|
||||
end
|
||||
|
||||
value ->
|
||||
do_at_escape(name, value)
|
||||
end
|
||||
@@ -3872,7 +3864,9 @@ defmodule Kernel do
|
||||
|
||||
defp do_at_escape(name, value) do
|
||||
try do
|
||||
:elixir_quote.escape(value, :none, false)
|
||||
# mark module attrs as shallow-generated since the ast for their representation
|
||||
# might contain opaque terms
|
||||
Macro.escape(value, generated: true)
|
||||
rescue
|
||||
ex in [ArgumentError] ->
|
||||
raise ArgumentError,
|
||||
@@ -4058,7 +4052,7 @@ defmodule Kernel do
|
||||
[optimize_boolean: true, type_check: :expr],
|
||||
quote do
|
||||
case unquote(condition) do
|
||||
x when :"Elixir.Kernel".in(x, [false, nil]) -> unquote(else_clause)
|
||||
x when unquote(x_is_false_or_nil()) -> unquote(else_clause)
|
||||
_ -> unquote(do_clause)
|
||||
end
|
||||
end
|
||||
@@ -4379,7 +4373,7 @@ defmodule Kernel do
|
||||
[type_check: :expr],
|
||||
quote do
|
||||
case unquote(left) do
|
||||
x when :"Elixir.Kernel".in(x, [false, nil]) ->
|
||||
x when unquote(x_is_false_or_nil()) ->
|
||||
x
|
||||
|
||||
_ ->
|
||||
@@ -4422,7 +4416,7 @@ defmodule Kernel do
|
||||
[type_check: :expr],
|
||||
quote do
|
||||
case unquote(left) do
|
||||
x when :"Elixir.Kernel".in(x, [false, nil]) ->
|
||||
x when unquote(x_is_false_or_nil()) ->
|
||||
unquote(right)
|
||||
|
||||
x ->
|
||||
@@ -4770,8 +4764,8 @@ defmodule Kernel do
|
||||
defp in_range(left, first, last, step) do
|
||||
quoted =
|
||||
quote do
|
||||
:erlang.is_integer(unquote(left)) and :erlang.is_integer(unquote(first)) and
|
||||
:erlang.is_integer(unquote(last)) and
|
||||
unquote(generated_is_integer(left)) and unquote(generated_is_integer(first)) and
|
||||
unquote(generated_is_integer(last)) and
|
||||
((:erlang.>(unquote(step), 0) and
|
||||
unquote(increasing_compare(left, first, last))) or
|
||||
(:erlang.<(unquote(step), 0) and
|
||||
@@ -4787,9 +4781,9 @@ defmodule Kernel do
|
||||
|
||||
defp in_range_literal(left, first, last, step) when step > 0 do
|
||||
quoted =
|
||||
quote do
|
||||
:erlang.andalso(
|
||||
:erlang.is_integer(unquote(left)),
|
||||
quote generated: true do
|
||||
Kernel.and(
|
||||
unquote(generated_is_integer(left)),
|
||||
unquote(increasing_compare(left, first, last))
|
||||
)
|
||||
end
|
||||
@@ -4799,9 +4793,9 @@ defmodule Kernel do
|
||||
|
||||
defp in_range_literal(left, first, last, step) when step < 0 do
|
||||
quoted =
|
||||
quote do
|
||||
:erlang.andalso(
|
||||
:erlang.is_integer(unquote(left)),
|
||||
quote generated: true do
|
||||
Kernel.and(
|
||||
unquote(generated_is_integer(left)),
|
||||
unquote(decreasing_compare(left, first, last))
|
||||
)
|
||||
end
|
||||
@@ -4815,7 +4809,7 @@ defmodule Kernel do
|
||||
|
||||
defp in_range_step(quoted, left, first, step) do
|
||||
quote do
|
||||
:erlang.andalso(
|
||||
Kernel.and(
|
||||
unquote(quoted),
|
||||
:erlang."=:="(:erlang.rem(unquote(left) - unquote(first), unquote(step)), 0)
|
||||
)
|
||||
@@ -4824,7 +4818,7 @@ defmodule Kernel do
|
||||
|
||||
defp in_list(left, head, tail, expand, right, in_body?) do
|
||||
[head | tail] = :lists.map(&comp(left, &1, expand, right, in_body?), [head | tail])
|
||||
:lists.foldl("e(do: :erlang.orelse(unquote(&2), unquote(&1))), head, tail)
|
||||
:lists.foldl("e(do: Kernel.or(unquote(&2), unquote(&1))), head, tail)
|
||||
end
|
||||
|
||||
defp comp(left, {:|, _, [head, tail]}, expand, right, in_body?) do
|
||||
@@ -4834,7 +4828,7 @@ defmodule Kernel do
|
||||
|
||||
[tail_head | tail] ->
|
||||
quote do
|
||||
:erlang.orelse(
|
||||
Kernel.or(
|
||||
:erlang."=:="(unquote(left), unquote(head)),
|
||||
unquote(in_list(left, tail_head, tail, expand, right, in_body?))
|
||||
)
|
||||
@@ -4842,7 +4836,7 @@ defmodule Kernel do
|
||||
|
||||
tail when in_body? ->
|
||||
quote do
|
||||
:erlang.orelse(
|
||||
Kernel.or(
|
||||
:erlang."=:="(unquote(left), unquote(head)),
|
||||
:lists.member(unquote(left), unquote(tail))
|
||||
)
|
||||
@@ -4857,9 +4851,13 @@ defmodule Kernel do
|
||||
quote(do: :erlang."=:="(unquote(left), unquote(right)))
|
||||
end
|
||||
|
||||
defp generated_is_integer(arg) do
|
||||
quote generated: true, do: :erlang.is_integer(unquote(arg))
|
||||
end
|
||||
|
||||
defp increasing_compare(var, first, last) do
|
||||
quote do
|
||||
:erlang.andalso(
|
||||
Kernel.and(
|
||||
:erlang.>=(unquote(var), unquote(first)),
|
||||
:erlang."=<"(unquote(var), unquote(last))
|
||||
)
|
||||
@@ -4868,7 +4866,7 @@ defmodule Kernel do
|
||||
|
||||
defp decreasing_compare(var, first, last) do
|
||||
quote do
|
||||
:erlang.andalso(
|
||||
Kernel.and(
|
||||
:erlang."=<"(unquote(var), unquote(first)),
|
||||
:erlang.>=(unquote(var), unquote(last))
|
||||
)
|
||||
@@ -5197,7 +5195,7 @@ defmodule Kernel do
|
||||
quote(do: Kernel.LexicalTracker.read_cache(unquote(pid), unquote(integer)))
|
||||
|
||||
%{} ->
|
||||
:elixir_quote.escape(block, :none, false)
|
||||
:elixir_quote.escape(block, :escape, false)
|
||||
end
|
||||
|
||||
versioned_vars = env.versioned_vars
|
||||
@@ -5477,7 +5475,7 @@ defmodule Kernel do
|
||||
store =
|
||||
case unquoted_expr or unquoted_call do
|
||||
true ->
|
||||
:elixir_quote.escape({call, expr}, :none, true)
|
||||
:elixir_quote.escape({call, expr}, :escape, true)
|
||||
|
||||
false ->
|
||||
key = :erlang.unique_integer()
|
||||
@@ -5605,8 +5603,8 @@ defmodule Kernel do
|
||||
## Types
|
||||
|
||||
It is recommended to define types for structs. By convention, such a type
|
||||
is called `t`. To define a struct inside a type, the struct literal syntax
|
||||
is used:
|
||||
is called `t`. To define a type for a struct, the struct literal syntax is
|
||||
used:
|
||||
|
||||
defmodule User do
|
||||
defstruct name: "John", age: 25
|
||||
@@ -6389,7 +6387,7 @@ defmodule Kernel do
|
||||
|
||||
With a timeout:
|
||||
|
||||
iex> to_timeout(5400000)
|
||||
iex> to_timeout(5_400_000)
|
||||
5400000
|
||||
iex> to_timeout(:infinity)
|
||||
:infinity
|
||||
@@ -6630,11 +6628,13 @@ defmodule Kernel do
|
||||
defmacro sigil_r(term, modifiers)
|
||||
|
||||
defmacro sigil_r({:<<>>, _meta, [binary]}, options) when is_binary(binary) do
|
||||
assert_no_match_or_guard_scope(__CALLER__.context, "the ~r sigil")
|
||||
binary = :elixir_interpolation.unescape_string(binary, ®ex_unescape_map/1)
|
||||
compile_regex(binary, options)
|
||||
end
|
||||
|
||||
defmacro sigil_r({:<<>>, meta, pieces}, options) do
|
||||
assert_no_match_or_guard_scope(__CALLER__.context, "the ~r sigil")
|
||||
tuple = {:<<>>, meta, unescape_tokens(pieces, ®ex_unescape_map/1)}
|
||||
compile_regex(tuple, options)
|
||||
end
|
||||
@@ -6653,13 +6653,14 @@ defmodule Kernel do
|
||||
end
|
||||
|
||||
defp compile_regex(binary_or_tuple, options) do
|
||||
# TODO: Remove this when we require Erlang/OTP 28+
|
||||
case is_binary(binary_or_tuple) and :erlang.system_info(:otp_release) < [?2, ?8] do
|
||||
bin_opts = :binary.list_to_bin(options)
|
||||
|
||||
case is_binary(binary_or_tuple) do
|
||||
true ->
|
||||
Macro.escape(Regex.compile!(binary_or_tuple, :binary.list_to_bin(options)))
|
||||
Macro.escape(Regex.compile!(binary_or_tuple, bin_opts))
|
||||
|
||||
false ->
|
||||
quote(do: Regex.compile!(unquote(binary_or_tuple), unquote(:binary.list_to_bin(options))))
|
||||
quote(do: Regex.compile!(unquote(binary_or_tuple), unquote(bin_opts)))
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -21,6 +21,15 @@ defmodule Kernel.LexicalTracker do
|
||||
:gen_server.call(pid, :references, @timeout)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Invoked during module expansion to annotate a require
|
||||
must be warned if unused.
|
||||
"""
|
||||
def warn_require(pid, meta, module, alias) do
|
||||
:gen_server.cast(pid, {:warn_require, module, meta, alias})
|
||||
module
|
||||
end
|
||||
|
||||
@doc """
|
||||
Invoked during module expansion to annotate an alias
|
||||
must be warned if unused.
|
||||
@@ -57,6 +66,11 @@ defmodule Kernel.LexicalTracker do
|
||||
:gen_server.cast(pid, {:add_export, module})
|
||||
end
|
||||
|
||||
@doc false
|
||||
def add_require(pid, module, meta) when is_atom(module) do
|
||||
:gen_server.cast(pid, {:add_require, module, meta})
|
||||
end
|
||||
|
||||
@doc false
|
||||
def add_import(pid, module, fas, meta, warn) when is_atom(module) do
|
||||
:gen_server.cast(pid, {:add_import, module, fas, meta, warn})
|
||||
@@ -119,12 +133,18 @@ defmodule Kernel.LexicalTracker do
|
||||
:gen_server.call(pid, :unused_aliases, @timeout)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def collect_unused_requires(pid) do
|
||||
:gen_server.call(pid, :unused_requires, @timeout)
|
||||
end
|
||||
|
||||
# Callbacks
|
||||
|
||||
def init(:ok) do
|
||||
state = %{
|
||||
aliases: %{},
|
||||
imports: %{},
|
||||
requires: %{},
|
||||
references: %{},
|
||||
exports: %{},
|
||||
cache: %{},
|
||||
@@ -150,6 +170,18 @@ defmodule Kernel.LexicalTracker do
|
||||
{:reply, Enum.sort(imports), state}
|
||||
end
|
||||
|
||||
def handle_call(:unused_requires, _from, state) do
|
||||
%{references: references, aliases: aliases} = state
|
||||
|
||||
unused_requires =
|
||||
for {module, {meta, alias}} <- state.requires,
|
||||
Map.get(references, module) != :compile do
|
||||
{module, meta, alias, Map.get(aliases, alias) == :used}
|
||||
end
|
||||
|
||||
{:reply, Enum.sort(unused_requires), state}
|
||||
end
|
||||
|
||||
def handle_call(:references, _from, state) do
|
||||
{compile, runtime} = partition(Map.to_list(state.references), [], [])
|
||||
{:reply, {compile, Map.keys(state.exports), runtime, state.compile_env}, state}
|
||||
@@ -245,6 +277,10 @@ defmodule Kernel.LexicalTracker do
|
||||
{:noreply, put_in(state.imports[module][@warn_key], true)}
|
||||
end
|
||||
|
||||
def handle_cast({:warn_require, module, meta, alias}, state) do
|
||||
{:noreply, put_in(state.requires[module], {meta, alias})}
|
||||
end
|
||||
|
||||
@doc false
|
||||
def handle_info(_msg, state) do
|
||||
{:noreply, state}
|
||||
|
||||
@@ -16,11 +16,43 @@ defmodule Kernel.ParallelCompiler do
|
||||
@type warning() :: {file :: Path.t(), Code.position(), message :: String.t()}
|
||||
@type error() :: {file :: Path.t(), Code.position(), message :: String.t()}
|
||||
|
||||
@typedoc """
|
||||
Options for parallel compilation functions.
|
||||
"""
|
||||
@type compile_opts :: [
|
||||
after_compile: (-> term()),
|
||||
each_file: (Path.t() -> term()),
|
||||
each_long_compilation: (Path.t() -> term()) | (Path.t(), pid() -> term()),
|
||||
each_long_verification: (module() -> term()) | (module(), pid() -> term()),
|
||||
each_module: (Path.t(), module(), binary() -> term()),
|
||||
each_cycle: ([module()], [Code.diagnostic(:warning)] ->
|
||||
{:compile, [module()], [Code.diagnostic(:warning)]}
|
||||
| {:runtime, [module()], [Code.diagnostic(:warning)]}),
|
||||
long_compilation_threshold: pos_integer(),
|
||||
long_verification_threshold: pos_integer(),
|
||||
verification: boolean(),
|
||||
profile: :time,
|
||||
dest: Path.t(),
|
||||
beam_timestamp: term(),
|
||||
return_diagnostics: boolean(),
|
||||
max_concurrency: pos_integer()
|
||||
]
|
||||
|
||||
@typedoc """
|
||||
Options for requiring files in parallel.
|
||||
"""
|
||||
@type require_opts :: [
|
||||
each_file: (Path.t() -> term()),
|
||||
each_module: (Path.t(), module(), binary() -> term()),
|
||||
max_concurrency: pos_integer(),
|
||||
return_diagnostics: boolean()
|
||||
]
|
||||
|
||||
@doc """
|
||||
Starts a task for parallel compilation.
|
||||
"""
|
||||
# TODO: Deprecate this on Elixir v1.20.
|
||||
@doc deprecated: "Use `pmap/2` instead"
|
||||
# TODO: Remove me on Elixir 2.0
|
||||
@deprecated "Use `pmap/2` instead"
|
||||
def async(fun) when is_function(fun, 0) do
|
||||
{ref, task} = inner_async(fun)
|
||||
send(task.pid, ref)
|
||||
@@ -114,10 +146,9 @@ defmodule Kernel.ParallelCompiler do
|
||||
the current file stops being compiled until the dependency is
|
||||
resolved.
|
||||
|
||||
It returns `{:ok, modules, warnings}` or `{:error, errors, warnings}`
|
||||
by default but we recommend using `return_diagnostics: true` so it returns
|
||||
diagnostics as maps as well as a map of compilation information.
|
||||
The map has the shape of:
|
||||
It must be invoked with `return_diagnostics: true` as option, so it returns
|
||||
`{:ok, modules, warnings_info}` or `{:error, errors, warnings_info}`,
|
||||
where `warnings_info` has the shape:
|
||||
|
||||
%{
|
||||
runtime_warnings: [warning],
|
||||
@@ -177,15 +208,16 @@ defmodule Kernel.ParallelCompiler do
|
||||
* `:beam_timestamp` - the modification timestamp to give all BEAM files
|
||||
|
||||
* `:return_diagnostics` (since v1.15.0) - returns maps with information instead of
|
||||
a list of warnings and returns diagnostics as maps instead of tuples
|
||||
a list of warnings and returns diagnostics as maps instead of tuples.
|
||||
This option must be set to true, except for backwards compatibibility reasons.
|
||||
|
||||
* `: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.
|
||||
Defaults to the number of schedulers online, or at least `2`.
|
||||
|
||||
"""
|
||||
@doc since: "1.6.0"
|
||||
@spec compile([Path.t()], keyword()) ::
|
||||
@spec compile([Path.t()], compile_opts()) ::
|
||||
{:ok, [atom], [warning] | info()}
|
||||
| {:error, [error] | [Code.diagnostic(:error)], [warning] | info()}
|
||||
def compile(files, options \\ []) when is_list(options) do
|
||||
@@ -198,7 +230,7 @@ defmodule Kernel.ParallelCompiler do
|
||||
See `compile/2` for more information.
|
||||
"""
|
||||
@doc since: "1.6.0"
|
||||
@spec compile_to_path([Path.t()], Path.t(), keyword()) ::
|
||||
@spec compile_to_path([Path.t()], Path.t(), compile_opts()) ::
|
||||
{:ok, [atom], [warning] | info()}
|
||||
| {:error, [error] | [Code.diagnostic(:error)], [warning] | info()}
|
||||
def compile_to_path(files, path, options \\ []) when is_binary(path) and is_list(options) do
|
||||
@@ -211,10 +243,9 @@ defmodule Kernel.ParallelCompiler do
|
||||
Opposite to compile, dependencies are not attempted to be
|
||||
automatically solved between files.
|
||||
|
||||
It returns `{:ok, modules, warnings}` or `{:error, errors, warnings}`
|
||||
by default but we recommend using `return_diagnostics: true` so it returns
|
||||
diagnostics as maps as well as a map of compilation information.
|
||||
The map has the shape of:
|
||||
It must be invoked with `return_diagnostics: true` as option, so it returns
|
||||
`{:ok, modules, warnings_info}` or `{:error, errors, warnings_info}`,
|
||||
where `warnings_info` has the shape:
|
||||
|
||||
%{
|
||||
runtime_warnings: [warning],
|
||||
@@ -231,11 +262,15 @@ defmodule Kernel.ParallelCompiler do
|
||||
|
||||
* `: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.
|
||||
Defaults to the number of schedulers online, or at least `2`.
|
||||
|
||||
* `:return_diagnostics` (since v1.15.0) - returns maps with information instead of
|
||||
a list of warnings and returns diagnostics as maps instead of tuples.
|
||||
This option must be set to true, except for backwards compatibibility reasons.
|
||||
|
||||
"""
|
||||
@doc since: "1.6.0"
|
||||
@spec require([Path.t()], keyword()) ::
|
||||
@spec require([Path.t()], require_opts()) ::
|
||||
{:ok, [atom], [warning] | info()}
|
||||
| {:error, [error] | [Code.diagnostic(:error)], [warning] | info()}
|
||||
def require(files, options \\ []) when is_list(options) do
|
||||
@@ -286,7 +321,10 @@ defmodule Kernel.ParallelCompiler do
|
||||
if Keyword.get(options, :return_diagnostics, false) do
|
||||
{status, modules_or_errors, info}
|
||||
else
|
||||
IO.warn("you must pass return_diagnostics: true when invoking Kernel.ParallelCompiler")
|
||||
IO.warn(
|
||||
"you must pass return_diagnostics: true when invoking Kernel.ParallelCompiler functions"
|
||||
)
|
||||
|
||||
to_tuples = &Enum.map(&1, fn diag -> {diag.file, diag.position, diag.message} end)
|
||||
|
||||
modules_or_errors =
|
||||
@@ -342,27 +380,73 @@ defmodule Kernel.ParallelCompiler do
|
||||
end
|
||||
end
|
||||
|
||||
defp write_module_binaries(result, {:compile, path}, timestamp) do
|
||||
File.mkdir_p!(path)
|
||||
Code.prepend_path(path)
|
||||
defp write_module_binaries(result, {:compile, path}, state) when map_size(result) > 0 do
|
||||
profile(state, "writing modules to disk", fn ->
|
||||
File.mkdir_p!(path)
|
||||
Code.prepend_path(path)
|
||||
timestamp = state.beam_timestamp
|
||||
|
||||
for {{:module, module}, {binary, _}} when is_binary(binary) <- result do
|
||||
full_path = Path.join(path, Atom.to_string(module) <> ".beam")
|
||||
File.write!(full_path, binary)
|
||||
if timestamp, do: File.touch!(full_path, timestamp)
|
||||
module
|
||||
end
|
||||
# We fan-out the writes as that improves performance
|
||||
# when writing hundreds of beam files. This is cheap as
|
||||
# we only transfer atoms and binaries across processes.
|
||||
pool_size = min(map_size(result), state.schedulers)
|
||||
|
||||
pool_list =
|
||||
for _ <- 1..pool_size do
|
||||
spawn_link(fn -> write_loop(path, timestamp) end)
|
||||
end
|
||||
|
||||
pool_tuple = List.to_tuple(pool_list)
|
||||
|
||||
{modules, _} =
|
||||
Enum.flat_map_reduce(result, 0, fn
|
||||
{{:module, module}, {binary, _}}, scheduler when is_binary(binary) ->
|
||||
send(elem(pool_tuple, scheduler), {:write, module, binary})
|
||||
{[module], rem(scheduler + 1, pool_size)}
|
||||
|
||||
_, scheduler ->
|
||||
{[], scheduler}
|
||||
end)
|
||||
|
||||
pool_refs =
|
||||
for pid <- pool_list do
|
||||
ref = Process.monitor(pid)
|
||||
send(pid, :done)
|
||||
ref
|
||||
end
|
||||
|
||||
for ref <- pool_refs do
|
||||
receive do
|
||||
{:DOWN, ^ref, _, _, _} -> :ok
|
||||
end
|
||||
end
|
||||
|
||||
modules
|
||||
end)
|
||||
end
|
||||
|
||||
defp write_module_binaries(result, _output, _timestamp) do
|
||||
defp write_module_binaries(result, _output, _state) do
|
||||
for {{:module, module}, {binary, _}} when is_binary(binary) <- result, do: module
|
||||
end
|
||||
|
||||
defp write_loop(path, timestamp) do
|
||||
receive do
|
||||
{:write, module, binary} ->
|
||||
full_path = Path.join(path, Atom.to_string(module) <> ".beam")
|
||||
File.write!(full_path, binary, [:raw])
|
||||
if timestamp, do: File.touch!(full_path, timestamp)
|
||||
write_loop(path, timestamp)
|
||||
|
||||
:done ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
## Verification
|
||||
|
||||
defp verify_modules(result, compile_warnings, dependent_modules, state) do
|
||||
modules = write_module_binaries(result, state.output, state.beam_timestamp)
|
||||
_ = state.after_compile.()
|
||||
modules = write_module_binaries(result, state.output, state)
|
||||
profile(state, "after compile callback", state.after_compile)
|
||||
|
||||
runtime_warnings =
|
||||
if state.verification? do
|
||||
|
||||
@@ -1593,9 +1593,9 @@ defmodule Kernel.SpecialForms do
|
||||
|
||||
Let's give it a try on IEx:
|
||||
|
||||
iex> opts = %{width: 10, height: 15}
|
||||
iex> with {:ok, width} <- Map.fetch(opts, :width),
|
||||
...> {:ok, height} <- Map.fetch(opts, :height) do
|
||||
iex> opts = %{"width" => 10, "height" => 15}
|
||||
iex> with {:ok, width} <- Map.fetch(opts, "width"),
|
||||
...> {:ok, height} <- Map.fetch(opts, "height") do
|
||||
...> {:ok, width * height}
|
||||
...> end
|
||||
{:ok, 150}
|
||||
@@ -1603,21 +1603,13 @@ defmodule Kernel.SpecialForms do
|
||||
If all clauses match, the `do` block is executed, returning its result.
|
||||
Otherwise the chain is aborted and the non-matched value is returned:
|
||||
|
||||
iex> opts = %{width: 10}
|
||||
iex> with {:ok, width} <- Map.fetch(opts, :width),
|
||||
...> {:ok, height} <- Map.fetch(opts, :height) do
|
||||
iex> opts = %{"width" => 10}
|
||||
iex> with {:ok, width} <- Map.fetch(opts, "width"),
|
||||
...> {:ok, height} <- Map.fetch(opts, "height") do
|
||||
...> {:ok, width * height}
|
||||
...> end
|
||||
:error
|
||||
|
||||
Guards can be used in patterns as well:
|
||||
|
||||
iex> users = %{"melany" => "guest", "bob" => :admin}
|
||||
iex> with {:ok, role} when not is_binary(role) <- Map.fetch(users, "bob") do
|
||||
...> {:ok, to_string(role)}
|
||||
...> end
|
||||
{:ok, "admin"}
|
||||
|
||||
As in `for/1`, variables bound inside `with/1` won't be accessible
|
||||
outside of `with/1`.
|
||||
|
||||
@@ -1661,22 +1653,18 @@ defmodule Kernel.SpecialForms do
|
||||
An `else` option can be given to modify what is being returned from
|
||||
`with` in the case of a failed match:
|
||||
|
||||
iex> opts = %{width: 10}
|
||||
iex> with {:ok, width} <- Map.fetch(opts, :width),
|
||||
...> {:ok, height} <- Map.fetch(opts, :height) do
|
||||
...> {:ok, width * height}
|
||||
...> else
|
||||
...> :error ->
|
||||
...> {:error, :wrong_data}
|
||||
...>
|
||||
...> _other_error ->
|
||||
...> :unexpected_error
|
||||
...> end
|
||||
{:error, :wrong_data}
|
||||
with {:ok, content} <- File.read(path),
|
||||
:ok <- File.write(path, [content, "!"]) do
|
||||
:ok
|
||||
else
|
||||
{:error, reason} ->
|
||||
Logger.error("could not append ! to \#{path} with reason: \#{reason}")
|
||||
:error
|
||||
end
|
||||
|
||||
The `else` block works like a `case`: 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.
|
||||
and the first match will be used. Variables bound inside `with`
|
||||
(such as `content` in this example) are not available in the `else` block.
|
||||
|
||||
If an `else` block is used and there are no matching clauses, a `WithClauseError`
|
||||
exception is raised.
|
||||
@@ -1987,13 +1975,13 @@ defmodule Kernel.SpecialForms do
|
||||
While it is not possible to match against multiple patterns in a single
|
||||
clause, it's possible to match against multiple values by using guards:
|
||||
|
||||
iex> case :two do
|
||||
...> value when value in [:one, :two] ->
|
||||
iex> case 2 do
|
||||
...> value when value in [1, 2] ->
|
||||
...> "#{value} has been matched"
|
||||
...> :three ->
|
||||
...> "three has been matched"
|
||||
...> 3 ->
|
||||
...> "3 has been matched"
|
||||
...> end
|
||||
"two has been matched"
|
||||
"2 has been matched"
|
||||
"""
|
||||
defmacro case(condition, clauses), do: error!([condition, clauses])
|
||||
|
||||
@@ -2355,7 +2343,7 @@ 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
|
||||
Consumes the first message matching any of the given clauses in the current
|
||||
process mailbox.
|
||||
|
||||
If there is no matching message, the current process waits until a matching
|
||||
|
||||
@@ -877,7 +877,15 @@ defmodule Kernel.Typespec do
|
||||
|
||||
defp typespec({:fun, meta, args}, vars, caller, state) do
|
||||
{args, state} = :lists.mapfoldl(&typespec(&1, vars, caller, &2), state, args)
|
||||
{{:type, location(meta), :fun, args}, state}
|
||||
|
||||
if args != [] do
|
||||
IO.warn(
|
||||
"fun/#{length(args)} is not valid in typespecs. Either specify fun() or use (... -> return) instead",
|
||||
caller
|
||||
)
|
||||
end
|
||||
|
||||
{{:type, location(meta), :fun, []}, state}
|
||||
end
|
||||
|
||||
defp typespec({:..., _meta, _args}, _vars, caller, _state) do
|
||||
|
||||
@@ -126,10 +126,11 @@ defmodule Kernel.Utils do
|
||||
key == :__struct__ and raise(ArgumentError, "cannot set :__struct__ in struct definition")
|
||||
|
||||
try do
|
||||
:elixir_quote.escape(val, :none, false)
|
||||
:elixir_quote.escape(val, {:struct, module}, false)
|
||||
rescue
|
||||
e in [ArgumentError] ->
|
||||
raise ArgumentError, "invalid value for struct field #{key}, " <> Exception.message(e)
|
||||
raise ArgumentError,
|
||||
"invalid default value for struct field #{key}, " <> Exception.message(e)
|
||||
else
|
||||
_ -> {key, val}
|
||||
end
|
||||
@@ -171,7 +172,7 @@ defmodule Kernel.Utils do
|
||||
|
||||
:lists.foreach(foreach, enforce_keys)
|
||||
struct = :maps.from_list([__struct__: module] ++ fields)
|
||||
escaped_struct = :elixir_quote.escape(struct, :none, false)
|
||||
escaped_struct = :elixir_quote.escape(struct, {:struct, module}, false)
|
||||
|
||||
body =
|
||||
case bootstrapped? do
|
||||
@@ -217,7 +218,7 @@ defmodule Kernel.Utils do
|
||||
case enforce_keys -- :maps.keys(struct) do
|
||||
[] ->
|
||||
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)})
|
||||
|
||||
@@ -437,7 +437,7 @@ defmodule Keyword do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets the value from `key` and updates it, all in one pass.
|
||||
Gets the value for `key` and updates it in one pass, deleting duplicate keys.
|
||||
|
||||
The `fun` argument receives the value of `key` (or `nil` if `key`
|
||||
is not present) and must return a two-element tuple: the current value
|
||||
@@ -483,7 +483,7 @@ defmodule Keyword do
|
||||
defp get_and_update([{key, current} | t], acc, key, fun) do
|
||||
case fun.(current) do
|
||||
{get, value} ->
|
||||
{get, :lists.reverse(acc, [{key, value} | t])}
|
||||
{get, :lists.reverse(acc, [{key, value} | delete(t, key)])}
|
||||
|
||||
:pop ->
|
||||
{current, :lists.reverse(acc, t)}
|
||||
@@ -509,7 +509,8 @@ defmodule Keyword do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets the value under `key` and updates it. Raises if there is no `key`.
|
||||
Gets the value for `key` and updates it in one pass, deleting duplicate keys,
|
||||
raising if `key` can't be found in `keywords`.
|
||||
|
||||
The `fun` argument receives the value under `key` and must return a
|
||||
two-element tuple: the current value (the retrieved value, which can be
|
||||
@@ -545,21 +546,21 @@ defmodule Keyword do
|
||||
get_and_update!(keywords, key, fun, [])
|
||||
end
|
||||
|
||||
defp get_and_update!([{key, value} | keywords], key, fun, acc) do
|
||||
defp get_and_update!([{key, value} | t], key, fun, acc) do
|
||||
case fun.(value) do
|
||||
{get, value} ->
|
||||
{get, :lists.reverse(acc, [{key, value} | delete(keywords, key)])}
|
||||
{get, :lists.reverse(acc, [{key, value} | delete(t, key)])}
|
||||
|
||||
:pop ->
|
||||
{value, :lists.reverse(acc, keywords)}
|
||||
{value, :lists.reverse(acc, t)}
|
||||
|
||||
other ->
|
||||
raise "the given function must return a two-element tuple or :pop, got: #{inspect(other)}"
|
||||
end
|
||||
end
|
||||
|
||||
defp get_and_update!([{_, _} = e | keywords], key, fun, acc) do
|
||||
get_and_update!(keywords, key, fun, [e | acc])
|
||||
defp get_and_update!([{_, _} = h | t], key, fun, acc) do
|
||||
get_and_update!(t, key, fun, [h | acc])
|
||||
end
|
||||
|
||||
defp get_and_update!([], key, _fun, acc) when is_atom(key) do
|
||||
|
||||
@@ -187,9 +187,10 @@ defmodule List do
|
||||
"""
|
||||
@spec duplicate(any, 0) :: []
|
||||
@spec duplicate(elem, pos_integer) :: [elem, ...] when elem: var
|
||||
def duplicate(elem, n) do
|
||||
:lists.duplicate(n, elem)
|
||||
end
|
||||
def duplicate(elem, n) when is_integer(n) and n >= 0, do: duplicate(n, elem, [])
|
||||
|
||||
defp duplicate(0, _elem, acc), do: acc
|
||||
defp duplicate(n, elem, acc), do: duplicate(n - 1, elem, [elem | acc])
|
||||
|
||||
@doc """
|
||||
Flattens the given `list` of nested lists.
|
||||
@@ -915,7 +916,7 @@ defmodule List do
|
||||
|
||||
If `prefix` is an empty list, it returns `true`.
|
||||
|
||||
### Examples
|
||||
## Examples
|
||||
|
||||
iex> List.starts_with?([1, 2, 3], [1, 2])
|
||||
true
|
||||
@@ -945,7 +946,7 @@ defmodule List do
|
||||
|
||||
If `suffix` is an empty list, it returns `true`.
|
||||
|
||||
### Examples
|
||||
## Examples
|
||||
|
||||
iex> List.ends_with?([1, 2, 3], [2, 3])
|
||||
true
|
||||
|
||||
@@ -19,12 +19,6 @@ defprotocol List.Chars do
|
||||
"""
|
||||
@spec to_charlist(t) :: charlist
|
||||
def to_charlist(term)
|
||||
|
||||
@doc false
|
||||
@deprecated "Use List.Chars.to_charlist/1 instead"
|
||||
Kernel.def to_char_list(term) do
|
||||
__MODULE__.to_charlist(term)
|
||||
end
|
||||
end
|
||||
|
||||
defimpl List.Chars, for: Atom do
|
||||
|
||||
+156
-52
@@ -197,6 +197,16 @@ defmodule Macro do
|
||||
@typedoc "A captured remote function in the format of &Mod.fun/arity"
|
||||
@type captured_remote_function :: fun
|
||||
|
||||
@type escape_opts :: [
|
||||
unquote: boolean(),
|
||||
prune_metadata: boolean(),
|
||||
generated: boolean()
|
||||
]
|
||||
|
||||
@type inspect_atom_opts :: [
|
||||
escape: (binary(), char() -> binary())
|
||||
]
|
||||
|
||||
@doc """
|
||||
Breaks a pipeline expression into a list.
|
||||
|
||||
@@ -498,13 +508,11 @@ defmodule Macro do
|
||||
Generates AST nodes for a given number of required argument
|
||||
variables using `Macro.unique_var/2`.
|
||||
|
||||
The second argument is generally the macro caller's module.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> [var1, var2] = Macro.generate_unique_arguments(2, __MODULE__)
|
||||
iex> {:arg1, [counter: c1], __MODULE__} = var1
|
||||
iex> {:arg2, [counter: c2], __MODULE__} = var2
|
||||
iex> is_integer(c1) and is_integer(c2)
|
||||
true
|
||||
[var1, var2] = Macro.generate_unique_arguments(2, __CALLER__.module)
|
||||
|
||||
"""
|
||||
@doc since: "1.11.3"
|
||||
@@ -560,11 +568,11 @@ defmodule Macro do
|
||||
generate another variable, with its own unique counter.
|
||||
See `var/2` for an alternative.
|
||||
|
||||
The second argument is generally the macro caller's module.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> {:foo, [counter: c], __MODULE__} = Macro.unique_var(:foo, __MODULE__)
|
||||
iex> is_integer(c)
|
||||
true
|
||||
var = Macro.unique_var(:foo, __CALLER__.module)
|
||||
|
||||
"""
|
||||
@doc since: "1.11.3"
|
||||
@@ -793,12 +801,18 @@ defmodule Macro do
|
||||
* `:unquote` - when `true`, this function leaves `unquote/1` and
|
||||
`unquote_splicing/1` expressions 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. Note this option
|
||||
will give a special meaning to `quote`/`unquote` nodes, which need
|
||||
to be valid AST before escaping. Defaults to `false`.
|
||||
|
||||
* `:prune_metadata` - when `true`, removes most 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`.
|
||||
|
||||
* `:generated` - (since v1.19.0) Whether the AST should be considered as generated
|
||||
by the compiler or not. This means the compiler and tools like Dialyzer may not
|
||||
emit certain warnings.
|
||||
|
||||
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
|
||||
@@ -834,12 +848,82 @@ defmodule Macro do
|
||||
`escape/2` is used to escape *values* (either directly passed or variable
|
||||
bound), while `quote/2` produces syntax trees for
|
||||
expressions.
|
||||
|
||||
## Dealing with references and other runtime values
|
||||
|
||||
Macros work at compile-time and therefore `Macro.escape/1` can only escape values
|
||||
that are valid during compilation, such as numbers, atoms, tuples, maps, binaries,
|
||||
etc.
|
||||
|
||||
However, you may have values at compile-time which cannot be escaped, such as
|
||||
`reference`s and `pid`s, since the process or memory address they point to will
|
||||
no longer exist once compilation completes. Attempting to escape said values will
|
||||
raise an exception. This is a common issue when working with NIFs.
|
||||
|
||||
Luckily, Elixir v1.19 introduces a mechanism that allows those values to be escaped,
|
||||
as long as they are encapsulated by a struct within a module that defines the
|
||||
`__escape__/1` function. This is possible as long as the reference has a natural
|
||||
text or binary representation that can be serialized during compilation.
|
||||
|
||||
Let's imagine we have the following struct:
|
||||
|
||||
defmodule WrapperStruct do
|
||||
defstruct [:ref]
|
||||
|
||||
def new(...), do: %WrapperStruct{ref: ...}
|
||||
|
||||
# efficiently dump to / load from binaries
|
||||
def dump_to_binary(%WrapperStruct{ref: ref}), do: ...
|
||||
def load_from_binary(binary), do: %WrapperStruct{ref: ...}
|
||||
end
|
||||
|
||||
Such a struct could not be used in module attributes or escaped with `Macro.escape/2`:
|
||||
|
||||
defmodule Foo do
|
||||
@my_struct WrapperStruct.new(...)
|
||||
def my_struct, do: @my_struct
|
||||
end
|
||||
|
||||
** (ArgumentError) cannot inject attribute @my_struct into function/macro because cannot escape #Reference<...>
|
||||
|
||||
To address this, structs can re-define how they should be escaped by defining a custom
|
||||
`__escape__/1` function which returns the AST. In our example:
|
||||
|
||||
defmodule WrapperStruct do
|
||||
# ...
|
||||
|
||||
def __escape__(struct) do
|
||||
# dump to a binary representation at compile-time
|
||||
binary = dump_to_binary(struct)
|
||||
quote do
|
||||
# load from the binary representation at runtime
|
||||
WrapperStruct.load_from_binary(unquote(Macro.escape(binary)))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Now, our example above will be expanded as:
|
||||
|
||||
def my_struct, do: WrapperStruct.load_from_binary(<<...>>)
|
||||
|
||||
When implementing `__escape__/1`, you must ensure that the quoted expression
|
||||
will evaluate to a struct that represents the one given as argument.
|
||||
"""
|
||||
@spec escape(term, keyword) :: t()
|
||||
@spec escape(term, escape_opts) :: t()
|
||||
def escape(expr, opts \\ []) do
|
||||
unquote = Keyword.get(opts, :unquote, false)
|
||||
kind = if Keyword.get(opts, :prune_metadata, false), do: :prune_metadata, else: :none
|
||||
:elixir_quote.escape(expr, kind, unquote)
|
||||
kind = if Keyword.get(opts, :prune_metadata, false), do: :escape_and_prune, else: :escape
|
||||
generated = Keyword.get(opts, :generated, false)
|
||||
|
||||
case :elixir_quote.escape(expr, kind, unquote) do
|
||||
# mark module attrs as shallow-generated since the ast for their representation
|
||||
# might contain opaque terms
|
||||
{caller, meta, args} when generated and is_list(meta) ->
|
||||
{caller, [generated: true] ++ meta, args}
|
||||
|
||||
ast ->
|
||||
ast
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: Deprecate me on Elixir v1.22
|
||||
@@ -852,21 +936,40 @@ defmodule Macro do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Extracts the struct information (equivalent to calling
|
||||
`module.__info__(:struct)`).
|
||||
Extracts the struct information.
|
||||
|
||||
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.
|
||||
not have been compiled (including structs in the defined
|
||||
under the module being compiled). For compiled modules,
|
||||
it will invoke `module.__info__(:struct)`.
|
||||
|
||||
Calling this function also adds an export dependency on the
|
||||
given struct.
|
||||
|
||||
It will raise `ArgumentError` if the struct is not available.
|
||||
|
||||
## Compatibility considerations
|
||||
|
||||
This function currently returns both `:required` and `:default`
|
||||
entries for each field. While this naming is inconsistent
|
||||
(a required field should not have a default), this is done for
|
||||
backwards compatibility purposes.
|
||||
|
||||
In future releases, Elixir may introduce truly required struct
|
||||
fields, the required field will be removed and default will be
|
||||
present only if the field is optional. Your code should prepare
|
||||
for such scenario accordingly.
|
||||
"""
|
||||
@doc since: "1.18.0"
|
||||
@spec struct_info!(module(), Macro.Env.t()) ::
|
||||
[%{field: atom(), required: boolean(), default: term()}]
|
||||
[
|
||||
%{
|
||||
required(:field) => atom(),
|
||||
optional(:required) => boolean(),
|
||||
optional(: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
|
||||
@@ -1738,12 +1841,17 @@ 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
|
||||
compile-time.
|
||||
This is used when you want to dynamically invoke a function at
|
||||
compile-time and force it to be tracked as a compile-time dependency.
|
||||
For example, this is used by `dbg/1` to force the `dbg_callback`
|
||||
configuration to be a compile-time dependency.
|
||||
|
||||
If you want to "invoke" a macro instead, remember macros are by
|
||||
definition compile-time, and you can use `Macro.expand/2`.
|
||||
"""
|
||||
@doc since: "1.16.0"
|
||||
def compile_apply(mod, fun, args, caller) do
|
||||
:elixir_env.trace({:remote_macro, [], mod, fun, length(args)}, caller)
|
||||
:elixir_env.trace({:remote_function, [], mod, fun, length(args)}, %{caller | function: nil})
|
||||
Kernel.apply(mod, fun, args)
|
||||
end
|
||||
|
||||
@@ -2399,7 +2507,7 @@ defmodule Macro do
|
||||
|
||||
"""
|
||||
@doc since: "1.14.0"
|
||||
@spec inspect_atom(:literal | :key | :remote_call, atom, keyword) :: binary
|
||||
@spec inspect_atom(:literal | :key | :remote_call, atom, inspect_atom_opts) :: binary
|
||||
def inspect_atom(source_format, atom, opts \\ [])
|
||||
|
||||
def inspect_atom(:literal, atom, _opts) when is_nil(atom) or is_boolean(atom) do
|
||||
@@ -2591,7 +2699,7 @@ defmodule Macro do
|
||||
:ok
|
||||
end
|
||||
|
||||
prelude = quote do: options = unquote(Macro.escape(options))
|
||||
prelude = quote do: options = unquote(options)
|
||||
acc = {prelude, dbg_format_header(env)}
|
||||
|
||||
{acc, nil} =
|
||||
@@ -2612,34 +2720,29 @@ defmodule Macro do
|
||||
# Pipelines.
|
||||
defp dbg_ast_to_debuggable({:|>, _meta, _args} = pipe_ast, _env) do
|
||||
value_var = unique_var(:value, __MODULE__)
|
||||
values_acc_var = unique_var(:values, __MODULE__)
|
||||
|
||||
[start_ast | rest_asts] = asts = for {ast, 0} <- unpipe(pipe_ast), do: ast
|
||||
rest_asts = Enum.map(rest_asts, &pipe(value_var, &1, 0))
|
||||
[start_ast | rest_asts] = for {ast, 0} <- unpipe(pipe_ast), do: ast
|
||||
piped_rest_asts = Enum.map(rest_asts, &{&1, pipe(value_var, &1, 0)})
|
||||
|
||||
initial_acc =
|
||||
first_entry =
|
||||
quote do
|
||||
unquote(value_var) = unquote(start_ast)
|
||||
unquote(values_acc_var) = [unquote(value_var)]
|
||||
{:multi_value, unquote(escape(start_ast)), unquote(value_var)}
|
||||
end
|
||||
|
||||
values_ast =
|
||||
for step_ast <- rest_asts, reduce: initial_acc do
|
||||
ast_acc ->
|
||||
quote do
|
||||
unquote(ast_acc)
|
||||
unquote(value_var) = unquote(step_ast)
|
||||
unquote(values_acc_var) = [unquote(value_var) | unquote(values_acc_var)]
|
||||
end
|
||||
end
|
||||
len = length(piped_rest_asts)
|
||||
|
||||
[
|
||||
quote do
|
||||
unquote(values_ast)
|
||||
pipe_entries =
|
||||
Enum.with_index(piped_rest_asts, fn {original_ast, step_ast}, i ->
|
||||
tag = if i + 1 == len, do: :pipe_end, else: :pipe
|
||||
|
||||
{:pipe, unquote(escape(asts)), Enum.reverse(unquote(values_acc_var))}
|
||||
end
|
||||
]
|
||||
quote do
|
||||
unquote(value_var) = unquote(step_ast)
|
||||
{unquote(tag), unquote(escape(original_ast)), unquote(value_var)}
|
||||
end
|
||||
end)
|
||||
|
||||
[first_entry | pipe_entries]
|
||||
end
|
||||
|
||||
dbg_decomposed_binary_operators = [:&&, :||, :and, :or]
|
||||
@@ -2859,18 +2962,19 @@ defmodule Macro do
|
||||
result
|
||||
end
|
||||
|
||||
defp dbg_format_ast_to_debug({:pipe, code_asts, values}, options) do
|
||||
result = List.last(values)
|
||||
code_strings = Enum.map(code_asts, &to_string_with_colors(&1, options))
|
||||
[{first_ast, first_value} | asts_with_values] = Enum.zip(code_strings, values)
|
||||
first_formatted = [dbg_format_ast(first_ast), " ", inspect(first_value, options), ?\n]
|
||||
defp dbg_format_ast_to_debug({:pipe, code_ast, value}, options) do
|
||||
formatted = [
|
||||
[:faint, "|> ", :reset],
|
||||
dbg_format_ast_with_value_no_newline(code_ast, value, options)
|
||||
]
|
||||
|
||||
rest_formatted =
|
||||
Enum.map(asts_with_values, fn {code_ast, value} ->
|
||||
[:faint, "|> ", :reset, dbg_format_ast(code_ast), " ", inspect(value, options), ?\n]
|
||||
end)
|
||||
{formatted, value}
|
||||
end
|
||||
|
||||
{[first_formatted | rest_formatted], result}
|
||||
defp dbg_format_ast_to_debug({:pipe_end, code_ast, value}, options) do
|
||||
{formatted, value} = dbg_format_ast_to_debug({:pipe, code_ast, value}, options)
|
||||
|
||||
{[formatted, ?\n], value}
|
||||
end
|
||||
|
||||
defp dbg_format_ast_to_debug({:case_argument, expr_ast, expr_value}, options) do
|
||||
|
||||
+64
-13
@@ -70,6 +70,42 @@ defmodule Macro.Env do
|
||||
@typep tracers :: [module]
|
||||
@typep versioned_vars :: %{optional(variable) => var_version :: non_neg_integer}
|
||||
|
||||
@type define_import_opts :: [
|
||||
trace: boolean(),
|
||||
emit_warnings: boolean(),
|
||||
info_callback: (atom() -> [{atom(), arity()}]),
|
||||
only: :functions | :macros | [{atom(), arity()}],
|
||||
except: [{atom(), arity()}],
|
||||
warn: boolean()
|
||||
]
|
||||
|
||||
@type define_alias_opts :: [
|
||||
trace: boolean(),
|
||||
as: atom(),
|
||||
warn: boolean()
|
||||
]
|
||||
|
||||
@type define_require_opts :: [
|
||||
trace: boolean(),
|
||||
as: atom(),
|
||||
warn: boolean()
|
||||
]
|
||||
|
||||
@type expand_alias_opts :: [
|
||||
trace: boolean()
|
||||
]
|
||||
|
||||
@type expand_import_opts :: [
|
||||
allow_locals: boolean() | (-> function() | false),
|
||||
check_deprecations: boolean(),
|
||||
trace: boolean()
|
||||
]
|
||||
|
||||
@type expand_require_opts :: [
|
||||
check_deprecations: boolean(),
|
||||
trace: boolean()
|
||||
]
|
||||
|
||||
@type t :: %{
|
||||
__struct__: __MODULE__,
|
||||
aliases: aliases,
|
||||
@@ -264,7 +300,7 @@ defmodule Macro.Env do
|
||||
|
||||
iex> Macro.Env.required?(__ENV__, Integer)
|
||||
false
|
||||
iex> require Integer
|
||||
iex> require Integer, warn: false
|
||||
iex> Macro.Env.required?(__ENV__, Integer)
|
||||
true
|
||||
|
||||
@@ -331,7 +367,7 @@ defmodule Macro.Env do
|
||||
|
||||
"""
|
||||
@doc since: "1.17.0"
|
||||
@spec define_require(t, Macro.metadata(), module) :: {:ok, t}
|
||||
@spec define_require(t, Macro.metadata(), module, define_require_opts) :: {: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)
|
||||
@@ -391,7 +427,8 @@ defmodule Macro.Env do
|
||||
|
||||
"""
|
||||
@doc since: "1.17.0"
|
||||
@spec define_import(t, Macro.metadata(), module, keyword) :: {:ok, t} | {:error, String.t()}
|
||||
@spec define_import(t, Macro.metadata(), module, define_import_opts) ::
|
||||
{: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)
|
||||
@@ -441,7 +478,8 @@ defmodule Macro.Env do
|
||||
|
||||
"""
|
||||
@doc since: "1.17.0"
|
||||
@spec define_alias(t, Macro.metadata(), module, keyword) :: {:ok, t} | {:error, String.t()}
|
||||
@spec define_alias(t, Macro.metadata(), module, define_alias_opts) ::
|
||||
{: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)
|
||||
@@ -487,7 +525,7 @@ defmodule Macro.Env do
|
||||
|
||||
"""
|
||||
@doc since: "1.17.0"
|
||||
@spec expand_alias(t, keyword, [atom()], keyword) ::
|
||||
@spec expand_alias(t, keyword, [atom()], expand_alias_opts) ::
|
||||
{:alias, atom()} | :error
|
||||
def expand_alias(env, meta, list, opts \\ [])
|
||||
when is_list(meta) and is_list(list) and is_list(opts) do
|
||||
@@ -517,8 +555,15 @@ defmodule Macro.Env do
|
||||
|
||||
## Options
|
||||
|
||||
* `:allow_locals` - when set to `false`, it does not attempt to capture
|
||||
local macros defined in the current module in `env`
|
||||
* `:allow_locals` - controls how local macros are resolved.
|
||||
Defaults to `true`.
|
||||
|
||||
- When `false`, does not attempt to capture local macros defined in the
|
||||
current module in `env`
|
||||
- When `true`, uses a default resolver that looks for public macros in
|
||||
the current module
|
||||
- When a function, it will be invoked to lazily compute a local function
|
||||
(or return false). It has signature `(-> function() | false)`
|
||||
|
||||
* `:check_deprecations` - when set to `false`, does not check for deprecations
|
||||
when expanding macros
|
||||
@@ -527,7 +572,7 @@ defmodule Macro.Env do
|
||||
|
||||
"""
|
||||
@doc since: "1.17.0"
|
||||
@spec expand_import(t, keyword, atom(), arity(), keyword) ::
|
||||
@spec expand_import(t, keyword, atom(), arity(), expand_import_opts) ::
|
||||
{:macro, module(), (Macro.metadata(), args :: [Macro.t()] -> Macro.t())}
|
||||
| {:function, module(), atom()}
|
||||
| {:error, :not_found | {:conflict, module()} | {:ambiguous, [module()]}}
|
||||
@@ -542,10 +587,16 @@ defmodule Macro.Env do
|
||||
trace = Keyword.get(opts, :trace, true)
|
||||
module = env.module
|
||||
|
||||
# When allow_locals is a callback, we don't need to pass module macros as extra
|
||||
# because the callback will handle local macro resolution
|
||||
extra =
|
||||
case allow_locals and function_exported?(module, :__info__, 1) do
|
||||
true -> [{module, module.__info__(:macros)}]
|
||||
false -> []
|
||||
if is_function(allow_locals, 0) do
|
||||
[]
|
||||
else
|
||||
case allow_locals and function_exported?(module, :__info__, 1) do
|
||||
true -> [{module, module.__info__(:macros)}]
|
||||
false -> []
|
||||
end
|
||||
end
|
||||
|
||||
case :elixir_dispatch.expand_import(meta, name, arity, env, extra, allow_locals, trace) do
|
||||
@@ -583,7 +634,7 @@ defmodule Macro.Env do
|
||||
|
||||
"""
|
||||
@doc since: "1.17.0"
|
||||
@spec expand_require(t, keyword, module(), atom(), arity(), keyword) ::
|
||||
@spec expand_require(t, keyword, module(), atom(), arity(), expand_require_opts) ::
|
||||
{:macro, module(), (Macro.metadata(), args :: [Macro.t()] -> Macro.t())}
|
||||
| :error
|
||||
def expand_require(env, meta, module, name, arity, opts \\ [])
|
||||
@@ -606,7 +657,7 @@ defmodule Macro.Env do
|
||||
:elixir_dispatch.check_deprecated(:macro, meta, receiver, name, arity, env)
|
||||
end
|
||||
|
||||
quoted = expander.(args, env)
|
||||
quoted = expander.(:elixir_dispatch.stop_generated(args), env)
|
||||
next = :elixir_module.next_counter(env.module)
|
||||
:elixir_quote.linify_with_context_counter(expansion_meta, {receiver, next}, quoted)
|
||||
end
|
||||
|
||||
+91
-49
@@ -287,8 +287,13 @@ defmodule Map do
|
||||
@doc """
|
||||
Fetches the value for a specific `key` in the given `map`.
|
||||
|
||||
If `map` contains the given `key` then its value is returned in the shape of `{:ok, value}`.
|
||||
If `map` doesn't contain `key`, `:error` is returned.
|
||||
If `map` contains the given `key` then its value is returned
|
||||
in the shape of `{:ok, value}`. If `map` doesn't contain `key`,
|
||||
`:error` is returned.
|
||||
|
||||
If the type system can verify `:error` is always returned
|
||||
(which means key is never available in the map), it will emit
|
||||
an error.
|
||||
|
||||
Inlined by the compiler.
|
||||
|
||||
@@ -296,7 +301,7 @@ defmodule Map do
|
||||
|
||||
iex> Map.fetch(%{a: 1}, :a)
|
||||
{:ok, 1}
|
||||
iex> Map.fetch(%{a: 1}, :b)
|
||||
iex> Map.fetch(%{"foo" => "bar"}, "unknown")
|
||||
:error
|
||||
|
||||
"""
|
||||
@@ -307,8 +312,11 @@ defmodule Map do
|
||||
Fetches the value for a specific `key` in the given `map`, erroring out if
|
||||
`map` doesn't contain `key`.
|
||||
|
||||
If `map` contains `key`, the corresponding value is returned. If
|
||||
`map` doesn't contain `key`, a `KeyError` exception is raised.
|
||||
The exclamation mark (`!`) implies this function can raise a `KeyError`
|
||||
exception at runtime if `map` doesn't contain `key`. If the type system
|
||||
can verify this function will always raise (which means the key is never
|
||||
available), then it will emit a warning at compile-time. See the "Type
|
||||
checking" section below.
|
||||
|
||||
Inlined by the compiler.
|
||||
|
||||
@@ -317,11 +325,54 @@ defmodule Map do
|
||||
iex> Map.fetch!(%{a: 1}, :a)
|
||||
1
|
||||
|
||||
When the key is missing, an exception is raised:
|
||||
|
||||
Map.fetch!(%{a: 1}, :b)
|
||||
** (KeyError) key :b not found in: %{a: 1}
|
||||
|
||||
## Type checking
|
||||
|
||||
The compiler will emit a warning if it can verify that
|
||||
none of the keys given are available in the map.
|
||||
|
||||
When the key is an atom, because only single key is given,
|
||||
a warning will be emitted in case the type system proves
|
||||
the key is not present.
|
||||
|
||||
However, this behaviour matters when the type of the key
|
||||
represents multiple values. For example:
|
||||
|
||||
key = returns_foo_or_bar() #=> :foo or :bar
|
||||
Map.fetch!(%{foo: 123}, key)
|
||||
|
||||
Although the key can be `:foo` or `:bar`, there is no
|
||||
warning emitted, as `:foo` will succeed. This is by design:
|
||||
the exclamation mark in Elixir denotes precisely that a
|
||||
runtime exception may be raised.
|
||||
|
||||
In case you are looking up multiple keys and you don't know
|
||||
if they may be present, you can use `Map.fetch/2` instead
|
||||
and deal with the error case accordingly:
|
||||
|
||||
case Map.fetch(%{foo: 123}, key) do
|
||||
{:ok, value} -> ...
|
||||
:error -> ...
|
||||
end
|
||||
|
||||
Both `Map.fetch!/2` and `Map.fetch/2` will emit a warning if
|
||||
it proves that both `:foo` or `:bar` are absent in the map.
|
||||
|
||||
Alternatively, if you want to statically prove that all of keys
|
||||
are in the map, you can match on the possible values and access
|
||||
them directly:
|
||||
|
||||
case returns_foo_or_bar() do
|
||||
:foo -> map.foo
|
||||
:bar -> map.bar
|
||||
end
|
||||
"""
|
||||
@spec fetch!(map, key) :: value
|
||||
def fetch!(map, key) do
|
||||
:maps.get(key, map)
|
||||
end
|
||||
def fetch!(map, key), do: :maps.get(key, map)
|
||||
|
||||
@doc """
|
||||
Puts the given `value` under `key` unless the entry `key`
|
||||
@@ -357,8 +408,8 @@ defmodule Map do
|
||||
iex> Map.replace(%{a: 1, b: 2}, :a, 3)
|
||||
%{a: 3, b: 2}
|
||||
|
||||
iex> Map.replace(%{a: 1}, :b, 2)
|
||||
%{a: 1}
|
||||
iex> Map.replace(%{"a" => 1}, "b", 2)
|
||||
%{"a" => 1}
|
||||
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
@@ -379,7 +430,11 @@ defmodule Map do
|
||||
@doc """
|
||||
Puts a value under `key` only if the `key` already exists in `map`.
|
||||
|
||||
If `key` is not present in `map`, a `KeyError` exception is raised.
|
||||
The exclamation mark (`!`) implies this function can raise a `KeyError`
|
||||
exception at runtime if `map` doesn't contain `key`. If the type system
|
||||
can verify this function will always raise (which means the key is never
|
||||
available), then it will emit a warning at compile-time. See the "Type
|
||||
checking" section in `Map.fetch!/2` for more information.
|
||||
|
||||
Inlined by the compiler.
|
||||
|
||||
@@ -388,8 +443,8 @@ defmodule Map do
|
||||
iex> Map.replace!(%{a: 1, b: 2}, :a, 3)
|
||||
%{a: 3, b: 2}
|
||||
|
||||
iex> Map.replace!(%{a: 1}, :b, 2)
|
||||
** (KeyError) key :b not found in:
|
||||
iex> Map.replace!(%{"foo" => "bar"}, "unknown", "new_bar")
|
||||
** (KeyError) key "unknown" not found in:
|
||||
...
|
||||
|
||||
"""
|
||||
@@ -412,8 +467,8 @@ defmodule Map do
|
||||
iex> Map.replace_lazy(%{a: 1, b: 2}, :a, fn v -> v * 4 end)
|
||||
%{a: 4, b: 2}
|
||||
|
||||
iex> Map.replace_lazy(%{a: 1, b: 2}, :c, fn v -> v * 4 end)
|
||||
%{a: 1, b: 2}
|
||||
iex> Map.replace_lazy(%{"a" => 1, "b" => 2}, "c", fn v -> v * 4 end)
|
||||
%{"a" => 1, "b" => 2}
|
||||
|
||||
"""
|
||||
@doc since: "1.14.0"
|
||||
@@ -516,15 +571,13 @@ defmodule Map do
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Map.get(%{}, :a)
|
||||
nil
|
||||
iex> Map.get(%{a: 1}, :a)
|
||||
iex> Map.get(%{"a" => 1}, "a")
|
||||
1
|
||||
iex> Map.get(%{a: 1}, :b)
|
||||
iex> Map.get(%{"a" => 1}, "b")
|
||||
nil
|
||||
iex> Map.get(%{a: 1}, :b, 3)
|
||||
iex> Map.get(%{"a" => 1}, "b", 3)
|
||||
3
|
||||
iex> Map.get(%{a: nil}, :a, 1)
|
||||
iex> Map.get(%{"a" => nil}, "a", 1)
|
||||
nil
|
||||
|
||||
"""
|
||||
@@ -553,15 +606,11 @@ defmodule Map do
|
||||
|
||||
## Examples
|
||||
|
||||
iex> map = %{a: 1}
|
||||
iex> fun = fn ->
|
||||
...> # some expensive operation here
|
||||
...> 13
|
||||
...> end
|
||||
iex> Map.get_lazy(map, :a, fun)
|
||||
iex> Map.get_lazy(%{a: 1}, :a, fn -> :expensive_value end)
|
||||
1
|
||||
iex> Map.get_lazy(map, :b, fun)
|
||||
13
|
||||
|
||||
iex> Map.get_lazy(%{"a" => 1}, "b", fn -> :expensive_value end)
|
||||
:expensive_value
|
||||
|
||||
"""
|
||||
@spec get_lazy(map, key, (-> value)) :: value
|
||||
@@ -700,10 +749,10 @@ defmodule Map do
|
||||
|
||||
iex> Map.pop(%{a: 1}, :a)
|
||||
{1, %{}}
|
||||
iex> Map.pop(%{a: 1}, :b)
|
||||
{nil, %{a: 1}}
|
||||
iex> Map.pop(%{a: 1}, :b, 3)
|
||||
{3, %{a: 1}}
|
||||
iex> Map.pop(%{"a" => 1}, "b")
|
||||
{nil, %{"a" => 1}}
|
||||
iex> Map.pop(%{"a" => 1}, "b", 3)
|
||||
{3, %{"a" => 1}}
|
||||
|
||||
"""
|
||||
@spec pop(map, key, default) :: {value, updated_map :: map} | {default, map} when default: value
|
||||
@@ -726,8 +775,8 @@ defmodule Map do
|
||||
{1, %{}}
|
||||
iex> Map.pop!(%{a: 1, b: 2}, :a)
|
||||
{1, %{b: 2}}
|
||||
iex> Map.pop!(%{a: 1}, :b)
|
||||
** (KeyError) key :b not found in:
|
||||
iex> Map.pop!(%{"a" => 1}, "b")
|
||||
** (KeyError) key "b" not found in:
|
||||
...
|
||||
|
||||
"""
|
||||
@@ -753,15 +802,11 @@ defmodule Map do
|
||||
|
||||
## Examples
|
||||
|
||||
iex> map = %{a: 1}
|
||||
iex> fun = fn ->
|
||||
...> # some expensive operation here
|
||||
...> 13
|
||||
...> end
|
||||
iex> Map.pop_lazy(map, :a, fun)
|
||||
iex> Map.pop_lazy(%{a: 1}, :a, fn -> :expensive_value end)
|
||||
{1, %{}}
|
||||
iex> Map.pop_lazy(map, :b, fun)
|
||||
{13, %{a: 1}}
|
||||
|
||||
iex> Map.pop_lazy(%{"a" => 1}, "b", fn -> :expensive_value end)
|
||||
{:expensive_value, %{"a" => 1}}
|
||||
|
||||
"""
|
||||
@spec pop_lazy(map, key, (-> value)) :: {value, map}
|
||||
@@ -913,8 +958,8 @@ defmodule Map do
|
||||
iex> Map.update!(%{a: 1}, :a, &(&1 * 2))
|
||||
%{a: 2}
|
||||
|
||||
iex> Map.update!(%{a: 1}, :b, &(&1 * 2))
|
||||
** (KeyError) key :b not found in:
|
||||
iex> Map.update!(%{"a" => 1}, "b", &(&1 * 2))
|
||||
** (KeyError) key "b" not found in:
|
||||
...
|
||||
|
||||
"""
|
||||
@@ -1033,6 +1078,7 @@ defmodule Map do
|
||||
#=> %{name: "john"}
|
||||
|
||||
"""
|
||||
# TODO: implement this using row polymorphism
|
||||
@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")
|
||||
@@ -1069,11 +1115,7 @@ defmodule Map do
|
||||
|
||||
"""
|
||||
@spec equal?(map, map) :: boolean
|
||||
def equal?(map1, map2)
|
||||
|
||||
def equal?(%{} = map1, %{} = map2), do: map1 === map2
|
||||
def equal?(%{} = map1, map2), do: :erlang.error({:badmap, map2}, [map1, map2])
|
||||
def equal?(term, other), do: :erlang.error({:badmap, term}, [term, other])
|
||||
|
||||
@doc false
|
||||
@deprecated "Use Kernel.map_size/1 instead"
|
||||
|
||||
@@ -55,7 +55,10 @@ defmodule MapSet do
|
||||
|
||||
@type value :: term
|
||||
|
||||
@opaque internal(value) :: :sets.set(value)
|
||||
# We don't use opaque because MapSets can be inlined,
|
||||
# either via module attributes or by the compiler.
|
||||
@typep internal(value) :: :sets.set(value)
|
||||
|
||||
@type t(value) :: %__MODULE__{map: internal(value)}
|
||||
@type t :: t(term)
|
||||
|
||||
|
||||
+38
-25
@@ -402,30 +402,21 @@ defmodule Module do
|
||||
|
||||
Accepts the function name (as an atom) of a function in the current module.
|
||||
The function must have an arity of 0 (no arguments). If the function does
|
||||
not return `:ok`, the loading of the module will be aborted.
|
||||
For example:
|
||||
not return `:ok`, the loading of the module will be aborted. Its primary
|
||||
use case is to load [NIFs](https://www.erlang.org/doc/man/erl_nif):
|
||||
|
||||
defmodule MyModule do
|
||||
@on_load :load_check
|
||||
@on_load :load_external_code
|
||||
|
||||
def load_check do
|
||||
if some_condition() do
|
||||
:ok
|
||||
else
|
||||
:abort
|
||||
end
|
||||
end
|
||||
|
||||
def some_condition do
|
||||
false
|
||||
def load_external_code do
|
||||
:erlang.load_nif(~c"path/to/extension.so_or_dll")
|
||||
end
|
||||
end
|
||||
|
||||
The function given to `on_load` should avoid calling functions from
|
||||
other modules. If you must call functions in other modules and those
|
||||
modules are defined within the same project, the called modules must
|
||||
have the `@compile {:autoload, true}` annotation, so they are loaded
|
||||
upfront (and not from within the `@on_load` callback).
|
||||
other modules. This is because, when running a `mix release`,
|
||||
`on_load` runs extremely early, before any application starts running,
|
||||
and therefore even systems like the `Logger` and `IO` are not yet available.
|
||||
|
||||
### `@vsn`
|
||||
|
||||
@@ -560,15 +551,20 @@ defmodule Module do
|
||||
callback is invoked under different scenarios, Elixir provides no guarantees
|
||||
of when in the compilation cycle nor in which process the callback runs.
|
||||
|
||||
Furthermore, after verification callbacks are not expected to raise.
|
||||
Given they run after the code is compiled, artifacts have already been
|
||||
written to disk, and therefore raising does not effectively halt compilation
|
||||
and may leave unused artifacts on disk. If you must raise, use `@after_compile`
|
||||
or other callback. Given modules have already been compiled, functions in
|
||||
this module, such as `get_attribute/2`, which expect modules to not have been
|
||||
yet compiled, do not work on `@after_verify` callback.
|
||||
|
||||
Accepts a module or a `{module, function_name}` tuple. The function
|
||||
must take one argument: the module name. When just a module is provided,
|
||||
the function is assumed to be `__after_verify__/1`.
|
||||
|
||||
Callbacks will run in the order they are registered.
|
||||
|
||||
`Module` functions expecting not yet compiled modules are no longer available
|
||||
at the time `@after_verify` is invoked.
|
||||
|
||||
#### Example
|
||||
|
||||
defmodule MyModule do
|
||||
@@ -681,12 +677,21 @@ defmodule Module do
|
||||
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}
|
||||
@type def_kind :: :def | :defp | :defmacro | :defmacrop
|
||||
|
||||
@type create_opts :: [
|
||||
file: binary(),
|
||||
line: pos_integer(),
|
||||
generated: boolean()
|
||||
]
|
||||
|
||||
@type get_definition_opts :: [
|
||||
skip_clauses: boolean()
|
||||
]
|
||||
|
||||
@extra_error_msg_defines? "Use Kernel.function_exported?/3 and Kernel.macro_exported?/3 " <>
|
||||
"to check for public functions and macros instead"
|
||||
|
||||
@@ -711,7 +716,8 @@ defmodule Module do
|
||||
|
||||
* `:module` - the module atom name
|
||||
|
||||
* `:struct` - (since v1.14.0) if the module defines a struct and if so each field in order
|
||||
* `:struct` - (since v1.14.0) if the module defines a struct and if so each field in order.
|
||||
See `Macro.struct_info!/2` for more information
|
||||
|
||||
"""
|
||||
@callback __info__(:attributes) :: keyword()
|
||||
@@ -721,7 +727,14 @@ defmodule Module do
|
||||
@callback __info__(:md5) :: binary()
|
||||
@callback __info__(:module) :: module()
|
||||
@callback __info__(:struct) ::
|
||||
list(%{required(:field) => atom(), optional(:default) => term()}) | nil
|
||||
[
|
||||
%{
|
||||
required(:field) => atom(),
|
||||
optional(:required) => boolean(),
|
||||
optional(:default) => term()
|
||||
}
|
||||
]
|
||||
| nil
|
||||
|
||||
@doc """
|
||||
Returns information about module attributes used by Elixir.
|
||||
@@ -918,7 +931,7 @@ defmodule Module do
|
||||
when defining the module, while `Kernel.defmodule/2`
|
||||
automatically uses the environment it is invoked at.
|
||||
"""
|
||||
@spec create(module, Macro.t(), Macro.Env.t() | keyword) :: {:module, module, binary, term}
|
||||
@spec create(module, Macro.t(), Macro.Env.t() | create_opts) :: {:module, module, binary, term}
|
||||
def create(module, quoted, opts)
|
||||
|
||||
def create(module, quoted, %Macro.Env{} = env) when is_atom(module) do
|
||||
@@ -1423,7 +1436,7 @@ defmodule Module do
|
||||
only an interest in fetching the kind and the metadata
|
||||
|
||||
"""
|
||||
@spec get_definition(module, definition, keyword) ::
|
||||
@spec get_definition(module, definition, get_definition_opts) ::
|
||||
{:v1, def_kind, meta :: keyword,
|
||||
[{meta :: keyword, arguments :: [Macro.t()], guards :: [Macro.t()], Macro.t()}]}
|
||||
| nil
|
||||
|
||||
@@ -181,8 +181,16 @@ defmodule Module.Behaviour do
|
||||
behaviour not in behaviours ->
|
||||
{:error, {:behaviour_not_declared, behaviour}}
|
||||
|
||||
not Code.ensure_loaded?(behaviour) ->
|
||||
# Module does not exist, but we have already warned about it.
|
||||
{:ok, []}
|
||||
|
||||
not behaviour_defined?(callbacks, behaviour) ->
|
||||
# Module does not define behaviour, but we have already warned about it.
|
||||
{:ok, []}
|
||||
|
||||
true ->
|
||||
{:error, {:behaviour_not_defined, behaviour, callbacks}}
|
||||
{:error, {:callback_not_defined, behaviour, callbacks}}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -215,6 +223,18 @@ defmodule Module.Behaviour do
|
||||
end
|
||||
end
|
||||
|
||||
# Determines whether there is at least one callback defined for the given behaviour.
|
||||
# If not, that means that the behaviour has not been defined.
|
||||
defp behaviour_defined?(callbacks, behaviour) do
|
||||
callbacks
|
||||
|> Map.values()
|
||||
|> List.flatten()
|
||||
|> Enum.any?(fn
|
||||
{_kind, ^behaviour, _optional?} -> true
|
||||
{_kind, _behaviour, _optional?} -> false
|
||||
end)
|
||||
end
|
||||
|
||||
defp warn_missing_impls(%{callbacks: callbacks} = context, _impl_contexts, _defs)
|
||||
when map_size(callbacks) == 0 do
|
||||
context
|
||||
@@ -390,13 +410,17 @@ defmodule Module.Behaviour do
|
||||
]
|
||||
end
|
||||
|
||||
defp format_warning({:behaviour_not_defined, callback, kind, behaviour, callbacks}) do
|
||||
defp format_warning({:callback_not_defined, callback, kind, behaviour, callbacks}) do
|
||||
behaviour_string = inspect(behaviour)
|
||||
|
||||
[
|
||||
"got \"@impl ",
|
||||
inspect(behaviour),
|
||||
behaviour_string,
|
||||
"\" for ",
|
||||
format_definition(kind, callback),
|
||||
" but this behaviour does not specify such callback",
|
||||
" but ",
|
||||
behaviour_string,
|
||||
" does not specify such callback",
|
||||
known_callbacks(callbacks)
|
||||
]
|
||||
end
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
defmodule Module.ParallelChecker do
|
||||
@moduledoc false
|
||||
@elixir_checker_version :elixir_erl.checker_version()
|
||||
|
||||
import Kernel, except: [spawn: 3]
|
||||
|
||||
@@ -11,9 +12,20 @@ defmodule Module.ParallelChecker do
|
||||
@type warning() :: term()
|
||||
@type mode() :: :erlang | :elixir | :protocol
|
||||
|
||||
@typedoc """
|
||||
Options for `start_link/1`.
|
||||
"""
|
||||
@type start_link_opts :: [
|
||||
{:max_concurrency, pos_integer()}
|
||||
| {:long_verification_threshold, pos_integer()}
|
||||
| {:each_long_verification, (module() -> term()) | (module(), pid() -> term())}
|
||||
| {atom(), term()}
|
||||
]
|
||||
|
||||
@doc """
|
||||
Initializes the parallel checker process.
|
||||
"""
|
||||
@spec start_link(start_link_opts()) :: {:ok, cache()}
|
||||
def start_link(opts \\ []) do
|
||||
:proc_lib.start_link(__MODULE__, :init, [opts])
|
||||
end
|
||||
@@ -51,14 +63,14 @@ defmodule Module.ParallelChecker do
|
||||
@doc """
|
||||
Spawns a process that runs the parallel checker.
|
||||
"""
|
||||
def spawn({pid, {checker, table}}, module, module_map, beam_location, log?) do
|
||||
def spawn({pid, {checker, table}}, module, module_map, signatures, beam_location, log?) do
|
||||
# Protocols may have been consolidated. So if we know their beam location,
|
||||
# we discard their module map on purpose and start from file.
|
||||
info =
|
||||
if beam_location != [] and Keyword.has_key?(module_map.attributes, :__protocol__) do
|
||||
List.to_string(beam_location)
|
||||
else
|
||||
cache_from_module_map(table, module_map)
|
||||
cache_from_module_map(table, module_map, signatures)
|
||||
end
|
||||
|
||||
inner_spawn(pid, checker, table, module, info, log?)
|
||||
@@ -85,10 +97,12 @@ defmodule Module.ParallelChecker do
|
||||
end
|
||||
|
||||
with {:ok, binary} <- File.read(location),
|
||||
{: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)
|
||||
{:ok,
|
||||
{_, [{:debug_info, {:debug_info_v1, backend, data}}, {~c"ExCk", checker}]}} <-
|
||||
:beam_lib.chunks(binary, [:debug_info, ~c"ExCk"]),
|
||||
{:ok, module_map} <- backend.debug_info(:elixir_v1, module, data, []),
|
||||
{@elixir_checker_version, contents} <- :erlang.binary_to_term(checker) do
|
||||
{cache_chunk(table, module, contents), module_map_to_module_tuple(module_map)}
|
||||
else
|
||||
_ -> {:not_found, nil}
|
||||
end
|
||||
@@ -166,9 +180,9 @@ defmodule Module.ParallelChecker do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Receives pairs of module maps and BEAM binaries. In parallel it verifies
|
||||
the modules and adds the ExCk chunk to the binaries. Returns the updated
|
||||
list of warnings from the verification.
|
||||
Receives pairs of module maps and BEAM binaries.
|
||||
|
||||
Returns the updated list of warnings from the verification.
|
||||
"""
|
||||
@spec verify(cache(), [{module(), Path.t()}]) :: [warning()]
|
||||
def verify({checker, table}, runtime_files) do
|
||||
@@ -206,14 +220,6 @@ defmodule Module.ParallelChecker do
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Test cache.
|
||||
"""
|
||||
def test_cache do
|
||||
{:ok, cache} = start_link()
|
||||
cache
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the export kind and deprecation reason for the given MFA from
|
||||
the cache. If the module does not exist return `:badmodule`,
|
||||
@@ -412,7 +418,7 @@ defmodule Module.ParallelChecker do
|
||||
mode =
|
||||
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
|
||||
{@elixir_checker_version, contents} <- :erlang.binary_to_term(chunk) do
|
||||
# The chunk has more information, so that's our preference
|
||||
cache_chunk(table, module, contents)
|
||||
else
|
||||
@@ -463,12 +469,12 @@ defmodule Module.ParallelChecker do
|
||||
if Keyword.has_key?(attributes, :__protocol__), do: :protocol, else: :elixir
|
||||
end
|
||||
|
||||
defp cache_from_module_map(table, map) do
|
||||
defp cache_from_module_map(table, map, signatures) do
|
||||
exports =
|
||||
behaviour_exports(map) ++
|
||||
for({function, :def, _meta, _clauses} <- map.definitions, do: function)
|
||||
|
||||
cache_info(table, map.module, exports, Map.new(map.deprecated), map.signatures)
|
||||
cache_info(table, map.module, exports, Map.new(map.deprecated), signatures)
|
||||
{elixir_mode(map.attributes), module_map_to_module_tuple(map)}
|
||||
end
|
||||
|
||||
|
||||
@@ -24,20 +24,17 @@ defmodule Module.Types do
|
||||
#
|
||||
# * :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]
|
||||
@modes [:static, :dynamic, :infer]
|
||||
|
||||
# These functions are not inferred because they are added/managed by the compiler
|
||||
@no_infer [behaviour_info: 1]
|
||||
|
||||
@doc false
|
||||
def infer(module, file, attrs, defs, private, used_private, env, {_, cache}) do
|
||||
def infer(module, file, attrs, defs, used_private, env, {_, cache}) do
|
||||
# We don't care about inferring signatures for protocols,
|
||||
# those will be replaced anyway. There is also nothing to
|
||||
# infer if there is no cache system, we only do traversals.
|
||||
@@ -49,8 +46,8 @@ defmodule Module.Types do
|
||||
finder =
|
||||
fn fun_arity ->
|
||||
case :lists.keyfind(fun_arity, 1, defs) do
|
||||
{_, kind, _, _} = clause ->
|
||||
{infer_mode(kind, infer_signatures?), clause, default_domain(fun_arity, impl)}
|
||||
{_, kind, _, _} = def ->
|
||||
default_domain(infer_mode(kind, infer_signatures?), def, fun_arity, impl)
|
||||
|
||||
false ->
|
||||
false
|
||||
@@ -75,23 +72,27 @@ defmodule Module.Types do
|
||||
|
||||
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} ->
|
||||
# Optimized version of finder, since we already the definition
|
||||
# In case there are loops, the other we traverse matters,
|
||||
# so we sort the definitions for determinism
|
||||
{types, private, %{local_sigs: reachable_sigs} = context} =
|
||||
for {fun_arity, kind, meta, _clauses} = def <- Enum.sort(defs),
|
||||
reduce: {[], [], context()} do
|
||||
{types, private, context} when kind in [:def, :defmacro] ->
|
||||
# Optimized version of finder, since we already have the definition
|
||||
finder = fn _ ->
|
||||
{infer_mode(kind, infer_signatures?), def, default_domain(fun_arity, impl)}
|
||||
default_domain(infer_mode(kind, infer_signatures?), def, fun_arity, impl)
|
||||
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}
|
||||
{[{fun_arity, inferred} | types], private, context}
|
||||
else
|
||||
{types, context}
|
||||
{types, private, context}
|
||||
end
|
||||
|
||||
{types, private, context} ->
|
||||
{types, [def | private], context}
|
||||
end
|
||||
|
||||
# Now traverse all used privates to find any other private that have been used by them.
|
||||
@@ -105,8 +106,8 @@ defmodule Module.Types do
|
||||
|
||||
{unreachable, _context} =
|
||||
Enum.reduce(private, {[], context}, fn
|
||||
{fun_arity, kind, _meta, _defaults} = info, {unreachable, context} ->
|
||||
warn_unused_def(info, used_sigs, env)
|
||||
{fun_arity, kind, meta, _clauses}, {unreachable, context} ->
|
||||
warn_unused_def(fun_arity, kind, meta, used_sigs, env)
|
||||
|
||||
# Find anything undefined within unused functions
|
||||
{_kind, _inferred, context} = local_handler([], fun_arity, stack, context, finder)
|
||||
@@ -125,7 +126,7 @@ defmodule Module.Types do
|
||||
end
|
||||
|
||||
defp infer_mode(kind, infer_signatures?) do
|
||||
if infer_signatures? and kind in [:def, :defp], do: :infer, else: :traversal
|
||||
if infer_signatures? and kind in [:def, :defp], do: :infer, else: :traverse
|
||||
end
|
||||
|
||||
defp protocol?(attrs) do
|
||||
@@ -146,12 +147,24 @@ defmodule Module.Types do
|
||||
end
|
||||
end
|
||||
|
||||
defp default_domain({_, arity} = fun_arity, impl) do
|
||||
defp default_domain(mode, def, {_, arity} = fun_arity, impl) do
|
||||
with {for, callbacks} <- impl,
|
||||
true <- fun_arity in callbacks do
|
||||
[Descr.dynamic(Module.Types.Of.impl(for)) | List.duplicate(Descr.dynamic(), arity - 1)]
|
||||
args = [
|
||||
Descr.dynamic(Module.Types.Of.impl(for))
|
||||
| List.duplicate(Descr.dynamic(), arity - 1)
|
||||
]
|
||||
|
||||
{_fun_arity, kind, meta, clauses} = def
|
||||
|
||||
clauses =
|
||||
for {meta, args, guards, body} <- clauses do
|
||||
{[type_check: {:impl, for}] ++ meta, args, guards, body}
|
||||
end
|
||||
|
||||
{mode, {fun_arity, kind, meta, clauses}, args}
|
||||
else
|
||||
_ -> List.duplicate(Descr.dynamic(), arity)
|
||||
_ -> {mode, def, List.duplicate(Descr.dynamic(), arity)}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -161,29 +174,30 @@ defmodule Module.Types do
|
||||
: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, used, env) do
|
||||
default = Keyword.get(meta, :defaults, 0)
|
||||
|
||||
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
|
||||
cond do
|
||||
Keyword.get(meta, :context) != nil ->
|
||||
:ok
|
||||
|
||||
:ok
|
||||
end
|
||||
default == 0 ->
|
||||
case is_map_key(used, fun_arity) do
|
||||
true -> :ok
|
||||
false -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_def, fun_arity, kind})
|
||||
end
|
||||
|
||||
defp warn_unused_def({tuple, kind, meta, default}, used, env) when default > 0 do
|
||||
{name, arity} = tuple
|
||||
min = arity - default
|
||||
max = arity
|
||||
default > 0 ->
|
||||
{name, arity} = fun_arity
|
||||
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})
|
||||
case min_reachable_default(max, min, :none, name, used) do
|
||||
:none -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_def, fun_arity, kind})
|
||||
^min -> :ok
|
||||
^max -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_args, fun_arity})
|
||||
diff -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_args, fun_arity, diff})
|
||||
end
|
||||
end
|
||||
|
||||
:ok
|
||||
@@ -208,7 +222,7 @@ defmodule Module.Types do
|
||||
|
||||
finder = fn fun_arity ->
|
||||
case :lists.keyfind(fun_arity, 1, defs) do
|
||||
{_, _, _, _} = clause -> {:dynamic, clause, default_domain(fun_arity, impl)}
|
||||
{_, _, _, _} = def -> default_domain(:dynamic, def, fun_arity, impl)
|
||||
false -> false
|
||||
end
|
||||
end
|
||||
@@ -219,7 +233,7 @@ defmodule Module.Types do
|
||||
context =
|
||||
Enum.reduce(defs, context(), fn {fun_arity, _kind, meta, _clauses} = def, context ->
|
||||
# Optimized version of finder, since we already the definition
|
||||
finder = fn _ -> {:dynamic, def, default_domain(fun_arity, impl)} end
|
||||
finder = fn _ -> default_domain(:dynamic, def, fun_arity, impl) end
|
||||
{_kind, _inferred, context} = local_handler(meta, fun_arity, stack, context, finder)
|
||||
context
|
||||
end)
|
||||
@@ -279,7 +293,7 @@ defmodule Module.Types do
|
||||
context = put_in(context.local_sigs, Map.put(local_sigs, fun_arity, kind))
|
||||
|
||||
{inferred, mapping, context} =
|
||||
local_handler(fun_arity, kind, meta, clauses, expected, mode, stack, context)
|
||||
local_handler(mode, fun_arity, kind, meta, clauses, expected, stack, context)
|
||||
|
||||
context =
|
||||
update_in(context.local_sigs, &Map.put(&1, fun_arity, {kind, inferred, mapping}))
|
||||
@@ -292,7 +306,17 @@ defmodule Module.Types do
|
||||
end
|
||||
end
|
||||
|
||||
defp local_handler(fun_arity, kind, meta, clauses, expected, mode, stack, context) do
|
||||
defp local_handler(:traverse, {_, arity}, _kind, _meta, clauses, _expected, stack, context) do
|
||||
context =
|
||||
Enum.reduce(clauses, context, fn {_meta, _args, _guards, body}, context ->
|
||||
Module.Types.Traverse.of_expr(body, stack, context)
|
||||
end)
|
||||
|
||||
inferred = {:infer, nil, [{List.duplicate(Descr.term(), arity), Descr.dynamic()}]}
|
||||
{inferred, [{0, 0}], context}
|
||||
end
|
||||
|
||||
defp local_handler(mode, fun_arity, kind, meta, clauses, expected, stack, context) do
|
||||
{fun, _arity} = fun_arity
|
||||
stack = stack |> fresh_stack(mode, fun_arity) |> with_file_meta(meta)
|
||||
|
||||
@@ -308,12 +332,7 @@ defmodule Module.Types do
|
||||
{return_type, context} =
|
||||
Expr.of_expr(body, Descr.term(), body, stack, context)
|
||||
|
||||
args_types =
|
||||
if stack.mode == :traversal do
|
||||
expected
|
||||
else
|
||||
Pattern.of_domain(trees, expected, context)
|
||||
end
|
||||
args_types = Pattern.of_domain(trees, context)
|
||||
|
||||
{type_index, inferred} =
|
||||
add_inferred(inferred, args_types, return_type, total - 1, [])
|
||||
@@ -416,10 +435,7 @@ defmodule Module.Types do
|
||||
# The mode to be used, see the @modes attribute
|
||||
mode: mode,
|
||||
# The function for handling local calls
|
||||
local_handler: handler,
|
||||
# Control if variable refinement is enabled.
|
||||
# It is disabled only on dynamic dispatches.
|
||||
refine_vars: true
|
||||
local_handler: handler
|
||||
}
|
||||
end
|
||||
|
||||
@@ -430,7 +446,9 @@ defmodule Module.Types do
|
||||
warnings: [],
|
||||
# All vars and their types
|
||||
vars: %{},
|
||||
# Variables and arguments from patterns
|
||||
# Variables that are specific to the current environment/conditional
|
||||
conditional_vars: nil,
|
||||
# Track metadata specific to matches and guards
|
||||
pattern_info: nil,
|
||||
# If type checking has found an error/failure
|
||||
failed: false,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+3051
-1255
File diff suppressed because it is too large
Load Diff
+263
-190
@@ -15,6 +15,7 @@ defmodule Module.Types.Expr do
|
||||
list_of_modules = list(atom())
|
||||
|
||||
@try_catch atom([:error, :exit, :throw])
|
||||
@atom_true atom([true])
|
||||
|
||||
@caller closed_map(
|
||||
__struct__: atom([Macro.Env]),
|
||||
@@ -34,9 +35,12 @@ defmodule Module.Types.Expr do
|
||||
versioned_vars: open_map()
|
||||
)
|
||||
|
||||
# An annotation for terms where the reverse arrow is not yet fully defined
|
||||
# An annotation for terms where the reverse arrow is not yet fully defined.
|
||||
# Also revisit all users of dynamic() in this module in a later date.
|
||||
@pending term()
|
||||
@atom_true atom([true])
|
||||
|
||||
# We do not make exception dynamic on purpose. If you do a blank rescue,
|
||||
# then we will assume you need to statically handle all possible exceptions.
|
||||
@exception open_map(__struct__: atom(), __exception__: @atom_true)
|
||||
|
||||
args_or_arity = union(list(term()), integer())
|
||||
@@ -83,34 +87,28 @@ defmodule Module.Types.Expr do
|
||||
def of_expr(list, expected, expr, stack, context) when is_list(list) do
|
||||
{prefix, suffix} = unpack_list(list, [])
|
||||
|
||||
if stack.mode == :traversal do
|
||||
{_, context} = Enum.map_reduce(prefix, context, &of_expr(&1, term(), expr, stack, &2))
|
||||
{_, context} = of_expr(suffix, term(), expr, stack, context)
|
||||
{dynamic(), context}
|
||||
else
|
||||
hd_type =
|
||||
case list_hd(expected) do
|
||||
{_, type} -> type
|
||||
_ -> term()
|
||||
end
|
||||
hd_type =
|
||||
case list_hd(expected) do
|
||||
{:ok, type} -> type
|
||||
_ -> term()
|
||||
end
|
||||
|
||||
{prefix, context} = Enum.map_reduce(prefix, context, &of_expr(&1, hd_type, expr, stack, &2))
|
||||
{prefix, context} = Enum.map_reduce(prefix, context, &of_expr(&1, hd_type, expr, stack, &2))
|
||||
|
||||
{suffix, context} =
|
||||
if suffix == [] do
|
||||
{empty_list(), context}
|
||||
else
|
||||
tl_type =
|
||||
case list_tl(expected) do
|
||||
{_, type} -> type
|
||||
_ -> term()
|
||||
end
|
||||
{suffix, context} =
|
||||
if suffix == [] do
|
||||
{empty_list(), context}
|
||||
else
|
||||
tl_type =
|
||||
case list_tl(expected) do
|
||||
{:ok, type} -> type
|
||||
:badnonemptylist -> term()
|
||||
end
|
||||
|
||||
of_expr(suffix, tl_type, expr, stack, context)
|
||||
end
|
||||
of_expr(suffix, tl_type, expr, stack, context)
|
||||
end
|
||||
|
||||
{non_empty_list(Enum.reduce(prefix, &union/2), suffix), context}
|
||||
end
|
||||
{non_empty_list(Enum.reduce(prefix, &union/2), suffix), context}
|
||||
end
|
||||
|
||||
# {left, right}
|
||||
@@ -125,8 +123,9 @@ defmodule Module.Types.Expr do
|
||||
|
||||
# <<...>>>
|
||||
def of_expr({:<<>>, _meta, args}, _expected, _expr, stack, context) do
|
||||
context = Of.binary(args, :expr, stack, context)
|
||||
{binary(), context}
|
||||
args
|
||||
|> Of.bitstring(:expr, stack, context)
|
||||
|> dynamic_unless_static(stack)
|
||||
end
|
||||
|
||||
def of_expr({:__CALLER__, _meta, var_context}, _expected, _expr, _stack, context)
|
||||
@@ -168,57 +167,32 @@ defmodule Module.Types.Expr do
|
||||
# allow variables defined on the left side of | to be available
|
||||
# on the right side, this is safe.
|
||||
{pairs_types, context} =
|
||||
Of.pairs(args, expected, stack, context, &of_expr(&1, &2, expr, &3, &4))
|
||||
Enum.map_reduce(args, context, fn {key, value}, context ->
|
||||
{key_type, context} = of_expr(key, term(), expr, stack, context)
|
||||
{value_type, context} = of_expr(value, term(), expr, stack, context)
|
||||
{{key_type, value_type}, context}
|
||||
end)
|
||||
|
||||
expected =
|
||||
if stack.mode == :traversal do
|
||||
expected
|
||||
else
|
||||
# TODO: Once we introduce domain keys, if we ever find a domain
|
||||
# that overlaps atoms, we can only assume optional(atom()) => term(),
|
||||
# which is what the `open_map()` below falls back into anyway.
|
||||
Enum.reduce_while(pairs_types, expected, fn
|
||||
{_, [key], _}, acc ->
|
||||
case map_fetch_and_put(acc, key, term()) do
|
||||
{_value, acc} -> {:cont, acc}
|
||||
_ -> {:halt, open_map()}
|
||||
end
|
||||
|
||||
_, _ ->
|
||||
{:halt, open_map()}
|
||||
end)
|
||||
end
|
||||
# The only information we can attach to the expected types is that
|
||||
# certain keys are expected.
|
||||
expected_pairs =
|
||||
Enum.flat_map(pairs_types, fn {key_type, _value_type} ->
|
||||
case atom_fetch(key_type) do
|
||||
{:finite, [key]} -> [{key, term()}]
|
||||
_ -> []
|
||||
end
|
||||
end)
|
||||
|
||||
expected = intersection(expected, open_map(expected_pairs))
|
||||
{map_type, context} = of_expr(map, expected, expr, stack, context)
|
||||
|
||||
try do
|
||||
Of.permutate_map(pairs_types, stack, fn fallback, keys_to_assert, pairs ->
|
||||
# Ensure all keys to assert and all type pairs exist in map
|
||||
keys_to_assert = Enum.map(pairs, &elem(&1, 0)) ++ keys_to_assert
|
||||
|
||||
Enum.each(Enum.map(pairs, &elem(&1, 0)) ++ keys_to_assert, fn key ->
|
||||
case map_fetch(map_type, key) do
|
||||
{_, _} -> :ok
|
||||
:badkey -> throw({:badkey, map_type, key, update, context})
|
||||
:badmap -> throw({:badmap, map_type, update, context})
|
||||
end
|
||||
end)
|
||||
|
||||
# If all keys are known is no fallback (i.e. we know all keys being updated),
|
||||
# we can update the existing map.
|
||||
if fallback == none() do
|
||||
Enum.reduce(pairs, map_type, fn {key, type}, acc ->
|
||||
case map_fetch_and_put(acc, key, type) do
|
||||
{_value, descr} -> descr
|
||||
:badkey -> throw({:badkey, map_type, key, update, context})
|
||||
:badmap -> throw({:badmap, map_type, update, context})
|
||||
end
|
||||
end)
|
||||
else
|
||||
# TODO: Use the fallback type to actually indicate if open or closed.
|
||||
# The fallback must be unioned with the result of map_values with all
|
||||
# `keys` deleted.
|
||||
dynamic(open_map(pairs))
|
||||
Enum.reduce(pairs_types, map_type, fn {key_type, value_type}, acc ->
|
||||
case literal_map_update(acc, key_type, value_type) do
|
||||
{:ok, descr} -> descr
|
||||
{:badkey, key} -> throw({:badkey, map_type, key, update, context})
|
||||
{:baddomain, domain} -> throw({:baddomain, map_type, domain, update, context})
|
||||
:badmap -> throw({:badmap, map_type, update, context})
|
||||
end
|
||||
end)
|
||||
catch
|
||||
@@ -229,23 +203,35 @@ defmodule Module.Types.Expr do
|
||||
end
|
||||
|
||||
# %Struct{map | ...}
|
||||
# This syntax is deprecated, so we simply traverse.
|
||||
def of_expr(
|
||||
{:%, _, [_, {:%{}, _, [{:|, _, [map, args]}]}]} = struct,
|
||||
{:%, meta, [module, {:%{}, _, [{:|, _, [map, pairs]}]}]} = struct,
|
||||
_expected,
|
||||
expr,
|
||||
stack,
|
||||
context
|
||||
) do
|
||||
{_, context} = of_expr(map, term(), struct, stack, context)
|
||||
# We pass the expected type as `term()` because the struct update
|
||||
# operator already expects it to be a map at this point.
|
||||
{map_type, context} = of_expr(map, term(), struct, stack, context)
|
||||
|
||||
context =
|
||||
Enum.reduce(args, context, fn {key, value}, context when is_atom(key) ->
|
||||
{_, context} = of_expr(value, term(), expr, stack, context)
|
||||
with {false, struct_key_type} <- map_fetch_key(map_type, :__struct__),
|
||||
{:finite, [^module]} <- atom_fetch(struct_key_type) do
|
||||
context
|
||||
end)
|
||||
else
|
||||
_ ->
|
||||
error(__MODULE__, {:badupdate, map_type, struct, context}, meta, stack, context)
|
||||
end
|
||||
|
||||
{dynamic(), context}
|
||||
Enum.reduce(pairs, {map_type, context}, fn {key, value}, {acc, context} ->
|
||||
# TODO: Once we support typed structs, we need to type check them here
|
||||
{type, context} = of_expr(value, term(), expr, stack, context)
|
||||
|
||||
case map_put_key(acc, key, type) do
|
||||
{:ok, acc} -> {acc, context}
|
||||
_ -> {acc, context}
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
# %{...}
|
||||
@@ -302,7 +288,7 @@ defmodule Module.Types.Expr do
|
||||
end
|
||||
|
||||
{body_type, context} = of_expr(body, expected, expr, stack, context)
|
||||
{union(body_type, acc), reset_vars(context, original)}
|
||||
{union(body_type, acc), Of.reset_vars(context, original)}
|
||||
end)
|
||||
|> dynamic_unless_static(stack)
|
||||
end
|
||||
@@ -311,10 +297,17 @@ defmodule Module.Types.Expr do
|
||||
{case_type, context} = of_expr(case_expr, @pending, case_expr, stack, context)
|
||||
info = {:case, meta, case_type, case_expr}
|
||||
|
||||
# If we are only type checking the expression and the expression is a literal,
|
||||
# let's mark it as generated, as it is most likely a macro code. However, if
|
||||
# no clause is matched, we should still check for that.
|
||||
if Macro.quoted_literal?(case_expr) do
|
||||
added_meta =
|
||||
if Macro.quoted_literal?(case_expr) do
|
||||
[generated: true]
|
||||
else
|
||||
case_expr |> get_meta() |> Keyword.take([:generated])
|
||||
end
|
||||
|
||||
# If the expression is generated or the construct is a literal,
|
||||
# it is most likely a macro code. However, if no clause is matched,
|
||||
# we should still check for that.
|
||||
if added_meta != [] do
|
||||
for {:->, meta, args} <- clauses, do: {:->, [generated: true] ++ meta, args}
|
||||
else
|
||||
clauses
|
||||
@@ -323,25 +316,20 @@ defmodule Module.Types.Expr do
|
||||
|> dynamic_unless_static(stack)
|
||||
end
|
||||
|
||||
# TODO: fn pat -> expr end
|
||||
# fn pat -> expr end
|
||||
def of_expr({:fn, _meta, clauses}, _expected, _expr, stack, context) do
|
||||
[{:->, _, [head, _]} | _] = clauses
|
||||
{patterns, _guards} = extract_head(head)
|
||||
domain = Enum.map(patterns, fn _ -> dynamic() end)
|
||||
|
||||
if stack.mode == :traversal do
|
||||
{_acc, context} = of_clauses(clauses, domain, @pending, nil, :fn, stack, context, none())
|
||||
{dynamic(fun(length(patterns))), context}
|
||||
else
|
||||
{acc, context} =
|
||||
of_clauses_fun(clauses, domain, @pending, nil, :fn, stack, context, [], fn
|
||||
trees, body, context, acc ->
|
||||
args = Pattern.of_domain(trees, domain, context)
|
||||
add_inferred(acc, args, body)
|
||||
end)
|
||||
{acc, context} =
|
||||
of_clauses_fun(clauses, domain, @pending, nil, :fn, stack, context, [], fn
|
||||
trees, body, context, acc ->
|
||||
args = Pattern.of_domain(trees, context)
|
||||
add_inferred(acc, args, body)
|
||||
end)
|
||||
|
||||
{fun_from_overlapping_clauses(acc), context}
|
||||
end
|
||||
{fun_from_inferred_clauses(acc), context}
|
||||
end
|
||||
|
||||
def of_expr({:try, _meta, [[do: body] ++ blocks]}, expected, expr, stack, original) do
|
||||
@@ -359,7 +347,7 @@ defmodule Module.Types.Expr do
|
||||
|
||||
{type, context} =
|
||||
blocks
|
||||
|> Enum.reduce({type, reset_vars(context, original)}, fn
|
||||
|> Enum.reduce({type, Of.reset_vars(context, original)}, fn
|
||||
{:rescue, clauses}, acc_context ->
|
||||
Enum.reduce(clauses, acc_context, fn
|
||||
{:->, _, [[{:in, meta, [var, exceptions]} = expr], body]}, {acc, context} ->
|
||||
@@ -405,7 +393,7 @@ defmodule Module.Types.Expr do
|
||||
{body_type, context} = of_expr(body, expected, expr, stack, context)
|
||||
|
||||
if compatible?(timeout_type, @timeout_type) do
|
||||
{union(body_type, acc), reset_vars(context, original)}
|
||||
{union(body_type, acc), Of.reset_vars(context, original)}
|
||||
else
|
||||
error = {:badtimeout, timeout_type, timeout, context}
|
||||
{union(body_type, acc), error(__MODULE__, error, meta, stack, context)}
|
||||
@@ -429,20 +417,26 @@ defmodule Module.Types.Expr do
|
||||
else
|
||||
# TODO: Use the collectable protocol for the output
|
||||
into = Keyword.get(opts, :into, [])
|
||||
{into_wrapper, gradual?, context} = for_into(into, meta, stack, context)
|
||||
{into_type, into_kind, context} = for_into(into, meta, stack, context)
|
||||
{block_type, context} = of_expr(block, @pending, block, stack, context)
|
||||
|
||||
for_type =
|
||||
for type <- into_wrapper do
|
||||
case type do
|
||||
:binary -> binary()
|
||||
:list -> list(block_type)
|
||||
:term -> term()
|
||||
end
|
||||
end
|
||||
|> Enum.reduce(&union/2)
|
||||
case into_kind do
|
||||
:bitstring ->
|
||||
case compatible_intersection(block_type, bitstring()) do
|
||||
{:ok, intersection} ->
|
||||
{return_union(into_type, intersection, stack), context}
|
||||
|
||||
{if(gradual?, do: dynamic(for_type), else: for_type), context}
|
||||
{:error, _} ->
|
||||
error = {:badbitbody, block_type, block, context}
|
||||
{error_type(), error(__MODULE__, error, meta, stack, context)}
|
||||
end
|
||||
|
||||
:non_empty_list ->
|
||||
{return_union(into_type, non_empty_list(block_type), stack), context}
|
||||
|
||||
:none ->
|
||||
{into_type, context}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -461,7 +455,7 @@ defmodule Module.Types.Expr do
|
||||
{args_types, context} =
|
||||
Enum.map_reduce(args, context, &of_expr(&1, @pending, &1, stack, &2))
|
||||
|
||||
Apply.fun_apply(fun_type, args_types, call, stack, context)
|
||||
Apply.fun(fun_type, args_types, call, stack, context)
|
||||
end
|
||||
|
||||
def of_expr({{:., _, [callee, key_or_fun]}, meta, []} = call, expected, expr, stack, context)
|
||||
@@ -482,7 +476,7 @@ defmodule Module.Types.Expr do
|
||||
apply_many(mods, name, args, expected, call, stack, context)
|
||||
end
|
||||
|
||||
# TODO: &Foo.bar/1
|
||||
# &Foo.bar/1
|
||||
def of_expr(
|
||||
{:&, _, [{:/, _, [{{:., _, [remote, name]}, meta, []}, arity]}]} = call,
|
||||
_expected,
|
||||
@@ -496,7 +490,7 @@ defmodule Module.Types.Expr do
|
||||
Apply.remote_capture(mods, name, arity, meta, stack, context)
|
||||
end
|
||||
|
||||
# TODO: &foo/1
|
||||
# &foo/1
|
||||
def of_expr({:&, _meta, [{:/, _, [{fun, meta, _}, arity]}]}, _expected, _expr, stack, context) do
|
||||
Apply.local_capture(fun, arity, meta, stack, context)
|
||||
end
|
||||
@@ -504,31 +498,24 @@ defmodule Module.Types.Expr do
|
||||
# Super
|
||||
def of_expr({:super, meta, args} = call, expected, _expr, stack, context) when is_list(args) do
|
||||
{_kind, fun} = Keyword.fetch!(meta, :super)
|
||||
apply_local(fun, args, expected, call, stack, context)
|
||||
Apply.local(fun, args, expected, call, stack, context, &of_expr/5)
|
||||
end
|
||||
|
||||
# Local calls
|
||||
def of_expr({fun, _meta, args} = call, expected, _expr, stack, context)
|
||||
when is_atom(fun) and is_list(args) do
|
||||
apply_local(fun, args, expected, call, stack, context)
|
||||
Apply.local(fun, args, expected, call, stack, context, &of_expr/5)
|
||||
end
|
||||
|
||||
# var
|
||||
def of_expr(var, expected, expr, stack, context) when is_var(var) do
|
||||
case stack do
|
||||
%{mode: :traversal} -> {dynamic(), context}
|
||||
%{refine_vars: false} -> {Of.var(var, context), context}
|
||||
%{} -> Of.refine_body_var(var, expected, expr, stack, context)
|
||||
end
|
||||
def of_expr({_, meta, _} = var, expected, expr, stack, context) when is_var(var) do
|
||||
version = Keyword.fetch!(meta, :version)
|
||||
{type, context} = Of.refine_body_var(version, expected, expr, stack, context)
|
||||
{type, Pattern.of_changed([version], stack, context)}
|
||||
end
|
||||
|
||||
## Tuples
|
||||
|
||||
defp of_tuple(elems, _expected, expr, %{mode: :traversal} = stack, context) do
|
||||
{_types, context} = Enum.map_reduce(elems, context, &of_expr(&1, term(), expr, stack, &2))
|
||||
{dynamic(), context}
|
||||
end
|
||||
|
||||
defp of_tuple(elems, expected, expr, stack, context) do
|
||||
of_tuple(elems, 0, [], expected, expr, stack, context)
|
||||
end
|
||||
@@ -575,12 +562,13 @@ defmodule Module.Types.Expr do
|
||||
_ ->
|
||||
expected = if structs == [], do: @exception, else: Enum.reduce(structs, &union/2)
|
||||
expr = {:__block__, [type_check: info], [expr]}
|
||||
context = Of.declare_var(var, context)
|
||||
{_ok?, _type, context} = Of.refine_head_var(var, expected, expr, stack, context)
|
||||
context
|
||||
end
|
||||
|
||||
{type, context} = of_expr(body, @pending, body, stack, context)
|
||||
{type, reset_vars(context, original)}
|
||||
{type, Of.reset_vars(context, original)}
|
||||
end
|
||||
|
||||
## Comprehensions
|
||||
@@ -590,19 +578,19 @@ defmodule Module.Types.Expr do
|
||||
{pattern, guards} = extract_head([left])
|
||||
|
||||
{_type, context} =
|
||||
apply_one(Enumerable, :count, [right], dynamic(), expr, stack, context)
|
||||
Apply.remote(Enumerable, :count, [right], dynamic(), expr, stack, context, &of_expr/5)
|
||||
|
||||
Pattern.of_generator(pattern, guards, dynamic(), :for, expr, stack, context)
|
||||
end
|
||||
|
||||
defp for_clause({:<<>>, _, [{:<-, meta, [left, right]}]} = expr, stack, context) do
|
||||
{right_type, context} = of_expr(right, binary(), expr, stack, context)
|
||||
context = Pattern.of_generator(left, [], binary(), :for, expr, stack, context)
|
||||
{right_type, context} = of_expr(right, bitstring(), expr, stack, context)
|
||||
context = Pattern.of_generator(left, [], bitstring(), :for, expr, stack, context)
|
||||
|
||||
if compatible?(right_type, binary()) do
|
||||
if compatible?(right_type, bitstring()) do
|
||||
context
|
||||
else
|
||||
error = {:badbinary, right_type, right, context}
|
||||
error = {:badbitgenerator, right_type, right, context}
|
||||
error(__MODULE__, error, meta, stack, context)
|
||||
end
|
||||
end
|
||||
@@ -612,13 +600,13 @@ defmodule Module.Types.Expr do
|
||||
context
|
||||
end
|
||||
|
||||
@into_compile union(binary(), empty_list())
|
||||
@into_compile union(bitstring(), empty_list())
|
||||
|
||||
defp for_into([], _meta, _stack, context),
|
||||
do: {[:list], false, context}
|
||||
do: {empty_list(), :non_empty_list, context}
|
||||
|
||||
defp for_into(binary, _meta, _stack, context) when is_binary(binary),
|
||||
do: {[:binary], false, context}
|
||||
do: {binary(), :bitstring, context}
|
||||
|
||||
defp for_into(into, meta, stack, context) do
|
||||
meta =
|
||||
@@ -635,21 +623,33 @@ defmodule Module.Types.Expr do
|
||||
{type, context} = of_expr(into, domain, expr, stack, context)
|
||||
|
||||
# We use subtype? instead of compatible because we want to handle
|
||||
# only binary/list, even if a dynamic with something else is given.
|
||||
# only bitstring/list, even if a dynamic with something else is given.
|
||||
if subtype?(type, @into_compile) do
|
||||
case {binary_type?(type), empty_list_type?(type)} do
|
||||
{false, true} -> {[:list], gradual?(type), context}
|
||||
{true, false} -> {[:binary], gradual?(type), context}
|
||||
{_, _} -> {[:binary, :list], gradual?(type), context}
|
||||
case {bitstring_type?(type), empty_list_type?(type)} do
|
||||
# If they can be both be true, then we don't know
|
||||
# what the contents of the block are for
|
||||
{true, true} ->
|
||||
type = union(bitstring(), list(term()))
|
||||
{if(gradual?(type), do: dynamic(type), else: type), :none, context}
|
||||
|
||||
{false, true} ->
|
||||
{type, :non_empty_list, context}
|
||||
|
||||
{true, false} ->
|
||||
{type, :bitstring, context}
|
||||
end
|
||||
else
|
||||
{_type, context} =
|
||||
Apply.remote_apply(info, Collectable, :into, [type], expr, stack, context)
|
||||
|
||||
{[:term], true, context}
|
||||
{dynamic(), :none, context}
|
||||
end
|
||||
end
|
||||
|
||||
defp return_union(left, right, stack) do
|
||||
Apply.return(union(left, right), [left, right], stack)
|
||||
end
|
||||
|
||||
## With
|
||||
|
||||
defp with_clause({:<-, _meta, [left, right]} = expr, stack, context) do
|
||||
@@ -665,7 +665,7 @@ defmodule Module.Types.Expr do
|
||||
|
||||
defp with_option({:do, body}, stack, context, original) do
|
||||
{_type, context} = of_expr(body, @pending, body, stack, context)
|
||||
reset_vars(context, original)
|
||||
Of.reset_vars(context, original)
|
||||
end
|
||||
|
||||
defp with_option({:else, clauses}, stack, context, _original) do
|
||||
@@ -677,46 +677,22 @@ defmodule Module.Types.Expr do
|
||||
|
||||
## General helpers
|
||||
|
||||
defp apply_local(fun, args, expected, {_, meta, _} = expr, stack, context) do
|
||||
{local_info, domain, context} = Apply.local_domain(fun, args, expected, meta, stack, context)
|
||||
|
||||
{args_types, context} =
|
||||
zip_map_reduce(args, domain, context, &of_expr(&1, &2, expr, stack, &3))
|
||||
|
||||
Apply.local_apply(local_info, fun, args_types, expr, stack, context)
|
||||
end
|
||||
|
||||
defp apply_one(mod, fun, args, expected, expr, stack, context) do
|
||||
{info, domain, context} =
|
||||
Apply.remote_domain(mod, fun, args, expected, elem(expr, 1), stack, context)
|
||||
|
||||
{args_types, context} =
|
||||
zip_map_reduce(args, domain, context, &of_expr(&1, &2, expr, stack, &3))
|
||||
|
||||
Apply.remote_apply(info, mod, fun, args_types, expr, stack, context)
|
||||
end
|
||||
|
||||
defp apply_many([], fun, args, expected, expr, stack, context) do
|
||||
{info, domain} = Apply.remote_domain(fun, args, expected, stack)
|
||||
|
||||
{args_types, context} =
|
||||
zip_map_reduce(args, domain, context, &of_expr(&1, &2, expr, stack, &3))
|
||||
|
||||
Apply.remote_apply(info, nil, fun, args_types, expr, stack, context)
|
||||
Apply.remote(fun, args, expected, expr, stack, context, &of_expr/5)
|
||||
end
|
||||
|
||||
defp apply_many([mod], fun, args, expected, expr, stack, context) do
|
||||
apply_one(mod, fun, args, expected, expr, stack, context)
|
||||
Apply.remote(mod, fun, args, expected, expr, stack, context, &of_expr/5)
|
||||
end
|
||||
|
||||
defp apply_many(mods, fun, args, expected, {remote, meta, args}, stack, context) do
|
||||
{returns, context} =
|
||||
Enum.map_reduce(mods, context, fn mod, context ->
|
||||
expr = {remote, [type_check: {:invoked_as, mod, fun, length(args)}] ++ meta, args}
|
||||
apply_one(mod, fun, args, expected, expr, %{stack | refine_vars: false}, context)
|
||||
end)
|
||||
defp apply_many(mods, fun, args, expected, call, stack, context) do
|
||||
{remote, meta, _} = call
|
||||
|
||||
{Enum.reduce(returns, &union/2), context}
|
||||
Of.with_conditional_vars(mods, none(), call, stack, context, fn mod, acc, context ->
|
||||
expr = {remote, [type_check: {:invoked_as, mod, fun, length(args)}] ++ meta, args}
|
||||
{type, context} = Apply.remote(mod, fun, args, expected, expr, stack, context, &of_expr/5)
|
||||
{union(acc, type), context}
|
||||
end)
|
||||
end
|
||||
|
||||
defp reduce_non_empty([last], acc, fun),
|
||||
@@ -728,14 +704,8 @@ defmodule Module.Types.Expr do
|
||||
defp dynamic_unless_static({_, _} = output, %{mode: :static}), do: output
|
||||
defp dynamic_unless_static({type, context}, %{mode: _}), do: {dynamic(type), context}
|
||||
|
||||
defp of_clauses(clauses, domain, expected, expr, info, %{mode: mode} = stack, context, acc) do
|
||||
fun =
|
||||
if mode == :traversal do
|
||||
fn _, _, _, _ -> dynamic() end
|
||||
else
|
||||
fn _trees, result, _context, acc -> union(result, acc) end
|
||||
end
|
||||
|
||||
defp of_clauses(clauses, domain, expected, expr, info, stack, context, acc) do
|
||||
fun = fn _trees, result, _context, acc -> union(result, acc) end
|
||||
of_clauses_fun(clauses, domain, expected, expr, info, stack, context, acc, fun)
|
||||
end
|
||||
|
||||
@@ -748,7 +718,9 @@ defmodule Module.Types.Expr do
|
||||
{trees, context} = Pattern.of_head(patterns, guards, domain, info, meta, stack, context)
|
||||
|
||||
{result, context} = of_expr(body, expected, expr || body, stack, context)
|
||||
{fun.(trees, result, context, acc), context |> set_failed(failed?) |> reset_vars(original)}
|
||||
|
||||
{fun.(trees, result, context, acc),
|
||||
context |> set_failed(failed?) |> Of.reset_vars(original)}
|
||||
end)
|
||||
end
|
||||
|
||||
@@ -758,8 +730,6 @@ defmodule Module.Types.Expr do
|
||||
defp set_failed(%{failed: false} = context, true), do: %{context | failed: true}
|
||||
defp set_failed(context, _bool), do: context
|
||||
|
||||
defp reset_vars(context, %{vars: vars}), do: %{context | vars: vars}
|
||||
|
||||
defp extract_head([{:when, _meta, args}]) do
|
||||
case Enum.split(args, -1) do
|
||||
{patterns, [guards]} -> {patterns, flatten_when(guards)}
|
||||
@@ -789,8 +759,69 @@ defmodule Module.Types.Expr do
|
||||
defp add_inferred([], args, return),
|
||||
do: [{args, return}]
|
||||
|
||||
defp literal_map_update(descr, key_descr, value_descr) do
|
||||
case map_update(descr, key_descr, value_descr, false, false) do
|
||||
{_type, descr, []} -> {:ok, descr}
|
||||
{_, _, [error | _]} -> error
|
||||
:badmap -> :badmap
|
||||
{:error, [error | _]} -> error
|
||||
{:error, []} -> {:baddomain, key_descr}
|
||||
end
|
||||
end
|
||||
|
||||
## Warning formatting
|
||||
|
||||
def format_diagnostic({:badupdate, type, expr, context}) do
|
||||
{:%, _, [module, {:%{}, _, [{:|, _, [map, _]}]}]} = expr
|
||||
traces = collect_traces(map, context)
|
||||
|
||||
fix =
|
||||
case map do
|
||||
{var, meta, context} when is_atom(var) and is_atom(context) ->
|
||||
if capture = meta[:capture] do
|
||||
"instead of using &#{capture}, you must define an anonymous function, define a variable and pattern match on \"%#{inspect(module)}{}\""
|
||||
else
|
||||
"when defining the variable \"#{Macro.to_string(map)}\", you must also pattern match on \"%#{inspect(module)}{}\""
|
||||
end
|
||||
|
||||
_ ->
|
||||
"you must assign \"#{Macro.to_string(map)}\" to variable and pattern match on \"%#{inspect(module)}{}\""
|
||||
end
|
||||
|
||||
%{
|
||||
details: %{typing_traces: traces},
|
||||
message:
|
||||
IO.iodata_to_binary([
|
||||
"""
|
||||
a struct for #{inspect(module)} is expected on struct update:
|
||||
|
||||
#{expr_to_string(expr, collapse_structs: false) |> indent(4)}
|
||||
|
||||
but got type:
|
||||
|
||||
#{to_quoted_string(type) |> indent(4)}
|
||||
""",
|
||||
format_traces(traces),
|
||||
"""
|
||||
|
||||
#{fix}.
|
||||
|
||||
#{hint()} given pattern matching is enough to catch typing errors, \
|
||||
you may optionally convert the struct update into a map update. For \
|
||||
example, instead of:
|
||||
|
||||
user = some_function()
|
||||
%User{user | name: "John Doe"}
|
||||
|
||||
it is enough to write:
|
||||
|
||||
%User{} = user = some_function()
|
||||
%{user | name: "John Doe"}
|
||||
"""
|
||||
])
|
||||
}
|
||||
end
|
||||
|
||||
def format_diagnostic({:badmap, type, expr, context}) do
|
||||
traces = collect_traces(expr, context)
|
||||
|
||||
@@ -833,7 +864,7 @@ defmodule Module.Types.Expr do
|
||||
}
|
||||
end
|
||||
|
||||
def format_diagnostic({:badbinary, type, expr, context}) do
|
||||
def format_diagnostic({:baddomain, type, key_type, expr, context}) do
|
||||
traces = collect_traces(expr, context)
|
||||
|
||||
%{
|
||||
@@ -841,7 +872,49 @@ defmodule Module.Types.Expr do
|
||||
message:
|
||||
IO.iodata_to_binary([
|
||||
"""
|
||||
expected the right side of <- in a binary generator to be a binary:
|
||||
expected a map with key of type #{to_quoted_string(key_type)} in map update syntax:
|
||||
|
||||
#{expr_to_string(expr, collapse_structs: false) |> indent(4)}
|
||||
|
||||
but got type:
|
||||
|
||||
#{to_quoted_string(type, collapse_structs: false) |> indent(4)}
|
||||
""",
|
||||
format_traces(traces)
|
||||
])
|
||||
}
|
||||
end
|
||||
|
||||
def format_diagnostic({:badbitgenerator, type, expr, context}) do
|
||||
traces = collect_traces(expr, context)
|
||||
|
||||
%{
|
||||
details: %{typing_traces: traces},
|
||||
message:
|
||||
IO.iodata_to_binary([
|
||||
"""
|
||||
expected the right side of <- in a binary generator to be a binary (or bitstring):
|
||||
|
||||
#{expr_to_string(expr) |> indent(4)}
|
||||
|
||||
but got type:
|
||||
|
||||
#{to_quoted_string(type) |> indent(4)}
|
||||
""",
|
||||
format_traces(traces)
|
||||
])
|
||||
}
|
||||
end
|
||||
|
||||
def format_diagnostic({:badbitbody, type, expr, context}) do
|
||||
traces = collect_traces(expr, context)
|
||||
|
||||
%{
|
||||
details: %{typing_traces: traces},
|
||||
message:
|
||||
IO.iodata_to_binary([
|
||||
"""
|
||||
expected the body of a for-comprehension with into: binary() (or bitstring()) to be a binary (or bitstring):
|
||||
|
||||
#{expr_to_string(expr) |> indent(4)}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ defmodule Module.Types.Helpers do
|
||||
@doc """
|
||||
Returns true if the mode cares about warnings.
|
||||
"""
|
||||
defguard is_warning(stack) when stack.mode not in [:traversal, :infer]
|
||||
defguard is_warning(stack) when stack.mode != :infer
|
||||
|
||||
@doc """
|
||||
Guard function to check if an AST node is a variable.
|
||||
@@ -91,29 +91,6 @@ defmodule Module.Types.Helpers do
|
||||
"var.fun()" (with parentheses) means "var" is an atom()
|
||||
"""
|
||||
|
||||
:interpolation ->
|
||||
"""
|
||||
|
||||
#{hint()} string interpolation uses the String.Chars protocol to \
|
||||
convert a data structure into a string. Either convert the data type into a \
|
||||
string upfront or implement the protocol accordingly
|
||||
"""
|
||||
|
||||
:generator ->
|
||||
"""
|
||||
|
||||
#{hint()} for-comprehensions use the Enumerable protocol to traverse \
|
||||
data structures. Either convert the data type into a list (or another Enumerable) \
|
||||
or implement the protocol accordingly
|
||||
"""
|
||||
|
||||
:into ->
|
||||
"""
|
||||
|
||||
#{hint()} the :into option in for-comprehensions use the Collectable protocol to \
|
||||
build its result. Either pass a valid data type or implement the protocol accordingly
|
||||
"""
|
||||
|
||||
:anonymous_rescue ->
|
||||
"""
|
||||
|
||||
@@ -132,6 +109,20 @@ defmodule Module.Types.Helpers do
|
||||
the union (which may be none)
|
||||
"""
|
||||
|
||||
{:impl, for} ->
|
||||
# Get the type without dynamic for better pretty printing
|
||||
type =
|
||||
for
|
||||
|> Module.Types.Of.impl()
|
||||
|> Module.Types.Descr.dynamic()
|
||||
|> Map.fetch!(:dynamic)
|
||||
|> Module.Types.Descr.to_quoted_string(collapse_structs: true)
|
||||
|
||||
"""
|
||||
|
||||
#{hint()} defimpl for #{inspect(for)} requires its callbacks to match exclusively on #{type}
|
||||
"""
|
||||
|
||||
:empty_domain ->
|
||||
"""
|
||||
|
||||
@@ -141,7 +132,8 @@ defmodule Module.Types.Helpers do
|
||||
end)
|
||||
end
|
||||
|
||||
defp hint, do: :elixir_errors.prefix(:hint)
|
||||
@doc "The hint prefix"
|
||||
def hint, do: :elixir_errors.prefix(:hint)
|
||||
|
||||
@doc """
|
||||
Collect traces from variables in expression.
|
||||
@@ -156,7 +148,7 @@ defmodule Module.Types.Helpers do
|
||||
version = meta[:version]
|
||||
|
||||
case vars do
|
||||
%{^version => %{off_traces: off_traces, name: name, context: context}} ->
|
||||
%{^version => %{off_traces: [_ | _] = off_traces, name: name, context: context}} ->
|
||||
{:ok,
|
||||
Map.put(versions, version, %{
|
||||
type: :variable,
|
||||
@@ -270,6 +262,10 @@ defmodule Module.Types.Helpers do
|
||||
translating inlined Erlang calls back to Elixir.
|
||||
|
||||
We also undo some macro expressions done by the Kernel module.
|
||||
|
||||
## Options
|
||||
|
||||
* `:collapse_structs` - when false, show structs full representation
|
||||
"""
|
||||
def expr_to_string(expr, opts \\ []) do
|
||||
string = prewalk_expr_to_string(expr, opts)
|
||||
@@ -340,6 +336,13 @@ defmodule Module.Types.Helpers do
|
||||
{{:., _, [mod, fun]}, meta, args} ->
|
||||
erl_to_ex(mod, fun, args, meta)
|
||||
|
||||
{:fn, meta, [{:->, _, [_args, return]}]} = expr ->
|
||||
if meta[:capture] do
|
||||
{:&, meta, [return]}
|
||||
else
|
||||
expr
|
||||
end
|
||||
|
||||
{:&, amp_meta, [{:/, slash_meta, [{{:., dot_meta, [mod, fun]}, call_meta, []}, arity]}]} ->
|
||||
{mod, fun} =
|
||||
case :elixir_rewrite.erl_to_ex(mod, fun, arity) do
|
||||
@@ -385,6 +388,13 @@ defmodule Module.Types.Helpers do
|
||||
case
|
||||
end
|
||||
|
||||
{var, meta, context} = expr when is_atom(var) and is_atom(context) ->
|
||||
if is_integer(meta[:capture]) do
|
||||
{:&, meta, [meta[:capture]]}
|
||||
else
|
||||
expr
|
||||
end
|
||||
|
||||
other ->
|
||||
other
|
||||
end)
|
||||
|
||||
+337
-182
@@ -1,5 +1,6 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
||||
# SPDX-FileCopyrightText: 2012 Plataformatec
|
||||
|
||||
defmodule Module.Types.Of do
|
||||
# Typing functionality shared between Expr and Pattern.
|
||||
@@ -11,10 +12,10 @@ defmodule Module.Types.Of do
|
||||
@suffix quote(do: ...)
|
||||
|
||||
@integer_or_float union(integer(), float())
|
||||
@integer_or_binary union(integer(), binary())
|
||||
@integer integer()
|
||||
@float float()
|
||||
@binary binary()
|
||||
@bitstring bitstring()
|
||||
|
||||
## Variables
|
||||
|
||||
@@ -29,19 +30,53 @@ defmodule Module.Types.Of do
|
||||
|
||||
@doc """
|
||||
Marks a variable with error.
|
||||
|
||||
This purposedly deletes all traces of the variable,
|
||||
as it is often invoked when the cause for error is elsewhere.
|
||||
"""
|
||||
def error_var(var, context) do
|
||||
def error_var({_, meta, _}, context) do
|
||||
error_var(Keyword.fetch!(meta, :version), context)
|
||||
end
|
||||
|
||||
def error_var(version, context) do
|
||||
update_in(context.vars[version], fn
|
||||
%{errored: true} = data -> data
|
||||
data -> Map.put(%{data | type: error_type(), off_traces: []}, :errored, true)
|
||||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Declares a variable.
|
||||
"""
|
||||
def declare_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: []
|
||||
}
|
||||
case context.vars do
|
||||
%{^version => _} ->
|
||||
context
|
||||
|
||||
put_in(context.vars[version], data)
|
||||
vars ->
|
||||
data = %{
|
||||
type: term(),
|
||||
name: var_name,
|
||||
context: var_context,
|
||||
off_traces: [],
|
||||
paths: [],
|
||||
deps: %{}
|
||||
}
|
||||
|
||||
%{context | vars: Map.put(vars, version, data)}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Tracks metadata about variables dependencies and paths.
|
||||
"""
|
||||
def track_var(version, new_deps, new_paths, context) do
|
||||
update_in(context.vars[version], fn %{paths: paths, deps: deps} = data ->
|
||||
%{data | paths: new_paths ++ paths, deps: Enum.reduce(new_deps, deps, &Map.put(&2, &1, []))}
|
||||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -52,10 +87,23 @@ defmodule Module.Types.Of do
|
||||
Returns `true` if there was a refinement, `false` otherwise.
|
||||
"""
|
||||
def refine_body_var({_, meta, _}, type, expr, stack, context) do
|
||||
version = Keyword.fetch!(meta, :version)
|
||||
refine_body_var(Keyword.fetch!(meta, :version), type, expr, stack, context)
|
||||
end
|
||||
|
||||
def refine_body_var(version, type, expr, stack, context)
|
||||
when is_integer(version) or is_reference(version) do
|
||||
%{vars: %{^version => %{type: old_type, off_traces: off_traces} = data} = vars} = context
|
||||
|
||||
if gradual?(old_type) and type not in [term(), dynamic()] do
|
||||
context =
|
||||
case context.conditional_vars do
|
||||
%{} = conditional_vars ->
|
||||
%{context | conditional_vars: Map.put(conditional_vars, version, true)}
|
||||
|
||||
nil ->
|
||||
context
|
||||
end
|
||||
|
||||
if gradual?(old_type) and type not in [term(), dynamic()] and not is_map_key(data, :errored) do
|
||||
case compatible_intersection(old_type, type) do
|
||||
{:ok, new_type} when new_type != old_type ->
|
||||
data = %{
|
||||
@@ -81,11 +129,16 @@ defmodule Module.Types.Of do
|
||||
because we want to refine types. Otherwise we should
|
||||
use compatibility.
|
||||
"""
|
||||
def refine_head_var(var, type, expr, stack, context) do
|
||||
{var_name, meta, var_context} = var
|
||||
version = Keyword.fetch!(meta, :version)
|
||||
def refine_head_var({_, meta, _}, type, expr, stack, context) do
|
||||
refine_head_var(Keyword.fetch!(meta, :version), type, expr, stack, context)
|
||||
end
|
||||
|
||||
def refine_head_var(version, type, expr, stack, context)
|
||||
when is_integer(version) or is_reference(version) do
|
||||
case context.vars do
|
||||
%{^version => %{errored: true}} ->
|
||||
{:ok, error_type(), context}
|
||||
|
||||
%{^version => %{type: old_type, off_traces: off_traces} = data} = vars ->
|
||||
new_type = intersection(type, old_type)
|
||||
|
||||
@@ -95,26 +148,14 @@ defmodule Module.Types.Of do
|
||||
off_traces: new_trace(expr, type, stack, off_traces)
|
||||
}
|
||||
|
||||
context = %{context | vars: %{vars | version => data}}
|
||||
|
||||
# We need to return error otherwise it leads to cascading errors
|
||||
if empty?(new_type) do
|
||||
{:error, error_type(),
|
||||
error({:refine_head_var, old_type, type, var, context}, meta, stack, context)}
|
||||
data = Map.put(%{data | type: error_type()}, :errored, true)
|
||||
context = %{context | vars: %{vars | version => data}}
|
||||
{:error, old_type, context}
|
||||
else
|
||||
context = %{context | vars: %{vars | version => data}}
|
||||
{:ok, new_type, context}
|
||||
end
|
||||
|
||||
%{} = vars ->
|
||||
data = %{
|
||||
type: type,
|
||||
name: var_name,
|
||||
context: var_context,
|
||||
off_traces: new_trace(expr, type, stack, [])
|
||||
}
|
||||
|
||||
context = %{context | vars: Map.put(vars, version, data)}
|
||||
{:ok, type, context}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -124,11 +165,57 @@ defmodule Module.Types.Of do
|
||||
defp new_trace(expr, type, stack, traces),
|
||||
do: [{expr, stack.file, type} | traces]
|
||||
|
||||
@doc """
|
||||
Preserves `context` in first argument while
|
||||
resetting it to the vars in the second argument.
|
||||
"""
|
||||
def reset_vars(context, %{vars: vars, conditional_vars: conditional_vars}),
|
||||
do: %{context | vars: vars, conditional_vars: conditional_vars}
|
||||
|
||||
@doc """
|
||||
Executes the args with acc using conditional variables.
|
||||
"""
|
||||
def with_conditional_vars(args, acc, expr, stack, context, fun) do
|
||||
%{vars: vars, conditional_vars: conditional_vars} = context
|
||||
|
||||
{vars_conds, {acc, context}} =
|
||||
Enum.map_reduce(args, {acc, context}, fn arg, {acc, context} ->
|
||||
{acc, context} = fun.(arg, acc, %{context | vars: vars, conditional_vars: %{}})
|
||||
%{vars: vars, conditional_vars: cond_vars} = context
|
||||
{{vars, cond_vars}, {acc, context}}
|
||||
end)
|
||||
|
||||
context = %{context | vars: vars, conditional_vars: conditional_vars}
|
||||
{acc, reduce_conditional_vars(vars_conds, expr, stack, context)}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Reduces conditional variables collected separately.
|
||||
"""
|
||||
def reduce_conditional_vars([{vars, cond} | vars_conds], expr, stack, context) do
|
||||
Enum.reduce(Map.keys(cond), context, fn version, context ->
|
||||
if Enum.all?(vars_conds, fn {_vars, cond} -> is_map_key(cond, version) end) do
|
||||
%{^version => %{type: type}} = vars
|
||||
|
||||
type =
|
||||
Enum.reduce(vars_conds, type, fn {vars, _cond}, acc ->
|
||||
%{^version => %{type: type}} = vars
|
||||
union(acc, type)
|
||||
end)
|
||||
|
||||
{_, context} = refine_body_var(version, type, expr, stack, context)
|
||||
context
|
||||
else
|
||||
context
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
## Implementations
|
||||
|
||||
impls = [
|
||||
{Atom, atom()},
|
||||
{BitString, binary()},
|
||||
{BitString, bitstring()},
|
||||
{Float, float()},
|
||||
{Function, fun()},
|
||||
{Integer, integer()},
|
||||
@@ -141,14 +228,25 @@ defmodule Module.Types.Of do
|
||||
{Any, term()}
|
||||
]
|
||||
|
||||
@doc """
|
||||
Currently, for protocol implementations, we only store
|
||||
the open struct definition. This is because we don't want
|
||||
to reconsolidate whenever the struct changes, but at the
|
||||
moment we can't store references either. Ideally struct
|
||||
types on protocol dispatches would be lazily resolved.
|
||||
"""
|
||||
def impl(for, mode \\ :closed)
|
||||
|
||||
for {for, type} <- impls do
|
||||
def impl(unquote(for)), do: unquote(Macro.escape(type))
|
||||
def impl(unquote(for), _mode), do: unquote(Macro.escape(type))
|
||||
end
|
||||
|
||||
def impl(struct) do
|
||||
# Elixir did not strictly require the implementation to be available, so we need a fallback.
|
||||
def impl(struct, mode) do
|
||||
# Elixir did not strictly require the implementation to be available,
|
||||
# so we need to deal with such cases accordingly.
|
||||
# TODO: Assume implementation is available on Elixir v2.0.
|
||||
if info = Code.ensure_loaded?(struct) && struct.__info__(:struct) do
|
||||
# A warning is emitted since v1.19+.
|
||||
if info = mode == :closed && Code.ensure_loaded?(struct) && struct.__info__(:struct) do
|
||||
struct_type(struct, info)
|
||||
else
|
||||
open_map(__struct__: atom([struct]))
|
||||
@@ -161,7 +259,7 @@ defmodule Module.Types.Of do
|
||||
Handles fetching a map key.
|
||||
"""
|
||||
def map_fetch(expr, type, field, stack, context) when is_atom(field) do
|
||||
case map_fetch(type, field) do
|
||||
case map_fetch_key(type, field) do
|
||||
{_optional?, value_type} ->
|
||||
{value_type, context}
|
||||
|
||||
@@ -176,107 +274,134 @@ defmodule Module.Types.Of do
|
||||
def closed_map(pairs, expected, stack, context, of_fun) do
|
||||
{pairs_types, context} = pairs(pairs, expected, stack, context, of_fun)
|
||||
|
||||
map =
|
||||
permutate_map(pairs_types, stack, fn fallback, _keys, pairs ->
|
||||
# TODO: Use the fallback type to actually indicate if open or closed.
|
||||
if fallback == none(), do: closed_map(pairs), else: dynamic(open_map(pairs))
|
||||
{dynamic?, domain, single, multiple} =
|
||||
Enum.reduce(pairs_types, {false, [], [], []}, fn
|
||||
{pos_neg_domain, dynamic_pair?, value_type}, {dynamic?, domain, single, multiple} ->
|
||||
dynamic? = dynamic? or dynamic_pair?
|
||||
|
||||
case pos_neg_domain do
|
||||
# If atom is included in domain keys, it unions all previous
|
||||
# single and multiple, except the ones negated:
|
||||
#
|
||||
# %{foo: :bar, term() => :baz}
|
||||
# #=> %{foo: :bar or :baz, term() => :baz}
|
||||
#
|
||||
# %{foo: :bar, not :foo => :baz}
|
||||
# #=> %{foo: :bar, term() => :baz}
|
||||
#
|
||||
# In case the negated term does not appear, we set it to none():
|
||||
#
|
||||
# %{foo: :bar, term() => :baz}
|
||||
# #=> %{term() => :baz, foo: :bar or :baz}
|
||||
#
|
||||
# %{not :foo => :baz}
|
||||
# #=> %{term() => :baz, foo: none()}
|
||||
#
|
||||
# In case we are dealing with multiple keys, we always merge the
|
||||
# domain. A more precise approach would be to postpone doing so
|
||||
# until the cartesian map is distributed but those should be very
|
||||
# uncommon.
|
||||
{[], negs, domain_keys} ->
|
||||
if :atom in domain_keys do
|
||||
{single, multiple} = union_negated(negs, value_type, single, multiple)
|
||||
{dynamic?, [{domain_keys, value_type} | domain], single, multiple}
|
||||
else
|
||||
{dynamic?, [{domain_keys, value_type} | domain], single, multiple}
|
||||
end
|
||||
|
||||
{pos, [], domain_keys} ->
|
||||
domain =
|
||||
case domain_keys do
|
||||
[] -> domain
|
||||
_ -> [{domain_keys, value_type} | domain]
|
||||
end
|
||||
|
||||
case pos do
|
||||
# Because a multiple key may override single keys, we can only
|
||||
# collect single keys while there are no multiples.
|
||||
[key] when multiple == [] ->
|
||||
{dynamic?, domain, [{key, value_type} | single], multiple}
|
||||
|
||||
_ ->
|
||||
{dynamic?, domain, single, [{pos, value_type} | multiple]}
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
{map, context}
|
||||
non_multiple = Enum.reverse(single, domain)
|
||||
|
||||
map =
|
||||
case Enum.reverse(multiple) do
|
||||
[] ->
|
||||
closed_map(non_multiple)
|
||||
|
||||
[{keys, type} | tail] ->
|
||||
for key <- keys, t <- cartesian_map(tail) do
|
||||
closed_map(non_multiple ++ [{key, type} | t])
|
||||
end
|
||||
|> Enum.reduce(&union/2)
|
||||
end
|
||||
|
||||
{if(dynamic?, do: dynamic(map), else: map), context}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Computes the types of key-value pairs.
|
||||
"""
|
||||
def pairs(pairs, _expected, %{mode: :traversal} = stack, context, of_fun) do
|
||||
Enum.map_reduce(pairs, context, fn {key, value}, context ->
|
||||
{_key_type, context} = of_fun.(key, term(), stack, context)
|
||||
{value_type, context} = of_fun.(value, term(), stack, context)
|
||||
{{true, :none, value_type}, context}
|
||||
end)
|
||||
defp union_negated([], new_type, single, multiple) do
|
||||
single = Enum.map(single, fn {key, old_type} -> {key, union(old_type, new_type)} end)
|
||||
multiple = Enum.map(multiple, fn {keys, old_type} -> {keys, union(old_type, new_type)} end)
|
||||
{single, multiple}
|
||||
end
|
||||
|
||||
def pairs(pairs, expected, stack, context, of_fun) do
|
||||
defp union_negated(negated, new_type, single, multiple) do
|
||||
{single, matched} =
|
||||
Enum.map_reduce(single, [], fn {key, old_type}, matched ->
|
||||
if key in negated do
|
||||
{{key, old_type}, [key | matched]}
|
||||
else
|
||||
{{key, union(old_type, new_type)}, matched}
|
||||
end
|
||||
end)
|
||||
|
||||
multiple =
|
||||
Enum.map(multiple, fn {keys, old_type} ->
|
||||
{keys, union(old_type, new_type)}
|
||||
end)
|
||||
|
||||
{Enum.map(negated -- matched, fn key -> {key, not_set()} end) ++ single, multiple}
|
||||
end
|
||||
|
||||
defp pairs(pairs, expected, stack, context, of_fun) do
|
||||
Enum.map_reduce(pairs, context, fn {key, value}, context ->
|
||||
{dynamic_key?, keys, context} = finite_key_type(key, stack, context, of_fun)
|
||||
{pos_neg_domain, dynamic_key?, context} = map_key_type(key, stack, context, of_fun)
|
||||
|
||||
expected_value_type =
|
||||
with [key] <- keys, {_, expected_value_type} <- map_fetch(expected, key) do
|
||||
with {[key], [], []} <- pos_neg_domain,
|
||||
{_, expected_value_type} <- map_fetch_key(expected, key) do
|
||||
expected_value_type
|
||||
else
|
||||
_ -> term()
|
||||
end
|
||||
|
||||
{value_type, context} = of_fun.(value, expected_value_type, stack, context)
|
||||
{{dynamic_key? or gradual?(value_type), keys, value_type}, context}
|
||||
{{pos_neg_domain, dynamic_key? or gradual?(value_type), value_type}, context}
|
||||
end)
|
||||
end
|
||||
|
||||
defp finite_key_type(key, _stack, context, _of_fun) when is_atom(key) do
|
||||
{false, [key], context}
|
||||
defp map_key_type(key, _stack, context, _of_fun) when is_atom(key) do
|
||||
{{[key], [], []}, false, context}
|
||||
end
|
||||
|
||||
defp finite_key_type(key, stack, context, of_fun) do
|
||||
defp map_key_type(key, stack, context, of_fun) do
|
||||
{key_type, context} = of_fun.(key, term(), stack, context)
|
||||
domain_keys = to_domain_keys(key_type)
|
||||
|
||||
case atom_fetch(key_type) do
|
||||
{:finite, list} -> {gradual?(key_type), list, context}
|
||||
_ -> {gradual?(key_type), :none, context}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Builds permutation of maps according to the given pairs types.
|
||||
"""
|
||||
def permutate_map(_pairs_types, %{mode: :traversal}, _of_map) do
|
||||
dynamic()
|
||||
end
|
||||
|
||||
def permutate_map(pairs_types, _stack, of_map) do
|
||||
{dynamic?, fallback, single, multiple, assert} =
|
||||
Enum.reduce(pairs_types, {false, none(), [], [], []}, fn
|
||||
{dynamic_pair?, keys, value_type}, {dynamic?, fallback, single, multiple, assert} ->
|
||||
dynamic? = dynamic? or dynamic_pair?
|
||||
|
||||
case keys do
|
||||
:none ->
|
||||
fallback = union(fallback, value_type)
|
||||
|
||||
{fallback, assert} =
|
||||
Enum.reduce(single, {fallback, assert}, fn {key, type}, {fallback, assert} ->
|
||||
{union(fallback, type), [key | assert]}
|
||||
end)
|
||||
|
||||
{fallback, assert} =
|
||||
Enum.reduce(multiple, {fallback, assert}, fn {keys, type}, {fallback, assert} ->
|
||||
{union(fallback, type), keys ++ assert}
|
||||
end)
|
||||
|
||||
{dynamic?, fallback, [], [], assert}
|
||||
|
||||
# Because a multiple key may override single keys, we can only
|
||||
# collect single keys while there are no multiples.
|
||||
[key] when multiple == [] ->
|
||||
{dynamic?, fallback, [{key, value_type} | single], multiple, assert}
|
||||
|
||||
keys ->
|
||||
{dynamic?, fallback, single, [{keys, value_type} | multiple], assert}
|
||||
end
|
||||
end)
|
||||
|
||||
map =
|
||||
case Enum.reverse(multiple) do
|
||||
[] ->
|
||||
of_map.(fallback, Enum.uniq(assert), Enum.reverse(single))
|
||||
|
||||
[{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)
|
||||
pos_neg_domain =
|
||||
case atom_fetch(key_type) do
|
||||
{:finite, list} -> {list, [], List.delete(domain_keys, :atom)}
|
||||
{:infinite, list} -> {[], list, domain_keys}
|
||||
:error -> {[], [], domain_keys}
|
||||
end
|
||||
|
||||
if dynamic?, do: dynamic(map), else: map
|
||||
{pos_neg_domain, gradual?(key_type), context}
|
||||
end
|
||||
|
||||
defp cartesian_map(lists) do
|
||||
@@ -293,7 +418,7 @@ defmodule Module.Types.Of do
|
||||
Handles instantiation of a new struct.
|
||||
"""
|
||||
# TODO: Type check the fields match the struct
|
||||
def struct_instance(struct, args, expected, meta, %{mode: mode} = stack, context, of_fun)
|
||||
def struct_instance(struct, args, expected, meta, stack, context, of_fun)
|
||||
when is_atom(struct) do
|
||||
{_info, context} = struct_info(struct, meta, stack, context)
|
||||
|
||||
@@ -301,10 +426,8 @@ defmodule Module.Types.Of do
|
||||
{args_types, context} =
|
||||
Enum.map_reduce(args, context, fn {key, value}, context when is_atom(key) ->
|
||||
value_type =
|
||||
with true <- mode != :traversal,
|
||||
{_, expected_value_type} <- map_fetch(expected, key) do
|
||||
expected_value_type
|
||||
else
|
||||
case map_fetch_key(expected, key) do
|
||||
{_, expected_value_type} -> expected_value_type
|
||||
_ -> term()
|
||||
end
|
||||
|
||||
@@ -351,45 +474,66 @@ defmodule Module.Types.Of do
|
||||
closed_map(pairs)
|
||||
end
|
||||
|
||||
## Binary
|
||||
## Bitstrings
|
||||
|
||||
@doc """
|
||||
Handles binaries.
|
||||
Handles bitstrings.
|
||||
|
||||
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 bitstring([], _kind, _stack, context) do
|
||||
{binary(), context}
|
||||
end
|
||||
|
||||
def binary([head], kind, stack, context) do
|
||||
binary_segment(head, kind, [head], stack, context)
|
||||
def bitstring([head], kind, stack, context) do
|
||||
{alignment, context} = bitstring_segment(head, kind, [head], stack, context)
|
||||
{alignment_to_type(alignment), context}
|
||||
end
|
||||
|
||||
def binary([head | tail], kind, stack, context) do
|
||||
context = binary_segment(head, kind, [head, @suffix], stack, context)
|
||||
binary_many(tail, kind, stack, context)
|
||||
def bitstring([head | tail], kind, stack, context) do
|
||||
{alignment, context} = bitstring_segment(head, kind, [head, @suffix], stack, context)
|
||||
bitstring_tail(tail, alignment, kind, stack, context)
|
||||
end
|
||||
|
||||
defp binary_many([last], kind, stack, context) do
|
||||
binary_segment(last, kind, [@prefix, last], stack, context)
|
||||
defp bitstring_tail([last], alignment, kind, stack, context) do
|
||||
{seg_alignment, context} = bitstring_segment(last, kind, [@prefix, last], stack, context)
|
||||
{alignment_to_type(alignment(seg_alignment, alignment)), 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)
|
||||
defp bitstring_tail([head | tail], alignment, kind, stack, context) do
|
||||
{seg_alignment, context} =
|
||||
bitstring_segment(head, kind, [@prefix, head, @suffix], stack, context)
|
||||
|
||||
bitstring_tail(tail, alignment(seg_alignment, alignment), kind, stack, context)
|
||||
end
|
||||
|
||||
defp alignment(left, right) when is_integer(left) and is_integer(right), do: left + right
|
||||
defp alignment(_left, _right), do: :unknown
|
||||
|
||||
defp alignment_to_type(:unknown), do: bitstring()
|
||||
defp alignment_to_type(integer) when rem(integer, 8) == 0, do: binary()
|
||||
defp alignment_to_type(_integer), do: bitstring_no_binary()
|
||||
|
||||
# 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)
|
||||
# so we just check the size.
|
||||
defp bitstring_segment({:"::", _meta, [left, right]}, kind, _args, stack, context)
|
||||
when is_binary(left) or is_number(left) do
|
||||
context
|
||||
{_type, alignment_type} = specifier_type(kind, right)
|
||||
{alignment_value, context} = specifier_size(kind, right, stack, {:default, context})
|
||||
|
||||
# We don't need to check for bitstrings because the left side
|
||||
# is either a binary (aligned), float (aligned), or integer
|
||||
# (which we check below).
|
||||
if alignment_type == :integer and alignment_value != :default do
|
||||
{alignment_value, context}
|
||||
else
|
||||
{0, context}
|
||||
end
|
||||
end
|
||||
|
||||
defp binary_segment({:"::", meta, [left, right]}, kind, args, stack, context) do
|
||||
type = specifier_type(kind, right)
|
||||
defp bitstring_segment({:"::", meta, [left, right]}, kind, args, stack, context) do
|
||||
{type, alignment_type} = specifier_type(kind, right)
|
||||
expr = {:<<>>, meta, args}
|
||||
|
||||
{actual, context} =
|
||||
@@ -406,10 +550,26 @@ defmodule Module.Types.Of do
|
||||
end
|
||||
|
||||
if compatible?(actual, type) do
|
||||
specifier_size(kind, right, stack, context)
|
||||
{alignment_value, context} = specifier_size(kind, right, stack, {:default, context})
|
||||
|
||||
case alignment_type do
|
||||
:aligned ->
|
||||
{0, context}
|
||||
|
||||
:integer when alignment_value == :default ->
|
||||
{0, context}
|
||||
|
||||
# There is no size, so the aligment depends on the type.
|
||||
# If the type is exclusively a binary, then it is aligned.
|
||||
:bitstring when alignment_value == :default ->
|
||||
if bitstring_no_binary_type?(actual), do: {:unknown, context}, else: {0, context}
|
||||
|
||||
_ ->
|
||||
{alignment_value, context}
|
||||
end
|
||||
else
|
||||
error = {:badbinary, kind, meta, expr, type, actual, context}
|
||||
error(error, meta, stack, context)
|
||||
{:unknown, error(error, meta, stack, context)}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -425,39 +585,48 @@ defmodule Module.Types.Of do
|
||||
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_type(:match, {:utf8, _, _}), do: {@integer, :aligned}
|
||||
defp specifier_type(:match, {:utf16, _, _}), do: {@integer, :aligned}
|
||||
defp specifier_type(:match, {:utf32, _, _}), do: {@integer, :aligned}
|
||||
defp specifier_type(:match, {:float, _, _}), do: {@float, :aligned}
|
||||
defp specifier_type(_kind, {:float, _, _}), do: {@integer_or_float, :aligned}
|
||||
defp specifier_type(_kind, {:utf8, _, _}), do: {@integer, :aligned}
|
||||
defp specifier_type(_kind, {:utf16, _, _}), do: {@integer, :aligned}
|
||||
defp specifier_type(_kind, {:utf32, _, _}), do: {@integer, :aligned}
|
||||
defp specifier_type(_kind, {:integer, _, _}), do: {@integer, :integer}
|
||||
defp specifier_type(_kind, {:bits, _, _}), do: {@bitstring, :bitstring}
|
||||
defp specifier_type(_kind, {:bitstring, _, _}), do: {@bitstring, :bitstring}
|
||||
defp specifier_type(_kind, {:bytes, _, _}), do: {@binary, :aligned}
|
||||
defp specifier_type(_kind, {:binary, _, _}), do: {@binary, :aligned}
|
||||
defp specifier_type(_kind, _specifier), do: {@integer, :integer}
|
||||
|
||||
defp specifier_size(kind, {:-, _, [left, right]}, stack, context) do
|
||||
specifier_size(kind, right, stack, specifier_size(kind, left, stack, context))
|
||||
defp specifier_size(kind, {:-, _, [left, right]}, stack, align_context) do
|
||||
specifier_size(kind, right, stack, specifier_size(kind, left, stack, align_context))
|
||||
end
|
||||
|
||||
defp specifier_size(:expr, {:size, _, [arg]} = expr, stack, context)
|
||||
when not is_integer(arg) do
|
||||
defp specifier_size(_, {:size, _, [arg]}, _stack, {unit, context})
|
||||
when is_integer(arg) do
|
||||
size = if unit == :default, do: arg, else: arg * unit
|
||||
{size, context}
|
||||
end
|
||||
|
||||
defp specifier_size(:expr, {:size, _, [arg]} = expr, stack, {_, context}) do
|
||||
{actual, context} = Module.Types.Expr.of_expr(arg, integer(), expr, stack, context)
|
||||
compatible_size(actual, expr, stack, context)
|
||||
{:unknown, compatible_size(actual, expr, stack, context)}
|
||||
end
|
||||
|
||||
defp specifier_size(_pattern_or_guard, {:size, _, [arg]} = expr, stack, context)
|
||||
when not is_integer(arg) do
|
||||
{actual, context} = Module.Types.Pattern.of_guard(arg, integer(), expr, stack, context)
|
||||
compatible_size(actual, expr, stack, context)
|
||||
defp specifier_size(match_or_guard, {:size, _, [arg]} = expr, stack, {_, context}) do
|
||||
{actual, context} = Module.Types.Pattern.of_size(match_or_guard, arg, expr, stack, context)
|
||||
{:unknown, compatible_size(actual, expr, stack, context)}
|
||||
end
|
||||
|
||||
defp specifier_size(_kind, _specifier, _stack, context) do
|
||||
context
|
||||
# We currently assume the unit always comes before size
|
||||
defp specifier_size(_, {:unit, _, [unit]}, _stack, {:default, context}) do
|
||||
{unit, context}
|
||||
end
|
||||
|
||||
defp specifier_size(_kind, _specifier, _stack, align_context) do
|
||||
align_context
|
||||
end
|
||||
|
||||
defp compatible_size(actual, expr, stack, context) do
|
||||
@@ -478,9 +647,12 @@ defmodule Module.Types.Of do
|
||||
"""
|
||||
def modules(type, fun, arity, hints \\ [], expr, meta, stack, context) do
|
||||
case atom_fetch(type) do
|
||||
{_, mods} ->
|
||||
{:finite, mods} ->
|
||||
{mods, context}
|
||||
|
||||
{:infinite, _} ->
|
||||
{[], context}
|
||||
|
||||
:error ->
|
||||
warning = {:badmodule, expr, type, fun, arity, hints, context}
|
||||
{[], error(warning, meta, stack, context)}
|
||||
@@ -493,23 +665,6 @@ defmodule Module.Types.Of do
|
||||
error(__MODULE__, warning, meta, stack, context)
|
||||
end
|
||||
|
||||
def format_diagnostic({:refine_head_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)
|
||||
])
|
||||
}
|
||||
end
|
||||
|
||||
def format_diagnostic({:badbinary, kind, meta, expr, expected_type, actual_type, context}) do
|
||||
type = if kind == :match, do: "matching", else: "construction"
|
||||
hints = if meta[:inferred_bitstring_spec], do: [:inferred_bitstring_spec], else: []
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,159 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
||||
|
||||
defmodule Module.Types.Traverse do
|
||||
@moduledoc false
|
||||
|
||||
# Traverses expressions to find local calls when inference is disabled.
|
||||
|
||||
# Literals
|
||||
def of_expr(literal, _stack, context)
|
||||
when is_atom(literal) or is_integer(literal) or is_float(literal) or is_binary(literal) or
|
||||
is_pid(literal) or literal == [] do
|
||||
context
|
||||
end
|
||||
|
||||
# [expr, ...]
|
||||
def of_expr(list, stack, context) when is_list(list) do
|
||||
Enum.reduce(list, context, &of_expr(&1, stack, &2))
|
||||
end
|
||||
|
||||
# {left, right}
|
||||
def of_expr({left, right}, stack, context) do
|
||||
context = of_expr(left, stack, context)
|
||||
of_expr(right, stack, context)
|
||||
end
|
||||
|
||||
# <<...>>
|
||||
def of_expr({:<<>>, _meta, args}, stack, context) do
|
||||
Enum.reduce(args, context, fn
|
||||
{:"::", _meta, [left, _right]}, context ->
|
||||
of_expr(left, stack, context)
|
||||
|
||||
expr, context ->
|
||||
of_expr(expr, stack, context)
|
||||
end)
|
||||
end
|
||||
|
||||
# Structs, map update, tail operator
|
||||
def of_expr({op, _meta, [left, right]}, stack, context) when op in [:%, :|] do
|
||||
context = of_expr(left, stack, context)
|
||||
of_expr(right, stack, context)
|
||||
end
|
||||
|
||||
# Tuples, maps
|
||||
def of_expr({container, _meta, exprs}, stack, context) when container in [:{}, :%{}] do
|
||||
Enum.reduce(exprs, context, &of_expr(&1, stack, &2))
|
||||
end
|
||||
|
||||
# left = right, left <_ right
|
||||
def of_expr({op, _meta, [_left, right]}, stack, context) when op in [:=, :<-] do
|
||||
# Skip the left side (pattern), only traverse right
|
||||
of_expr(right, stack, context)
|
||||
end
|
||||
|
||||
# Blocks
|
||||
def of_expr({:__block__, _, args}, stack, context) do
|
||||
Enum.reduce(args, context, &of_expr(&1, stack, &2))
|
||||
end
|
||||
|
||||
# cond do ... end
|
||||
def of_expr({:cond, _meta, [[{:do, clauses}]]}, stack, context) do
|
||||
Enum.reduce(clauses, context, fn {:->, _meta, [[head], body]}, context ->
|
||||
context = of_expr(head, stack, context)
|
||||
of_expr(body, stack, context)
|
||||
end)
|
||||
end
|
||||
|
||||
# Treat -> as patterns for simplicity
|
||||
def of_expr({:->, _, [_head, body]}, stack, context) do
|
||||
of_expr(body, stack, context)
|
||||
end
|
||||
|
||||
# case expr do ... end
|
||||
def of_expr({:case, _meta, [case_expr, [{:do, clauses}]]}, stack, context) do
|
||||
context = of_expr(case_expr, stack, context)
|
||||
of_expr(clauses, stack, context)
|
||||
end
|
||||
|
||||
# fn pat -> expr end
|
||||
def of_expr({:fn, _meta, clauses}, stack, context) do
|
||||
of_expr(clauses, stack, context)
|
||||
end
|
||||
|
||||
# try do ... end
|
||||
def of_expr({:try, _meta, [blocks]}, stack, context) do
|
||||
Enum.reduce(blocks, context, fn {_, clauses_or_body}, context ->
|
||||
of_expr(clauses_or_body, stack, context)
|
||||
end)
|
||||
end
|
||||
|
||||
# receive do ... end
|
||||
def of_expr({:receive, _meta, [blocks]}, stack, context) do
|
||||
Enum.reduce(blocks, context, fn
|
||||
{:do, clauses_or_empty_body}, context ->
|
||||
of_expr(clauses_or_empty_body, stack, context)
|
||||
|
||||
{:after, [{:->, _meta, [[timeout], body]}]}, context ->
|
||||
context = of_expr(timeout, stack, context)
|
||||
of_expr(body, stack, context)
|
||||
end)
|
||||
end
|
||||
|
||||
# for, with
|
||||
def of_expr({op, _meta, [_ | _] = args}, stack, context) when op in [:for, :with] do
|
||||
Enum.reduce(args, context, &of_expr(&1, stack, &2))
|
||||
end
|
||||
|
||||
# fun.(args)
|
||||
def of_expr({{:., _meta, [fun]}, _call_meta, args}, stack, context) do
|
||||
context = of_expr(fun, stack, context)
|
||||
Enum.reduce(args, context, &of_expr(&1, stack, &2))
|
||||
end
|
||||
|
||||
# remote.fun(args)
|
||||
def of_expr({{:., _, [remote, name]}, _meta, args}, stack, context)
|
||||
when is_atom(name) do
|
||||
context = of_expr(remote, stack, context)
|
||||
Enum.reduce(args, context, &of_expr(&1, stack, &2))
|
||||
end
|
||||
|
||||
# &Mod.fun/arity
|
||||
def of_expr({:&, _, [{:/, _, [{{:., _, [_remote, name]}, _, []}, arity]}]}, _stack, context)
|
||||
when is_atom(name) and is_integer(arity) do
|
||||
context
|
||||
end
|
||||
|
||||
# &fun/arity
|
||||
def of_expr({:&, meta, [{:/, _, [{name, _, _ctx}, arity]}]}, stack, context)
|
||||
when is_atom(name) and is_integer(arity) do
|
||||
local_fun(meta, name, arity, stack, context)
|
||||
end
|
||||
|
||||
# super(args)
|
||||
def of_expr({:super, meta, args}, stack, context) when is_list(args) do
|
||||
{_kind, name} = Keyword.fetch!(meta, :super)
|
||||
context = local_fun(meta, name, length(args), stack, context)
|
||||
Enum.reduce(args, context, &of_expr(&1, stack, &2))
|
||||
end
|
||||
|
||||
# local_fun(args)
|
||||
def of_expr({name, meta, args}, stack, context)
|
||||
when is_atom(name) and is_list(args) do
|
||||
context = local_fun(meta, name, length(args), stack, context)
|
||||
Enum.reduce(args, context, &of_expr(&1, stack, &2))
|
||||
end
|
||||
|
||||
# var
|
||||
def of_expr({name, _meta, ctx}, _stack, context)
|
||||
when is_atom(name) and is_atom(ctx) do
|
||||
context
|
||||
end
|
||||
|
||||
defp local_fun(meta, fun, arity, stack, context) do
|
||||
case stack.local_handler.(meta, {fun, arity}, stack, context) do
|
||||
false -> context
|
||||
{_kind, _info, context} -> context
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -129,6 +129,7 @@ defmodule OptionParser do
|
||||
* `:integer` - parses the value as an integer
|
||||
* `:float` - parses the value as a float
|
||||
* `:string` - parses the value as a string
|
||||
* `:regex` - parses the value as a regular expression with Unicode support
|
||||
|
||||
If a switch can't be parsed according to the given type, it is
|
||||
returned in the invalid options list.
|
||||
@@ -282,11 +283,11 @@ defmodule OptionParser do
|
||||
|
||||
iex> OptionParser.parse!(["--limit", "xyz"], strict: [limit: :integer])
|
||||
** (OptionParser.ParseError) 1 error found!
|
||||
--limit : Expected type integer, got "xyz"
|
||||
--limit : Expected type integer, got "xyz"...
|
||||
|
||||
iex> OptionParser.parse!(["--unknown", "xyz"], strict: [])
|
||||
** (OptionParser.ParseError) 1 error found!
|
||||
--unknown : Unknown option
|
||||
--unknown : Unknown option...
|
||||
|
||||
iex> OptionParser.parse!(
|
||||
...> ["-l", "xyz", "-f", "bar"],
|
||||
@@ -295,7 +296,7 @@ defmodule OptionParser do
|
||||
...> )
|
||||
** (OptionParser.ParseError) 2 errors found!
|
||||
-l : Expected type integer, got "xyz"
|
||||
-f : Expected type integer, got "bar"
|
||||
-f : Expected type integer, got "bar"...
|
||||
|
||||
"""
|
||||
@spec parse!(argv, options) :: {parsed, argv}
|
||||
@@ -354,7 +355,7 @@ defmodule OptionParser do
|
||||
...> strict: [number: :integer]
|
||||
...> )
|
||||
** (OptionParser.ParseError) 1 error found!
|
||||
--number : Expected type integer, got "lib"
|
||||
--number : Expected type integer, got "lib"...
|
||||
|
||||
iex> OptionParser.parse_head!(
|
||||
...> ["--verbose", "--source", "lib", "test/enum_test.exs", "--unlock"],
|
||||
@@ -362,7 +363,7 @@ defmodule OptionParser do
|
||||
...> )
|
||||
** (OptionParser.ParseError) 2 errors found!
|
||||
--verbose : Missing argument of type integer
|
||||
--source : Expected type integer, got "lib"
|
||||
--source : Expected type integer, got "lib"...
|
||||
|
||||
"""
|
||||
@spec parse_head!(argv, options) :: {parsed, argv}
|
||||
@@ -664,7 +665,7 @@ defmodule OptionParser do
|
||||
end
|
||||
|
||||
defp validate_switch({_name, type_or_type_and_modifiers}) do
|
||||
valid = [:boolean, :count, :integer, :float, :string, :keep]
|
||||
valid = [:boolean, :count, :integer, :float, :string, :regex, :keep]
|
||||
invalid = List.wrap(type_or_type_and_modifiers) -- valid
|
||||
|
||||
if invalid != [] do
|
||||
@@ -704,6 +705,12 @@ defmodule OptionParser do
|
||||
_ -> {true, value}
|
||||
end
|
||||
|
||||
:regex in kinds ->
|
||||
case Regex.compile(value, "u") do
|
||||
{:ok, regex} -> {false, regex}
|
||||
{:error, _} -> {true, value}
|
||||
end
|
||||
|
||||
true ->
|
||||
{false, value}
|
||||
end
|
||||
@@ -863,15 +870,20 @@ defmodule OptionParser do
|
||||
error_count = length(errors)
|
||||
error = if error_count == 1, do: "error", else: "errors"
|
||||
|
||||
"#{error_count} #{error} found!\n" <>
|
||||
Enum.map_join(errors, "\n", &format_error(&1, opts, types))
|
||||
slogan =
|
||||
"#{error_count} #{error} found!\n" <>
|
||||
Enum.map_join(errors, "\n", &format_error(&1, opts, types))
|
||||
|
||||
case format_available_options(opts, types) do
|
||||
"" -> slogan
|
||||
available_options -> slogan <> "\n\n#{available_options}"
|
||||
end
|
||||
end
|
||||
|
||||
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}"
|
||||
@@ -891,7 +903,13 @@ defmodule OptionParser do
|
||||
|
||||
defp format_error({option, value}, opts, types) do
|
||||
type = get_type(option, opts, types)
|
||||
"#{option} : Expected type #{type}, got #{inspect(value)}"
|
||||
|
||||
with :regex <- type,
|
||||
{:error, {reason, position}} <- Regex.compile(value, "u") do
|
||||
"#{option} : Invalid regular expression #{inspect(value)}: #{reason} at position #{position}"
|
||||
else
|
||||
_ -> "#{option} : Expected type #{type}, got #{inspect(value)}"
|
||||
end
|
||||
end
|
||||
|
||||
defp get_type(option, opts, types) do
|
||||
@@ -917,4 +935,57 @@ defmodule OptionParser do
|
||||
option = String.replace(source, "_", "-")
|
||||
if score < current, do: best, else: {option, score}
|
||||
end
|
||||
|
||||
defp format_available_options(opts, switches) do
|
||||
reverse_aliases =
|
||||
opts
|
||||
|> Keyword.get(:aliases, [])
|
||||
|> Enum.reduce(%{}, fn {alias, target}, acc ->
|
||||
Map.update(acc, target, [alias], &[alias | &1])
|
||||
end)
|
||||
|
||||
formatted_options =
|
||||
switches
|
||||
|> Enum.sort()
|
||||
|> Enum.map(fn {name, types} ->
|
||||
types = List.wrap(types)
|
||||
|
||||
case types |> List.delete(:keep) |> List.first(:string) do
|
||||
:boolean ->
|
||||
base = "#{to_switch(name)}, #{to_switch(name, "--no-")}"
|
||||
add_aliases(base, name, reverse_aliases)
|
||||
|
||||
type ->
|
||||
base = "#{to_switch(name)} #{String.upcase(Atom.to_string(type))}"
|
||||
base = add_aliases(base, name, reverse_aliases)
|
||||
|
||||
if :keep in types do
|
||||
base <> " (may be given more than once)"
|
||||
else
|
||||
base
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
if formatted_options == [] do
|
||||
""
|
||||
else
|
||||
"Supported options:\n" <> Enum.map_join(formatted_options, "\n", &(" " <> &1))
|
||||
end
|
||||
end
|
||||
|
||||
defp add_aliases(base, name, reverse_aliases) do
|
||||
case Map.get(reverse_aliases, name, []) do
|
||||
[] ->
|
||||
base
|
||||
|
||||
alias_list ->
|
||||
alias_str =
|
||||
alias_list
|
||||
|> Enum.sort()
|
||||
|> Enum.map_join(", ", &("-" <> Atom.to_string(&1)))
|
||||
|
||||
base <> " (alias: #{alias_str})"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -450,7 +450,7 @@ defmodule PartitionSupervisor do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a list with information about all children.
|
||||
Returns a list with information about all children of the given supervisor.
|
||||
|
||||
This function returns a list of tuples containing:
|
||||
|
||||
@@ -546,7 +546,7 @@ defmodule PartitionSupervisor do
|
||||
end
|
||||
|
||||
@doc false
|
||||
def unregister_name(_, _) do
|
||||
def unregister_name(_) do
|
||||
raise "{:via, PartitionSupervisor, _} cannot be given on unregistration"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -22,6 +22,8 @@ defmodule Path do
|
||||
"""
|
||||
@type t :: IO.chardata()
|
||||
|
||||
@type relative_to_opts :: [force: boolean()]
|
||||
|
||||
@doc """
|
||||
Converts the given path to an absolute one.
|
||||
|
||||
@@ -401,7 +403,7 @@ defmodule Path do
|
||||
Path.relative_to("../foo", "/usr/local") #=> "../foo"
|
||||
|
||||
"""
|
||||
@spec relative_to(t, t, keyword) :: binary
|
||||
@spec relative_to(t, t, relative_to_opts) :: binary
|
||||
def relative_to(path, cwd, opts \\ []) when is_list(opts) do
|
||||
os_type = major_os_type()
|
||||
split_path = split(path)
|
||||
@@ -479,7 +481,7 @@ defmodule Path do
|
||||
|
||||
Check `relative_to/3` for the supported options.
|
||||
"""
|
||||
@spec relative_to_cwd(t, keyword) :: binary
|
||||
@spec relative_to_cwd(t, relative_to_opts) :: binary
|
||||
def relative_to_cwd(path, opts \\ []) when is_list(opts) do
|
||||
case :file.get_cwd() do
|
||||
{:ok, base} -> relative_to(path, IO.chardata_to_string(base), opts)
|
||||
@@ -801,7 +803,7 @@ defmodule Path do
|
||||
Path.wildcard("projects/*/ebin/**/*.{beam,app}")
|
||||
|
||||
"""
|
||||
@spec wildcard(t, keyword) :: [binary]
|
||||
@spec wildcard(t, match_dot: boolean()) :: [binary]
|
||||
def wildcard(glob, opts \\ []) when is_list(opts) do
|
||||
mod = if Keyword.get(opts, :match_dot), do: :file, else: Path.Wildcard
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ defmodule Port do
|
||||
|
||||
The port can be opened through four main mechanisms.
|
||||
|
||||
As a short summary, prefer to using the `:spawn` and `:spawn_executable`
|
||||
As a short summary, prefer to use the `:spawn` and `:spawn_executable`
|
||||
options mentioned below. The other two options, `:spawn_driver` and `:fd`
|
||||
are for advanced usage within the VM. Also consider using `System.cmd/3`
|
||||
if all you want is to execute a program and retrieve its return value.
|
||||
|
||||
+16
-17
@@ -202,24 +202,23 @@ 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`
|
||||
or `:kill`:
|
||||
Exit behavior differs based on the value of `reason`:
|
||||
|
||||
1. If `pid` is not trapping exits, `pid` will exit with the given
|
||||
`reason`.
|
||||
- If `:normal`, `pid` will not exit unless it is the calling process, in
|
||||
which case it will exit with the reason `:normal`. If it is trapping exits,
|
||||
the exit signal is transformed into a message `{:EXIT, from, :normal}` and
|
||||
delivered to its message queue.
|
||||
|
||||
2. If `pid` is trapping exits, the exit signal is transformed into a
|
||||
message `{:EXIT, from, reason}` and delivered to the message queue
|
||||
of `pid`.
|
||||
- If `:kill`, which occurs when `Process.exit(pid, :kill)` is called, an
|
||||
untrappable exit signal is sent to `pid` which will unconditionally exit
|
||||
with reason `:killed`.
|
||||
|
||||
If `reason` is the atom `:normal`, `pid` will not exit (unless `pid` is
|
||||
the calling process, in which case it will exit with the reason `:normal`).
|
||||
If it is trapping exits, the exit signal is transformed into a message
|
||||
`{:EXIT, from, :normal}` and delivered to its message queue.
|
||||
- If any other term and `pid` is not trapping exits, `pid` will exit with
|
||||
the given `reason`.
|
||||
|
||||
If `reason` is the atom `:kill`, that is if `Process.exit(pid, :kill)` is called,
|
||||
an untrappable exit signal is sent to `pid` which will unconditionally exit
|
||||
with reason `:killed`.
|
||||
- If any other term and `pid` is trapping exits, the exit signal is
|
||||
transformed into a message `{:EXIT, from, reason}` and delivered to its
|
||||
message queue.
|
||||
|
||||
Inlined by the compiler.
|
||||
|
||||
@@ -535,7 +534,7 @@ defmodule Process do
|
||||
If the process is already dead when calling `Process.monitor/1`, a
|
||||
`:DOWN` message is delivered immediately.
|
||||
|
||||
See ["The need for monitoring"](genservers.md#the-need-for-monitoring)
|
||||
See ["Links and monitors"](genservers.md#links-and-monitors)
|
||||
for an example. See `:erlang.monitor/2` for more information.
|
||||
|
||||
Inlined by the compiler.
|
||||
@@ -840,7 +839,7 @@ defmodule Process do
|
||||
@spec flag(:min_bin_vheap_size, non_neg_integer) :: non_neg_integer
|
||||
@spec flag(:min_heap_size, non_neg_integer) :: non_neg_integer
|
||||
@spec flag(:priority, priority_level) :: priority_level
|
||||
@spec flag(:save_calls, 0..10000) :: 0..10000
|
||||
@spec flag(:save_calls, 0..10_000) :: 0..10_000
|
||||
@spec flag(:sensitive, boolean) :: boolean
|
||||
@spec flag(:trap_exit, boolean) :: boolean
|
||||
defdelegate flag(flag, value), to: :erlang, as: :process_flag
|
||||
@@ -859,7 +858,7 @@ defmodule Process do
|
||||
|
||||
Inlined by the compiler.
|
||||
"""
|
||||
@spec flag(pid, :save_calls, 0..10000) :: 0..10000
|
||||
@spec flag(pid, :save_calls, 0..10_000) :: 0..10_000
|
||||
defdelegate flag(pid, flag, value), to: :erlang, as: :process_flag
|
||||
|
||||
@doc """
|
||||
|
||||
+91
-54
@@ -21,7 +21,7 @@ defmodule Protocol do
|
||||
the data structure.
|
||||
|
||||
Although Elixir includes specific functions such as `tuple_size`,
|
||||
`binary_size` and `map_size`, sometimes we want to be able to
|
||||
`byte_size` and `map_size`, sometimes we want to be able to
|
||||
retrieve the size of a data structure regardless of its type.
|
||||
In Elixir we can write polymorphic code, i.e. code that works
|
||||
with different shapes/types, by using protocols. A size protocol
|
||||
@@ -267,6 +267,8 @@ defmodule Protocol do
|
||||
|
||||
@optional_callbacks __deriving__: 2
|
||||
|
||||
@elixir_checker_version :elixir_erl.checker_version()
|
||||
|
||||
@doc false
|
||||
defmacro def(signature)
|
||||
|
||||
@@ -451,7 +453,7 @@ defmodule Protocol do
|
||||
true
|
||||
|
||||
"""
|
||||
@spec extract_protocols([charlist | String.t()]) :: [atom]
|
||||
@spec extract_protocols([charlist | String.t() | {charlist, [charlist]}]) :: [atom]
|
||||
def extract_protocols(paths) do
|
||||
extract_matching_by_attribute(paths, [?E, ?l, ?i, ?x, ?i, ?r, ?.], fn module, attributes ->
|
||||
case attributes[:__protocol__] do
|
||||
@@ -480,7 +482,7 @@ defmodule Protocol do
|
||||
true
|
||||
|
||||
"""
|
||||
@spec extract_impls(module, [charlist | String.t()]) :: [atom]
|
||||
@spec extract_impls(module, [charlist | String.t() | {charlist, [charlist]}]) :: [atom]
|
||||
def extract_impls(protocol, paths) when is_atom(protocol) do
|
||||
prefix = Atom.to_charlist(protocol) ++ [?.]
|
||||
|
||||
@@ -494,17 +496,25 @@ 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)),
|
||||
file <- list_dir(path),
|
||||
{path, files} = list_dir(path),
|
||||
file <- files,
|
||||
mod = extract_from_file(path, file, prefix, callback),
|
||||
do: mod
|
||||
end
|
||||
|
||||
# Do not use protocols as they may be consolidating
|
||||
defp list_dir({path, files}) when is_list(path) and is_list(files) do
|
||||
{path, files}
|
||||
end
|
||||
|
||||
defp list_dir(path) when is_binary(path) do
|
||||
list_dir(String.to_charlist(path))
|
||||
end
|
||||
|
||||
defp list_dir(path) when is_list(path) do
|
||||
case :file.list_dir(path) do
|
||||
{:ok, files} -> files
|
||||
_ -> []
|
||||
{:ok, files} -> {path, files}
|
||||
_ -> {path, []}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -564,31 +574,34 @@ defmodule Protocol do
|
||||
# Ensure the types are sorted so the compiled beam is deterministic
|
||||
types = Enum.sort(types)
|
||||
|
||||
with {:ok, any, definitions, signatures, compile_info} <- beam_protocol(protocol),
|
||||
{:ok, definitions, signatures} <-
|
||||
consolidate(protocol, any, definitions, signatures, types),
|
||||
do: compile(definitions, signatures, compile_info)
|
||||
with {:ok, any, definitions, checker, compile_info} <- beam_protocol(protocol),
|
||||
{:ok, definitions, checker} <-
|
||||
consolidate(protocol, any, definitions, checker, types),
|
||||
do: compile(definitions, checker, compile_info)
|
||||
end
|
||||
|
||||
defp beam_protocol(protocol) do
|
||||
chunk_ids = [:debug_info, [?D, ?o, ?c, ?s]]
|
||||
chunk_ids = [:debug_info, [?E, ?x, ?C, ?k], [?D, ?o, ?c, ?s]]
|
||||
opts = [:allow_missing_chunks]
|
||||
|
||||
case :beam_lib.chunks(beam_file(protocol), chunk_ids, opts) do
|
||||
{:ok, {^protocol, [{:debug_info, debug_info} | chunks]}} ->
|
||||
{:ok, {^protocol, [{:debug_info, debug_info}, {_, checker} | chunks]}} ->
|
||||
{:debug_info_v1, _backend, {:elixir_v1, module_map, specs}} = debug_info
|
||||
%{attributes: attributes, definitions: definitions} = module_map
|
||||
|
||||
# Protocols in precompiled archives may not have signatures, so we default to an empty map.
|
||||
# TODO: Remove this on Elixir v1.23.
|
||||
signatures = Map.get(module_map, :signatures, %{})
|
||||
|
||||
chunks = :lists.filter(fn {_name, value} -> value != :missing_chunk end, chunks)
|
||||
chunks = :lists.map(fn {name, value} -> {List.to_string(name), value} end, chunks)
|
||||
|
||||
case attributes[:__protocol__] do
|
||||
[fallback_to_any: any] ->
|
||||
{:ok, any, definitions, signatures, {module_map, specs, chunks}}
|
||||
checker =
|
||||
with true <- is_binary(checker),
|
||||
{@elixir_checker_version, contents} <- :erlang.binary_to_term(checker) do
|
||||
contents
|
||||
else
|
||||
_ -> nil
|
||||
end
|
||||
|
||||
chunks = :lists.filter(fn {_name, value} -> value != :missing_chunk end, chunks)
|
||||
chunks = :lists.map(fn {name, value} -> {List.to_string(name), value} end, chunks)
|
||||
{:ok, any, definitions, checker, {module_map, specs, chunks}}
|
||||
|
||||
_ ->
|
||||
{:error, :not_a_protocol}
|
||||
@@ -607,7 +620,7 @@ defmodule Protocol do
|
||||
end
|
||||
|
||||
# Consolidate the protocol for faster implementations and fine-grained type information.
|
||||
defp consolidate(protocol, fallback_to_any?, definitions, signatures, types) do
|
||||
defp consolidate(protocol, fallback_to_any?, definitions, checker, types) do
|
||||
case List.keytake(definitions, {:__protocol__, 1}, 0) do
|
||||
{protocol_def, definitions} ->
|
||||
types = if fallback_to_any?, do: types, else: List.delete(types, Any)
|
||||
@@ -623,25 +636,37 @@ defmodule Protocol do
|
||||
protocol_def = change_protocol(protocol_def, types)
|
||||
impl_for = change_impl_for(impl_for, protocol, types)
|
||||
struct_impl_for = change_struct_impl_for(struct_impl_for, protocol, types, structs)
|
||||
new_signatures = new_signatures(definitions, protocol_funs, protocol, types)
|
||||
|
||||
definitions = [protocol_def, impl_for, impl_for!, struct_impl_for] ++ definitions
|
||||
signatures = Enum.into(new_signatures, signatures)
|
||||
{:ok, definitions, signatures}
|
||||
|
||||
checker =
|
||||
if checker do
|
||||
update_in(checker.exports, fn exports ->
|
||||
signatures = new_signatures(definitions, protocol_funs, protocol, types, structs)
|
||||
|
||||
for {fun, info} <- exports do
|
||||
if sig = Map.get(signatures, fun) do
|
||||
{fun, %{info | sig: sig}}
|
||||
else
|
||||
{fun, info}
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
{:ok, definitions, checker}
|
||||
|
||||
nil ->
|
||||
{:error, :not_a_protocol}
|
||||
end
|
||||
end
|
||||
|
||||
defp new_signatures(definitions, protocol_funs, protocol, types) do
|
||||
defp new_signatures(definitions, protocol_funs, protocol, types, structs) do
|
||||
alias Module.Types.Descr
|
||||
types_minus_any = List.delete(types, Any)
|
||||
|
||||
clauses =
|
||||
types
|
||||
|> List.delete(Any)
|
||||
|> Enum.map(fn impl ->
|
||||
{[Module.Types.Of.impl(impl)], Descr.atom([__concat__(protocol, impl)])}
|
||||
Enum.map(types_minus_any, fn impl ->
|
||||
{[Module.Types.Of.impl(impl, :open)], Descr.atom([__concat__(protocol, impl)])}
|
||||
end)
|
||||
|
||||
{domain, impl_for, impl_for!} =
|
||||
@@ -656,10 +681,16 @@ defmodule Protocol do
|
||||
end
|
||||
|
||||
_ ->
|
||||
structs_domain =
|
||||
case structs do
|
||||
[] -> Descr.none()
|
||||
_ -> Descr.open_map(__struct__: Descr.atom(structs))
|
||||
end
|
||||
|
||||
domain =
|
||||
clauses
|
||||
|> Enum.map(fn {[domain], _} -> domain end)
|
||||
|> Enum.reduce(&Descr.union/2)
|
||||
Enum.reduce(types_minus_any -- structs, structs_domain, fn impl, acc ->
|
||||
Descr.union(Module.Types.Of.impl(impl, :open), acc)
|
||||
end)
|
||||
|
||||
not_domain = Descr.negation(domain)
|
||||
|
||||
@@ -680,10 +711,12 @@ defmodule Protocol do
|
||||
{fun_arity, {:strong, nil, [{[domain | rest], Descr.dynamic()}]}}
|
||||
end
|
||||
|
||||
[
|
||||
{{:impl_for, 1}, {:strong, [Descr.term()], impl_for}},
|
||||
{{:impl_for!, 1}, {:strong, [domain], impl_for!}}
|
||||
] ++ new_signatures
|
||||
Map.new(
|
||||
[
|
||||
{{:impl_for, 1}, {:strong, [Descr.term()], impl_for}},
|
||||
{{:impl_for!, 1}, {:strong, [domain], impl_for!}}
|
||||
] ++ new_signatures
|
||||
)
|
||||
end
|
||||
|
||||
defp get_protocol_functions({_name, _kind, _meta, clauses}) do
|
||||
@@ -752,11 +785,9 @@ defmodule Protocol do
|
||||
end
|
||||
|
||||
# Finally compile the module and emit its bytecode.
|
||||
defp compile(definitions, signatures, {module_map, specs, docs_chunk}) do
|
||||
# Protocols in precompiled archives may not have signatures, so we default to an empty map.
|
||||
# TODO: Remove this on Elixir v1.23.
|
||||
module_map = %{module_map | definitions: definitions} |> Map.put(:signatures, signatures)
|
||||
{:ok, :elixir_erl.consolidate(module_map, specs, docs_chunk)}
|
||||
defp compile(definitions, checker, {module_map, specs, docs_chunk}) do
|
||||
module_map = %{module_map | definitions: definitions}
|
||||
{:ok, :elixir_erl.consolidate(module_map, checker, specs, docs_chunk)}
|
||||
end
|
||||
|
||||
## Definition callbacks
|
||||
@@ -769,16 +800,7 @@ defmodule Protocol do
|
||||
@before_compile Protocol
|
||||
|
||||
# We don't allow function definition inside protocols
|
||||
import Kernel,
|
||||
except: [
|
||||
def: 1,
|
||||
def: 2,
|
||||
defdelegate: 2,
|
||||
defguard: 1,
|
||||
defguardp: 1,
|
||||
defstruct: 1,
|
||||
defexception: 1
|
||||
]
|
||||
import Kernel, except: [def: 1, def: 2]
|
||||
|
||||
# Import the new `def` that is used by protocols
|
||||
import Protocol, only: [def: 1]
|
||||
@@ -841,6 +863,21 @@ defmodule Protocol do
|
||||
)
|
||||
end
|
||||
|
||||
extra =
|
||||
((Module.definitions_in(env.module, :def) ++ Module.definitions_in(env.module, :defmacro)) --
|
||||
functions) --
|
||||
[impl_for: 1, impl_for!: 1, __protocol__: 1, __deriving__: 2, __deriving__: 3]
|
||||
|
||||
# TODO: Make an error on Elixir v2.0
|
||||
if extra != [] do
|
||||
warn(
|
||||
"protocols can only define functions without implementation via def/1, found: " <>
|
||||
Enum.map_join(extra, ", ", fn {name, arity} -> "#{name}/#{arity}" end),
|
||||
env,
|
||||
nil
|
||||
)
|
||||
end
|
||||
|
||||
callback_metas = callback_metas(env.module, :callback)
|
||||
callbacks = :maps.keys(callback_metas)
|
||||
|
||||
|
||||
@@ -207,12 +207,6 @@ defmodule Range do
|
||||
%Range{first: first, last: last, step: step}
|
||||
end
|
||||
|
||||
def new(first, last) do
|
||||
raise ArgumentError,
|
||||
"ranges (first..last) expect both sides to be integers, " <>
|
||||
"got: #{inspect(first)}..#{inspect(last)}"
|
||||
end
|
||||
|
||||
@doc """
|
||||
Creates a new range with `step`.
|
||||
|
||||
@@ -290,9 +284,12 @@ defmodule Range do
|
||||
"""
|
||||
@doc since: "1.14.0"
|
||||
@spec shift(t, integer) :: t
|
||||
def shift(first..last//step, steps_to_shift)
|
||||
when is_integer(steps_to_shift) do
|
||||
new(first + steps_to_shift * step, last + steps_to_shift * step, step)
|
||||
def shift(%Range{} = range, 0), do: range
|
||||
|
||||
def shift(first..last//step, steps_to_shift) when is_integer(steps_to_shift) do
|
||||
shift = steps_to_shift * step
|
||||
|
||||
new(first + shift, last + shift, step)
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
||||
@@ -27,8 +27,9 @@ defmodule Record do
|
||||
|
||||
## Types
|
||||
|
||||
Types can be defined for tuples with the `record/2` macro (only available in
|
||||
typespecs). This macro will expand to a tuple as seen in the example below:
|
||||
Types can be defined for tuples with the `record/2` construct (which is only
|
||||
available in typespecs), with the record name as an atom and a keyword list
|
||||
of fields and their types as argument:
|
||||
|
||||
defmodule MyModule do
|
||||
require Record
|
||||
@@ -45,6 +46,13 @@ defmodule Record do
|
||||
a module by calling `Code.fetch_docs/1`.
|
||||
"""
|
||||
|
||||
@type extract_opts :: [
|
||||
from: binary(),
|
||||
from_lib: binary(),
|
||||
includes: [binary()],
|
||||
macros: keyword()
|
||||
]
|
||||
|
||||
@doc """
|
||||
Extracts record information from an Erlang file.
|
||||
|
||||
@@ -102,7 +110,7 @@ defmodule Record do
|
||||
]
|
||||
|
||||
"""
|
||||
@spec extract(name :: atom, keyword) :: keyword
|
||||
@spec extract(name :: atom, extract_opts) :: keyword
|
||||
def extract(name, opts) when is_atom(name) and is_list(opts) do
|
||||
Record.Extractor.extract(name, opts)
|
||||
end
|
||||
@@ -119,7 +127,7 @@ defmodule Record do
|
||||
Accepts the same options as listed for `Record.extract/2`.
|
||||
|
||||
"""
|
||||
@spec extract_all(keyword) :: [{name :: atom, keyword}]
|
||||
@spec extract_all(extract_opts) :: [{name :: atom, keyword}]
|
||||
def extract_all(opts) when is_list(opts) do
|
||||
Record.Extractor.extract_all(opts)
|
||||
end
|
||||
|
||||
+159
-14
@@ -3,6 +3,8 @@
|
||||
# SPDX-FileCopyrightText: 2012 Plataformatec
|
||||
|
||||
defmodule Regex do
|
||||
# TODO: Remove the "Starting from Erlang/OTP 28" part in the Modifiers'
|
||||
# section once Erlang/OTP 28+ is exclusively supported.
|
||||
@moduledoc ~S"""
|
||||
Provides regular expressions for Elixir.
|
||||
|
||||
@@ -76,9 +78,16 @@ defmodule Regex do
|
||||
|
||||
* `:caseless` (i) - adds case insensitivity
|
||||
|
||||
* `:dotall` (s) - causes dot to match newlines and also set newline to
|
||||
anycrlf; the new line setting can be overridden by setting `(*CR)` or
|
||||
`(*LF)` or `(*CRLF)` or `(*ANY)` according to `:re` documentation
|
||||
* `:dotall` (s) - causes dot to match newlines and also sets newline to
|
||||
`(*ANYCRLF)`.\
|
||||
The new line setting, as described in the [`:re` documentation](`:re`),
|
||||
can be overridden by starting the regular expression pattern with:
|
||||
* `(*CR)` - carriage return
|
||||
* `(*LF)` - line feed
|
||||
* `(*CRLF)` - carriage return, followed by line feed
|
||||
* `(*ANYCRLF)` - any of the three above
|
||||
* `(*ANY)` - all Unicode newline sequences
|
||||
* _Starting from Erlang/OTP 28, `(*NUL)` - the NUL character (binary zero)_
|
||||
|
||||
* `:multiline` (m) - causes `^` and `$` to mark the beginning and end of
|
||||
each line; use `\A` and `\z` to match the end or beginning of the string
|
||||
@@ -92,6 +101,13 @@ defmodule Regex do
|
||||
* `:ungreedy` (U) - inverts the "greediness" of the regexp
|
||||
(the previous `r` option is deprecated in favor of `U`)
|
||||
|
||||
* `:export` (E) (since Elixir 1.19.3) - uses an exported pattern
|
||||
which can be shared across nodes or passed through config, at the cost of a runtime
|
||||
overhead to re-import it every time it is executed.
|
||||
This modifier only has an effect starting on Erlang/OTP 28, and it is ignored
|
||||
on older versions (i.e. `~r/foo/E == ~r/foo/`). This is because patterns cannot
|
||||
and do not need to be exported in order to be shared in these versions.
|
||||
|
||||
## Captures
|
||||
|
||||
Many functions in this module handle what to capture in a regex
|
||||
@@ -164,6 +180,11 @@ defmodule Regex do
|
||||
|
||||
@type t :: %__MODULE__{re_pattern: term, source: binary, opts: [term]}
|
||||
|
||||
@type named_captures_opts :: [
|
||||
return: :binary | :index,
|
||||
offset: non_neg_integer()
|
||||
]
|
||||
|
||||
defmodule CompileError do
|
||||
@moduledoc """
|
||||
An exception raised when a regular expression could not be compiled.
|
||||
@@ -237,7 +258,7 @@ defmodule Regex do
|
||||
This checks the version stored in the regular expression
|
||||
and recompiles the regex in case of version mismatch.
|
||||
"""
|
||||
# Remove me on Elixir v1.22
|
||||
# TODO: Deprecate on Elixir v1.22
|
||||
@doc deprecated: "It can be removed and it has no effect"
|
||||
@doc since: "1.4.0"
|
||||
def recompile(%Regex{} = regex) do
|
||||
@@ -247,17 +268,46 @@ defmodule Regex do
|
||||
@doc """
|
||||
Recompiles the existing regular expression and raises `Regex.CompileError` in case of errors.
|
||||
"""
|
||||
# Remove me on Elixir v1.22
|
||||
# TODO: Deprecate on Elixir v1.22
|
||||
@doc deprecated: "It can be removed and it has no effect"
|
||||
@doc since: "1.4.0"
|
||||
def recompile!(regex) do
|
||||
regex
|
||||
end
|
||||
|
||||
@doc """
|
||||
Imports a `regex` that has been exported, otherwise returns the `regex` unchanged.
|
||||
|
||||
This means it will lose the ability to be sent across nodes or passed through config,
|
||||
but will be faster since it won't need to be imported on the fly every time it is executed.
|
||||
|
||||
Exported regexes only exist on OTP 28, so this has no effect on older versions.
|
||||
|
||||
## Examples
|
||||
|
||||
Regex.import(~r/foo/E)
|
||||
~r/foo/
|
||||
|
||||
Regex.import(~r/foo/)
|
||||
~r/foo/
|
||||
|
||||
"""
|
||||
@doc since: "1.20.0"
|
||||
@spec import(t) :: t
|
||||
def import(%Regex{re_pattern: re_pattern} = regex) do
|
||||
case re_pattern do
|
||||
{:re_exported_pattern, _, _, _, _} ->
|
||||
%{regex | re_pattern: :re.import(re_pattern), opts: regex.opts -- [:export]}
|
||||
|
||||
_ ->
|
||||
regex
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the version of the underlying Regex engine.
|
||||
"""
|
||||
# Remove me on Elixir v1.22
|
||||
# TODO: Deprecate on Elixir v1.22
|
||||
@doc deprecated: "Use :re.version() instead"
|
||||
@doc since: "1.4.0"
|
||||
def version do
|
||||
@@ -301,7 +351,7 @@ defmodule Regex do
|
||||
* `:capture` - what to capture in the result. See the ["Captures" section](#module-captures)
|
||||
to see the possible capture values.
|
||||
* `:offset` - (since v1.12.0) specifies the starting offset to match in the given string.
|
||||
Defaults to zero.
|
||||
Defaults to `0`.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -321,7 +371,7 @@ defmodule Regex do
|
||||
["d", ""]
|
||||
|
||||
"""
|
||||
@spec run(t, binary, [term]) :: nil | [binary] | [{integer, integer}]
|
||||
@spec run(t, binary, capture_opts) :: nil | [binary] | [{integer, integer}]
|
||||
def run(regex, string, options \\ [])
|
||||
|
||||
def run(%Regex{} = regex, string, options) when is_binary(string) do
|
||||
@@ -343,6 +393,8 @@ defmodule Regex do
|
||||
|
||||
* `:return` - when set to `:index`, returns byte index and match length.
|
||||
Defaults to `:binary`.
|
||||
* `:offset` - (since v1.12.0) specifies the starting offset to match in the given string.
|
||||
Defaults to `0`.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -363,7 +415,7 @@ defmodule Regex do
|
||||
|
||||
You can then use `binary_part/3` to fetch the relevant part from the given string.
|
||||
"""
|
||||
@spec named_captures(t, String.t(), keyword) :: map | nil
|
||||
@spec named_captures(t, String.t(), named_captures_opts) :: map | nil
|
||||
def named_captures(regex, string, options \\ []) when is_binary(string) do
|
||||
names = names(regex)
|
||||
options = Keyword.put(options, :capture, names)
|
||||
@@ -499,7 +551,7 @@ defmodule Regex do
|
||||
"""
|
||||
@spec names(t) :: [String.t()]
|
||||
def names(%Regex{re_pattern: re_pattern}) do
|
||||
{:namelist, names} = :re.inspect(re_pattern, :namelist)
|
||||
{:namelist, names} = :re.inspect(maybe_import_pattern(re_pattern), :namelist)
|
||||
names
|
||||
end
|
||||
|
||||
@@ -516,7 +568,7 @@ defmodule Regex do
|
||||
* `:capture` - what to capture in the result. See the ["Captures" section](#module-captures)
|
||||
to see the possible capture values.
|
||||
* `:offset` - (since v1.12.0) specifies the starting offset to match in the given string.
|
||||
Defaults to zero.
|
||||
Defaults to `0`.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -545,7 +597,7 @@ defmodule Regex do
|
||||
[["cd"], ["ce"]]
|
||||
|
||||
"""
|
||||
@spec scan(t(), String.t(), [term()]) :: [[String.t()]] | [[{integer(), integer()}]]
|
||||
@spec scan(t(), String.t(), capture_opts) :: [[String.t()]] | [[{integer(), integer()}]]
|
||||
def scan(regex, string, options \\ [])
|
||||
|
||||
def scan(%Regex{} = regex, string, options) when is_binary(string) do
|
||||
@@ -569,10 +621,36 @@ defmodule Regex do
|
||||
%Regex{source: source, opts: compile_opts} = regex
|
||||
:re.run(string, source, compile_opts ++ options)
|
||||
else
|
||||
_ -> :re.run(string, re_pattern, options)
|
||||
_ -> :re.run(string, maybe_import_pattern(re_pattern), options)
|
||||
end
|
||||
end
|
||||
|
||||
@compile {:inline, maybe_import_pattern: 1}
|
||||
@compile {:no_warn_undefined, {:re, :import, 1}}
|
||||
defp maybe_import_pattern({:re_exported_pattern, _, _, _, _} = exported),
|
||||
do: :re.import(exported)
|
||||
|
||||
defp maybe_import_pattern(pattern), do: pattern
|
||||
|
||||
@typedoc """
|
||||
Options for regex functions that capture matches.
|
||||
"""
|
||||
@type capture_opts :: [
|
||||
return: :binary | :index,
|
||||
capture: :all | :first | :all_but_first | :none | :all_names | [binary() | atom()],
|
||||
offset: non_neg_integer()
|
||||
]
|
||||
|
||||
@typedoc """
|
||||
Options for `split/3`.
|
||||
"""
|
||||
@type split_opts :: [
|
||||
parts: pos_integer() | :infinity,
|
||||
trim: boolean(),
|
||||
on: :first | :all | :all_but_first | :none | :all_names | [atom() | integer()],
|
||||
include_captures: boolean()
|
||||
]
|
||||
|
||||
@doc """
|
||||
Splits the given target based on the given pattern and in the given number of
|
||||
parts.
|
||||
@@ -626,7 +704,7 @@ defmodule Regex do
|
||||
["a", "b", "c"]
|
||||
|
||||
"""
|
||||
@spec split(t, String.t(), [term]) :: [String.t()]
|
||||
@spec split(t, String.t(), split_opts) :: [String.t()]
|
||||
def split(regex, string, options \\ [])
|
||||
|
||||
def split(%Regex{}, "", opts) do
|
||||
@@ -972,6 +1050,73 @@ defmodule Regex do
|
||||
translate_options(t, [:ungreedy | acc])
|
||||
end
|
||||
|
||||
defp translate_options(<<?E, t::binary>>, acc) do
|
||||
# on OTP 27-, the E modifier is a no-op since the feature doesn't exist but isn't needed
|
||||
# (regexes aren't using references and can be shared across nodes or stored in config)
|
||||
# TODO: remove this check on Erlang/OTP 28+ and update docs
|
||||
case Code.ensure_loaded?(:re) and function_exported?(:re, :import, 1) do
|
||||
true -> translate_options(t, [:export | acc])
|
||||
false -> translate_options(t, acc)
|
||||
end
|
||||
end
|
||||
|
||||
defp translate_options(<<>>, acc), do: acc
|
||||
defp translate_options(t, _acc), do: {:error, t}
|
||||
|
||||
@doc false
|
||||
def __escape__(%{__struct__: Regex} = regex) do
|
||||
# OTP 28.0 introduced refs in patterns, which can't be used in AST anymore
|
||||
# OTP 28.1 introduced :re.import/1 which allows us to work with pre-compiled binaries again
|
||||
|
||||
pattern_ast =
|
||||
cond do
|
||||
# TODO: Remove this when we require Erlang/OTP 28+
|
||||
# Before OTP 28.0, patterns did not contain any refs and could be safely be escaped
|
||||
:erlang.system_info(:otp_release) < [?2, ?8] ->
|
||||
Macro.escape(regex.re_pattern)
|
||||
|
||||
:lists.member(:export, regex.opts) ->
|
||||
Macro.escape(regex.re_pattern)
|
||||
|
||||
# OTP 28.1+ introduced the ability to export and import regexes from compiled binaries
|
||||
Code.ensure_loaded?(:re) and function_exported?(:re, :import, 1) ->
|
||||
{:ok, exported} = :re.compile(regex.source, [:export] ++ regex.opts)
|
||||
|
||||
quote do
|
||||
Regex.__import_pattern__(unquote(Macro.escape(exported)))
|
||||
end
|
||||
# we now that the Regex module is defined at this stage, so this macro can be safely called
|
||||
|> Macro.update_meta(&([required: true] ++ &1))
|
||||
|
||||
# TODO: Remove this when we require Erlang/OTP 28.1+
|
||||
# OTP 28.0 works in degraded mode performance-wise, we need to recompile from the source
|
||||
true ->
|
||||
quote do
|
||||
{:ok, pattern} =
|
||||
:re.compile(unquote(Macro.escape(regex.source)), unquote(Macro.escape(regex.opts)))
|
||||
|
||||
pattern
|
||||
end
|
||||
end
|
||||
|
||||
quote do
|
||||
%{
|
||||
__struct__: unquote(Regex),
|
||||
re_pattern: unquote(pattern_ast),
|
||||
source: unquote(Macro.escape(regex.source)),
|
||||
opts: unquote(Macro.escape(regex.opts))
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
defmacro __import_pattern__(pattern) do
|
||||
if __CALLER__.context in [:match, :guard] do
|
||||
raise ArgumentError, "escaped Regex structs are not allowed in match or guards"
|
||||
end
|
||||
|
||||
quote do
|
||||
:re.import(unquote(pattern))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
+132
-36
@@ -187,7 +187,7 @@ defmodule Registry do
|
||||
Note that the registry uses one ETS table plus two ETS tables per partition.
|
||||
"""
|
||||
|
||||
@keys [:unique, :duplicate]
|
||||
@keys [:unique, :duplicate, {:duplicate, :key}, {:duplicate, :pid}]
|
||||
@all_info -1
|
||||
@key_info -2
|
||||
|
||||
@@ -195,7 +195,7 @@ defmodule Registry do
|
||||
@type registry :: atom
|
||||
|
||||
@typedoc "The type of the registry"
|
||||
@type keys :: :unique | :duplicate
|
||||
@type keys :: :unique | :duplicate | {:duplicate, :key} | {:duplicate, :pid}
|
||||
|
||||
@typedoc "The type of keys allowed on registration"
|
||||
@type key :: term
|
||||
@@ -242,6 +242,11 @@ defmodule Registry do
|
||||
{:register, registry, key, registry_partition :: pid, value}
|
||||
| {:unregister, registry, key, registry_partition :: pid}
|
||||
|
||||
@typedoc """
|
||||
Options used for `dispatch/4`.
|
||||
"""
|
||||
@type dispatch_opts :: [parallel: boolean()]
|
||||
|
||||
## Via callbacks
|
||||
|
||||
@doc false
|
||||
@@ -261,8 +266,8 @@ defmodule Registry do
|
||||
:undefined
|
||||
end
|
||||
|
||||
{kind, _, _} ->
|
||||
raise ArgumentError, ":via is not supported for #{kind} registries"
|
||||
{{:duplicate, _}, _, _} ->
|
||||
raise ArgumentError, ":via is not supported for duplicate registries"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -324,11 +329,25 @@ defmodule Registry do
|
||||
{Registry, keys: :unique, name: MyApp.Registry, partitions: System.schedulers_online()}
|
||||
], strategy: :one_for_one)
|
||||
|
||||
For `:duplicate` registries with many different keys (e.g., many topics with
|
||||
few subscribers each), you can optimize key-based lookups by partitioning by key:
|
||||
|
||||
Registry.start_link(
|
||||
keys: {:duplicate, :key},
|
||||
name: MyApp.TopicRegistry,
|
||||
partitions: System.schedulers_online()
|
||||
)
|
||||
|
||||
This allows key-based lookups to check only a single partition instead of
|
||||
searching all partitions. Use the default `:pid` partitioning when you have
|
||||
fewer keys with many entries each (e.g., one topic with many subscribers).
|
||||
|
||||
## Options
|
||||
|
||||
The registry requires the following keys:
|
||||
|
||||
* `:keys` - chooses if keys are `:unique` or `:duplicate`
|
||||
* `:keys` - chooses if keys are `:unique`, `:duplicate`,
|
||||
`{:duplicate, :key}`, or `{:duplicate, :pid}`
|
||||
* `:name` - the name of the registry and its tables
|
||||
|
||||
The following keys are optional:
|
||||
@@ -340,16 +359,40 @@ defmodule Registry do
|
||||
crashes. Messages sent to listeners are of type `t:listener_message/0`.
|
||||
* `:meta` - a keyword list of metadata to be attached to the registry.
|
||||
|
||||
For `:duplicate` registries, you can specify the partitioning strategy
|
||||
directly in the `:keys` option:
|
||||
|
||||
* `:duplicate` or `{:duplicate, :pid}` - Use `:pid` partitioning (default)
|
||||
when you have keys with many entries (e.g., one topic with many subscribers).
|
||||
This is the traditional behavior and groups all entries from the same process together.
|
||||
|
||||
* `{:duplicate, :key}` - Use `:key` partitioning when entries are spread across
|
||||
many different keys (e.g., many topics with few subscribers each). This makes
|
||||
key-based lookups more efficient as they only need to check a single partition
|
||||
instead of all partitions.
|
||||
|
||||
"""
|
||||
@doc since: "1.5.0"
|
||||
@spec start_link([start_option]) :: {:ok, pid} | {:error, term}
|
||||
def start_link(options) do
|
||||
keys = Keyword.get(options, :keys)
|
||||
|
||||
if keys not in @keys do
|
||||
raise ArgumentError,
|
||||
"expected :keys to be given and be one of :unique or :duplicate, got: #{inspect(keys)}"
|
||||
end
|
||||
# Validate and normalize keys format
|
||||
kind =
|
||||
case keys do
|
||||
{:duplicate, partition_strategy} when partition_strategy in [:key, :pid] ->
|
||||
{:duplicate, partition_strategy}
|
||||
|
||||
:unique ->
|
||||
:unique
|
||||
|
||||
:duplicate ->
|
||||
{:duplicate, :pid}
|
||||
|
||||
_ ->
|
||||
raise ArgumentError,
|
||||
"expected :keys to be given and be one of :unique, :duplicate, {:duplicate, :key}, or {:duplicate, :pid}, got: #{inspect(keys)}"
|
||||
end
|
||||
|
||||
name =
|
||||
case Keyword.fetch(options, :name) do
|
||||
@@ -392,11 +435,18 @@ defmodule Registry do
|
||||
|
||||
# The @info format must be kept in sync with Registry.Partition optimization.
|
||||
entries = [
|
||||
{@all_info, {keys, partitions, nil, nil, listeners}},
|
||||
{@key_info, {keys, partitions, nil}} | meta
|
||||
{@all_info, {kind, partitions, nil, nil, listeners}},
|
||||
{@key_info, {kind, partitions, nil}} | meta
|
||||
]
|
||||
|
||||
Registry.Supervisor.start_link(keys, name, partitions, listeners, entries, compressed)
|
||||
Registry.Supervisor.start_link(
|
||||
kind,
|
||||
name,
|
||||
partitions,
|
||||
listeners,
|
||||
entries,
|
||||
compressed
|
||||
)
|
||||
end
|
||||
|
||||
@doc false
|
||||
@@ -463,7 +513,8 @@ defmodule Registry do
|
||||
end
|
||||
|
||||
{kind, _, _} ->
|
||||
raise ArgumentError, "Registry.update_value/3 is not supported for #{kind} registries"
|
||||
raise ArgumentError,
|
||||
"Registry.update_value/3 is not supported for #{inspect(kind)} registries"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -483,9 +534,15 @@ defmodule Registry do
|
||||
|
||||
See the module documentation for examples of using the `dispatch/3`
|
||||
function for building custom dispatching or a pubsub system.
|
||||
|
||||
## Options
|
||||
|
||||
* `:parallel` - if `true`, the dispatching is done in parallel
|
||||
across all partitions. Defaults to `false`.
|
||||
|
||||
"""
|
||||
@doc since: "1.4.0"
|
||||
@spec dispatch(registry, key, dispatcher, keyword) :: :ok
|
||||
@spec dispatch(registry, key, dispatcher, dispatch_opts) :: :ok
|
||||
when dispatcher: (entries :: [{pid, value}] -> term) | {module(), atom(), [term()]}
|
||||
def dispatch(registry, key, mfa_or_fun, opts \\ [])
|
||||
when is_atom(registry) and is_function(mfa_or_fun, 1)
|
||||
@@ -497,12 +554,12 @@ defmodule Registry do
|
||||
|> List.wrap()
|
||||
|> apply_non_empty_to_mfa_or_fun(mfa_or_fun)
|
||||
|
||||
{:duplicate, 1, key_ets} ->
|
||||
{{:duplicate, _}, 1, key_ets} ->
|
||||
key_ets
|
||||
|> safe_lookup_second(key)
|
||||
|> apply_non_empty_to_mfa_or_fun(mfa_or_fun)
|
||||
|
||||
{:duplicate, partitions, _} ->
|
||||
{{:duplicate, _}, partitions, _} ->
|
||||
if Keyword.get(opts, :parallel, false) do
|
||||
registry
|
||||
|> dispatch_parallel(key, mfa_or_fun, partitions)
|
||||
@@ -614,10 +671,14 @@ defmodule Registry do
|
||||
[]
|
||||
end
|
||||
|
||||
{:duplicate, 1, key_ets} ->
|
||||
{{:duplicate, _}, 1, key_ets} ->
|
||||
safe_lookup_second(key_ets, key)
|
||||
|
||||
{:duplicate, partitions, _key_ets} ->
|
||||
{{:duplicate, :key}, partitions, _key_ets} ->
|
||||
partition = hash(key, partitions)
|
||||
safe_lookup_second(key_ets!(registry, partition), key)
|
||||
|
||||
{{:duplicate, :pid}, partitions, _key_ets} ->
|
||||
for partition <- 0..(partitions - 1),
|
||||
pair <- safe_lookup_second(key_ets!(registry, partition), key),
|
||||
do: pair
|
||||
@@ -738,10 +799,10 @@ defmodule Registry do
|
||||
key_ets = key_ets || key_ets!(registry, key, partitions)
|
||||
:ets.select(key_ets, spec)
|
||||
|
||||
{:duplicate, 1, key_ets} ->
|
||||
{{:duplicate, _}, 1, key_ets} ->
|
||||
:ets.select(key_ets, spec)
|
||||
|
||||
{:duplicate, partitions, _key_ets} ->
|
||||
{{:duplicate, _}, partitions, _key_ets} ->
|
||||
for partition <- 0..(partitions - 1),
|
||||
pair <- :ets.select(key_ets!(registry, partition), spec),
|
||||
do: pair
|
||||
@@ -784,15 +845,34 @@ defmodule Registry do
|
||||
@spec keys(registry, pid) :: [key]
|
||||
def keys(registry, pid) when is_atom(registry) and is_pid(pid) do
|
||||
{kind, partitions, _, pid_ets, _} = info!(registry)
|
||||
{_, pid_ets} = pid_ets || pid_ets!(registry, pid, partitions)
|
||||
|
||||
pid_etses =
|
||||
if pid_ets do
|
||||
{_, pid_ets} = pid_ets
|
||||
[pid_ets]
|
||||
else
|
||||
case kind do
|
||||
{:duplicate, :key} ->
|
||||
for partition <- 0..(partitions - 1) do
|
||||
{_, pid_ets} = pid_ets!(registry, partition)
|
||||
pid_ets
|
||||
end
|
||||
|
||||
_ ->
|
||||
{_, pid_ets} = pid_ets!(registry, pid, partitions)
|
||||
[pid_ets]
|
||||
end
|
||||
end
|
||||
|
||||
keys =
|
||||
try do
|
||||
spec = [{{pid, :"$1", :"$2", :_}, [], [{{:"$1", :"$2"}}]}]
|
||||
:ets.select(pid_ets, spec)
|
||||
catch
|
||||
:error, :badarg -> []
|
||||
end
|
||||
Enum.flat_map(pid_etses, fn pid_ets ->
|
||||
try do
|
||||
spec = [{{pid, :"$1", :"$2", :_}, [], [{{:"$1", :"$2"}}]}]
|
||||
:ets.select(pid_ets, spec)
|
||||
catch
|
||||
:error, :badarg -> []
|
||||
end
|
||||
end)
|
||||
|
||||
# Handle the possibility of fake keys
|
||||
keys = gather_keys(keys, [], false)
|
||||
@@ -871,8 +951,17 @@ defmodule Registry do
|
||||
[]
|
||||
end
|
||||
|
||||
{:duplicate, partitions, key_ets} ->
|
||||
key_ets = key_ets || key_ets!(registry, pid, partitions)
|
||||
{{:duplicate, _}, 1, key_ets} ->
|
||||
for {^pid, value} <- safe_lookup_second(key_ets, key), do: value
|
||||
|
||||
{{:duplicate, :key}, partitions, _key_ets} ->
|
||||
partition = hash(key, partitions)
|
||||
key_ets = key_ets!(registry, partition)
|
||||
for {^pid, value} <- safe_lookup_second(key_ets, key), do: value
|
||||
|
||||
{{:duplicate, :pid}, partitions, _key_ets} ->
|
||||
partition = hash(pid, partitions)
|
||||
key_ets = key_ets!(registry, partition)
|
||||
for {^pid, value} <- safe_lookup_second(key_ets, key), do: value
|
||||
end
|
||||
end
|
||||
@@ -1110,7 +1199,7 @@ defmodule Registry do
|
||||
end
|
||||
end
|
||||
|
||||
defp register_key(:duplicate, key_ets, _key, entry) do
|
||||
defp register_key({:duplicate, _}, key_ets, _key, entry) do
|
||||
true = :ets.insert(key_ets, entry)
|
||||
:ok
|
||||
end
|
||||
@@ -1328,10 +1417,10 @@ defmodule Registry do
|
||||
key_ets = key_ets || key_ets!(registry, key, partitions)
|
||||
:ets.select_count(key_ets, spec)
|
||||
|
||||
{:duplicate, 1, key_ets} ->
|
||||
{{:duplicate, _}, 1, key_ets} ->
|
||||
:ets.select_count(key_ets, spec)
|
||||
|
||||
{:duplicate, partitions, _key_ets} ->
|
||||
{{:duplicate, _}, partitions, _key_ets} ->
|
||||
Enum.sum_by(0..(partitions - 1), fn partition_index ->
|
||||
:ets.select_count(key_ets!(registry, partition_index), spec)
|
||||
end)
|
||||
@@ -1501,7 +1590,12 @@ defmodule Registry do
|
||||
{hash(key, partitions), hash(pid, partitions)}
|
||||
end
|
||||
|
||||
defp partitions(:duplicate, _key, pid, partitions) do
|
||||
defp partitions({:duplicate, :key}, key, _pid, partitions) do
|
||||
partition = hash(key, partitions)
|
||||
{partition, partition}
|
||||
end
|
||||
|
||||
defp partitions({:duplicate, :pid}, _key, pid, partitions) do
|
||||
partition = hash(pid, partitions)
|
||||
{partition, partition}
|
||||
end
|
||||
@@ -1565,9 +1659,10 @@ defmodule Registry.Supervisor do
|
||||
defp strategy_for_kind(:unique), do: :one_for_all
|
||||
|
||||
# Duplicate registries have both key and pid partitions hashed
|
||||
# by pid. This means that, if a PID partition crashes, all of
|
||||
# by key ({:duplicate, :key}) or pid ({:duplicate, :pid}).
|
||||
# This means that, if a PID or key partition crashes, all of
|
||||
# its associated entries are in its sibling table, so we crash one.
|
||||
defp strategy_for_kind(:duplicate), do: :one_for_one
|
||||
defp strategy_for_kind({:duplicate, _}), do: :one_for_one
|
||||
end
|
||||
|
||||
defmodule Registry.Partition do
|
||||
@@ -1622,6 +1717,7 @@ defmodule Registry.Partition do
|
||||
|
||||
def init({kind, registry, i, partitions, key_partition, pid_partition, listeners, compressed}) do
|
||||
Process.flag(:trap_exit, true)
|
||||
|
||||
key_ets = init_key_ets(kind, key_partition, compressed)
|
||||
pid_ets = init_pid_ets(kind, pid_partition)
|
||||
|
||||
@@ -1648,7 +1744,7 @@ defmodule Registry.Partition do
|
||||
:ets.new(key_partition, compression_opt(opts, compressed))
|
||||
end
|
||||
|
||||
defp init_key_ets(:duplicate, key_partition, compressed) do
|
||||
defp init_key_ets({:duplicate, _}, key_partition, compressed) do
|
||||
opts = [:duplicate_bag, :public, read_concurrency: true, write_concurrency: true]
|
||||
:ets.new(key_partition, compression_opt(opts, compressed))
|
||||
end
|
||||
|
||||
+22
-22
@@ -964,17 +964,13 @@ defmodule Stream do
|
||||
after_fun.(user_acc)
|
||||
:erlang.raise(kind, reason, __STACKTRACE__)
|
||||
else
|
||||
{:suspended, [val], next} ->
|
||||
do_transform_user(val, user_acc, :cont, next, inner_acc, funs)
|
||||
{:suspended, vals, next} ->
|
||||
do_transform_user(:lists.reverse(vals), user_acc, :cont, next, inner_acc, funs)
|
||||
|
||||
{_, result} ->
|
||||
{_, vals} ->
|
||||
# Do not attempt to call the resource again, it has either done or halted
|
||||
next = fn _ -> {:done, []} end
|
||||
|
||||
case result do
|
||||
[val] -> do_transform_user(val, user_acc, :last, next, inner_acc, funs)
|
||||
[] -> do_transform(user_acc, :last, next, inner_acc, funs)
|
||||
end
|
||||
do_transform_user(:lists.reverse(vals), user_acc, :last, next, inner_acc, funs)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -989,7 +985,7 @@ defmodule Stream do
|
||||
after_fun.(user_acc)
|
||||
:erlang.raise(kind, reason, __STACKTRACE__)
|
||||
else
|
||||
result -> do_transform_result(result, :halt, next, inner_acc, funs)
|
||||
result -> do_transform_result(result, [], :halt, next, inner_acc, funs)
|
||||
end
|
||||
else
|
||||
do_transform(user_acc, :halt, next, inner_acc, funs)
|
||||
@@ -1002,7 +998,11 @@ defmodule Stream do
|
||||
{:halted, elem(inner_acc, 1)}
|
||||
end
|
||||
|
||||
defp do_transform_user(val, user_acc, next_op, next, inner_acc, funs) do
|
||||
defp do_transform_user([], user_acc, next_op, next, inner_acc, funs) do
|
||||
do_transform(user_acc, next_op, next, inner_acc, funs)
|
||||
end
|
||||
|
||||
defp do_transform_user([val | vals], user_acc, next_op, next, inner_acc, funs) do
|
||||
{user, _, _, _, after_fun} = funs
|
||||
|
||||
try do
|
||||
@@ -1013,20 +1013,20 @@ defmodule Stream do
|
||||
after_fun.(user_acc)
|
||||
:erlang.raise(kind, reason, __STACKTRACE__)
|
||||
else
|
||||
result -> do_transform_result(result, next_op, next, inner_acc, funs)
|
||||
result -> do_transform_result(result, vals, next_op, next, inner_acc, funs)
|
||||
end
|
||||
end
|
||||
|
||||
defp do_transform_result(result, next_op, next, inner_acc, funs) do
|
||||
defp do_transform_result(result, vals, next_op, next, inner_acc, funs) do
|
||||
{_, fun, inner, _, after_fun} = funs
|
||||
|
||||
case result do
|
||||
{[], user_acc} ->
|
||||
do_transform(user_acc, next_op, next, inner_acc, funs)
|
||||
do_transform_user(vals, user_acc, next_op, next, inner_acc, funs)
|
||||
|
||||
{list, user_acc} when is_list(list) ->
|
||||
reduce = &Enumerable.List.reduce(list, &1, fun)
|
||||
do_transform_inner_list(user_acc, next_op, next, inner_acc, reduce, funs)
|
||||
do_transform_inner_list(vals, user_acc, next_op, next, inner_acc, reduce, funs)
|
||||
|
||||
{:halt, user_acc} ->
|
||||
next.({:halt, []})
|
||||
@@ -1035,11 +1035,11 @@ defmodule Stream do
|
||||
|
||||
{other, user_acc} ->
|
||||
reduce = &Enumerable.reduce(other, &1, inner)
|
||||
do_transform_inner_enum(user_acc, next_op, next, inner_acc, reduce, funs)
|
||||
do_transform_inner_enum(vals, user_acc, next_op, next, inner_acc, reduce, funs)
|
||||
end
|
||||
end
|
||||
|
||||
defp do_transform_inner_list(user_acc, next_op, next, inner_acc, reduce, funs) do
|
||||
defp do_transform_inner_list(vals, user_acc, next_op, next, inner_acc, reduce, funs) do
|
||||
{_, _, _, _, after_fun} = funs
|
||||
|
||||
try do
|
||||
@@ -1051,7 +1051,7 @@ defmodule Stream do
|
||||
:erlang.raise(kind, reason, __STACKTRACE__)
|
||||
else
|
||||
{:done, acc} ->
|
||||
do_transform(user_acc, next_op, next, {:cont, acc}, funs)
|
||||
do_transform_user(vals, user_acc, next_op, next, {:cont, acc}, funs)
|
||||
|
||||
{:halted, acc} ->
|
||||
next.({:halt, []})
|
||||
@@ -1059,12 +1059,12 @@ defmodule Stream do
|
||||
{:halted, acc}
|
||||
|
||||
{:suspended, acc, continuation} ->
|
||||
resume = &do_transform_inner_list(user_acc, next_op, next, &1, continuation, funs)
|
||||
resume = &do_transform_inner_list(vals, user_acc, next_op, next, &1, continuation, funs)
|
||||
{:suspended, acc, resume}
|
||||
end
|
||||
end
|
||||
|
||||
defp do_transform_inner_enum(user_acc, next_op, next, {op, inner_acc}, reduce, funs) do
|
||||
defp do_transform_inner_enum(vals, user_acc, next_op, next, {op, inner_acc}, reduce, funs) do
|
||||
{_, _, _, _, after_fun} = funs
|
||||
|
||||
try do
|
||||
@@ -1078,7 +1078,7 @@ defmodule Stream do
|
||||
# The user wanted to cont/suspend but the stream halted,
|
||||
# so we continue with the user intention.
|
||||
{:halted, [inner_op | acc]} when op != :halt and inner_op != :halt ->
|
||||
do_transform(user_acc, next_op, next, {inner_op, acc}, funs)
|
||||
do_transform_user(vals, user_acc, next_op, next, {inner_op, acc}, funs)
|
||||
|
||||
{:halted, [_ | acc]} ->
|
||||
next.({:halt, []})
|
||||
@@ -1086,10 +1086,10 @@ defmodule Stream do
|
||||
{:halted, acc}
|
||||
|
||||
{:done, [_ | acc]} ->
|
||||
do_transform(user_acc, next_op, next, {:cont, acc}, funs)
|
||||
do_transform_user(vals, user_acc, next_op, next, {:cont, acc}, funs)
|
||||
|
||||
{:suspended, [_ | acc], continuation} ->
|
||||
resume = &do_transform_inner_enum(user_acc, next_op, next, &1, continuation, funs)
|
||||
resume = &do_transform_inner_enum(vals, user_acc, next_op, next, &1, continuation, funs)
|
||||
{:suspended, acc, resume}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -22,7 +22,7 @@ defmodule String do
|
||||
"hello world"
|
||||
|
||||
The functions in this module act according to
|
||||
[The Unicode Standard, Version 16.0.0](http://www.unicode.org/versions/Unicode16.0.0/).
|
||||
[The Unicode Standard, Version 17.0.0](https://www.unicode.org/versions/Unicode17.0.0/).
|
||||
|
||||
## Interpolation
|
||||
|
||||
@@ -298,6 +298,15 @@ defmodule String do
|
||||
| [nonempty_binary]
|
||||
| (compiled_search_pattern :: :binary.cp())
|
||||
|
||||
@type split_opts :: [
|
||||
parts: pos_integer() | :infinity,
|
||||
trim: boolean()
|
||||
]
|
||||
|
||||
@type splitter_opts :: [trim: boolean()]
|
||||
|
||||
@type replace_opts :: [global: boolean()]
|
||||
|
||||
@conditional_mappings [:greek, :turkic]
|
||||
|
||||
@doc """
|
||||
@@ -502,7 +511,8 @@ defmodule String do
|
||||
["a", "b", " c "]
|
||||
|
||||
"""
|
||||
@spec split(t, pattern | Regex.t(), keyword) :: [t]
|
||||
@spec split(t, pattern, split_opts()) :: [t]
|
||||
@spec split(t, Regex.t(), Regex.split_opts()) :: [t]
|
||||
def split(string, pattern, options \\ [])
|
||||
|
||||
def split(string, %Regex{} = pattern, options) when is_binary(string) and is_list(options) do
|
||||
@@ -607,7 +617,7 @@ defmodule String do
|
||||
["1", "2", "3", "4"]
|
||||
|
||||
"""
|
||||
@spec splitter(t, pattern, keyword) :: Enumerable.t()
|
||||
@spec splitter(t, pattern, splitter_opts) :: Enumerable.t()
|
||||
def splitter(string, pattern, options \\ [])
|
||||
|
||||
def splitter(string, "", options) when is_binary(string) and is_list(options) do
|
||||
@@ -1616,7 +1626,7 @@ defmodule String do
|
||||
"é"
|
||||
|
||||
"""
|
||||
@spec replace(t, pattern | Regex.t(), t | (t -> t | iodata), keyword) :: t
|
||||
@spec replace(t, pattern | Regex.t(), t | (t -> t | iodata), replace_opts) :: t
|
||||
def replace(subject, pattern, replacement, options \\ [])
|
||||
when is_binary(subject) and
|
||||
(is_binary(replacement) or is_function(replacement, 1)) and
|
||||
|
||||
@@ -17,6 +17,11 @@ defmodule StringIO do
|
||||
|
||||
"""
|
||||
|
||||
@type open_opts :: [
|
||||
capture_prompt: boolean(),
|
||||
encoding: :unicode | :latin1
|
||||
]
|
||||
|
||||
# We're implementing the GenServer behaviour instead of using the
|
||||
# `use GenServer` macro, because we don't want the `child_spec/1`
|
||||
# function as it doesn't make sense to be started under a supervisor.
|
||||
@@ -59,7 +64,7 @@ defmodule StringIO do
|
||||
|
||||
"""
|
||||
@doc since: "1.7.0"
|
||||
@spec open(binary, keyword, (pid -> res)) :: {:ok, res} when res: var
|
||||
@spec open(binary, open_opts, (pid -> res)) :: {:ok, res} when res: var
|
||||
def open(string, options, function)
|
||||
when is_binary(string) and is_list(options) and is_function(function, 1) do
|
||||
{:ok, pid} = GenServer.start(__MODULE__, {self(), string, options}, [])
|
||||
@@ -83,7 +88,8 @@ defmodule StringIO do
|
||||
If options are provided, the result will be `{:ok, pid}`, returning the
|
||||
IO device created. The option `:capture_prompt`, when set to `true`, causes
|
||||
prompts (which are specified as arguments to `IO.get*` functions) to be
|
||||
included in the device's output.
|
||||
included in the device's output. See `options/3` for the list of supported
|
||||
options.
|
||||
|
||||
If a function is provided, the device will be created and sent to the
|
||||
function. When the function returns, the device will be closed. The final
|
||||
@@ -111,7 +117,7 @@ defmodule StringIO do
|
||||
{:ok, {"", "The input was foo"}}
|
||||
|
||||
"""
|
||||
@spec open(binary, keyword) :: {:ok, pid}
|
||||
@spec open(binary, open_opts) :: {:ok, pid}
|
||||
@spec open(binary, (pid -> res)) :: {:ok, res} when res: var
|
||||
def open(string, options_or_function \\ [])
|
||||
|
||||
|
||||
@@ -659,6 +659,19 @@ defmodule Supervisor do
|
||||
@typedoc since: "1.16.0"
|
||||
@type module_spec :: {module(), args :: term()} | module()
|
||||
|
||||
@typedoc """
|
||||
Options for overriding child specification fields.
|
||||
"""
|
||||
@type child_spec_overrides :: [
|
||||
id: atom() | term(),
|
||||
start: {module(), atom(), [term()]},
|
||||
restart: restart(),
|
||||
shutdown: shutdown(),
|
||||
type: type(),
|
||||
modules: [module()] | :dynamic,
|
||||
significant: boolean()
|
||||
]
|
||||
|
||||
@doc """
|
||||
Starts a supervisor with the given children.
|
||||
|
||||
@@ -896,7 +909,7 @@ defmodule Supervisor do
|
||||
#=> start: {Agent, :start_link, [fn -> :ok end]}}
|
||||
|
||||
"""
|
||||
@spec child_spec(child_spec() | module_spec(), keyword()) :: child_spec()
|
||||
@spec child_spec(child_spec() | module_spec(), child_spec_overrides()) :: child_spec()
|
||||
def child_spec(module_or_map, overrides)
|
||||
|
||||
def child_spec({_, _, _, _, _, _} = tuple, _overrides) do
|
||||
@@ -1098,7 +1111,8 @@ defmodule Supervisor do
|
||||
Returns a list with information about all children of the given supervisor.
|
||||
|
||||
Note that calling this function when supervising a large number of children
|
||||
under low memory conditions can cause an out of memory exception.
|
||||
under low memory conditions can bring the system down due to an out of memory
|
||||
error.
|
||||
|
||||
This function returns a list of `{id, child, type, modules}` tuples, where:
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user