Compare commits
582
Commits
v1.17.0-rc.1
...
v1.18.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
175c8243b2 | ||
|
|
2c1a836db3 | ||
|
|
f29e18bca1 | ||
|
|
329442c481 | ||
|
|
01a88e7137 | ||
|
|
4def31f8ab | ||
|
|
88c75bcbf5 | ||
|
|
786f3ce797 | ||
|
|
98aeee34b6 | ||
|
|
6f7eaf1122 | ||
|
|
9593cef2f5 | ||
|
|
c78f426bf5 | ||
|
|
ff29628602 | ||
|
|
f5e42ba11a | ||
|
|
b0dae83353 | ||
|
|
40da7e8857 | ||
|
|
ac910c1cc2 | ||
|
|
5c340e488e | ||
|
|
defef37856 | ||
|
|
432d6fa1c2 | ||
|
|
24714c68a7 | ||
|
|
f088fc9cdb | ||
|
|
dddb8f7b19 | ||
|
|
2da3300bfc | ||
|
|
c7a841baae | ||
|
|
f3dfb72c69 | ||
|
|
7e149619f5 | ||
|
|
6dbb932bf3 | ||
|
|
0cb79b0ba1 | ||
|
|
265b9aaeb3 | ||
|
|
b9bd0e3dd9 | ||
|
|
6a3301f237 | ||
|
|
4911916f62 | ||
|
|
546a0db392 | ||
|
|
463f1aa593 | ||
|
|
a3a632efd1 | ||
|
|
b7a5fd7b56 | ||
|
|
04378bd9a6 | ||
|
|
fae36c5e49 | ||
|
|
712f24af0a | ||
|
|
8c9f303e37 | ||
|
|
1a2be16109 | ||
|
|
4adaac702e | ||
|
|
a1be4fbc86 | ||
|
|
d4e6a558cb | ||
|
|
4e3203f19a | ||
|
|
4a5fb1eb66 | ||
|
|
0fe6f68c34 | ||
|
|
5aa049ccb3 | ||
|
|
e4c86d4b5f | ||
|
|
e5033c94ce | ||
|
|
5d65d60721 | ||
|
|
3564c68cdc | ||
|
|
9df42f5a0a | ||
|
|
c6597bcb20 | ||
|
|
ca6edfd389 | ||
|
|
ed83c407a5 | ||
|
|
a426520b66 | ||
|
|
1c0f585f40 | ||
|
|
7c86c4bdb3 | ||
|
|
4931ab40a5 | ||
|
|
c7f78e8015 | ||
|
|
6a24bafbda | ||
|
|
8ddef06e81 | ||
|
|
472c4e1a24 | ||
|
|
305d8e603c | ||
|
|
1c7fc86e9c | ||
|
|
617dc72759 | ||
|
|
4329a3c2f2 | ||
|
|
8e8c9a7334 | ||
|
|
36c06c3a56 | ||
|
|
048cc2ddb1 | ||
|
|
048ae58d6d | ||
|
|
85e00cddd5 | ||
|
|
6c866e8ab2 | ||
|
|
55eca5dff5 | ||
|
|
070a6d51bf | ||
|
|
9c28fb7a39 | ||
|
|
cda1a09c58 | ||
|
|
3ca0bddd9d | ||
|
|
e07a91594b | ||
|
|
dfc659104c | ||
|
|
a4fa71ab23 | ||
|
|
2273bafb47 | ||
|
|
a78f1715dc | ||
|
|
b709865976 | ||
|
|
da7e04a540 | ||
|
|
66c5908619 | ||
|
|
31d8da6c77 | ||
|
|
3bdf5a912c | ||
|
|
0e9b5ec070 | ||
|
|
71441623cc | ||
|
|
c22a222176 | ||
|
|
aff4fdbb8e | ||
|
|
793c114721 | ||
|
|
480945a8db | ||
|
|
051bffa0ec | ||
|
|
db2a39afd2 | ||
|
|
3f01b31ce1 | ||
|
|
9753a10158 | ||
|
|
64e4b89053 | ||
|
|
beaca3b5ad | ||
|
|
8994297ec6 | ||
|
|
b75cccc8df | ||
|
|
4328b090ae | ||
|
|
41832ba397 | ||
|
|
d49937c7bd | ||
|
|
67573f1a6b | ||
|
|
768fa4963e | ||
|
|
eadbd8bdf3 | ||
|
|
d97b29fa64 | ||
|
|
b087a7501b | ||
|
|
44107f1010 | ||
|
|
13d3cc0a3f | ||
|
|
521216f427 | ||
|
|
afc99e2ca4 | ||
|
|
ede345f3e9 | ||
|
|
80993a465a | ||
|
|
13f527aaa7 | ||
|
|
e3bad83233 | ||
|
|
1f98708649 | ||
|
|
12780b15dd | ||
|
|
8eba863f8f | ||
|
|
a7264aa816 | ||
|
|
a6d7232db6 | ||
|
|
0c0f53cd66 | ||
|
|
f0ab9d6507 | ||
|
|
67c891a5f3 | ||
|
|
6cfb4e833f | ||
|
|
32e0b9c54f | ||
|
|
0ae9ac33d2 | ||
|
|
aa108ebb64 | ||
|
|
fb4b1ffe22 | ||
|
|
6e07eb619f | ||
|
|
2db97960d0 | ||
|
|
2f844fd77b | ||
|
|
50619b1dbf | ||
|
|
bb028b951f | ||
|
|
84b5db7380 | ||
|
|
4162d230d2 | ||
|
|
8e1d0411ba | ||
|
|
0174d50d92 | ||
|
|
a8c92060ea | ||
|
|
d7f8578742 | ||
|
|
5c88118868 | ||
|
|
3b01b2a632 | ||
|
|
9c7604ccf2 | ||
|
|
a6ad2b8c03 | ||
|
|
a7c8be77e6 | ||
|
|
52d6020787 | ||
|
|
bb9ddb3317 | ||
|
|
7c6efcf960 | ||
|
|
8b87b4dc9b | ||
|
|
f869345e27 | ||
|
|
bfebc1e30b | ||
|
|
60bd0ae7e8 | ||
|
|
0182f7d146 | ||
|
|
26318ebb0a | ||
|
|
2c3a9067a9 | ||
|
|
cb2e03688e | ||
|
|
6689eb9462 | ||
|
|
485ca72ae2 | ||
|
|
b35b0ef435 | ||
|
|
9d1933b679 | ||
|
|
57de6ea33a | ||
|
|
f18343d8c6 | ||
|
|
8ed104d175 | ||
|
|
f44aa8d710 | ||
|
|
731e714b6f | ||
|
|
3146c4a71a | ||
|
|
baf77f1cbf | ||
|
|
5b221a5545 | ||
|
|
9197ce51fc | ||
|
|
fbeccf9681 | ||
|
|
0e052fd18e | ||
|
|
578fd6e2f2 | ||
|
|
57841a09f4 | ||
|
|
7d421b197c | ||
|
|
c3dc6c86dd | ||
|
|
e28a84ce2a | ||
|
|
75fd563691 | ||
|
|
01474e0cd4 | ||
|
|
d0f7c03744 | ||
|
|
ee90126527 | ||
|
|
1ca06361b6 | ||
|
|
3e9fb4d5ad | ||
|
|
4c7e723bec | ||
|
|
0a694164dc | ||
|
|
2b687fe733 | ||
|
|
7abcb484f8 | ||
|
|
7e1aaacf53 | ||
|
|
e54b87c181 | ||
|
|
ef56e81d34 | ||
|
|
1557d1db5f | ||
|
|
5d626cb873 | ||
|
|
9bf8233f68 | ||
|
|
ae396c109b | ||
|
|
9f182f9abd | ||
|
|
0c75027e1f | ||
|
|
bd7d428ca0 | ||
|
|
322159cc7f | ||
|
|
cc68ad91cb | ||
|
|
9162de7c52 | ||
|
|
e3f017d0b2 | ||
|
|
69a95a8e8c | ||
|
|
4dd0d5d86e | ||
|
|
dfa733314d | ||
|
|
aa3d2357f3 | ||
|
|
c90bd0fca5 | ||
|
|
f2d8064121 | ||
|
|
4a97256839 | ||
|
|
8ad110aba0 | ||
|
|
8f594a3b0a | ||
|
|
f9eda1c382 | ||
|
|
af953c15b2 | ||
|
|
705407b61a | ||
|
|
63aa5703de | ||
|
|
91e76d311c | ||
|
|
166c03dbd7 | ||
|
|
8c78a0ea9c | ||
|
|
e6857d5af3 | ||
|
|
1c19c60abb | ||
|
|
f2565849b1 | ||
|
|
c2a0f53951 | ||
|
|
418ab7618a | ||
|
|
5c29b7e73e | ||
|
|
b5eb309e07 | ||
|
|
6886f7c6c6 | ||
|
|
4a3f61f703 | ||
|
|
809971a690 | ||
|
|
2539f85355 | ||
|
|
c85bfdfabf | ||
|
|
4b72da2e1b | ||
|
|
1572aacb9e | ||
|
|
5bbe055037 | ||
|
|
fce88ba5ca | ||
|
|
d133c704ac | ||
|
|
139f96c84e | ||
|
|
385b046370 | ||
|
|
16f1917c7a | ||
|
|
909f4ce265 | ||
|
|
42cc3ec06f | ||
|
|
268bb2df93 | ||
|
|
c5cae45963 | ||
|
|
31e5f5ba1e | ||
|
|
bd254aa815 | ||
|
|
6740246636 | ||
|
|
db0e336eae | ||
|
|
5f5961284e | ||
|
|
5d6648b10a | ||
|
|
7f64c51fa3 | ||
|
|
63ed546755 | ||
|
|
50d628cc57 | ||
|
|
aec6d6840c | ||
|
|
29a385c2ea | ||
|
|
771cfe6b9b | ||
|
|
77f1a858f8 | ||
|
|
1b29932cae | ||
|
|
c2dc6d18de | ||
|
|
90f6cede5b | ||
|
|
a0e061f94a | ||
|
|
17448e7429 | ||
|
|
76faed3cb9 | ||
|
|
af7c8ef11c | ||
|
|
bbbf3c9bc7 | ||
|
|
503c28ddce | ||
|
|
b068710ee7 | ||
|
|
54be23261d | ||
|
|
61157aabfd | ||
|
|
8e455b7661 | ||
|
|
f43b47622d | ||
|
|
74525303fb | ||
|
|
8d60093bff | ||
|
|
1db2d29e16 | ||
|
|
c7fa5f95fc | ||
|
|
90f504e9c3 | ||
|
|
51289220af | ||
|
|
47b712eb61 | ||
|
|
04741c80f6 | ||
|
|
6acc6a065b | ||
|
|
ad1ffd2547 | ||
|
|
b39205bc69 | ||
|
|
50547c1115 | ||
|
|
94759fb32f | ||
|
|
8f31a4b73b | ||
|
|
23982cb78c | ||
|
|
9598d555f1 | ||
|
|
c1ef879746 | ||
|
|
f0f42eb08d | ||
|
|
d59b298dc0 | ||
|
|
799caf3cf8 | ||
|
|
9429e1e742 | ||
|
|
fbc618ea35 | ||
|
|
e8a670959e | ||
|
|
09d43562e7 | ||
|
|
f39d85d93b | ||
|
|
ccc5753d4d | ||
|
|
26899a9d3a | ||
|
|
74beb7ffa9 | ||
|
|
90fae5d42b | ||
|
|
f17fc8b4c3 | ||
|
|
860f485bdf | ||
|
|
673fe4a893 | ||
|
|
e7ec9a0d08 | ||
|
|
f99c856927 | ||
|
|
9c42348f64 | ||
|
|
fa2a49f66e | ||
|
|
9860f96f7f | ||
|
|
5ec9ca5ff1 | ||
|
|
2ce6cfdb7c | ||
|
|
7e909060a0 | ||
|
|
251badd7c6 | ||
|
|
656944b30c | ||
|
|
072c56e05b | ||
|
|
8ef2684e3a | ||
|
|
3320a11456 | ||
|
|
a992ee9c66 | ||
|
|
7190f41cc9 | ||
|
|
0c7459e6fd | ||
|
|
e8821d145e | ||
|
|
ed75e6066c | ||
|
|
08c72e35b8 | ||
|
|
bd899cc399 | ||
|
|
4b4d2cb3ff | ||
|
|
8321e905df | ||
|
|
8786bcca7f | ||
|
|
be84111523 | ||
|
|
2551fbd1a7 | ||
|
|
39d1ecb499 | ||
|
|
63e9caf507 | ||
|
|
fb31253a8f | ||
|
|
a4adaa871f | ||
|
|
290ad65ddf | ||
|
|
c60b4774bf | ||
|
|
49979d241b | ||
|
|
8f98f5019a | ||
|
|
35d7046f31 | ||
|
|
3ca7aaed24 | ||
|
|
6c5502b589 | ||
|
|
0edf4d29cc | ||
|
|
9d5af3fe32 | ||
|
|
6152ecb82f | ||
|
|
e2556fd478 | ||
|
|
8dcc969517 | ||
|
|
7fbb7a89ea | ||
|
|
a2012c26e2 | ||
|
|
f685a76c87 | ||
|
|
1f9233213e | ||
|
|
d1271966e7 | ||
|
|
44bef71c51 | ||
|
|
ae49e4e19f | ||
|
|
0fb34f922f | ||
|
|
d690c48737 | ||
|
|
2a25d4e1f4 | ||
|
|
dcf31bed0e | ||
|
|
6cd46d76ee | ||
|
|
b8b79474c0 | ||
|
|
03cc62933d | ||
|
|
cc0cb4f1d0 | ||
|
|
261a3ed759 | ||
|
|
13cab5f801 | ||
|
|
619b1eebba | ||
|
|
b5282802a7 | ||
|
|
4126cdd8ab | ||
|
|
5902c29ad5 | ||
|
|
fb065b7709 | ||
|
|
5eddb4bbfe | ||
|
|
8e31621327 | ||
|
|
357793a6f0 | ||
|
|
0d4d1e3db1 | ||
|
|
ad4db2e8ec | ||
|
|
e0967c24fc | ||
|
|
8cb6abf6d2 | ||
|
|
7bfe5caf08 | ||
|
|
6b15b093c7 | ||
|
|
4104bfc193 | ||
|
|
7a634a2fe6 | ||
|
|
a0b5cc8ad5 | ||
|
|
f6856cf801 | ||
|
|
12dc4b5459 | ||
|
|
6d44df6573 | ||
|
|
6342bb2803 | ||
|
|
22bcecd049 | ||
|
|
ad3646e4fe | ||
|
|
c6477803ed | ||
|
|
38e9de44f7 | ||
|
|
79f00192f7 | ||
|
|
69ad427ff2 | ||
|
|
cf3132e89e | ||
|
|
44d94b09aa | ||
|
|
fb6dd0f903 | ||
|
|
66d44bee60 | ||
|
|
05d9735121 | ||
|
|
400ed5d415 | ||
|
|
4bbddcc053 | ||
|
|
aef7e4eab5 | ||
|
|
5137c33098 | ||
|
|
5cb8633a54 | ||
|
|
7a2ca783a3 | ||
|
|
ad97f93108 | ||
|
|
37e5539239 | ||
|
|
defad93a11 | ||
|
|
9b9ab9948f | ||
|
|
1286a6257f | ||
|
|
99beb29837 | ||
|
|
1ccb7a1135 | ||
|
|
00f26629da | ||
|
|
2f84c1e945 | ||
|
|
31f3a74a2d | ||
|
|
281a9df6f7 | ||
|
|
e1b265ab5f | ||
|
|
14595bb63f | ||
|
|
1889ee9882 | ||
|
|
a564c2eeb4 | ||
|
|
27b894c05f | ||
|
|
f408063d69 | ||
|
|
79667c6f04 | ||
|
|
0678be68d2 | ||
|
|
0e4aaf00c5 | ||
|
|
f2f4f3a621 | ||
|
|
07137a1edd | ||
|
|
b8cec3037c | ||
|
|
cf49f8beba | ||
|
|
5b742531b2 | ||
|
|
66f0bd0996 | ||
|
|
17a01309e0 | ||
|
|
db72cdd5a8 | ||
|
|
02a1f07367 | ||
|
|
1b4ec98478 | ||
|
|
19057692cf | ||
|
|
00f8b1e0e5 | ||
|
|
e770710379 | ||
|
|
fe2425ab05 | ||
|
|
cc707370bb | ||
|
|
b09914b2d9 | ||
|
|
699ba28b83 | ||
|
|
4aace69e01 | ||
|
|
bd92cba860 | ||
|
|
1b73c4997f | ||
|
|
8f24d452b4 | ||
|
|
960a568cf9 | ||
|
|
f6d8eb4df5 | ||
|
|
4629fb6c0e | ||
|
|
d57658a45d | ||
|
|
cb5e812bef | ||
|
|
99be673c3a | ||
|
|
b799e9eda4 | ||
|
|
220c228f75 | ||
|
|
438e13dc8c | ||
|
|
3f0813eee3 | ||
|
|
25b589851e | ||
|
|
eb6231d2be | ||
|
|
1e914b04b4 | ||
|
|
205ec0330e | ||
|
|
a3a3210772 | ||
|
|
a077d50249 | ||
|
|
fd9de33122 | ||
|
|
998e66de8c | ||
|
|
c887a8ab2a | ||
|
|
9e82da40c6 | ||
|
|
d3b8dcd99d | ||
|
|
763543ce26 | ||
|
|
7a90b59b1f | ||
|
|
797e503e40 | ||
|
|
58e76cc8d2 | ||
|
|
bc436c8ac8 | ||
|
|
d9cf285d71 | ||
|
|
18bbfed45e | ||
|
|
35b22cf642 | ||
|
|
8706d02fa2 | ||
|
|
b8886741bd | ||
|
|
8539b5b4fc | ||
|
|
786f9592b7 | ||
|
|
2d1f280034 | ||
|
|
bb902bc395 | ||
|
|
c08a9b371c | ||
|
|
692b13dcfe | ||
|
|
f01dc17d85 | ||
|
|
9924afff5d | ||
|
|
fcfa87ff36 | ||
|
|
c83334e5f2 | ||
|
|
bd99dcc6f8 | ||
|
|
5deee85546 | ||
|
|
f082b2dc87 | ||
|
|
31f5f126a6 | ||
|
|
f0fcd64f93 | ||
|
|
8ed692d4c9 | ||
|
|
1b604f57dc | ||
|
|
8745b9ea71 | ||
|
|
f1f54df22a | ||
|
|
2eb85eaf4b | ||
|
|
ab881db292 | ||
|
|
a2fe01d56a | ||
|
|
ef81dedfd3 | ||
|
|
0df128c33a | ||
|
|
e2e38f3440 | ||
|
|
134941e2b5 | ||
|
|
0793fc91b9 | ||
|
|
e8ea6a5259 | ||
|
|
61acf336c1 | ||
|
|
ace841ff85 | ||
|
|
1c17f1eed3 | ||
|
|
a39f469a76 | ||
|
|
b56b49fa2c | ||
|
|
1e0559e9ed | ||
|
|
c911466bbb | ||
|
|
7890e56c3c | ||
|
|
8c5c421b58 | ||
|
|
420fefcae5 | ||
|
|
d25845c9d5 | ||
|
|
5945fb6fb4 | ||
|
|
1e6f2d48ae | ||
|
|
69bc8a756f | ||
|
|
252d4faef7 | ||
|
|
ac933f71f6 | ||
|
|
63d0932304 | ||
|
|
ce92b1cbb3 | ||
|
|
9a06273ad2 | ||
|
|
f3c3d3dde6 | ||
|
|
7a4060cbf3 | ||
|
|
d5095f64d2 | ||
|
|
ecac839810 | ||
|
|
c69938bae7 | ||
|
|
26c6b1bcf9 | ||
|
|
ab7de3a779 | ||
|
|
f484960800 | ||
|
|
4119ec4b2c | ||
|
|
8ca99ab24d | ||
|
|
cabd7d8506 | ||
|
|
b0459d2c44 | ||
|
|
67b8b6c218 | ||
|
|
21840ed563 | ||
|
|
09c602d10e | ||
|
|
ad1ac3b783 | ||
|
|
844c4c30de | ||
|
|
d6e6924c89 | ||
|
|
f712557db0 | ||
|
|
14e9a07b78 | ||
|
|
c28a4eb040 | ||
|
|
1f6eba887d | ||
|
|
23824621f1 | ||
|
|
48113d471e | ||
|
|
b50d986631 | ||
|
|
11aa0fc526 | ||
|
|
ae60fa6bd5 | ||
|
|
29bb5116d1 | ||
|
|
478256b59e | ||
|
|
60f6515c24 | ||
|
|
7de811cfd5 | ||
|
|
ff26a8fac6 | ||
|
|
b56ed82fe3 | ||
|
|
a52133fdf0 | ||
|
|
ed67d6be18 | ||
|
|
91feadd479 | ||
|
|
c2c4cade84 | ||
|
|
8dabe63030 | ||
|
|
b665ddd6db | ||
|
|
a7cd3647e8 | ||
|
|
1b7a7341e1 | ||
|
|
7cf57ec815 | ||
|
|
967c87b9d3 | ||
|
|
cd1529fc3c | ||
|
|
a49a9d88e5 | ||
|
|
fcc7ea20ef | ||
|
|
29fb214f14 | ||
|
|
a64f28c8dd | ||
|
|
c923f731d8 | ||
|
|
420a939f1b | ||
|
|
642cc79bb7 | ||
|
|
76fec7eda2 | ||
|
|
ae5be3106f | ||
|
|
47079bc0b2 | ||
|
|
19406b1f55 | ||
|
|
1b6c31a0eb | ||
|
|
1508494761 | ||
|
|
e29af5ac43 | ||
|
|
22371ce3a9 | ||
|
|
5957b0b274 | ||
|
|
43788c0b4b | ||
|
|
25e6ce4271 | ||
|
|
54b3736eac | ||
|
|
5a4356fd56 |
+1
-2
@@ -17,6 +17,5 @@
|
||||
|
||||
# Float tests
|
||||
float_assert: 1
|
||||
],
|
||||
normalize_bitstring_modifiers: false
|
||||
]
|
||||
]
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
name: builds.hex.pm
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- v*.*
|
||||
tags:
|
||||
- v*
|
||||
|
||||
env:
|
||||
ELIXIR_OPTS: "--warnings-as-errors"
|
||||
ERLC_OPTS: "warnings_as_errors"
|
||||
LANG: C.UTF-8
|
||||
|
||||
concurrency: builds_txt
|
||||
|
||||
jobs:
|
||||
release_pre_built:
|
||||
strategy:
|
||||
fail-fast: true
|
||||
max-parallel: 1
|
||||
matrix:
|
||||
include:
|
||||
- otp: 25
|
||||
otp_version: "25.3"
|
||||
upload_generic_version: upload_generic_version
|
||||
- otp: 26
|
||||
otp_version: "26.0"
|
||||
- otp: 27
|
||||
otp_version: "27.0"
|
||||
build_docs: build_docs
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 50
|
||||
- name: Get tags
|
||||
run: git fetch --tags origin
|
||||
- uses: ./.github/workflows/release_pre_built
|
||||
with:
|
||||
otp_version: ${{ matrix.otp_version }}
|
||||
otp: ${{ matrix.otp }}
|
||||
build_docs: ${{ matrix.build_docs }}
|
||||
- name: Utils.sh
|
||||
run: |
|
||||
cat << 'EOF' > utils.sh
|
||||
function purge_key() {
|
||||
curl \
|
||||
-X POST \
|
||||
-H "Fastly-Key: ${FASTLY_KEY}" \
|
||||
-H "Accept: application/json" \
|
||||
-H "Content-Length: 0" \
|
||||
"https://api.fastly.com/service/$1/purge/$2"
|
||||
}
|
||||
function purge() {
|
||||
purge_key ${FASTLY_REPO_SERVICE_ID} $1
|
||||
purge_key ${FASTLY_BUILDS_SERVICE_ID} $1
|
||||
sleep 2
|
||||
purge_key ${FASTLY_REPO_SERVICE_ID} $1
|
||||
purge_key ${FASTLY_BUILDS_SERVICE_ID} $1
|
||||
sleep 2
|
||||
purge_key ${FASTLY_REPO_SERVICE_ID} $1
|
||||
purge_key ${FASTLY_BUILDS_SERVICE_ID} $1
|
||||
}
|
||||
EOF
|
||||
chmod +x utils.sh
|
||||
- name: Upload Docs to S3
|
||||
if: ${{ matrix.build_docs }}
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.HEX_AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.HEX_AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_REGION: ${{ secrets.HEX_AWS_REGION }}
|
||||
AWS_S3_BUCKET: ${{ secrets.HEX_AWS_S3_BUCKET }}
|
||||
FASTLY_REPO_SERVICE_ID: ${{ secrets.HEX_FASTLY_REPO_SERVICE_ID }}
|
||||
FASTLY_BUILDS_SERVICE_ID: ${{ secrets.HEX_FASTLY_BUILDS_SERVICE_ID }}
|
||||
FASTLY_KEY: ${{ secrets.HEX_FASTLY_KEY }}
|
||||
run: |
|
||||
source utils.sh
|
||||
version=$(echo ${{ github.ref_name }} | sed -e 's/^v//g')
|
||||
for f in doc/*; do
|
||||
if [ -d "$f" ]; then
|
||||
app=`echo $f | sed s/"doc\/"//`
|
||||
tarball="${app}-${version}.tar.gz"
|
||||
surrogate_key="docs/${app}-${version}"
|
||||
tar -czf "${tarball}" -C "doc/${app}" .
|
||||
aws s3 cp "${tarball}" "s3://${{ env.AWS_S3_BUCKET }}/docs/${tarball}" \
|
||||
--cache-control "public,max-age=3600" \
|
||||
--metadata "{\"surrogate-key\":\"${surrogate_key}\",\"surrogate-control\":\"public,max-age=604800\"}"
|
||||
purge "${surrogate_key}"
|
||||
fi
|
||||
done
|
||||
- name: Upload Precompiled to S3
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.HEX_AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.HEX_AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_REGION: ${{ secrets.HEX_AWS_REGION }}
|
||||
AWS_S3_BUCKET: ${{ secrets.HEX_AWS_S3_BUCKET }}
|
||||
FASTLY_REPO_SERVICE_ID: ${{ secrets.HEX_FASTLY_REPO_SERVICE_ID }}
|
||||
FASTLY_BUILDS_SERVICE_ID: ${{ secrets.HEX_FASTLY_BUILDS_SERVICE_ID }}
|
||||
FASTLY_KEY: ${{ secrets.HEX_FASTLY_KEY }}
|
||||
run: |
|
||||
source utils.sh
|
||||
aws s3 cp elixir-otp-${{ matrix.otp }}.zip "s3://${{ env.AWS_S3_BUCKET }}/builds/elixir/${{github.ref_name}}-otp-${{matrix.otp}}.zip" --cache-control "public,max-age=3600" --metadata "{\"surrogate-key\":\"builds builds/elixir builds/elixir/${{github.ref_name}}-otp-${{matrix.otp}}\",\"surrogate-control\":\"public,max-age=604800\"}"
|
||||
purge builds/elixir/${{github.ref_name}}-otp-${{matrix.otp}}
|
||||
if [ "${{matrix.upload_generic_version}}" == "upload_generic_version" ]; then
|
||||
aws s3 cp elixir-otp-${{ matrix.otp }}.zip "s3://${{ env.AWS_S3_BUCKET }}/builds/elixir/${{github.ref_name}}.zip" --cache-control "public,max-age=3600" --metadata "{\"surrogate-key\":\"builds builds/elixir builds/elixir/${{github.ref_name}}\",\"surrogate-control\":\"public,max-age=604800\"}"
|
||||
purge builds/elixir/${{github.ref_name}}
|
||||
fi
|
||||
- name: Update builds txt
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.HEX_AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.HEX_AWS_SECRET_ACCESS_KEY }}
|
||||
AWS_REGION: ${{ secrets.HEX_AWS_REGION }}
|
||||
AWS_S3_BUCKET: ${{ secrets.HEX_AWS_S3_BUCKET }}
|
||||
FASTLY_REPO_SERVICE_ID: ${{ secrets.HEX_FASTLY_REPO_SERVICE_ID }}
|
||||
FASTLY_BUILDS_SERVICE_ID: ${{ secrets.HEX_FASTLY_BUILDS_SERVICE_ID }}
|
||||
FASTLY_KEY: ${{ secrets.HEX_FASTLY_KEY }}
|
||||
run: |
|
||||
source utils.sh
|
||||
date=$(date -u '+%Y-%m-%dT%H:%M:%SZ')
|
||||
build_sha256=$(sha256sum elixir-otp-${{ matrix.otp }}.zip | cut -d ' ' -f 1)
|
||||
ref_name=${{ github.ref_name }}
|
||||
aws s3 cp s3://${{ env.AWS_S3_BUCKET }}/builds/elixir/builds.txt builds.txt || true
|
||||
touch builds.txt
|
||||
sed -i "/^${ref_name}-otp-${{ matrix.otp }} /d" builds.txt
|
||||
echo -e "${ref_name}-otp-${{ matrix.otp }} ${{ github.sha }} ${date} ${build_sha256} \n$(cat builds.txt)" > builds.txt
|
||||
if [ "${{matrix.upload_generic_version}}" == "upload_generic_version" ]; then
|
||||
sed -i "/^${ref_name} /d" builds.txt
|
||||
echo -e "${ref_name} ${{ github.sha }} ${date} ${build_sha256} \n$(cat builds.txt)" > builds.txt
|
||||
fi
|
||||
sort -u -k1,1 -o builds.txt builds.txt
|
||||
aws s3 cp builds.txt s3://${{ env.AWS_S3_BUCKET }}/builds/elixir/builds.txt --cache-control "public,max-age=3600" --metadata '{"surrogate-key":"builds builds/elixir builds/elixir/txt","surrogate-control":"public,max-age=604800"}'
|
||||
purge builds/elixir/txt
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
fetch-depth: 10
|
||||
|
||||
- name: Run markdownlint
|
||||
uses: DavidAnson/markdownlint-cli2-action@v16.0.0
|
||||
uses: DavidAnson/markdownlint-cli2-action@v18.0.0
|
||||
with:
|
||||
globs: |
|
||||
lib/elixir/pages/**/*.md
|
||||
|
||||
@@ -11,7 +11,6 @@ on:
|
||||
env:
|
||||
ELIXIR_ASSERT_TIMEOUT: 2000
|
||||
ELIXIRC_OPTS: "--warnings-as-errors"
|
||||
ERLC_OPTS: "warnings_as_errors"
|
||||
LANG: C.UTF-8
|
||||
|
||||
permissions:
|
||||
@@ -19,13 +18,16 @@ permissions:
|
||||
|
||||
jobs:
|
||||
test_linux:
|
||||
name: Ubuntu 20.04, Erlang/OTP ${{ matrix.otp_version }}
|
||||
name: Ubuntu 24.04, Erlang/OTP ${{ matrix.otp_version }}${{ matrix.deterministic && ' (deterministic)' || '' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- otp_version: "27.0"
|
||||
- otp_version: "27.1"
|
||||
deterministic: true
|
||||
- otp_version: "27.1"
|
||||
otp_latest: true
|
||||
- otp_version: "27.0"
|
||||
- otp_version: "26.0"
|
||||
- otp_version: "25.3"
|
||||
- otp_version: "25.0"
|
||||
@@ -33,7 +35,7 @@ jobs:
|
||||
development: true
|
||||
- otp_version: maint
|
||||
development: true
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -41,6 +43,9 @@ jobs:
|
||||
- uses: erlef/setup-beam@v1
|
||||
with:
|
||||
otp-version: ${{ matrix.otp_version }}
|
||||
- name: Set ERL_COMPILER_OPTIONS
|
||||
if: ${{ matrix.deterministic }}
|
||||
run: echo "ERL_COMPILER_OPTIONS=deterministic" >> $GITHUB_ENV
|
||||
- name: Compile Elixir
|
||||
run: |
|
||||
make compile
|
||||
@@ -65,19 +70,19 @@ jobs:
|
||||
cd ../elixir/
|
||||
make docs
|
||||
- name: Check reproducible builds
|
||||
if: ${{ matrix.deterministic }}
|
||||
run: |
|
||||
rm -rf .git
|
||||
# Recompile System without .git
|
||||
cd lib/elixir && ../../bin/elixirc -o ebin lib/system.ex && cd -
|
||||
taskset 1 make check_reproducible
|
||||
if: ${{ matrix.otp_latest }}
|
||||
|
||||
test_windows:
|
||||
name: Windows Server 2019, Erlang/OTP ${{ matrix.otp_version }}
|
||||
strategy:
|
||||
matrix:
|
||||
otp_version: ["25.3", "26.0"]
|
||||
runs-on: windows-2019
|
||||
otp_version: ["25.3", "26.2", "27.1"]
|
||||
runs-on: windows-2022
|
||||
steps:
|
||||
- name: Configure Git
|
||||
run: git config --global core.autocrlf input
|
||||
@@ -104,7 +109,7 @@ jobs:
|
||||
|
||||
check_posix_compliant:
|
||||
name: Check POSIX-compliant
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
|
||||
+286
-16
@@ -2,16 +2,20 @@ name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- v*.*
|
||||
tags:
|
||||
- v*
|
||||
|
||||
env:
|
||||
ELIXIR_OPTS: "--warnings-as-errors"
|
||||
ERLC_OPTS: "warnings_as_errors"
|
||||
LANG: C.UTF-8
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
attestations: write
|
||||
|
||||
jobs:
|
||||
create_draft_release:
|
||||
@@ -20,6 +24,7 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
steps:
|
||||
- name: Create draft release
|
||||
if: github.ref_type != 'branch'
|
||||
run: |
|
||||
gh release create \
|
||||
--repo ${{ github.repository }} \
|
||||
@@ -27,8 +32,29 @@ jobs:
|
||||
--notes '' \
|
||||
--draft \
|
||||
${{ github.ref_name }}
|
||||
release_pre_built:
|
||||
needs: create_draft_release
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
if: github.ref_type == 'branch'
|
||||
with:
|
||||
fetch-depth: 50
|
||||
|
||||
- name: Update ${{ github.ref_name }}-latest
|
||||
if: github.ref_type == 'branch'
|
||||
run: |
|
||||
ref_name=${{ github.ref_name }}-latest
|
||||
|
||||
if ! gh release view $ref_name; then
|
||||
gh release create \
|
||||
--latest=false \
|
||||
--title $ref_name \
|
||||
--notes "Automated release for latest ${{ github.ref_name }}." \
|
||||
$ref_name
|
||||
fi
|
||||
|
||||
git tag $ref_name --force
|
||||
git push origin $ref_name --force
|
||||
|
||||
build:
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
@@ -45,25 +71,269 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 50
|
||||
- uses: ./.github/workflows/release_pre_built
|
||||
|
||||
- name: "Build Release"
|
||||
uses: ./.github/workflows/release_pre_built
|
||||
with:
|
||||
otp_version: ${{ matrix.otp_version }}
|
||||
otp: ${{ matrix.otp }}
|
||||
build_docs: ${{ matrix.build_docs }}
|
||||
- name: Upload Pre-built
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
gh release upload --clobber "${{ github.ref_name }}" \
|
||||
elixir-otp-${{ matrix.otp }}.zip \
|
||||
elixir-otp-${{ matrix.otp }}.zip.sha{1,256}sum \
|
||||
elixir-otp-${{ matrix.otp }}.exe \
|
||||
elixir-otp-${{ matrix.otp }}.exe.sha{1,256}sum
|
||||
- name: Upload Docs to GitHub
|
||||
|
||||
- name: "Attest docs provenance"
|
||||
uses: actions/attest-build-provenance@v2
|
||||
id: attest-docs-provenance
|
||||
if: ${{ matrix.build_docs }}
|
||||
with:
|
||||
subject-path: "Docs.zip"
|
||||
- name: "Copy docs provenance"
|
||||
if: ${{ matrix.build_docs }}
|
||||
run: cp "$ATTESTATION" Docs.zip.sigstore
|
||||
env:
|
||||
ATTESTATION: "${{ steps.attest-docs-provenance.outputs.bundle-path }}"
|
||||
|
||||
- name: Create Docs Hashes
|
||||
if: ${{ matrix.build_docs }}
|
||||
run: |
|
||||
shasum -a 1 Docs.zip > Docs.zip.sha1sum
|
||||
shasum -a 256 Docs.zip > Docs.zip.sha256sum
|
||||
|
||||
- name: "Upload linux release artifacts"
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: build-linux-elixir-otp-${{ matrix.otp }}
|
||||
path: elixir-otp-${{ matrix.otp }}.zip
|
||||
|
||||
- name: "Upload windows release artifacts"
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: build-windows-elixir-otp-${{ matrix.otp }}
|
||||
path: elixir-otp-${{ matrix.otp }}.exe
|
||||
|
||||
- name: "Upload doc artifacts"
|
||||
uses: actions/upload-artifact@v4
|
||||
if: ${{ matrix.build_docs }}
|
||||
with:
|
||||
name: Docs
|
||||
path: Docs.zip*
|
||||
|
||||
sign:
|
||||
needs: [build]
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
otp: [25, 26, 27]
|
||||
flavor: [windows, linux]
|
||||
|
||||
env:
|
||||
RELEASE_FILE: elixir-otp-${{ matrix.otp }}.${{ matrix.flavor == 'linux' && 'zip' || 'exe' }}
|
||||
|
||||
runs-on: ${{ matrix.flavor == 'linux' && 'ubuntu-22.04' || 'windows-2022' }}
|
||||
|
||||
steps:
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: build-${{ matrix.flavor }}-elixir-otp-${{ matrix.otp }}
|
||||
|
||||
- name: "Sign files with Trusted Signing"
|
||||
if: github.repository == 'elixir-lang/elixir' && matrix.flavor == 'windows'
|
||||
uses: azure/trusted-signing-action@v0.5.0
|
||||
with:
|
||||
azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
azure-client-id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
azure-client-secret: ${{ secrets.AZURE_CLIENT_SECRET }}
|
||||
endpoint: https://eus.codesigning.azure.net/
|
||||
trusted-signing-account-name: trusted-signing-elixir
|
||||
certificate-profile-name: Elixir
|
||||
files-folder: ${{ github.workspace }}
|
||||
files-folder-filter: exe
|
||||
file-digest: SHA256
|
||||
timestamp-rfc3161: http://timestamp.acs.microsoft.com
|
||||
timestamp-digest: SHA256
|
||||
|
||||
- name: "Attest release provenance"
|
||||
uses: actions/attest-build-provenance@v2
|
||||
id: attest-provenance
|
||||
with:
|
||||
subject-path: ${{ env.RELEASE_FILE }}
|
||||
- name: "Copy release .zip provenance"
|
||||
shell: bash
|
||||
run: cp "$ATTESTATION" "${RELEASE_FILE}.sigstore"
|
||||
env:
|
||||
ATTESTATION: "${{ steps.attest-provenance.outputs.bundle-path }}"
|
||||
|
||||
- name: Create Release Hashes
|
||||
if: matrix.flavor == 'windows'
|
||||
shell: pwsh
|
||||
run: |
|
||||
$sha1 = Get-FileHash "$env:RELEASE_FILE" -Algorithm SHA1
|
||||
$sha1.Hash.ToLower() + " " + $env:RELEASE_FILE | Out-File "$env:RELEASE_FILE.sha1sum"
|
||||
|
||||
$sha256 = Get-FileHash "$env:RELEASE_FILE" -Algorithm SHA256
|
||||
$sha256.Hash.ToLower() + " " + $env:RELEASE_FILE | Out-File "$env:RELEASE_FILE.sha256sum"
|
||||
|
||||
- name: Create Release Hashes
|
||||
if: matrix.flavor == 'linux'
|
||||
shell: bash
|
||||
run: |
|
||||
shasum -a 1 "$RELEASE_FILE" > "${RELEASE_FILE}.sha1sum"
|
||||
shasum -a 256 "$RELEASE_FILE" > "${RELEASE_FILE}.sha256sum"
|
||||
|
||||
- name: "Upload linux release artifacts"
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: sign-${{ matrix.flavor }}-elixir-otp-${{ matrix.otp }}
|
||||
path: ${{ env.RELEASE_FILE }}*
|
||||
|
||||
upload-release:
|
||||
needs: [create_draft_release, build, sign]
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: "{sign-*-elixir-otp-*,Docs}"
|
||||
merge-multiple: true
|
||||
|
||||
- name: Upload Pre-built
|
||||
shell: bash
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
gh release upload --clobber "${{ github.ref_name }}" \
|
||||
if [ "${{ github.ref_type }}" == "branch" ]; then
|
||||
tag=${{ github.ref_name }}-latest
|
||||
else
|
||||
tag="${{ github.ref_name }}"
|
||||
fi
|
||||
|
||||
gh release upload \
|
||||
--repo ${{ github.repository }} \
|
||||
--clobber \
|
||||
"$tag" \
|
||||
elixir-otp-*.zip \
|
||||
elixir-otp-*.zip.sha{1,256}sum \
|
||||
elixir-otp-*.zip.sigstore \
|
||||
elixir-otp-*.exe \
|
||||
elixir-otp-*.exe.sha{1,256}sum \
|
||||
elixir-otp-*.exe.sigstore \
|
||||
Docs.zip \
|
||||
Docs.zip.sha{1,256}sum
|
||||
Docs.zip.sha{1,256}sum \
|
||||
Docs.zip.sigstore
|
||||
|
||||
upload-builds-hex-pm:
|
||||
needs: [build, sign]
|
||||
runs-on: ubuntu-22.04
|
||||
concurrency: builds-hex-pm
|
||||
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"
|
||||
steps:
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: "{sign-*-elixir-otp-*,Docs}"
|
||||
merge-multiple: true
|
||||
|
||||
- name: Init purge keys file
|
||||
run: |
|
||||
touch purge_keys.txt
|
||||
|
||||
- name: Upload Precompiled to S3
|
||||
run: |
|
||||
ref_name=${{ github.ref_name }}
|
||||
|
||||
for zip in $(find . -type f -name 'elixir-otp-*.zip' | sed 's/^\.\///'); do
|
||||
dest=${zip/elixir/${ref_name}}
|
||||
surrogate_key=${dest/.zip$/}
|
||||
|
||||
aws s3 cp "${zip}" "s3://${AWS_S3_BUCKET}/builds/elixir/${dest}" \
|
||||
--cache-control "public,max-age=3600" \
|
||||
--metadata "{\"surrogate-key\":\"builds builds/elixir builds/elixir/${surrogate_key}\",\"surrogate-control\":\"public,max-age=604800\"}"
|
||||
echo "builds/elixir/${surrogate_key}" >> purge_keys.txt
|
||||
|
||||
if [ "$zip" == "elixir-otp-${OTP_GENERIC_VERSION}.zip" ]; then
|
||||
aws s3 cp "${zip}" "s3://${AWS_S3_BUCKET}/builds/elixir/${ref_name}.zip" \
|
||||
--cache-control "public,max-age=3600" \
|
||||
--metadata "{\"surrogate-key\":\"builds builds/elixir builds/elixir/${ref_name}\",\"surrogate-control\":\"public,max-age=604800\"}"
|
||||
echo builds/elixir/${ref_name} >> purge_keys.txt
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Upload Docs to S3
|
||||
run: |
|
||||
version=$(echo ${{ github.ref_name }} | sed -e 's/^v//g')
|
||||
|
||||
unzip Docs.zip
|
||||
|
||||
for f in doc/*; do
|
||||
if [ -d "$f" ]; then
|
||||
app=$(echo "$f" | sed s/"doc\/"//)
|
||||
tarball="${app}-${version}.tar.gz"
|
||||
surrogate_key="docs/${app}-${version}"
|
||||
|
||||
tar -czf "${tarball}" -C "doc/${app}" .
|
||||
aws s3 cp "${tarball}" "s3://${AWS_S3_BUCKET}/docs/${tarball}" \
|
||||
--cache-control "public,max-age=3600" \
|
||||
--metadata "{\"surrogate-key\":\"${surrogate_key}\",\"surrogate-control\":\"public,max-age=604800\"}"
|
||||
echo "${surrogate_key}" >> ../purge_keys.txt
|
||||
fi
|
||||
done
|
||||
|
||||
- name: Update builds txt
|
||||
run: |
|
||||
date="$(date -u '+%Y-%m-%dT%H:%M:%SZ')"
|
||||
ref_name=${{ github.ref_name }}
|
||||
|
||||
aws s3 cp "s3://${AWS_S3_BUCKET}/builds/elixir/builds.txt" builds.txt || true
|
||||
touch builds.txt
|
||||
|
||||
for sha256_file in $(find . -name 'elixir-otp-*.zip.sha256sum' | sed 's/^\.\///'); do
|
||||
otp_version=$(echo "${sha256_file}" | sed -r 's/^elixir-otp-([[:digit:]]+)\.zip\.sha256sum/otp-\1/')
|
||||
build_sha256=$(cut -d ' ' -f 1 "${sha256_file}")
|
||||
|
||||
sed -i "/^${ref_name}-${otp_version} /d" builds.txt
|
||||
echo -e "${ref_name}-${otp_version} ${{ github.sha }} ${date} ${build_sha256} \n$(cat builds.txt)" > builds.txt
|
||||
|
||||
if [ "${otp_version}" == "otp-${OTP_GENERIC_VERSION}" ]; then
|
||||
sed -i "/^${ref_name} /d" builds.txt
|
||||
echo -e "${ref_name} ${{ github.sha }} ${date} ${build_sha256} \n$(cat builds.txt)" > builds.txt
|
||||
fi
|
||||
done
|
||||
|
||||
sort -u -k1,1 -o builds.txt builds.txt
|
||||
aws s3 cp builds.txt "s3://${AWS_S3_BUCKET}/builds/elixir/builds.txt" \
|
||||
--cache-control "public,max-age=3600" \
|
||||
--metadata '{"surrogate-key":"builds builds/elixir builds/elixir/txt","surrogate-control":"public,max-age=604800"}'
|
||||
|
||||
echo 'builds/elixir/txt' >> purge_keys.txt
|
||||
|
||||
- name: Flush cache
|
||||
if: github.repository == 'elixir-lang/elixir'
|
||||
run: |
|
||||
function purge_key() {
|
||||
curl \
|
||||
-X POST \
|
||||
-H "Fastly-Key: ${FASTLY_KEY}" \
|
||||
-H "Accept: application/json" \
|
||||
-H "Content-Length: 0" \
|
||||
"https://api.fastly.com/service/$1/purge/$2"
|
||||
}
|
||||
|
||||
function purge() {
|
||||
purge_key ${FASTLY_REPO_SERVICE_ID} $1
|
||||
purge_key ${FASTLY_BUILDS_SERVICE_ID} $1
|
||||
sleep 2
|
||||
purge_key ${FASTLY_REPO_SERVICE_ID} $1
|
||||
purge_key ${FASTLY_BUILDS_SERVICE_ID} $1
|
||||
sleep 2
|
||||
purge_key ${FASTLY_REPO_SERVICE_ID} $1
|
||||
purge_key ${FASTLY_BUILDS_SERVICE_ID} $1
|
||||
}
|
||||
|
||||
for key in $(cat purge_keys.txt); do
|
||||
purge "${key}"
|
||||
done
|
||||
|
||||
@@ -19,8 +19,6 @@ runs:
|
||||
run: |
|
||||
make Precompiled.zip
|
||||
mv Precompiled.zip elixir-otp-${{ inputs.otp }}.zip
|
||||
shasum -a 1 elixir-otp-${{ inputs.otp }}.zip > elixir-otp-${{ inputs.otp }}.zip.sha1sum
|
||||
shasum -a 256 elixir-otp-${{ inputs.otp }}.zip > elixir-otp-${{ inputs.otp }}.zip.sha256sum
|
||||
echo "$PWD/bin" >> $GITHUB_PATH
|
||||
- name: Install NSIS
|
||||
shell: bash
|
||||
@@ -34,8 +32,6 @@ runs:
|
||||
export ELIXIR_ZIP=$PWD/elixir-otp-${{ inputs.otp }}.zip
|
||||
(cd lib/elixir/scripts/windows_installer && ./build.sh)
|
||||
mv lib/elixir/scripts/windows_installer/tmp/elixir-otp-${{ inputs.otp }}.exe .
|
||||
shasum -a 1 elixir-otp-${{ inputs.otp }}.exe > elixir-otp-${{ inputs.otp }}.exe.sha1sum
|
||||
shasum -a 256 elixir-otp-${{ inputs.otp }}.exe > elixir-otp-${{ inputs.otp }}.exe.sha256sum
|
||||
- name: Get ExDoc ref
|
||||
if: ${{ inputs.build_docs }}
|
||||
shell: bash
|
||||
@@ -46,7 +42,7 @@ runs:
|
||||
ref=v$(curl -s https://hex.pm/api/packages/ex_doc | jq --raw-output '.latest_stable_version')
|
||||
fi
|
||||
echo "EX_DOC_REF=$ref" >> $GITHUB_ENV
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
if: ${{ inputs.build_docs }}
|
||||
with:
|
||||
repository: elixir-lang/ex_doc
|
||||
@@ -64,6 +60,5 @@ runs:
|
||||
if: ${{ inputs.build_docs }}
|
||||
shell: bash
|
||||
run: |
|
||||
git fetch --tags
|
||||
make Docs.zip
|
||||
shasum -a 1 Docs.zip > Docs.zip.sha1sum
|
||||
shasum -a 256 Docs.zip > Docs.zip.sha256sum
|
||||
|
||||
+368
-118
@@ -1,167 +1,417 @@
|
||||
# Changelog for Elixir v1.17
|
||||
# Changelog for Elixir v1.18
|
||||
|
||||
## Warnings from gradual set-theoretic types
|
||||
Elixir v1.18 is an impressive release with improvements across the two main efforts happening within the Elixir ecosystem right now: set-theoretic types and language servers. It also comes with built-in JSON support and adds new capabilities to its unit testing library. Here is a quick break down.
|
||||
|
||||
This release introduces gradual set-theoretic types to infer types from patterns and guards and use them to type check programs, enabling the Elixir compiler to find faults and bugs in codebases without requiring changes to existing software. The underlying principles, theory, and roadmap of our work have been outlined in ["The Design Principles of the Elixir Type System" by Giuseppe Castagna, Guillaume Duboc, José Valim](https://arxiv.org/abs/2306.06391).
|
||||
## Type system improvements
|
||||
|
||||
At the moment, Elixir developers will interact with set-theoretic types only through warnings found by the type system. The current implementation models all data types in the language:
|
||||
|
||||
* `binary()`, `integer()`, `float()`, `pid()`, `port()`, `reference()` - these types are indivisible. This means both `1` and `13` get the same `integer()` type.
|
||||
|
||||
* `atom()` - it represents all atoms and it is divisible. For instance, the atom `:foo` and `:hello_world` are also valid (distinct) types.
|
||||
|
||||
* `map()` and structs - maps can be "closed" or "open". Closed maps only allow the specified allows keys, such as `%{key: atom(), value: integer()}`. Open maps support any other keys in addition to the ones listed and their definition starts with `...`, such as `%{..., key: atom(), value: integer()}`. Structs are closed maps with the `__struct__` key.
|
||||
|
||||
* `tuple()`, `list()`, and `function()` - currently they are modelled as indivisible types. The next Elixir versions will also introduce fine-grained support to them.
|
||||
|
||||
We focused on atoms and maps on this initial release as they are respectively the simplest and the most complex types representations, so we can stress the performance of the type system and quality of error messages. Modelling these types will also provide the most immediate benefits to Elixir developers. Assuming there is a variable named `user`, holding a `%User{}` struct with an `address` field, Elixir v1.17 will emit the following warnings at compile-time:
|
||||
|
||||
* Pattern matching against a map or a struct that does not have the given key, such as `%{adress: ...} = user` (notice `address` vs `adress`)
|
||||
|
||||
* Accessing a key on a map or a struct that does not have the given key, such as `user.adress`
|
||||
|
||||
* Updating a struct or a map that does not define the given key, such as `%{user | adress: ...}`
|
||||
|
||||
* Invoking a function on non-modules, such as `user.address()`
|
||||
|
||||
* Capturing a function on non-modules, such as `&user.address/0`
|
||||
|
||||
* Performing structural comparisons with structs, such as `my_date < ~D[2010-04-17]`
|
||||
|
||||
* Performing structural comparisons between non-overlapping types, such as `integer >= string`
|
||||
|
||||
* Building and pattern matching on binaries without the relevant specifiers, such as `<<string>>` (this warns because by default it expects an integer)
|
||||
|
||||
* Attempting to rescue an undefined exception or an exception that is not a struct
|
||||
|
||||
* Accessing a field that is not defined in a rescued exception
|
||||
|
||||
These new warnings help Elixir developers find bugs earlier and give more confidence when refactoring code, especially around maps and structs. While some of these warnings were emitted in the past, they were discovered using syntax analysis. The new warnings are more reliable, precise, and with better error messages. Keep in mind that not all maps have statically known keys, and the Elixir typechecker only infers types from patterns at the moment.
|
||||
|
||||
Future Elixir versions will infer and type check more constructs, bringing Elixir developers more warnings and quality of life improvements without changes to code. For more details, see our new [reference document on gradual set-theoretic types](https://hexdocs.pm/elixir/main/gradual-set-theoretic-types.html).
|
||||
|
||||
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/).
|
||||
|
||||
## Erlang/OTP support
|
||||
|
||||
This release adds support for Erlang/OTP 27 and drops support for Erlang/OTP 24. We recommend Elixir developers to migrate to Erlang/OTP 26 or later, especially on Windows. Support for WERL (a graphical user interface for the Erlang terminal on Windows) will be removed in Elixir v1.18.
|
||||
|
||||
## Adding `Duration` and `shift/2` functions
|
||||
|
||||
Elixir introduces the `Duration` data type and APIs to shift dates, times, and date times by a given duration, considering different calendars and time zones.
|
||||
The most exciting change in Elixir v1.18 is type checking of function calls, alongside gradual inference of patterns and return types. To understand how this will impact your programs, consider the following code in "lib/user.ex":
|
||||
|
||||
```elixir
|
||||
iex> Date.shift(~D[2016-01-31], month: 2)
|
||||
~D[2016-03-31]
|
||||
defmodule User do
|
||||
defstruct [:age, :car_choice]
|
||||
|
||||
def drive(%User{age: age, car_choice: car}, car_choices) when age >= 18 do
|
||||
if car in car_choices do
|
||||
{:ok, car}
|
||||
else
|
||||
{:error, :no_choice}
|
||||
end
|
||||
end
|
||||
|
||||
def drive(%User{}, _car_choices) do
|
||||
{:error, :not_allowed}
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
Note the operation is called `shift` (instead of `add`) since working with durations does not obey properties such as associativity. For instance, adding one month and then one month does not give the same result as adding two months:
|
||||
Elixir's type system will infer that the `drive/2` function expects a `%User{}` struct and returns either `{:ok, dynamic()}`, `{:error, :no_choice}`, or `{:error, :not_allowed}`.
|
||||
|
||||
Therefore, the following code in a separate module (either in a separate or the same file), should emit a violation, due to an invalid argument:
|
||||
|
||||
```elixir
|
||||
iex> ~D[2016-01-31] |> Date.shift(month: 1) |> Date.shift(month: 1)
|
||||
~D[2016-03-29]
|
||||
User.drive({:ok, %User{}}, car_choices)
|
||||
```
|
||||
|
||||
Still, durations are essential for building intervals, recurring events, and modelling scheduling complexities found in the world around us. For `DateTime`s, Elixir will correctly deal with time zone changes (such as Daylight Saving Time), but provisions are also available in case you want to surface conflicts (for example, you shifted to a wall clock that does not exist, because the clock has been moved forward by one hour). See `DateTime.shift/2` for examples.
|
||||
Here is the warning:
|
||||
|
||||
Finally, a new `Kernel.to_timeout/1` function has been added, which helps developers normalize durations and integers to a timeout used by Process APIs. For example, to send a message after one hour, one can now write:
|
||||
```
|
||||
warning: incompatible types given to User.drive/2:
|
||||
|
||||
User.drive({:ok, %User{age: nil, car_choice: nil}}, car_choices)
|
||||
|
||||
given types:
|
||||
|
||||
{:ok, %User{age: nil, car_choice: nil}}, empty_list()
|
||||
|
||||
but expected one of:
|
||||
|
||||
dynamic(%User{age: term(), car_choice: term()}), dynamic()
|
||||
|
||||
where "car_choices" was given the type:
|
||||
|
||||
# type: empty_list()
|
||||
# from: lib/foo.ex:21:17
|
||||
car_choices = []
|
||||
|
||||
typing violation found at:
|
||||
│
|
||||
22 │ User.drive({:ok, %User{}}, car_choices)
|
||||
│ ~
|
||||
│
|
||||
└─ lib/foo.ex:22:10: Example.run/0
|
||||
```
|
||||
|
||||
> The mismatched arguments are shown in red, if your terminal supports ANSI coloring.
|
||||
|
||||
And the next snippet will warn because the `:error` clause will never match, as that's not a valid return type of the `User.drive/2` call:
|
||||
|
||||
```elixir
|
||||
Process.send_after(pid, :wake_up, to_timeout(hour: 1))
|
||||
case User.drive(user, car_choices) do
|
||||
{:ok, car} -> car
|
||||
:error -> Logger.error("User cannot drive")
|
||||
end
|
||||
```
|
||||
|
||||
## v1.17.0-dev
|
||||
And here is the warning:
|
||||
|
||||
```
|
||||
warning: the following clause will never match:
|
||||
|
||||
:error
|
||||
|
||||
because it attempts to match on the result of:
|
||||
|
||||
User.drive(user, car_choices)
|
||||
|
||||
which has type:
|
||||
|
||||
dynamic({:ok, term()} or {:error, :no_choice} or {:error, :not_allowed})
|
||||
|
||||
typing violation found at:
|
||||
│
|
||||
26 │ :error -> Logger.error("User cannot drive")
|
||||
│ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
│
|
||||
└─ lib/foo.ex:26: Example.run/0
|
||||
```
|
||||
|
||||
For more details on typing inference and the trade-offs made by the Elixir team, [see our official documentation](https://hexdocs.pm/elixir/1.18/gradual-set-theoretic-types.html#type-inference).
|
||||
|
||||
There are many other improvements to the type system, which we will go in detail within the official release. Meanwhile, here is a list summary of the overall improvements done to the type system:
|
||||
|
||||
* Type inference of patterns (typing inference of guards will be part of an upcoming release)
|
||||
|
||||
* Type checking of all language constructs, including local and remote calls, except `for`, `with`, and closures
|
||||
|
||||
* Type checking of all functions inlined by the compiler found in `Kernel`
|
||||
|
||||
* Type checking of all conversion functions inlined by the compiler
|
||||
|
||||
* [Support for tuples and lists as composite types](https://elixir-lang.org/blog/2024/08/28/typing-lists-and-tuples/) as well as type checking of their basic operations
|
||||
|
||||
* Detection of clauses and patterns that will never match from `case`, `cond`, and `=`
|
||||
|
||||
* Detection of unused clauses in private functions
|
||||
|
||||
## ExUnit improvements
|
||||
|
||||
ExUnit now supports parameterized tests to run the same test module multiple times under different parameters.
|
||||
|
||||
For example, Elixir ships a local, decentralized and scalable key-value process storage called `Registry`. The registry can be partitioned and its implementation differs depending if partitioning is enabled or not. Therefore, during tests, we want to ensure both modes are exercised. With Elixir v1.18, we can achieve this by writing:
|
||||
|
||||
```elixir
|
||||
defmodule Registry.Test do
|
||||
use ExUnit.Case,
|
||||
async: true,
|
||||
parameterize: [
|
||||
%{partitions: 1},
|
||||
%{partitions: 8}
|
||||
]
|
||||
|
||||
# ... the actual tests ...
|
||||
end
|
||||
```
|
||||
|
||||
ExUnit parameterizes whole test modules. If your modules are configured to run concurrently, as above, so will the parameterized ones.
|
||||
|
||||
ExUnit also comes with the ability of specifying test groups. While ExUnit supports running tests concurrently, those tests must not have shared state between them. However, in large applications, it may be common for some tests to depend on some shared state, and other tests to depend on a completely separate state. For example, part of your tests may depend on Cassandra, while others depend on Redis. Prior to Elixir v1.18, these tests could not run concurrently, but in v1.18 they might as long as they are assigned to different groups. Tests modules within the same group do not run concurrently, but across groups, they might.
|
||||
|
||||
With features like async tests, suite partitioning, and now grouping, Elixir developers have plenty of flexibility to make the most use of their machine resources, both in development and in CI.
|
||||
|
||||
## `mix format --migrate`
|
||||
|
||||
The `mix format` command now supports an explicit `--migrate` flag, which will convert constructs that have been deprecated in Elixir to their latest version. Because this flag rewrites the AST, it is not guaranteed the migrated format will always be valid when used in combination with macros that also perform AST rewriting.
|
||||
|
||||
As of this release, the following migrations are executed:
|
||||
|
||||
* Normalize parens in bitstring modifiers - it removes unnecessary parentheses in known bitstring modifiers, for example `<<foo::binary()>>` becomes `<<foo::binary>>`, or adds parentheses for custom modifiers, where `<<foo::custom_type>>` becomes `<<foo::custom_type()>>`.
|
||||
|
||||
* Charlists as sigils - formats charlists as `~c` sigils, for example `'foo'` becomes `~c"foo"`.
|
||||
|
||||
* `unless` as negated `if`s - rewrites `unless` expressions using `if` with a negated condition, for example `unless foo do` becomes `if !foo do`.
|
||||
|
||||
More migrations may be added in future releases.
|
||||
|
||||
## JSON support
|
||||
|
||||
This release includes official support for JSON encoding and decoding.
|
||||
|
||||
Both encoder and decoder fully conform to [RFC 8259](https://tools.ietf.org/html/rfc8259) and [ECMA 404](https://ecma-international.org/publications-and-standards/standards/ecma-404/) standards.
|
||||
|
||||
### Encoding
|
||||
|
||||
Encoding can be done via `JSON.encode!/1` and `JSON.encode_to_iodata!/1` functions. The default encoding rules are applied as follows:
|
||||
|
||||
| **Elixir** | **JSON** |
|
||||
|------------------------|----------|
|
||||
| `integer() \| float()` | Number |
|
||||
| `true \| false ` | Boolean |
|
||||
| `nil` | Null |
|
||||
| `binary()` | String |
|
||||
| `atom()` | String |
|
||||
| `list()` | Array |
|
||||
| `%{binary() => _}` | Object |
|
||||
| `%{atom() => _}` | Object |
|
||||
| `%{integer() => _}` | Object |
|
||||
|
||||
You may also implement the `JSON.Encoder` protocol for custom data structures. Elixir already implements the protocol for all Calendar types.
|
||||
|
||||
If you have a struct, you can derive the implementation of the `JSON.Encoder` by specifying which fields should be encoded to JSON:
|
||||
|
||||
```elixir
|
||||
@derive {JSON.Encoder, only: [...]}
|
||||
defstruct ...
|
||||
```
|
||||
|
||||
### Decoding
|
||||
|
||||
Decoding can be done via `JSON.decode/2` and `JSON.decode!/2` functions. The default decoding rules are applied as follows:
|
||||
|
||||
| **JSON** | **Elixir** |
|
||||
|----------|------------------------|
|
||||
| Number | `integer() \| float()` |
|
||||
| Boolean | `true \| false` |
|
||||
| Null | `nil` |
|
||||
| String | `binary()` |
|
||||
| Object | `%{binary() => _}` |
|
||||
|
||||
## Language server listeners
|
||||
|
||||
4 months ago, we welcomed [the Official Language Server team](https://elixir-lang.org/blog/2024/08/15/welcome-elixir-language-server-team/), with the goal of unifying the efforts behind code intelligence, tools, and editors in Elixir. Elixir v1.18 brings new features on this front by introducing locks and listeners to its compilation. Let's understand what it means.
|
||||
|
||||
At the moment, all language server implementations have their own compilation environment. This means that your project and dependencies during development are compiled once, for your own use, and then again for the language server. This duplicate effort could cause the language server experience to lag, when it could be relying on the already compiled artifacts of your project.
|
||||
|
||||
This release address by introducing a compiler lock, ensuring that only a single operating system process running Elixir compiles your project at a given moment, and by providing the ability for one operating system process to listen to the compilation results of others. In other words, different Elixir instances can now communicate over the same compilation build, instead of racing each other.
|
||||
|
||||
These enhancements do not only improve editor tooling, but they also directly benefit projects like IEx and Phoenix. For example, you can invoke `IEx.configure(auto_reload: true)` and IEx will automatically reload modules changed elsewhere, either by a separate terminal or your IDE.
|
||||
|
||||
## Potential incompatibilities
|
||||
|
||||
This release no longer supports WERL (a graphical user interface on Windows used by Erlang 25 and earlier). For a better user experience on Windows terminals, use Erlang/OTP 26+ (this is also the last Elixir release to support Erlang/OTP 25).
|
||||
|
||||
Furthermore, in order to support inference of patterns, Elixir will raise if it finds recursive variable definitions. This means patterns that never match, such as this one, will no longer compile:
|
||||
|
||||
def foo(x = {:ok, y}, x = y)
|
||||
|
||||
However, recursion of root variables (where variables directly point to each other), will also fail to compile:
|
||||
|
||||
def foo(x = y, y = z, z = x)
|
||||
|
||||
While the definition above could succeed (as long as all three arguments are equal), the cycle is not necessary and could be removed, as below:
|
||||
|
||||
def foo(x = y, y = z, z)
|
||||
|
||||
You may also prefer to write using guards:
|
||||
|
||||
def foo(x, y, z) when x == y and y == z
|
||||
|
||||
## v1.18.2 (2024-01-22)
|
||||
|
||||
### 1. Enhancements
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Access] Add `Access.find/1` that mirrors `Enum.find/2`
|
||||
* [Code] Support cursor inside fn/rescue/catch/else/after inside `Code.Fragment.container_cursor_to_quoted/2`
|
||||
* [Date] Add `Date.shift/2` to shift dates with duration and calendar-specific semantics
|
||||
* [Date] Allow `Date` to accept years outside of `-9999..9999` range
|
||||
* [DateTime] Add `DateTime.shift/2` to shift datetimes with duration and calendar-specific semantics
|
||||
* [Duration] Add a new `Duration` data type
|
||||
* [GenServer] Add `c:GenServer.format_status/1` callback
|
||||
* [Kernel] Add `Kernel.get_in/1` with safe nil-handling for access and structs
|
||||
* [Kernel] Add `Kernel.is_non_struct_map/1` guard
|
||||
* [Kernel] Add `Kernel.to_timeout/1`
|
||||
* [Kernel] Emit warnings for undefined functions from modules defined within the same context as the caller code
|
||||
* [Kernel] Support integers in uppercase sigils
|
||||
* [Macro] Add `Macro.Env.define_alias/4`, `Macro.Env.define_import/4`, `Macro.Env.define_require/4`, `Macro.Env.expand_alias/4`, `Macro.Env.expand_import/5`, and `Macro.Env.expand_require/6` to aid the implementation of language servers and embedded languages
|
||||
* [NaiveDateTime] Add `NaiveDateTime.shift/2` to shift naive datetimes with duration and calendar-specific semantics
|
||||
* [Process] Add `Process.set_label/1`
|
||||
* [String] Add `String.byte_slice/3` to slice a string to a maximum number of bytes while keeping it UTF-8 encoded
|
||||
* [System] Support `use_stdio: false` in `System.cmd/3` and `System.shell/2`
|
||||
* [Time] Add `Time.shift/2` to shift times with duration and calendar-specific semantics
|
||||
|
||||
#### ExUnit
|
||||
|
||||
* [ExUnit] Propagate the test process itself as a caller in `start_supervised`
|
||||
* [ExUnit] Include max cases in ExUnit reports
|
||||
|
||||
#### IEx
|
||||
|
||||
* [IEx.Helpers] Warns if `recompile` was called and the current working directory changed
|
||||
* [IEx.Helpers] Add `c/0` as an alias to `continue/0`
|
||||
* [IEx.Pry] Add `IEx.Pry.annotate_quoted/3` to annotate a quoted expression with pry breakpoints
|
||||
|
||||
#### Logger
|
||||
|
||||
* [Logger] Format `:gen_statem` reports using Elixir data structures
|
||||
* [Logger] Include process label in logger events
|
||||
* [CLI] Add `--color`/`--no-color` for enabling and disabling of ANSI colors
|
||||
* [Code.Fragment] Provide more AST context when invoking `container_cursor_to_quoted` with trailing fragments
|
||||
* [Regex] Ensure compatibility with Erlang/OTP 28+ new Regex engine
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix deps] Add `:depth` option to `Mix.SCM.Git`, thus supporting shallow clones of Git dependencies
|
||||
* [mix deps] Warn if `:optional` is used in combination with `:in_umbrella`
|
||||
* [mix deps.get] Do not add optional dependency requirements if its parent dep was skipped
|
||||
* [mix deps.tree] Add `--umbrella-only` to `mix deps.tree`
|
||||
* [mix test] Add `mix test --breakpoints` that sets up a breakpoint before each test that will run
|
||||
* [mix test] Add `mix test --repeat-until-failure` to rerun tests until a failure occurs
|
||||
* [mix test] Add `mix test --slowest-modules` to print slowest modules based on all of the tests they hold
|
||||
* [mix] Print compilation lock waiting message to stderr
|
||||
* [mix] Add an environment variable to optionally disable compilation locking
|
||||
|
||||
### 2. Bug fixes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [bin/elixir.bat] Improve handling of quotes and exclamation marks in flags
|
||||
* [Code] Address a bug where AST nodes for `(a -> b)` were not wrapped as part of the literal encoder
|
||||
* [Kernel] Resolve inconsistencies of how `..` and `...` are handled at the AST level
|
||||
* [Kernel] Fix parsing precedence of ambiguous operators followed by containers
|
||||
* [Kernel] Do not expand code in `quote bind_quoted: ...` twice
|
||||
* [Kernel] Respect `:line` property when `:file` is given as option to `quote`
|
||||
* [Kernel] Do not crash on `Macro.escape/2` when passing a quote triplet without valid meta
|
||||
* [Module] Return default value in `Module.get_attribute/3` for persisted attributes which have not yet been written to
|
||||
* [CLI] Temporarily remove PowerShell scripts for `elixir`, `elixirc`, and `mix` on Windows, as they leave the shell broken after quitting Erlang
|
||||
|
||||
#### ExUnit
|
||||
|
||||
* [ExUnit] Fix crash when diffing bitstring specifiers
|
||||
|
||||
#### IEx
|
||||
|
||||
* [IEx.Helpers] Update the history size whenever history is pruned
|
||||
* [IEx.Autocomplete] Fix crashing whhen autocompleting structs with runtime values
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix deps] Fix error message for diverged SCM definition in sibling
|
||||
* [mix] Track compilation locks per user to avoid permission errors
|
||||
* [mix deps.update] Ensure Git dependencies can be upgraded by doing so against the origin
|
||||
|
||||
## v1.18.1 (2024-12-24)
|
||||
|
||||
### 1. Enhancements
|
||||
|
||||
* [Kernel] Do not emit type violation warnings when comparing or matching against literals
|
||||
* [Kernel] Do not validate clauses of private overridable functions
|
||||
|
||||
### 2. Bug fixes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Code.Fragment] Ensure `Code.Fragment.container_cursor_to_quoted/2` with `:trailing_fragment` parses expressions that were supported in previous versions
|
||||
* [Kernel] Do not crash when typing violation is detected on dynamic dispatch
|
||||
* [Kernel] Properly annotate the source for warnings emitted by the compiler with the `@file` annotation
|
||||
* [Kernel] Properly annotate the source for warnings emitted by the type system with the `@file` annotation
|
||||
* [Kernel] Remove `:no_parens` metadata when using capture with arity on all cases
|
||||
* [Kernel] Ensure diagnostic traces are kept backwards compatible
|
||||
|
||||
#### ExUnit
|
||||
|
||||
* [ExUnit.Case] Ensure async groups do not run concurrenly while the test suite is still loading
|
||||
* [ExUnit.Case] Ensure `--repeat-until-failure` can be combined with groups
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix compile.elixir] Store compilation results if compilation fails due to `--warnings-as-errors`
|
||||
* [mix deps.loadpaths] Add build lock
|
||||
* [mix escript.build] Ensure build succeeds when protocol consolidation is disabled
|
||||
* [Mix.Shell] Ensure encoding is properly respected on Windows and Unix systems
|
||||
|
||||
## v1.18.0 (2024-12-19)
|
||||
|
||||
### 1. Enhancements
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [CLI] Add experimental PowerShell scripts for `elixir`, `elixirc`, and `mix` on Windows. Those provide a safer entry point for running Elixir from other platforms
|
||||
* [Calendar] Add `Duration.to_string/1`
|
||||
* [Code] Support several migration options in `Code.format_string!/2`
|
||||
* [Code] Add parenthesis around `--` and `---` in `Code.format_string!/2` to make precedence clearer
|
||||
* [Code] Include more metadata in `Code.string_to_quoted/2` when `token_metadata: true` to help compute ranges from the AST
|
||||
* [Code.Fragment] Have `:capture_arg` as its own entry in `Code.Fragment.surround_context/2`
|
||||
* [Config] Add `Config.read_config/1`
|
||||
* [Enumerable] Add `Enum.product_by/2` and `Enum.sum_by/2`
|
||||
* [Exception] Add `MissingApplicationsError` exception to denote missing applications
|
||||
* [JSON] Add a new `JSON` module with encoding and decoding functionality
|
||||
* [JSON] Implement `JSON.Encoder` for all Calendar types
|
||||
* [Kernel] Update source code parsing to match [UTS #55](https://www.unicode.org/reports/tr55/) latest recommendations. In particular, mixed script is allowed in identifiers as long as they are separate by underscores (`_`), such as `http_сервер`. Previously allowed highly restrictive identifiers, which mixed Latin and other scripts, such as the japanese word for t-shirt, `Tシャツ`, now require the underscore as well
|
||||
* [Kernel] Warn on bidirectional confusability in identifiers
|
||||
* [Kernel] Verify the type of the binary generators
|
||||
* [Kernel] Track the type of tuples in patterns and inside `elem/2`
|
||||
* [Kernel] Perform validation of root AST nodes in `unquote` and `unquote_splicing` to catch bugs earlier
|
||||
* [Kernel] Add source, behaviour, and record information to Docs chunk metadata
|
||||
* [Kernel] Support deterministic builds in tandem with Erlang by setting `ERL_COMPILER_OPTIONS=deterministic`. Keep in mind deterministic builds strip source and other compile time information, which may be relevant for programs
|
||||
* [Kernel] Allow aliases and imports to be enabled conditionally in module body
|
||||
* [List] Add `List.ends_with?/2`
|
||||
* [Macro] Improve `dbg` handling of `if/2`, `with/1` and of code blocks
|
||||
* [Macro] Add `Macro.struct_info!/2` to return struct information mirroring `mod.__info__(:struct)`
|
||||
* [Registry] Add `Registry.lock/3` for local locking
|
||||
* [PartitionSupervisor] Add `PartitionSupervisor.resize!/2` to resize the number of partitions in a supervisor (up to the limit it was started with)
|
||||
* [Process] Handle arbitrarily high integer values in `Process.sleep/1`
|
||||
* [Protocol] Add `@undefined_impl_description` to customize error message when an implementation is undefined
|
||||
* [Protocol] Add `__deriving__/1` as optional macro callback to `Protocol`, no longer requiring empty implementations
|
||||
* [String] Inspect special whitespace and zero-width characters using their Unicode representation
|
||||
* [String] Update Unicode to 16.0
|
||||
|
||||
#### ExUnit
|
||||
|
||||
* [ExUnit] Support parameterized tests on `ExUnit.Case`
|
||||
* [ExUnit] Support test groups: tests in the same group never run concurrently
|
||||
* [ExUnit.Case] Add `test_pid` as a tag
|
||||
|
||||
#### IEx
|
||||
|
||||
* [IEx] Add `IEx.configure(auto_reload: true)` to automatically pick up modules recompiled from other operating system processes
|
||||
* [IEx] Add `:dot_iex` support to `IEx.configure/1`
|
||||
* [IEx] Add report for normal/shutdown exits in IEx
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix compile] Ensure only a single operating system process can compile at a given time
|
||||
* [mix deps.get] Ensure only a single operating system process can fetch deps at a given time
|
||||
* [mix format] Add `mix format --migrate` to migrate from deprecated functionality
|
||||
* [mix format] Add new options and metadata to improve formatting applying by editors and other environments
|
||||
* [mix test] Taint failure manifest if requiring or compiling tests fail
|
||||
* [Mix.Project] Add a `:listeners` configuration to listen to compilation events from the current and other operating system processes
|
||||
* [Mix.Task.Compiler] Add API for fetching all persisted compiler diagnostics
|
||||
* [Mix.Task.Compiler] Add API for fetching all compiler tasks
|
||||
|
||||
### 2. Bug fixes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Code] Fix delimiter metadata for single quoted atoms and remote calls in `Code.string_to_quoted/2`
|
||||
* [Code.Formatter] Fix formatter adding extra escapes to quoted remote calls
|
||||
* [Code.Fragment] Properly handle keyword keys as their own entry
|
||||
* [Inspect.Algebra] Ensure `next_break_fits` respects `line_length`
|
||||
* [Kernel] Validate AST on `unquote` and `unquote_splicing` to provide better error reports instead of failing too late inside the compiler
|
||||
* [Kernel] Avoid crashes when emitting diagnostics on code using \t for indentation
|
||||
* [Module] Include module attribute line and name when tracing its aliases
|
||||
* [Stream] Do not halt streams twice in `Stream.transform/5`
|
||||
* [URI] Fix a bug when a schemaless URI is given to `URI.merge/2`
|
||||
|
||||
#### ExUnit
|
||||
|
||||
* [ExUnit.Assertions] Raise if guards are used in `assert/1` with `=`
|
||||
* [ExUnit.Assertions] Format inserted/deleted maps in list assertions
|
||||
|
||||
#### IEx
|
||||
|
||||
* [IEx.Helpers] `IEx.Helpers.recompile/0` will reload modules changed by other operating system processes
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix compile] Ensure warnings from external resources are emitted with `--all-warnings` when files do not change
|
||||
* [mix deps.compile] Fix escaping issues when invoking `rebar3` in some cases
|
||||
* [mix escript] Fix escript layout and support storing `priv` directories
|
||||
* [mix release] Make `.app` files deterministic in releases
|
||||
* [Mix.Shell] Fix `Mix.Shell` on Windows when outputting non UTF-8 characters
|
||||
|
||||
### 3. Soft deprecations (no warnings emitted)
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [GenServer] Deprecate `c:GenServer.format_status/2` callback to align with Erlang/OTP 25+
|
||||
* [Inspect.Algebra] `color/3` is deprecated in favor of `color_doc/3`
|
||||
* [Inspect.Algebra] `fold_doc/2` is deprecated in favor of `fold/2`
|
||||
* [Kernel] Deprecate `unless` in favor of `if`. Use `mix format --migrate` to automate the migration
|
||||
* [Macro] `Macro.struct!/2` is deprecated in favor of `Macro.struct_info!/2`
|
||||
* [Protocol] Defining `__deriving__/3` inside the `Any` implementation is deprecated, derive it inside the protocol definition itself
|
||||
|
||||
### 4. Hard deprecations
|
||||
|
||||
#### EEx
|
||||
|
||||
* [EEx] `<%#` is deprecated in favor of `<%!--` or `<% #`
|
||||
* [EEx] `c:EEx.handle_text/2` is deprecated in favor of `c:EEx.handle_text/3`
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [IO] Passing `:all` to `IO.read/2` and `IO.binread/2` is deprecated, pass `:eof` instead
|
||||
* [Kernel] Single-quote charlists are deprecated, use `~c` instead
|
||||
* [Kernel] Deprecate escaping closing delimiter in uppercase sigils
|
||||
* [Range] `left..right` without explicit steps inside patterns and guards is deprecated, write `left..right//step` instead
|
||||
* [Range] Decreasing ranges, such as `10..1` without an explicit step is deprecated, write `10..1//-1` instead
|
||||
* [Code] Setting `:warnings_as_errors` is deprecated via `Code.put_compiler_option/2`. This must not affect developers, as the `:warnings_as_errors` option is managed by Mix tasks, and not directly used via the `Code` module
|
||||
* [Enumerable] Deprecate returning a two-arity function in `Enumerable.slice/1`
|
||||
* [List] `List.zip/1` is deprecated in favor of `Enum.zip/1`
|
||||
* [Module] Deprecate `Module.eval_quoted/3` in favor of `Code.eval_quoted/3`
|
||||
* [Range] Deprecate inferring negative ranges on `Range.new/2`
|
||||
* [Tuple] `Tuple.append/2` is deprecated, use `Tuple.insert_at/3` instead
|
||||
|
||||
#### ExUnit
|
||||
#### Mix
|
||||
|
||||
* [ExUnit.Case] `register_test/4` is deprecated in favor of `register_test/6` for performance reasons
|
||||
* [mix cmd] Deprecate `mix cmd --app APP` in favor of `mix do --app APP`
|
||||
* [mix compile] `:warnings_as_errors` configuration in `:elixirc_options` is deprecated. Instead pass the `--warnings-as-errors` flag to `mix compile`. Alternatively, you might alias the task: `aliases: [compile: "compile --warnings-as-errors"]`
|
||||
* [mix test] `:warnings_as_errors` configuration in `:test_elixirc_options` is deprecated. Instead pass the `--warnings-as-errors` flag to `mix test`. Alternatively, you might alias the task: `aliases: [test: "test --warnings-as-errors"]`
|
||||
* [Mix.Tasks.Compile] Deprecate `compilers/0` in favor of `Mix.Task.Compiler.compilers/0`
|
||||
|
||||
## v1.16
|
||||
## v1.17
|
||||
|
||||
The CHANGELOG for v1.16 releases can be found [in the v1.16 branch](https://github.com/elixir-lang/elixir/blob/v1.16/CHANGELOG.md).
|
||||
The CHANGELOG for v1.17 releases can be found [in the v1.17 branch](https://github.com/elixir-lang/elixir/blob/v1.17/CHANGELOG.md).
|
||||
|
||||
@@ -2,10 +2,10 @@ PREFIX ?= /usr/local
|
||||
TEST_FILES ?= "*_test.exs"
|
||||
SHARE_PREFIX ?= $(PREFIX)/share
|
||||
MAN_PREFIX ?= $(SHARE_PREFIX)/man
|
||||
CANONICAL := main/
|
||||
# CANONICAL := main/
|
||||
ELIXIRC := bin/elixirc --ignore-module-conflict $(ELIXIRC_OPTS)
|
||||
ERLC := erlc -I lib/elixir/include
|
||||
ERL_MAKE := if [ -n "$(ERLC_OPTS)" ]; then ERL_COMPILER_OPTIONS=$(ERLC_OPTS) erl -make; else erl -make; fi
|
||||
ERL_MAKE := erl -make
|
||||
ERL := erl -I lib/elixir/include -noshell -pa lib/elixir/ebin
|
||||
GENERATE_APP := $(CURDIR)/lib/elixir/scripts/generate_app.escript
|
||||
VERSION := $(strip $(shell cat VERSION))
|
||||
@@ -17,11 +17,11 @@ INSTALL_DIR = $(INSTALL) -m755 -d
|
||||
INSTALL_DATA = $(INSTALL) -m644
|
||||
INSTALL_PROGRAM = $(INSTALL) -m755
|
||||
GIT_REVISION = $(strip $(shell git rev-parse HEAD 2> /dev/null ))
|
||||
GIT_TAG = $(strip $(shell head="$(call GIT_REVISION)"; git tag --points-at $$head 2> /dev/null | tail -1) )
|
||||
GIT_TAG = $(strip $(shell head="$(call GIT_REVISION)"; git tag --points-at $$head 2> /dev/null | grep -v latest | tail -1))
|
||||
SOURCE_DATE_EPOCH_PATH = lib/elixir/tmp/ebin_reproducible
|
||||
SOURCE_DATE_EPOCH_FILE = $(SOURCE_DATE_EPOCH_PATH)/SOURCE_DATE_EPOCH
|
||||
|
||||
.PHONY: install compile erlang elixir unicode app build_plt clean_plt dialyze test check_reproducible clean clean_residual_files format install_man clean_man docs Docs.zip Precompiled.zip zips
|
||||
.PHONY: install install_man build_plt clean_plt dialyze test check_reproducible clean clean_elixir clean_man format docs Docs.zip Precompiled.zip zips
|
||||
.NOTPARALLEL:
|
||||
|
||||
#==> Functions
|
||||
@@ -65,17 +65,21 @@ endef
|
||||
#==> Compilation tasks
|
||||
|
||||
APP := lib/elixir/ebin/elixir.app
|
||||
EEX := lib/eex/ebin/Elixir.EEx.beam
|
||||
ELIXIR := lib/elixir/ebin/elixir.beam
|
||||
PARSER := lib/elixir/src/elixir_parser.erl
|
||||
KERNEL := lib/elixir/ebin/Elixir.Kernel.beam
|
||||
UNICODE := lib/elixir/ebin/Elixir.String.Unicode.beam
|
||||
|
||||
default: compile
|
||||
|
||||
compile: erlang $(APP) elixir
|
||||
compile: erlang elixir
|
||||
|
||||
erlang: $(PARSER)
|
||||
erlang: $(ELIXIR)
|
||||
$(ELIXIR): $(PARSER) lib/elixir/src/*
|
||||
$(Q) if [ ! -f $(APP) ]; then $(call CHECK_ERLANG_RELEASE); fi
|
||||
$(Q) cd lib/elixir && mkdir -p ebin && $(ERL_MAKE)
|
||||
$(Q) $(GENERATE_APP) $(VERSION)
|
||||
|
||||
$(PARSER): lib/elixir/src/elixir_parser.yrl
|
||||
$(Q) erlc -o $@ +'{verbose,true}' +'{report,true}' $<
|
||||
@@ -83,29 +87,27 @@ $(PARSER): lib/elixir/src/elixir_parser.yrl
|
||||
# Since Mix depends on EEx and EEx depends on Mix,
|
||||
# we first compile EEx without the .app file,
|
||||
# then Mix, and then compile EEx fully
|
||||
elixir: stdlib lib/eex/ebin/Elixir.EEx.beam mix ex_unit logger eex iex
|
||||
elixir: stdlib $(EEX) mix ex_unit logger eex iex
|
||||
stdlib: $(KERNEL) $(UNICODE) $(APP)
|
||||
|
||||
stdlib: $(KERNEL) VERSION
|
||||
$(KERNEL): lib/elixir/lib/*.ex lib/elixir/lib/*/*.ex lib/elixir/lib/*/*/*.ex
|
||||
$(KERNEL): lib/elixir/src/* lib/elixir/lib/*.ex lib/elixir/lib/*/*.ex lib/elixir/lib/*/*/*.ex VERSION
|
||||
$(Q) if [ ! -f $(KERNEL) ]; then \
|
||||
echo "==> bootstrap (compile)"; \
|
||||
$(ERL) -s elixir_compiler bootstrap -s erlang halt; \
|
||||
"$(MAKE)" unicode; \
|
||||
fi
|
||||
$(Q) "$(MAKE)" unicode
|
||||
@ echo "==> elixir (compile)";
|
||||
$(Q) cd lib/elixir && ../../$(ELIXIRC) "lib/**/*.ex" -o ebin;
|
||||
$(Q) "$(MAKE)" app
|
||||
|
||||
app: $(APP)
|
||||
$(APP): lib/elixir/src/elixir.app.src lib/elixir/ebin VERSION $(GENERATE_APP)
|
||||
$(Q) $(GENERATE_APP) $< $@ $(VERSION)
|
||||
$(Q) $(GENERATE_APP) $(VERSION)
|
||||
|
||||
unicode: $(UNICODE)
|
||||
$(UNICODE): lib/elixir/unicode/*
|
||||
@ echo "==> unicode (compile)";
|
||||
$(Q) $(ELIXIRC) lib/elixir/unicode/unicode.ex -o lib/elixir/ebin;
|
||||
$(Q) $(ELIXIRC) lib/elixir/unicode/security.ex -o lib/elixir/ebin;
|
||||
$(Q) $(ELIXIRC) lib/elixir/unicode/tokenizer.ex -o lib/elixir/ebin;
|
||||
$(Q) $(ELIXIRC) lib/elixir/unicode/security.ex -o lib/elixir/ebin;
|
||||
|
||||
$(eval $(call APP_TEMPLATE,ex_unit,ExUnit))
|
||||
$(eval $(call APP_TEMPLATE,logger,Logger))
|
||||
@@ -144,6 +146,7 @@ check_reproducible: compile
|
||||
$(Q) mv lib/iex/ebin/* lib/iex/tmp/ebin_reproducible/
|
||||
$(Q) mv lib/logger/ebin/* lib/logger/tmp/ebin_reproducible/
|
||||
$(Q) mv lib/mix/ebin/* lib/mix/tmp/ebin_reproducible/
|
||||
$(Q) rm -rf lib/*/ebin
|
||||
SOURCE_DATE_EPOCH=$(call READ_SOURCE_DATE_EPOCH) "$(MAKE)" compile
|
||||
$(Q) echo "Diffing..."
|
||||
$(Q) bin/elixir lib/elixir/scripts/diff.exs lib/elixir/ebin/ lib/elixir/tmp/ebin_reproducible/
|
||||
@@ -154,16 +157,10 @@ check_reproducible: compile
|
||||
$(Q) bin/elixir lib/elixir/scripts/diff.exs lib/mix/ebin/ lib/mix/tmp/ebin_reproducible/
|
||||
$(Q) echo "Builds are reproducible"
|
||||
|
||||
clean:
|
||||
clean: clean_man
|
||||
rm -rf ebin
|
||||
rm -rf lib/*/ebin
|
||||
rm -rf $(PARSER)
|
||||
$(Q) "$(MAKE)" clean_residual_files
|
||||
|
||||
clean_elixir:
|
||||
$(Q) rm -f lib/*/ebin/Elixir.*.beam
|
||||
|
||||
clean_residual_files:
|
||||
rm -rf lib/*/_build/
|
||||
rm -rf lib/*/tmp/
|
||||
rm -rf lib/elixir/test/ebin/
|
||||
@@ -173,7 +170,9 @@ clean_residual_files:
|
||||
rm -rf lib/mix/test/fixtures/git_sparse_repo/
|
||||
rm -rf lib/mix/test/fixtures/archive/ebin/
|
||||
rm -f erl_crash.dump
|
||||
$(Q) "$(MAKE)" clean_man
|
||||
|
||||
clean_elixir:
|
||||
$(Q) rm -f lib/*/ebin/Elixir.*.beam
|
||||
|
||||
#==> Documentation tasks
|
||||
|
||||
@@ -305,17 +304,15 @@ dialyze: compile $(PLT)
|
||||
|
||||
build_man: man/iex.1 man/elixir.1
|
||||
|
||||
man/iex.1:
|
||||
$(Q) cp man/iex.1.in man/iex.1
|
||||
$(Q) sed -i.bak "/{COMMON}/r man/common" man/iex.1
|
||||
$(Q) sed -i.bak "/{COMMON}/d" man/iex.1
|
||||
$(Q) rm -f man/iex.1.bak
|
||||
define BUILD_MANPAGES
|
||||
man/$(APP).1:
|
||||
$(Q) cp man/$(APP).1.in man/$(APP).1
|
||||
$(Q) sed -i.bak "/{COMMON}/r man/common" man/$(APP).1
|
||||
$(Q) sed -i.bak "/{COMMON}/d" man/$(APP).1
|
||||
$(Q) rm -f man/$(APP).1.bak
|
||||
endef
|
||||
|
||||
man/elixir.1:
|
||||
$(Q) cp man/elixir.1.in man/elixir.1
|
||||
$(Q) sed -i.bak "/{COMMON}/r man/common" man/elixir.1
|
||||
$(Q) sed -i.bak "/{COMMON}/d" man/elixir.1
|
||||
$(Q) rm -f man/elixir.1.bak
|
||||
$(foreach APP, elixir iex, $(eval $(BUILD_MANPAGES)))
|
||||
|
||||
clean_man:
|
||||
rm -f man/elixir.1
|
||||
|
||||
@@ -3,9 +3,10 @@ LEGAL NOTICE INFORMATION
|
||||
|
||||
All the files in this distribution are copyright to the terms below.
|
||||
|
||||
== lib/elixir/src/elixir_json.erl
|
||||
== lib/elixir/src/elixir_parser.erl (generated by build scripts)
|
||||
|
||||
Copyright Ericsson AB 1996-2015
|
||||
Copyright Ericsson AB 1996-2024
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
@@ -103,7 +103,9 @@ on Windows](https://github.com/elixir-lang/elixir/wiki/Windows).
|
||||
In case you want to use this Elixir version as your system version,
|
||||
you need to add the `bin` directory to [your PATH environment variable](https://elixir-lang.org/install.html#setting-path-environment-variable).
|
||||
|
||||
Additionally, you may choose to run the test suite with `make clean test`.
|
||||
When updating the repository, you may want to run `make clean` before
|
||||
recompiling. For deterministic builds, you should set the environment
|
||||
variable `ERL_COMPILER_OPTIONS=deterministic`.
|
||||
|
||||
## Contributing
|
||||
|
||||
@@ -137,13 +139,20 @@ bin/elixirc lib/elixir/lib/string.ex -o lib/elixir/ebin
|
||||
bin/elixir lib/elixir/test/elixir/string_test.exs
|
||||
```
|
||||
|
||||
Some test files need their `test_helper.exs` to be explicitly required
|
||||
before, such as:
|
||||
|
||||
```sh
|
||||
bin/elixir -r lib/logger/test/test_helper.exs lib/logger/test/logger_test.exs
|
||||
```
|
||||
|
||||
You can also use the `LINE` env var to run a single test:
|
||||
|
||||
```sh
|
||||
LINE=123 bin/elixir lib/elixir/test/elixir/string_test.exs
|
||||
````
|
||||
|
||||
To recompile (including Erlang modules):
|
||||
To recompile all (including Erlang modules):
|
||||
|
||||
```sh
|
||||
make compile
|
||||
|
||||
+9
-7
@@ -2,21 +2,23 @@
|
||||
|
||||
## Shipping a new version
|
||||
|
||||
1. Update version in /VERSION, bin/elixir and bin/elixir.bat
|
||||
1. Update version in /VERSION, bin/elixir, bin/elixir.bat, and bin/elixir.ps1
|
||||
|
||||
2. Ensure /CHANGELOG.md is updated, versioned and add the current date
|
||||
|
||||
3. Update "Compatibility and Deprecations" if a new OTP version is supported
|
||||
|
||||
4. Commit changes above with title "Release vVERSION", generate a new tag, and push it
|
||||
4. Commit changes above with title "Release vVERSION" and push it
|
||||
|
||||
5. Wait until GitHub Actions publish artifacts to the draft release and the CI is green
|
||||
5. Once GitHub actions completes, generate a new tag, and push it
|
||||
|
||||
6. Copy the relevant bits from /CHANGELOG.md to the GitHub release and publish it (link to the announcement if there is one)
|
||||
6. Wait until GitHub Actions publish artifacts to the draft release
|
||||
|
||||
7. Update `_data/elixir-versions.yml` (except for RCs) in `elixir-lang/elixir-lang.github.com`
|
||||
7. Copy the relevant bits from /CHANGELOG.md to the GitHub release and publish it (link to the announcement if there is one)
|
||||
|
||||
## Creating a new vMAJOR.MINOR branch (after first rc)
|
||||
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)
|
||||
|
||||
### In the new branch
|
||||
|
||||
@@ -28,7 +30,7 @@
|
||||
|
||||
### Back in main
|
||||
|
||||
1. Bump /VERSION file, bin/elixir and bin/elixir.bat
|
||||
1. Bump /VERSION file, bin/elixir, bin/elixir.bat, and bin/elixir.ps1
|
||||
|
||||
2. Start new /CHANGELOG.md
|
||||
|
||||
|
||||
+3
-4
@@ -6,12 +6,11 @@ Elixir applies bug fixes only to the latest minor branch. Security patches are a
|
||||
|
||||
Elixir version | Support
|
||||
:------------- | :-----------------------------
|
||||
1.17 | Development
|
||||
1.16 | Bug fixes and security patches
|
||||
1.18 | Bug fixes and security patches
|
||||
1.17 | Security patches only
|
||||
1.16 | Security patches only
|
||||
1.15 | Security patches only
|
||||
1.14 | Security patches only
|
||||
1.13 | Security patches only
|
||||
1.12 | Security patches only
|
||||
|
||||
## Announcements
|
||||
|
||||
|
||||
+3
-7
@@ -1,7 +1,7 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
ELIXIR_VERSION=1.17.0-dev
|
||||
ELIXIR_VERSION=1.18.2
|
||||
|
||||
if [ $# -eq 0 ] || { [ $# -eq 1 ] && { [ "$1" = "--help" ] || [ "$1" = "-h" ]; }; }; then
|
||||
cat <<USAGE >&2
|
||||
@@ -18,13 +18,13 @@ Usage: $(basename "$0") [options] [.exs file] [data]
|
||||
-pz "PATH" Appends the given path to Erlang code path (*)
|
||||
-v, --version Prints Erlang/OTP and Elixir versions (standalone)
|
||||
|
||||
--color, --no-color Enables or disables ANSI coloring
|
||||
--erl "SWITCHES" Switches to be passed down to Erlang (*)
|
||||
--eval "COMMAND" Evaluates the given command, same as -e (*)
|
||||
--logger-otp-reports BOOL Enables or disables OTP reporting
|
||||
--logger-sasl-reports BOOL Enables or disables SASL reporting
|
||||
--no-halt Does not halt the Erlang VM after execution
|
||||
--short-version Prints Elixir version (standalone)
|
||||
--werl Uses Erlang's Windows shell GUI (Windows only)
|
||||
|
||||
Options given after the .exs file or -- are passed down to the executed code.
|
||||
Options can be passed to the Erlang runtime using \$ELIXIR_ERL_OPTIONS or --erl.
|
||||
@@ -112,7 +112,7 @@ while [ $I -le $LENGTH ]; do
|
||||
C=1
|
||||
MODE="iex"
|
||||
;;
|
||||
-v|--no-halt)
|
||||
-v|--no-halt|--color|--no-color)
|
||||
C=1
|
||||
;;
|
||||
-e|-r|-pr|-pa|-pz|--eval|--remsh|--dot-iex|--dbg)
|
||||
@@ -182,10 +182,6 @@ while [ $I -le $LENGTH ]; do
|
||||
echo "--pipe-to : LOGDIR cannot be a switch" >&2 && exit 1
|
||||
fi
|
||||
;;
|
||||
--werl)
|
||||
S=1
|
||||
if [ "$OS" = "Windows_NT" ]; then ERL_EXEC="werl"; fi
|
||||
;;
|
||||
*)
|
||||
while [ $I -le $LENGTH ]; do
|
||||
I=$((I + 1))
|
||||
|
||||
+6
-17
@@ -1,6 +1,6 @@
|
||||
@echo off
|
||||
|
||||
set ELIXIR_VERSION=1.17.0-dev
|
||||
set ELIXIR_VERSION=1.18.2
|
||||
|
||||
if ""%1""=="""" if ""%2""=="""" goto documentation
|
||||
if /I ""%1""==""--help"" if ""%2""=="""" goto documentation
|
||||
@@ -24,13 +24,13 @@ echo -pa "PATH" Prepends the given path to Erlang code path
|
||||
echo -pz "PATH" Appends the given path to Erlang code path (*)
|
||||
echo -v, --version Prints Erlang/OTP and Elixir versions (standalone)
|
||||
echo.
|
||||
echo --color, --no-color Enables or disables ANSI coloring
|
||||
echo --erl "SWITCHES" Switches to be passed down to Erlang (*)
|
||||
echo --eval "COMMAND" Evaluates the given command, same as -e (*)
|
||||
echo --logger-otp-reports BOOL Enables or disables OTP reporting
|
||||
echo --logger-sasl-reports BOOL Enables or disables SASL reporting
|
||||
echo --no-halt Does not halt the Erlang VM after execution
|
||||
echo --short-version Prints Elixir version (standalone)
|
||||
echo --werl Uses Erlang's Windows shell GUI (Windows only)
|
||||
echo.
|
||||
echo Options given after the .exs file or -- are passed down to the executed code.
|
||||
echo Options can be passed to the Erlang runtime using $ELIXIR_ERL_OPTIONS or --erl.
|
||||
@@ -95,7 +95,6 @@ if "!par!"=="" (
|
||||
shift
|
||||
set par="!par:"=\"!"
|
||||
rem ******* EXECUTION OPTIONS **********************
|
||||
if !par!=="--werl" (set useWerl=1 && goto startloop)
|
||||
if !par!=="+iex" (set useIEx=1 && goto startloop)
|
||||
if !par!=="+elixirc" (goto startloop)
|
||||
rem ******* ELIXIR PARAMETERS **********************
|
||||
@@ -109,6 +108,8 @@ if ""==!par:-pz=! (shift && goto startloop)
|
||||
if ""==!par:-v=! (goto startloop)
|
||||
if ""==!par:--version=! (goto startloop)
|
||||
if ""==!par:--no-halt=! (goto startloop)
|
||||
if ""==!par:--color=! (goto startloop)
|
||||
if ""==!par:--no-color=! (goto startloop)
|
||||
if ""==!par:--remsh=! (shift && goto startloop)
|
||||
if ""==!par:--dot-iex=! (shift && goto startloop)
|
||||
if ""==!par:--dbg=! (shift && goto startloop)
|
||||
@@ -128,10 +129,6 @@ if ""==!par:--pipe-to=! (echo --pipe-to : Option is not supported on
|
||||
|
||||
:run
|
||||
setlocal disabledelayedexpansion
|
||||
reg query HKCU\Console /v VirtualTerminalLevel 2>nul | findstr /e "0x1" >nul 2>nul
|
||||
if %errorlevel% == 0 (
|
||||
set beforeExtra=-elixir ansi_enabled true %beforeExtra%
|
||||
)
|
||||
if not defined useIEx (
|
||||
set beforeExtra=-s elixir start_cli %beforeExtra%
|
||||
)
|
||||
@@ -139,17 +136,9 @@ if not defined useIEx (
|
||||
set beforeExtra=-noshell -elixir_root "%SCRIPT_PATH%..\lib" -pa "%SCRIPT_PATH%..\lib\elixir\ebin" %beforeExtra%
|
||||
|
||||
if defined ELIXIR_CLI_DRY_RUN (
|
||||
if defined useWerl (
|
||||
echo start "" "%ERTS_BIN%werl.exe" %ext_libs% %ELIXIR_ERL_OPTIONS% %parsErlang% %beforeExtra% -extra %*
|
||||
) else (
|
||||
echo "%ERTS_BIN%erl.exe" %ext_libs% %ELIXIR_ERL_OPTIONS% %parsErlang% %beforeExtra% -extra %*
|
||||
)
|
||||
echo "%ERTS_BIN%erl.exe" %ELIXIR_ERL_OPTIONS% %parsErlang% %beforeExtra% -extra %*
|
||||
) else (
|
||||
if defined useWerl (
|
||||
start "" "%ERTS_BIN%werl.exe" %ext_libs% %ELIXIR_ERL_OPTIONS% %parsErlang% %beforeExtra% -extra %*
|
||||
) else (
|
||||
"%ERTS_BIN%erl.exe" %ext_libs% %ELIXIR_ERL_OPTIONS% %parsErlang% %beforeExtra% -extra %*
|
||||
)
|
||||
"%ERTS_BIN%erl.exe" %ELIXIR_ERL_OPTIONS% %parsErlang% %beforeExtra% -extra %*
|
||||
)
|
||||
exit /B %ERRORLEVEL%
|
||||
:end
|
||||
|
||||
+1
-4
@@ -16,14 +16,11 @@ echo --dot-iex "FILE" Evaluates FILE, line by line, to set up IEx' environm
|
||||
echo Defaults to evaluating .iex.exs or ~/.iex.exs, if any exists.
|
||||
echo If FILE is empty, then no file will be loaded.
|
||||
echo --remsh NAME Connects to a node using a remote shell
|
||||
echo --werl Uses Erlang's Windows shell GUI (Windows only)
|
||||
echo.
|
||||
echo Set the IEX_WITH_WERL environment variable to always use werl.
|
||||
echo It accepts all other options listed by "elixir --help".
|
||||
goto end
|
||||
|
||||
:run
|
||||
if defined IEX_WITH_WERL (set __ELIXIR_IEX_FLAGS=--werl) else (set __ELIXIR_IEX_FLAGS=)
|
||||
call "%~dp0\elixir.bat" --no-halt --erl "-user elixir" +iex %__ELIXIR_IEX_FLAGS% %*
|
||||
call "%~dp0\elixir.bat" --no-halt --erl "-user elixir" +iex %*
|
||||
:end
|
||||
endlocal
|
||||
|
||||
Regular → Executable
+1
-1
@@ -20,4 +20,4 @@ for ($i = 0; $i -lt $args.length; $i++)
|
||||
}
|
||||
|
||||
# Corrected arguments are ready to pass to batch file
|
||||
& $mixBatPath $newArgs
|
||||
& $mixBatPath $newArgs
|
||||
+23
-10
@@ -48,8 +48,14 @@ defmodule EEx.Compiler do
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: Deprecate this on Elixir v1.18
|
||||
# TODO: Remove me on Elixir v2.0
|
||||
defp tokenize(~c"<%#" ++ t, line, column, state, buffer, acc) do
|
||||
IO.warn("<%# is deprecated, use <%!-- or add a space between <% and # instead",
|
||||
line: line,
|
||||
column: column,
|
||||
file: state.file
|
||||
)
|
||||
|
||||
case expr(t, line, column + 3, state, []) do
|
||||
{:error, message} ->
|
||||
{:error, message, %{line: line, column: column}}
|
||||
@@ -299,6 +305,14 @@ defmodule EEx.Compiler do
|
||||
}
|
||||
|
||||
init = state.engine.init(opts)
|
||||
|
||||
if function_exported?(state.engine, :handle_text, 2) and
|
||||
not function_exported?(state.engine, :handle_text, 3) do
|
||||
IO.warn(
|
||||
"#{inspect(state.engine)}.handle_text/2 is deprecated, implement handle_text/3 instead"
|
||||
)
|
||||
end
|
||||
|
||||
generate_buffer(tokens, init, [], state)
|
||||
end
|
||||
|
||||
@@ -316,8 +330,7 @@ defmodule EEx.Compiler do
|
||||
meta = [line: meta.line, column: meta.column]
|
||||
state.engine.handle_text(buffer, meta, IO.chardata_to_string(chars))
|
||||
else
|
||||
# TODO: Deprecate this branch on Elixir v1.18.
|
||||
# We should most likely move this check to init to emit the deprecation once.
|
||||
# TODO: Remove this on Elixir v2.0. The deprecation is on init.
|
||||
state.engine.handle_text(buffer, IO.chardata_to_string(chars))
|
||||
end
|
||||
|
||||
@@ -340,13 +353,6 @@ defmodule EEx.Compiler do
|
||||
scope,
|
||||
state
|
||||
) do
|
||||
if mark == ~c"" do
|
||||
message =
|
||||
"the contents of this expression won't be output unless the EEx block starts with \"<%=\""
|
||||
|
||||
:elixir_errors.erl_warn({meta.line, meta.column}, state.file, message)
|
||||
end
|
||||
|
||||
{rest, line, contents} = look_ahead_middle(rest, meta.line, chars) || {rest, meta.line, chars}
|
||||
start_line = meta.line
|
||||
start_column = column(meta.column, mark)
|
||||
@@ -359,6 +365,13 @@ defmodule EEx.Compiler do
|
||||
%{state | quoted: [], line: line}
|
||||
)
|
||||
|
||||
if mark == ~c"" and not match?({:=, _, [_, _]}, contents) do
|
||||
message =
|
||||
"the contents of this expression won't be output unless the EEx block starts with \"<%=\""
|
||||
|
||||
:elixir_errors.erl_warn({meta.line, meta.column}, state.file, message)
|
||||
end
|
||||
|
||||
buffer = state.engine.handle_expr(buffer, IO.chardata_to_string(mark), contents)
|
||||
generate_buffer(rest, buffer, scope, state)
|
||||
end
|
||||
|
||||
@@ -43,15 +43,6 @@ defmodule EEx.SmartEngineTest do
|
||||
assert_received :found
|
||||
end
|
||||
|
||||
test "error with unused \"do\" block without \"<%=\" modifier" do
|
||||
stderr =
|
||||
ExUnit.CaptureIO.capture_io(:stderr, fn ->
|
||||
assert_eval("", "<% if true do %>I'm invisible!<% end %>", assigns: %{})
|
||||
end)
|
||||
|
||||
assert stderr =~ "the contents of this expression won't be output"
|
||||
end
|
||||
|
||||
defp assert_eval(expected, actual, binding \\ []) do
|
||||
result = EEx.eval_string(actual, binding, file: __ENV__.file, engine: EEx.SmartEngine)
|
||||
assert result == expected
|
||||
|
||||
@@ -126,51 +126,21 @@ defmodule EEx.TokenizerTest do
|
||||
end
|
||||
|
||||
test "EEx comments" do
|
||||
exprs = [
|
||||
{:text, ~c"foo ", %{column: 1, line: 1}},
|
||||
{:eof, %{column: 16, line: 1}}
|
||||
]
|
||||
ExUnit.CaptureIO.capture_io(:stderr, fn ->
|
||||
exprs = [
|
||||
{:text, ~c"foo ", %{column: 1, line: 1}},
|
||||
{:eof, %{column: 16, line: 1}}
|
||||
]
|
||||
|
||||
assert EEx.tokenize(~c"foo <%# true %>", @opts) == {:ok, exprs}
|
||||
assert EEx.tokenize(~c"foo <%# true %>", @opts) == {:ok, exprs}
|
||||
|
||||
exprs = [
|
||||
{:text, ~c"foo ", %{column: 1, line: 1}},
|
||||
{:eof, %{column: 8, line: 2}}
|
||||
]
|
||||
exprs = [
|
||||
{:text, ~c"foo ", %{column: 1, line: 1}},
|
||||
{:eof, %{column: 8, line: 2}}
|
||||
]
|
||||
|
||||
assert EEx.tokenize(~c"foo <%#\ntrue %>", @opts) == {:ok, exprs}
|
||||
end
|
||||
|
||||
test "EEx comments with do-end" do
|
||||
exprs = [
|
||||
{:text, ~c"foo ", %{column: 1, line: 1}},
|
||||
{:text, ~c"bar", %{column: 19, line: 1}},
|
||||
{:eof, %{column: 32, line: 1}}
|
||||
]
|
||||
|
||||
assert EEx.tokenize(~c"foo <%# true do %>bar<%# end %>", @opts) == {:ok, exprs}
|
||||
end
|
||||
|
||||
test "EEx comments inside do-end" do
|
||||
exprs = [
|
||||
{:start_expr, ~c"", ~c" if true do ", %{column: 1, line: 1}},
|
||||
{:text, ~c"bar", %{column: 31, line: 1}},
|
||||
{:end_expr, [], ~c" end ", %{column: 34, line: 1}},
|
||||
{:eof, %{column: 43, line: 1}}
|
||||
]
|
||||
|
||||
assert EEx.tokenize(~c"<% if true do %><%# comment %>bar<% end %>", @opts) == {:ok, exprs}
|
||||
|
||||
exprs = [
|
||||
{:start_expr, [], ~c" case true do ", %{column: 1, line: 1}},
|
||||
{:middle_expr, ~c"", ~c" true -> ", %{column: 33, line: 1}},
|
||||
{:text, ~c"bar", %{column: 46, line: 1}},
|
||||
{:end_expr, [], ~c" end ", %{column: 49, line: 1}},
|
||||
{:eof, %{column: 58, line: 1}}
|
||||
]
|
||||
|
||||
assert EEx.tokenize(~c"<% case true do %><%# comment %><% true -> %>bar<% end %>", @opts) ==
|
||||
{:ok, exprs}
|
||||
assert EEx.tokenize(~c"foo <%#\ntrue %>", @opts) == {:ok, exprs}
|
||||
end)
|
||||
end
|
||||
|
||||
test "EEx multi-line comments" do
|
||||
@@ -321,15 +291,6 @@ defmodule EEx.TokenizerTest do
|
||||
assert EEx.tokenize(template, [trim: true] ++ @opts) == {:ok, exprs}
|
||||
end
|
||||
|
||||
test "trim mode with comment" do
|
||||
exprs = [
|
||||
{:text, ~c"\n123", %{column: 19, line: 1}},
|
||||
{:eof, %{column: 4, line: 2}}
|
||||
]
|
||||
|
||||
assert EEx.tokenize(~c" <%# comment %> \n123", [trim: true] ++ @opts) == {:ok, exprs}
|
||||
end
|
||||
|
||||
test "trim mode with multi-line comment" do
|
||||
exprs = [
|
||||
{:comment, ~c" comment ", %{column: 3, line: 1}},
|
||||
@@ -384,9 +345,6 @@ defmodule EEx.TokenizerTest do
|
||||
assert EEx.tokenize(~c"foo <% :bar", @opts) ==
|
||||
{:error, message, %{column: 5, line: 1}}
|
||||
|
||||
assert EEx.tokenize(~c"<%# true ", @opts) ==
|
||||
{:error, "expected closing '%>' for EEx expression", %{column: 1, line: 1}}
|
||||
|
||||
message = """
|
||||
expected closing '--%>' for EEx expression
|
||||
|
|
||||
|
||||
@@ -198,15 +198,6 @@ defmodule EExTest do
|
||||
assert_eval("foo baz", "foo <%= if false do %>bar<% else %>baz<% end %>")
|
||||
end
|
||||
|
||||
test "embedded code with comments in do end" do
|
||||
assert_eval("foo bar", "foo <%= case true do %><%# comment %><% true -> %>bar<% end %>")
|
||||
|
||||
assert_eval(
|
||||
"foo\n\nbar\n",
|
||||
"foo\n<%= case true do %>\n<%# comment %>\n<% true -> %>\nbar\n<% end %>"
|
||||
)
|
||||
end
|
||||
|
||||
test "embedded code with multi-line comments in do end" do
|
||||
assert_eval("foo bar", "foo <%= case true do %><%!-- comment --%><% true -> %>bar<% end %>")
|
||||
|
||||
@@ -543,6 +534,16 @@ defmodule EExTest do
|
||||
~s[unexpected beginning of EEx tag \"<%=\" on \"<%= end %>\"]
|
||||
end
|
||||
|
||||
test "unused \"do\" block without \"<%=\" modifier" do
|
||||
assert ExUnit.CaptureIO.capture_io(:stderr, fn ->
|
||||
EEx.compile_string("<% if true do %>I'm invisible!<% end %>")
|
||||
end) =~ "the contents of this expression won't be output"
|
||||
|
||||
# These are fine though
|
||||
EEx.compile_string("<% foo = fn -> %>Hello<% end %>")
|
||||
EEx.compile_string("<% foo = if true do %>Hello<% end %>")
|
||||
end
|
||||
|
||||
test "from tokenizer" do
|
||||
warning =
|
||||
ExUnit.CaptureIO.capture_io(:stderr, fn ->
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
warn_deprecated_function,
|
||||
warn_obsolete_guard,
|
||||
warn_exported_vars,
|
||||
%% warn_missing_spec,
|
||||
%% warn_untyped_record,
|
||||
%% Enable this when we require Erlang/OTP 27+
|
||||
%% warnings_as_errors,
|
||||
debug_info,
|
||||
{outdir, "ebin/"}
|
||||
]}.
|
||||
|
||||
@@ -350,7 +350,7 @@ defmodule Access do
|
||||
Accessing a list by index is typically discouraged in Elixir, \
|
||||
instead we prefer to use the Enum module to manipulate lists \
|
||||
as a whole. If you really must access a list element by index, \
|
||||
you can Enum.at/1 or the functions in the List module\
|
||||
you can use Enum.at/2 or the functions in the List module\
|
||||
"""
|
||||
end
|
||||
|
||||
@@ -418,8 +418,8 @@ defmodule Access do
|
||||
|
||||
Accessing a list by index is typically discouraged in Elixir, \
|
||||
instead we prefer to use the Enum module to manipulate lists \
|
||||
as a whole. If you really must mostify a list element by index, \
|
||||
you can Access.at/1 or the functions in the List module\
|
||||
as a whole. If you really must modify a list element by index, \
|
||||
you can use Access.at/1 or the functions in the List module\
|
||||
"""
|
||||
end
|
||||
|
||||
|
||||
@@ -207,7 +207,7 @@ defmodule Agent do
|
||||
@doc false
|
||||
defmacro __using__(opts) do
|
||||
quote location: :keep, bind_quoted: [opts: opts] do
|
||||
unless Module.has_attribute?(__MODULE__, :doc) do
|
||||
if not Module.has_attribute?(__MODULE__, :doc) do
|
||||
@doc """
|
||||
Returns a specification to start this module under a supervisor.
|
||||
|
||||
@@ -284,7 +284,7 @@ defmodule Agent do
|
||||
instead of an anonymous function; `fun` in `module` will be called with the
|
||||
given arguments `args` to initialize the state.
|
||||
"""
|
||||
@spec start_link(module, atom, [any], GenServer.options()) :: on_start
|
||||
@spec start_link(module, atom, [term], GenServer.options()) :: on_start
|
||||
def start_link(module, fun, args, options \\ []) do
|
||||
GenServer.start_link(Agent.Server, {module, fun, args}, options)
|
||||
end
|
||||
@@ -311,7 +311,7 @@ defmodule Agent do
|
||||
|
||||
See `start_link/4` for more information.
|
||||
"""
|
||||
@spec start(module, atom, [any], GenServer.options()) :: on_start
|
||||
@spec start(module, atom, [term], GenServer.options()) :: on_start
|
||||
def start(module, fun, args, options \\ []) do
|
||||
GenServer.start(Agent.Server, {module, fun, args}, options)
|
||||
end
|
||||
@@ -348,7 +348,7 @@ defmodule Agent do
|
||||
instead of an anonymous function. The state is added as first
|
||||
argument to the given list of arguments.
|
||||
"""
|
||||
@spec get(agent, module, atom, [term], timeout) :: any
|
||||
@spec get(agent, module, atom, [term], timeout) :: term
|
||||
def get(agent, module, fun, args, timeout \\ 5000) do
|
||||
GenServer.call(agent, {:get, {module, fun, args}}, timeout)
|
||||
end
|
||||
@@ -389,7 +389,7 @@ defmodule Agent do
|
||||
instead of an anonymous function. The state is added as first
|
||||
argument to the given list of arguments.
|
||||
"""
|
||||
@spec get_and_update(agent, module, atom, [term], timeout) :: any
|
||||
@spec get_and_update(agent, module, atom, [term], timeout) :: term
|
||||
def get_and_update(agent, module, fun, args, timeout \\ 5000) do
|
||||
GenServer.call(agent, {:get_and_update, {module, fun, args}}, timeout)
|
||||
end
|
||||
|
||||
@@ -54,12 +54,12 @@ defmodule Application do
|
||||
You can also change the application environment dynamically by using functions
|
||||
such as `put_env/3` and `delete_env/2`.
|
||||
|
||||
> #### Environment in libraries {: .tip}
|
||||
> #### Application environment in libraries {: .info}
|
||||
>
|
||||
> The config files `config/config.exs` and `config/runtime.exs`
|
||||
> are rarely used by libraries. Libraries typically define their environment
|
||||
> in the `application/0` function of their `mix.exs`. Configuration files
|
||||
> are rather used by applications to configure their libraries.
|
||||
> If you are writing a library to be used by other developers,
|
||||
> it is generally recommended to avoid the application environment, as the
|
||||
> application environment is effectively a global storage. For more information,
|
||||
> read about this [anti-pattern](design-anti-patterns.md#using-application-configuration-for-libraries).
|
||||
|
||||
> #### Reading the environment of other applications {: .warning}
|
||||
>
|
||||
@@ -673,13 +673,6 @@ defmodule Application do
|
||||
> You must use this function to read only your own application
|
||||
> environment. Do not read the environment of other applications.
|
||||
|
||||
> #### Application environment in libraries {: .info}
|
||||
>
|
||||
> If you are writing a library to be used by other developers,
|
||||
> it is generally recommended to avoid the application environment, as the
|
||||
> application environment is effectively a global storage. For more information,
|
||||
> read our [library guidelines](library-guidelines.md).
|
||||
|
||||
## Examples
|
||||
|
||||
`get_env/3` is commonly used to read the configuration of your OTP applications.
|
||||
@@ -796,6 +789,12 @@ defmodule Application do
|
||||
@doc """
|
||||
Puts the `value` in `key` for the given `app`.
|
||||
|
||||
> #### Compile environment {: .warning}
|
||||
>
|
||||
> Do not use this function to change environment variables read
|
||||
> via `Application.compile_env/2`. The compile environment must
|
||||
> be exclusively set before compilation, in your config files.
|
||||
|
||||
## Options
|
||||
|
||||
* `:timeout` - the timeout for the change (defaults to `5_000` milliseconds)
|
||||
|
||||
@@ -9,7 +9,8 @@ defmodule Behaviour do
|
||||
attributes.
|
||||
|
||||
Instead of `MyModule.__behaviour__(:callbacks)`,
|
||||
`MyModule.behaviour_info(:callbacks)` can be used.
|
||||
`MyModule.behaviour_info(:callbacks)` can be used. `behaviour_info/1`
|
||||
is documented in `Module`.
|
||||
"""
|
||||
|
||||
@moduledoc deprecated: "Use @callback and @macrocallback attributes instead"
|
||||
|
||||
@@ -764,8 +764,9 @@ defmodule Date do
|
||||
|
||||
When using the default ISO calendar, durations are collapsed and
|
||||
applied in the order of months and then days:
|
||||
- when shifting by 1 year and 2 months the date is actually shifted by 14 months
|
||||
- when shifting by 2 weeks and 3 days the date is shifted by 17 days
|
||||
|
||||
* when shifting by 1 year and 2 months the date is actually shifted by 14 months
|
||||
* when shifting by 2 weeks and 3 days the date is shifted by 17 days
|
||||
|
||||
When shifting by month, days are rounded down to the nearest valid date.
|
||||
|
||||
@@ -1072,13 +1073,7 @@ defmodule Date do
|
||||
def year_of_era(date)
|
||||
|
||||
def year_of_era(%{calendar: calendar, year: year, month: month, day: day}) do
|
||||
# TODO: Remove me on 1.17
|
||||
# The behaviour implementation already warns on missing callback.
|
||||
if function_exported?(calendar, :year_of_era, 3) do
|
||||
calendar.year_of_era(year, month, day)
|
||||
else
|
||||
calendar.year_of_era(year)
|
||||
end
|
||||
calendar.year_of_era(year, month, day)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -1158,12 +1153,11 @@ defmodule Date do
|
||||
|
||||
defimpl Inspect do
|
||||
def inspect(%{calendar: calendar, year: year, month: month, day: day}, _)
|
||||
when year in -9999..9999 do
|
||||
when calendar != Calendar.ISO or year in -9999..9999 do
|
||||
"~D[" <> calendar.date_to_string(year, month, day) <> suffix(calendar) <> "]"
|
||||
end
|
||||
|
||||
def inspect(%{calendar: calendar, year: year, month: month, day: day}, _)
|
||||
when calendar == Calendar.ISO do
|
||||
def inspect(%{calendar: Calendar.ISO, year: year, month: month, day: day}, _) do
|
||||
"Date.new!(#{Integer.to_string(year)}, #{Integer.to_string(month)}, #{Integer.to_string(day)})"
|
||||
end
|
||||
|
||||
|
||||
@@ -28,7 +28,12 @@ defmodule DateTime do
|
||||
## Time zone database
|
||||
|
||||
Many functions in this module require a time zone database.
|
||||
By default, it uses the default time zone database returned by
|
||||
A time zone database is a record of the UTC offsets that its locales have
|
||||
used at various times in the past, are using, and are expected to use in the
|
||||
future.
|
||||
Because those plans can change, it needs to be periodically updated.
|
||||
|
||||
By default, `DateTime` uses the default time zone database returned by
|
||||
`Calendar.get_time_zone_database/0`, which defaults to
|
||||
`Calendar.UTCOnlyTimeZoneDatabase` which only handles "Etc/UTC"
|
||||
datetimes and returns `{:error, :utc_only_time_zone_database}`
|
||||
@@ -43,7 +48,7 @@ defmodule DateTime do
|
||||
* [`zoneinfo`](https://github.com/smartrent/zoneinfo) -
|
||||
recommended for embedded devices
|
||||
|
||||
To use them, first make sure it is added as a dependency in `mix.exs`.
|
||||
To use one of them, first make sure it is added as a dependency in `mix.exs`.
|
||||
It can then be configured either via configuration:
|
||||
|
||||
config :elixir, :time_zone_database, Tz.TimeZoneDatabase
|
||||
@@ -1025,9 +1030,12 @@ defmodule DateTime do
|
||||
By default, `DateTime.to_iso8601/2` returns datetimes formatted in the "extended"
|
||||
format, for human readability. It also supports the "basic" format through passing the `:basic` option.
|
||||
|
||||
Only supports converting datetimes which are in the ISO calendar,
|
||||
attempting to convert datetimes from other calendars will raise.
|
||||
You can also optionally specify an offset for the formatted string.
|
||||
If none is given, the one in the given `datetime` is used.
|
||||
|
||||
Only supports converting datetimes which are in the ISO calendar.
|
||||
If another calendar is given, it is automatically converted to ISO.
|
||||
It raises if not possible.
|
||||
|
||||
WARNING: the ISO 8601 datetime format does not contain the time zone nor
|
||||
its abbreviation, which means information is lost when converting to such
|
||||
@@ -1341,6 +1349,11 @@ defmodule DateTime do
|
||||
@doc """
|
||||
Converts the given `datetime` to a string according to its calendar.
|
||||
|
||||
Unfortunately, there is no standard that specifies rendering of a
|
||||
datetime with its complete time zone information, so Elixir uses a
|
||||
custom (but relatively common) representation which appends the time
|
||||
zone abbreviation and full name to the datetime.
|
||||
|
||||
### Examples
|
||||
|
||||
iex> dt = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "CET",
|
||||
@@ -1708,8 +1721,9 @@ defmodule DateTime do
|
||||
|
||||
When using the default ISO calendar, durations are collapsed and
|
||||
applied in the order of months, then seconds and microseconds:
|
||||
- when shifting by 1 year and 2 months the date is actually shifted by 14 months
|
||||
- weeks, days and smaller units are collapsed into seconds and microseconds
|
||||
|
||||
* when shifting by 1 year and 2 months the date is actually shifted by 14 months
|
||||
* weeks, days and smaller units are collapsed into seconds and microseconds
|
||||
|
||||
When shifting by month, days are rounded down to the nearest valid date.
|
||||
|
||||
@@ -2078,7 +2092,8 @@ defmodule DateTime do
|
||||
)
|
||||
|
||||
case datetime do
|
||||
%{utc_offset: 0, std_offset: 0, time_zone: "Etc/UTC"} ->
|
||||
%{utc_offset: 0, std_offset: 0, time_zone: "Etc/UTC", year: year}
|
||||
when calendar != Calendar.ISO or year in -9999..9999 ->
|
||||
"~U[" <> formatted <> suffix(calendar) <> "]"
|
||||
|
||||
_ ->
|
||||
|
||||
@@ -26,7 +26,7 @@ defmodule Duration do
|
||||
iex> Date.shift(~D[2016-01-03], Duration.new!(month: 2))
|
||||
~D[2016-03-03]
|
||||
|
||||
It is important to notice that shifting is not an arithmetic operation.
|
||||
It is important to note that shifting is not an arithmetic operation.
|
||||
For example, adding `date + 1 month + 1 month` does not yield the same
|
||||
result as `date + 2 months`. Let's see an example:
|
||||
|
||||
@@ -46,9 +46,19 @@ defmodule Duration do
|
||||
* larger units (such as years and months) are applied before
|
||||
smaller ones (such as weeks, hours, days, and so on)
|
||||
|
||||
* units are collapsed into months (`:year` and `:month`),
|
||||
seconds (`:week`, `:day`, `:hour`, `:minute`, `:second`)
|
||||
and microseconds (`:microsecond`) before they are applied
|
||||
|
||||
* 1 year is equivalent to 12 months, 1 week is equivalent to 7 days.
|
||||
Therefore, 4 weeks _are not_ equivalent to 1 month
|
||||
|
||||
* in case of non-existing dates, the results are rounded down to the
|
||||
nearest valid date
|
||||
|
||||
As the `shift/2` functions are calendar aware, they are guaranteed to return
|
||||
valid date/times, considering leap years as well as DST in applicable time zones.
|
||||
|
||||
## Intervals
|
||||
|
||||
Durations in Elixir can be combined with stream operations to build intervals.
|
||||
@@ -124,6 +134,8 @@ defmodule Duration do
|
||||
"""
|
||||
@type duration :: t | [unit_pair]
|
||||
|
||||
@microseconds_per_second 1_000_000
|
||||
|
||||
@doc """
|
||||
Creates a new `Duration` struct from given `unit_pairs`.
|
||||
|
||||
@@ -181,9 +193,9 @@ defmodule Duration do
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Duration.add(%Duration{week: 2, day: 1}, %Duration{day: 2})
|
||||
iex> Duration.add(Duration.new!(week: 2, day: 1), Duration.new!(day: 2))
|
||||
%Duration{week: 2, day: 3}
|
||||
iex> Duration.add(%Duration{microsecond: {400, 3}}, %Duration{microsecond: {600, 6}})
|
||||
iex> Duration.add(Duration.new!(microsecond: {400, 3}), Duration.new!(microsecond: {600, 6}))
|
||||
%Duration{microsecond: {1000, 6}}
|
||||
|
||||
"""
|
||||
@@ -211,9 +223,9 @@ defmodule Duration do
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Duration.subtract(%Duration{week: 2, day: 1}, %Duration{day: 2})
|
||||
iex> Duration.subtract(Duration.new!(week: 2, day: 1), Duration.new!(day: 2))
|
||||
%Duration{week: 2, day: -1}
|
||||
iex> Duration.subtract(%Duration{microsecond: {400, 6}}, %Duration{microsecond: {600, 3}})
|
||||
iex> Duration.subtract(Duration.new!(microsecond: {400, 6}), Duration.new!(microsecond: {600, 3}))
|
||||
%Duration{microsecond: {-200, 6}}
|
||||
|
||||
"""
|
||||
@@ -239,9 +251,9 @@ defmodule Duration do
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Duration.multiply(%Duration{day: 1, minute: 15, second: -10}, 3)
|
||||
iex> Duration.multiply(Duration.new!(day: 1, minute: 15, second: -10), 3)
|
||||
%Duration{day: 3, minute: 45, second: -30}
|
||||
iex> Duration.multiply(%Duration{microsecond: {200, 4}}, 3)
|
||||
iex> Duration.multiply(Duration.new!(microsecond: {200, 4}), 3)
|
||||
%Duration{microsecond: {600, 4}}
|
||||
|
||||
"""
|
||||
@@ -264,9 +276,9 @@ defmodule Duration do
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Duration.negate(%Duration{day: 1, minute: 15, second: -10})
|
||||
iex> Duration.negate(Duration.new!(day: 1, minute: 15, second: -10))
|
||||
%Duration{day: -1, minute: -15, second: 10}
|
||||
iex> Duration.negate(%Duration{microsecond: {500000, 4}})
|
||||
iex> Duration.negate(Duration.new!(microsecond: {500000, 4}))
|
||||
%Duration{microsecond: {-500000, 4}}
|
||||
|
||||
"""
|
||||
@@ -283,4 +295,248 @@ defmodule Duration do
|
||||
microsecond: {-ms, p}
|
||||
}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Parses an [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601#Durations) formatted duration string to a `Duration` struct.
|
||||
|
||||
Duration strings, as well as individual units, may be prefixed with plus/minus signs so that:
|
||||
|
||||
- `-PT6H3M` parses as `%Duration{hour: -6, minute: -3}`
|
||||
- `-PT6H-3M` parses as `%Duration{hour: -6, minute: 3}`
|
||||
- `+PT6H3M` parses as `%Duration{hour: 6, minute: 3}`
|
||||
- `+PT6H-3M` parses as `%Duration{hour: 6, minute: -3}`
|
||||
|
||||
Duration designators must be provided in order of magnitude: `P[n]Y[n]M[n]W[n]DT[n]H[n]M[n]S`.
|
||||
|
||||
Only seconds may be specified with a decimal fraction, using either a comma or a full stop: `P1DT4,5S`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Duration.from_iso8601("P1Y2M3DT4H5M6S")
|
||||
{:ok, %Duration{year: 1, month: 2, day: 3, hour: 4, minute: 5, second: 6}}
|
||||
iex> Duration.from_iso8601("P3Y-2MT3H")
|
||||
{:ok, %Duration{year: 3, month: -2, hour: 3}}
|
||||
iex> Duration.from_iso8601("-PT10H-30M")
|
||||
{:ok, %Duration{hour: -10, minute: 30}}
|
||||
iex> Duration.from_iso8601("PT4.650S")
|
||||
{:ok, %Duration{second: 4, microsecond: {650000, 3}}}
|
||||
|
||||
"""
|
||||
@spec from_iso8601(String.t()) :: {:ok, t} | {:error, atom}
|
||||
def from_iso8601(string) when is_binary(string) do
|
||||
case Calendar.ISO.parse_duration(string) do
|
||||
{:ok, duration} ->
|
||||
{:ok, new!(duration)}
|
||||
|
||||
error ->
|
||||
error
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Same as `from_iso8601/1` but raises an `ArgumentError`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Duration.from_iso8601!("P1Y2M3DT4H5M6S")
|
||||
%Duration{year: 1, month: 2, day: 3, hour: 4, minute: 5, second: 6}
|
||||
iex> Duration.from_iso8601!("P10D")
|
||||
%Duration{day: 10}
|
||||
|
||||
"""
|
||||
@spec from_iso8601!(String.t()) :: t
|
||||
def from_iso8601!(string) when is_binary(string) do
|
||||
case from_iso8601(string) do
|
||||
{:ok, duration} ->
|
||||
duration
|
||||
|
||||
{:error, reason} ->
|
||||
raise ArgumentError, ~s/failed to parse duration "#{string}". reason: #{inspect(reason)}/
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts the given `duration` to a human readable representation.
|
||||
|
||||
## Options
|
||||
|
||||
* `:units` - the units to be used alongside each duration component.
|
||||
The default units follow the ISO 80000-3 standard:
|
||||
|
||||
[
|
||||
year: "a",
|
||||
month: "mo",
|
||||
week: "wk",
|
||||
day: "d",
|
||||
hour: "h",
|
||||
minute: "min",
|
||||
second: "s"
|
||||
]
|
||||
|
||||
* `:separator` - a string used to separate the distinct components. Defaults to `" "`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Duration.to_string(Duration.new!(second: 30))
|
||||
"30s"
|
||||
iex> Duration.to_string(Duration.new!(day: 40, hour: 12, minute: 42, second: 12))
|
||||
"40d 12h 42min 12s"
|
||||
|
||||
By default, this function uses ISO 80000-3 units, which uses "a" for years.
|
||||
But you can customize all units via the units option:
|
||||
|
||||
iex> Duration.to_string(Duration.new!(year: 3))
|
||||
"3a"
|
||||
iex> Duration.to_string(Duration.new!(year: 3), units: [year: "y"])
|
||||
"3y"
|
||||
|
||||
You may also choose the separator:
|
||||
|
||||
iex> Duration.to_string(Duration.new!(day: 40, hour: 12, minute: 42, second: 12), separator: ", ")
|
||||
"40d, 12h, 42min, 12s"
|
||||
|
||||
A duration without components is rendered as "0s":
|
||||
|
||||
iex> Duration.to_string(Duration.new!([]))
|
||||
"0s"
|
||||
|
||||
Microseconds are rendered as part of seconds with the appropriate precision:
|
||||
|
||||
iex> Duration.to_string(Duration.new!(second: 1, microsecond: {2_200, 3}))
|
||||
"1.002s"
|
||||
iex> Duration.to_string(Duration.new!(second: 1, microsecond: {-1_200_000, 4}))
|
||||
"-0.2000s"
|
||||
|
||||
"""
|
||||
@doc since: "1.18.0"
|
||||
def to_string(%Duration{} = duration, opts \\ []) do
|
||||
units = Keyword.get(opts, :units, [])
|
||||
separator = Keyword.get(opts, :separator, " ")
|
||||
|
||||
case to_string_year(duration, [], units) do
|
||||
[] ->
|
||||
"0" <> Keyword.get(units, :second, "s")
|
||||
|
||||
[part] ->
|
||||
IO.iodata_to_binary(part)
|
||||
|
||||
parts ->
|
||||
parts |> Enum.reduce(&[&1, separator | &2]) |> IO.iodata_to_binary()
|
||||
end
|
||||
end
|
||||
|
||||
defp to_string_part(0, _units, _key, _default, acc),
|
||||
do: acc
|
||||
|
||||
defp to_string_part(x, units, key, default, acc),
|
||||
do: [[Integer.to_string(x) | Keyword.get(units, key, default)] | acc]
|
||||
|
||||
defp to_string_year(%{year: year} = duration, acc, units) do
|
||||
to_string_month(duration, to_string_part(year, units, :year, "a", acc), units)
|
||||
end
|
||||
|
||||
defp to_string_month(%{month: month} = duration, acc, units) do
|
||||
to_string_week(duration, to_string_part(month, units, :month, "mo", acc), units)
|
||||
end
|
||||
|
||||
defp to_string_week(%{week: week} = duration, acc, units) do
|
||||
to_string_day(duration, to_string_part(week, units, :week, "wk", acc), units)
|
||||
end
|
||||
|
||||
defp to_string_day(%{day: day} = duration, acc, units) do
|
||||
to_string_hour(duration, to_string_part(day, units, :day, "d", acc), units)
|
||||
end
|
||||
|
||||
defp to_string_hour(%{hour: hour} = duration, acc, units) do
|
||||
to_string_minute(duration, to_string_part(hour, units, :hour, "h", acc), units)
|
||||
end
|
||||
|
||||
defp to_string_minute(%{minute: minute} = duration, acc, units) do
|
||||
to_string_second(duration, to_string_part(minute, units, :minute, "min", acc), units)
|
||||
end
|
||||
|
||||
defp to_string_second(%{second: 0, microsecond: {0, _}}, acc, _units) do
|
||||
acc
|
||||
end
|
||||
|
||||
defp to_string_second(%{second: s, microsecond: {ms, p}}, acc, units) do
|
||||
[[second_component(s, ms, p) | Keyword.get(units, :second, "s")] | acc]
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts the given `duration` to an [ISO 8601-2:2019](https://en.wikipedia.org/wiki/ISO_8601) formatted string.
|
||||
|
||||
This function implements the extension of ISO 8601:2019, allowing weeks to appear between months and days: `P3M3W3D`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Duration.to_iso8601(Duration.new!(year: 3))
|
||||
"P3Y"
|
||||
iex> Duration.to_iso8601(Duration.new!(day: 40, hour: 12, minute: 42, second: 12))
|
||||
"P40DT12H42M12S"
|
||||
iex> Duration.to_iso8601(Duration.new!(second: 30))
|
||||
"PT30S"
|
||||
|
||||
iex> Duration.to_iso8601(Duration.new!([]))
|
||||
"PT0S"
|
||||
|
||||
iex> Duration.to_iso8601(Duration.new!(second: 1, microsecond: {2_200, 3}))
|
||||
"PT1.002S"
|
||||
iex> Duration.to_iso8601(Duration.new!(second: 1, microsecond: {-1_200_000, 4}))
|
||||
"PT-0.2000S"
|
||||
"""
|
||||
|
||||
@spec to_iso8601(t) :: String.t()
|
||||
def to_iso8601(%Duration{} = duration) do
|
||||
case {to_iso8601_duration_date(duration), to_iso8601_duration_time(duration)} do
|
||||
{[], []} -> "PT0S"
|
||||
{date, time} -> IO.iodata_to_binary([?P, date, time])
|
||||
end
|
||||
end
|
||||
|
||||
defp to_iso8601_duration_date(%{year: 0, month: 0, week: 0, day: 0}) do
|
||||
[]
|
||||
end
|
||||
|
||||
defp to_iso8601_duration_date(%{year: year, month: month, week: week, day: day}) do
|
||||
[pair(year, ?Y), pair(month, ?M), pair(week, ?W), pair(day, ?D)]
|
||||
end
|
||||
|
||||
defp to_iso8601_duration_time(%{hour: 0, minute: 0, second: 0, microsecond: {0, _}}) do
|
||||
[]
|
||||
end
|
||||
|
||||
defp to_iso8601_duration_time(%{hour: hour, minute: minute} = d) do
|
||||
[?T, pair(hour, ?H), pair(minute, ?M), second_component(d)]
|
||||
end
|
||||
|
||||
defp second_component(%{second: 0, microsecond: {0, _}}) do
|
||||
[]
|
||||
end
|
||||
|
||||
defp second_component(%{second: second, microsecond: {ms, p}}) do
|
||||
[second_component(second, ms, p), ?S]
|
||||
end
|
||||
|
||||
defp second_component(second, _ms, 0) do
|
||||
Integer.to_string(second)
|
||||
end
|
||||
|
||||
defp second_component(second, ms, p) do
|
||||
total_ms = second * @microseconds_per_second + ms
|
||||
second = total_ms |> div(@microseconds_per_second) |> abs()
|
||||
ms = total_ms |> rem(@microseconds_per_second) |> abs()
|
||||
sign = if total_ms < 0, do: ?-, else: []
|
||||
|
||||
[
|
||||
sign,
|
||||
Integer.to_string(second),
|
||||
?.,
|
||||
ms |> Integer.to_string() |> String.pad_leading(6, "0") |> binary_part(0, p)
|
||||
]
|
||||
end
|
||||
|
||||
@compile {:inline, pair: 2}
|
||||
defp pair(0, _key), do: []
|
||||
defp pair(num, key), do: [Integer.to_string(num), key]
|
||||
end
|
||||
|
||||
@@ -18,7 +18,8 @@ defmodule Calendar.ISO do
|
||||
|
||||
The standard library supports a minimal set of possible ISO 8601 features.
|
||||
Specifically, the parser only supports calendar dates and does not support
|
||||
ordinal and week formats.
|
||||
ordinal and week formats. Additionally, it supports parsing ISO 8601
|
||||
formatted durations, including negative time units and fractional seconds.
|
||||
|
||||
By default Elixir only parses extended-formatted date/times. You can opt-in
|
||||
to parse basic-formatted date/times.
|
||||
@@ -29,7 +30,7 @@ defmodule Calendar.ISO do
|
||||
|
||||
Elixir does not support reduced accuracy formats (for example, a date without
|
||||
the day component) nor decimal precisions in the lowest component (such as
|
||||
`10:01:25,5`). No functions exist to parse ISO 8601 durations or time intervals.
|
||||
`10:01:25,5`).
|
||||
|
||||
#### Examples
|
||||
|
||||
@@ -663,6 +664,87 @@ defmodule Calendar.ISO do
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Parses an ISO 8601 formatted duration string to a list of `Duration` compabitble unit pairs.
|
||||
|
||||
See `Duration.from_iso8601/1`.
|
||||
"""
|
||||
@doc since: "1.17.0"
|
||||
@spec parse_duration(String.t()) :: {:ok, [Duration.unit_pair()]} | {:error, atom}
|
||||
def parse_duration("P" <> string) when byte_size(string) > 0 do
|
||||
parse_duration_date(string, [], year: ?Y, month: ?M, week: ?W, day: ?D)
|
||||
end
|
||||
|
||||
def parse_duration("+P" <> string) when byte_size(string) > 0 do
|
||||
parse_duration_date(string, [], year: ?Y, month: ?M, week: ?W, day: ?D)
|
||||
end
|
||||
|
||||
def parse_duration("-P" <> string) when byte_size(string) > 0 do
|
||||
with {:ok, fields} <- parse_duration_date(string, [], year: ?Y, month: ?M, week: ?W, day: ?D) do
|
||||
{:ok,
|
||||
Enum.map(fields, fn
|
||||
{:microsecond, {value, precision}} -> {:microsecond, {-value, precision}}
|
||||
{unit, value} -> {unit, -value}
|
||||
end)}
|
||||
end
|
||||
end
|
||||
|
||||
def parse_duration(_) do
|
||||
{:error, :invalid_duration}
|
||||
end
|
||||
|
||||
defp parse_duration_date("", acc, _allowed), do: {:ok, acc}
|
||||
|
||||
defp parse_duration_date("T" <> string, acc, _allowed) when byte_size(string) > 0 do
|
||||
parse_duration_time(string, acc, hour: ?H, minute: ?M, second: ?S)
|
||||
end
|
||||
|
||||
defp parse_duration_date(string, acc, allowed) do
|
||||
with {integer, <<next, rest::binary>>} <- Integer.parse(string),
|
||||
{key, allowed} <- find_unit(allowed, next) do
|
||||
parse_duration_date(rest, [{key, integer} | acc], allowed)
|
||||
else
|
||||
_ -> {:error, :invalid_date_component}
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_duration_time("", acc, _allowed), do: {:ok, acc}
|
||||
|
||||
defp parse_duration_time(string, acc, allowed) do
|
||||
case Integer.parse(string) do
|
||||
{second, <<delimiter, _::binary>> = rest} when delimiter in [?., ?,] ->
|
||||
case parse_microsecond(rest) do
|
||||
{{ms, precision}, "S"} ->
|
||||
ms =
|
||||
case string do
|
||||
"-" <> _ ->
|
||||
-ms
|
||||
|
||||
_ ->
|
||||
ms
|
||||
end
|
||||
|
||||
{:ok, [second: second, microsecond: {ms, precision}] ++ acc}
|
||||
|
||||
_ ->
|
||||
{:error, :invalid_time_component}
|
||||
end
|
||||
|
||||
{integer, <<next, rest::binary>>} ->
|
||||
case find_unit(allowed, next) do
|
||||
{key, allowed} -> parse_duration_time(rest, [{key, integer} | acc], allowed)
|
||||
false -> {:error, :invalid_time_component}
|
||||
end
|
||||
|
||||
_ ->
|
||||
{:error, :invalid_time_component}
|
||||
end
|
||||
end
|
||||
|
||||
defp find_unit([{key, unit} | rest], unit), do: {key, rest}
|
||||
defp find_unit([_ | rest], unit), do: find_unit(rest, unit)
|
||||
defp find_unit([], _unit), do: false
|
||||
|
||||
@doc """
|
||||
Returns the `t:Calendar.iso_days/0` format of the specified date.
|
||||
|
||||
@@ -1628,7 +1710,7 @@ defmodule Calendar.ISO do
|
||||
hour: 0,
|
||||
minute: 0,
|
||||
second: 0,
|
||||
microsecond: {0, 0}
|
||||
microsecond: {0, _precision}
|
||||
}) do
|
||||
[
|
||||
month: year * 12 + month,
|
||||
|
||||
@@ -110,30 +110,12 @@ defmodule NaiveDateTime do
|
||||
@spec utc_now(Calendar.calendar() | :native | :microsecond | :millisecond | :second) :: t
|
||||
def utc_now(calendar_or_time_unit \\ Calendar.ISO)
|
||||
|
||||
def utc_now(Calendar.ISO) do
|
||||
{:ok, {year, month, day}, {hour, minute, second}, microsecond} =
|
||||
Calendar.ISO.from_unix(:os.system_time(), :native)
|
||||
|
||||
%NaiveDateTime{
|
||||
year: year,
|
||||
month: month,
|
||||
day: day,
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
second: second,
|
||||
microsecond: microsecond,
|
||||
calendar: Calendar.ISO
|
||||
}
|
||||
end
|
||||
|
||||
def utc_now(time_unit) when time_unit in [:microsecond, :millisecond, :second, :native] do
|
||||
utc_now(time_unit, Calendar.ISO)
|
||||
end
|
||||
|
||||
def utc_now(calendar) do
|
||||
calendar
|
||||
|> DateTime.utc_now()
|
||||
|> DateTime.to_naive()
|
||||
utc_now(:native, calendar)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -158,7 +140,20 @@ defmodule NaiveDateTime do
|
||||
@spec utc_now(:native | :microsecond | :millisecond | :second, Calendar.calendar()) :: t
|
||||
def utc_now(time_unit, calendar)
|
||||
when time_unit in [:native, :microsecond, :millisecond, :second] do
|
||||
DateTime.utc_now(time_unit, calendar) |> DateTime.to_naive()
|
||||
{:ok, {year, month, day}, {hour, minute, second}, microsecond} =
|
||||
Calendar.ISO.from_unix(System.os_time(time_unit), time_unit)
|
||||
|
||||
%NaiveDateTime{
|
||||
year: year,
|
||||
month: month,
|
||||
day: day,
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
second: second,
|
||||
microsecond: microsecond,
|
||||
calendar: Calendar.ISO
|
||||
}
|
||||
|> convert!(calendar)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -252,7 +247,7 @@ defmodule NaiveDateTime do
|
||||
Calendar.hour(),
|
||||
Calendar.minute(),
|
||||
Calendar.second(),
|
||||
Calendar.microsecond() | non_neg_integer,
|
||||
Calendar.microsecond() | non_neg_integer(),
|
||||
Calendar.calendar()
|
||||
) :: {:ok, t} | {:error, atom}
|
||||
def new(year, month, day, hour, minute, second, microsecond \\ {0, 0}, calendar \\ Calendar.ISO)
|
||||
@@ -317,7 +312,7 @@ defmodule NaiveDateTime do
|
||||
Calendar.hour(),
|
||||
Calendar.minute(),
|
||||
Calendar.second(),
|
||||
Calendar.microsecond() | non_neg_integer,
|
||||
Calendar.microsecond() | non_neg_integer(),
|
||||
Calendar.calendar()
|
||||
) :: t
|
||||
def new!(
|
||||
@@ -578,8 +573,9 @@ defmodule NaiveDateTime do
|
||||
|
||||
When using the default ISO calendar, durations are collapsed and
|
||||
applied in the order of months, then seconds and microseconds:
|
||||
- when shifting by 1 year and 2 months the date is actually shifted by 14 months
|
||||
- weeks, days and smaller units are collapsed into seconds and microseconds
|
||||
|
||||
* when shifting by 1 year and 2 months the date is actually shifted by 14 months
|
||||
* weeks, days and smaller units are collapsed into seconds and microseconds
|
||||
|
||||
When shifting by month, days are rounded down to the nearest valid date.
|
||||
|
||||
@@ -758,6 +754,9 @@ defmodule NaiveDateTime do
|
||||
@doc """
|
||||
Converts the given naive datetime to a string according to its calendar.
|
||||
|
||||
For redability, this function follows the RFC3339 suggestion of removing
|
||||
the "T" separator between the date and time components.
|
||||
|
||||
### Examples
|
||||
|
||||
iex> NaiveDateTime.to_string(~N[2000-02-28 23:00:13])
|
||||
@@ -992,6 +991,8 @@ defmodule NaiveDateTime do
|
||||
|
||||
iex> NaiveDateTime.from_erl({{2000, 1, 1}, {13, 30, 15}})
|
||||
{:ok, ~N[2000-01-01 13:30:15]}
|
||||
iex> NaiveDateTime.from_erl({{2000, 1, 1}, {13, 30, 15}}, 5000)
|
||||
{:ok, ~N[2000-01-01 13:30:15.005000]}
|
||||
iex> NaiveDateTime.from_erl({{2000, 1, 1}, {13, 30, 15}}, {5000, 3})
|
||||
{:ok, ~N[2000-01-01 13:30:15.005]}
|
||||
iex> NaiveDateTime.from_erl({{2000, 13, 1}, {13, 30, 15}})
|
||||
@@ -1000,7 +1001,11 @@ defmodule NaiveDateTime do
|
||||
{:error, :invalid_date}
|
||||
|
||||
"""
|
||||
@spec from_erl(:calendar.datetime(), Calendar.microsecond(), Calendar.calendar()) ::
|
||||
@spec from_erl(
|
||||
:calendar.datetime(),
|
||||
Calendar.microsecond() | non_neg_integer(),
|
||||
Calendar.calendar()
|
||||
) ::
|
||||
{:ok, t} | {:error, atom}
|
||||
def from_erl(tuple, microsecond \\ {0, 0}, calendar \\ Calendar.ISO)
|
||||
|
||||
@@ -1019,13 +1024,19 @@ defmodule NaiveDateTime do
|
||||
|
||||
iex> NaiveDateTime.from_erl!({{2000, 1, 1}, {13, 30, 15}})
|
||||
~N[2000-01-01 13:30:15]
|
||||
iex> NaiveDateTime.from_erl!({{2000, 1, 1}, {13, 30, 15}}, 5000)
|
||||
~N[2000-01-01 13:30:15.005000]
|
||||
iex> NaiveDateTime.from_erl!({{2000, 1, 1}, {13, 30, 15}}, {5000, 3})
|
||||
~N[2000-01-01 13:30:15.005]
|
||||
iex> NaiveDateTime.from_erl!({{2000, 13, 1}, {13, 30, 15}})
|
||||
** (ArgumentError) cannot convert {{2000, 13, 1}, {13, 30, 15}} to naive datetime, reason: :invalid_date
|
||||
|
||||
"""
|
||||
@spec from_erl!(:calendar.datetime(), Calendar.microsecond(), Calendar.calendar()) :: t
|
||||
@spec from_erl!(
|
||||
:calendar.datetime(),
|
||||
Calendar.microsecond() | non_neg_integer(),
|
||||
Calendar.calendar()
|
||||
) :: t
|
||||
def from_erl!(tuple, microsecond \\ {0, 0}, calendar \\ Calendar.ISO) do
|
||||
case from_erl(tuple, microsecond, calendar) do
|
||||
{:ok, value} ->
|
||||
@@ -1249,6 +1260,10 @@ defmodule NaiveDateTime do
|
||||
{:ok, t} | {:error, :incompatible_calendars}
|
||||
|
||||
# Keep it multiline for proper function clause errors.
|
||||
def convert(%NaiveDateTime{calendar: calendar} = ndt, calendar) do
|
||||
{:ok, ndt}
|
||||
end
|
||||
|
||||
def convert(
|
||||
%{
|
||||
calendar: calendar,
|
||||
@@ -1450,10 +1465,15 @@ defmodule NaiveDateTime do
|
||||
calendar: calendar
|
||||
} = naive_datetime
|
||||
|
||||
formatted =
|
||||
calendar.naive_datetime_to_string(year, month, day, hour, minute, second, microsecond)
|
||||
if calendar != Calendar.ISO or year in -9999..9999 do
|
||||
formatted =
|
||||
calendar.naive_datetime_to_string(year, month, day, hour, minute, second, microsecond)
|
||||
|
||||
"~N[" <> formatted <> suffix(calendar) <> "]"
|
||||
"~N[" <> formatted <> suffix(calendar) <> "]"
|
||||
else
|
||||
"NaiveDateTime.new!(#{Integer.to_string(year)}, #{Integer.to_string(month)}, #{Integer.to_string(day)}, " <>
|
||||
"#{Integer.to_string(hour)}, #{Integer.to_string(minute)}, #{Integer.to_string(second)}, #{inspect(microsecond)})"
|
||||
end
|
||||
end
|
||||
|
||||
defp suffix(Calendar.ISO), do: ""
|
||||
|
||||
@@ -115,7 +115,7 @@ defmodule Time do
|
||||
Calendar.hour(),
|
||||
Calendar.minute(),
|
||||
Calendar.second(),
|
||||
Calendar.microsecond() | non_neg_integer,
|
||||
Calendar.microsecond() | non_neg_integer(),
|
||||
Calendar.calendar()
|
||||
) :: {:ok, t} | {:error, atom}
|
||||
def new(hour, minute, second, microsecond \\ {0, 0}, calendar \\ Calendar.ISO)
|
||||
@@ -355,13 +355,21 @@ defmodule Time do
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Time.from_erl({23, 30, 15})
|
||||
{:ok, ~T[23:30:15]}
|
||||
iex> Time.from_erl({23, 30, 15}, 5000)
|
||||
{:ok, ~T[23:30:15.005000]}
|
||||
iex> Time.from_erl({23, 30, 15}, {5000, 3})
|
||||
{:ok, ~T[23:30:15.005]}
|
||||
iex> Time.from_erl({24, 30, 15})
|
||||
{:error, :invalid_time}
|
||||
|
||||
"""
|
||||
@spec from_erl(:calendar.time(), Calendar.microsecond(), Calendar.calendar()) ::
|
||||
@spec from_erl(
|
||||
:calendar.time(),
|
||||
Calendar.microsecond() | non_neg_integer(),
|
||||
Calendar.calendar()
|
||||
) ::
|
||||
{:ok, t} | {:error, atom}
|
||||
def from_erl(tuple, microsecond \\ {0, 0}, calendar \\ Calendar.ISO)
|
||||
|
||||
@@ -377,6 +385,8 @@ defmodule Time do
|
||||
|
||||
iex> Time.from_erl!({23, 30, 15})
|
||||
~T[23:30:15]
|
||||
iex> Time.from_erl!({23, 30, 15}, 5000)
|
||||
~T[23:30:15.005000]
|
||||
iex> Time.from_erl!({23, 30, 15}, {5000, 3})
|
||||
~T[23:30:15.005]
|
||||
iex> Time.from_erl!({24, 30, 15})
|
||||
@@ -521,7 +531,7 @@ defmodule Time do
|
||||
do: unit > 0,
|
||||
else: unit in ~w(second millisecond microsecond nanosecond)a
|
||||
|
||||
unless valid? do
|
||||
if not valid? do
|
||||
raise ArgumentError,
|
||||
"unsupported time unit. Expected :hour, :minute, :second, :millisecond, :microsecond, :nanosecond, or a positive integer, got #{inspect(unit)}"
|
||||
end
|
||||
@@ -797,15 +807,8 @@ defmodule Time do
|
||||
@doc since: "1.5.0"
|
||||
@spec convert!(Calendar.time(), Calendar.calendar()) :: t
|
||||
def convert!(time, calendar) do
|
||||
case convert(time, calendar) do
|
||||
{:ok, value} ->
|
||||
value
|
||||
|
||||
{:error, reason} ->
|
||||
raise ArgumentError,
|
||||
"cannot convert #{inspect(time)} to target calendar #{inspect(calendar)}, " <>
|
||||
"reason: #{inspect(reason)}"
|
||||
end
|
||||
{:ok, value} = convert(time, calendar)
|
||||
value
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
||||
+99
-38
@@ -123,6 +123,11 @@ defmodule Code do
|
||||
of the imported function/macro. A :remote_function/:remote_macro event
|
||||
may still be emitted for the imported module/name/arity.
|
||||
|
||||
* `{:imported_quoted, meta, module, name, [arity]}` - traced whenever an
|
||||
imported function or macro is processed inside a `quote/2`. `meta` is the
|
||||
call AST metadata, `module` is the module the import is from, followed by
|
||||
the `name` and a list of `arities` of the imported function/macro.
|
||||
|
||||
* `{:alias, meta, alias, as, opts}` - traced whenever `alias` is aliased
|
||||
to `as`. `meta` is the alias AST metadata and `opts` are the alias options.
|
||||
|
||||
@@ -180,7 +185,7 @@ defmodule Code do
|
||||
|
||||
defmodule MyTracer do
|
||||
def trace({:remote_function, _meta, module, name, arity}, env) do
|
||||
IO.puts "#{env.file}:#{env.line} #{inspect(module)}.#{name}/#{arity}"
|
||||
IO.puts("#{env.file}:#{env.line} #{inspect(module)}.#{name}/#{arity}")
|
||||
:ok
|
||||
end
|
||||
|
||||
@@ -244,8 +249,8 @@ defmodule Code do
|
||||
:debug_info,
|
||||
:ignore_already_consolidated,
|
||||
:ignore_module_conflict,
|
||||
:relative_paths,
|
||||
:warnings_as_errors
|
||||
:infer_signatures,
|
||||
:relative_paths
|
||||
]
|
||||
|
||||
@list_compiler_options [:no_warn_undefined, :tracers, :parser_options]
|
||||
@@ -507,14 +512,9 @@ defmodule Code do
|
||||
|
||||
## Options
|
||||
|
||||
Options can be:
|
||||
|
||||
* `:file` - the file to be considered in the evaluation
|
||||
|
||||
* `:line` - the line on which the script starts
|
||||
|
||||
Additionally, you may also pass an environment as second argument,
|
||||
so the evaluation happens within that environment.
|
||||
It accepts the same options as `env_for_eval/1`. Additionally, you may
|
||||
also pass an environment as second argument, so the evaluation happens
|
||||
within that environment.
|
||||
|
||||
Returns a tuple of the form `{value, binding}`, where `value` is the value
|
||||
returned from evaluating `string`. If an error occurs while evaluating
|
||||
@@ -659,6 +659,8 @@ defmodule Code do
|
||||
|
||||
## Options
|
||||
|
||||
Regular options (do not change the AST):
|
||||
|
||||
* `:file` - the file which contains the string, used for error
|
||||
reporting
|
||||
|
||||
@@ -682,28 +684,34 @@ defmodule Code do
|
||||
If you set it to `false` later on, `do`-`end` blocks won't be
|
||||
converted back to keywords.
|
||||
|
||||
* `:normalize_bitstring_modifiers` (since v1.14.0) - when `true`,
|
||||
Migration options (change the AST), see the "Migration formatting" section below:
|
||||
|
||||
* `:migrate` (since v1.18.0) - when `true`, sets all other migration options
|
||||
to `true` by default. Defaults to `false`.
|
||||
|
||||
* `:migrate_bitstring_modifiers` (since v1.18.0) - when `true`,
|
||||
removes unnecessary parentheses in known bitstring
|
||||
[modifiers](`<<>>/1`), for example `<<foo::binary()>>`
|
||||
becomes `<<foo::binary>>`, or adds parentheses for custom
|
||||
modifiers, where `<<foo::custom_type>>` becomes `<<foo::custom_type()>>`.
|
||||
Defaults to `true`. This option changes the AST.
|
||||
Defaults to the value of the `:migrate` option. This option changes the AST.
|
||||
|
||||
* `:normalize_charlists_as_sigils` (since v1.15.0) - when `true`,
|
||||
* `:migrate_charlists_as_sigils` (since v1.18.0) - when `true`,
|
||||
formats charlists as [`~c`](`Kernel.sigil_c/2`) sigils, for example
|
||||
`'foo'` becomes `~c"foo"`.
|
||||
Defaults to `true`. This option changes the AST.
|
||||
Defaults to the value of the `:migrate` option. This option changes the AST.
|
||||
|
||||
* `:migrate_unless` (since v1.18.0) - when `true`,
|
||||
rewrites `unless` expressions using `if` with a negated condition, for example
|
||||
`unless foo, do:` becomes `if !foo, do:`.
|
||||
Defaults to the value of the `:migrate` option. This option changes the AST.
|
||||
|
||||
## Design principles
|
||||
|
||||
The formatter was designed under three principles.
|
||||
|
||||
First, the formatter never changes the semantics of the code.
|
||||
First, the formatter never changes the semantics of the code by default.
|
||||
This means the input AST and the output AST are almost always equivalent.
|
||||
The only cases where the formatter will change the AST is when the input AST
|
||||
would cause *compiler warnings* and the output AST won't. The cases where
|
||||
the formatter changes the AST can be disabled through formatting options
|
||||
if desired.
|
||||
|
||||
The second principle is to provide as little configuration as possible.
|
||||
This eases the formatter adoption by removing contention points while
|
||||
@@ -937,9 +945,8 @@ defmodule Code do
|
||||
|
||||
## Code comments
|
||||
|
||||
The formatter also handles code comments in a way to guarantee a space
|
||||
is always added between the beginning of the comment (#) and the next
|
||||
character.
|
||||
The formatter handles code comments and guarantees a space is always added
|
||||
between the beginning of the comment (#) and the next character.
|
||||
|
||||
The formatter also extracts all trailing comments to their previous line.
|
||||
For example, the code below
|
||||
@@ -951,9 +958,19 @@ defmodule Code do
|
||||
# world
|
||||
hello
|
||||
|
||||
Because code comments are handled apart from the code representation (AST),
|
||||
there are some situations where code comments are seen as ambiguous by the
|
||||
code formatter. For example, the comment in the anonymous function below
|
||||
While the formatter attempts to preserve comments in most situations,
|
||||
that's not always possible, because code comments are handled apart from
|
||||
the code representation (AST). While the formatter can preserve code
|
||||
comments between expressions and function arguments, the formatter
|
||||
cannot currently preserve them around operators. For example, the following
|
||||
code will move the code comments to before the operator usage:
|
||||
|
||||
foo() ||
|
||||
# also check for bar
|
||||
bar()
|
||||
|
||||
In some situations, code comments can be seen as ambiguous by the formatter.
|
||||
For example, the comment in the anonymous function below
|
||||
|
||||
fn
|
||||
arg1 ->
|
||||
@@ -982,6 +999,21 @@ defmodule Code do
|
||||
## Newlines
|
||||
|
||||
The formatter converts all newlines in code from `\r\n` to `\n`.
|
||||
|
||||
## Migration formatting
|
||||
|
||||
As part of the Elixir release cycle, deprecations are being introduced,
|
||||
emitting warnings which might require existing code to be changed.
|
||||
In order to reduce the burden on developers when upgrading Elixir to the
|
||||
next version, the formatter exposes some options, disabled by default,
|
||||
in order to automate this process.
|
||||
|
||||
These options should address most of the typical use cases, but given they
|
||||
introduce changes to the AST, there is a non-zero risk for meta-programming
|
||||
heavy projects that relied on a specific AST, or projects that are
|
||||
re-defining functions from the `Kernel`. In such cases, migrations cannot
|
||||
be applied blindly and some extra changes might be needed in order to
|
||||
address the deprecation warnings.
|
||||
"""
|
||||
@doc since: "1.6.0"
|
||||
@spec format_string!(binary, keyword) :: iodata
|
||||
@@ -1075,6 +1107,8 @@ defmodule Code do
|
||||
* `:file` - the file to be considered in the evaluation
|
||||
|
||||
* `:line` - the line on which the script starts
|
||||
|
||||
* `:module` - the module to run the environment on
|
||||
"""
|
||||
@doc since: "1.14.0"
|
||||
def env_for_eval(env_or_opts), do: :elixir.env_for_eval(env_or_opts)
|
||||
@@ -1528,8 +1562,8 @@ defmodule Code do
|
||||
|
||||
## Examples
|
||||
|
||||
Code.compiler_options(warnings_as_errors: true)
|
||||
#=> %{warnings_as_errors: false}
|
||||
Code.compiler_options(infer_signatures: false)
|
||||
#=> %{infer_signatures: true}
|
||||
|
||||
"""
|
||||
@spec compiler_options(Enumerable.t({atom, term})) :: %{optional(atom) => term}
|
||||
@@ -1558,6 +1592,12 @@ defmodule Code do
|
||||
:elixir_config.get(key)
|
||||
end
|
||||
|
||||
# TODO: Remove me in Elixir v2.0
|
||||
def get_compiler_option(:warnings_as_errors) do
|
||||
IO.warn(":warnings_as_errors is deprecated as part of Code.get_compiler_option/1")
|
||||
:ok
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a list with all available compiler options.
|
||||
|
||||
@@ -1586,16 +1626,19 @@ defmodule Code do
|
||||
Defaults to `true`.
|
||||
|
||||
* `:debug_info` - when `true`, retains debug information in the compiled
|
||||
module. Defaults to `true`.
|
||||
This enables static analysis tools as it allows developers to
|
||||
partially reconstruct the original source code. Therefore, disabling
|
||||
module. This option can also be overridden per module using the `@compile`
|
||||
directive. Defaults to `true`.
|
||||
|
||||
This enables tooling to partially reconstruct the original source code,
|
||||
for instance, to perform static analysis of code. Therefore, disabling
|
||||
`:debug_info` is not recommended as it removes the ability of the
|
||||
Elixir compiler and other tools to provide feedback. If you want to
|
||||
remove the `:debug_info` while deploying, tools like `mix release`
|
||||
already do such by default.
|
||||
Additionally, `mix test` disables it via the `:test_elixirc_options`
|
||||
project configuration option.
|
||||
This option can also be overridden per module using the `@compile` directive.
|
||||
|
||||
Other environments, such as `mix test`, automatically disables this
|
||||
via the `:test_elixirc_options` project configuration, as there is
|
||||
typically no need to store debug chunks for test files.
|
||||
|
||||
* `:ignore_already_consolidated` (since v1.10.0) - when `true`, does not warn
|
||||
when a protocol has already been consolidated and a new implementation is added.
|
||||
@@ -1604,13 +1647,19 @@ defmodule Code do
|
||||
* `:ignore_module_conflict` - when `true`, does not warn when a module has
|
||||
already been defined. Defaults to `false`.
|
||||
|
||||
* `:infer_signatures` (since v1.18.0) - when `false`, it disables module-local
|
||||
signature inference used when type checking remote calls to the compiled
|
||||
module. Type checking will be executed regardless of the value of this option.
|
||||
Defaults to `true`.
|
||||
|
||||
`mix test` automatically disables this option via the `:test_elixirc_options`
|
||||
project configuration, as there is typically no need to store infer signatures
|
||||
for test files.
|
||||
|
||||
* `:relative_paths` - when `true`, uses relative paths in quoted nodes,
|
||||
warnings, and errors generated by the compiler. Note disabling this option
|
||||
won't affect runtime warnings and errors. Defaults to `true`.
|
||||
|
||||
* `:warnings_as_errors` - causes compilation to fail when warnings are
|
||||
generated. Defaults to `false`.
|
||||
|
||||
* `:no_warn_undefined` (since v1.10.0) - list of modules and `{Mod, fun, arity}`
|
||||
tuples that will not emit warnings that the module or function does not exist
|
||||
at compilation time. Pass atom `:all` to skip warning for all undefined
|
||||
@@ -1656,6 +1705,18 @@ defmodule Code do
|
||||
:ok
|
||||
end
|
||||
|
||||
# TODO: Remove me in Elixir v2.0
|
||||
def put_compiler_option(:warnings_as_errors, _value) do
|
||||
IO.warn(
|
||||
":warnings_as_errors is deprecated as part of Code.put_compiler_option/2, " <>
|
||||
"instead you must pass it as a --warnings-as-errors flag. " <>
|
||||
"If you need to set it as a default in a mix task, you can also set it under aliases: " <>
|
||||
"[compile: \"compile --warnings-as-errors\"]"
|
||||
)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
def put_compiler_option(:no_warn_undefined, value) do
|
||||
if value != :all and not is_list(value) do
|
||||
raise "compiler option :no_warn_undefined should be a list or the atom :all, " <>
|
||||
@@ -1685,7 +1746,7 @@ defmodule Code do
|
||||
:ok
|
||||
end
|
||||
|
||||
# TODO: Make this option have no effect on Elixir v2.0
|
||||
# TODO: Remove this option on Elixir v2.0
|
||||
# TODO: Warn if mode is :warn on Elixir v1.19
|
||||
def put_compiler_option(:on_undefined_variable, value) when value in [:raise, :warn] do
|
||||
:elixir_config.put(:on_undefined_variable, value)
|
||||
|
||||
@@ -16,7 +16,7 @@ defmodule Code.Formatter do
|
||||
@ampersand_prec Code.Identifier.unary_op(:&) |> elem(1)
|
||||
|
||||
# Operators that are composed of multiple binary operators
|
||||
@multi_binary_operators [:"..//"]
|
||||
@multi_binary_operators [:..//]
|
||||
|
||||
# Operators that do not have space between operands
|
||||
@no_space_binary_operators [:.., :"//"]
|
||||
@@ -31,30 +31,37 @@ defmodule Code.Formatter do
|
||||
@right_new_line_before_binary_operators [:|, :when]
|
||||
|
||||
# Operators that are logical cannot be mixed without parens
|
||||
@required_parens_logical_binary_operands [:||, :|||, :or, :&&, :&&&, :and]
|
||||
@required_parens_logical_binary_operands [:|||, :||, :or, :&&&, :&&, :and]
|
||||
|
||||
# Operators with next break fits
|
||||
@next_break_fits_operators [:<-, :==, :!=, :=~, :===, :!==, :<, :>, :<=, :>=, :=, :"::"]
|
||||
|
||||
# Operators that always require parens on operands when they are the parent
|
||||
# Operators that always require parens even
|
||||
# when they are their own parents as they are not semantically associative
|
||||
@required_parens_even_when_parent [:--, :---]
|
||||
|
||||
# Operators that always require parens on operands
|
||||
# when they are the parent of another operator with a difference precedence
|
||||
# Most operators are listed, except comparison, arithmetic, and low precedence
|
||||
@required_parens_on_binary_operands [
|
||||
:|>,
|
||||
:<<<,
|
||||
:>>>,
|
||||
:|>,
|
||||
:<~,
|
||||
:~>,
|
||||
:<<~,
|
||||
:~>>,
|
||||
:<~>,
|
||||
:"<|>",
|
||||
:"^^^",
|
||||
:+++,
|
||||
:---,
|
||||
:in,
|
||||
:"^^^",
|
||||
:"//",
|
||||
:++,
|
||||
:--,
|
||||
:..,
|
||||
:<>
|
||||
:+++,
|
||||
:---,
|
||||
:<>,
|
||||
:..
|
||||
]
|
||||
|
||||
@locals_without_parens [
|
||||
@@ -182,8 +189,10 @@ defmodule Code.Formatter do
|
||||
locals_without_parens = Keyword.get(opts, :locals_without_parens, [])
|
||||
file = Keyword.get(opts, :file, nil)
|
||||
sigils = Keyword.get(opts, :sigils, [])
|
||||
normalize_bitstring_modifiers = Keyword.get(opts, :normalize_bitstring_modifiers, true)
|
||||
normalize_charlists_as_sigils = Keyword.get(opts, :normalize_charlists_as_sigils, true)
|
||||
migrate = Keyword.get(opts, :migrate, false)
|
||||
migrate_bitstring_modifiers = Keyword.get(opts, :migrate_bitstring_modifiers, migrate)
|
||||
migrate_charlists_as_sigils = Keyword.get(opts, :migrate_charlists_as_sigils, migrate)
|
||||
migrate_unless = Keyword.get(opts, :migrate_unless, migrate)
|
||||
syntax_colors = Keyword.get(opts, :syntax_colors, [])
|
||||
|
||||
sigils =
|
||||
@@ -208,8 +217,9 @@ defmodule Code.Formatter do
|
||||
comments: comments,
|
||||
sigils: sigils,
|
||||
file: file,
|
||||
normalize_bitstring_modifiers: normalize_bitstring_modifiers,
|
||||
normalize_charlists_as_sigils: normalize_charlists_as_sigils,
|
||||
migrate_bitstring_modifiers: migrate_bitstring_modifiers,
|
||||
migrate_charlists_as_sigils: migrate_charlists_as_sigils,
|
||||
migrate_unless: migrate_unless,
|
||||
inspect_opts: %Inspect.Opts{syntax_colors: syntax_colors}
|
||||
}
|
||||
end
|
||||
@@ -266,7 +276,7 @@ defmodule Code.Formatter do
|
||||
end
|
||||
|
||||
defp quoted_to_algebra({var, _meta, var_context}, _context, state) when is_atom(var_context) do
|
||||
{var |> Atom.to_string() |> string() |> color(:variable, state.inspect_opts), state}
|
||||
{var |> Atom.to_string() |> string() |> color_doc(:variable, state.inspect_opts), state}
|
||||
end
|
||||
|
||||
defp quoted_to_algebra({:<<>>, meta, entries}, _context, state) do
|
||||
@@ -397,7 +407,7 @@ defmodule Code.Formatter do
|
||||
{@double_heredoc
|
||||
|> concat(string)
|
||||
|> concat(@double_heredoc)
|
||||
|> color(:string, state.inspect_opts)
|
||||
|> color_doc(:string, state.inspect_opts)
|
||||
|> force_unfit(), state}
|
||||
else
|
||||
string = escape_string(string, @double_quote)
|
||||
@@ -405,7 +415,7 @@ defmodule Code.Formatter do
|
||||
{@double_quote
|
||||
|> concat(string)
|
||||
|> concat(@double_quote)
|
||||
|> color(:string, state.inspect_opts), state}
|
||||
|> color_doc(:string, state.inspect_opts), state}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -437,7 +447,7 @@ defmodule Code.Formatter do
|
||||
end
|
||||
|
||||
defp quoted_to_algebra({:__block__, _meta, []}, _context, state) do
|
||||
{color("nil", nil, state.inspect_opts), state}
|
||||
{color_doc("nil", nil, state.inspect_opts), state}
|
||||
end
|
||||
|
||||
defp quoted_to_algebra({:__block__, meta, args} = block, _context, state) when is_list(args) do
|
||||
@@ -454,7 +464,7 @@ defmodule Code.Formatter do
|
||||
end
|
||||
|
||||
{Enum.reduce(tail, doc, &concat(&2, "." <> Atom.to_string(&1)))
|
||||
|> color(:atom, state.inspect_opts), state}
|
||||
|> color_doc(:atom, state.inspect_opts), state}
|
||||
end
|
||||
|
||||
# &1
|
||||
@@ -477,6 +487,53 @@ defmodule Code.Formatter do
|
||||
binary_op_to_algebra(:in, "not in", meta, left, right, context, state)
|
||||
end
|
||||
|
||||
# disable migrate_unless within defmacro
|
||||
defp quoted_to_algebra(
|
||||
{atom, _, [{:unless, _, _}, _]} = ast,
|
||||
context,
|
||||
%{migrate_unless: true} = state
|
||||
)
|
||||
when atom in [:defmacro, :defmacrop] do
|
||||
quoted_to_algebra(ast, context, %{state | migrate_unless: false})
|
||||
end
|
||||
|
||||
# rewrite unless as if!
|
||||
defp quoted_to_algebra(
|
||||
{:unless, meta, [condition, block]},
|
||||
context,
|
||||
%{migrate_unless: true} = state
|
||||
) do
|
||||
quoted_to_algebra({:if, meta, [negate_condition(condition), block]}, context, state)
|
||||
end
|
||||
|
||||
# a |> b() |> unless(...) => a |> b() |> Kernel.!() |> unless(...)
|
||||
defp quoted_to_algebra(
|
||||
{:|>, meta1, [{:|>, _, _} = condition, {:unless, meta2, [block]}]},
|
||||
context,
|
||||
%{migrate_unless: true} = state
|
||||
) do
|
||||
negated_condition = {:|>, [], [condition, {{:., [], [Kernel, :!]}, [closing: []], []}]}
|
||||
|
||||
quoted_to_algebra(
|
||||
{:|>, meta1, [negated_condition, {:if, meta2, [block]}]},
|
||||
context,
|
||||
state
|
||||
)
|
||||
end
|
||||
|
||||
# condition |> unless(...) => negated(condition) |> unless(...)
|
||||
defp quoted_to_algebra(
|
||||
{:|>, meta1, [condition, {:unless, meta2, [block]}]},
|
||||
context,
|
||||
%{migrate_unless: true} = state
|
||||
) do
|
||||
quoted_to_algebra(
|
||||
{:|>, meta1, [negate_condition(condition), {:if, meta2, [block]}]},
|
||||
context,
|
||||
state
|
||||
)
|
||||
end
|
||||
|
||||
# ..
|
||||
defp quoted_to_algebra({:.., _meta, []}, context, state) do
|
||||
if context in [:no_parens_arg, :no_parens_one_arg] do
|
||||
@@ -492,7 +549,7 @@ defmodule Code.Formatter do
|
||||
end
|
||||
|
||||
# 1..2//3
|
||||
defp quoted_to_algebra({:"..//", meta, [left, middle, right]}, context, state) do
|
||||
defp quoted_to_algebra({:..//, meta, [left, middle, right]}, context, state) do
|
||||
quoted_to_algebra({:"//", meta, [{:.., meta, [left, middle]}, right]}, context, state)
|
||||
end
|
||||
|
||||
@@ -524,20 +581,12 @@ defmodule Code.Formatter do
|
||||
if keyword_key?(left_arg) do
|
||||
{left, state} =
|
||||
case left_arg do
|
||||
# TODO: Remove this clause in v1.18 when we no longer quote operator :..//
|
||||
{:__block__, _, [:"..//"]} ->
|
||||
{string(~S{"..//":}), state}
|
||||
|
||||
{:__block__, _, [atom]} when is_atom(atom) ->
|
||||
iodata =
|
||||
if Macro.classify_atom(atom) in [:identifier, :unquoted] do
|
||||
[Atom.to_string(atom), ?:]
|
||||
else
|
||||
[?", atom |> Atom.to_string() |> String.replace("\"", "\\\""), ?", ?:]
|
||||
end
|
||||
formatted = Macro.inspect_atom(:key, atom, escape: &escape_atom/2)
|
||||
|
||||
{iodata |> IO.iodata_to_binary() |> string() |> color(:atom, state.inspect_opts),
|
||||
state}
|
||||
{formatted
|
||||
|> string()
|
||||
|> color_doc(:atom, state.inspect_opts), state}
|
||||
|
||||
{{:., _, [:erlang, :binary_to_atom]}, _, [{:<<>>, _, entries}, :utf8]} ->
|
||||
interpolation_to_algebra(entries, @double_quote, state, "\"", "\":")
|
||||
@@ -561,8 +610,11 @@ defmodule Code.Formatter do
|
||||
end
|
||||
|
||||
# #PID's and #Ref's may appear on regular AST
|
||||
# Other foreign structures, such as maps and structs,
|
||||
# may appear from Macro.to_string, so we stick a limit,
|
||||
# although they won't be formatted accordingly.
|
||||
defp quoted_to_algebra(unknown, _context, state) do
|
||||
{inspect(unknown), state}
|
||||
{inspect(unknown, printable_limit: :infinity), state}
|
||||
end
|
||||
|
||||
## Blocks
|
||||
@@ -632,7 +684,7 @@ defmodule Code.Formatter do
|
||||
Atom.to_string(op)
|
||||
end
|
||||
|
||||
{color(op_string, :operator, state.inspect_opts) |> concat(doc), state}
|
||||
{color_doc(op_string, :operator, state.inspect_opts) |> concat(doc), state}
|
||||
end
|
||||
|
||||
defp maybe_binary_op_to_algebra(fun, meta, args, context, state) do
|
||||
@@ -738,11 +790,11 @@ defmodule Code.Formatter do
|
||||
doc =
|
||||
cond do
|
||||
op in @no_space_binary_operators ->
|
||||
op_doc = color(op_string, :operator, state.inspect_opts)
|
||||
op_doc = color_doc(op_string, :operator, state.inspect_opts)
|
||||
concat(concat(group(left), op_doc), group(right))
|
||||
|
||||
op in @no_newline_binary_operators ->
|
||||
op_doc = color(" " <> op_string <> " ", :operator, state.inspect_opts)
|
||||
op_doc = color_doc(" " <> op_string <> " ", :operator, state.inspect_opts)
|
||||
concat(concat(group(left), op_doc), group(right))
|
||||
|
||||
true ->
|
||||
@@ -752,7 +804,7 @@ defmodule Code.Formatter do
|
||||
op in @next_break_fits_operators and next_break_fits?(right_arg, state) and not eol?
|
||||
|
||||
with_next_break_fits(next_break_fits?, right, fn right ->
|
||||
op_doc = color(" " <> op_string, :operator, state.inspect_opts)
|
||||
op_doc = color_doc(" " <> op_string, :operator, state.inspect_opts)
|
||||
right = nest(glue(op_doc, group(right)), nesting, :break)
|
||||
right = if eol?, do: force_unfit(right), else: right
|
||||
concat(group(left), group(right))
|
||||
@@ -789,14 +841,13 @@ defmodule Code.Formatter do
|
||||
op_string = Atom.to_string(op)
|
||||
|
||||
cond do
|
||||
# If the operator has the same precedence as the parent and is on
|
||||
# the correct side, we respect the nesting rule to avoid multiple
|
||||
# nestings. This only applies for left associativity or same operator.
|
||||
parent_prec == prec and parent_assoc == side and (side == :left or op == parent_op) ->
|
||||
# If we have the same operator and it is in the correct side,
|
||||
# we don't add parens unless it is explicitly required.
|
||||
parent_assoc == side and op == parent_op and op not in @required_parens_even_when_parent ->
|
||||
binary_op_to_algebra(op, op_string, meta, left, right, context, state, nesting)
|
||||
|
||||
# If the parent requires parens or the precedence is inverted or
|
||||
# it is in the wrong side, then we *need* parenthesis.
|
||||
# If the operator requires parens (most of them do) or we are mixing logical operators
|
||||
# or the precedence is inverted or it is in the wrong side, then we *need* parenthesis.
|
||||
(parent_op in @required_parens_on_binary_operands and op not in @no_space_binary_operators) or
|
||||
(op in @required_parens_logical_binary_operands and
|
||||
parent_op in @required_parens_logical_binary_operands) or parent_prec > prec or
|
||||
@@ -953,7 +1004,7 @@ defmodule Code.Formatter do
|
||||
)
|
||||
when is_atom(fun) and is_integer(arity) do
|
||||
{target_doc, state} = remote_target_to_algebra(target, state)
|
||||
fun = Macro.inspect_atom(:remote_call, fun)
|
||||
fun = Macro.inspect_atom(:remote_call, fun, escape: &escape_atom/2)
|
||||
{target_doc |> nest(1) |> concat(string(".#{fun}/#{arity}")), state}
|
||||
end
|
||||
|
||||
@@ -1000,7 +1051,9 @@ defmodule Code.Formatter do
|
||||
{target_doc, state} = remote_target_to_algebra(target, state)
|
||||
|
||||
fun_doc =
|
||||
Macro.inspect_atom(:remote_call, fun) |> string() |> color(:call, state.inspect_opts)
|
||||
Macro.inspect_atom(:remote_call, fun, escape: &escape_atom/2)
|
||||
|> string()
|
||||
|> color_doc(:call, state.inspect_opts)
|
||||
|
||||
remote_doc = target_doc |> concat(".") |> concat(fun_doc)
|
||||
|
||||
@@ -1068,7 +1121,7 @@ defmodule Code.Formatter do
|
||||
fun
|
||||
|> Atom.to_string()
|
||||
|> string()
|
||||
|> color(:call, state.inspect_opts)
|
||||
|> color_doc(:call, state.inspect_opts)
|
||||
|> concat(call_doc)
|
||||
|
||||
doc = if wrap_in_parens?, do: wrap_in_parens(doc), else: doc
|
||||
@@ -1431,7 +1484,7 @@ defmodule Code.Formatter do
|
||||
{doc, state} = quoted_to_algebra(segment, :parens_arg, state)
|
||||
|
||||
{spec, state} =
|
||||
bitstring_spec_to_algebra(spec, state, state.normalize_bitstring_modifiers, :"::")
|
||||
bitstring_spec_to_algebra(spec, state, state.migrate_bitstring_modifiers, :"::")
|
||||
|
||||
spec = wrap_in_parens_if_inspected_atom(spec)
|
||||
spec = if i == last, do: bitstring_wrap_parens(spec, i, last), else: spec
|
||||
@@ -1509,8 +1562,8 @@ defmodule Code.Formatter do
|
||||
{args_doc, _join, state} =
|
||||
args_to_algebra_with_comments(args, meta, false, :none, join, state, fun)
|
||||
|
||||
left_bracket = color("[", :list, state.inspect_opts)
|
||||
right_bracket = color("]", :list, state.inspect_opts)
|
||||
left_bracket = color_doc("[", :list, state.inspect_opts)
|
||||
right_bracket = color_doc("]", :list, state.inspect_opts)
|
||||
|
||||
{surround(left_bracket, args_doc, right_bracket), state}
|
||||
end
|
||||
@@ -1542,8 +1595,8 @@ defmodule Code.Formatter do
|
||||
end
|
||||
|
||||
defp do_map_to_algebra(name_doc, args_doc, state) do
|
||||
name_doc = "%" |> concat(name_doc) |> concat("{") |> color(:map, state.inspect_opts)
|
||||
{surround(name_doc, args_doc, color("}", :map, state.inspect_opts)), state}
|
||||
name_doc = "%" |> concat(name_doc) |> concat("{") |> color_doc(:map, state.inspect_opts)
|
||||
{surround(name_doc, args_doc, color_doc("}", :map, state.inspect_opts)), state}
|
||||
end
|
||||
|
||||
defp tuple_to_algebra(meta, args, join, state) do
|
||||
@@ -1553,8 +1606,8 @@ defmodule Code.Formatter do
|
||||
{args_doc, join, state} =
|
||||
args_to_algebra_with_comments(args, meta, false, :none, join, state, fun)
|
||||
|
||||
left_bracket = color("{", :tuple, state.inspect_opts)
|
||||
right_bracket = color("}", :tuple, state.inspect_opts)
|
||||
left_bracket = color_doc("{", :tuple, state.inspect_opts)
|
||||
right_bracket = color_doc("}", :tuple, state.inspect_opts)
|
||||
|
||||
if join == :flex_break do
|
||||
{left_bracket |> concat(args_doc) |> nest(1) |> concat(right_bracket) |> group(), state}
|
||||
@@ -1564,16 +1617,11 @@ defmodule Code.Formatter do
|
||||
end
|
||||
|
||||
defp atom_to_algebra(atom, _, inspect_opts) when atom in [true, false] do
|
||||
Atom.to_string(atom) |> color(:boolean, inspect_opts)
|
||||
Atom.to_string(atom) |> color_doc(:boolean, inspect_opts)
|
||||
end
|
||||
|
||||
defp atom_to_algebra(nil, _, inspect_opts) do
|
||||
Atom.to_string(nil) |> color(nil, inspect_opts)
|
||||
end
|
||||
|
||||
# TODO: Remove this clause in v1.18 when we no longer quote operator :..//
|
||||
defp atom_to_algebra(:"..//", _, inspect_opts) do
|
||||
string(":\"..//\"") |> color(:atom, inspect_opts)
|
||||
Atom.to_string(nil) |> color_doc(nil, inspect_opts)
|
||||
end
|
||||
|
||||
defp atom_to_algebra(:\\, meta, inspect_opts) do
|
||||
@@ -1586,7 +1634,7 @@ defmodule Code.Formatter do
|
||||
_ -> ":\\\\"
|
||||
end
|
||||
|
||||
string(string) |> color(:atom, inspect_opts)
|
||||
string(string) |> color_doc(:atom, inspect_opts)
|
||||
end
|
||||
|
||||
defp atom_to_algebra(atom, _, inspect_opts) do
|
||||
@@ -1599,7 +1647,7 @@ defmodule Code.Formatter do
|
||||
[?:, ?", String.replace(string, "\"", "\\\""), ?"]
|
||||
end
|
||||
|
||||
iodata |> IO.iodata_to_binary() |> string() |> color(:atom, inspect_opts)
|
||||
iodata |> IO.iodata_to_binary() |> string() |> color_doc(:atom, inspect_opts)
|
||||
end
|
||||
|
||||
defp integer_to_algebra(text, inspect_otps) do
|
||||
@@ -1616,7 +1664,7 @@ defmodule Code.Formatter do
|
||||
decimal ->
|
||||
insert_underscores(decimal)
|
||||
end
|
||||
|> color(:number, inspect_otps)
|
||||
|> color_doc(:number, inspect_otps)
|
||||
end
|
||||
|
||||
defp float_to_algebra(text, inspect_otps) do
|
||||
@@ -1624,7 +1672,7 @@ defmodule Code.Formatter do
|
||||
decimal_part = String.downcase(decimal_part)
|
||||
|
||||
string = insert_underscores(int_part) <> "." <> decimal_part
|
||||
color(string, :number, inspect_otps)
|
||||
color_doc(string, :number, inspect_otps)
|
||||
end
|
||||
|
||||
defp insert_underscores("-" <> digits) do
|
||||
@@ -2389,6 +2437,10 @@ defmodule Code.Formatter do
|
||||
meta[:closing][:line] || @min_line
|
||||
end
|
||||
|
||||
defp escape_atom(string, char) do
|
||||
String.replace(string, <<char>>, <<?\\, char>>)
|
||||
end
|
||||
|
||||
## Algebra helpers
|
||||
|
||||
# Relying on the inner document is brittle and error prone.
|
||||
@@ -2434,7 +2486,7 @@ defmodule Code.Formatter do
|
||||
end
|
||||
|
||||
defp get_charlist_quotes(:heredoc, state) do
|
||||
if state.normalize_charlists_as_sigils do
|
||||
if state.migrate_charlists_as_sigils do
|
||||
{@sigil_c_heredoc, @double_heredoc}
|
||||
else
|
||||
{@single_heredoc, @single_heredoc}
|
||||
@@ -2443,7 +2495,7 @@ defmodule Code.Formatter do
|
||||
|
||||
defp get_charlist_quotes({:regular, chunks}, state) do
|
||||
cond do
|
||||
!state.normalize_charlists_as_sigils -> {@single_quote, @single_quote}
|
||||
!state.migrate_charlists_as_sigils -> {@single_quote, @single_quote}
|
||||
Enum.any?(chunks, &has_double_quote?/1) -> {@sigil_c_single, @single_quote}
|
||||
true -> {@sigil_c_double, @double_quote}
|
||||
end
|
||||
@@ -2452,4 +2504,46 @@ defmodule Code.Formatter do
|
||||
defp has_double_quote?(chunk) do
|
||||
is_binary(chunk) and chunk =~ @double_quote
|
||||
end
|
||||
|
||||
# Migration rewrites
|
||||
|
||||
@bool_operators [
|
||||
:>,
|
||||
:>=,
|
||||
:<,
|
||||
:<=,
|
||||
:in
|
||||
]
|
||||
@guards [
|
||||
:is_atom,
|
||||
:is_boolean,
|
||||
:is_nil,
|
||||
:is_number,
|
||||
:is_integer,
|
||||
:is_float,
|
||||
:is_binary,
|
||||
:is_map,
|
||||
:is_struct,
|
||||
:is_non_struct_map,
|
||||
:is_exception,
|
||||
:is_list,
|
||||
:is_tuple,
|
||||
:is_function,
|
||||
:is_reference,
|
||||
:is_pid,
|
||||
:is_port
|
||||
]
|
||||
|
||||
defp negate_condition(condition) do
|
||||
case condition do
|
||||
{neg, _, [condition]} when neg in [:!, :not] -> condition
|
||||
{op, _, [_, _]} when op in @bool_operators -> {:not, [], [condition]}
|
||||
{guard, _, [_ | _]} when guard in @guards -> {:not, [], [condition]}
|
||||
{:==, meta, [left, right]} -> {:!=, meta, [left, right]}
|
||||
{:===, meta, [left, right]} -> {:!==, meta, [left, right]}
|
||||
{:!=, meta, [left, right]} -> {:==, meta, [left, right]}
|
||||
{:!==, meta, [left, right]} -> {:===, meta, [left, right]}
|
||||
_ -> {:!, [], [condition]}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
+144
-28
@@ -320,6 +320,9 @@ defmodule Code.Fragment do
|
||||
_ ->
|
||||
{{:local_or_var, acc}, count}
|
||||
end
|
||||
|
||||
{:capture_arg, acc, count} ->
|
||||
{{:capture_arg, acc}, count}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -362,6 +365,14 @@ defmodule Code.Fragment do
|
||||
:none
|
||||
end
|
||||
|
||||
defp rest_identifier([?& | tail] = rest, count, acc) when tail == [] or hd(tail) != ?& do
|
||||
if Enum.all?(acc, &(&1 in ?0..?9)) do
|
||||
{:capture_arg, [?& | acc], count + 1}
|
||||
else
|
||||
tokenize_identifier(rest, count, acc)
|
||||
end
|
||||
end
|
||||
|
||||
defp rest_identifier(rest, count, acc) do
|
||||
tokenize_identifier(rest, count, acc)
|
||||
end
|
||||
@@ -562,13 +573,13 @@ defmodule Code.Fragment do
|
||||
`local_call`/`local_arity` and `local_or_var`, since the latter can
|
||||
be a local or variable
|
||||
|
||||
* `@` when not followed by any identifier is returned as `{:operator, '@'}`
|
||||
(in contrast to `{:module_attribute, ''}` in `cursor_context/2`
|
||||
* `@` when not followed by any identifier is returned as `{:operator, ~c"@"}`
|
||||
(in contrast to `{:module_attribute, ~c""}` in `cursor_context/2`
|
||||
|
||||
* This function never returns empty sigils `{:sigil, ''}` or empty structs
|
||||
`{:struct, ''}` as context
|
||||
* This function never returns empty sigils `{:sigil, ~c""}` or empty structs
|
||||
`{:struct, ~c""}` as context
|
||||
|
||||
* This function returns keywords as `{:keyword, 'do'}`
|
||||
* This function returns keywords as `{:keyword, ~c"do"}`
|
||||
|
||||
* This function never returns `:expr`
|
||||
|
||||
@@ -590,7 +601,9 @@ defmodule Code.Fragment do
|
||||
| {:sigil, charlist}
|
||||
| {:struct, inside_struct}
|
||||
| {:unquoted_atom, charlist}
|
||||
| {:keyword, charlist},
|
||||
| {:keyword, charlist}
|
||||
| {:key, charlist}
|
||||
| {:capture_arg, charlist},
|
||||
inside_dot:
|
||||
{:alias, charlist}
|
||||
| {:alias, inside_alias, charlist}
|
||||
@@ -640,7 +653,16 @@ defmodule Code.Fragment do
|
||||
maybe_operator(reversed_pre, post, line, opts)
|
||||
|
||||
{:identifier, reversed_post, rest} ->
|
||||
{rest, _} = strip_spaces(rest, 0)
|
||||
{keyword_key?, rest} =
|
||||
case rest do
|
||||
[?: | tail] when tail == [] or hd(tail) in @space ->
|
||||
{true, rest}
|
||||
|
||||
_ ->
|
||||
{rest, _} = strip_spaces(rest, 0)
|
||||
{false, rest}
|
||||
end
|
||||
|
||||
reversed = reversed_post ++ reversed_pre
|
||||
|
||||
case codepoint_cursor_context(reversed, opts) do
|
||||
@@ -656,6 +678,9 @@ defmodule Code.Fragment do
|
||||
{{:dot, _, [_ | _]} = dot, offset} ->
|
||||
build_surround(dot, reversed, line, offset)
|
||||
|
||||
{{:local_or_var, acc}, offset} when keyword_key? ->
|
||||
build_surround({:key, acc}, reversed, line, offset)
|
||||
|
||||
{{:local_or_var, acc}, offset} when hd(rest) == ?( ->
|
||||
build_surround({:local_call, acc}, reversed, line, offset)
|
||||
|
||||
@@ -683,6 +708,9 @@ defmodule Code.Fragment do
|
||||
{{:unquoted_atom, acc}, offset} ->
|
||||
build_surround({:unquoted_atom, acc}, reversed, line, offset)
|
||||
|
||||
{{:capture_arg, acc}, offset} ->
|
||||
build_surround({:capture_arg, acc}, reversed, line, offset)
|
||||
|
||||
_ ->
|
||||
maybe_operator(reversed_pre, post, line, opts)
|
||||
end
|
||||
@@ -715,6 +743,16 @@ defmodule Code.Fragment do
|
||||
reversed = reversed_post ++ reversed_pre
|
||||
|
||||
case codepoint_cursor_context(reversed, opts) do
|
||||
{{:operator, ~c"&"}, offset} when hd(rest) in ?0..?9 ->
|
||||
arg = Enum.take_while(rest, &(&1 in ?0..?9))
|
||||
|
||||
build_surround(
|
||||
{:capture_arg, ~c"&" ++ arg},
|
||||
:lists.reverse(arg, reversed),
|
||||
line,
|
||||
offset + length(arg)
|
||||
)
|
||||
|
||||
{{:operator, acc}, offset} ->
|
||||
build_surround({:operator, acc}, reversed, line, offset)
|
||||
|
||||
@@ -1061,6 +1099,13 @@ defmodule Code.Fragment do
|
||||
iex> Code.Fragment.container_cursor_to_quoted("foo +")
|
||||
{:ok, {:+, [line: 1], [{:foo, [line: 1], nil}, {:__cursor__, [line: 1], []}]}}
|
||||
|
||||
In order to parse the left-side of `->` properly, which appears both
|
||||
in anonymous functions and do-end blocks, the trailing fragment option
|
||||
must be given with the rest of the contents:
|
||||
|
||||
iex> Code.Fragment.container_cursor_to_quoted("fn x", trailing_fragment: " -> :ok end")
|
||||
{:ok, {:fn, [line: 1], [{:->, [line: 1], [[{:__cursor__, [line: 1], []}], :ok]}]}}
|
||||
|
||||
## Options
|
||||
|
||||
* `:file` - the filename to be reported in case of parsing errors.
|
||||
@@ -1083,46 +1128,117 @@ defmodule Code.Fragment do
|
||||
* `:literal_encoder` - a function to encode literals in the AST.
|
||||
See the documentation for `Code.string_to_quoted/2` for more information.
|
||||
|
||||
* `:trailing_fragment` (since v1.18.0) - the rest of the contents after
|
||||
the cursor. This is necessary to correctly complete anonymous functions
|
||||
and the left-hand side of `->`
|
||||
|
||||
"""
|
||||
@doc since: "1.13.0"
|
||||
@spec container_cursor_to_quoted(List.Chars.t(), keyword()) ::
|
||||
{:ok, Macro.t()} | {:error, {location :: keyword, binary | {binary, binary}, binary}}
|
||||
def container_cursor_to_quoted(fragment, opts \\ []) do
|
||||
{trailing_fragment, opts} = Keyword.pop(opts, :trailing_fragment)
|
||||
opts = Keyword.take(opts, [:columns, :token_metadata, :literal_encoder])
|
||||
opts = [cursor_completion: true, emit_warnings: false] ++ opts
|
||||
opts = [check_terminators: {:cursor, []}, emit_warnings: false] ++ opts
|
||||
|
||||
file = Keyword.get(opts, :file, "nofile")
|
||||
line = Keyword.get(opts, :line, 1)
|
||||
column = Keyword.get(opts, :column, 1)
|
||||
|
||||
case :elixir_tokenizer.tokenize(to_charlist(fragment), line, column, opts) do
|
||||
{:ok, line, column, _warnings, rev_tokens, rev_terminators}
|
||||
when trailing_fragment == nil ->
|
||||
{rev_tokens, rev_terminators} =
|
||||
with [close, open, {_, _, :__cursor__} = cursor | rev_tokens] <- rev_tokens,
|
||||
{_, [_ | after_fn]} <- Enum.split_while(rev_terminators, &(elem(&1, 0) != :fn)),
|
||||
true <- maybe_missing_stab?(rev_tokens, false),
|
||||
[_ | rev_tokens] <- Enum.drop_while(rev_tokens, &(elem(&1, 0) != :fn)) do
|
||||
{[close, open, cursor | rev_tokens], after_fn}
|
||||
else
|
||||
_ -> {rev_tokens, rev_terminators}
|
||||
end
|
||||
|
||||
tokens = reverse_tokens(line, column, rev_tokens, rev_terminators)
|
||||
:elixir.tokens_to_quoted(tokens, file, opts)
|
||||
|
||||
{:ok, line, column, _warnings, rev_tokens, rev_terminators} ->
|
||||
tokens = :lists.reverse(rev_tokens, rev_terminators)
|
||||
tokens =
|
||||
with {before_start, [_ | _] = after_start} <-
|
||||
Enum.split_while(rev_terminators, &(elem(&1, 0) not in [:do, :fn])),
|
||||
true <- maybe_missing_stab?(rev_tokens, true),
|
||||
opts =
|
||||
Keyword.put(opts, :check_terminators, {:cursor, before_start}),
|
||||
{:error, {meta, _, ~c"end"}, _rest, _warnings, trailing_rev_tokens} <-
|
||||
:elixir_tokenizer.tokenize(to_charlist(trailing_fragment), line, column, opts) do
|
||||
trailing_tokens =
|
||||
reverse_tokens(meta[:line], meta[:column], trailing_rev_tokens, after_start)
|
||||
|
||||
case :elixir.tokens_to_quoted(tokens, file, opts) do
|
||||
{:ok, ast} ->
|
||||
{:ok, ast}
|
||||
# If the cursor has its own line, then we do not trim new lines trailing tokens.
|
||||
# Otherwise we want to drop any newline so we drop the next tokens after eol.
|
||||
trailing_tokens =
|
||||
case rev_tokens do
|
||||
[_close, _open, {_, _, :__cursor__}, {:eol, _} | _] -> trailing_tokens
|
||||
_ -> Enum.drop_while(trailing_tokens, &match?({:eol, _}, &1))
|
||||
end
|
||||
|
||||
{:error, error} ->
|
||||
# In case parsing fails, we give it another shot but handling fn/do/else/catch/rescue/after.
|
||||
tokens =
|
||||
:lists.reverse(
|
||||
rev_tokens,
|
||||
[{:stab_op, {line, column, nil}, :->}, {nil, {line, column + 2, nil}}] ++
|
||||
Enum.map(rev_terminators, fn tuple ->
|
||||
{line, column, info} = elem(tuple, 1)
|
||||
put_elem(tuple, 1, {line, column + 5, info})
|
||||
end)
|
||||
)
|
||||
Enum.reverse(rev_tokens, drop_tokens(trailing_tokens, 0))
|
||||
else
|
||||
_ -> reverse_tokens(line, column, rev_tokens, rev_terminators)
|
||||
end
|
||||
|
||||
case :elixir.tokens_to_quoted(tokens, file, opts) do
|
||||
{:ok, ast} -> {:ok, ast}
|
||||
{:error, _} -> {:error, error}
|
||||
end
|
||||
end
|
||||
:elixir.tokens_to_quoted(tokens, file, opts)
|
||||
|
||||
{:error, info, _rest, _warnings, _so_far} ->
|
||||
{:error, :elixir.format_token_error(info)}
|
||||
end
|
||||
end
|
||||
|
||||
defp reverse_tokens(line, column, tokens, terminators) do
|
||||
{terminators, _} =
|
||||
Enum.map_reduce(terminators, column, fn {start, _, _}, column ->
|
||||
atom = :elixir_tokenizer.terminator(start)
|
||||
|
||||
{{atom, {line, column, nil}}, column + length(Atom.to_charlist(atom))}
|
||||
end)
|
||||
|
||||
Enum.reverse(tokens, terminators)
|
||||
end
|
||||
|
||||
# Otherwise we drop all tokens, trying to build a minimal AST
|
||||
# for cursor completion.
|
||||
defp drop_tokens([{:"}", _} | _] = tokens, 0), do: tokens
|
||||
defp drop_tokens([{:"]", _} | _] = tokens, 0), do: tokens
|
||||
defp drop_tokens([{:")", _} | _] = tokens, 0), do: tokens
|
||||
defp drop_tokens([{:">>", _} | _] = tokens, 0), do: tokens
|
||||
defp drop_tokens([{:end, _} | _] = tokens, 0), do: tokens
|
||||
defp drop_tokens([{:",", _} | _] = tokens, 0), do: tokens
|
||||
defp drop_tokens([{:";", _} | _] = tokens, 0), do: tokens
|
||||
defp drop_tokens([{:eol, _} | _] = tokens, 0), do: tokens
|
||||
defp drop_tokens([{:stab_op, _, :->} | _] = tokens, 0), do: tokens
|
||||
|
||||
defp drop_tokens([{:"}", _} | tokens], counter), do: drop_tokens(tokens, counter - 1)
|
||||
defp drop_tokens([{:"]", _} | tokens], counter), do: drop_tokens(tokens, counter - 1)
|
||||
defp drop_tokens([{:")", _} | tokens], counter), do: drop_tokens(tokens, counter - 1)
|
||||
defp drop_tokens([{:">>", _} | tokens], counter), do: drop_tokens(tokens, counter - 1)
|
||||
defp drop_tokens([{:end, _} | tokens], counter), do: drop_tokens(tokens, counter - 1)
|
||||
|
||||
defp drop_tokens([{:"{", _} | tokens], counter), do: drop_tokens(tokens, counter + 1)
|
||||
defp drop_tokens([{:"[", _} | tokens], counter), do: drop_tokens(tokens, counter + 1)
|
||||
defp drop_tokens([{:"(", _} | tokens], counter), do: drop_tokens(tokens, counter + 1)
|
||||
defp drop_tokens([{:"<<", _} | tokens], counter), do: drop_tokens(tokens, counter + 1)
|
||||
defp drop_tokens([{:fn, _} | tokens], counter), do: drop_tokens(tokens, counter + 1)
|
||||
defp drop_tokens([{:do, _} | tokens], counter), do: drop_tokens(tokens, counter + 1)
|
||||
|
||||
defp drop_tokens([_ | tokens], counter), do: drop_tokens(tokens, counter)
|
||||
defp drop_tokens([], 0), do: []
|
||||
|
||||
defp maybe_missing_stab?([{:after, _} | _], _stab_choice?), do: true
|
||||
defp maybe_missing_stab?([{:do, _} | _], _stab_choice?), do: true
|
||||
defp maybe_missing_stab?([{:fn, _} | _], _stab_choice?), do: true
|
||||
defp maybe_missing_stab?([{:else, _} | _], _stab_choice?), do: true
|
||||
defp maybe_missing_stab?([{:catch, _} | _], _stab_choice?), do: true
|
||||
defp maybe_missing_stab?([{:rescue, _} | _], _stab_choice?), do: true
|
||||
defp maybe_missing_stab?([{:stab_op, _, :->} | _], stab_choice?), do: stab_choice?
|
||||
defp maybe_missing_stab?([_ | tail], stab_choice?), do: maybe_missing_stab?(tail, stab_choice?)
|
||||
defp maybe_missing_stab?([], _stab_choice?), do: false
|
||||
end
|
||||
|
||||
@@ -71,11 +71,11 @@ defmodule Code.Identifier do
|
||||
Escapes the given identifier.
|
||||
"""
|
||||
@spec escape(binary(), char() | nil, :infinity | non_neg_integer, (char() -> iolist() | false)) ::
|
||||
{escaped :: iolist(), remaining :: binary()}
|
||||
{escaped :: binary(), remaining :: binary()}
|
||||
def escape(binary, char, limit \\ :infinity, fun \\ &escape_map/1)
|
||||
when ((char in 0..0x10FFFF or is_nil(char)) and limit == :infinity) or
|
||||
when (is_binary(binary) and ((char in 0..0x10FFFF or is_nil(char)) and limit == :infinity)) or
|
||||
(is_integer(limit) and limit >= 0) do
|
||||
escape(binary, char, limit, [], fun)
|
||||
escape(binary, char, limit, <<>>, fun)
|
||||
end
|
||||
|
||||
defp escape(<<_, _::binary>> = binary, _char, 0, acc, _fun) do
|
||||
@@ -83,64 +83,96 @@ defmodule Code.Identifier do
|
||||
end
|
||||
|
||||
defp escape(<<char, t::binary>>, char, count, acc, fun) do
|
||||
escape(t, char, decrement(count), [acc | [?\\, char]], fun)
|
||||
escape(t, char, decrement(count), <<acc::binary, ?\\, char>>, fun)
|
||||
end
|
||||
|
||||
defp escape(<<?#, ?{, t::binary>>, char, count, acc, fun) do
|
||||
escape(t, char, decrement(count), [acc | [?\\, ?#, ?{]], fun)
|
||||
escape(t, char, decrement(count), <<acc::binary, ?\\, ?#, ?{>>, fun)
|
||||
end
|
||||
|
||||
defp escape(<<h::utf8, t::binary>>, char, count, acc, fun) do
|
||||
escaped = if value = fun.(h), do: value, else: escape_char(h)
|
||||
escape(t, char, decrement(count), [acc | escaped], fun)
|
||||
if value = fun.(h) do
|
||||
value = IO.iodata_to_binary(value)
|
||||
escape(t, char, decrement(count), <<acc::binary, value::binary>>, fun)
|
||||
else
|
||||
escape(t, char, decrement(count), escape_char(h, acc), fun)
|
||||
end
|
||||
end
|
||||
|
||||
defp escape(<<a::4, b::4, t::binary>>, char, count, acc, fun) do
|
||||
escape(t, char, decrement(count), [acc | [?\\, ?x, to_hex(a), to_hex(b)]], fun)
|
||||
escape(t, char, decrement(count), <<acc::binary, ?\\, ?x, to_hex(a), to_hex(b)>>, fun)
|
||||
end
|
||||
|
||||
defp escape(<<>>, _char, _count, acc, _fun) do
|
||||
{acc, <<>>}
|
||||
end
|
||||
|
||||
defp escape_char(0), do: [?\\, ?0]
|
||||
defp escape_char(0, acc), do: <<acc::binary, ?\\, ?0>>
|
||||
|
||||
@escaped_bom :binary.bin_to_list("\\uFEFF")
|
||||
defp escape_char(65279), do: @escaped_bom
|
||||
defp escape_char(char, acc)
|
||||
# Some characters that are confusing (zero-width / alternative spaces) are displayed
|
||||
# using their unicode representation:
|
||||
# https://en.wikipedia.org/wiki/Universal_Character_Set_characters#Special-purpose_characters
|
||||
|
||||
defp escape_char(char)
|
||||
# BOM
|
||||
when char == 0xFEFF
|
||||
# Mathematical invisibles
|
||||
when char in 0x2061..0x2064
|
||||
# Bidirectional neutral
|
||||
when char in [0x061C, 0x200E, 0x200F]
|
||||
# Bidirectional general (source of vulnerabilities)
|
||||
when char in 0x202A..0x202E
|
||||
when char in 0x2066..0x2069
|
||||
# Interlinear annotations
|
||||
when char in 0xFFF9..0xFFFC
|
||||
# Zero-width joiners and non-joiners
|
||||
when char in [0x200C, 0x200D, 0x034F]
|
||||
# Non-break space / zero-width space
|
||||
when char in [0x00A0, 0x200B, 0x2060]
|
||||
# Line/paragraph separators
|
||||
when char in [0x2028, 0x2029]
|
||||
# Spaces
|
||||
when char in 0x2000..0x200A
|
||||
when char == 0x205F do
|
||||
<<a::4, b::4, c::4, d::4>> = <<char::16>>
|
||||
<<acc::binary, ?\\, ?u, to_hex(a), to_hex(b), to_hex(c), to_hex(d)>>
|
||||
end
|
||||
|
||||
defp escape_char(char, acc)
|
||||
when char in 0x20..0x7E
|
||||
when char in 0xA0..0xD7FF
|
||||
when char in 0xE000..0xFFFD
|
||||
when char in 0x10000..0x10FFFF do
|
||||
<<char::utf8>>
|
||||
<<acc::binary, char::utf8>>
|
||||
end
|
||||
|
||||
defp escape_char(char) when char < 0x100 do
|
||||
defp escape_char(char, acc) when char < 0x100 do
|
||||
<<a::4, b::4>> = <<char::8>>
|
||||
[?\\, ?x, to_hex(a), to_hex(b)]
|
||||
<<acc::binary, ?\\, ?x, to_hex(a), to_hex(b)>>
|
||||
end
|
||||
|
||||
defp escape_char(char) when char < 0x10000 do
|
||||
defp escape_char(char, acc) when char < 0x10000 do
|
||||
<<a::4, b::4, c::4, d::4>> = <<char::16>>
|
||||
[?\\, ?x, ?{, to_hex(a), to_hex(b), to_hex(c), to_hex(d), ?}]
|
||||
<<acc::binary, ?\\, ?x, ?{, to_hex(a), to_hex(b), to_hex(c), to_hex(d), ?}>>
|
||||
end
|
||||
|
||||
defp escape_char(char) when char < 0x1000000 do
|
||||
defp escape_char(char, acc) when char < 0x1000000 do
|
||||
<<a::4, b::4, c::4, d::4, e::4, f::4>> = <<char::24>>
|
||||
[?\\, ?x, ?{, to_hex(a), to_hex(b), to_hex(c), to_hex(d), to_hex(e), to_hex(f), ?}]
|
||||
|
||||
<<acc::binary, ?\\, ?x, ?{, to_hex(a), to_hex(b), to_hex(c), to_hex(d), to_hex(e), to_hex(f),
|
||||
?}>>
|
||||
end
|
||||
|
||||
defp escape_map(?\a), do: [?\\, ?a]
|
||||
defp escape_map(?\b), do: [?\\, ?b]
|
||||
defp escape_map(?\d), do: [?\\, ?d]
|
||||
defp escape_map(?\e), do: [?\\, ?e]
|
||||
defp escape_map(?\f), do: [?\\, ?f]
|
||||
defp escape_map(?\n), do: [?\\, ?n]
|
||||
defp escape_map(?\r), do: [?\\, ?r]
|
||||
defp escape_map(?\t), do: [?\\, ?t]
|
||||
defp escape_map(?\v), do: [?\\, ?v]
|
||||
defp escape_map(?\\), do: [?\\, ?\\]
|
||||
defp escape_map(?\a), do: "\\a"
|
||||
defp escape_map(?\b), do: "\\b"
|
||||
defp escape_map(?\d), do: "\\d"
|
||||
defp escape_map(?\e), do: "\\e"
|
||||
defp escape_map(?\f), do: "\\f"
|
||||
defp escape_map(?\n), do: "\\n"
|
||||
defp escape_map(?\r), do: "\\r"
|
||||
defp escape_map(?\t), do: "\\t"
|
||||
defp escape_map(?\v), do: "\\v"
|
||||
defp escape_map(?\\), do: "\\\\"
|
||||
defp escape_map(_), do: false
|
||||
|
||||
@compile {:inline, to_hex: 1, decrement: 1}
|
||||
|
||||
@@ -58,7 +58,7 @@ defmodule Code.Normalizer do
|
||||
{:.., meta, [left, right]}
|
||||
else
|
||||
step = do_normalize(step, state)
|
||||
{:"..//", meta, [left, right, step]}
|
||||
{:..//, meta, [left, right, step]}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -126,13 +126,13 @@ defmodule Code.Normalizer do
|
||||
{:., meta, [Access, :get]}
|
||||
end
|
||||
|
||||
# Only normalize the left side of the dot operator
|
||||
# The right hand side is an atom in the AST but it's not an atom literal, so
|
||||
# it should not be wrapped
|
||||
defp do_normalize({:., meta, [left, right]}, state) do
|
||||
# it should not be wrapped. However, it should be escaped if applicable.
|
||||
defp do_normalize({:., meta, [left, right]}, state) when is_atom(right) do
|
||||
meta = patch_meta_line(meta, state.parent_meta)
|
||||
|
||||
left = do_normalize(left, %{state | parent_meta: meta})
|
||||
right = maybe_escape_literal(right, state)
|
||||
|
||||
{:., meta, [left, right]}
|
||||
end
|
||||
@@ -174,16 +174,17 @@ defmodule Code.Normalizer do
|
||||
|
||||
# Sigils
|
||||
defp do_normalize({sigil, meta, [{:<<>>, _, args} = string, modifiers]} = quoted, state)
|
||||
when is_list(args) and is_atom(sigil) do
|
||||
case Atom.to_string(sigil) do
|
||||
"sigil_" <> _ ->
|
||||
meta =
|
||||
meta
|
||||
|> patch_meta_line(state.parent_meta)
|
||||
|> Keyword.put_new(:delimiter, "\"")
|
||||
|
||||
{sigil, meta, [do_normalize(string, %{state | parent_meta: meta}), modifiers]}
|
||||
when is_atom(sigil) and is_list(args) and is_list(modifiers) do
|
||||
with "sigil_" <> _ <- Atom.to_string(sigil),
|
||||
true <- binary_interpolated?(args),
|
||||
true <- List.ascii_printable?(modifiers) do
|
||||
meta =
|
||||
meta
|
||||
|> patch_meta_line(state.parent_meta)
|
||||
|> Keyword.put_new(:delimiter, "\"")
|
||||
|
||||
{sigil, meta, [do_normalize(string, %{state | parent_meta: meta}), modifiers]}
|
||||
else
|
||||
_ ->
|
||||
normalize_call(quoted, state)
|
||||
end
|
||||
@@ -288,21 +289,18 @@ defmodule Code.Normalizer do
|
||||
# Lists
|
||||
defp normalize_literal(list, meta, state) when is_list(list) do
|
||||
if list != [] and List.ascii_printable?(list) do
|
||||
# It's a charlist
|
||||
list =
|
||||
# It's a charlist, we normalize it as a ~C sigil
|
||||
string =
|
||||
if state.escape do
|
||||
{string, _} = Code.Identifier.escape(IO.chardata_to_string(list), nil)
|
||||
IO.iodata_to_binary(string) |> to_charlist()
|
||||
{iolist, _} = Code.Identifier.escape(IO.chardata_to_string(list), nil)
|
||||
IO.iodata_to_binary(iolist)
|
||||
else
|
||||
list
|
||||
List.to_string(list)
|
||||
end
|
||||
|
||||
meta =
|
||||
meta
|
||||
|> Keyword.put_new(:delimiter, "'")
|
||||
|> patch_meta_line(state.parent_meta)
|
||||
meta = patch_meta_line([delimiter: "\""], state.parent_meta)
|
||||
|
||||
{:__block__, meta, [list]}
|
||||
{:sigil_c, meta, [{:<<>>, [], [string]}, []]}
|
||||
else
|
||||
meta =
|
||||
if line = state.parent_meta[:line] do
|
||||
@@ -349,14 +347,14 @@ defmodule Code.Normalizer do
|
||||
args = normalize_args(args, %{state | parent_meta: meta})
|
||||
{form, meta, args}
|
||||
|
||||
Keyword.has_key?(meta, :do) or match?([{{:__block__, _, [:do]}, _} | _], last) ->
|
||||
Keyword.has_key?(meta, :do) ->
|
||||
# def foo do :ok end
|
||||
# def foo, do: :ok
|
||||
normalize_kw_blocks(form, meta, args, state)
|
||||
|
||||
match?([{:do, _} | _], last) and Keyword.keyword?(last) ->
|
||||
# Non normalized kw blocks
|
||||
line = state.parent_meta[:line]
|
||||
line = state.parent_meta[:line] || meta[:line]
|
||||
meta = meta ++ [do: [line: line], end: [line: line]]
|
||||
normalize_kw_blocks(form, meta, args, state)
|
||||
|
||||
@@ -366,11 +364,20 @@ defmodule Code.Normalizer do
|
||||
|
||||
last_args =
|
||||
case last_arg do
|
||||
{:__block__, _, [[{{:__block__, key_meta, _}, _} | _]] = last_args} ->
|
||||
if key_meta[:format] == :keyword do
|
||||
last_args
|
||||
else
|
||||
[last_arg]
|
||||
{:__block__, _meta, [[{{:__block__, key_meta, _}, _} | _] = keyword]} ->
|
||||
cond do
|
||||
key_meta[:format] == :keyword ->
|
||||
[keyword]
|
||||
|
||||
block_keyword?(keyword) ->
|
||||
[
|
||||
Enum.map(keyword, fn {{:__block__, meta, args}, value} ->
|
||||
{{:__block__, [format: :keyword] ++ meta, args}, value}
|
||||
end)
|
||||
]
|
||||
|
||||
true ->
|
||||
[last_arg]
|
||||
end
|
||||
|
||||
[] ->
|
||||
@@ -384,6 +391,12 @@ defmodule Code.Normalizer do
|
||||
end
|
||||
end
|
||||
|
||||
defp block_keyword?([{{:__block__, _, [key]}, _val} | tail]) when is_atom(key),
|
||||
do: block_keyword?(tail)
|
||||
|
||||
defp block_keyword?([]), do: true
|
||||
defp block_keyword?(_), do: false
|
||||
|
||||
defp allow_keyword?(:when, 2), do: true
|
||||
defp allow_keyword?(:{}, _), do: false
|
||||
defp allow_keyword?(op, arity), do: not is_atom(op) or not Macro.operator?(op, arity)
|
||||
|
||||
@@ -27,11 +27,6 @@ defmodule Code.Typespec do
|
||||
end
|
||||
end
|
||||
|
||||
def spec_to_quoted(name, {:type, anno, :fun, []}) when is_atom(name) do
|
||||
meta = meta(anno)
|
||||
{:"::", meta, [{name, meta, []}, quote(do: term)]}
|
||||
end
|
||||
|
||||
def spec_to_quoted(name, {:type, anno, :bounded_fun, [type, constrs]}) when is_atom(name) do
|
||||
meta = meta(anno)
|
||||
{:type, _, :fun, [{:type, _, :product, args}, result]} = type
|
||||
@@ -317,10 +312,6 @@ defmodule Code.Typespec do
|
||||
[{:->, meta(anno), [[typespec_to_quoted(args)], typespec_to_quoted(result)]}]
|
||||
end
|
||||
|
||||
defp typespec_to_quoted({:type, anno, :fun, []}) do
|
||||
typespec_to_quoted({:type, anno, :fun, [{:type, anno, :any}, {:type, anno, :any, []}]})
|
||||
end
|
||||
|
||||
defp typespec_to_quoted({:type, anno, :range, [left, right]}) do
|
||||
{:.., meta(anno), [typespec_to_quoted(left), typespec_to_quoted(right)]}
|
||||
end
|
||||
|
||||
@@ -41,7 +41,8 @@ defmodule Config do
|
||||
> application environment is effectively a global storage. Also note that
|
||||
> the `config/config.exs` of a library is not evaluated when the library is
|
||||
> used as a dependency, as configuration is always meant to configure the
|
||||
> current project. For more information, read our [library guidelines](library-guidelines.md).
|
||||
> current project. For more information, see ["Using application configuration for
|
||||
> libraries"](design-anti-patterns.md#using-application-configuration-for-libraries).
|
||||
|
||||
## Migrating from `use Mix.Config`
|
||||
|
||||
@@ -143,7 +144,7 @@ defmodule Config do
|
||||
"""
|
||||
@doc since: "1.9.0"
|
||||
def config(root_key, opts) when is_atom(root_key) and is_list(opts) do
|
||||
unless Keyword.keyword?(opts) do
|
||||
if not Keyword.keyword?(opts) do
|
||||
raise ArgumentError, "config/2 expected a keyword list, got: #{inspect(opts)}"
|
||||
end
|
||||
|
||||
@@ -194,6 +195,31 @@ defmodule Config do
|
||||
|> put_config()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Reads the configuration for the given root key.
|
||||
|
||||
This function only reads the configuration from a previous
|
||||
`config/2` or `config/3` call. If `root_key` points to an
|
||||
application, it does not read its actual application environment.
|
||||
Its main use case is to make it easier to access and share
|
||||
configuration values across files.
|
||||
|
||||
If the `root_key` was not configured, it returns `nil`.
|
||||
|
||||
## Examples
|
||||
|
||||
# In config/config.exs
|
||||
config :my_app, foo: :bar
|
||||
|
||||
# In config/dev.exs
|
||||
config :another_app, foo: read_config(:my_app)[:foo] || raise "missing parent configuration"
|
||||
|
||||
"""
|
||||
@doc since: "1.18.0"
|
||||
def read_config(root_key) when is_atom(root_key) do
|
||||
get_config!()[root_key]
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the environment this configuration file is executed on.
|
||||
|
||||
|
||||
@@ -215,10 +215,7 @@ defmodule DynamicSupervisor do
|
||||
extra_arguments: [term()]
|
||||
}
|
||||
|
||||
@typedoc "Options given to `start_link` functions"
|
||||
@type option :: GenServer.option()
|
||||
|
||||
@typedoc "Options given to `start_link` and `init/1` functions"
|
||||
@typedoc "Options given to `start_link/1` and `init/1` functions"
|
||||
@type init_option ::
|
||||
{:strategy, strategy()}
|
||||
| {:max_restarts, non_neg_integer()}
|
||||
@@ -277,7 +274,7 @@ defmodule DynamicSupervisor do
|
||||
defmacro __using__(opts) do
|
||||
quote location: :keep, bind_quoted: [opts: opts] do
|
||||
@behaviour DynamicSupervisor
|
||||
unless Module.has_attribute?(__MODULE__, :doc) do
|
||||
if not Module.has_attribute?(__MODULE__, :doc) do
|
||||
@doc """
|
||||
Returns a specification to start this module under a supervisor.
|
||||
|
||||
@@ -345,9 +342,11 @@ defmodule DynamicSupervisor do
|
||||
specified in the child spec given to `start_child/2`. Defaults to
|
||||
an empty list.
|
||||
|
||||
* Any of the standard [GenServer options](`t:GenServer.option/0`)
|
||||
|
||||
"""
|
||||
@doc since: "1.6.0"
|
||||
@spec start_link([option | init_option]) :: Supervisor.on_start()
|
||||
@spec start_link([init_option | GenServer.option()]) :: Supervisor.on_start()
|
||||
def start_link(options) when is_list(options) do
|
||||
keys = [:extra_arguments, :max_children, :max_seconds, :max_restarts, :strategy]
|
||||
{sup_opts, start_opts} = Keyword.split(options, keys)
|
||||
@@ -381,9 +380,15 @@ defmodule DynamicSupervisor do
|
||||
Note that a supervisor started with this function is linked to the parent
|
||||
process and exits not only on crashes but also if the parent process exits
|
||||
with `:normal` reason.
|
||||
|
||||
## Options
|
||||
|
||||
This function accepts any regular [`GenServer` options](`t:GenServer.option/0`).
|
||||
Options specific to `DynamicSupervisor` must be returned from the `c:init/1`
|
||||
callback.
|
||||
"""
|
||||
@doc since: "1.6.0"
|
||||
@spec start_link(module, term, [option]) :: Supervisor.on_start()
|
||||
@spec start_link(module, term, [GenServer.option()]) :: Supervisor.on_start()
|
||||
def start_link(module, init_arg, opts \\ []) do
|
||||
GenServer.start_link(__MODULE__, {module, init_arg, opts[:name]}, opts)
|
||||
end
|
||||
|
||||
+136
-30
@@ -837,7 +837,7 @@ defmodule Enum do
|
||||
end
|
||||
|
||||
def dedup(enumerable) do
|
||||
Enum.reduce(enumerable, [], fn x, acc ->
|
||||
reduce(enumerable, [], fn x, acc ->
|
||||
case acc do
|
||||
[^x | _] -> acc
|
||||
_ -> [x | acc]
|
||||
@@ -1257,6 +1257,14 @@ defmodule Enum do
|
||||
iex> Enum.flat_map([:a, :b, :c], fn x -> [[x]] end)
|
||||
[[:a], [:b], [:c]]
|
||||
|
||||
This is frequently used to to transform and filter in one pass, returning empty
|
||||
lists to exclude results:
|
||||
|
||||
iex> Enum.flat_map([4, 0, 2, 0], fn x ->
|
||||
...> if x != 0, do: [1 / x], else: []
|
||||
...> end)
|
||||
[0.25, 0.5]
|
||||
|
||||
"""
|
||||
@spec flat_map(t, (element -> t)) :: list
|
||||
def flat_map(enumerable, fun) when is_list(enumerable) do
|
||||
@@ -1266,6 +1274,7 @@ defmodule Enum do
|
||||
def flat_map(enumerable, fun) do
|
||||
reduce(enumerable, [], fn entry, acc ->
|
||||
case fun.(entry) do
|
||||
[] -> acc
|
||||
list when is_list(list) -> [list | acc]
|
||||
other -> [to_list(other) | acc]
|
||||
end
|
||||
@@ -1273,6 +1282,8 @@ defmodule Enum do
|
||||
|> flat_reverse([])
|
||||
end
|
||||
|
||||
# the first clause is an optimization
|
||||
defp flat_reverse([[elem] | t], acc), do: flat_reverse(t, [elem | acc])
|
||||
defp flat_reverse([h | t], acc), do: flat_reverse(t, h ++ acc)
|
||||
defp flat_reverse([], acc), do: acc
|
||||
|
||||
@@ -1538,7 +1549,7 @@ defmodule Enum do
|
||||
do: Map.merge(collectable, :maps.from_list(enumerable))
|
||||
|
||||
defp into_map(enumerable, collectable),
|
||||
do: Enum.reduce(enumerable, collectable, fn {key, val}, acc -> Map.put(acc, key, val) end)
|
||||
do: reduce(enumerable, collectable, fn {key, val}, acc -> Map.put(acc, key, val) end)
|
||||
|
||||
defp into_protocol(enumerable, collectable) do
|
||||
{initial, fun} = Collectable.into(collectable)
|
||||
@@ -1581,7 +1592,7 @@ defmodule Enum do
|
||||
"""
|
||||
@spec into(Enumerable.t(), Collectable.t(), (term -> term)) :: Collectable.t()
|
||||
def into(enumerable, [], transform) do
|
||||
Enum.map(enumerable, transform)
|
||||
map(enumerable, transform)
|
||||
end
|
||||
|
||||
def into(%_{} = enumerable, collectable, transform) do
|
||||
@@ -1594,9 +1605,9 @@ defmodule Enum do
|
||||
|
||||
def into(enumerable, %{} = collectable, transform) do
|
||||
if map_size(collectable) == 0 do
|
||||
enumerable |> Enum.map(transform) |> :maps.from_list()
|
||||
enumerable |> map(transform) |> :maps.from_list()
|
||||
else
|
||||
Enum.reduce(enumerable, collectable, fn entry, acc ->
|
||||
reduce(enumerable, collectable, fn entry, acc ->
|
||||
{key, val} = transform.(entry)
|
||||
Map.put(acc, key, val)
|
||||
end)
|
||||
@@ -2416,13 +2427,17 @@ defmodule Enum do
|
||||
{:ok, count, fun} when is_function(fun, 1) ->
|
||||
slice_list(fun.(enumerable), random_count(count), 1, 1)
|
||||
|
||||
# TODO: Deprecate me in Elixir v1.18.
|
||||
{:ok, count, fun} when is_function(fun, 2) ->
|
||||
fun.(random_count(count), 1)
|
||||
|
||||
{:ok, count, fun} when is_function(fun, 3) ->
|
||||
fun.(random_count(count), 1, 1)
|
||||
|
||||
# TODO: Remove deprecation on Elixir v1.20.
|
||||
{:ok, count, fun} when is_function(fun, 2) ->
|
||||
IO.warn(
|
||||
"#{inspect(Enumerable.impl_for(enumerable))} must return a three arity function on slice/1"
|
||||
)
|
||||
|
||||
fun.(random_count(count), 1)
|
||||
|
||||
{:error, _} ->
|
||||
take_random(enumerable, 1)
|
||||
end
|
||||
@@ -2702,11 +2717,11 @@ defmodule Enum do
|
||||
iex> Enum.slide([:a, :b, :c, :d, :e, :f, :g], 5, 1)
|
||||
[:a, :f, :b, :c, :d, :e, :g]
|
||||
|
||||
# Slide a range of elements backward
|
||||
# Slide a range of elements towards the head of the list
|
||||
iex> Enum.slide([:a, :b, :c, :d, :e, :f, :g], 3..5, 1)
|
||||
[:a, :d, :e, :f, :b, :c, :g]
|
||||
|
||||
# Slide a range of elements forward
|
||||
# Slide a range of elements towards the tail of the list
|
||||
iex> Enum.slide([:a, :b, :c, :d, :e, :f, :g], 1..3, 5)
|
||||
[:a, :e, :f, :b, :c, :d, :g]
|
||||
|
||||
@@ -2793,7 +2808,7 @@ defmodule Enum do
|
||||
# Then at the end, we're going to reassemble and reverse them, and end up with the
|
||||
# chunks in the correct order.
|
||||
{_size, pre, post} =
|
||||
Enum.reduce(enumerable, {0, [], []}, fn item, {index, pre, post} ->
|
||||
reduce(enumerable, {0, [], []}, fn item, {index, pre, post} ->
|
||||
{pre, post} =
|
||||
cond do
|
||||
index < start -> {[item | pre], post}
|
||||
@@ -3338,7 +3353,7 @@ defmodule Enum do
|
||||
|
||||
def sort_by(enumerable, mapper, :desc) when is_function(mapper, 1) do
|
||||
enumerable
|
||||
|> Enum.reduce([], &[{&1, mapper.(&1)} | &2])
|
||||
|> reduce([], &[{&1, mapper.(&1)} | &2])
|
||||
|> List.keysort(1, :asc)
|
||||
|> List.foldl([], &[elem(&1, 0) | &2])
|
||||
end
|
||||
@@ -3443,6 +3458,8 @@ defmodule Enum do
|
||||
|
||||
Raises `ArithmeticError` if `enumerable` contains a non-numeric value.
|
||||
|
||||
If you need to apply a transformation first, consider using `Enum.sum_by/2` instead.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Enum.sum([1, 2, 3])
|
||||
@@ -3469,11 +3486,47 @@ defmodule Enum do
|
||||
reduce(enumerable, 0, &+/2)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Maps and sums the given `enumerable` in one pass.
|
||||
|
||||
Raises `ArithmeticError` if `mapper` returns a non-numeric value.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Enum.sum_by([%{count: 1}, %{count: 2}, %{count: 3}], fn x -> x.count end)
|
||||
6
|
||||
|
||||
iex> Enum.sum_by(1..3, fn x -> x ** 2 end)
|
||||
14
|
||||
|
||||
iex> Enum.sum_by([], fn x -> x.count end)
|
||||
0
|
||||
|
||||
Filtering can be achieved by returning `0` to ignore elements:
|
||||
|
||||
iex> Enum.sum_by([1, -2, 3], fn x -> if x > 0, do: x, else: 0 end)
|
||||
4
|
||||
|
||||
"""
|
||||
@doc since: "1.18.0"
|
||||
@spec sum_by(t, (element -> number)) :: number
|
||||
def sum_by(enumerable, mapper)
|
||||
|
||||
def sum_by(list, mapper) when is_list(list) and is_function(mapper, 1) do
|
||||
sum_by_list(list, mapper, 0)
|
||||
end
|
||||
|
||||
def sum_by(enumerable, mapper) when is_function(mapper, 1) do
|
||||
reduce(enumerable, 0, fn x, acc -> acc + mapper.(x) end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the product of all elements.
|
||||
|
||||
Raises `ArithmeticError` if `enumerable` contains a non-numeric value.
|
||||
|
||||
If you need to apply a transformation first, consider using `Enum.product_by/2` instead.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Enum.product([])
|
||||
@@ -3490,6 +3543,40 @@ defmodule Enum do
|
||||
reduce(enumerable, 1, &*/2)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Maps and computes the product of the given `enumerable` in one pass.
|
||||
|
||||
Raises `ArithmeticError` if `mapper` returns a non-numeric value.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Enum.product_by([%{count: 2}, %{count: 4}, %{count: 3}], fn x -> x.count end)
|
||||
24
|
||||
|
||||
iex> Enum.product_by(1..3, fn x -> x ** 2 end)
|
||||
36
|
||||
|
||||
iex> Enum.product_by([], fn x -> x.count end)
|
||||
1
|
||||
|
||||
Filtering can be achieved by returning `1` to ignore elements:
|
||||
|
||||
iex> Enum.product_by([2, -1, 3], fn x -> if x > 0, do: x, else: 1 end)
|
||||
6
|
||||
|
||||
"""
|
||||
@doc since: "1.18.0"
|
||||
@spec product_by(t, (element -> number)) :: number
|
||||
def product_by(enumerable, mapper)
|
||||
|
||||
def product_by(list, mapper) when is_list(list) and is_function(mapper, 1) do
|
||||
product_by_list(list, mapper, 1)
|
||||
end
|
||||
|
||||
def product_by(enumerable, mapper) when is_function(mapper, 1) do
|
||||
reduce(enumerable, 1, fn x, acc -> acc * mapper.(x) end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Takes an `amount` of elements from the beginning or the end of the `enumerable`.
|
||||
|
||||
@@ -3845,13 +3932,11 @@ defmodule Enum do
|
||||
Returns the `enumerable` with each element wrapped in a tuple
|
||||
alongside its index or according to a given function.
|
||||
|
||||
May receive a function or an integer offset.
|
||||
If an integer offset is given as `fun_or_offset`, it will index from the given
|
||||
offset instead of from zero.
|
||||
|
||||
If an `offset` is given, it will index from the given offset instead of from
|
||||
zero.
|
||||
|
||||
If a `function` is given, it will index by invoking the function for each
|
||||
element and index (zero-based) of the enumerable.
|
||||
If a function is given as `fun_or_offset`, it will index by invoking the function
|
||||
for each element and index (zero-based) of the enumerable.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -4051,7 +4136,7 @@ defmodule Enum do
|
||||
|
||||
def zip_reduce(left, right, acc, reducer) when is_function(reducer, 3) do
|
||||
reduce = fn [l, r], acc -> {:cont, reducer.(l, r, acc)} end
|
||||
Stream.zip_with([left, right], & &1).({:cont, acc}, reduce) |> elem(1)
|
||||
R.zip_with([left, right], & &1).({:cont, acc}, reduce) |> elem(1)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -4086,7 +4171,7 @@ defmodule Enum do
|
||||
def zip_reduce([], acc, reducer) when is_function(reducer, 2), do: acc
|
||||
|
||||
def zip_reduce(enums, acc, reducer) when is_function(reducer, 2) do
|
||||
Stream.zip_with(enums, & &1).({:cont, acc}, &{:cont, reducer.(&1, &2)}) |> elem(1)
|
||||
R.zip_with(enums, & &1).({:cont, acc}, &{:cont, reducer.(&1, &2)}) |> elem(1)
|
||||
end
|
||||
|
||||
## Helpers
|
||||
@@ -4351,6 +4436,9 @@ defmodule Enum do
|
||||
|
||||
defp flat_map_list([head | tail], fun) do
|
||||
case fun.(head) do
|
||||
# the two first clauses are an optimization
|
||||
[] -> flat_map_list(tail, fun)
|
||||
[elem] -> [elem | flat_map_list(tail, fun)]
|
||||
list when is_list(list) -> list ++ flat_map_list(tail, fun)
|
||||
other -> to_list(other) ++ flat_map_list(tail, fun)
|
||||
end
|
||||
@@ -4492,8 +4580,16 @@ defmodule Enum do
|
||||
amount = Kernel.min(amount, count - start) |> amount_with_step(step)
|
||||
enumerable |> fun.() |> slice_exact(start, amount, step, count)
|
||||
|
||||
# TODO: Deprecate me in Elixir v1.18.
|
||||
{:ok, count, fun} when is_function(fun, 3) ->
|
||||
amount = Kernel.min(amount, count - start) |> amount_with_step(step)
|
||||
fun.(start, amount, step)
|
||||
|
||||
# TODO: Remove me on v2.0.
|
||||
{:ok, count, fun} when is_function(fun, 2) ->
|
||||
IO.warn(
|
||||
"#{inspect(Enumerable.impl_for(enumerable))} must return a three arity function on slice/1"
|
||||
)
|
||||
|
||||
amount = Kernel.min(amount, count - start)
|
||||
|
||||
if step == 1 do
|
||||
@@ -4503,10 +4599,6 @@ defmodule Enum do
|
||||
|> take_every_list(amount, step - 1)
|
||||
end
|
||||
|
||||
{:ok, count, fun} when is_function(fun, 3) ->
|
||||
amount = Kernel.min(amount, count - start) |> amount_with_step(step)
|
||||
fun.(start, amount, step)
|
||||
|
||||
{:error, module} ->
|
||||
slice_enum(enumerable, module, start, amount, step)
|
||||
end
|
||||
@@ -4565,11 +4657,18 @@ defmodule Enum do
|
||||
|
||||
defp slice_count_and_fun(enumerable, step) do
|
||||
case Enumerable.slice(enumerable) do
|
||||
{:ok, count, fun} when is_function(fun, 1) ->
|
||||
{count, &slice_exact(fun.(enumerable), &1, &2, &3, count)}
|
||||
|
||||
{:ok, count, fun} when is_function(fun, 3) ->
|
||||
{count, fun}
|
||||
|
||||
# TODO: Deprecate me in Elixir v1.18.
|
||||
# TODO: Remove me on v2.0
|
||||
{:ok, count, fun} when is_function(fun, 2) ->
|
||||
IO.warn(
|
||||
"#{inspect(Enumerable.impl_for(enumerable))} must return a three arity function on slice/1"
|
||||
)
|
||||
|
||||
if step == 1 do
|
||||
{count, fn start, amount, 1 -> fun.(start, amount) end}
|
||||
else
|
||||
@@ -4580,9 +4679,6 @@ defmodule Enum do
|
||||
end}
|
||||
end
|
||||
|
||||
{:ok, count, fun} when is_function(fun, 1) ->
|
||||
{count, &slice_exact(fun.(enumerable), &1, &2, &3, count)}
|
||||
|
||||
{:error, module} ->
|
||||
{list, count} =
|
||||
enumerable
|
||||
@@ -4758,6 +4854,16 @@ defmodule Enum do
|
||||
{:lists.reverse(acc), []}
|
||||
end
|
||||
|
||||
## sum_by
|
||||
|
||||
defp sum_by_list([], _, acc), do: acc
|
||||
defp sum_by_list([h | t], mapper, acc), do: sum_by_list(t, mapper, acc + mapper.(h))
|
||||
|
||||
## product_by
|
||||
|
||||
defp product_by_list([], _, acc), do: acc
|
||||
defp product_by_list([h | t], mapper, acc), do: product_by_list(t, mapper, acc * mapper.(h))
|
||||
|
||||
## take
|
||||
|
||||
defp take_list(_list, 0), do: []
|
||||
|
||||
+132
-55
@@ -49,7 +49,7 @@ defmodule Exception do
|
||||
@doc """
|
||||
Receives the exception struct and must return its message.
|
||||
|
||||
Most commonly exceptions have a message field which by default is accessed
|
||||
Many exceptions have a message field which by default is accessed
|
||||
by this function. However, if an exception does not have a message field,
|
||||
this function must be explicitly implemented.
|
||||
"""
|
||||
@@ -346,7 +346,7 @@ defmodule Exception do
|
||||
defp rewrite_arg(arg) do
|
||||
Macro.prewalk(arg, fn
|
||||
{:%{}, meta, [__struct__: Range, first: first, last: last, step: step]} ->
|
||||
{:"..//", meta, [first, last, step]}
|
||||
{:..//, meta, [first, last, step]}
|
||||
|
||||
other ->
|
||||
other
|
||||
@@ -407,8 +407,8 @@ defmodule Exception do
|
||||
|
||||
defp erl_to_ex(mod, fun, args, meta) do
|
||||
case :elixir_rewrite.erl_to_ex(mod, fun, args) do
|
||||
{Kernel, fun, args} -> {fun, meta, args}
|
||||
{mod, fun, args} -> {{:., [], [mod, fun]}, meta, args}
|
||||
{Kernel, fun, args, _} -> {fun, meta, args}
|
||||
{mod, fun, args, _} -> {{:., [], [mod, fun]}, meta, args}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1212,7 +1212,7 @@ defmodule SyntaxError do
|
||||
})
|
||||
when not is_nil(snippet) and not is_nil(column) do
|
||||
snippet =
|
||||
:elixir_errors.format_snippet({line, column}, file, description, snippet, :error, [], nil)
|
||||
:elixir_errors.format_snippet(:error, {line, column}, file, description, snippet, %{})
|
||||
|
||||
format_message(file, line, column, snippet)
|
||||
end
|
||||
@@ -1225,7 +1225,7 @@ defmodule SyntaxError do
|
||||
description: description
|
||||
}) do
|
||||
snippet =
|
||||
:elixir_errors.format_snippet({line, column}, file, description, nil, :error, [], nil)
|
||||
:elixir_errors.format_snippet(:error, {line, column}, file, description, nil, %{})
|
||||
|
||||
padded = " " <> String.replace(snippet, "\n", "\n ")
|
||||
format_message(file, line, column, padded)
|
||||
@@ -1316,7 +1316,7 @@ defmodule TokenMissingError do
|
||||
description: description
|
||||
}) do
|
||||
snippet =
|
||||
:elixir_errors.format_snippet({line, column}, file, description, snippet, :error, [], nil)
|
||||
:elixir_errors.format_snippet(:error, {line, column}, file, description, snippet, %{})
|
||||
|
||||
format_message(file, line, column, snippet)
|
||||
end
|
||||
@@ -1581,13 +1581,15 @@ defmodule UndefinedFunctionError do
|
||||
|
||||
"""
|
||||
|
||||
@function_threshold 0.77
|
||||
@max_suggestions 5
|
||||
defexception [:module, :function, :arity, :reason, :message]
|
||||
|
||||
@impl true
|
||||
def message(%{message: nil} = exception) do
|
||||
%{reason: reason, module: module, function: function, arity: arity} = exception
|
||||
{message, _hint_type} = message(reason, module, function, arity)
|
||||
message
|
||||
{message, hint_type} = message(reason, module, function, arity)
|
||||
message <> if(hint_type == :suggest_module, do: hint_for_missing_alias(module), else: "")
|
||||
end
|
||||
|
||||
def message(%{message: message}) do
|
||||
@@ -1648,12 +1650,25 @@ defmodule UndefinedFunctionError do
|
||||
end
|
||||
|
||||
defp hint(module, function, arity, :suggest_function) do
|
||||
behaviour_hint(module, function, arity) <>
|
||||
hint_for_loaded_module(module, function, arity, nil)
|
||||
hint_for_behaviour(module, function, arity) <>
|
||||
hint_for_loaded_module(module, function, arity)
|
||||
end
|
||||
|
||||
@max_suggestions 5
|
||||
defp hint(module, function, arity, :suggest_module) do
|
||||
hint_for_missing_module(module, function, arity)
|
||||
end
|
||||
|
||||
defp hint_for_missing_alias(module) do
|
||||
with "Elixir." <> rest <- Atom.to_string(module),
|
||||
false <- rest =~ "." do
|
||||
". Make sure the module name is correct and has been specified in full (or that an alias has been defined)"
|
||||
else
|
||||
_ -> ""
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def hint_for_missing_module(module, function, arity) do
|
||||
downcased_module = downcase_module_name(module)
|
||||
stripped_module = module |> Atom.to_string() |> String.replace_leading("Elixir.", "")
|
||||
|
||||
@@ -1674,9 +1689,9 @@ defmodule UndefinedFunctionError do
|
||||
["\n * ", Exception.format_mfa(module, function, arity)]
|
||||
end)
|
||||
|
||||
IO.iodata_to_binary([". Did you mean:\n", suggestions, "\n"])
|
||||
". Did you mean:\n#{suggestions}\n"
|
||||
else
|
||||
""
|
||||
hint_for_missing_alias(module)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1693,7 +1708,7 @@ defmodule UndefinedFunctionError do
|
||||
end
|
||||
|
||||
@doc false
|
||||
def hint_for_loaded_module(module, function, arity, exports) do
|
||||
def hint_for_loaded_module(module, function, arity) do
|
||||
cond do
|
||||
macro_exported?(module, function, arity) ->
|
||||
". However, there is a macro with the same name and arity. " <>
|
||||
@@ -1703,7 +1718,7 @@ defmodule UndefinedFunctionError do
|
||||
", #{message}"
|
||||
|
||||
true ->
|
||||
IO.iodata_to_binary(did_you_mean(module, function, exports))
|
||||
IO.iodata_to_binary(did_you_mean(module, function))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1714,11 +1729,39 @@ defmodule UndefinedFunctionError do
|
||||
end
|
||||
end
|
||||
|
||||
@function_threshold 0.77
|
||||
@max_suggestions 5
|
||||
defp hint_for_behaviour(module, function, arity) do
|
||||
case behaviours_for(module) do
|
||||
[] ->
|
||||
""
|
||||
|
||||
defp did_you_mean(module, function, exports) do
|
||||
exports = exports || exports_for(module)
|
||||
behaviours ->
|
||||
case Enum.find(behaviours, &expects_callback?(&1, function, arity)) do
|
||||
nil -> ""
|
||||
behaviour -> ", but the behaviour #{inspect(behaviour)} expects it to be present"
|
||||
end
|
||||
end
|
||||
rescue
|
||||
# In case the module was removed while we are computing this
|
||||
UndefinedFunctionError -> ""
|
||||
end
|
||||
|
||||
defp behaviours_for(module) do
|
||||
:attributes
|
||||
|> module.module_info()
|
||||
|> Keyword.get(:behaviour, [])
|
||||
end
|
||||
|
||||
defp expects_callback?(behaviour, function, arity) do
|
||||
callbacks =
|
||||
behaviour.behaviour_info(:callbacks) -- behaviour.behaviour_info(:optional_callbacks)
|
||||
|
||||
Enum.member?(callbacks, {function, arity})
|
||||
end
|
||||
|
||||
## Shared helpers across hints
|
||||
|
||||
defp did_you_mean(module, function) do
|
||||
exports = exports_for(module)
|
||||
|
||||
result =
|
||||
case Keyword.take(exports, [function]) do
|
||||
@@ -1748,35 +1791,6 @@ defmodule UndefinedFunctionError do
|
||||
[" * ", Macro.inspect_atom(:remote_call, fun), ?/, Integer.to_string(arity), ?\n]
|
||||
end
|
||||
|
||||
defp behaviour_hint(module, function, arity) do
|
||||
case behaviours_for(module) do
|
||||
[] ->
|
||||
""
|
||||
|
||||
behaviours ->
|
||||
case Enum.find(behaviours, &expects_callback?(&1, function, arity)) do
|
||||
nil -> ""
|
||||
behaviour -> ", but the behaviour #{inspect(behaviour)} expects it to be present"
|
||||
end
|
||||
end
|
||||
rescue
|
||||
# In case the module was removed while we are computing this
|
||||
UndefinedFunctionError -> ""
|
||||
end
|
||||
|
||||
defp behaviours_for(module) do
|
||||
:attributes
|
||||
|> module.module_info()
|
||||
|> Keyword.get(:behaviour, [])
|
||||
end
|
||||
|
||||
defp expects_callback?(behaviour, function, arity) do
|
||||
callbacks =
|
||||
behaviour.behaviour_info(:callbacks) -- behaviour.behaviour_info(:optional_callbacks)
|
||||
|
||||
Enum.member?(callbacks, {function, arity})
|
||||
end
|
||||
|
||||
defp exports_for(module) do
|
||||
if function_exported?(module, :__info__, 1) do
|
||||
module.__info__(:macros) ++ module.__info__(:functions)
|
||||
@@ -1785,8 +1799,7 @@ defmodule UndefinedFunctionError do
|
||||
end
|
||||
rescue
|
||||
# In case the module was removed while we are computing this
|
||||
UndefinedFunctionError ->
|
||||
[]
|
||||
UndefinedFunctionError -> []
|
||||
end
|
||||
|
||||
defp deprecated_functions_for(module) do
|
||||
@@ -1797,8 +1810,7 @@ defmodule UndefinedFunctionError do
|
||||
end
|
||||
rescue
|
||||
# In case the module was removed while we are computing this
|
||||
UndefinedFunctionError ->
|
||||
[]
|
||||
UndefinedFunctionError -> []
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1985,8 +1997,23 @@ defmodule Protocol.UndefinedError do
|
||||
|
||||
@impl true
|
||||
def message(%{protocol: protocol, value: value, description: description}) do
|
||||
"protocol #{inspect(protocol)} not implemented for #{inspect(value)} of type " <>
|
||||
value_type(value) <> maybe_description(description) <> maybe_available(protocol)
|
||||
inspected =
|
||||
value
|
||||
|> inspect(pretty: true)
|
||||
# Indent only lines with contents on them
|
||||
|> String.replace(~r/^(?=.+)/m, " ")
|
||||
|
||||
"protocol #{inspect(protocol)} not implemented for type " <>
|
||||
value_type(value) <>
|
||||
maybe_description(description) <>
|
||||
maybe_available(protocol) <>
|
||||
"""
|
||||
|
||||
|
||||
Got value:
|
||||
|
||||
#{inspected}
|
||||
"""
|
||||
end
|
||||
|
||||
defp value_type(%{__struct__: struct}), do: "#{inspect(struct)} (a struct)"
|
||||
@@ -2131,6 +2158,56 @@ defmodule UnicodeConversionError do
|
||||
end
|
||||
end
|
||||
|
||||
defmodule MissingApplicationsError do
|
||||
@moduledoc """
|
||||
An exception that is raised when an application depends on one or more
|
||||
missing applications.
|
||||
|
||||
This exception is used by Mix and other tools. It can also be used by library authors
|
||||
when their library only requires an external application (like a dependency) for a subset
|
||||
of features.
|
||||
|
||||
The fields of this exception are public. See `t:t/0`.
|
||||
|
||||
*Available since v1.18.0.*
|
||||
|
||||
## Examples
|
||||
|
||||
unless Application.spec(:plug, :vsn) do
|
||||
raise MissingApplicationsError,
|
||||
description: "application :plug is required for testing Plug-related functionality",
|
||||
apps: [{:plug, "~> 1.0"}]
|
||||
end
|
||||
|
||||
"""
|
||||
|
||||
@moduledoc since: "1.18.0"
|
||||
|
||||
@type t() :: %__MODULE__{
|
||||
apps: [{Application.app(), Version.requirement()}, ...],
|
||||
description: String.t()
|
||||
}
|
||||
|
||||
defexception apps: [], description: "missing applications found"
|
||||
|
||||
@impl true
|
||||
def message(%__MODULE__{apps: apps, description: description}) do
|
||||
# We explicitly format these as tuples so that they're easier to copy-paste
|
||||
# into dependencies.
|
||||
formatted_apps =
|
||||
Enum.map(apps, fn {app_name, requirement} ->
|
||||
~s(\n {#{inspect(app_name)}, "#{requirement}"})
|
||||
end)
|
||||
|
||||
"""
|
||||
#{description}
|
||||
|
||||
To address this, include these applications as your dependencies:
|
||||
#{formatted_apps}\
|
||||
"""
|
||||
end
|
||||
end
|
||||
|
||||
defmodule Enum.OutOfBoundsError do
|
||||
@moduledoc """
|
||||
An exception that is raised when a function expects an enumerable to have
|
||||
|
||||
+11
-11
@@ -139,6 +139,8 @@ defmodule Float do
|
||||
iex> Float.parse("56.5xyz")
|
||||
{56.5, "xyz"}
|
||||
|
||||
iex> Float.parse(".12")
|
||||
:error
|
||||
iex> Float.parse("pi")
|
||||
:error
|
||||
iex> Float.parse("1.7976931348623159e+308")
|
||||
@@ -575,18 +577,14 @@ defmodule Float do
|
||||
Returns a charlist which corresponds to the shortest text representation
|
||||
of the given float.
|
||||
|
||||
The underlying algorithm changes depending on the Erlang/OTP version:
|
||||
|
||||
* For OTP >= 24, it uses the algorithm presented in "Ryū: fast
|
||||
float-to-string conversion" in Proceedings of the SIGPLAN '2018
|
||||
Conference on Programming Language Design and Implementation.
|
||||
|
||||
* For OTP < 24, it uses the algorithm presented in "Printing Floating-Point
|
||||
Numbers Quickly and Accurately" in Proceedings of the SIGPLAN '1996
|
||||
Conference on Programming Language Design and Implementation.
|
||||
It uses the algorithm presented in "Ryū: fast float-to-string conversion"
|
||||
in Proceedings of the SIGPLAN '2018 Conference on Programming Language
|
||||
Design and Implementation.
|
||||
|
||||
For a configurable representation, use `:erlang.float_to_list/2`.
|
||||
|
||||
Inlined by the compiler.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Float.to_charlist(7.0)
|
||||
@@ -594,7 +592,7 @@ defmodule Float do
|
||||
|
||||
"""
|
||||
@spec to_charlist(float) :: charlist
|
||||
def to_charlist(float) when is_float(float) do
|
||||
def to_charlist(float) do
|
||||
:erlang.float_to_list(float, [:short])
|
||||
end
|
||||
|
||||
@@ -614,6 +612,8 @@ defmodule Float do
|
||||
|
||||
For a configurable representation, use `:erlang.float_to_binary/2`.
|
||||
|
||||
Inlined by the compiler.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Float.to_string(7.0)
|
||||
@@ -621,7 +621,7 @@ defmodule Float do
|
||||
|
||||
"""
|
||||
@spec to_string(float) :: String.t()
|
||||
def to_string(float) when is_float(float) do
|
||||
def to_string(float) do
|
||||
:erlang.float_to_binary(float, [:short])
|
||||
end
|
||||
|
||||
|
||||
@@ -46,8 +46,8 @@ defmodule GenEvent do
|
||||
@callback init(args :: term) ::
|
||||
{:ok, state}
|
||||
| {:ok, state, :hibernate}
|
||||
| {:error, reason :: any}
|
||||
when state: any
|
||||
| {:error, reason :: term}
|
||||
when state: term
|
||||
|
||||
@callback handle_event(event :: term, state :: term) ::
|
||||
{:ok, new_state}
|
||||
|
||||
@@ -415,7 +415,7 @@ defmodule GenServer do
|
||||
Let's see how we could use those functions for debugging the stack server
|
||||
we defined earlier.
|
||||
|
||||
iex> {:ok, pid} = Stack.start_link([])
|
||||
iex> {:ok, pid} = Stack.start_link("")
|
||||
iex> :sys.statistics(pid, true) # turn on collecting process statistics
|
||||
iex> :sys.trace(pid, true) # turn on event printing
|
||||
iex> Stack.push(pid, 1)
|
||||
@@ -523,8 +523,8 @@ defmodule GenServer do
|
||||
{:ok, state}
|
||||
| {:ok, state, timeout | :hibernate | {:continue, continue_arg :: term}}
|
||||
| :ignore
|
||||
| {:stop, reason :: any}
|
||||
when state: any
|
||||
| {:stop, reason :: term}
|
||||
when state: term
|
||||
|
||||
@doc """
|
||||
Invoked to handle synchronous `call/3` messages. `call/3` will block until a
|
||||
@@ -845,7 +845,7 @@ defmodule GenServer do
|
||||
quote location: :keep, bind_quoted: [opts: opts] do
|
||||
@behaviour GenServer
|
||||
|
||||
unless Module.has_attribute?(__MODULE__, :doc) do
|
||||
if not Module.has_attribute?(__MODULE__, :doc) do
|
||||
@doc """
|
||||
Returns a specification to start this module under a supervisor.
|
||||
|
||||
@@ -945,7 +945,7 @@ defmodule GenServer do
|
||||
end
|
||||
|
||||
defmacro __before_compile__(env) do
|
||||
unless Module.defines?(env.module, {:init, 1}) do
|
||||
if not Module.defines?(env.module, {:init, 1}) do
|
||||
message = """
|
||||
function init/1 required by behaviour GenServer is not implemented \
|
||||
(in module #{inspect(env.module)}).
|
||||
@@ -1018,7 +1018,7 @@ defmodule GenServer do
|
||||
or `:ignore`, the process is terminated and this function returns
|
||||
`{:error, reason}` or `:ignore`, respectively.
|
||||
"""
|
||||
@spec start_link(module, any, options) :: on_start
|
||||
@spec start_link(module, term, options) :: on_start
|
||||
def start_link(module, init_arg, options \\ []) when is_atom(module) and is_list(options) do
|
||||
do_start(:link, module, init_arg, options)
|
||||
end
|
||||
@@ -1028,7 +1028,7 @@ defmodule GenServer do
|
||||
|
||||
See `start_link/3` for more information.
|
||||
"""
|
||||
@spec start(module, any, options) :: on_start
|
||||
@spec start(module, term, options) :: on_start
|
||||
def start(module, init_arg, options \\ []) when is_atom(module) and is_list(options) do
|
||||
do_start(:nolink, module, init_arg, options)
|
||||
end
|
||||
|
||||
+104
-92
@@ -139,6 +139,85 @@ defprotocol Inspect do
|
||||
# Handle structs in Any
|
||||
@fallback_to_any true
|
||||
|
||||
@impl true
|
||||
defmacro __deriving__(module, options) do
|
||||
info = Macro.struct_info!(module, __CALLER__)
|
||||
fields = Enum.sort(Enum.map(info, & &1.field) -- [:__exception__, :__struct__])
|
||||
|
||||
only = Keyword.get(options, :only, fields)
|
||||
except = Keyword.get(options, :except, [])
|
||||
optional = Keyword.get(options, :optional, [])
|
||||
|
||||
:ok = validate_option(:only, only, fields, module)
|
||||
:ok = validate_option(:except, except, fields, module)
|
||||
:ok = validate_option(:optional, optional, fields, module)
|
||||
|
||||
inspect_module =
|
||||
if fields == Enum.sort(only) and except == [] do
|
||||
Inspect.Map
|
||||
else
|
||||
Inspect.Any
|
||||
end
|
||||
|
||||
filtered_fields =
|
||||
fields
|
||||
|> Enum.reject(&(&1 in except))
|
||||
|> Enum.filter(&(&1 in only))
|
||||
|
||||
filtered_guard =
|
||||
quote do
|
||||
var!(field) in unquote(filtered_fields)
|
||||
end
|
||||
|
||||
field_guard =
|
||||
if optional == [] do
|
||||
filtered_guard
|
||||
else
|
||||
optional_map =
|
||||
for field <- optional, into: %{} do
|
||||
default = Enum.find(info, %{}, &(&1.field == field)) |> Map.get(:default, nil)
|
||||
{field, default}
|
||||
end
|
||||
|
||||
quote do
|
||||
unquote(filtered_guard) and
|
||||
not case unquote(Macro.escape(optional_map)) do
|
||||
%{^var!(field) => var!(default)} ->
|
||||
var!(default) == Map.get(var!(struct), var!(field))
|
||||
|
||||
%{} ->
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
quote do
|
||||
defimpl Inspect, for: unquote(module) do
|
||||
def inspect(var!(struct), var!(opts)) do
|
||||
var!(infos) =
|
||||
for %{field: var!(field)} = var!(info) <- unquote(module).__info__(:struct),
|
||||
unquote(field_guard),
|
||||
do: var!(info)
|
||||
|
||||
var!(name) = Macro.inspect_atom(:literal, unquote(module))
|
||||
unquote(inspect_module).inspect(var!(struct), var!(name), var!(infos), var!(opts))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp validate_option(option, option_list, fields, module) do
|
||||
case option_list -- fields do
|
||||
[] ->
|
||||
:ok
|
||||
|
||||
unknown_fields ->
|
||||
raise ArgumentError,
|
||||
"unknown fields #{Kernel.inspect(unknown_fields)} in #{Kernel.inspect(option)} " <>
|
||||
"when deriving the Inspect protocol for #{Kernel.inspect(module)}"
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts `term` into an algebra document.
|
||||
|
||||
@@ -155,7 +234,7 @@ defimpl Inspect, for: Atom do
|
||||
require Macro
|
||||
|
||||
def inspect(atom, opts) do
|
||||
color(Macro.inspect_atom(:literal, atom), color_key(atom), opts)
|
||||
color_doc(Macro.inspect_atom(:literal, atom), color_key(atom), opts)
|
||||
end
|
||||
|
||||
defp color_key(atom) when is_boolean(atom), do: :boolean
|
||||
@@ -175,7 +254,7 @@ defimpl Inspect, for: BitString do
|
||||
{escaped, _} -> [?", escaped, ?", " <> ..."]
|
||||
end
|
||||
|
||||
color(IO.iodata_to_binary(inspected), :string, opts)
|
||||
color_doc(IO.iodata_to_binary(inspected), :string, opts)
|
||||
else
|
||||
inspect_bitstring(term, opts)
|
||||
end
|
||||
@@ -186,12 +265,12 @@ defimpl Inspect, for: BitString do
|
||||
end
|
||||
|
||||
defp inspect_bitstring("", opts) do
|
||||
color("<<>>", :binary, opts)
|
||||
color_doc("<<>>", :binary, opts)
|
||||
end
|
||||
|
||||
defp inspect_bitstring(bitstring, opts) do
|
||||
left = color("<<", :binary, opts)
|
||||
right = color(">>", :binary, opts)
|
||||
left = color_doc("<<", :binary, opts)
|
||||
right = color_doc(">>", :binary, opts)
|
||||
inner = each_bit(bitstring, opts.limit, opts)
|
||||
group(concat(concat(left, nest(inner, 2)), right))
|
||||
end
|
||||
@@ -228,7 +307,7 @@ end
|
||||
|
||||
defimpl Inspect, for: List do
|
||||
def inspect([], opts) do
|
||||
color("[]", :list, opts)
|
||||
color_doc("[]", :list, opts)
|
||||
end
|
||||
|
||||
# TODO: Remove :char_list and :as_char_lists handling on v2.0
|
||||
@@ -259,9 +338,9 @@ defimpl Inspect, for: List do
|
||||
lists
|
||||
end
|
||||
|
||||
open = color("[", :list, opts)
|
||||
sep = color(",", :list, opts)
|
||||
close = color("]", :list, opts)
|
||||
open = color_doc("[", :list, opts)
|
||||
sep = color_doc(",", :list, opts)
|
||||
close = color_doc("]", :list, opts)
|
||||
|
||||
cond do
|
||||
lists == :as_charlists or (lists == :infer and List.ascii_printable?(term, printable_limit)) ->
|
||||
@@ -271,7 +350,7 @@ defimpl Inspect, for: List do
|
||||
{escaped, _} -> [?~, ?c, ?", escaped, ?", " ++ ..."]
|
||||
end
|
||||
|
||||
color(IO.iodata_to_binary(inspected), :charlist, opts)
|
||||
color_doc(IO.iodata_to_binary(inspected), :charlist, opts)
|
||||
|
||||
keyword?(term) ->
|
||||
container_doc(open, term, close, opts, &keyword/2, separator: sep, break: :strict)
|
||||
@@ -283,7 +362,7 @@ defimpl Inspect, for: List do
|
||||
|
||||
@doc false
|
||||
def keyword({key, value}, opts) do
|
||||
key = color(Macro.inspect_atom(:key, key), :atom, opts)
|
||||
key = color_doc(Macro.inspect_atom(:key, key), :atom, opts)
|
||||
concat(key, concat(" ", to_doc(value, opts)))
|
||||
end
|
||||
|
||||
@@ -301,9 +380,9 @@ end
|
||||
|
||||
defimpl Inspect, for: Tuple do
|
||||
def inspect(tuple, opts) do
|
||||
open = color("{", :tuple, opts)
|
||||
sep = color(",", :tuple, opts)
|
||||
close = color("}", :tuple, opts)
|
||||
open = color_doc("{", :tuple, opts)
|
||||
sep = color_doc(",", :tuple, opts)
|
||||
close = color_doc("}", :tuple, opts)
|
||||
container_opts = [separator: sep, break: :flex]
|
||||
container_doc(open, Tuple.to_list(tuple), close, opts, &to_doc/2, container_opts)
|
||||
end
|
||||
@@ -322,7 +401,7 @@ defimpl Inspect, for: Map do
|
||||
if Inspect.List.keyword?(list) do
|
||||
&Inspect.List.keyword/2
|
||||
else
|
||||
sep = color(" => ", :map, opts)
|
||||
sep = color_doc(" => ", :map, opts)
|
||||
&to_assoc(&1, &2, sep)
|
||||
end
|
||||
|
||||
@@ -339,9 +418,9 @@ defimpl Inspect, for: Map do
|
||||
end
|
||||
|
||||
defp map_container_doc(list, name, opts, fun) do
|
||||
open = color("%" <> name <> "{", :map, opts)
|
||||
sep = color(",", :map, opts)
|
||||
close = color("}", :map, opts)
|
||||
open = color_doc("%" <> name <> "{", :map, opts)
|
||||
sep = color_doc(",", :map, opts)
|
||||
close = color_doc("}", :map, opts)
|
||||
container_doc(open, list, close, opts, fun, separator: sep, break: :strict)
|
||||
end
|
||||
end
|
||||
@@ -349,7 +428,7 @@ end
|
||||
defimpl Inspect, for: Integer do
|
||||
def inspect(term, %Inspect.Opts{base: base} = opts) do
|
||||
inspected = Integer.to_string(term, base_to_value(base)) |> prepend_prefix(base)
|
||||
color(inspected, :number, opts)
|
||||
color_doc(inspected, :number, opts)
|
||||
end
|
||||
|
||||
defp base_to_value(base) do
|
||||
@@ -387,10 +466,10 @@ defimpl Inspect, for: Float do
|
||||
if abs >= 1.0 and abs < 1.0e16 and trunc(float) == float do
|
||||
[Integer.to_string(trunc(float)), ?., ?0]
|
||||
else
|
||||
:erlang.float_to_list(float, [:short])
|
||||
Float.to_charlist(float)
|
||||
end
|
||||
|
||||
color(IO.iodata_to_binary(formatted), :number, opts)
|
||||
color_doc(IO.iodata_to_binary(formatted), :number, opts)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -413,7 +492,7 @@ defimpl Inspect, for: Regex do
|
||||
|> Identifier.escape(?/, :infinity, &escape_map/1)
|
||||
|
||||
source = IO.iodata_to_binary([?~, ?r, ?/, escaped, ?/, translated_opts])
|
||||
color(source, :regex, opts)
|
||||
color_doc(source, :regex, opts)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -548,73 +627,6 @@ defimpl Inspect, for: Reference do
|
||||
end
|
||||
|
||||
defimpl Inspect, for: Any do
|
||||
defmacro __deriving__(module, struct, options) do
|
||||
fields = Enum.sort(Map.keys(struct) -- [:__exception__, :__struct__])
|
||||
|
||||
only = Keyword.get(options, :only, fields)
|
||||
except = Keyword.get(options, :except, [])
|
||||
optional = Keyword.get(options, :optional, [])
|
||||
|
||||
:ok = validate_option(:only, only, fields, module)
|
||||
:ok = validate_option(:except, except, fields, module)
|
||||
:ok = validate_option(:optional, optional, fields, module)
|
||||
|
||||
inspect_module =
|
||||
if fields == Enum.sort(only) and except == [] do
|
||||
Inspect.Map
|
||||
else
|
||||
Inspect.Any
|
||||
end
|
||||
|
||||
filtered_fields =
|
||||
fields
|
||||
|> Enum.reject(&(&1 in except))
|
||||
|> Enum.filter(&(&1 in only))
|
||||
|
||||
optional? =
|
||||
if optional == [] do
|
||||
false
|
||||
else
|
||||
optional_map = for field <- optional, into: %{}, do: {field, Map.fetch!(struct, field)}
|
||||
|
||||
quote do
|
||||
case unquote(Macro.escape(optional_map)) do
|
||||
%{^var!(field) => var!(default)} ->
|
||||
var!(default) == Map.get(var!(struct), var!(field))
|
||||
|
||||
%{} ->
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
quote do
|
||||
defimpl Inspect, for: unquote(module) do
|
||||
def inspect(var!(struct), var!(opts)) do
|
||||
var!(infos) =
|
||||
for %{field: var!(field)} = var!(info) <- unquote(module).__info__(:struct),
|
||||
var!(field) in unquote(filtered_fields) and not unquote(optional?),
|
||||
do: var!(info)
|
||||
|
||||
var!(name) = Macro.inspect_atom(:literal, unquote(module))
|
||||
unquote(inspect_module).inspect(var!(struct), var!(name), var!(infos), var!(opts))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp validate_option(option, option_list, fields, module) do
|
||||
case option_list -- fields do
|
||||
[] ->
|
||||
:ok
|
||||
|
||||
unknown_fields ->
|
||||
raise ArgumentError,
|
||||
"unknown fields #{Kernel.inspect(unknown_fields)} in #{Kernel.inspect(option)} " <>
|
||||
"when deriving the Inspect protocol for #{Kernel.inspect(module)}"
|
||||
end
|
||||
end
|
||||
|
||||
def inspect(%module{} = struct, opts) do
|
||||
try do
|
||||
{module.__struct__(), module.__info__(:struct)}
|
||||
@@ -636,9 +648,9 @@ defimpl Inspect, for: Any do
|
||||
end
|
||||
|
||||
def inspect(map, name, infos, opts) do
|
||||
open = color("#" <> name <> "<", :map, opts)
|
||||
sep = color(",", :map, opts)
|
||||
close = color(">", :map, opts)
|
||||
open = color_doc("#" <> name <> "<", :map, opts)
|
||||
sep = color_doc(",", :map, opts)
|
||||
close = color_doc(">", :map, opts)
|
||||
|
||||
fun = fn
|
||||
%{field: field}, opts -> Inspect.List.keyword({field, Map.get(map, field)}, opts)
|
||||
|
||||
@@ -333,6 +333,7 @@ defmodule Inspect.Algebra do
|
||||
defguardp is_width(limit) when limit == :infinity or (is_integer(limit) and limit >= 0)
|
||||
|
||||
# Elixir + Inspect.Opts conveniences
|
||||
# These have the _doc suffix.
|
||||
|
||||
@doc """
|
||||
Converts an Elixir term to an algebra document
|
||||
@@ -438,7 +439,7 @@ defmodule Inspect.Algebra do
|
||||
|
||||
"""
|
||||
@doc since: "1.6.0"
|
||||
@spec container_doc(t, [any], t, Inspect.Opts.t(), (term, Inspect.Opts.t() -> t), keyword()) ::
|
||||
@spec container_doc(t, [term], t, Inspect.Opts.t(), (term, Inspect.Opts.t() -> t), keyword()) ::
|
||||
t
|
||||
def container_doc(left, collection, right, inspect_opts, fun, opts \\ [])
|
||||
when is_doc(left) and is_list(collection) and is_doc(right) and is_function(fun, 2) and
|
||||
@@ -455,7 +456,7 @@ defmodule Inspect.Algebra do
|
||||
container_each(collection, inspect_opts.limit, inspect_opts, fun, [], break == :maybe)
|
||||
|
||||
flex? = simple? or break == :flex
|
||||
docs = fold_doc(docs, &join(&1, &2, flex?, separator))
|
||||
docs = fold(docs, &join(&1, &2, flex?, separator))
|
||||
|
||||
case flex? do
|
||||
true -> group(concat(concat(left, nest(docs, 1)), right))
|
||||
@@ -525,6 +526,32 @@ defmodule Inspect.Algebra do
|
||||
container_doc(left, docs, right, inspect, fun, separator: separator)
|
||||
end
|
||||
|
||||
# TODO: Deprecate me on Elixir v1.23
|
||||
@doc deprecated: "Use color_doc/3 instead"
|
||||
def color(doc, key, opts) do
|
||||
color_doc(doc, key, opts)
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
Colors a document if the `color_key` has a color in the options.
|
||||
"""
|
||||
@doc since: "1.18.0"
|
||||
@spec color_doc(t, Inspect.Opts.color_key(), Inspect.Opts.t()) :: t
|
||||
def color_doc(doc, color_key, %Inspect.Opts{syntax_colors: syntax_colors}) when is_doc(doc) do
|
||||
if precolor = Keyword.get(syntax_colors, color_key) do
|
||||
postcolor = Keyword.get(syntax_colors, :reset, :reset)
|
||||
concat(doc_color(doc, ansi(precolor)), doc_color(empty(), ansi(postcolor)))
|
||||
else
|
||||
doc
|
||||
end
|
||||
end
|
||||
|
||||
defp ansi(color) do
|
||||
color
|
||||
|> IO.ANSI.format_fragment(true)
|
||||
|> IO.iodata_to_binary()
|
||||
end
|
||||
|
||||
# Algebra API
|
||||
|
||||
@doc """
|
||||
@@ -588,6 +615,21 @@ defmodule Inspect.Algebra do
|
||||
doc_cons(doc1, doc2)
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
Disable any rendering limit while rendering the given document.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> doc = Inspect.Algebra.glue("hello", "world") |> Inspect.Algebra.group()
|
||||
iex> Inspect.Algebra.format(doc, 10)
|
||||
["hello", "\n", "world"]
|
||||
iex> doc = Inspect.Algebra.no_limit(doc)
|
||||
iex> Inspect.Algebra.format(doc, 10)
|
||||
["hello", " ", "world"]
|
||||
|
||||
"""
|
||||
@doc since: "1.14.0"
|
||||
@spec no_limit(t) :: t
|
||||
def no_limit(doc) do
|
||||
doc_limit(doc, :infinity)
|
||||
end
|
||||
@@ -604,21 +646,16 @@ defmodule Inspect.Algebra do
|
||||
"""
|
||||
@spec concat([t]) :: t
|
||||
def concat(docs) when is_list(docs) do
|
||||
fold_doc(docs, &concat(&1, &2))
|
||||
fold(docs, &concat(&1, &2))
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
Colors a document if the `color_key` has a color in the options.
|
||||
Colors a document with the given color (preceding the document itself).
|
||||
"""
|
||||
@doc since: "1.4.0"
|
||||
@spec color(t, Inspect.Opts.color_key(), Inspect.Opts.t()) :: t
|
||||
def color(doc, color_key, %Inspect.Opts{syntax_colors: syntax_colors}) when is_doc(doc) do
|
||||
if precolor = Keyword.get(syntax_colors, color_key) do
|
||||
postcolor = Keyword.get(syntax_colors, :reset, :reset)
|
||||
concat(doc_color(doc, precolor), doc_color(empty(), postcolor))
|
||||
else
|
||||
doc
|
||||
end
|
||||
@doc since: "1.18.0"
|
||||
@spec color(t, binary) :: t
|
||||
def color(doc, color) when is_doc(doc) and is_binary(color) do
|
||||
doc_color(doc, color)
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
@@ -918,6 +955,10 @@ defmodule Inspect.Algebra do
|
||||
@spec line(t, t) :: t
|
||||
def line(doc1, doc2), do: concat(doc1, concat(line(), doc2))
|
||||
|
||||
# TODO: Deprecate me on Elixir v1.23
|
||||
@doc deprecated: "Use fold/2 instead"
|
||||
def fold_doc(docs, folder_fun), do: fold(docs, folder_fun)
|
||||
|
||||
@doc ~S"""
|
||||
Folds a list of documents into a document using the given folder function.
|
||||
|
||||
@@ -929,21 +970,22 @@ defmodule Inspect.Algebra do
|
||||
|
||||
iex> docs = ["A", "B", "C"]
|
||||
iex> docs =
|
||||
...> Inspect.Algebra.fold_doc(docs, fn doc, acc ->
|
||||
...> Inspect.Algebra.fold(docs, fn doc, acc ->
|
||||
...> Inspect.Algebra.concat([doc, "!", acc])
|
||||
...> end)
|
||||
iex> Inspect.Algebra.format(docs, 80)
|
||||
["A", "!", "B", "!", "C"]
|
||||
|
||||
"""
|
||||
@spec fold_doc([t], (t, t -> t)) :: t
|
||||
def fold_doc(docs, folder_fun)
|
||||
@doc since: "1.18.0"
|
||||
@spec fold([t], (t, t -> t)) :: t
|
||||
def fold(docs, folder_fun)
|
||||
|
||||
def fold_doc([], _folder_fun), do: empty()
|
||||
def fold_doc([doc], _folder_fun), do: doc
|
||||
def fold([], _folder_fun), do: empty()
|
||||
def fold([doc], _folder_fun), do: doc
|
||||
|
||||
def fold_doc([doc | docs], folder_fun) when is_function(folder_fun, 2),
|
||||
do: folder_fun.(doc, fold_doc(docs, folder_fun))
|
||||
def fold([doc | docs], folder_fun) when is_function(folder_fun, 2),
|
||||
do: folder_fun.(doc, fold(docs, folder_fun))
|
||||
|
||||
@doc ~S"""
|
||||
Formats a given document for a given width.
|
||||
@@ -973,20 +1015,17 @@ defmodule Inspect.Algebra do
|
||||
#
|
||||
# * flat - represents a document with breaks as flats (a break may fit, as it may break)
|
||||
# * break - represents a document with breaks as breaks (a break always fits, since it breaks)
|
||||
#
|
||||
# The following modes are exclusive to fitting:
|
||||
#
|
||||
# * flat_no_break - represents a document with breaks as flat not allowed to enter in break mode
|
||||
# * break_no_flat - represents a document with breaks as breaks not allowed to enter in flat mode
|
||||
#
|
||||
@typep mode :: :flat | :flat_no_break | :break | :break_no_flat
|
||||
|
||||
@spec fits?(
|
||||
@spec fits(
|
||||
width :: non_neg_integer() | :infinity,
|
||||
column :: non_neg_integer(),
|
||||
break? :: boolean(),
|
||||
entries
|
||||
) :: boolean()
|
||||
) :: :fit | :no_fit | :break_next
|
||||
when entries:
|
||||
maybe_improper_list({integer(), mode(), t()}, {:tail, boolean(), entries} | [])
|
||||
|
||||
@@ -995,63 +1034,68 @@ defmodule Inspect.Algebra do
|
||||
#
|
||||
# In case we have groups and the group fits, we need to consider the group
|
||||
# parent without the child breaks, hence {:tail, b?, t} below.
|
||||
defp fits?(w, k, b?, _) when k > w and b?, do: false
|
||||
defp fits?(_, _, _, []), do: true
|
||||
defp fits?(w, k, _, {:tail, b?, t}), do: fits?(w, k, b?, t)
|
||||
defp fits(w, k, b?, _) when k > w and b?, do: :no_fit
|
||||
defp fits(_, _, _, []), do: :fit
|
||||
defp fits(w, k, _, {:tail, b?, t}), do: fits(w, k, b?, t)
|
||||
|
||||
## Flat no break
|
||||
|
||||
defp fits?(w, k, b?, [{i, _, doc_fits(x, :disabled)} | t]),
|
||||
do: fits?(w, k, b?, [{i, :flat_no_break, x} | t])
|
||||
defp fits(w, k, b?, [{i, _, doc_fits(x, :disabled)} | t]),
|
||||
do: fits(w, k, b?, [{i, :flat_no_break, x} | t])
|
||||
|
||||
defp fits?(w, k, b?, [{i, :flat_no_break, doc_fits(x, _)} | t]),
|
||||
do: fits?(w, k, b?, [{i, :flat_no_break, x} | t])
|
||||
defp fits(w, k, b?, [{i, :flat_no_break, doc_fits(x, _)} | t]),
|
||||
do: fits(w, k, b?, [{i, :flat_no_break, x} | t])
|
||||
|
||||
## Breaks no flat
|
||||
|
||||
defp fits?(w, k, b?, [{i, _, doc_fits(x, :enabled)} | t]),
|
||||
do: fits?(w, k, b?, [{i, :break_no_flat, x} | t])
|
||||
defp fits(w, k, b?, [{i, _, doc_fits(x, :enabled)} | t]),
|
||||
do: fits(w, k, b?, [{i, :break_no_flat, x} | t])
|
||||
|
||||
defp fits?(w, k, b?, [{i, :break_no_flat, doc_force(x)} | t]),
|
||||
do: fits?(w, k, b?, [{i, :break_no_flat, x} | t])
|
||||
defp fits(w, k, b?, [{i, :break_no_flat, doc_force(x)} | t]),
|
||||
do: fits(w, k, b?, [{i, :break_no_flat, x} | t])
|
||||
|
||||
defp fits?(_, _, _, [{_, :break_no_flat, doc_break(_, _)} | _]), do: true
|
||||
defp fits?(_, _, _, [{_, :break_no_flat, :doc_line} | _]), do: true
|
||||
defp fits(w, k, b?, [{i, :break_no_flat, x} | t])
|
||||
when x == :doc_line or (is_tuple(x) and elem(x, 0) == :doc_break) do
|
||||
case fits(w, k, b?, [{i, :flat, x} | t]) do
|
||||
:no_fit -> :break_next
|
||||
fits -> fits
|
||||
end
|
||||
end
|
||||
|
||||
## Breaks
|
||||
|
||||
defp fits?(_, _, _, [{_, :break, doc_break(_, _)} | _]), do: true
|
||||
defp fits?(_, _, _, [{_, :break, :doc_line} | _]), do: true
|
||||
defp fits(_, _, _, [{_, :break, doc_break(_, _)} | _]), do: :fit
|
||||
defp fits(_, _, _, [{_, :break, :doc_line} | _]), do: :fit
|
||||
|
||||
defp fits?(w, k, b?, [{i, :break, doc_group(x, _)} | t]),
|
||||
do: fits?(w, k, b?, [{i, :flat, x} | {:tail, b?, t}])
|
||||
defp fits(w, k, b?, [{i, :break, doc_group(x, _)} | t]),
|
||||
do: fits(w, k, b?, [{i, :flat, x} | {:tail, b?, t}])
|
||||
|
||||
## Catch all
|
||||
|
||||
defp fits?(w, _, _, [{i, _, :doc_line} | t]), do: fits?(w, i, false, t)
|
||||
defp fits?(w, k, b?, [{_, _, :doc_nil} | t]), do: fits?(w, k, b?, t)
|
||||
defp fits?(w, _, b?, [{i, _, doc_collapse(_)} | t]), do: fits?(w, i, b?, t)
|
||||
defp fits?(w, k, b?, [{i, m, doc_color(x, _)} | t]), do: fits?(w, k, b?, [{i, m, x} | t])
|
||||
defp fits?(w, k, b?, [{_, _, doc_string(_, l)} | t]), do: fits?(w, k + l, b?, t)
|
||||
defp fits?(w, k, b?, [{_, _, s} | t]) when is_binary(s), do: fits?(w, k + byte_size(s), b?, t)
|
||||
defp fits?(_, _, _, [{_, _, doc_force(_)} | _]), do: false
|
||||
defp fits?(w, k, _, [{_, _, doc_break(s, _)} | t]), do: fits?(w, k + byte_size(s), true, t)
|
||||
defp fits?(w, k, b?, [{i, m, doc_nest(x, _, :break)} | t]), do: fits?(w, k, b?, [{i, m, x} | t])
|
||||
defp fits(w, _, _, [{i, _, :doc_line} | t]), do: fits(w, i, false, t)
|
||||
defp fits(w, k, b?, [{_, _, :doc_nil} | t]), do: fits(w, k, b?, t)
|
||||
defp fits(w, _, b?, [{i, _, doc_collapse(_)} | t]), do: fits(w, i, b?, t)
|
||||
defp fits(w, k, b?, [{i, m, doc_color(x, _)} | t]), do: fits(w, k, b?, [{i, m, x} | t])
|
||||
defp fits(w, k, b?, [{_, _, doc_string(_, l)} | t]), do: fits(w, k + l, b?, t)
|
||||
defp fits(w, k, b?, [{_, _, s} | t]) when is_binary(s), do: fits(w, k + byte_size(s), b?, t)
|
||||
defp fits(_, _, _, [{_, _, doc_force(_)} | _]), do: :no_fit
|
||||
defp fits(w, k, _, [{_, _, doc_break(s, _)} | t]), do: fits(w, k + byte_size(s), true, t)
|
||||
defp fits(w, k, b?, [{i, m, doc_nest(x, _, :break)} | t]), do: fits(w, k, b?, [{i, m, x} | t])
|
||||
|
||||
defp fits?(w, k, b?, [{i, m, doc_nest(x, j, _)} | t]),
|
||||
do: fits?(w, k, b?, [{apply_nesting(i, k, j), m, x} | t])
|
||||
defp fits(w, k, b?, [{i, m, doc_nest(x, j, _)} | t]),
|
||||
do: fits(w, k, b?, [{apply_nesting(i, k, j), m, x} | t])
|
||||
|
||||
defp fits?(w, k, b?, [{i, m, doc_cons(x, y)} | t]),
|
||||
do: fits?(w, k, b?, [{i, m, x}, {i, m, y} | t])
|
||||
defp fits(w, k, b?, [{i, m, doc_cons(x, y)} | t]),
|
||||
do: fits(w, k, b?, [{i, m, x}, {i, m, y} | t])
|
||||
|
||||
defp fits?(w, k, b?, [{i, m, doc_group(x, _)} | t]),
|
||||
do: fits?(w, k, b?, [{i, m, x} | {:tail, b?, t}])
|
||||
defp fits(w, k, b?, [{i, m, doc_group(x, _)} | t]),
|
||||
do: fits(w, k, b?, [{i, m, x} | {:tail, b?, t}])
|
||||
|
||||
defp fits?(w, k, b?, [{i, m, doc_limit(x, :infinity)} | t]) when w != :infinity,
|
||||
do: fits?(:infinity, k, b?, [{i, :flat, x}, {i, m, doc_limit(empty(), w)} | t])
|
||||
defp fits(w, k, b?, [{i, m, doc_limit(x, :infinity)} | t]) when w != :infinity,
|
||||
do: fits(:infinity, k, b?, [{i, :flat, x}, {i, m, doc_limit(empty(), w)} | t])
|
||||
|
||||
defp fits?(_w, k, b?, [{i, m, doc_limit(x, w)} | t]),
|
||||
do: fits?(w, k, b?, [{i, m, x} | t])
|
||||
defp fits(_w, k, b?, [{i, m, doc_limit(x, w)} | t]),
|
||||
do: fits(w, k, b?, [{i, m, x} | t])
|
||||
|
||||
@spec format(
|
||||
width :: non_neg_integer() | :infinity,
|
||||
@@ -1062,10 +1106,14 @@ defmodule Inspect.Algebra do
|
||||
defp format(w, k, [{_, _, :doc_nil} | t]), do: format(w, k, t)
|
||||
defp format(w, _, [{i, _, :doc_line} | t]), do: [indent(i) | format(w, i, t)]
|
||||
defp format(w, k, [{i, m, doc_cons(x, y)} | t]), do: format(w, k, [{i, m, x}, {i, m, y} | t])
|
||||
defp format(w, k, [{i, m, doc_color(x, c)} | t]), do: [ansi(c) | format(w, k, [{i, m, x} | t])]
|
||||
defp format(w, k, [{i, m, doc_color(x, c)} | t]), do: [c | format(w, k, [{i, m, x} | t])]
|
||||
defp format(w, k, [{_, _, doc_string(s, l)} | t]), do: [s | format(w, k + l, t)]
|
||||
defp format(w, k, [{_, _, s} | t]) when is_binary(s), do: [s | format(w, k + byte_size(s), t)]
|
||||
defp format(w, k, [{i, m, doc_force(x)} | t]), do: format(w, k, [{i, m, x} | t])
|
||||
|
||||
defp format(w, k, [{i, :flat_no_break, doc_fits(x, :enabled)} | t]),
|
||||
do: format(w, k, [{i, :break_no_flat, x} | t])
|
||||
|
||||
defp format(w, k, [{i, m, doc_fits(x, _)} | t]), do: format(w, k, [{i, m, x} | t])
|
||||
defp format(w, _, [{i, _, doc_collapse(max)} | t]), do: collapse(format(w, i, t), max, 0, i)
|
||||
|
||||
@@ -1073,7 +1121,7 @@ defmodule Inspect.Algebra do
|
||||
defp format(w, k, [{i, m, doc_break(s, :flex)} | t]) do
|
||||
k = k + byte_size(s)
|
||||
|
||||
if w == :infinity or m == :flat or fits?(w, k, true, t) do
|
||||
if w == :infinity or m == :flat or fits(w, k, true, t) != :no_fit do
|
||||
[s | format(w, k, t)]
|
||||
else
|
||||
[indent(i) | format(w, i, t)]
|
||||
@@ -1103,11 +1151,17 @@ defmodule Inspect.Algebra do
|
||||
format(w, k, [{i, :break, x} | t])
|
||||
end
|
||||
|
||||
defp format(w, k, [{i, :break_no_flat, doc_group(x, _)} | t]) do
|
||||
format(w, k, [{i, :break, x} | t])
|
||||
end
|
||||
|
||||
defp format(w, k, [{i, _, doc_group(x, _)} | t]) do
|
||||
if w == :infinity or fits?(w, k, false, [{i, :flat, x}]) do
|
||||
format(w, k, [{i, :flat, x} | t])
|
||||
else
|
||||
format(w, k, [{i, :break, x} | t])
|
||||
fits = if w == :infinity, do: :fit, else: fits(w, k, false, [{i, :flat, x}])
|
||||
|
||||
case fits do
|
||||
:fit -> format(w, k, [{i, :flat, x} | t])
|
||||
:no_fit -> format(w, k, [{i, :break, x} | t])
|
||||
:break_next -> format(w, k, [{i, :flat_no_break, x} | t])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1136,10 +1190,6 @@ defmodule Inspect.Algebra do
|
||||
defp apply_nesting(_, _, :reset), do: 0
|
||||
defp apply_nesting(i, _, j), do: i + j
|
||||
|
||||
defp ansi(color) do
|
||||
IO.ANSI.format_fragment(color, true)
|
||||
end
|
||||
|
||||
defp indent(0), do: @newline
|
||||
defp indent(i), do: @newline <> :binary.copy(" ", i)
|
||||
end
|
||||
|
||||
@@ -189,13 +189,16 @@ defmodule Integer do
|
||||
@spec digits(integer, pos_integer) :: [integer, ...]
|
||||
def digits(integer, base \\ 10)
|
||||
when is_integer(integer) and is_integer(base) and base >= 2 do
|
||||
do_digits(integer, base, [])
|
||||
case integer do
|
||||
0 -> [0]
|
||||
_integer -> digits(integer, base, [])
|
||||
end
|
||||
end
|
||||
|
||||
defp do_digits(integer, base, acc) when abs(integer) < base, do: [integer | acc]
|
||||
defp digits(0, _base, acc), do: acc
|
||||
|
||||
defp do_digits(integer, base, acc),
|
||||
do: do_digits(div(integer, base), base, [rem(integer, base) | acc])
|
||||
defp digits(integer, base, acc),
|
||||
do: digits(div(integer, base), base, [rem(integer, base) | acc])
|
||||
|
||||
@doc """
|
||||
Returns the integer represented by the ordered `digits`.
|
||||
@@ -217,16 +220,16 @@ defmodule Integer do
|
||||
"""
|
||||
@spec undigits([integer], pos_integer) :: integer
|
||||
def undigits(digits, base \\ 10) when is_list(digits) and is_integer(base) and base >= 2 do
|
||||
do_undigits(digits, base, 0)
|
||||
undigits(digits, base, 0)
|
||||
end
|
||||
|
||||
defp do_undigits([], _base, acc), do: acc
|
||||
defp undigits([], _base, acc), do: acc
|
||||
|
||||
defp do_undigits([digit | _], base, _) when is_integer(digit) and digit >= base,
|
||||
defp undigits([digit | _], base, _) when is_integer(digit) and digit >= base,
|
||||
do: raise(ArgumentError, "invalid digit #{digit} in base #{base}")
|
||||
|
||||
defp do_undigits([digit | tail], base, acc) when is_integer(digit),
|
||||
do: do_undigits(tail, base, acc * base + digit)
|
||||
defp undigits([digit | tail], base, acc) when is_integer(digit),
|
||||
do: undigits(tail, base, acc * base + digit)
|
||||
|
||||
@doc """
|
||||
Parses a text representation of an integer.
|
||||
|
||||
+22
-8
@@ -12,6 +12,8 @@ defmodule IO do
|
||||
(as shown in typespecs). For more information on chardata, see the
|
||||
"IO data" section below.
|
||||
|
||||
The functions of this module use UNIX-style naming where possible.
|
||||
|
||||
## IO devices
|
||||
|
||||
An IO device may be an atom or a PID. In case it is an atom,
|
||||
@@ -131,9 +133,11 @@ defmodule IO do
|
||||
The `device` is iterated as specified by the `line_or_chars` argument:
|
||||
|
||||
* if `line_or_chars` is an integer, it represents a number of bytes. The device is
|
||||
iterated by that number of bytes.
|
||||
iterated by that number of bytes. This should be the preferred mode for reading
|
||||
non-textual inputs.
|
||||
|
||||
* if `line_or_chars` is `:line`, the device is iterated line by line.
|
||||
CRFL newlines ("\r\n") are automatically normalized to "\n".
|
||||
|
||||
* if `line_or_chars` is `:eof` (since v1.13), the device is iterated until `:eof`.
|
||||
If the device is already at the end, it returns `:eof` itself.
|
||||
@@ -184,9 +188,11 @@ defmodule IO do
|
||||
The `device` is iterated as specified by the `line_or_chars` argument:
|
||||
|
||||
* if `line_or_chars` is an integer, it represents a number of bytes. The device is
|
||||
iterated by that number of bytes.
|
||||
iterated by that number of bytes. This should be the preferred mode for reading
|
||||
non-textual inputs.
|
||||
|
||||
* if `line_or_chars` is `:line`, the device is iterated line by line.
|
||||
CRFL newlines ("\r\n") are automatically normalized to "\n".
|
||||
|
||||
* if `line_or_chars` is `:eof` (since v1.13), the device is iterated until `:eof`.
|
||||
If the device is already at the end, it returns `:eof` itself.
|
||||
@@ -290,6 +296,8 @@ defmodule IO do
|
||||
By default, the `device` is the standard output. It returns `:ok`
|
||||
if it succeeds.
|
||||
|
||||
Trivia: `puts` is shorthand for `put string`.
|
||||
|
||||
## Examples
|
||||
|
||||
IO.puts("Hello World!")
|
||||
@@ -565,6 +573,8 @@ defmodule IO do
|
||||
for instance, `{:error, :estale}` if reading from an
|
||||
NFS volume
|
||||
|
||||
Trivia: `gets` is shorthand for `get string`.
|
||||
|
||||
## Examples
|
||||
|
||||
To display "What is your name?" as a prompt and await user input:
|
||||
@@ -596,8 +606,9 @@ defmodule IO do
|
||||
`Collectable`, allowing it to be used for both read
|
||||
and write.
|
||||
|
||||
The `device` is iterated by the given number of characters or line by line if
|
||||
`:line` is given.
|
||||
The `device` is iterated by the given number of characters
|
||||
or line by line if `:line` is given. In case `:line` is given,
|
||||
"\r\n" is automatically normalized to "\n".
|
||||
|
||||
This reads from the IO as UTF-8. Check out
|
||||
`IO.binstream/2` to handle the IO as a raw binary.
|
||||
@@ -650,14 +661,17 @@ defmodule IO do
|
||||
`Collectable`, allowing it to be used for both read
|
||||
and write.
|
||||
|
||||
The `device` is iterated by the given number of bytes or line by line if
|
||||
`:line` is given. This reads from the IO device as a raw binary.
|
||||
The `device` is iterated by the given number of bytes or line
|
||||
by line if `:line` is given. In case `:line` is given, "\r\n"
|
||||
is automatically normalized to "\n". Passing the number of bytes
|
||||
should be the preferred mode for reading non-textual inputs.
|
||||
|
||||
Note that an IO stream has side effects and every time
|
||||
you go over the stream you may get different results.
|
||||
|
||||
Finally, do not use this function on IO devices in Unicode
|
||||
mode as it will return the wrong result.
|
||||
This reads from the IO device as a raw binary. Therefore,
|
||||
do not use this function on IO devices in Unicode mode as
|
||||
it will return the wrong result.
|
||||
|
||||
`binstream/0` has been introduced in Elixir v1.12.0,
|
||||
while `binstream/2` has been available since v1.0.0.
|
||||
|
||||
@@ -350,7 +350,7 @@ defmodule IO.ANSI.Docs do
|
||||
|> String.split(@spaces)
|
||||
|> write_with_wrap(options[:width] - byte_size(indent), indent, no_wrap, prefix)
|
||||
|
||||
unless no_wrap, do: newline_after_block(options)
|
||||
if !no_wrap, do: newline_after_block(options)
|
||||
end
|
||||
|
||||
defp format_text(text, options) do
|
||||
|
||||
@@ -0,0 +1,410 @@
|
||||
defprotocol JSON.Encoder do
|
||||
@moduledoc """
|
||||
A protocol for custom JSON encoding of data structures.
|
||||
|
||||
If you have a struct, you can derive the implementation of this protocol
|
||||
by specifying which fields should be encoded to JSON:
|
||||
|
||||
@derive {JSON.Encoder, only: [....]}
|
||||
defstruct ...
|
||||
|
||||
It is also possible to encode all fields or skip some fields via the
|
||||
`:except` option:
|
||||
|
||||
@derive JSON.Encoder
|
||||
defstruct ...
|
||||
|
||||
> #### Leaking Private Information {: .error}
|
||||
>
|
||||
> The `:except` approach should be used carefully to avoid
|
||||
> accidentally leaking private information when new fields are added.
|
||||
|
||||
Finally, if you don't own the struct you want to encode to JSON,
|
||||
you may use `Protocol.derive/3` placed outside of any module:
|
||||
|
||||
Protocol.derive(JSON.Encoder, NameOfTheStruct, only: [...])
|
||||
Protocol.derive(JSON.Encoder, NameOfTheStruct)
|
||||
|
||||
"""
|
||||
|
||||
@undefined_impl_description """
|
||||
the protocol must be explicitly implemented.
|
||||
|
||||
If you have a struct, you can derive the implementation specifying \
|
||||
which fields should be encoded to JSON:
|
||||
|
||||
@derive {JSON.Encoder, only: [....]}
|
||||
defstruct ...
|
||||
|
||||
It is also possible to encode all fields, although this should be \
|
||||
used carefully to avoid accidentally leaking private information \
|
||||
when new fields are added:
|
||||
|
||||
@derive JSON.Encoder
|
||||
defstruct ...
|
||||
|
||||
Finally, if you don't own the struct you want to encode to JSON, \
|
||||
you may use Protocol.derive/3 placed outside of any module:
|
||||
|
||||
Protocol.derive(JSON.Encoder, NameOfTheStruct, only: [...])
|
||||
Protocol.derive(JSON.Encoder, NameOfTheStruct)\
|
||||
"""
|
||||
|
||||
@impl true
|
||||
defmacro __deriving__(module, opts) do
|
||||
fields = module |> Macro.struct_info!(__CALLER__) |> Enum.map(& &1.field)
|
||||
fields = fields_to_encode(fields, opts)
|
||||
vars = Macro.generate_arguments(length(fields), __MODULE__)
|
||||
kv = Enum.zip(fields, vars)
|
||||
|
||||
{io, _prefix} =
|
||||
Enum.flat_map_reduce(kv, ?{, fn {field, value}, prefix ->
|
||||
key = IO.iodata_to_binary([prefix, :elixir_json.encode_binary(Atom.to_string(field)), ?:])
|
||||
{[key, quote(do: encoder.(unquote(value), encoder))], ?,}
|
||||
end)
|
||||
|
||||
io = if io == [], do: "{}", else: io ++ [?}]
|
||||
|
||||
quote do
|
||||
defimpl JSON.Encoder, for: unquote(module) do
|
||||
def encode(%{unquote_splicing(kv)}, encoder) do
|
||||
unquote(io)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp fields_to_encode(fields, opts) do
|
||||
cond do
|
||||
only = Keyword.get(opts, :only) ->
|
||||
case only -- fields do
|
||||
[] ->
|
||||
only
|
||||
|
||||
error_keys ->
|
||||
raise ArgumentError,
|
||||
"unknown struct fields #{inspect(error_keys)} specified in :only. Expected one of: " <>
|
||||
"#{inspect(fields -- [:__struct__])}"
|
||||
end
|
||||
|
||||
except = Keyword.get(opts, :except) ->
|
||||
case except -- fields do
|
||||
[] ->
|
||||
fields -- [:__struct__ | except]
|
||||
|
||||
error_keys ->
|
||||
raise ArgumentError,
|
||||
"unknown struct fields #{inspect(error_keys)} specified in :except. Expected one of: " <>
|
||||
"#{inspect(fields -- [:__struct__])}"
|
||||
end
|
||||
|
||||
true ->
|
||||
fields -- [:__struct__]
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
A function invoked to encode the given term to `t:iodata/0`.
|
||||
"""
|
||||
def encode(term, encoder)
|
||||
end
|
||||
|
||||
defimpl JSON.Encoder, for: Atom do
|
||||
def encode(value, encoder) do
|
||||
case value do
|
||||
nil -> "null"
|
||||
true -> "true"
|
||||
false -> "false"
|
||||
_ -> encoder.(Atom.to_string(value), encoder)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defimpl JSON.Encoder, for: BitString do
|
||||
def encode(value, _encoder) do
|
||||
:elixir_json.encode_binary(value)
|
||||
end
|
||||
end
|
||||
|
||||
defimpl JSON.Encoder, for: List do
|
||||
def encode(value, encoder) do
|
||||
:elixir_json.encode_list(value, encoder)
|
||||
end
|
||||
end
|
||||
|
||||
defimpl JSON.Encoder, for: Integer do
|
||||
def encode(value, _encoder) do
|
||||
:elixir_json.encode_integer(value)
|
||||
end
|
||||
end
|
||||
|
||||
defimpl JSON.Encoder, for: Float do
|
||||
def encode(value, _encoder) do
|
||||
:elixir_json.encode_float(value)
|
||||
end
|
||||
end
|
||||
|
||||
defimpl JSON.Encoder, for: Map do
|
||||
def encode(value, encoder) do
|
||||
:elixir_json.encode_map(value, encoder)
|
||||
end
|
||||
end
|
||||
|
||||
defimpl JSON.Encoder, for: [Date, Time, NaiveDateTime, DateTime, Duration] do
|
||||
def encode(value, _encoder) do
|
||||
[?", @for.to_iso8601(value), ?"]
|
||||
end
|
||||
end
|
||||
|
||||
defmodule JSON.DecodeError do
|
||||
@moduledoc """
|
||||
The exception raised by `JSON.decode!/1`.
|
||||
"""
|
||||
defexception [:message, :offset, :data]
|
||||
end
|
||||
|
||||
defmodule JSON do
|
||||
@moduledoc ~S"""
|
||||
JSON encoding and decoding.
|
||||
|
||||
Both encoder and decoder fully conform to [RFC 8259](https://tools.ietf.org/html/rfc8259) and
|
||||
[ECMA 404](https://ecma-international.org/publications-and-standards/standards/ecma-404/)
|
||||
standards.
|
||||
|
||||
## Encoding
|
||||
|
||||
Elixir built-in data structures are encoded to JSON as follows:
|
||||
|
||||
| **Elixir** | **JSON** |
|
||||
|------------------------|----------|
|
||||
| `integer() \| float()` | Number |
|
||||
| `true \| false ` | Boolean |
|
||||
| `nil` | Null |
|
||||
| `binary()` | String |
|
||||
| `atom()` | String |
|
||||
| `list()` | Array |
|
||||
| `%{binary() => _}` | Object |
|
||||
| `%{atom() => _}` | Object |
|
||||
| `%{integer() => _}` | Object |
|
||||
|
||||
You may also implement the `JSON.Encoder` protocol for custom data structures.
|
||||
|
||||
## Decoding
|
||||
|
||||
Elixir built-in data structures are decoded from JSON as follows:
|
||||
|
||||
| **JSON** | **Elixir** |
|
||||
|----------|------------------------|
|
||||
| Number | `integer() \| float()` |
|
||||
| Boolean | `true \| false` |
|
||||
| Null | `nil` |
|
||||
| String | `binary()` |
|
||||
| Object | `%{binary() => _}` |
|
||||
|
||||
"""
|
||||
|
||||
@moduledoc since: "1.18.0"
|
||||
|
||||
@type encoder :: (term(), encoder() -> iodata())
|
||||
|
||||
@type decode_error_reason ::
|
||||
{:unexpected_end, non_neg_integer()}
|
||||
| {:invalid_byte, non_neg_integer(), byte()}
|
||||
| {:unexpected_sequence, non_neg_integer(), binary()}
|
||||
|
||||
@doc ~S"""
|
||||
Decodes the given JSON.
|
||||
|
||||
Returns `{:ok, decoded}` or `{:error, reason}`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> JSON.decode("[null,123,\"string\",{\"key\":\"value\"}]")
|
||||
{:ok, [nil, 123, "string", %{"key" => "value"}]}
|
||||
|
||||
## Error reasons
|
||||
|
||||
The error tuple will have one of the following reasons.
|
||||
|
||||
* `{:unexpected_end, offset}` if `binary` contains incomplete JSON value
|
||||
* `{:invalid_byte, offset, byte}` if `binary` contains unexpected byte or invalid UTF-8 byte
|
||||
* `{:unexpected_sequence, offset, bytes}` if `binary` contains invalid UTF-8 escape
|
||||
"""
|
||||
@spec decode(binary()) :: {:ok, term()} | {:error, decode_error_reason()}
|
||||
def decode(binary) when is_binary(binary) do
|
||||
with {decoded, :ok, rest} <- decode(binary, :ok, []) do
|
||||
if rest == "" do
|
||||
{:ok, decoded}
|
||||
else
|
||||
{:error, {:invalid_byte, byte_size(binary) - byte_size(rest), :binary.at(rest, 0)}}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
Decodes the given JSON with the given decoders.
|
||||
|
||||
Returns `{decoded, acc, rest}` or `{:error, reason}`.
|
||||
See `decode/1` for the error reasons.
|
||||
|
||||
## Decoders
|
||||
|
||||
All decoders are optional. If not provided, they will fall back to
|
||||
implementations used by the `decode/1` function:
|
||||
|
||||
* for `array_start`: `fn _ -> [] end`
|
||||
* for `array_push`: `fn elem, acc -> [elem | acc] end`
|
||||
* for `array_finish`: `fn acc, old_acc -> {Enum.reverse(acc), old_acc} end`
|
||||
* for `object_start`: `fn _ -> [] end`
|
||||
* for `object_push`: `fn key, value, acc -> [{key, value} | acc] end`
|
||||
* for `object_finish`: `fn acc, old_acc -> {Map.new(acc), old_acc} end`
|
||||
* for `float`: `&String.to_float/1`
|
||||
* for `integer`: `&String.to_integer/1`
|
||||
* for `string`: `&Function.identity/1`
|
||||
* for `null`: the atom `nil`
|
||||
|
||||
For streaming decoding, see Erlang's `:json` module.
|
||||
"""
|
||||
@spec decode(binary(), term(), keyword()) ::
|
||||
{term(), term(), binary()} | {:error, decode_error_reason()}
|
||||
def decode(binary, acc, decoders) when is_binary(binary) and is_list(decoders) do
|
||||
decoders = Keyword.put_new(decoders, :null, nil)
|
||||
|
||||
try do
|
||||
:elixir_json.decode(binary, acc, Map.new(decoders))
|
||||
catch
|
||||
:error, :unexpected_end ->
|
||||
{:error, {:unexpected_end, byte_size(binary)}}
|
||||
|
||||
:error, {:invalid_byte, byte} ->
|
||||
{:error, {:invalid_byte, offset(__STACKTRACE__), byte}}
|
||||
|
||||
:error, {:unexpected_sequence, bytes} ->
|
||||
{:error, {:unexpected_sequence, offset(__STACKTRACE__), bytes}}
|
||||
end
|
||||
end
|
||||
|
||||
defp offset(stacktrace) do
|
||||
with [{_, _, _, opts} | _] <- stacktrace,
|
||||
%{cause: %{position: position}} <- opts[:error_info] do
|
||||
position
|
||||
else
|
||||
_ -> 0
|
||||
end
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
Decodes the given JSON but raises an exception in case of errors.
|
||||
|
||||
Returns the decoded content. See `decode/1` for possible errors.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> JSON.decode!("[null,123,\"string\",{\"key\":\"value\"}]")
|
||||
[nil, 123, "string", %{"key" => "value"}]
|
||||
"""
|
||||
@spec decode!(binary()) :: term()
|
||||
def decode!(binary) when is_binary(binary) do
|
||||
case decode(binary) do
|
||||
{:ok, decoded} ->
|
||||
decoded
|
||||
|
||||
{:error, {:unexpected_end, offset}} ->
|
||||
raise JSON.DecodeError,
|
||||
message: "unexpected end of JSON binary at position (byte offset) #{offset}",
|
||||
data: binary,
|
||||
offset: offset
|
||||
|
||||
{:error, {:invalid_byte, offset, byte}} ->
|
||||
raise JSON.DecodeError,
|
||||
message: "invalid byte #{byte} at position (byte offset) #{offset}",
|
||||
data: binary,
|
||||
offset: offset
|
||||
|
||||
{:error, {:unexpected_sequence, offset, bytes}} ->
|
||||
raise JSON.DecodeError,
|
||||
message: "unexpected sequence #{inspect(bytes)} at position (byte offset) #{offset}",
|
||||
data: binary,
|
||||
offset: offset
|
||||
end
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
Encodes the given term to JSON as a binary.
|
||||
|
||||
The second argument is a function that is recursively
|
||||
invoked to encode a term.
|
||||
|
||||
> #### IO and performance {: .tip}
|
||||
>
|
||||
> If you need to encode data to be sent over the network
|
||||
> or written to the filesystem, consider using the more
|
||||
> efficient `encode_to_iodata!/2`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> JSON.encode!([123, "string", %{key: "value"}])
|
||||
"[123,\"string\",{\"key\":\"value\"}]"
|
||||
|
||||
"""
|
||||
@spec encode!(term(), encoder()) :: binary()
|
||||
def encode!(term, encoder \\ &protocol_encode/2) do
|
||||
IO.iodata_to_binary(encoder.(term, encoder))
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
Encodes the given term to JSON as an iodata.
|
||||
|
||||
This is the most efficient format if the JSON is going to be
|
||||
used for IO purposes.
|
||||
|
||||
The second argument is a function that is recursively
|
||||
invoked to encode a term.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> data = JSON.encode_to_iodata!([123, "string", %{key: "value"}])
|
||||
iex> IO.iodata_to_binary(data)
|
||||
"[123,\"string\",{\"key\":\"value\"}]"
|
||||
|
||||
"""
|
||||
@spec encode_to_iodata!(term(), encoder()) :: iodata()
|
||||
def encode_to_iodata!(term, encoder \\ &protocol_encode/2) do
|
||||
encoder.(term, encoder)
|
||||
end
|
||||
|
||||
@doc """
|
||||
This is the default encode implementation passed to `encode!/1`.
|
||||
|
||||
This function is most typically passed as second argument to
|
||||
`encode!/2` and `encode_to_iodata!/2`. The default implementation
|
||||
is an optimized dispatch to the `JSON.Encoder` protocol.
|
||||
"""
|
||||
@spec protocol_encode(term(), encoder()) :: iodata()
|
||||
def protocol_encode(value, encoder) when is_atom(value) do
|
||||
case value do
|
||||
nil -> "null"
|
||||
true -> "true"
|
||||
false -> "false"
|
||||
_ -> encoder.(Atom.to_string(value), encoder)
|
||||
end
|
||||
end
|
||||
|
||||
def protocol_encode(value, _encoder) when is_binary(value),
|
||||
do: :elixir_json.encode_binary(value)
|
||||
|
||||
def protocol_encode(value, _encoder) when is_integer(value),
|
||||
do: :elixir_json.encode_integer(value)
|
||||
|
||||
def protocol_encode(value, _encoder) when is_float(value),
|
||||
do: :elixir_json.encode_float(value)
|
||||
|
||||
def protocol_encode(value, encoder) when is_list(value),
|
||||
do: :elixir_json.encode_list(value, encoder)
|
||||
|
||||
def protocol_encode(%{} = value, encoder) when not is_map_key(value, :__struct__),
|
||||
do: :elixir_json.encode_map(value, encoder)
|
||||
|
||||
def protocol_encode(value, encoder),
|
||||
do: JSON.Encoder.encode(value, encoder)
|
||||
end
|
||||
+125
-111
@@ -238,7 +238,7 @@ defmodule Kernel do
|
||||
When comparing two numbers of different types (a number being either
|
||||
an integer or a float), a conversion to the type with greater precision
|
||||
will always occur, unless the comparison operator used is either `===/2`
|
||||
or `!==`. A float will be considered more precise than an integer, unless
|
||||
or `!==/2`. A float will be considered more precise than an integer, unless
|
||||
the float is greater/less than +/-9007199254740992.0 respectively,
|
||||
at which point all the significant figures of the float are to the left
|
||||
of the decimal point. This behavior exists so that the comparison of large
|
||||
@@ -247,9 +247,7 @@ defmodule Kernel do
|
||||
The collection types are compared using the following rules:
|
||||
|
||||
* Tuples are compared by size, then element by element.
|
||||
* Maps are compared by size, then by keys in ascending term order,
|
||||
then by values in key order. In the specific case of maps' key
|
||||
ordering, integers are always considered to be less than floats.
|
||||
* Maps are compared by size, then by key-value pairs.
|
||||
* Lists are compared element by element.
|
||||
* Bitstrings are compared byte by byte, incomplete bytes are compared bit by bit.
|
||||
* Atoms are compared using their string value, codepoint by codepoint.
|
||||
@@ -302,7 +300,7 @@ defmodule Kernel do
|
||||
@compile {:inline, bootstrapped?: 1}
|
||||
case :code.ensure_loaded(Kernel) do
|
||||
{:module, _} ->
|
||||
defp bootstrapped?(_), do: true
|
||||
defp bootstrapped?(module), do: is_atom(module)
|
||||
|
||||
{:error, _} ->
|
||||
defp bootstrapped?(module), do: :code.ensure_loaded(module) == {:module, module}
|
||||
@@ -630,7 +628,7 @@ defmodule Kernel do
|
||||
|
||||
"""
|
||||
@doc guard: true
|
||||
@spec hd(nonempty_maybe_improper_list(elem, any)) :: elem when elem: term
|
||||
@spec hd(nonempty_maybe_improper_list(elem, term)) :: elem when elem: term
|
||||
def hd(list) do
|
||||
:erlang.hd(list)
|
||||
end
|
||||
@@ -1598,7 +1596,7 @@ defmodule Kernel do
|
||||
|
||||
This function is optimized so the complexity of `a -- b` is proportional
|
||||
to `length(a) * log(length(b))`. See also the [Erlang efficiency
|
||||
guide](https://www.erlang.org/doc/efficiency_guide/retired_myths.html).
|
||||
guide](https://www.erlang.org/doc/system/efficiency_guide.html).
|
||||
|
||||
Inlined by the compiler.
|
||||
|
||||
@@ -1905,8 +1903,8 @@ defmodule Kernel do
|
||||
|
||||
## Implemented in Elixir
|
||||
|
||||
defp optimize_boolean({:case, meta, args}) do
|
||||
{:case, [{:optimize_boolean, true} | meta], args}
|
||||
defp annotate_case(extra, {:case, meta, args}) do
|
||||
{:case, extra ++ meta, args}
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -1973,7 +1971,8 @@ defmodule Kernel do
|
||||
end
|
||||
|
||||
defp build_boolean_check(operator, check, true_clause, false_clause) do
|
||||
optimize_boolean(
|
||||
annotate_case(
|
||||
[optimize_boolean: true, type_check: :expr],
|
||||
quote do
|
||||
case unquote(check) do
|
||||
false -> unquote(false_clause)
|
||||
@@ -2006,7 +2005,8 @@ defmodule Kernel do
|
||||
defmacro !{:!, _, [value]} do
|
||||
assert_no_match_or_guard_scope(__CALLER__.context, "!")
|
||||
|
||||
optimize_boolean(
|
||||
annotate_case(
|
||||
[optimize_boolean: true, type_check: :expr],
|
||||
quote do
|
||||
case unquote(value) do
|
||||
x when :"Elixir.Kernel".in(x, [false, nil]) -> false
|
||||
@@ -2019,7 +2019,8 @@ defmodule Kernel do
|
||||
defmacro !value do
|
||||
assert_no_match_or_guard_scope(__CALLER__.context, "!")
|
||||
|
||||
optimize_boolean(
|
||||
annotate_case(
|
||||
[optimize_boolean: true, type_check: :expr],
|
||||
quote do
|
||||
case unquote(value) do
|
||||
x when :"Elixir.Kernel".in(x, [false, nil]) -> true
|
||||
@@ -2217,28 +2218,22 @@ defmodule Kernel do
|
||||
case Macro.expand(message, __CALLER__) do
|
||||
message when is_binary(message) ->
|
||||
quote do
|
||||
:erlang.error(
|
||||
:erlang.raise(:error, RuntimeError.exception(unquote(message)), unquote(stacktrace))
|
||||
)
|
||||
:erlang.raise(:error, RuntimeError.exception(unquote(message)), unquote(stacktrace))
|
||||
end
|
||||
|
||||
{:<<>>, _, _} = message ->
|
||||
quote do
|
||||
:erlang.error(
|
||||
:erlang.raise(:error, RuntimeError.exception(unquote(message)), unquote(stacktrace))
|
||||
)
|
||||
:erlang.raise(:error, RuntimeError.exception(unquote(message)), unquote(stacktrace))
|
||||
end
|
||||
|
||||
alias when is_atom(alias) ->
|
||||
quote do
|
||||
:erlang.error(:erlang.raise(:error, unquote(alias).exception([]), unquote(stacktrace)))
|
||||
:erlang.raise(:error, unquote(alias).exception([]), unquote(stacktrace))
|
||||
end
|
||||
|
||||
message ->
|
||||
quote do
|
||||
:erlang.error(
|
||||
:erlang.raise(:error, Kernel.Utils.raise(unquote(message)), unquote(stacktrace))
|
||||
)
|
||||
:erlang.raise(:error, Kernel.Utils.raise(unquote(message)), unquote(stacktrace))
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -2452,8 +2447,6 @@ defmodule Kernel do
|
||||
* when updating a struct, as in `struct!(%SomeStruct{}, key: :value)`,
|
||||
it is equivalent to `%SomeStruct{struct | key: :value}` and therefore this
|
||||
function will check if every given key-value belongs to the struct.
|
||||
However, updating structs does not enforce keys, as keys are enforced
|
||||
only when building;
|
||||
|
||||
"""
|
||||
@spec struct!(module | struct, Enumerable.t()) :: struct
|
||||
@@ -2973,7 +2966,7 @@ defmodule Kernel do
|
||||
(term | nil -> {current_value, new_value} | :pop)
|
||||
) :: {current_value, new_structure :: structure}
|
||||
when structure: Access.t(),
|
||||
keys: nonempty_list(any),
|
||||
keys: nonempty_list(term),
|
||||
current_value: Access.value(),
|
||||
new_value: Access.value()
|
||||
def get_and_update_in(data, keys, fun)
|
||||
@@ -3313,7 +3306,7 @@ defmodule Kernel do
|
||||
end
|
||||
|
||||
defp nest_pop_in(:map, h, [{:access, key}]) do
|
||||
quote do
|
||||
quote generated: true do
|
||||
case unquote(h) do
|
||||
nil -> {nil, nil}
|
||||
h -> Access.pop(h, unquote(key))
|
||||
@@ -3334,7 +3327,7 @@ defmodule Kernel do
|
||||
end
|
||||
|
||||
defp nest_pop_in(_, h, [{:access, key}]) do
|
||||
quote do
|
||||
quote generated: true do
|
||||
case unquote(h) do
|
||||
nil -> :pop
|
||||
h -> Access.pop(h, unquote(key))
|
||||
@@ -3424,7 +3417,7 @@ defmodule Kernel do
|
||||
|
||||
## Examples
|
||||
|
||||
iex> is_nil(1)
|
||||
iex> is_nil(1 + 2)
|
||||
false
|
||||
|
||||
iex> is_nil(nil)
|
||||
@@ -3649,7 +3642,7 @@ defmodule Kernel do
|
||||
:ok
|
||||
|
||||
true ->
|
||||
pos = :elixir_locals.cache_env(__CALLER__)
|
||||
pos = :elixir_module.cache_env(__CALLER__)
|
||||
%{line: line, file: file, module: module} = __CALLER__
|
||||
|
||||
quote do
|
||||
@@ -3916,7 +3909,8 @@ defmodule Kernel do
|
||||
end
|
||||
|
||||
defp build_if(condition, do: do_clause, else: else_clause) do
|
||||
optimize_boolean(
|
||||
annotate_case(
|
||||
[optimize_boolean: true, type_check: :expr],
|
||||
quote do
|
||||
case unquote(condition) do
|
||||
x when :"Elixir.Kernel".in(x, [false, nil]) -> unquote(else_clause)
|
||||
@@ -3956,6 +3950,8 @@ defmodule Kernel do
|
||||
"Math still works"
|
||||
|
||||
"""
|
||||
# TODO: Deprecate this on Elixir v1.22.
|
||||
@doc deprecated: "Use if/2 instead"
|
||||
defmacro unless(condition, clauses) do
|
||||
build_unless(condition, clauses)
|
||||
end
|
||||
@@ -4070,7 +4066,7 @@ defmodule Kernel do
|
||||
end
|
||||
|
||||
defp stepless_range(nil, first, last, _caller) do
|
||||
quote(do: Elixir.Range.new(unquote(first), unquote(last)))
|
||||
quote(do: Function.identity(Elixir.Range.new(unquote(first), unquote(last))))
|
||||
end
|
||||
|
||||
defp stepless_range(:guard, first, last, caller) do
|
||||
@@ -4232,15 +4228,18 @@ defmodule Kernel do
|
||||
defmacro left && right do
|
||||
assert_no_match_or_guard_scope(__CALLER__.context, "&&")
|
||||
|
||||
quote do
|
||||
case unquote(left) do
|
||||
x when :"Elixir.Kernel".in(x, [false, nil]) ->
|
||||
x
|
||||
annotate_case(
|
||||
[type_check: :expr],
|
||||
quote do
|
||||
case unquote(left) do
|
||||
x when :"Elixir.Kernel".in(x, [false, nil]) ->
|
||||
x
|
||||
|
||||
_ ->
|
||||
unquote(right)
|
||||
_ ->
|
||||
unquote(right)
|
||||
end
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -4272,15 +4271,18 @@ defmodule Kernel do
|
||||
defmacro left || right do
|
||||
assert_no_match_or_guard_scope(__CALLER__.context, "||")
|
||||
|
||||
quote do
|
||||
case unquote(left) do
|
||||
x when :"Elixir.Kernel".in(x, [false, nil]) ->
|
||||
unquote(right)
|
||||
annotate_case(
|
||||
[type_check: :expr],
|
||||
quote do
|
||||
case unquote(left) do
|
||||
x when :"Elixir.Kernel".in(x, [false, nil]) ->
|
||||
unquote(right)
|
||||
|
||||
x ->
|
||||
x
|
||||
x ->
|
||||
x
|
||||
end
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -4582,6 +4584,9 @@ defmodule Kernel do
|
||||
defp in_var(true, {atom, _, context} = var, fun) when is_atom(atom) and is_atom(context),
|
||||
do: fun.(var)
|
||||
|
||||
defp in_var(true, var, fun) when is_atom(var) or is_binary(var) or is_number(var),
|
||||
do: fun.(var)
|
||||
|
||||
defp in_var(true, ast, fun) do
|
||||
quote do
|
||||
var = unquote(ast)
|
||||
@@ -5008,12 +5013,14 @@ defmodule Kernel do
|
||||
assert_no_match_or_guard_scope(env.context, "defmodule/2")
|
||||
expanded = expand_module_alias(alias, env)
|
||||
|
||||
module_meta = module_meta(alias)
|
||||
|
||||
{expanded, with_alias} =
|
||||
case is_atom(expanded) do
|
||||
true ->
|
||||
{full, old, opts} = alias_defmodule(alias, expanded, env)
|
||||
# Expand the module considering the current environment/nesting
|
||||
meta = [defined: full] ++ alias_meta(alias)
|
||||
meta = [defined: full] ++ module_meta
|
||||
{full, {:require, meta, [old, opts]}}
|
||||
|
||||
false ->
|
||||
@@ -5045,6 +5052,7 @@ defmodule Kernel do
|
||||
unquote(with_alias)
|
||||
|
||||
:elixir_module.compile(
|
||||
unquote(module_meta),
|
||||
unquote(expanded),
|
||||
unquote(escaped),
|
||||
unquote(module_vars),
|
||||
@@ -5054,8 +5062,8 @@ defmodule Kernel do
|
||||
end
|
||||
end
|
||||
|
||||
defp alias_meta({:__aliases__, meta, _}), do: meta
|
||||
defp alias_meta(_), do: []
|
||||
defp module_meta({_, meta, _}), do: meta
|
||||
defp module_meta(_), do: []
|
||||
|
||||
# We don't want to trace :alias_reference since we are defining the alias
|
||||
defp expand_module_alias({:__aliases__, meta, list} = alias, env) do
|
||||
@@ -5314,7 +5322,7 @@ defmodule Kernel do
|
||||
key
|
||||
end
|
||||
|
||||
pos = :elixir_locals.cache_env(env)
|
||||
pos = :elixir_module.cache_env(env)
|
||||
|
||||
quote do
|
||||
:elixir_def.store_definition(unquote(kind), unquote(store), unquote(pos))
|
||||
@@ -5326,13 +5334,10 @@ defmodule Kernel do
|
||||
|
||||
A struct is a tagged map that allows developers to provide
|
||||
default values for keys, tags to be used in polymorphic
|
||||
dispatches and compile time assertions. For more information
|
||||
about structs, please check `%/2`.
|
||||
dispatches and compile time assertions.
|
||||
|
||||
It is only possible to define a struct per module, as the
|
||||
struct is tied to the module itself. Calling `defstruct/1`
|
||||
also defines a `__struct__/0` function that returns the
|
||||
struct itself.
|
||||
struct is tied to the module itself.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -5366,6 +5371,13 @@ defmodule Kernel do
|
||||
defstruct [:title, :content, :author]
|
||||
end
|
||||
|
||||
Once a struct is defined, it is possible to create them as follows:
|
||||
|
||||
%Post{title: "Hello world!"}
|
||||
|
||||
For more information on creating, updating, and pattern matching on
|
||||
structs, please check `%/2`.
|
||||
|
||||
## Deriving
|
||||
|
||||
Although structs are maps, by default structs do not implement
|
||||
@@ -5395,12 +5407,12 @@ defmodule Kernel do
|
||||
defstruct name: nil, age: nil
|
||||
end
|
||||
|
||||
For each protocol in `@derive`, Elixir will assert the protocol has
|
||||
been implemented for `Any`. If the `Any` implementation defines a
|
||||
`__deriving__/3` callback, the callback will be invoked and it should define
|
||||
the implementation module. Otherwise an implementation that simply points to
|
||||
the `Any` implementation is automatically derived. For more information on
|
||||
the `__deriving__/3` callback, see `Protocol.derive/3`.
|
||||
For each protocol in `@derive`, Elixir will verify if the protocol
|
||||
has implemented the `c:Protocol.__deriving__/2` callback. If so,
|
||||
the callback will be invoked and it should define the implementation
|
||||
module. Otherwise an implementation that simply points to the `Any`
|
||||
implementation is automatically derived. For more information, see
|
||||
`Protocol.derive/3`.
|
||||
|
||||
## Enforcing keys
|
||||
|
||||
@@ -5452,7 +5464,7 @@ defmodule Kernel do
|
||||
"""
|
||||
defmacro defstruct(fields) do
|
||||
quote bind_quoted: [fields: fields, bootstrapped?: bootstrapped?(Enum)] do
|
||||
{struct, derive, kv, body} =
|
||||
{struct, derive, escaped_struct, kv, body} =
|
||||
Kernel.Utils.defstruct(__MODULE__, fields, bootstrapped?, __ENV__)
|
||||
|
||||
case derive do
|
||||
@@ -5460,7 +5472,7 @@ defmodule Kernel do
|
||||
_ -> Protocol.__derive__(derive, __MODULE__, __ENV__)
|
||||
end
|
||||
|
||||
def __struct__(), do: @__struct__
|
||||
def __struct__(), do: unquote(escaped_struct)
|
||||
def __struct__(unquote(kv)), do: unquote(body)
|
||||
|
||||
Kernel.Utils.announce_struct(__MODULE__)
|
||||
@@ -5547,27 +5559,8 @@ defmodule Kernel do
|
||||
# it is the bootstrapped version or not.
|
||||
Elixir.Kernel.@(impl(true))
|
||||
|
||||
# TODO: Change the implementation on v2.0 to simply call Kernel.struct!/2
|
||||
def exception(args) when Kernel.is_list(args) do
|
||||
struct = __struct__()
|
||||
{valid, invalid} = Enum.split_with(args, fn {k, _} -> Map.has_key?(struct, k) end)
|
||||
|
||||
case invalid do
|
||||
[] ->
|
||||
:ok
|
||||
|
||||
_ ->
|
||||
IO.warn(
|
||||
"the following fields are unknown when raising " <>
|
||||
"#{Kernel.inspect(__MODULE__)}: #{Kernel.inspect(invalid)}. " <>
|
||||
"Please make sure to only give known fields when raising " <>
|
||||
"or redefine #{Kernel.inspect(__MODULE__)}.exception/1 to " <>
|
||||
"discard unknown fields. Future Elixir versions will raise on " <>
|
||||
"unknown fields given to raise/2"
|
||||
)
|
||||
end
|
||||
|
||||
Kernel.struct!(struct, valid)
|
||||
struct!(__MODULE__, args)
|
||||
end
|
||||
|
||||
defoverridable exception: 1
|
||||
@@ -5692,16 +5685,15 @@ defmodule Kernel do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Generates a macro suitable for use in guard expressions.
|
||||
Defines a macro suitable for use in guard expressions.
|
||||
|
||||
It raises at compile time if the definition uses expressions that aren't
|
||||
allowed in guards, and otherwise creates a macro that can be used both inside
|
||||
or outside guards.
|
||||
It raises at compile time if the `guard` uses expressions that aren't
|
||||
allowed in [guard clauses](patterns-and-guards.html#guards),
|
||||
and otherwise creates a macro that can be used both inside or outside guards.
|
||||
|
||||
Note the convention in Elixir is to prefix all guards that return a boolean
|
||||
with the `is_` prefix, such as `is_list/1`. If, however, the function/macro
|
||||
returns a boolean and is not allowed in guards, it should have no prefix and
|
||||
end with a question mark, such as `Keyword.keyword?/1`.
|
||||
When defining your own guards, consider the
|
||||
[naming conventions](naming-conventions.html#is_-prefix-is_foo)
|
||||
around boolean-returning guards.
|
||||
|
||||
## Example
|
||||
|
||||
@@ -5738,12 +5730,17 @@ defmodule Kernel do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Generates a private macro suitable for use in guard expressions.
|
||||
Defines a private macro suitable for use in guard expressions.
|
||||
|
||||
It raises at compile time if the definition uses expressions that aren't
|
||||
allowed in guards, and otherwise creates a private macro that can be used
|
||||
It raises at compile time if the `guard` uses expressions that aren't
|
||||
allowed in [guard clauses](patterns-and-guards.html#guards),
|
||||
and otherwise creates a private macro that can be used
|
||||
both inside or outside guards in the current module.
|
||||
|
||||
When defining your own guards, consider the
|
||||
[naming conventions](naming-conventions.html#is_-prefix-is_foo)
|
||||
around boolean-returning guards.
|
||||
|
||||
Similar to `defmacrop/2`, `defguardp/1` must be defined before its use
|
||||
in the current module.
|
||||
"""
|
||||
@@ -5761,8 +5758,11 @@ defmodule Kernel do
|
||||
"only a single when clause is allowed"
|
||||
|
||||
{call, impls} ->
|
||||
case Macro.decompose_call(call) do
|
||||
{_name, args} ->
|
||||
case decompose_args(call) do
|
||||
:error ->
|
||||
raise ArgumentError, "invalid syntax in defguard #{Macro.to_string(call)}"
|
||||
|
||||
args ->
|
||||
validate_variable_only_args!(call, args)
|
||||
|
||||
macro_definition =
|
||||
@@ -5784,13 +5784,20 @@ defmodule Kernel do
|
||||
Elixir.Kernel.@(doc(guard: true))
|
||||
unquote(macro_definition)
|
||||
end
|
||||
|
||||
_invalid_definition ->
|
||||
raise ArgumentError, "invalid syntax in defguard #{Macro.to_string(call)}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp decompose_args({name, _, args}) when is_atom(name) and is_atom(args), do: []
|
||||
|
||||
defp decompose_args({name, _, args}) when is_atom(name) and is_list(args), do: args
|
||||
|
||||
defp decompose_args({{:unquote, _, _}, _, args}) when is_atom(args), do: []
|
||||
|
||||
defp decompose_args({{:unquote, _, _}, _, args}) when is_list(args), do: args
|
||||
|
||||
defp decompose_args(_), do: :error
|
||||
|
||||
defp validate_variable_only_args!(call, args) do
|
||||
Enum.each(args, fn
|
||||
{ref, _meta, context} when is_atom(ref) and is_atom(context) ->
|
||||
@@ -6197,9 +6204,8 @@ defmodule Kernel do
|
||||
|
||||
"""
|
||||
@doc since: "1.17.0"
|
||||
@spec to_timeout([component, ...] | timeout() | Duration.t()) :: timeout()
|
||||
when component: [{unit, non_neg_integer()}, ...],
|
||||
unit: :week | :day | :hour | :minute | :second | :millisecond
|
||||
@spec to_timeout([{unit, non_neg_integer()}] | timeout() | Duration.t()) :: timeout()
|
||||
when unit: :week | :day | :hour | :minute | :second | :millisecond
|
||||
def to_timeout(duration)
|
||||
|
||||
def to_timeout(:infinity), do: :infinity
|
||||
@@ -6431,29 +6437,37 @@ defmodule Kernel do
|
||||
"""
|
||||
defmacro sigil_r(term, modifiers)
|
||||
|
||||
defmacro sigil_r({:<<>>, _meta, [string]}, options) when is_binary(string) do
|
||||
binary = :elixir_interpolation.unescape_string(string, ®ex_unescape_map/1)
|
||||
regex = Regex.compile!(binary, :binary.list_to_bin(options))
|
||||
Macro.escape(regex)
|
||||
defmacro sigil_r({:<<>>, _meta, [binary]}, options) when is_binary(binary) do
|
||||
binary = :elixir_interpolation.unescape_string(binary, ®ex_unescape_map/1)
|
||||
compile_regex(binary, options)
|
||||
end
|
||||
|
||||
defmacro sigil_r({:<<>>, meta, pieces}, options) do
|
||||
binary = {:<<>>, meta, unescape_tokens(pieces, ®ex_unescape_map/1)}
|
||||
quote(do: Regex.compile!(unquote(binary), unquote(:binary.list_to_bin(options))))
|
||||
tuple = {:<<>>, meta, unescape_tokens(pieces, ®ex_unescape_map/1)}
|
||||
compile_regex(tuple, options)
|
||||
end
|
||||
|
||||
defp regex_unescape_map(:newline), do: true
|
||||
defp regex_unescape_map(_), do: false
|
||||
|
||||
@doc false
|
||||
defmacro sigil_R({:<<>>, _meta, [string]}, options) when is_binary(string) do
|
||||
defmacro sigil_R({:<<>>, _meta, [binary]}, options) when is_binary(binary) do
|
||||
IO.warn(
|
||||
"~R/.../ is deprecated, use ~r/.../ instead",
|
||||
Macro.Env.stacktrace(__CALLER__)
|
||||
)
|
||||
|
||||
regex = Regex.compile!(string, :binary.list_to_bin(options))
|
||||
Macro.escape(regex)
|
||||
compile_regex(binary, options)
|
||||
end
|
||||
|
||||
defp compile_regex(binary_or_tuple, options) do
|
||||
case is_binary(binary_or_tuple) and :erlang.system_info(:otp_release) < [?2, ?8] do
|
||||
true ->
|
||||
Macro.escape(Regex.compile!(binary_or_tuple, :binary.list_to_bin(options)))
|
||||
|
||||
false ->
|
||||
quote(do: Regex.compile!(unquote(binary_or_tuple), unquote(:binary.list_to_bin(options))))
|
||||
end
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
@@ -6691,7 +6705,7 @@ defmodule Kernel do
|
||||
|
||||
defp to_calendar_struct(type, fields) do
|
||||
quote do
|
||||
%{unquote_splicing([__struct__: type] ++ fields)}
|
||||
%unquote(type){unquote_splicing(fields)}
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ defmodule Kernel.CLI do
|
||||
compile: [],
|
||||
no_halt: false,
|
||||
compiler_options: [],
|
||||
warnings_as_errors: false,
|
||||
errors: [],
|
||||
verbose_compile: false,
|
||||
profile: nil,
|
||||
@@ -195,7 +196,7 @@ defmodule Kernel.CLI do
|
||||
end
|
||||
|
||||
@elixir_internals [:elixir, :elixir_aliases, :elixir_expand, :elixir_compiler, :elixir_module] ++
|
||||
[:elixir_clauses, :elixir_lexical, :elixir_def, :elixir_map, :elixir_locals] ++
|
||||
[:elixir_clauses, :elixir_lexical, :elixir_def, :elixir_map] ++
|
||||
[:elixir_erl, :elixir_erl_clauses, :elixir_erl_compiler, :elixir_erl_pass] ++
|
||||
[Kernel.ErrorHandler, Module.ParallelChecker]
|
||||
|
||||
@@ -294,6 +295,16 @@ defmodule Kernel.CLI do
|
||||
parse_argv(t, %{config | commands: [{:parallel_require, h} | config.commands]})
|
||||
end
|
||||
|
||||
defp parse_argv([~c"--color" | t], config) do
|
||||
Application.put_env(:elixir, :ansi_enabled, true)
|
||||
parse_argv(t, config)
|
||||
end
|
||||
|
||||
defp parse_argv([~c"--no-color" | t], config) do
|
||||
Application.put_env(:elixir, :ansi_enabled, true)
|
||||
parse_argv(t, config)
|
||||
end
|
||||
|
||||
## Compiler
|
||||
|
||||
defp parse_argv([~c"-o", h | t], %{mode: :elixirc} = config) do
|
||||
@@ -315,8 +326,7 @@ defmodule Kernel.CLI do
|
||||
end
|
||||
|
||||
defp parse_argv([~c"--warnings-as-errors" | t], %{mode: :elixirc} = config) do
|
||||
compiler_options = [{:warnings_as_errors, true} | config.compiler_options]
|
||||
parse_argv(t, %{config | compiler_options: compiler_options})
|
||||
parse_argv(t, %{config | warnings_as_errors: true})
|
||||
end
|
||||
|
||||
defp parse_argv([~c"--verbose" | t], %{mode: :elixirc} = config) do
|
||||
@@ -499,15 +509,11 @@ defmodule Kernel.CLI do
|
||||
]
|
||||
end
|
||||
|
||||
profile_opts =
|
||||
if config.profile do
|
||||
[profile: config.profile]
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
||||
output = IO.chardata_to_string(config.output)
|
||||
opts = verbose_opts ++ profile_opts
|
||||
|
||||
opts =
|
||||
verbose_opts ++
|
||||
[profile: config.profile, warnings_as_errors: config.warnings_as_errors]
|
||||
|
||||
case Kernel.ParallelCompiler.compile_to_path(files, output, opts) do
|
||||
{:ok, _, _} -> :ok
|
||||
|
||||
@@ -8,6 +8,7 @@ defmodule Kernel.LexicalTracker do
|
||||
@moduledoc false
|
||||
@timeout :infinity
|
||||
@behaviour :gen_server
|
||||
@warn_key 0
|
||||
|
||||
@doc """
|
||||
Returns all references in this lexical scope.
|
||||
@@ -16,6 +17,24 @@ defmodule Kernel.LexicalTracker do
|
||||
:gen_server.call(pid, :references, @timeout)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Invoked during module expansion to annotate an alias
|
||||
must be warned if unused.
|
||||
"""
|
||||
def warn_alias(pid, meta, alias, module) do
|
||||
:gen_server.cast(pid, {:warn_alias, alias, meta})
|
||||
module
|
||||
end
|
||||
|
||||
@doc """
|
||||
Invoked during module expansion to annotate an import
|
||||
must be warned if unused.
|
||||
"""
|
||||
def warn_import(pid, module) do
|
||||
:gen_server.cast(pid, {:warn_import, module})
|
||||
module
|
||||
end
|
||||
|
||||
# Internal API
|
||||
|
||||
# Starts the tracker and returns its PID.
|
||||
@@ -39,11 +58,6 @@ defmodule Kernel.LexicalTracker do
|
||||
:gen_server.cast(pid, {:add_import, module, fas, meta, warn})
|
||||
end
|
||||
|
||||
@doc false
|
||||
def add_alias(pid, module, meta, warn) when is_atom(module) do
|
||||
:gen_server.cast(pid, {:add_alias, module, meta, warn})
|
||||
end
|
||||
|
||||
@doc false
|
||||
def remote_dispatch(pid, module, mode) when is_atom(module) do
|
||||
:gen_server.cast(pid, {:remote_dispatch, module, mode})
|
||||
@@ -93,16 +107,12 @@ defmodule Kernel.LexicalTracker do
|
||||
|
||||
@doc false
|
||||
def collect_unused_imports(pid) do
|
||||
unused(pid, :unused_imports)
|
||||
:gen_server.call(pid, :unused_imports, @timeout)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def collect_unused_aliases(pid) do
|
||||
unused(pid, :unused_aliases)
|
||||
end
|
||||
|
||||
defp unused(pid, tag) do
|
||||
:gen_server.call(pid, tag, @timeout)
|
||||
:gen_server.call(pid, :unused_aliases, @timeout)
|
||||
end
|
||||
|
||||
# Callbacks
|
||||
@@ -123,11 +133,17 @@ defmodule Kernel.LexicalTracker do
|
||||
|
||||
@doc false
|
||||
def handle_call(:unused_aliases, _from, state) do
|
||||
{:reply, Enum.sort(state.aliases), state}
|
||||
aliases = for {alias, meta} when is_list(meta) <- state.aliases, do: {alias, meta}
|
||||
{:reply, Enum.sort(aliases), state}
|
||||
end
|
||||
|
||||
def handle_call(:unused_imports, _from, state) do
|
||||
{:reply, Enum.sort(state.imports), state}
|
||||
imports =
|
||||
for {module, %{@warn_key => _} = map} <- state.imports do
|
||||
{module, Map.delete(map, @warn_key)}
|
||||
end
|
||||
|
||||
{:reply, Enum.sort(imports), state}
|
||||
end
|
||||
|
||||
def handle_call(:references, _from, state) do
|
||||
@@ -171,8 +187,8 @@ defmodule Kernel.LexicalTracker do
|
||||
{:noreply, %{state | imports: imports, references: references}}
|
||||
end
|
||||
|
||||
def handle_cast({:alias_dispatch, module}, %{aliases: aliases} = state) do
|
||||
{:noreply, %{state | aliases: Map.delete(aliases, module)}}
|
||||
def handle_cast({:alias_dispatch, module}, state) do
|
||||
{:noreply, put_in(state.aliases[module], :used)}
|
||||
end
|
||||
|
||||
def handle_cast({:import_quoted, module, function, arities}, state) do
|
||||
@@ -210,20 +226,19 @@ defmodule Kernel.LexicalTracker do
|
||||
end
|
||||
|
||||
def handle_cast({:add_import, module, fas, meta, warn}, state) do
|
||||
if warn do
|
||||
imports = for module_or_fa <- [module | fas], do: {module_or_fa, meta}, into: %{}
|
||||
{:noreply, put_in(state.imports[module], imports)}
|
||||
else
|
||||
{:noreply, state}
|
||||
keys = if warn, do: [@warn_key, module | fas], else: [module | fas]
|
||||
{:noreply, put_in(state.imports[module], Map.from_keys(keys, meta))}
|
||||
end
|
||||
|
||||
def handle_cast({:warn_alias, alias, meta}, %{aliases: aliases} = state) do
|
||||
case aliases do
|
||||
%{^alias => :used} -> {:noreply, state}
|
||||
%{} -> {:noreply, %{state | aliases: Map.put(aliases, alias, meta)}}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_cast({:add_alias, module, meta, warn}, state) do
|
||||
if warn do
|
||||
{:noreply, put_in(state.aliases[module], meta)}
|
||||
else
|
||||
{:noreply, state}
|
||||
end
|
||||
def handle_cast({:warn_import, module}, state) do
|
||||
{:noreply, put_in(state.imports[module][@warn_key], true)}
|
||||
end
|
||||
|
||||
@doc false
|
||||
|
||||
@@ -31,11 +31,11 @@ defmodule Kernel.ParallelCompiler do
|
||||
dest = :erlang.get(:elixir_compiler_dest)
|
||||
|
||||
{:error_handler, error_handler} = :erlang.process_info(self(), :error_handler)
|
||||
{_parent, checker} = Module.ParallelChecker.get()
|
||||
{_parent, cache} = Module.ParallelChecker.get()
|
||||
|
||||
task =
|
||||
Task.async(fn ->
|
||||
Module.ParallelChecker.put(compiler_pid, checker)
|
||||
Module.ParallelChecker.put(compiler_pid, cache)
|
||||
:erlang.put(:elixir_compiler_info, {compiler_pid, file_pid})
|
||||
:erlang.put(:elixir_compiler_file, file)
|
||||
dest != :undefined and :erlang.put(:elixir_compiler_dest, dest)
|
||||
@@ -158,6 +158,10 @@ defmodule Kernel.ParallelCompiler do
|
||||
* `:return_diagnostics` (since v1.15.0) - returns maps with information instead of
|
||||
a list of warnings and returns diagnostics as maps instead of tuples
|
||||
|
||||
* `:max_concurrency` - the maximum number of files to compile in parallel.
|
||||
Setting this option to 1 will compile files sequentially.
|
||||
Defaults to the number of schedulers online, or at least 2.
|
||||
|
||||
"""
|
||||
@doc since: "1.6.0"
|
||||
@spec compile([Path.t()], keyword()) ::
|
||||
@@ -204,6 +208,10 @@ defmodule Kernel.ParallelCompiler do
|
||||
* `:each_module` - for each module compiled, invokes the callback passing
|
||||
the file, module and the module bytecode
|
||||
|
||||
* `:max_concurrency` - the maximum number of files to compile in parallel.
|
||||
Setting this option to 1 will compile files sequentially.
|
||||
Defaults to the number of schedulers online, or at least 2.
|
||||
|
||||
"""
|
||||
@doc since: "1.6.0"
|
||||
@spec require([Path.t()], keyword()) ::
|
||||
@@ -239,36 +247,26 @@ defmodule Kernel.ParallelCompiler do
|
||||
|
||||
defp spawn_workers(files, output, options) do
|
||||
{:module, _} = :code.ensure_loaded(Kernel.ErrorHandler)
|
||||
schedulers = max(:erlang.system_info(:schedulers_online), 2)
|
||||
{:ok, checker} = Module.ParallelChecker.start_link(schedulers)
|
||||
|
||||
schedulers =
|
||||
Keyword.get_lazy(options, :max_concurrency, fn ->
|
||||
max(:erlang.system_info(:schedulers_online), 2)
|
||||
end)
|
||||
|
||||
{:ok, cache} = Module.ParallelChecker.start_link(schedulers)
|
||||
|
||||
{status, modules_or_errors, info} =
|
||||
try do
|
||||
outcome = spawn_workers(schedulers, checker, files, output, options)
|
||||
{outcome, Code.get_compiler_option(:warnings_as_errors)}
|
||||
spawn_workers(schedulers, cache, files, output, options)
|
||||
else
|
||||
{{:ok, _, %{runtime_warnings: r_warnings, compile_warnings: c_warnings} = info}, true}
|
||||
when r_warnings != [] or c_warnings != [] ->
|
||||
message =
|
||||
"Compilation failed due to warnings while using the --warnings-as-errors option"
|
||||
|
||||
IO.puts(:stderr, message)
|
||||
errors = Enum.map(r_warnings ++ c_warnings, &Map.replace!(&1, :severity, :error))
|
||||
{:error, errors, %{info | runtime_warnings: [], compile_warnings: []}}
|
||||
|
||||
{{:ok, outcome, info}, _} ->
|
||||
{:ok, outcome, info} ->
|
||||
beam_timestamp = Keyword.get(options, :beam_timestamp)
|
||||
{:ok, write_module_binaries(outcome, output, beam_timestamp), info}
|
||||
|
||||
{{:error, errors, info}, true} ->
|
||||
%{runtime_warnings: r_warnings, compile_warnings: c_warnings} = info
|
||||
info = %{info | runtime_warnings: [], compile_warnings: []}
|
||||
{:error, c_warnings ++ r_warnings ++ errors, info}
|
||||
|
||||
{{:error, errors, info}, _} ->
|
||||
{:error, errors, info} ->
|
||||
{:error, errors, info}
|
||||
after
|
||||
Module.ParallelChecker.stop(checker)
|
||||
Module.ParallelChecker.stop(cache)
|
||||
end
|
||||
|
||||
# TODO: Require this to be set from Elixir v1.19
|
||||
@@ -353,32 +351,33 @@ defmodule Kernel.ParallelCompiler do
|
||||
end
|
||||
|
||||
defp maybe_check_modules(result, runtime_modules, state) do
|
||||
%{profile: profile, checker: checker} = state
|
||||
|
||||
compiled_modules =
|
||||
for {{:module, module}, binary} when is_binary(binary) <- result,
|
||||
do: module
|
||||
|
||||
profile_checker(profile, compiled_modules, runtime_modules, fn ->
|
||||
Module.ParallelChecker.verify(checker, runtime_modules)
|
||||
end)
|
||||
profile(
|
||||
state,
|
||||
fn ->
|
||||
num_modules = length(compiled_modules) + length(runtime_modules)
|
||||
"group pass check of #{num_modules} modules"
|
||||
end,
|
||||
fn -> Module.ParallelChecker.verify(state.checker, runtime_modules) end
|
||||
)
|
||||
end
|
||||
|
||||
defp profile_init(:time), do: {:time, System.monotonic_time(), 0}
|
||||
defp profile_init(nil), do: :none
|
||||
|
||||
defp profile_checker({:time, _, _}, compiled_modules, runtime_modules, fun) do
|
||||
defp profile(%{profile: :none}, _what, fun), do: fun.()
|
||||
|
||||
defp profile(%{profile: {:time, _, _}}, what, fun) do
|
||||
{time, result} = :timer.tc(fun)
|
||||
time = div(time, 1000)
|
||||
num_modules = length(compiled_modules) + length(runtime_modules)
|
||||
IO.puts(:stderr, "[profile] Finished group pass check of #{num_modules} modules in #{time}ms")
|
||||
what = if is_binary(what), do: what, else: what.()
|
||||
IO.puts(:stderr, "[profile] Finished #{what} in #{time}ms")
|
||||
result
|
||||
end
|
||||
|
||||
defp profile_checker(:none, _compiled_modules, _runtime_modules, fun) do
|
||||
fun.()
|
||||
end
|
||||
|
||||
## Compiler worker spawning
|
||||
|
||||
# We already have n=schedulers currently running, don't spawn new ones
|
||||
@@ -414,13 +413,13 @@ defmodule Kernel.ParallelCompiler do
|
||||
end
|
||||
|
||||
defp spawn_workers([file | queue], spawned, waiting, files, result, warnings, errors, state) do
|
||||
%{output: output, dest: dest, checker: checker} = state
|
||||
%{output: output, dest: dest, checker: cache} = state
|
||||
parent = self()
|
||||
file = Path.expand(file)
|
||||
|
||||
{pid, ref} =
|
||||
:erlang.spawn_monitor(fn ->
|
||||
Module.ParallelChecker.put(parent, checker)
|
||||
Module.ParallelChecker.put(parent, cache)
|
||||
:erlang.put(:elixir_compiler_info, {parent, self()})
|
||||
:erlang.put(:elixir_compiler_file, file)
|
||||
|
||||
@@ -462,7 +461,10 @@ defmodule Kernel.ParallelCompiler do
|
||||
end)
|
||||
|
||||
[] = files
|
||||
cycle_return = each_cycle_return(state.each_cycle.())
|
||||
|
||||
cycle_return =
|
||||
profile(state, "cycle resolution", fn -> each_cycle_return(state.each_cycle.()) end)
|
||||
|
||||
state = cycle_timing(result, state)
|
||||
|
||||
case cycle_return do
|
||||
@@ -506,13 +508,15 @@ defmodule Kernel.ParallelCompiler do
|
||||
# Finally, note there is no difference between hard and raise, the
|
||||
# difference is where the raise is happening, inside the compiler
|
||||
# or in the caller.
|
||||
waiting_list = Map.to_list(waiting)
|
||||
|
||||
deadlocked =
|
||||
deadlocked(waiting_list, :soft, false) ||
|
||||
deadlocked(waiting_list, :soft, true) ||
|
||||
deadlocked(waiting_list, :hard, false) ||
|
||||
without_definition(waiting_list, files)
|
||||
profile(state, "deadlock resolution", fn ->
|
||||
waiting_list = Map.to_list(waiting)
|
||||
|
||||
deadlocked(waiting_list, :soft, false) ||
|
||||
deadlocked(waiting_list, :soft, true) ||
|
||||
deadlocked(waiting_list, :hard, false) ||
|
||||
without_definition(waiting_list, files)
|
||||
end)
|
||||
|
||||
if deadlocked do
|
||||
spawn_workers(deadlocked, spawned, waiting, files, result, warnings, errors, state)
|
||||
|
||||
@@ -112,26 +112,24 @@ defmodule Kernel.SpecialForms do
|
||||
|
||||
%User{age: age} = user
|
||||
|
||||
An update operation specific for structs is also available:
|
||||
|
||||
%User{user | age: 28}
|
||||
|
||||
The advantage of structs is that they validate that the given
|
||||
keys are part of the defined struct. The example below will fail
|
||||
because there is no key `:full_name` in the `User` struct:
|
||||
|
||||
%User{full_name: "john doe"}
|
||||
|
||||
The syntax above will guarantee the given keys are valid at
|
||||
compilation time and it will guarantee at runtime the given
|
||||
argument is a struct, failing with `BadStructError` otherwise.
|
||||
An update operation specific for structs is also available:
|
||||
|
||||
Although structs are maps, by default structs do not implement
|
||||
any of the protocols implemented for maps. Check
|
||||
`Kernel.defprotocol/2` for more information on how structs
|
||||
can be used with protocols for polymorphic dispatch. Also
|
||||
see `Kernel.struct/2` and `Kernel.struct!/2` for examples on
|
||||
how to create and update structs dynamically.
|
||||
%User{user | age: 28}
|
||||
|
||||
Once again, the syntax above will guarantee the given keys
|
||||
are valid at compilation time and it will guarantee at runtime
|
||||
the given argument is a struct, failing with `BadStructError`
|
||||
otherwise. The map update syntax can also be used for updating
|
||||
structs, and it is useful when you want to update any struct,
|
||||
regardless of their name, as long as they have matching fields:
|
||||
|
||||
%{user | age: 28}
|
||||
|
||||
## Pattern matching on struct names
|
||||
|
||||
@@ -1048,7 +1046,7 @@ defmodule Kernel.SpecialForms do
|
||||
end
|
||||
end
|
||||
|
||||
require Hygiene
|
||||
require ContextHygiene
|
||||
ContextHygiene.write()
|
||||
ContextHygiene.read()
|
||||
#=> 1
|
||||
@@ -1541,7 +1539,7 @@ defmodule Kernel.SpecialForms do
|
||||
@doc """
|
||||
Combine matching clauses.
|
||||
|
||||
One of the ways to understand with is to show which code
|
||||
One of the ways to understand `with` is to show which code
|
||||
patterns it improves. Imagine you have a map where the fields
|
||||
`width` and `height` are optional and you want to compute its
|
||||
area, as `{:ok, area}` or return `:error`. We could implement
|
||||
@@ -1663,7 +1661,7 @@ defmodule Kernel.SpecialForms do
|
||||
...> end
|
||||
{:error, :wrong_data}
|
||||
|
||||
The `else` block works like a `case` clause: it can have multiple clauses,
|
||||
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.
|
||||
|
||||
@@ -1788,6 +1786,15 @@ defmodule Kernel.SpecialForms do
|
||||
|
||||
&local_function/1
|
||||
|
||||
Note that `&local_function/1` creates a local capture, but
|
||||
`&__MODULE__.local_function/1` or `&imported_function/1` create a remote
|
||||
capture. For more information, refer to the ["Functions" section in the Erlang Reference Manual](https://www.erlang.org/doc/system/eff_guide_functions.html#function-calls).
|
||||
|
||||
Whether a capture is local or remote has implications when using hot code
|
||||
reloading: local captures dispatch to the version of the module that existed
|
||||
at the time they were created, while remote captures dispatch to the current
|
||||
version of the module.
|
||||
|
||||
See also `Function.capture/3`.
|
||||
|
||||
## Anonymous functions
|
||||
|
||||
@@ -118,7 +118,7 @@ defmodule Kernel.Typespec do
|
||||
case spec_to_signature(expr) do
|
||||
{name, arity} ->
|
||||
# store doc only once in case callback has multiple clauses
|
||||
unless :ets.member(set, {kind, name, arity}) do
|
||||
if not :ets.member(set, {kind, name, arity}) do
|
||||
{line, doc} = get_doc_info(set, :doc, line)
|
||||
store_doc(set, kind, name, arity, line, :doc, doc, %{})
|
||||
end
|
||||
@@ -138,7 +138,7 @@ defmodule Kernel.Typespec do
|
||||
case type_to_signature(expr) do
|
||||
{name, arity} = signature when signature in @reserved_signatures ->
|
||||
compile_error(
|
||||
:elixir_locals.get_cached_env(pos),
|
||||
:elixir_module.get_cached_env(pos),
|
||||
"type #{name}/#{arity} is a reserved type and it cannot be defined"
|
||||
)
|
||||
|
||||
@@ -247,7 +247,7 @@ defmodule Kernel.Typespec do
|
||||
|
||||
defp collect_defined_type_pairs(type_typespecs) do
|
||||
fun = fn {_kind, expr, pos}, type_pairs ->
|
||||
%{file: file, line: line} = env = :elixir_locals.get_cached_env(pos)
|
||||
%{file: file, line: line} = env = :elixir_module.get_cached_env(pos)
|
||||
|
||||
case type_to_signature(expr) do
|
||||
{name, arity} = type_pair ->
|
||||
@@ -290,8 +290,9 @@ defmodule Kernel.Typespec do
|
||||
:lists.filter(fun, types)
|
||||
end
|
||||
|
||||
defp translate_type({kind, {:"::", _, [{name, _, args}, definition]}, pos}, state) do
|
||||
caller = :elixir_locals.get_cached_env(pos)
|
||||
defp translate_type({kind, {:"::", _, [{name, meta, args}, definition]}, pos}, state)
|
||||
when is_list(meta) do
|
||||
caller = :elixir_module.get_cached_env(pos)
|
||||
state = clean_local_state(state)
|
||||
|
||||
args =
|
||||
@@ -320,7 +321,7 @@ defmodule Kernel.Typespec do
|
||||
|
||||
invalid_args = :lists.filter(&(not valid_variable_ast?(&1)), args)
|
||||
|
||||
unless invalid_args == [] do
|
||||
if invalid_args != [] do
|
||||
invalid_args = :lists.join(", ", :lists.map(&Macro.to_string/1, invalid_args))
|
||||
|
||||
message =
|
||||
@@ -335,7 +336,7 @@ defmodule Kernel.Typespec do
|
||||
IO.warn(message, caller)
|
||||
end
|
||||
|
||||
{{kind, {name, arity}, caller.line, type, export}, state}
|
||||
{{kind, {name, arity}, meta, type, export}, state}
|
||||
end
|
||||
|
||||
defp valid_variable_ast?({variable_name, _, context})
|
||||
@@ -348,17 +349,17 @@ defmodule Kernel.Typespec do
|
||||
defp underspecified?(_kind, _arity, _spec), do: false
|
||||
|
||||
defp translate_spec({kind, {:when, _meta, [spec, guard]}, pos}, state) do
|
||||
caller = :elixir_locals.get_cached_env(pos)
|
||||
caller = :elixir_module.get_cached_env(pos)
|
||||
translate_spec(kind, spec, guard, caller, state)
|
||||
end
|
||||
|
||||
defp translate_spec({kind, spec, pos}, state) do
|
||||
caller = :elixir_locals.get_cached_env(pos)
|
||||
caller = :elixir_module.get_cached_env(pos)
|
||||
translate_spec(kind, spec, [], caller, state)
|
||||
end
|
||||
|
||||
defp translate_spec(kind, {:"::", meta, [{name, _, args}, return]}, guard, caller, state)
|
||||
when is_atom(name) and name != :"::" do
|
||||
defp translate_spec(kind, {:"::", _, [{name, meta, args}, return]}, guard, caller, state)
|
||||
when is_atom(name) and name != :"::" and is_list(meta) and (is_list(args) or is_atom(args)) do
|
||||
translate_spec(kind, meta, name, args, return, guard, caller, state)
|
||||
end
|
||||
|
||||
@@ -380,7 +381,7 @@ defmodule Kernel.Typespec do
|
||||
ensure_no_defaults!(args)
|
||||
state = clean_local_state(state)
|
||||
|
||||
unless Keyword.keyword?(guard) do
|
||||
if not Keyword.keyword?(guard) do
|
||||
error = "expected keywords as guard in type specification, got: #{Macro.to_string(guard)}"
|
||||
compile_error(caller, error)
|
||||
end
|
||||
@@ -401,7 +402,7 @@ defmodule Kernel.Typespec do
|
||||
ensure_no_unused_local_vars!(caller, state.local_vars)
|
||||
|
||||
arity = length(args)
|
||||
{{kind, {name, arity}, caller.line, spec}, state}
|
||||
{{kind, {name, arity}, meta, spec}, state}
|
||||
end
|
||||
|
||||
# TODO: Remove char_list type by v2.0
|
||||
@@ -567,27 +568,23 @@ defmodule Kernel.Typespec do
|
||||
defp typespec({:%, _, [name, {:%{}, meta, fields}]} = node, vars, caller, state) do
|
||||
case Macro.expand(name, %{caller | function: {:__info__, 1}}) do
|
||||
module when is_atom(module) ->
|
||||
struct =
|
||||
module
|
||||
|> Macro.struct!(caller)
|
||||
|> Map.delete(:__struct__)
|
||||
|> Map.to_list()
|
||||
struct_info = Macro.struct_info!(module, caller)
|
||||
|
||||
unless Keyword.keyword?(fields) do
|
||||
if not Keyword.keyword?(fields) do
|
||||
compile_error(caller, "expected key-value pairs in struct #{Macro.to_string(name)}")
|
||||
end
|
||||
|
||||
types =
|
||||
:lists.map(
|
||||
fn
|
||||
{:__exception__ = field, true} -> {field, Keyword.get(fields, field, true)}
|
||||
{field, _} -> {field, Keyword.get(fields, field, quote(do: term()))}
|
||||
fn %{field: field} ->
|
||||
default_type = if field == :__exception__, do: true, else: quote(do: term())
|
||||
{field, Keyword.get(fields, field, default_type)}
|
||||
end,
|
||||
:lists.sort(struct)
|
||||
struct_info
|
||||
)
|
||||
|
||||
fun = fn {field, _} ->
|
||||
unless Keyword.has_key?(struct, field) do
|
||||
if not Enum.any?(struct_info, &(&1.field == field)) do
|
||||
compile_error(
|
||||
caller,
|
||||
"undefined field #{inspect(field)} on struct #{inspect(module)}"
|
||||
@@ -596,7 +593,7 @@ defmodule Kernel.Typespec do
|
||||
end
|
||||
|
||||
:lists.foreach(fun, fields)
|
||||
typespec({:%{}, meta, [__struct__: module] ++ types}, vars, caller, state)
|
||||
typespec({:%{}, meta, [__struct__: module] ++ :lists.usort(types)}, vars, caller, state)
|
||||
|
||||
_ ->
|
||||
compile_error(
|
||||
@@ -630,7 +627,7 @@ defmodule Kernel.Typespec do
|
||||
)
|
||||
|
||||
fun = fn {field, _} ->
|
||||
unless Keyword.has_key?(fields, field) do
|
||||
if not Keyword.has_key?(fields, field) do
|
||||
compile_error(caller, "undefined field #{field} on record #{inspect(tag)}")
|
||||
end
|
||||
end
|
||||
@@ -671,14 +668,7 @@ defmodule Kernel.Typespec do
|
||||
when is_list(args) do
|
||||
{args, state} = fn_args(meta, args, vars, caller, state)
|
||||
{spec, state} = typespec(return, vars, caller, state)
|
||||
|
||||
fun_args =
|
||||
case [args, spec] do
|
||||
[{:type, _, :any}, {:type, _, :any, []}] -> []
|
||||
pair -> pair
|
||||
end
|
||||
|
||||
{{:type, location(meta), :fun, fun_args}, state}
|
||||
{{:type, location(meta), :fun, [args, spec]}, state}
|
||||
end
|
||||
|
||||
# Handle type operator
|
||||
@@ -754,7 +744,7 @@ defmodule Kernel.Typespec do
|
||||
) do
|
||||
remote = Module.get_attribute(caller.module, attr)
|
||||
|
||||
unless is_atom(remote) and remote != nil do
|
||||
if not (is_atom(remote) and remote != nil) do
|
||||
message =
|
||||
"invalid remote in typespec: #{Macro.to_string(orig)} (@#{attr} is #{inspect(remote)})"
|
||||
|
||||
@@ -948,7 +938,7 @@ defmodule Kernel.Typespec do
|
||||
|
||||
## Helpers
|
||||
|
||||
# This is a backport of Macro.expand/2 because we want to expand
|
||||
# This is a modified backport of Macro.expand/2 because we want to expand
|
||||
# aliases but we don't them to become compile-time references.
|
||||
defp expand_remote({:__aliases__, meta, list} = alias, env) do
|
||||
case :elixir_aliases.expand_or_concat(meta, list, env, true) do
|
||||
@@ -956,9 +946,12 @@ defmodule Kernel.Typespec do
|
||||
receiver
|
||||
|
||||
[head | tail] ->
|
||||
case Macro.expand_once(head, env) do
|
||||
head when is_atom(head) -> :elixir_aliases.concat([head | tail])
|
||||
_ -> alias
|
||||
case Macro.expand(head, env) do
|
||||
head when is_atom(head) ->
|
||||
:elixir_aliases.concat([head | tail])
|
||||
|
||||
_ ->
|
||||
compile_error(env, "unexpected expression in typespec: #{Macro.to_string(alias)}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -103,7 +103,7 @@ defmodule Kernel.Utils do
|
||||
def defstruct(module, fields, bootstrapped?, env) do
|
||||
{set, bag} = :elixir_module.data_tables(module)
|
||||
|
||||
if :ets.member(set, :__struct__) do
|
||||
if :ets.member(set, {:elixir, :struct}) do
|
||||
raise ArgumentError,
|
||||
"defstruct has already been called for " <>
|
||||
"#{Kernel.inspect(module)}, defstruct can only be called once per module"
|
||||
@@ -119,6 +119,8 @@ defmodule Kernel.Utils do
|
||||
|
||||
mapper = fn
|
||||
{key, val} when is_atom(key) ->
|
||||
key == :__struct__ and raise(ArgumentError, "cannot set :__struct__ in struct definition")
|
||||
|
||||
try do
|
||||
:elixir_quote.escape(val, :none, false)
|
||||
rescue
|
||||
@@ -129,6 +131,7 @@ defmodule Kernel.Utils do
|
||||
end
|
||||
|
||||
key when is_atom(key) ->
|
||||
key == :__struct__ and raise(ArgumentError, "cannot set :__struct__ in struct definition")
|
||||
{key, nil}
|
||||
|
||||
other ->
|
||||
@@ -163,23 +166,24 @@ defmodule Kernel.Utils do
|
||||
end
|
||||
|
||||
:lists.foreach(foreach, enforce_keys)
|
||||
struct = :maps.put(:__struct__, module, :maps.from_list(fields))
|
||||
struct = :maps.from_list([__struct__: module] ++ fields)
|
||||
escaped_struct = :elixir_quote.escape(struct, :none, false)
|
||||
|
||||
body =
|
||||
case bootstrapped? do
|
||||
true ->
|
||||
case enforce_keys do
|
||||
[] ->
|
||||
quote do
|
||||
Enum.reduce(kv, @__struct__, fn {key, val}, map ->
|
||||
quote line: 0, generated: true do
|
||||
Enum.reduce(kv, unquote(escaped_struct), fn {key, val}, map ->
|
||||
%{map | key => val}
|
||||
end)
|
||||
end
|
||||
|
||||
_ ->
|
||||
quote do
|
||||
quote line: 0, generated: true do
|
||||
{map, keys} =
|
||||
Enum.reduce(kv, {@__struct__, unquote(enforce_keys)}, fn
|
||||
Enum.reduce(kv, {unquote(escaped_struct), unquote(enforce_keys)}, fn
|
||||
{key, val}, {map, keys} ->
|
||||
{%{map | key => val}, List.delete(keys, key)}
|
||||
end)
|
||||
@@ -197,10 +201,10 @@ defmodule Kernel.Utils do
|
||||
end
|
||||
|
||||
false ->
|
||||
quote do
|
||||
quote line: 0, generated: true do
|
||||
:lists.foldl(
|
||||
fn {key, val}, acc -> %{acc | key => val} end,
|
||||
@__struct__,
|
||||
unquote(escaped_struct),
|
||||
kv
|
||||
)
|
||||
end
|
||||
@@ -208,17 +212,13 @@ defmodule Kernel.Utils do
|
||||
|
||||
case enforce_keys -- :maps.keys(struct) do
|
||||
[] ->
|
||||
# The __struct__ attribute is during expansion and for loading remote structs
|
||||
:ets.insert(set, {:__struct__, struct, nil, []})
|
||||
|
||||
# The complete metadata goes into __info__(:struct)
|
||||
mapper = fn {key, val} ->
|
||||
%{field: key, default: val, required: :lists.member(key, enforce_keys)}
|
||||
%{field: key, default: val}
|
||||
end
|
||||
|
||||
:ets.insert(set, {{:elixir, :struct}, :lists.map(mapper, fields)})
|
||||
derive = :lists.map(fn {_, value} -> value end, :ets.take(bag, {:accumulate, :derive}))
|
||||
{struct, :lists.reverse(derive), quote(do: kv), body}
|
||||
{struct, :lists.reverse(derive), escaped_struct, quote(do: kv), body}
|
||||
|
||||
error_keys ->
|
||||
raise ArgumentError,
|
||||
@@ -330,8 +330,11 @@ defmodule Kernel.Utils do
|
||||
|
||||
quote do
|
||||
case Macro.Env.in_guard?(__CALLER__) do
|
||||
true -> unquote(literal_quote(unquote_every_ref(expr, vars)))
|
||||
false -> unquote(literal_quote(unquote_refs_once(expr, vars, env.module)))
|
||||
true ->
|
||||
unquote(literal_quote(unquote_every_ref(expr, vars), []))
|
||||
|
||||
false ->
|
||||
unquote(literal_quote(unquote_refs_once(expr, vars, env.module), generated: true))
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -396,8 +399,8 @@ defmodule Kernel.Utils do
|
||||
end
|
||||
end
|
||||
|
||||
defp literal_quote(ast) do
|
||||
{:quote, [], [[do: ast]]}
|
||||
defp literal_quote(ast, args) do
|
||||
{:quote, [], [args, [do: ast]]}
|
||||
end
|
||||
|
||||
defp literal_unquote(ast) do
|
||||
|
||||
@@ -6,7 +6,7 @@ defmodule Keyword do
|
||||
The second element, known as the *value*, can be any term.
|
||||
|
||||
Keywords are mostly used to work with optional values. For a general introduction
|
||||
to keywords and how the compare with maps, see our [Keyword and Maps](keywords-and-maps.md)
|
||||
to keywords and how they compare with maps, see our [Keyword and Maps](keywords-and-maps.md)
|
||||
guide.
|
||||
|
||||
## Examples
|
||||
@@ -960,6 +960,36 @@ defmodule Keyword do
|
||||
:lists.sort(left) === :lists.sort(right)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Intersects two keyword lists, returning a keyword with the common keys.
|
||||
|
||||
By default, it returns the values of the intersected keys in `keyword2`.
|
||||
The keys are returned in the order found in `keyword1`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Keyword.intersect([a: 1, b: 2], [b: "b", c: "c"])
|
||||
[b: "b"]
|
||||
|
||||
iex> Keyword.intersect([a: 1, b: 2], [b: 2, c: 3], fn _k, v1, v2 ->
|
||||
...> v1 + v2
|
||||
...> end)
|
||||
[b: 4]
|
||||
|
||||
"""
|
||||
@doc since: "1.17.0"
|
||||
@spec intersect(keyword, keyword, (key, value, value -> value)) :: keyword
|
||||
def intersect(keyword1, keyword2, fun \\ fn _key, _v1, v2 -> v2 end)
|
||||
|
||||
def intersect([{k, v1} | keyword1], keyword2, fun) do
|
||||
case :lists.keyfind(k, 1, keyword2) do
|
||||
{_, v2} -> [{k, fun.(k, v1, v2)} | intersect(keyword1, keyword2, fun)]
|
||||
false -> intersect(keyword1, keyword2, fun)
|
||||
end
|
||||
end
|
||||
|
||||
def intersect([], _keyword2, _fun), do: []
|
||||
|
||||
@doc """
|
||||
Merges two keyword lists into one.
|
||||
|
||||
|
||||
+57
-46
@@ -633,27 +633,39 @@ defmodule List do
|
||||
[other]
|
||||
end
|
||||
|
||||
@doc """
|
||||
Zips corresponding elements from each list in `list_of_lists`.
|
||||
|
||||
The zipping finishes as soon as any list terminates.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> List.zip([[1, 2], [3, 4], [5, 6]])
|
||||
[{1, 3, 5}, {2, 4, 6}]
|
||||
|
||||
iex> List.zip([[1, 2], [3], [5, 6]])
|
||||
[{1, 3, 5}]
|
||||
|
||||
"""
|
||||
@spec zip([list]) :: [tuple]
|
||||
@deprecated "Use Enum.zip/1 instead"
|
||||
# We keep the old implementation because it also supported lists
|
||||
# of tuples, even though this was not included in its @spec.
|
||||
def zip([]), do: []
|
||||
def zip(list_of_lists) when is_list(list_of_lists), do: do_zip(list_of_lists, [])
|
||||
|
||||
def zip(list_of_lists) when is_list(list_of_lists) do
|
||||
do_zip(list_of_lists, [])
|
||||
defp do_zip(list, acc) do
|
||||
converter = fn x, acc -> do_zip_each(to_list(x), acc) end
|
||||
|
||||
case :lists.mapfoldl(converter, [], list) do
|
||||
{_, nil} ->
|
||||
:lists.reverse(acc)
|
||||
|
||||
{mlist, heads} ->
|
||||
do_zip(mlist, [to_tuple(:lists.reverse(heads)) | acc])
|
||||
end
|
||||
end
|
||||
|
||||
defp do_zip_each(_, nil) do
|
||||
{nil, nil}
|
||||
end
|
||||
|
||||
defp do_zip_each([head | tail], acc) do
|
||||
{tail, [head | acc]}
|
||||
end
|
||||
|
||||
defp do_zip_each([], _) do
|
||||
{nil, nil}
|
||||
end
|
||||
|
||||
defp to_list(tuple) when is_tuple(tuple), do: Tuple.to_list(tuple)
|
||||
defp to_list(list) when is_list(list), do: list
|
||||
|
||||
@doc ~S"""
|
||||
Checks if `list` is a charlist made only of printable ASCII characters.
|
||||
|
||||
@@ -924,6 +936,34 @@ defmodule List do
|
||||
def starts_with?(list, []) when is_list(list), do: true
|
||||
def starts_with?(list, [_ | _]) when is_list(list), do: false
|
||||
|
||||
@doc """
|
||||
Returns `true` if `list` ends with the given `suffix` list, otherwise returns `false`.
|
||||
|
||||
If `suffix` is an empty list, it returns `true`.
|
||||
|
||||
### Examples
|
||||
|
||||
iex> List.ends_with?([1, 2, 3], [2, 3])
|
||||
true
|
||||
|
||||
iex> List.ends_with?([1, 2], [1, 2, 3])
|
||||
false
|
||||
|
||||
iex> List.ends_with?([:alpha], [])
|
||||
true
|
||||
|
||||
iex> List.ends_with?([], [:alpha])
|
||||
false
|
||||
|
||||
"""
|
||||
@doc since: "1.18.0"
|
||||
@spec ends_with?(nonempty_list, nonempty_list) :: boolean
|
||||
@spec ends_with?(list, []) :: true
|
||||
@spec ends_with?([], nonempty_list) :: false
|
||||
def ends_with?(list, suffix) do
|
||||
:lists.suffix(suffix, list)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts a charlist to an atom.
|
||||
|
||||
@@ -1372,33 +1412,4 @@ defmodule List do
|
||||
defp do_pop_at([head | tail], index, default, acc) do
|
||||
do_pop_at(tail, index - 1, default, [head | acc])
|
||||
end
|
||||
|
||||
# zip
|
||||
|
||||
defp do_zip(list, acc) do
|
||||
converter = fn x, acc -> do_zip_each(to_list(x), acc) end
|
||||
|
||||
case :lists.mapfoldl(converter, [], list) do
|
||||
{_, nil} ->
|
||||
:lists.reverse(acc)
|
||||
|
||||
{mlist, heads} ->
|
||||
do_zip(mlist, [to_tuple(:lists.reverse(heads)) | acc])
|
||||
end
|
||||
end
|
||||
|
||||
defp do_zip_each(_, nil) do
|
||||
{nil, nil}
|
||||
end
|
||||
|
||||
defp do_zip_each([head | tail], acc) do
|
||||
{tail, [head | acc]}
|
||||
end
|
||||
|
||||
defp do_zip_each([], _) do
|
||||
{nil, nil}
|
||||
end
|
||||
|
||||
defp to_list(tuple) when is_tuple(tuple), do: Tuple.to_list(tuple)
|
||||
defp to_list(list) when is_list(list), do: list
|
||||
end
|
||||
|
||||
@@ -58,6 +58,6 @@ end
|
||||
|
||||
defimpl List.Chars, for: Float do
|
||||
def to_charlist(term) do
|
||||
:erlang.float_to_list(term, [:short])
|
||||
Float.to_charlist(term)
|
||||
end
|
||||
end
|
||||
|
||||
+267
-66
@@ -146,8 +146,6 @@ defmodule Macro do
|
||||
* `:delimiter` - contains the opening delimiter for sigils, strings,
|
||||
and charlists as a string (such as `"{"`, `"/"`, `"'"`, and the like)
|
||||
|
||||
* `:format` - set to `:keyword` when an atom is defined as a keyword
|
||||
|
||||
* `:do` - contains metadata about the `do` location in a function call with
|
||||
`do`-`end` blocks (when `:token_metadata` is true)
|
||||
|
||||
@@ -159,10 +157,18 @@ defmodule Macro do
|
||||
expressions inside "blocks of code", which are either direct children
|
||||
of a `__block__` or the right side of `->`. The last expression of the
|
||||
block does not have metadata if it is not followed by an end of line
|
||||
character (either a newline or `;`)
|
||||
character (either a newline or `;`). This entry may appear multiple times
|
||||
in the same metadata if the expression is surround by parens
|
||||
|
||||
* `:format` - set to `:keyword` when an atom is defined as a keyword.
|
||||
It may also be set to `:atom` to distinguish `nil`, `false`, and `true`
|
||||
|
||||
* `:indentation` - indentation of a sigil heredoc
|
||||
|
||||
* `:parens` - denotes a node was surrounded by parens for grouping.
|
||||
This entry may appear multiple times in the same metadata if
|
||||
multiple pairs are used for grouping
|
||||
|
||||
The following metadata keys are private:
|
||||
|
||||
* `:alias` - Used for alias hygiene.
|
||||
@@ -495,10 +501,8 @@ defmodule Macro do
|
||||
"""
|
||||
@doc since: "1.11.3"
|
||||
@spec generate_unique_arguments(0, context :: atom) :: []
|
||||
@spec generate_unique_arguments(pos_integer, context) :: [
|
||||
{atom, [counter: integer], context},
|
||||
...
|
||||
]
|
||||
@spec generate_unique_arguments(pos_integer, context) ::
|
||||
[{atom, [counter: integer], context}, ...]
|
||||
when context: atom
|
||||
def generate_unique_arguments(amount, context),
|
||||
do: generate_arguments(amount, context, &unique_var/2)
|
||||
@@ -778,23 +782,23 @@ defmodule Macro do
|
||||
|
||||
## Options
|
||||
|
||||
* `:unquote` - when true, this function leaves `unquote/1` and
|
||||
`unquote_splicing/1` statements unescaped, effectively unquoting
|
||||
* `: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. Defaults to `false`.
|
||||
|
||||
* `:prune_metadata` - when true, removes metadata from escaped AST
|
||||
* `: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.
|
||||
it should only be used when escaping ASTs. Defaults to `false`.
|
||||
|
||||
As an example, `ExUnit` stores the AST of every assertion, so when
|
||||
an assertion fails we can show code snippets to users. Without this
|
||||
option, each time the test module is compiled, we get a different
|
||||
MD5 of the module bytecode, because the AST contains metadata,
|
||||
As an example for `:prune_metadata`, `ExUnit` stores the AST of every
|
||||
assertion, so when an assertion fails we can show code snippets to users.
|
||||
Without this option, each time the test module is compiled, we would get a
|
||||
different MD5 of the module bytecode, because the AST contains metadata,
|
||||
such as counters, specific to the compilation environment. By pruning
|
||||
the metadata, we ensure that the module is deterministic and reduce
|
||||
the amount of data `ExUnit` needs to keep around. Only the minimal
|
||||
amount of metadata is kept, such as `:line` and `:no_parens`.
|
||||
amount of metadata is kept, such as `:line`, `:no_parens` and `:delimiter`.
|
||||
|
||||
## Comparison to `quote/2`
|
||||
|
||||
@@ -816,7 +820,8 @@ defmodule Macro do
|
||||
|
||||
iex> value = {:a, :b, :c}
|
||||
iex> quote do: unquote(value)
|
||||
{:a, :b, :c}
|
||||
** (ArgumentError) tried to unquote invalid AST: {:a, :b, :c}
|
||||
Did you forget to escape term using Macro.escape/1?
|
||||
|
||||
`escape/2` is used to escape *values* (either directly passed or variable
|
||||
bound), while `quote/2` produces syntax trees for
|
||||
@@ -829,30 +834,36 @@ defmodule Macro do
|
||||
:elixir_quote.escape(expr, kind, unquote)
|
||||
end
|
||||
|
||||
# TODO: Deprecate me on Elixir v1.22
|
||||
@doc deprecated: "Use Macro.struct_info!/2 instead"
|
||||
def struct!(module, env) when is_atom(module) do
|
||||
pairs =
|
||||
for %{field: field, default: default} <- struct_info!(module, env), do: {field, default}
|
||||
|
||||
:maps.from_list([__struct__: module] ++ pairs)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Expands the struct given by `module` in the given `env`.
|
||||
Extracts the struct information (equivalent to calling
|
||||
`module.__info__(:struct)`).
|
||||
|
||||
This is useful when a struct needs to be expanded at
|
||||
compilation time and the struct being expanded may or may
|
||||
not have been compiled. This function is also capable of
|
||||
expanding structs defined under the module being compiled.
|
||||
Calling this function also adds an export dependency on the
|
||||
given struct.
|
||||
|
||||
It will raise `CompileError` if the struct is not available.
|
||||
From Elixir v1.12, calling this function also adds an export
|
||||
dependency on the given struct.
|
||||
It will raise `ArgumentError` if the struct is not available.
|
||||
"""
|
||||
@doc since: "1.8.0"
|
||||
@spec struct!(module, Macro.Env.t()) ::
|
||||
%{required(:__struct__) => module, optional(atom) => any}
|
||||
when module: module()
|
||||
def struct!(module, env) when is_atom(module) do
|
||||
if module == env.module do
|
||||
Module.get_attribute(module, :__struct__)
|
||||
end ||
|
||||
case :elixir_map.maybe_load_struct([line: env.line], module, [], [], env) do
|
||||
{:ok, struct} -> struct
|
||||
{:error, desc} -> raise ArgumentError, List.to_string(:elixir_map.format_error(desc))
|
||||
end
|
||||
@doc since: "1.18.0"
|
||||
@spec struct_info!(module(), Macro.Env.t()) ::
|
||||
[%{field: atom(), required: boolean(), default: term()}]
|
||||
def struct_info!(module, env) when is_atom(module) do
|
||||
case :elixir_map.maybe_load_struct_info([line: env.line], module, [], true, env) do
|
||||
{:ok, info} -> info
|
||||
{:error, desc} -> raise ArgumentError, List.to_string(:elixir_map.format_error(desc))
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -897,8 +908,8 @@ defmodule Macro do
|
||||
defp find_invalid(bin) when is_binary(bin), do: nil
|
||||
|
||||
defp find_invalid(fun) when is_function(fun) do
|
||||
unless Function.info(fun, :env) == {:env, []} and
|
||||
Function.info(fun, :type) == {:type, :external} do
|
||||
if Function.info(fun, :env) != {:env, []} or
|
||||
Function.info(fun, :type) != {:type, :external} do
|
||||
{:error, fun}
|
||||
end
|
||||
end
|
||||
@@ -1127,6 +1138,10 @@ defmodule Macro do
|
||||
`Code.quoted_to_algebra/2` as a lower level function
|
||||
with more control around formatting.
|
||||
|
||||
If the AST contains invalid nodes, this function will
|
||||
attempt to inspect them, to aid debugging, although
|
||||
the elements won't be formatted accordingly.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Macro.to_string(quote(do: foo.bar(1, 2, 3)))
|
||||
@@ -1135,7 +1150,9 @@ defmodule Macro do
|
||||
"""
|
||||
@spec to_string(t()) :: String.t()
|
||||
def to_string(tree) do
|
||||
doc = Inspect.Algebra.format(Code.quoted_to_algebra(tree), 98)
|
||||
doc =
|
||||
Inspect.Algebra.format(Code.quoted_to_algebra(tree, migrate_charlists_as_sigils: true), 98)
|
||||
|
||||
IO.iodata_to_binary(doc)
|
||||
end
|
||||
|
||||
@@ -1484,7 +1501,7 @@ defmodule Macro do
|
||||
:error
|
||||
end
|
||||
|
||||
defp op_call({:"..//", _, [left, middle, right]} = ast, fun) do
|
||||
defp op_call({:..//, _, [left, middle, right]} = ast, fun) do
|
||||
left = op_to_string(left, fun, :.., :left)
|
||||
middle = op_to_string(middle, fun, :.., :right)
|
||||
right = op_to_string(right, fun, :"//", :right)
|
||||
@@ -1834,13 +1851,13 @@ defmodule Macro do
|
||||
defp do_expand_once({:__DIR__, _, atom}, env) when is_atom(atom),
|
||||
do: {:filename.dirname(env.file), true}
|
||||
|
||||
defp do_expand_once({:__ENV__, _, atom}, env) when is_atom(atom) do
|
||||
defp do_expand_once({:__ENV__, _, atom}, env) when is_atom(atom) and env.context != :match do
|
||||
env = update_in(env.versioned_vars, &maybe_escape_map/1)
|
||||
{maybe_escape_map(env), true}
|
||||
end
|
||||
|
||||
defp do_expand_once({{:., _, [{:__ENV__, _, atom}, field]}, _, []} = original, env)
|
||||
when is_atom(atom) and is_atom(field) do
|
||||
when is_atom(atom) and is_atom(field) and env.context != :match do
|
||||
if Map.has_key?(env, field) do
|
||||
{maybe_escape_map(Map.get(env, field)), true}
|
||||
else
|
||||
@@ -1935,7 +1952,7 @@ defmodule Macro do
|
||||
@spec operator?(name :: atom(), arity()) :: boolean()
|
||||
def operator?(name, arity)
|
||||
|
||||
def operator?(:"..//", 3),
|
||||
def operator?(:..//, 3),
|
||||
do: true
|
||||
|
||||
# Code.Identifier treats :// as a binary operator for precedence
|
||||
@@ -2138,9 +2155,8 @@ defmodule Macro do
|
||||
@doc """
|
||||
Converts the given argument to a string with the underscore-slash format.
|
||||
|
||||
The argument must either be an atom or a string.
|
||||
If an atom is given, it is assumed to be an Elixir module,
|
||||
so it is converted to a string and then processed.
|
||||
The argument must either be an atom, representing an Elixir module,
|
||||
or a string representing a module without the `Elixir.` prefix.
|
||||
|
||||
This function was designed to format language identifiers/tokens with the underscore-slash format,
|
||||
that's why it belongs to the `Macro` module. Do not use it as a general
|
||||
@@ -2174,7 +2190,7 @@ defmodule Macro do
|
||||
"Foo.Bar"
|
||||
|
||||
"""
|
||||
@spec underscore(module() | atom() | String.t()) :: String.t()
|
||||
@spec underscore(atom() | String.t()) :: String.t()
|
||||
def underscore(atom_or_string)
|
||||
|
||||
def underscore(atom) when is_atom(atom) do
|
||||
@@ -2318,6 +2334,15 @@ defmodule Macro do
|
||||
as a key (`:key`), or as the function name of a remote call
|
||||
(`:remote_call`).
|
||||
|
||||
## Options
|
||||
|
||||
* `:escape` - a two-arity function used to escape a quoted
|
||||
atom content, if necessary. The function receives the atom
|
||||
content as string and a quote delimiter character, which
|
||||
should always be escaped. By default the content is escaped
|
||||
such that the inspected sequence would be parsed as the
|
||||
given atom.
|
||||
|
||||
## Examples
|
||||
|
||||
### As a literal
|
||||
@@ -2366,14 +2391,14 @@ defmodule Macro do
|
||||
|
||||
"""
|
||||
@doc since: "1.14.0"
|
||||
@spec inspect_atom(:literal | :key | :remote_call, atom) :: binary
|
||||
def inspect_atom(source_format, atom)
|
||||
@spec inspect_atom(:literal | :key | :remote_call, atom, keyword) :: binary
|
||||
def inspect_atom(source_format, atom, opts \\ [])
|
||||
|
||||
def inspect_atom(:literal, atom) when is_nil(atom) or is_boolean(atom) do
|
||||
def inspect_atom(:literal, atom, _opts) when is_nil(atom) or is_boolean(atom) do
|
||||
Atom.to_string(atom)
|
||||
end
|
||||
|
||||
def inspect_atom(:literal, atom) when is_atom(atom) do
|
||||
def inspect_atom(:literal, atom, opts) when is_atom(atom) do
|
||||
binary = Atom.to_string(atom)
|
||||
|
||||
case classify_atom(atom) do
|
||||
@@ -2385,7 +2410,7 @@ defmodule Macro do
|
||||
end
|
||||
|
||||
:quoted ->
|
||||
{escaped, _} = Code.Identifier.escape(binary, ?")
|
||||
escaped = inspect_atom_escape(opts, binary, ?")
|
||||
IO.iodata_to_binary([?:, ?", escaped, ?"])
|
||||
|
||||
_ ->
|
||||
@@ -2393,7 +2418,7 @@ defmodule Macro do
|
||||
end
|
||||
end
|
||||
|
||||
def inspect_atom(:key, atom) when is_atom(atom) do
|
||||
def inspect_atom(:key, atom, opts) when is_atom(atom) do
|
||||
binary = Atom.to_string(atom)
|
||||
|
||||
case classify_atom(atom) do
|
||||
@@ -2401,7 +2426,7 @@ defmodule Macro do
|
||||
IO.iodata_to_binary([?", binary, ?", ?:])
|
||||
|
||||
:quoted ->
|
||||
{escaped, _} = Code.Identifier.escape(binary, ?")
|
||||
escaped = inspect_atom_escape(opts, binary, ?")
|
||||
IO.iodata_to_binary([?", escaped, ?", ?:])
|
||||
|
||||
_ ->
|
||||
@@ -2409,7 +2434,7 @@ defmodule Macro do
|
||||
end
|
||||
end
|
||||
|
||||
def inspect_atom(:remote_call, atom) when is_atom(atom) do
|
||||
def inspect_atom(:remote_call, atom, opts) when is_atom(atom) do
|
||||
binary = Atom.to_string(atom)
|
||||
|
||||
case inner_classify(atom) do
|
||||
@@ -2421,13 +2446,22 @@ defmodule Macro do
|
||||
if type in [:not_callable, :alias] do
|
||||
binary
|
||||
else
|
||||
elem(Code.Identifier.escape(binary, ?"), 0)
|
||||
inspect_atom_escape(opts, binary, ?")
|
||||
end
|
||||
|
||||
IO.iodata_to_binary([?", escaped, ?"])
|
||||
end
|
||||
end
|
||||
|
||||
defp inspect_atom_escape(opts, string, char) do
|
||||
if escape = opts[:escape] do
|
||||
escape.(string, char)
|
||||
else
|
||||
{escaped, _} = Code.Identifier.escape(string, char)
|
||||
escaped
|
||||
end
|
||||
end
|
||||
|
||||
# Classifies the given atom into one of the following categories:
|
||||
#
|
||||
# * `:alias` - a valid Elixir alias, like `Foo`, `Foo.Bar` and so on
|
||||
@@ -2454,7 +2488,7 @@ defmodule Macro do
|
||||
#
|
||||
defp inner_classify(atom) when is_atom(atom) do
|
||||
cond do
|
||||
atom in [:%, :%{}, :{}, :<<>>, :..., :.., :., :"..//", :->] ->
|
||||
atom in [:%, :%{}, :{}, :<<>>, :..., :.., :., :..//, :->] ->
|
||||
:not_callable
|
||||
|
||||
# <|>, ^^^, and ~~~ are deprecated
|
||||
@@ -2552,13 +2586,13 @@ defmodule Macro do
|
||||
header = dbg_format_header(env)
|
||||
|
||||
quote do
|
||||
to_debug = unquote(dbg_ast_to_debuggable(code))
|
||||
to_debug = unquote(dbg_ast_to_debuggable(code, env))
|
||||
unquote(__MODULE__).__dbg__(unquote(header), to_debug, unquote(options))
|
||||
end
|
||||
end
|
||||
|
||||
# Pipelines.
|
||||
defp dbg_ast_to_debuggable({:|>, _meta, _args} = pipe_ast) do
|
||||
defp dbg_ast_to_debuggable({:|>, _meta, _args} = pipe_ast, _env) do
|
||||
value_var = unique_var(:value, __MODULE__)
|
||||
values_acc_var = unique_var(:values, __MODULE__)
|
||||
|
||||
@@ -2591,7 +2625,7 @@ defmodule Macro do
|
||||
dbg_decomposed_binary_operators = [:&&, :||, :and, :or]
|
||||
|
||||
# Logic operators.
|
||||
defp dbg_ast_to_debuggable({op, _meta, [_left, _right]} = ast)
|
||||
defp dbg_ast_to_debuggable({op, _meta, [_left, _right]} = ast, _env)
|
||||
when op in unquote(dbg_decomposed_binary_operators) do
|
||||
acc_var = unique_var(:acc, __MODULE__)
|
||||
result_var = unique_var(:result, __MODULE__)
|
||||
@@ -2599,11 +2633,22 @@ defmodule Macro do
|
||||
quote do
|
||||
unquote(acc_var) = []
|
||||
unquote(dbg_boolean_tree(ast, acc_var, result_var))
|
||||
{:logic_op, Enum.reverse(unquote(acc_var))}
|
||||
{:logic_op, Enum.reverse(unquote(acc_var)), unquote(result_var)}
|
||||
end
|
||||
end
|
||||
|
||||
defp dbg_ast_to_debuggable({:case, _meta, [expr, [do: clauses]]} = ast) do
|
||||
defp dbg_ast_to_debuggable({:__block__, _meta, exprs} = ast, _env) when exprs != [] do
|
||||
acc_var = unique_var(:acc, __MODULE__)
|
||||
result_var = unique_var(:result, __MODULE__)
|
||||
|
||||
quote do
|
||||
unquote(acc_var) = []
|
||||
unquote(dbg_block(ast, acc_var, result_var))
|
||||
{:block, Enum.reverse(unquote(acc_var)), unquote(result_var)}
|
||||
end
|
||||
end
|
||||
|
||||
defp dbg_ast_to_debuggable({:case, _meta, [expr, [do: clauses]]} = ast, _env) do
|
||||
clauses_returning_index =
|
||||
Enum.with_index(clauses, fn {:->, meta, [left, right]}, index ->
|
||||
{:->, meta, [left, {right, index}]}
|
||||
@@ -2621,7 +2666,7 @@ defmodule Macro do
|
||||
end
|
||||
end
|
||||
|
||||
defp dbg_ast_to_debuggable({:cond, _meta, [[do: clauses]]} = ast) do
|
||||
defp dbg_ast_to_debuggable({:cond, _meta, [[do: clauses]]} = ast, _env) do
|
||||
modified_clauses =
|
||||
Enum.with_index(clauses, fn {:->, _meta, [[left], right]}, index ->
|
||||
hd(
|
||||
@@ -2642,8 +2687,104 @@ defmodule Macro do
|
||||
end
|
||||
end
|
||||
|
||||
defp dbg_ast_to_debuggable({:if, meta, [condition_ast, clauses]} = ast, env) do
|
||||
case Macro.Env.lookup_import(env, {:if, 2}) do
|
||||
[macro: Kernel] ->
|
||||
condition_result_var = unique_var(:condition_result, __MODULE__)
|
||||
|
||||
quote do
|
||||
unquote(condition_result_var) = unquote(condition_ast)
|
||||
result = unquote({:if, meta, [condition_result_var, clauses]})
|
||||
|
||||
{:if, unquote(escape(ast)), unquote(escape(condition_ast)),
|
||||
unquote(condition_result_var), result}
|
||||
end
|
||||
|
||||
_ ->
|
||||
quote do: {:value, unquote(escape(ast)), unquote(ast)}
|
||||
end
|
||||
end
|
||||
|
||||
defp dbg_ast_to_debuggable({:with, meta, args} = ast, _env) do
|
||||
{opts, clauses} = List.pop_at(args, -1)
|
||||
|
||||
acc_var = unique_var(:acc, __MODULE__)
|
||||
|
||||
modified_clauses =
|
||||
Enum.flat_map(clauses, fn
|
||||
# We only detail assignments and pattern-matching clauses that
|
||||
# can be helpful to understand how the result is constructed.
|
||||
{:<-, _meta, [left, right]} ->
|
||||
modified_left =
|
||||
case left do
|
||||
{:when, meta, [pattern, guard]} -> {:when, meta, [{pattern, quote(do: _)}, guard]}
|
||||
pattern -> {pattern, quote(do: _)}
|
||||
end
|
||||
|
||||
quote do
|
||||
[
|
||||
value = unquote(right),
|
||||
unquote(acc_var) = [{unquote(escape(right)), value} | unquote(acc_var)],
|
||||
unquote(modified_left) <- {value, unquote(acc_var)}
|
||||
]
|
||||
end
|
||||
|
||||
{:=, _meta, [left, right]} ->
|
||||
quote do
|
||||
[
|
||||
value = unquote(right),
|
||||
unquote(acc_var) = [{unquote(escape(right)), value} | unquote(acc_var)],
|
||||
unquote(left) = value
|
||||
]
|
||||
end
|
||||
|
||||
# Other expressions like side effects are omitted.
|
||||
expr ->
|
||||
[expr]
|
||||
end)
|
||||
|
||||
modified_opts =
|
||||
Enum.map(opts, fn
|
||||
{:do, do_block} ->
|
||||
{:do, {do_block, acc_var}}
|
||||
|
||||
{:else, else_block} ->
|
||||
clauses =
|
||||
Enum.map(else_block, fn
|
||||
{:->, meta, [[{:when, meta2, [pattern, guard]}], right]} ->
|
||||
{:->, meta, [[{:when, meta2, [{pattern, acc_var}, guard]}], {right, acc_var}]}
|
||||
|
||||
{:->, meta, [[left], right]} ->
|
||||
{:->, meta, [[{left, acc_var}], {right, acc_var}]}
|
||||
|
||||
invalid ->
|
||||
invalid
|
||||
end)
|
||||
|
||||
error_clause =
|
||||
quote generated: true do
|
||||
{other, _acc} -> raise WithClauseError, term: other
|
||||
end
|
||||
|
||||
{:else, clauses ++ error_clause}
|
||||
|
||||
invalid ->
|
||||
invalid
|
||||
end)
|
||||
|
||||
modified_with_ast = {:with, meta, modified_clauses ++ [modified_opts]}
|
||||
|
||||
quote do
|
||||
unquote(acc_var) = []
|
||||
|
||||
{value, acc} = unquote(modified_with_ast)
|
||||
|
||||
{:with, unquote(escape(ast)), Enum.reverse(acc), value}
|
||||
end
|
||||
end
|
||||
|
||||
# Any other AST.
|
||||
defp dbg_ast_to_debuggable(ast) do
|
||||
defp dbg_ast_to_debuggable(ast, _env) do
|
||||
quote do: {:value, unquote(escape(ast)), unquote(ast)}
|
||||
end
|
||||
|
||||
@@ -2674,6 +2815,18 @@ defmodule Macro do
|
||||
end
|
||||
end
|
||||
|
||||
defp dbg_block({:__block__, meta, exprs}, acc_var, result_var) do
|
||||
modified_exprs =
|
||||
Enum.map(exprs, fn expr ->
|
||||
quote do
|
||||
unquote(result_var) = unquote(expr)
|
||||
unquote(acc_var) = [{unquote(escape(expr)), unquote(result_var)} | unquote(acc_var)]
|
||||
end
|
||||
end)
|
||||
|
||||
{:__block__, meta, modified_exprs}
|
||||
end
|
||||
|
||||
# Made public to be called from Macro.dbg/3, so that we generate as little code
|
||||
# as possible and call out into a function as soon as we can.
|
||||
@doc false
|
||||
@@ -2711,15 +2864,27 @@ defmodule Macro do
|
||||
{[first_formatted | rest_formatted], result}
|
||||
end
|
||||
|
||||
defp dbg_format_ast_to_debug({:logic_op, components}, options) do
|
||||
{_ast, final_value} = List.last(components)
|
||||
|
||||
defp dbg_format_ast_to_debug({:logic_op, components, value}, options) do
|
||||
formatted =
|
||||
Enum.map(components, fn {ast, value} ->
|
||||
[dbg_format_ast(to_string_with_colors(ast, options)), " ", inspect(value, options), ?\n]
|
||||
end)
|
||||
|
||||
{formatted, final_value}
|
||||
{formatted, value}
|
||||
end
|
||||
|
||||
defp dbg_format_ast_to_debug({:block, components, value}, options) do
|
||||
formatted =
|
||||
[
|
||||
dbg_maybe_underline("Code block", options),
|
||||
":\n(\n",
|
||||
Enum.map(components, fn {ast, value} ->
|
||||
[" ", dbg_format_ast_with_value(ast, value, options)]
|
||||
end),
|
||||
")\n"
|
||||
]
|
||||
|
||||
{formatted, value}
|
||||
end
|
||||
|
||||
defp dbg_format_ast_to_debug({:case, ast, expr_value, clause_index, value}, options) do
|
||||
@@ -2755,6 +2920,42 @@ defmodule Macro do
|
||||
{formatted, value}
|
||||
end
|
||||
|
||||
defp dbg_format_ast_to_debug(
|
||||
{:if, ast, condition_ast, condition_result, result},
|
||||
options
|
||||
) do
|
||||
formatted = [
|
||||
dbg_maybe_underline("If condition", options),
|
||||
":\n",
|
||||
dbg_format_ast_with_value(condition_ast, condition_result, options),
|
||||
?\n,
|
||||
dbg_maybe_underline("If expression", options),
|
||||
":\n",
|
||||
dbg_format_ast_with_value(ast, result, options)
|
||||
]
|
||||
|
||||
{formatted, result}
|
||||
end
|
||||
|
||||
defp dbg_format_ast_to_debug({:with, ast, clauses, result}, options) do
|
||||
formatted_clauses =
|
||||
Enum.map(clauses, fn {clause_ast, clause_result} ->
|
||||
dbg_format_ast_with_value(clause_ast, clause_result, options)
|
||||
end)
|
||||
|
||||
formatted = [
|
||||
dbg_maybe_underline("With clauses", options),
|
||||
":\n",
|
||||
formatted_clauses,
|
||||
?\n,
|
||||
dbg_maybe_underline("With expression", options),
|
||||
":\n",
|
||||
dbg_format_ast_with_value(ast, result, options)
|
||||
]
|
||||
|
||||
{formatted, result}
|
||||
end
|
||||
|
||||
defp dbg_format_ast_to_debug({:value, code_ast, value}, options) do
|
||||
{dbg_format_ast_with_value(code_ast, value, options), value}
|
||||
end
|
||||
|
||||
@@ -102,8 +102,10 @@ defmodule Macro.Env do
|
||||
]
|
||||
|
||||
# Define the __struct__ callbacks by hand for bootstrap reasons.
|
||||
{struct, [], kv, body} = Kernel.Utils.defstruct(__MODULE__, fields, false, __ENV__)
|
||||
def __struct__(), do: unquote(:elixir_quote.escape(struct, :none, false))
|
||||
{_struct, [], escaped_struct, kv, body} =
|
||||
Kernel.Utils.defstruct(__MODULE__, fields, false, __ENV__)
|
||||
|
||||
def __struct__(), do: unquote(escaped_struct)
|
||||
def __struct__(unquote(kv)), do: unquote(body)
|
||||
|
||||
@doc """
|
||||
@@ -349,6 +351,11 @@ defmodule Macro.Env do
|
||||
|
||||
* #{trace_option}
|
||||
|
||||
* `:info_callback` - a function to use instead of `c:Module.__info__/1`.
|
||||
The function will be invoked with `:functions` or `:macros` argument.
|
||||
It has to return a list of `{function, arity}` key value pairs.
|
||||
If it fails, it defaults to using module metadata based on `module_info/1`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> env = __ENV__
|
||||
@@ -367,15 +374,27 @@ defmodule Macro.Env do
|
||||
iex> Macro.Env.lookup_import(env, {:is_odd, 1})
|
||||
[{:macro, Integer}]
|
||||
|
||||
## Info callback override
|
||||
|
||||
iex> env = __ENV__
|
||||
iex> Macro.Env.lookup_import(env, {:flatten, 1})
|
||||
[]
|
||||
iex> {:ok, env} = Macro.Env.define_import(env, [line: 10], SomeModule, [info_callback: fn :functions -> [{:flatten, 1}]; :macros -> [{:some, 2}]; end])
|
||||
iex> Macro.Env.lookup_import(env, {:flatten, 1})
|
||||
[{:function, SomeModule}]
|
||||
iex> Macro.Env.lookup_import(env, {:some, 2})
|
||||
[{:macro, SomeModule}]
|
||||
|
||||
"""
|
||||
@doc since: "1.17.0"
|
||||
@spec define_import(t, Macro.metadata(), module) :: {:ok, t} | {:error, String.t()}
|
||||
@spec define_import(t, Macro.metadata(), module, keyword) :: {:ok, t} | {:error, String.t()}
|
||||
def define_import(env, meta, module, opts \\ [])
|
||||
when is_list(meta) and is_atom(module) and is_list(opts) do
|
||||
{trace, opts} = Keyword.pop(opts, :trace, true)
|
||||
{warnings, opts} = Keyword.pop(opts, :emit_warnings, true)
|
||||
{info_callback, opts} = Keyword.pop(opts, :info_callback, &module.__info__/1)
|
||||
|
||||
result = :elixir_import.import(meta, module, opts, env, warnings, trace)
|
||||
result = :elixir_import.import(meta, module, opts, env, warnings, trace, info_callback)
|
||||
maybe_define_error(result, :elixir_import)
|
||||
end
|
||||
|
||||
@@ -426,7 +445,7 @@ defmodule Macro.Env do
|
||||
maybe_define_error(result, :elixir_aliases)
|
||||
end
|
||||
|
||||
defp maybe_define_error({:ok, env}, _mod),
|
||||
defp maybe_define_error({:ok, _info, env}, _mod),
|
||||
do: {:ok, env}
|
||||
|
||||
defp maybe_define_error({:error, reason}, mod),
|
||||
@@ -583,7 +602,7 @@ defmodule Macro.Env do
|
||||
:elixir_dispatch.check_deprecated(:macro, meta, receiver, name, arity, env)
|
||||
end
|
||||
|
||||
quoted = expander.(args, :elixir_env.env_to_ex(env))
|
||||
quoted = expander.(args, env)
|
||||
next = :elixir_module.next_counter(env.module)
|
||||
:elixir_quote.linify_with_context_counter(expansion_meta, {receiver, next}, quoted)
|
||||
end
|
||||
|
||||
+13
-16
@@ -96,11 +96,11 @@ defmodule Map do
|
||||
iex> %{map | one: "one"}
|
||||
%{one: "one", two: 2}
|
||||
|
||||
Or any other key:
|
||||
Or any other keys:
|
||||
|
||||
iex> other_map = %{"three" => 3, "four" => 4}
|
||||
iex> %{other_map | "three" => "three"}
|
||||
%{"four" => 4, "three" => "three"}
|
||||
iex> other_map = %{"three" => 3, "four" => 4, "five" => 5}
|
||||
iex> %{other_map | "three" => "three", "four" => "four"}
|
||||
%{"five" => 5, "four" => "four", "three" => "three"}
|
||||
|
||||
When a key that does not exist in the map is updated a `KeyError` exception will be raised:
|
||||
|
||||
@@ -128,6 +128,8 @@ defmodule Map do
|
||||
@doc """
|
||||
Builds a map from the given `keys` and the fixed `value`.
|
||||
|
||||
Inlined by the compiler.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Map.from_keys([1, 2, 3], :number)
|
||||
@@ -136,9 +138,7 @@ defmodule Map do
|
||||
"""
|
||||
@doc since: "1.14.0"
|
||||
@spec from_keys([key], value) :: map
|
||||
def from_keys(keys, value) do
|
||||
:maps.from_keys(keys, value)
|
||||
end
|
||||
defdelegate from_keys(keys, value), to: :maps
|
||||
|
||||
@doc """
|
||||
Returns all keys from `map`.
|
||||
@@ -709,10 +709,10 @@ defmodule Map do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Removes the value associated with `key` in `map` and returns the value
|
||||
and the updated map, or it raises if `key` is not present.
|
||||
Removes and returns the value associated with `key` in `map` alongside
|
||||
the updated map, or raises if `key` is not present.
|
||||
|
||||
Behaves the same as `pop/3` but raises if `key` is not present in `map`.
|
||||
Behaves the same as `pop/3` but raises a `KeyError` exception if `key` is not present in `map`.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -1011,9 +1011,8 @@ defmodule Map do
|
||||
@doc """
|
||||
Converts a `struct` to map.
|
||||
|
||||
It accepts the struct module or a struct itself and
|
||||
simply removes the `__struct__` field from the given struct
|
||||
or from a new struct generated from the given module.
|
||||
It accepts a struct and simply removes the `__struct__` field
|
||||
from the given struct.
|
||||
|
||||
## Example
|
||||
|
||||
@@ -1021,15 +1020,13 @@ defmodule Map do
|
||||
defstruct [:name]
|
||||
end
|
||||
|
||||
Map.from_struct(User)
|
||||
#=> %{name: nil}
|
||||
|
||||
Map.from_struct(%User{name: "john"})
|
||||
#=> %{name: "john"}
|
||||
|
||||
"""
|
||||
@spec from_struct(atom | struct) :: map
|
||||
def from_struct(struct) when is_atom(struct) do
|
||||
IO.warn("Map.from_struct/1 with a module is deprecated, please pass a struct instead")
|
||||
delete(struct.__struct__(), :__struct__)
|
||||
end
|
||||
|
||||
|
||||
@@ -401,7 +401,7 @@ defmodule MapSet do
|
||||
|
||||
"""
|
||||
@doc since: "1.15.0"
|
||||
@spec split_with(MapSet.t(), (any() -> as_boolean(term))) :: {MapSet.t(), MapSet.t()}
|
||||
@spec split_with(MapSet.t(), (term() -> as_boolean(term))) :: {MapSet.t(), MapSet.t()}
|
||||
def split_with(%MapSet{map: map}, fun) when is_function(fun, 1) do
|
||||
{while_true, while_false} = Map.split_with(map, fn {key, _} -> fun.(key) end)
|
||||
{%MapSet{map: while_true}, %MapSet{map: while_false}}
|
||||
|
||||
+103
-70
@@ -270,6 +270,9 @@ defmodule Module do
|
||||
a dependency on it, causing the module to be recompiled as soon
|
||||
as the file is added.
|
||||
|
||||
For more control over when a module is recompiled, see
|
||||
[`__mix_recompile__?/0`](`m:Mix.Tasks.Compile.Elixir#module-__mix_recompile__-0`).
|
||||
|
||||
### `@file`
|
||||
|
||||
Changes the filename used in stacktraces for the function or macro that
|
||||
@@ -441,7 +444,8 @@ defmodule Module do
|
||||
* `@typep` - defines a private type to be used in `@spec`
|
||||
* `@opaque` - defines an opaque type to be used in `@spec`
|
||||
* `@spec` - provides a specification for a function
|
||||
* `@callback` - provides a specification for a behaviour callback
|
||||
* `@callback` - provides a specification for a behaviour callback (and generates
|
||||
a `behaviour_info/1` function in the module, see below)
|
||||
* `@macrocallback` - provides a specification for a macro behaviour callback
|
||||
* `@optional_callbacks` - specifies which behaviour callbacks and macro
|
||||
behaviour callbacks are optional
|
||||
@@ -590,6 +594,78 @@ defmodule Module do
|
||||
`@compile {:no_warn_undefined, {Mod, fun, arity}}` - does not warn if
|
||||
the given module or the given `Mod.fun/arity` are not defined
|
||||
|
||||
## Generated functions
|
||||
|
||||
Sometimes the compiler will generate public functions within modules. These
|
||||
are documented below.
|
||||
|
||||
### `behaviour_info/1`
|
||||
|
||||
This function is generated for modules that define a behaviour, that is,
|
||||
that have one or more `@callback` definitions. The signature for this function,
|
||||
expressed as a spec, is:
|
||||
|
||||
@spec behaviour_info(:callbacks) :: [function_info]
|
||||
when function_info: {function_name :: atom(), arity :: non_neg_integer()}
|
||||
|
||||
@spec behaviour_info(:optional_callbacks) :: [function_info]
|
||||
when function_info: {function_name :: atom(), arity :: non_neg_integer()}
|
||||
|
||||
`behaviour_info(:callbacks)` includes optional callbacks.
|
||||
|
||||
For example:
|
||||
|
||||
iex> Enum.sort(GenServer.behaviour_info(:callbacks))
|
||||
[
|
||||
code_change: 3,
|
||||
format_status: 1,
|
||||
format_status: 2,
|
||||
handle_call: 3,
|
||||
handle_cast: 2,
|
||||
handle_continue: 2,
|
||||
handle_info: 2,
|
||||
init: 1,
|
||||
terminate: 2
|
||||
]
|
||||
|
||||
### `module_info/0`
|
||||
|
||||
This function is generated for all modules. It returns all the attributes
|
||||
returned by `module_info/1` (see below), but as a single keyword list. See also the
|
||||
[Erlang documentation](https://www.erlang.org/doc/system/modules.html#module_info-0).
|
||||
|
||||
### `module_info/1`
|
||||
|
||||
This function is generated for all modules and returns
|
||||
information about the module. The signature for this function,
|
||||
expressed as a spec, is:
|
||||
|
||||
@spec module_info(:module) :: module() # Returns the module itself
|
||||
@spec module_info(:attributes) :: keyword()
|
||||
@spec module_info(:compile) :: keyword()
|
||||
@spec module_info(:md5) :: binary()
|
||||
@spec module_info(:nifs) :: module()
|
||||
@spec module_info(:exports) :: [function_info]
|
||||
when function_info: {function_name :: atom(), arity :: non_neg_integer()}
|
||||
@spec module_info(:functions) :: [function_info]
|
||||
when function_info: {function_name :: atom(), arity :: non_neg_integer()}
|
||||
|
||||
For example:
|
||||
|
||||
iex> URI.module_info(:module)
|
||||
URI
|
||||
iex> {:decode_www_form, 1} in URI.module_info(:exports)
|
||||
true
|
||||
|
||||
For more information about `module_info/1`, also check out the [Erlang
|
||||
documentation](https://www.erlang.org/doc/system/modules.html#module_info-1).
|
||||
|
||||
### `__info__/1`
|
||||
|
||||
This function is generated for all modules. It's similar to `module_info/1` but
|
||||
includes some additional Elixir-specific information, such as struct and macro
|
||||
information. For documentation, see `c:Module.__info__/1`.
|
||||
|
||||
'''
|
||||
|
||||
@type definition :: {atom, arity}
|
||||
@@ -628,7 +704,8 @@ defmodule Module do
|
||||
@callback __info__(:macros) :: keyword()
|
||||
@callback __info__(:md5) :: binary()
|
||||
@callback __info__(:module) :: module()
|
||||
@callback __info__(:struct) :: list(%{field: atom(), required: boolean()}) | nil
|
||||
@callback __info__(:struct) ::
|
||||
list(%{required(:field) => atom(), optional(:default) => term()}) | nil
|
||||
|
||||
@doc """
|
||||
Returns information about module attributes used by Elixir.
|
||||
@@ -669,6 +746,10 @@ defmodule Module do
|
||||
doc:
|
||||
"If set to `true` generates a default protocol implementation for all types (inside `defprotocol`)."
|
||||
},
|
||||
undefined_impl_description: %{
|
||||
doc:
|
||||
"A string with additional description to be used on `Protocol.UndefinedError` when looking up the implementation fails."
|
||||
},
|
||||
for: %{
|
||||
doc:
|
||||
"The current module/type a protocol implementation is being defined for (inside `defimpl`)."
|
||||
@@ -753,50 +834,7 @@ defmodule Module do
|
||||
:elixir_module.is_open(module)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Evaluates the quoted contents in the given module's context.
|
||||
|
||||
A list of environment options can also be given as argument.
|
||||
See `Code.eval_string/3` for more information.
|
||||
|
||||
Raises an error if the module was already compiled.
|
||||
|
||||
## Examples
|
||||
|
||||
defmodule Foo do
|
||||
contents =
|
||||
quote do
|
||||
def sum(a, b), do: a + b
|
||||
end
|
||||
|
||||
Module.eval_quoted(__MODULE__, contents)
|
||||
end
|
||||
|
||||
Foo.sum(1, 2)
|
||||
#=> 3
|
||||
|
||||
For convenience, you can pass any `Macro.Env` struct, such
|
||||
as `__ENV__/0`, as the first argument or as options. Both
|
||||
the module and all options will be automatically extracted
|
||||
from the environment:
|
||||
|
||||
defmodule Foo do
|
||||
contents =
|
||||
quote do
|
||||
def sum(a, b), do: a + b
|
||||
end
|
||||
|
||||
Module.eval_quoted(__ENV__, contents)
|
||||
end
|
||||
|
||||
Foo.sum(1, 2)
|
||||
#=> 3
|
||||
|
||||
Note that if you pass a `Macro.Env` struct as first argument
|
||||
while also passing `opts`, they will be merged with `opts`
|
||||
having precedence.
|
||||
"""
|
||||
@spec eval_quoted(module | Macro.Env.t(), Macro.t(), list, keyword | Macro.Env.t()) :: term
|
||||
@deprecated "Use Code.eval_quoted/3 instead"
|
||||
def eval_quoted(module_or_env, quoted, binding \\ [], opts \\ [])
|
||||
|
||||
def eval_quoted(%Macro.Env{} = env, quoted, binding, opts)
|
||||
@@ -827,7 +865,8 @@ defmodule Module do
|
||||
the given quoted expressions.
|
||||
|
||||
The line where the module is defined and its file **must**
|
||||
be passed as options.
|
||||
be passed as options. See `Code.env_for_eval/1` for a complete
|
||||
list of options.
|
||||
|
||||
It returns a tuple of shape `{:module, module, binary, term}`
|
||||
where `module` is the module name, `binary` is the module
|
||||
@@ -871,7 +910,7 @@ defmodule Module do
|
||||
end
|
||||
|
||||
def create(module, quoted, opts) when is_atom(module) and is_list(opts) do
|
||||
unless Keyword.has_key?(opts, :file) do
|
||||
if not Keyword.has_key?(opts, :file) do
|
||||
raise ArgumentError, "expected :file to be given as option"
|
||||
end
|
||||
|
||||
@@ -882,7 +921,7 @@ defmodule Module do
|
||||
defp create(meta, module, quoted, env_or_opts) do
|
||||
next = :elixir_module.next_counter(nil)
|
||||
quoted = :elixir_quote.linify_with_context_counter(meta, {module, next}, quoted)
|
||||
:elixir_module.compile(module, quoted, [], false, :elixir.env_for_eval(env_or_opts))
|
||||
:elixir_module.compile(meta, module, quoted, [], false, :elixir.env_for_eval(env_or_opts))
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -1381,15 +1420,7 @@ defmodule Module do
|
||||
def delete_definition(module, {name, arity})
|
||||
when is_atom(module) and is_atom(name) and is_integer(arity) do
|
||||
assert_not_readonly!(__ENV__.function, module)
|
||||
|
||||
case :elixir_def.take_definition(module, {name, arity}) do
|
||||
false ->
|
||||
false
|
||||
|
||||
_ ->
|
||||
:elixir_locals.yank({name, arity}, module)
|
||||
true
|
||||
end
|
||||
:elixir_def.take_definition(module, {name, arity}) != false
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -1418,8 +1449,7 @@ defmodule Module do
|
||||
"overridable because it was not defined"
|
||||
|
||||
clause ->
|
||||
neighbours = :elixir_locals.yank(tuple, module)
|
||||
:elixir_overridable.record_overridable(module, tuple, clause, neighbours)
|
||||
:elixir_overridable.record_overridable(module, tuple, clause)
|
||||
end
|
||||
|
||||
other ->
|
||||
@@ -1643,11 +1673,11 @@ defmodule Module do
|
||||
|
||||
case :ets.lookup(set, key) do
|
||||
[{_, _, :accumulate, traces}] ->
|
||||
trace_attribute(true, module, traces, set, key, [])
|
||||
trace_attribute(true, module, traces, set, key, nil, [])
|
||||
reverse_values(:ets.take(bag, {:accumulate, key}), [])
|
||||
|
||||
[{_, value, _, traces}] ->
|
||||
trace_attribute(module, traces)
|
||||
trace_attribute(module, key, nil, traces)
|
||||
:ets.delete(set, key)
|
||||
value
|
||||
|
||||
@@ -1910,7 +1940,7 @@ defmodule Module do
|
||||
|
||||
_ ->
|
||||
message =
|
||||
case :ets.lookup(set, :__struct__) do
|
||||
case :ets.lookup(set, {:elixir, :struct}) do
|
||||
[] ->
|
||||
"warning: module attribute @derive was set but never used (it must come before defstruct)"
|
||||
|
||||
@@ -1964,15 +1994,15 @@ defmodule Module do
|
||||
default
|
||||
|
||||
[{_, _, :accumulate, traces}] ->
|
||||
trace_attribute(trace?, module, traces, set, key, [])
|
||||
trace_attribute(trace?, module, traces, set, key, caller_line, [])
|
||||
lookup_accumulate_attribute(bag, key, default, last_accumulated?)
|
||||
|
||||
[{_, value, warn_line, traces}] when is_integer(warn_line) ->
|
||||
trace_attribute(trace?, module, traces, set, key, [{3, :used}])
|
||||
trace_attribute(trace?, module, traces, set, key, caller_line, [{3, :used}])
|
||||
value
|
||||
|
||||
[{_, value, _, traces}] ->
|
||||
trace_attribute(trace?, module, traces, set, key, [])
|
||||
trace_attribute(trace?, module, traces, set, key, caller_line, [])
|
||||
value
|
||||
|
||||
[] when is_integer(caller_line) ->
|
||||
@@ -2000,12 +2030,15 @@ defmodule Module do
|
||||
end
|
||||
end
|
||||
|
||||
defp trace_attribute(module, traces) do
|
||||
defp trace_attribute(module, key, caller_line, traces) do
|
||||
:lists.foreach(
|
||||
fn {line, lexical_tracker, tracers, aliases} ->
|
||||
line = caller_line || line
|
||||
|
||||
env = %{
|
||||
Macro.Env.__struct__()
|
||||
| line: line,
|
||||
file: "@#{key}",
|
||||
lexical_tracker: lexical_tracker,
|
||||
module: module,
|
||||
tracers: tracers
|
||||
@@ -2022,10 +2055,10 @@ defmodule Module do
|
||||
)
|
||||
end
|
||||
|
||||
defp trace_attribute(trace?, module, traces, set, key, updates) do
|
||||
defp trace_attribute(trace?, module, traces, set, key, caller_line, updates) do
|
||||
updates =
|
||||
if trace? and traces != [] do
|
||||
trace_attribute(module, traces)
|
||||
trace_attribute(module, key, caller_line, traces)
|
||||
updates ++ [{4, []}]
|
||||
else
|
||||
updates
|
||||
@@ -2229,7 +2262,7 @@ defmodule Module do
|
||||
end
|
||||
|
||||
defp preprocess_attribute(:nifs, value) do
|
||||
unless function_arity_list?(value) do
|
||||
if not function_arity_list?(value) do
|
||||
raise ArgumentError,
|
||||
"@nifs is a built-in module attribute for specifying a list " <>
|
||||
"of functions and their arities that are NIFs, got: #{inspect(value)}"
|
||||
|
||||
@@ -4,7 +4,7 @@ defmodule Module.Behaviour do
|
||||
|
||||
@doc false
|
||||
def callbacks(behaviour) do
|
||||
for callback <- behaviour_info(behaviour, :callbacks) do
|
||||
for callback <- behaviour.behaviour_info(:callbacks) do
|
||||
{pair, _kind} = normalize_macro_or_function_callback(callback)
|
||||
pair
|
||||
end
|
||||
@@ -33,9 +33,7 @@ defmodule Module.Behaviour do
|
||||
module: module,
|
||||
file: file,
|
||||
line: line,
|
||||
# Map containing the callbacks to be implemented
|
||||
callbacks: %{},
|
||||
# list of warnings {message, env}
|
||||
warnings: []
|
||||
}
|
||||
end
|
||||
@@ -57,8 +55,8 @@ defmodule Module.Behaviour do
|
||||
warn(context, {:module_does_not_define_behaviour, context.module, behaviour})
|
||||
|
||||
true ->
|
||||
optional_callbacks = behaviour_info(behaviour, :optional_callbacks)
|
||||
callbacks = behaviour_info(behaviour, :callbacks)
|
||||
optional_callbacks = behaviour.behaviour_info(:optional_callbacks)
|
||||
callbacks = behaviour.behaviour_info(:callbacks)
|
||||
Enum.reduce(callbacks, context, &add_callback(&2, &1, behaviour, optional_callbacks))
|
||||
end
|
||||
end)
|
||||
@@ -144,7 +142,7 @@ defmodule Module.Behaviour do
|
||||
end
|
||||
|
||||
defp impl_behaviours({function, arity}, defaults, kind, value, behaviours, callbacks) do
|
||||
impls = for n <- arity..(arity - defaults), do: {function, n}
|
||||
impls = for n <- (arity - defaults)..arity, do: {function, n}
|
||||
impl_behaviours(impls, kind, value, behaviours, callbacks)
|
||||
end
|
||||
|
||||
@@ -266,13 +264,6 @@ defmodule Module.Behaviour do
|
||||
". The known callbacks are:\n#{formatted_callbacks}\n"
|
||||
end
|
||||
|
||||
defp behaviour_info(module, key) do
|
||||
case module.behaviour_info(key) do
|
||||
list when is_list(list) -> list
|
||||
:undefined -> []
|
||||
end
|
||||
end
|
||||
|
||||
defp normalize_macro_or_function_callback({function_name, arity}) do
|
||||
case :erlang.atom_to_list(function_name) do
|
||||
# Macros are always provided one extra argument in behaviour_info/1
|
||||
@@ -284,7 +275,11 @@ defmodule Module.Behaviour do
|
||||
end
|
||||
end
|
||||
|
||||
def format_warning({:undefined_behaviour, module, behaviour}) do
|
||||
def format_diagnostic(warning) do
|
||||
%{message: IO.iodata_to_binary(format_warning(warning))}
|
||||
end
|
||||
|
||||
defp format_warning({:undefined_behaviour, module, behaviour}) do
|
||||
[
|
||||
"@behaviour ",
|
||||
inspect(behaviour),
|
||||
@@ -294,12 +289,12 @@ defmodule Module.Behaviour do
|
||||
]
|
||||
end
|
||||
|
||||
def format_warning({:module_does_not_define_behaviour, module, behaviour}) do
|
||||
defp format_warning({:module_does_not_define_behaviour, module, behaviour}) do
|
||||
["module ", inspect(behaviour), " is not a behaviour (in module ", inspect(module), ")"]
|
||||
end
|
||||
|
||||
def format_warning({:duplicate_behaviour, module, behaviour, conflict, kind, callback})
|
||||
when conflict == behaviour do
|
||||
defp format_warning({:duplicate_behaviour, module, behaviour, conflict, kind, callback})
|
||||
when conflict == behaviour do
|
||||
[
|
||||
"the behaviour ",
|
||||
inspect(behaviour),
|
||||
@@ -311,7 +306,7 @@ defmodule Module.Behaviour do
|
||||
]
|
||||
end
|
||||
|
||||
def format_warning({:duplicate_behaviour, module, behaviour, conflict, kind, callback}) do
|
||||
defp format_warning({:duplicate_behaviour, module, behaviour, conflict, kind, callback}) do
|
||||
[
|
||||
"conflicting behaviours found. Callback ",
|
||||
format_definition(kind, callback),
|
||||
@@ -325,7 +320,7 @@ defmodule Module.Behaviour do
|
||||
]
|
||||
end
|
||||
|
||||
def format_warning({:missing_callback, module, callback, kind, behaviour}) do
|
||||
defp format_warning({:missing_callback, module, callback, kind, behaviour}) do
|
||||
[
|
||||
format_callback(callback, kind, behaviour),
|
||||
" is not implemented (in module ",
|
||||
@@ -334,7 +329,7 @@ defmodule Module.Behaviour do
|
||||
]
|
||||
end
|
||||
|
||||
def format_warning({:callback_mismatch, module, callback, kind, wrong_kind, behaviour}) do
|
||||
defp format_warning({:callback_mismatch, module, callback, kind, wrong_kind, behaviour}) do
|
||||
[
|
||||
format_callback(callback, kind, behaviour),
|
||||
" was implemented as \"",
|
||||
@@ -347,14 +342,14 @@ defmodule Module.Behaviour do
|
||||
]
|
||||
end
|
||||
|
||||
def format_warning({:private_function, callback, kind}) do
|
||||
defp format_warning({:private_function, callback, kind}) do
|
||||
[
|
||||
format_definition(kind, callback),
|
||||
" is private, @impl attribute is always discarded for private functions/macros"
|
||||
]
|
||||
end
|
||||
|
||||
def format_warning({:no_behaviours, callback, kind, value}) do
|
||||
defp format_warning({:no_behaviours, callback, kind, value}) do
|
||||
[
|
||||
"got \"@impl ",
|
||||
inspect(value),
|
||||
@@ -364,7 +359,7 @@ defmodule Module.Behaviour do
|
||||
]
|
||||
end
|
||||
|
||||
def format_warning({:impl_not_defined, callback, kind, {_fa, behaviour}}) do
|
||||
defp format_warning({:impl_not_defined, callback, kind, {_fa, behaviour}}) do
|
||||
[
|
||||
"got \"@impl false\" for ",
|
||||
format_definition(kind, callback),
|
||||
@@ -373,7 +368,7 @@ defmodule Module.Behaviour do
|
||||
]
|
||||
end
|
||||
|
||||
def format_warning({:impl_defined, callback, kind, callbacks}) do
|
||||
defp format_warning({:impl_defined, callback, kind, callbacks}) do
|
||||
[
|
||||
"got \"@impl true\" for ",
|
||||
format_definition(kind, callback),
|
||||
@@ -382,7 +377,7 @@ defmodule Module.Behaviour do
|
||||
]
|
||||
end
|
||||
|
||||
def format_warning({:behaviour_not_declared, callback, kind, behaviour}) do
|
||||
defp format_warning({:behaviour_not_declared, callback, kind, behaviour}) do
|
||||
[
|
||||
"got \"@impl ",
|
||||
inspect(behaviour),
|
||||
@@ -392,7 +387,7 @@ defmodule Module.Behaviour do
|
||||
]
|
||||
end
|
||||
|
||||
def format_warning({:behaviour_not_defined, callback, kind, behaviour, callbacks}) do
|
||||
defp format_warning({:behaviour_not_defined, callback, kind, behaviour, callbacks}) do
|
||||
[
|
||||
"got \"@impl ",
|
||||
inspect(behaviour),
|
||||
@@ -403,7 +398,7 @@ defmodule Module.Behaviour do
|
||||
]
|
||||
end
|
||||
|
||||
def format_warning({:missing_impl, callback, kind, behaviour}) do
|
||||
defp format_warning({:missing_impl, callback, kind, behaviour}) do
|
||||
[
|
||||
"module attribute @impl was not set for ",
|
||||
format_definition(kind, callback),
|
||||
|
||||
@@ -1,258 +0,0 @@
|
||||
# This is an Elixir module responsible for tracking
|
||||
# calls in order to extract Elixir modules' behaviour
|
||||
# during compilation time.
|
||||
#
|
||||
# ## Implementation
|
||||
#
|
||||
# The implementation uses ETS to track all dependencies
|
||||
# resembling a graph. The keys and what they point to are:
|
||||
#
|
||||
# * `:reattach` points to `{name, arity}`
|
||||
# * `{:local, {name, arity}}` points to `{{name, arity}, line, macro_dispatch?}`
|
||||
# * `{:import, {name, arity}}` points to `Module`
|
||||
#
|
||||
# This is built on top of the internal module tables.
|
||||
defmodule Module.LocalsTracker do
|
||||
@moduledoc false
|
||||
|
||||
@defmacros [:defmacro, :defmacrop]
|
||||
|
||||
@doc """
|
||||
Adds and tracks defaults for a definition into the tracker.
|
||||
"""
|
||||
def add_defaults({_set, bag}, kind, {name, arity} = pair, defaults, meta) do
|
||||
for i <- :lists.seq(arity - defaults, arity - 1) do
|
||||
put_edge(bag, {:local, {name, i}}, {pair, get_line(meta), kind in @defmacros})
|
||||
end
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
@doc """
|
||||
Adds a local dispatch from-to the given target.
|
||||
"""
|
||||
def add_local({_set, bag}, from, to, meta, macro_dispatch?)
|
||||
when is_tuple(from) and is_tuple(to) and is_boolean(macro_dispatch?) do
|
||||
put_edge(bag, {:local, from}, {to, get_position(meta), macro_dispatch?})
|
||||
:ok
|
||||
end
|
||||
|
||||
@doc """
|
||||
Adds an import dispatch to the given target.
|
||||
"""
|
||||
def add_import({set, _bag}, function, module, imported)
|
||||
when is_tuple(function) and is_atom(module) do
|
||||
put_edge(set, {:import, imported}, module)
|
||||
:ok
|
||||
end
|
||||
|
||||
@doc """
|
||||
Yanks a local node. Returns its in and out vertices in a tuple.
|
||||
"""
|
||||
def yank({_set, bag}, local) do
|
||||
:lists.usort(take_out_neighbours(bag, {:local, local}))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Reattach a previously yanked node.
|
||||
"""
|
||||
def reattach({_set, bag}, tuple, kind, function, out_neighbours, meta) do
|
||||
for out_neighbour <- out_neighbours do
|
||||
put_edge(bag, {:local, function}, out_neighbour)
|
||||
end
|
||||
|
||||
# Make a call from the old function to the new one
|
||||
if function != tuple do
|
||||
put_edge(bag, {:local, function}, {tuple, get_line(meta), kind in @defmacros})
|
||||
end
|
||||
|
||||
# Finally marked the new one as reattached
|
||||
put_edge(bag, :reattach, tuple)
|
||||
:ok
|
||||
end
|
||||
|
||||
@doc """
|
||||
Collect all conflicting imports with the given functions
|
||||
"""
|
||||
def collect_imports_conflicts({set, _bag}, all_defined) do
|
||||
for {pair, _, meta, _} <- all_defined, n = out_neighbour(set, {:import, pair}) do
|
||||
{meta, {n, pair}}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Collect all unused definitions based on the private
|
||||
given, also accounting the expected number of default
|
||||
clauses a private function have.
|
||||
"""
|
||||
def collect_unused_locals({_set, bag}, all_defined, private) do
|
||||
reachable =
|
||||
Enum.reduce(all_defined, %{}, fn {pair, kind, _, _}, acc ->
|
||||
if kind in [:def, :defmacro] do
|
||||
reachable_from(bag, pair, acc)
|
||||
else
|
||||
acc
|
||||
end
|
||||
end)
|
||||
|
||||
reattached = :lists.usort(out_neighbours(bag, :reattach))
|
||||
{unreachable(reachable, reattached, private), collect_warnings(reachable, private)}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Collect undefined functions based on local calls and existing definitions.
|
||||
"""
|
||||
def collect_undefined_locals({set, bag}, all_defined, file) do
|
||||
undefined =
|
||||
for {pair, _, meta, _} <- all_defined,
|
||||
{{local_name, _} = local, position, macro_dispatch?} <-
|
||||
out_neighbours(bag, {:local, pair}),
|
||||
error = undefined_local_error(set, local, macro_dispatch?) do
|
||||
file =
|
||||
case Keyword.get(meta, :file) do
|
||||
{keep_file, _keep_line} -> keep_file
|
||||
nil -> file
|
||||
end
|
||||
|
||||
meta = build_meta(position, local_name)
|
||||
{pair, meta, file, local, error}
|
||||
end
|
||||
|
||||
:lists.usort(undefined)
|
||||
end
|
||||
|
||||
defp undefined_local_error(set, local, true) do
|
||||
case :ets.member(set, {:def, local}) do
|
||||
true -> false
|
||||
false -> :undefined_function
|
||||
end
|
||||
end
|
||||
|
||||
defp undefined_local_error(set, local, false) do
|
||||
try do
|
||||
if :ets.lookup_element(set, {:def, local}, 2) in @defmacros do
|
||||
:incorrect_dispatch
|
||||
else
|
||||
false
|
||||
end
|
||||
catch
|
||||
_, _ -> :undefined_function
|
||||
end
|
||||
end
|
||||
|
||||
defp unreachable(reachable, reattached, private) do
|
||||
for {tuple, kind, _, _} <- private,
|
||||
not reachable?(tuple, kind, reachable, reattached),
|
||||
do: tuple
|
||||
end
|
||||
|
||||
defp reachable?(tuple, :defmacrop, reachable, reattached) do
|
||||
# All private macros are unreachable unless they have been
|
||||
# reattached and they are reachable.
|
||||
:lists.member(tuple, reattached) and Map.has_key?(reachable, tuple)
|
||||
end
|
||||
|
||||
defp reachable?(tuple, :defp, reachable, _reattached) do
|
||||
Map.has_key?(reachable, tuple)
|
||||
end
|
||||
|
||||
defp collect_warnings(reachable, private) do
|
||||
:lists.foldl(&collect_warnings(&1, &2, reachable), [], private)
|
||||
end
|
||||
|
||||
defp collect_warnings({_, _, false, _}, acc, _reachable) do
|
||||
acc
|
||||
end
|
||||
|
||||
defp collect_warnings({tuple, kind, meta, 0}, acc, reachable) do
|
||||
if Map.has_key?(reachable, tuple) do
|
||||
acc
|
||||
else
|
||||
[{meta, {:unused_def, tuple, kind}} | acc]
|
||||
end
|
||||
end
|
||||
|
||||
defp collect_warnings({tuple, kind, meta, default}, acc, reachable) when default > 0 do
|
||||
{name, arity} = tuple
|
||||
min = arity - default
|
||||
max = arity
|
||||
|
||||
case min_reachable_default(max, min, :none, name, reachable) do
|
||||
:none -> [{meta, {:unused_def, tuple, kind}} | acc]
|
||||
^min -> acc
|
||||
^max -> [{meta, {:unused_args, tuple}} | acc]
|
||||
diff -> [{meta, {:unused_args, tuple, diff}} | acc]
|
||||
end
|
||||
end
|
||||
|
||||
defp min_reachable_default(max, min, last, name, reachable) when max >= min do
|
||||
case Map.has_key?(reachable, {name, max}) do
|
||||
true -> min_reachable_default(max - 1, min, max, name, reachable)
|
||||
false -> min_reachable_default(max - 1, min, last, name, reachable)
|
||||
end
|
||||
end
|
||||
|
||||
defp min_reachable_default(_max, _min, last, _name, _reachable) do
|
||||
last
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns all local nodes reachable from `vertex`.
|
||||
|
||||
By default, all public functions are reachable.
|
||||
A private function is only reachable if it has
|
||||
a public function that it invokes directly.
|
||||
"""
|
||||
def reachable_from({_, bag}, local) do
|
||||
bag
|
||||
|> reachable_from(local, %{})
|
||||
|> Map.keys()
|
||||
end
|
||||
|
||||
defp reachable_from(bag, local, vertices) do
|
||||
vertices = Map.put(vertices, local, true)
|
||||
|
||||
Enum.reduce(out_neighbours(bag, {:local, local}), vertices, fn {local, _line, _}, acc ->
|
||||
case acc do
|
||||
%{^local => true} -> acc
|
||||
_ -> reachable_from(bag, local, acc)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp get_line(meta), do: Keyword.get(meta, :line)
|
||||
defp get_position(meta), do: {get_line(meta), meta[:column]}
|
||||
|
||||
defp build_meta(position, function_name) do
|
||||
case position do
|
||||
{line, nil} -> [line: line]
|
||||
{line, col} -> :elixir_env.calculate_span([line: line, column: col], function_name)
|
||||
end
|
||||
end
|
||||
|
||||
## Lightweight digraph implementation
|
||||
|
||||
defp put_edge(d, from, to) do
|
||||
:ets.insert(d, {from, to})
|
||||
end
|
||||
|
||||
defp out_neighbour(d, from) do
|
||||
try do
|
||||
:ets.lookup_element(d, from, 2)
|
||||
catch
|
||||
:error, :badarg -> nil
|
||||
end
|
||||
end
|
||||
|
||||
defp out_neighbours(d, from) do
|
||||
try do
|
||||
:ets.lookup_element(d, from, 2)
|
||||
catch
|
||||
:error, :badarg -> []
|
||||
end
|
||||
end
|
||||
|
||||
defp take_out_neighbours(d, from) do
|
||||
Keyword.values(:ets.take(d, from))
|
||||
end
|
||||
end
|
||||
@@ -5,20 +5,19 @@ defmodule Module.ParallelChecker do
|
||||
|
||||
@type cache() :: {pid(), :ets.tid()}
|
||||
@type warning() :: term()
|
||||
@type kind() :: :def | :defmacro
|
||||
@type mode() :: :elixir | :erlang
|
||||
|
||||
@doc """
|
||||
Initializes the parallel checker process.
|
||||
"""
|
||||
def start_link(schedulers \\ nil) do
|
||||
:gen_server.start_link(__MODULE__, schedulers, [])
|
||||
:proc_lib.start_link(__MODULE__, :init, [schedulers])
|
||||
end
|
||||
|
||||
@doc """
|
||||
Stops the parallel checker process.
|
||||
"""
|
||||
def stop(checker) do
|
||||
def stop({checker, _table}) do
|
||||
send(checker, {__MODULE__, :stop})
|
||||
:ok
|
||||
end
|
||||
@@ -29,26 +28,30 @@ defmodule Module.ParallelChecker do
|
||||
def get do
|
||||
case :erlang.get(:elixir_checker_info) do
|
||||
{parent, nil} ->
|
||||
{:ok, checker} = start_link()
|
||||
put(parent, checker)
|
||||
{parent, checker}
|
||||
{:ok, cache} = start_link()
|
||||
put(parent, cache)
|
||||
{parent, cache}
|
||||
|
||||
{parent, checker} ->
|
||||
{parent, checker}
|
||||
{parent, cache} ->
|
||||
{parent, cache}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Stores the parallel checker information.
|
||||
"""
|
||||
def put(pid, checker) when is_pid(pid) and is_pid(checker) do
|
||||
:erlang.put(:elixir_checker_info, {pid, checker})
|
||||
def put(pid, {checker, table}) when is_pid(pid) do
|
||||
:erlang.put(:elixir_checker_info, {pid, {checker, table}})
|
||||
end
|
||||
|
||||
@doc """
|
||||
Spawns a process that runs the parallel checker.
|
||||
"""
|
||||
def spawn({pid, checker}, module, info, log?) do
|
||||
def spawn({pid, {checker, table}}, module, module_map, log?) do
|
||||
inner_spawn(pid, checker, table, module, cache_from_module_map(table, module_map), log?)
|
||||
end
|
||||
|
||||
defp inner_spawn(pid, checker, table, module, info, log?) do
|
||||
ref = make_ref()
|
||||
|
||||
spawned =
|
||||
@@ -56,20 +59,25 @@ defmodule Module.ParallelChecker do
|
||||
mon_ref = Process.monitor(pid)
|
||||
|
||||
receive do
|
||||
{^ref, :cache, ets} ->
|
||||
{^ref, :cache} ->
|
||||
Process.link(pid)
|
||||
|
||||
module_map =
|
||||
if is_map(info) do
|
||||
info
|
||||
else
|
||||
case File.read(info) do
|
||||
{:ok, binary} -> maybe_module_map(binary, module)
|
||||
{:error, _} -> nil
|
||||
end
|
||||
module_tuple =
|
||||
cond do
|
||||
is_tuple(info) ->
|
||||
info
|
||||
|
||||
is_binary(info) ->
|
||||
with {:ok, binary} <- File.read(info),
|
||||
{:ok, {_, [debug_info: chunk]}} <- :beam_lib.chunks(binary, [:debug_info]),
|
||||
{:debug_info_v1, backend, data} = chunk,
|
||||
{:ok, module_map} <- backend.debug_info(:elixir_v1, module, data, []) do
|
||||
cache_from_module_map(table, module_map)
|
||||
else
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
|
||||
module_map && cache_from_module_map(ets, module_map)
|
||||
send(checker, {ref, :cached})
|
||||
|
||||
receive do
|
||||
@@ -78,8 +86,8 @@ defmodule Module.ParallelChecker do
|
||||
:erlang.put(:elixir_compiler_info, {pid, self()})
|
||||
|
||||
warnings =
|
||||
if module_map do
|
||||
check_module(module_map, {checker, ets}, log?)
|
||||
if module_tuple do
|
||||
check_module(module_tuple, {checker, table}, log?)
|
||||
else
|
||||
[]
|
||||
end
|
||||
@@ -113,12 +121,12 @@ defmodule Module.ParallelChecker do
|
||||
|
||||
case :erlang.get(:elixir_checker_info) do
|
||||
{_, nil} -> :ok
|
||||
{_, checker} -> verify(checker, [])
|
||||
{_, cache} -> verify(cache, [])
|
||||
end
|
||||
|
||||
result
|
||||
after
|
||||
{_, checker} = :erlang.get(:elixir_checker_info)
|
||||
{_, cache} = :erlang.get(:elixir_checker_info)
|
||||
|
||||
if previous != :undefined do
|
||||
:erlang.put(:elixir_checker_info, previous)
|
||||
@@ -126,7 +134,7 @@ defmodule Module.ParallelChecker do
|
||||
:erlang.erase(:elixir_checker_info)
|
||||
end
|
||||
|
||||
checker && stop(checker)
|
||||
cache && stop(cache)
|
||||
end
|
||||
|
||||
_ ->
|
||||
@@ -141,13 +149,13 @@ defmodule Module.ParallelChecker do
|
||||
the modules and adds the ExCk chunk to the binaries. Returns the updated
|
||||
list of warnings from the verification.
|
||||
"""
|
||||
@spec verify(pid(), [{module(), Path.t()}]) :: [warning()]
|
||||
def verify(checker, runtime_files) do
|
||||
@spec verify(cache(), [{module(), Path.t()}]) :: [warning()]
|
||||
def verify({checker, table}, runtime_files) do
|
||||
value = :erlang.get(:elixir_code_diagnostics)
|
||||
log? = not match?({_, false}, value)
|
||||
|
||||
for {module, file} <- runtime_files do
|
||||
spawn({self(), checker}, module, file, log?)
|
||||
inner_spawn(self(), checker, table, module, file, log?)
|
||||
end
|
||||
|
||||
count = :gen_server.call(checker, :start, :infinity)
|
||||
@@ -181,20 +189,8 @@ defmodule Module.ParallelChecker do
|
||||
Test cache.
|
||||
"""
|
||||
def test_cache do
|
||||
{:ok, checker} = start_link()
|
||||
{checker, :gen_server.call(checker, :ets, :infinity)}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Preloads a module into the cache. Call this function before any other
|
||||
cache lookups for the module.
|
||||
"""
|
||||
@spec preload_module(cache(), module()) :: :ok
|
||||
def preload_module({server, ets}, module) do
|
||||
case :ets.lookup(ets, {:cached, module}) do
|
||||
[{_key, _}] -> :ok
|
||||
[] -> cache_module({server, ets}, module)
|
||||
end
|
||||
{:ok, cache} = start_link()
|
||||
cache
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -203,74 +199,81 @@ defmodule Module.ParallelChecker do
|
||||
or if the function does not exist return `{:error, :function}`.
|
||||
"""
|
||||
@spec fetch_export(cache(), module(), atom(), arity()) ::
|
||||
{:ok, mode(), kind(), binary() | nil} | {:error, :function | :module}
|
||||
def fetch_export({_server, ets}, module, fun, arity) do
|
||||
case :ets.lookup(ets, {:cached, module}) do
|
||||
{:ok, mode(), binary() | nil, {:infer, [term()]} | :none}
|
||||
| {:error, :function | :module}
|
||||
def fetch_export({checker, table}, module, fun, arity) do
|
||||
case :ets.lookup(table, module) do
|
||||
[] ->
|
||||
cache_module({checker, table}, module)
|
||||
fetch_export({checker, table}, module, fun, arity)
|
||||
|
||||
[{_key, false}] ->
|
||||
{:error, :module}
|
||||
|
||||
[{_key, mode}] ->
|
||||
case :ets.lookup(ets, {:export, module, {fun, arity}}) do
|
||||
[{_key, kind, reason}] -> {:ok, mode, kind, reason}
|
||||
case :ets.lookup(table, {module, {fun, arity}}) do
|
||||
[{_key, reason, signature}] -> {:ok, mode, reason, signature}
|
||||
[] -> {:error, :function}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns all exported functions and macros for the given module from
|
||||
the cache.
|
||||
"""
|
||||
@spec all_exports(cache(), module()) :: [{atom(), arity()}]
|
||||
def all_exports({_server, ets}, module) do
|
||||
# This is only called after we get a deprecation notice
|
||||
# so we can assume it's a cached module
|
||||
ets
|
||||
|> :ets.match({{:export, module, :"$1"}, :_, :_})
|
||||
|> Enum.flat_map(& &1)
|
||||
|> Enum.sort()
|
||||
end
|
||||
|
||||
## Module checking
|
||||
|
||||
defp check_module(module_map, cache, log?) do
|
||||
%{
|
||||
module: module,
|
||||
file: file,
|
||||
line: line,
|
||||
compile_opts: compile_opts,
|
||||
definitions: definitions,
|
||||
uses_behaviours: uses_behaviours,
|
||||
impls: impls
|
||||
} = module_map
|
||||
|
||||
no_warn_undefined =
|
||||
compile_opts
|
||||
|> extract_no_warn_undefined()
|
||||
|> merge_compiler_no_warn_undefined()
|
||||
defp check_module(module_tuple, cache, log?) do
|
||||
{module, file, line, definitions, no_warn_undefined, behaviours, impls, after_verify} =
|
||||
module_tuple
|
||||
|
||||
behaviour_warnings =
|
||||
Module.Behaviour.check_behaviours_and_impls(
|
||||
module,
|
||||
file,
|
||||
line,
|
||||
uses_behaviours,
|
||||
behaviours,
|
||||
impls,
|
||||
definitions
|
||||
)
|
||||
|
||||
warnings =
|
||||
diagnostics =
|
||||
module
|
||||
|> Module.Types.warnings(file, definitions, no_warn_undefined, cache)
|
||||
|> Kernel.++(behaviour_warnings)
|
||||
|> group_warnings()
|
||||
|> emit_warnings(log?)
|
||||
|> emit_warnings(file, log?)
|
||||
|
||||
module_map
|
||||
|> Map.get(:after_verify, [])
|
||||
|> Enum.each(fn {verify_mod, verify_fun} -> apply(verify_mod, verify_fun, [module]) end)
|
||||
Enum.each(after_verify, fn {verify_mod, verify_fun} ->
|
||||
apply(verify_mod, verify_fun, [module])
|
||||
end)
|
||||
|
||||
warnings
|
||||
diagnostics
|
||||
end
|
||||
|
||||
defp module_map_to_module_tuple(module_map) do
|
||||
%{
|
||||
module: module,
|
||||
file: file,
|
||||
compile_opts: compile_opts,
|
||||
definitions: definitions,
|
||||
attributes: attributes,
|
||||
impls: impls,
|
||||
after_verify: after_verify
|
||||
} = module_map
|
||||
|
||||
# TODO: Match on anno directly in Elixir v1.22+
|
||||
line =
|
||||
case module_map do
|
||||
%{anno: anno} -> :erl_anno.line(anno)
|
||||
%{line: line} -> line
|
||||
end
|
||||
|
||||
behaviours = for {:behaviour, module} <- attributes, do: module
|
||||
|
||||
no_warn_undefined =
|
||||
compile_opts
|
||||
|> extract_no_warn_undefined()
|
||||
|> merge_compiler_no_warn_undefined()
|
||||
|
||||
{module, file, line, definitions, no_warn_undefined, behaviours, impls, after_verify}
|
||||
end
|
||||
|
||||
defp extract_no_warn_undefined(compile_opts) do
|
||||
@@ -291,42 +294,60 @@ defmodule Module.ParallelChecker do
|
||||
## Warning helpers
|
||||
|
||||
defp group_warnings(warnings) do
|
||||
warnings
|
||||
|> Enum.reduce(%{}, fn {module, warning, location}, acc ->
|
||||
locations = MapSet.new([location])
|
||||
Map.update(acc, {module, warning}, locations, &MapSet.put(&1, location))
|
||||
end)
|
||||
|> Enum.map(fn {{module, warning}, locations} -> {module, warning, Enum.sort(locations)} end)
|
||||
|> Enum.sort()
|
||||
{ungrouped, grouped} =
|
||||
Enum.reduce(warnings, {[], %{}}, fn {module, warning, location}, {ungrouped, grouped} ->
|
||||
%{message: _} = diagnostic = module.format_diagnostic(warning)
|
||||
|
||||
if Map.get(diagnostic, :group, false) do
|
||||
locations = MapSet.new([location])
|
||||
|
||||
grouped =
|
||||
Map.update(grouped, warning, {locations, diagnostic}, fn
|
||||
{locations, diagnostic} -> {MapSet.put(locations, location), diagnostic}
|
||||
end)
|
||||
|
||||
{ungrouped, grouped}
|
||||
else
|
||||
{[{[location], diagnostic} | ungrouped], grouped}
|
||||
end
|
||||
end)
|
||||
|
||||
grouped =
|
||||
Enum.map(grouped, fn {_warning, {locations, diagnostic}} ->
|
||||
{Enum.sort(locations), diagnostic}
|
||||
end)
|
||||
|
||||
Enum.sort(ungrouped ++ grouped)
|
||||
end
|
||||
|
||||
defp emit_warnings(warnings, log?) do
|
||||
Enum.flat_map(warnings, fn {module, warning, locations} ->
|
||||
message = module.format_warning(warning)
|
||||
diagnostics = Enum.map(locations, &to_diagnostic(message, &1))
|
||||
log? and print_warning(message, diagnostics)
|
||||
defp emit_warnings(warnings, file, log?) do
|
||||
Enum.flat_map(warnings, fn {locations, diagnostic} ->
|
||||
diagnostics = Enum.map(locations, &to_diagnostic(diagnostic, file, &1))
|
||||
log? and print_diagnostics(diagnostics)
|
||||
diagnostics
|
||||
end)
|
||||
end
|
||||
|
||||
defp print_warning(message, [diagnostic]) do
|
||||
:elixir_errors.print_warning(message, diagnostic)
|
||||
defp print_diagnostics([diagnostic]) do
|
||||
:elixir_errors.print_diagnostic(diagnostic, true)
|
||||
end
|
||||
|
||||
defp print_warning(message, grouped_warnings) do
|
||||
:elixir_errors.print_warning_group(message, grouped_warnings)
|
||||
defp print_diagnostics(diagnostics) do
|
||||
:elixir_errors.print_diagnostics(diagnostics)
|
||||
end
|
||||
|
||||
defp to_diagnostic(message, {file, position, mfa}) when is_list(position) do
|
||||
defp to_diagnostic(diagnostic, source, {file, position, mfa}) when is_list(position) do
|
||||
file = Path.absname(file)
|
||||
|
||||
%{
|
||||
severity: :warning,
|
||||
source: file,
|
||||
source: source,
|
||||
file: file,
|
||||
position: position_to_tuple(position),
|
||||
message: IO.iodata_to_binary(message),
|
||||
stacktrace: [to_stacktrace(file, position, mfa)],
|
||||
span: nil
|
||||
}
|
||||
|> Map.merge(diagnostic)
|
||||
end
|
||||
|
||||
defp position_to_tuple(position) do
|
||||
@@ -351,15 +372,15 @@ defmodule Module.ParallelChecker do
|
||||
|
||||
## Cache
|
||||
|
||||
defp cache_module({server, ets}, module) do
|
||||
if lock(server, module) do
|
||||
defp cache_module({checker, table}, module) do
|
||||
if lock(checker, module) do
|
||||
object_code = :code.get_object_code(module)
|
||||
|
||||
# The chunk has more information, so that's our preference
|
||||
with {^module, binary, _filename} <- object_code,
|
||||
{:ok, {^module, [{~c"ExCk", chunk}]}} <- :beam_lib.chunks(binary, [~c"ExCk"]),
|
||||
{:elixir_checker_v1, contents} <- :erlang.binary_to_term(chunk) do
|
||||
cache_chunk(ets, module, contents.exports)
|
||||
cache_chunk(table, module, contents.exports)
|
||||
else
|
||||
_ ->
|
||||
# Otherwise, if the module is loaded, use its info
|
||||
@@ -367,37 +388,28 @@ defmodule Module.ParallelChecker do
|
||||
true ->
|
||||
{mode, exports} = info_exports(module)
|
||||
deprecated = info_deprecated(module)
|
||||
cache_info(ets, module, exports, deprecated, mode)
|
||||
cache_info(table, module, exports, deprecated, %{}, mode)
|
||||
|
||||
false ->
|
||||
# Or load exports from chunk
|
||||
with {^module, binary, _filename} <- object_code,
|
||||
{:ok, {^module, [exports: exports]}} <- :beam_lib.chunks(binary, [:exports]) do
|
||||
exports = Map.new(Enum.map(exports, &{&1, :def}))
|
||||
cache_info(ets, module, exports, %{}, :erlang)
|
||||
cache_info(table, module, exports, %{}, %{}, :erlang)
|
||||
else
|
||||
_ ->
|
||||
:ets.insert(ets, {{:cached, module}, false})
|
||||
:ets.insert(table, {module, false})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
unlock(server, module)
|
||||
unlock(checker, module)
|
||||
end
|
||||
end
|
||||
|
||||
defp info_exports(module) do
|
||||
map =
|
||||
Map.new(
|
||||
[{{:__info__, 1}, :def}] ++
|
||||
behaviour_exports(module) ++
|
||||
Enum.map(module.__info__(:macros), &{&1, :defmacro}) ++
|
||||
Enum.map(module.__info__(:functions), &{&1, :def})
|
||||
)
|
||||
|
||||
{:elixir, map}
|
||||
{:elixir, behaviour_exports(module) ++ [{:__info__, 1} | module.__info__(:functions)]}
|
||||
rescue
|
||||
_ -> {:erlang, Map.new(Enum.map(module.module_info(:exports), &{&1, :def}))}
|
||||
_ -> {:erlang, module.module_info(:exports)}
|
||||
end
|
||||
|
||||
defp info_deprecated(module) do
|
||||
@@ -406,70 +418,49 @@ defmodule Module.ParallelChecker do
|
||||
_ -> %{}
|
||||
end
|
||||
|
||||
defp maybe_module_map(binary, module) when is_binary(binary) do
|
||||
# If a module was compiled without debug_info,
|
||||
# then there is no module_map for further verification.
|
||||
with {:ok, {_, [debug_info: chunk]}} <- :beam_lib.chunks(binary, [:debug_info]),
|
||||
{:debug_info_v1, backend, data} = chunk,
|
||||
{:ok, module_map} <- backend.debug_info(:elixir_v1, module, data, []) do
|
||||
module_map
|
||||
else
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
|
||||
defp cache_from_module_map(ets, map) do
|
||||
defp cache_from_module_map(table, map) do
|
||||
exports =
|
||||
[{{:__info__, 1}, :def}] ++
|
||||
[{:__info__, 1}] ++
|
||||
behaviour_exports(map) ++
|
||||
definitions_to_exports(map.definitions)
|
||||
for({function, :def, _meta, _clauses} <- map.definitions, do: function)
|
||||
|
||||
deprecated = Map.new(map.deprecated)
|
||||
cache_info(ets, map.module, exports, deprecated, :elixir)
|
||||
cache_info(table, map.module, exports, Map.new(map.deprecated), map.signatures, :elixir)
|
||||
module_map_to_module_tuple(map)
|
||||
end
|
||||
|
||||
defp cache_info(ets, module, exports, deprecated, mode) do
|
||||
Enum.each(exports, fn {{fun, arity}, kind} ->
|
||||
reason = Map.get(deprecated, {fun, arity})
|
||||
:ets.insert(ets, {{:export, module, {fun, arity}}, kind, reason})
|
||||
{{fun, arity}, kind}
|
||||
defp cache_info(table, module, exports, deprecated, sigs, mode) do
|
||||
Enum.each(exports, fn fa ->
|
||||
reason = Map.get(deprecated, fa)
|
||||
:ets.insert(table, {{module, fa}, reason, Map.get(sigs, fa, :none)})
|
||||
end)
|
||||
|
||||
:ets.insert(ets, {{:cached, module}, mode})
|
||||
:ets.insert(table, {module, mode})
|
||||
end
|
||||
|
||||
defp cache_chunk(ets, module, exports) do
|
||||
Enum.each(exports, fn {{fun, arity}, %{kind: kind, deprecated_reason: reason}} ->
|
||||
:ets.insert(ets, {{:export, module, {fun, arity}}, kind, reason})
|
||||
|
||||
{{fun, arity}, kind}
|
||||
defp cache_chunk(table, module, exports) do
|
||||
Enum.each(exports, fn {{fun, arity}, info} ->
|
||||
# TODO: Match on signature directly in Elixir v1.22+
|
||||
:ets.insert(
|
||||
table,
|
||||
{{module, {fun, arity}}, Map.get(info, :deprecated), Map.get(info, :sig, :none)}
|
||||
)
|
||||
end)
|
||||
|
||||
:ets.insert(ets, {{:export, module, {:__info__, 1}}, :def, nil})
|
||||
:ets.insert(ets, {{:cached, module}, :elixir})
|
||||
:ets.insert(table, {{module, {:__info__, 1}}, nil, :none})
|
||||
:ets.insert(table, {module, :elixir})
|
||||
end
|
||||
|
||||
defp behaviour_exports(%{defines_behaviour: true}), do: [{{:behaviour_info, 1}, :def}]
|
||||
defp behaviour_exports(%{defines_behaviour: true}), do: [{:behaviour_info, 1}]
|
||||
defp behaviour_exports(%{defines_behaviour: false}), do: []
|
||||
|
||||
defp behaviour_exports(module) when is_atom(module) do
|
||||
if function_exported?(module, :behaviour_info, 1) do
|
||||
[{{:behaviour_info, 1}, :def}]
|
||||
[{:behaviour_info, 1}]
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
defp definitions_to_exports(definitions) do
|
||||
Enum.flat_map(definitions, fn {function, kind, _meta, _clauses} ->
|
||||
if kind in [:def, :defmacro] do
|
||||
[{function, kind}]
|
||||
else
|
||||
[]
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp lock(server, module) do
|
||||
:gen_server.call(server, {:lock, module}, :infinity)
|
||||
end
|
||||
@@ -485,22 +476,22 @@ defmodule Module.ParallelChecker do
|
||||
## Server callbacks
|
||||
|
||||
def init(schedulers) do
|
||||
ets = :ets.new(__MODULE__, [:set, :public, {:read_concurrency, true}])
|
||||
table = :ets.new(__MODULE__, [:set, :public, {:read_concurrency, true}])
|
||||
:proc_lib.init_ack({:ok, {self(), table}})
|
||||
|
||||
state = %{
|
||||
ets: ets,
|
||||
waiting: %{},
|
||||
modules: [],
|
||||
spawned: 0,
|
||||
schedulers: schedulers || max(:erlang.system_info(:schedulers_online), 2)
|
||||
}
|
||||
|
||||
{:ok, state}
|
||||
:gen_server.enter_loop(__MODULE__, [], state)
|
||||
end
|
||||
|
||||
def handle_call(:start, _from, %{ets: ets, modules: modules} = state) do
|
||||
def handle_call(:start, _from, %{modules: modules} = state) do
|
||||
for {pid, ref} <- modules do
|
||||
send(pid, {ref, :cache, ets})
|
||||
send(pid, {ref, :cache})
|
||||
end
|
||||
|
||||
for {_pid, ref} <- modules do
|
||||
@@ -512,14 +503,10 @@ defmodule Module.ParallelChecker do
|
||||
{:reply, length(modules), run_checkers(state)}
|
||||
end
|
||||
|
||||
def handle_call(:ets, _from, state) do
|
||||
{:reply, state.ets, state}
|
||||
end
|
||||
|
||||
def handle_call({:lock, module}, from, %{waiting: waiting} = state) do
|
||||
case waiting do
|
||||
%{^module => froms} ->
|
||||
waiting = Map.put(state.waiting, module, [from | froms])
|
||||
%{^module => from_list} ->
|
||||
waiting = Map.put(state.waiting, module, [from | from_list])
|
||||
{:noreply, %{state | waiting: waiting}}
|
||||
|
||||
%{} ->
|
||||
@@ -529,8 +516,8 @@ defmodule Module.ParallelChecker do
|
||||
end
|
||||
|
||||
def handle_call({:unlock, module}, _from, %{waiting: waiting} = state) do
|
||||
froms = Map.fetch!(waiting, module)
|
||||
Enum.each(froms, &:gen_server.reply(&1, false))
|
||||
from_list = Map.fetch!(waiting, module)
|
||||
Enum.each(from_list, &:gen_server.reply(&1, false))
|
||||
waiting = Map.delete(waiting, module)
|
||||
{:reply, :ok, %{state | waiting: waiting}}
|
||||
end
|
||||
|
||||
+355
-47
@@ -1,48 +1,304 @@
|
||||
defmodule Module.Types do
|
||||
@moduledoc false
|
||||
alias Module.Types.{Descr, Expr, Pattern, Helpers}
|
||||
|
||||
alias Module.Types.{Expr, Pattern}
|
||||
# The mode controls what happens on function application when
|
||||
# there are gradual arguments. Non-gradual arguments always
|
||||
# perform subtyping and return its output (OUT).
|
||||
#
|
||||
# * :strict - Requires types signatures (not implemented).
|
||||
# * Strong arrows with gradual performs subtyping and returns OUT
|
||||
# * Weak arrows with gradual performs subtyping and returns OUT
|
||||
#
|
||||
# * :static - Type signatures have been given.
|
||||
# * Strong arrows with gradual performs compatibility and returns OUT
|
||||
# * Weak arrows with gradual performs compatibility and returns dynamic()
|
||||
#
|
||||
# * :dynamic - Type signatures have not been given.
|
||||
# * Strong arrows with gradual performs compatibility and returns dynamic(OUT)
|
||||
# * Weak arrows with gradual performs compatibility and returns dynamic()
|
||||
#
|
||||
# * :infer - Same as :dynamic but skips remote calls.
|
||||
#
|
||||
# * :traversal - Focused mostly on traversing AST, skips most type system
|
||||
# operations. Used by macros and when skipping inference.
|
||||
#
|
||||
# The mode may also control exhaustiveness checks in the future (to be decided).
|
||||
# We may also want for applications with subtyping in dynamic mode to always
|
||||
# intersect with dynamic, but this mode may be too lax (to be decided based on
|
||||
# feedback).
|
||||
@modes [:static, :dynamic, :infer, :traversal]
|
||||
|
||||
# These functions are not inferred because they are added/managed by the compiler
|
||||
@no_infer [__protocol__: 1, behaviour_info: 1]
|
||||
|
||||
@doc false
|
||||
def infer(module, file, defs, private, used_private, env, {_, cache}) do
|
||||
infer_signatures? = :elixir_config.get(:infer_signatures) and cache != nil
|
||||
|
||||
finder =
|
||||
fn fun_arity ->
|
||||
case :lists.keyfind(fun_arity, 1, defs) do
|
||||
{_, kind, _, _} = clause -> {infer_mode(kind, infer_signatures?), clause}
|
||||
false -> false
|
||||
end
|
||||
end
|
||||
|
||||
handler = fn meta, fun_arity, stack, context ->
|
||||
case local_handler(meta, fun_arity, stack, context, finder) do
|
||||
false ->
|
||||
undefined_function!(:undefined_function, meta, fun_arity, stack, env)
|
||||
false
|
||||
|
||||
{kind, _, _} = triplet ->
|
||||
if (kind == :defmacro or kind == :defmacrop) and not Keyword.has_key?(meta, :super) do
|
||||
undefined_function!(:incorrect_dispatch, meta, fun_arity, stack, env)
|
||||
false
|
||||
else
|
||||
triplet
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
stack = stack(:infer, file, module, {:__info__, 1}, env, cache, handler)
|
||||
|
||||
{types, %{local_sigs: reachable_sigs} = context} =
|
||||
for {fun_arity, kind, meta, _clauses} = def <- defs,
|
||||
kind in [:def, :defmacro],
|
||||
reduce: {[], context()} do
|
||||
{types, context} ->
|
||||
finder = fn _ -> {infer_mode(kind, infer_signatures?), def} end
|
||||
{_kind, inferred, context} = local_handler(meta, fun_arity, stack, context, finder)
|
||||
|
||||
if infer_signatures? and kind == :def and fun_arity not in @no_infer do
|
||||
{[{fun_arity, inferred} | types], context}
|
||||
else
|
||||
{types, context}
|
||||
end
|
||||
end
|
||||
|
||||
# Now traverse all used privates to find any other private that have been used by them.
|
||||
context =
|
||||
%{local_sigs: used_sigs} =
|
||||
for fun_arity <- used_private, reduce: context do
|
||||
context ->
|
||||
{_kind, _inferred, context} = local_handler([], fun_arity, stack, context, finder)
|
||||
context
|
||||
end
|
||||
|
||||
{unreachable, _context} =
|
||||
Enum.reduce(private, {[], context}, fn
|
||||
{fun_arity, kind, _meta, _defaults} = info, {unreachable, context} ->
|
||||
warn_unused_def(info, used_sigs, env)
|
||||
|
||||
# Find anything undefined within unused functions
|
||||
{_kind, _inferred, context} = local_handler([], fun_arity, stack, context, finder)
|
||||
|
||||
# defp is reachable if used, defmacrop only if directly invoked
|
||||
private_sigs = if kind == :defp, do: used_sigs, else: reachable_sigs
|
||||
|
||||
if is_map_key(private_sigs, fun_arity) do
|
||||
{unreachable, context}
|
||||
else
|
||||
{[fun_arity | unreachable], context}
|
||||
end
|
||||
end)
|
||||
|
||||
{Map.new(types), unreachable}
|
||||
end
|
||||
|
||||
defp infer_mode(kind, infer_signatures?) do
|
||||
if infer_signatures? and kind in [:def, :defp], do: :infer, else: :traversal
|
||||
end
|
||||
|
||||
defp undefined_function!(reason, meta, {fun, arity}, stack, env) do
|
||||
env = %{env | function: stack.function, file: stack.file}
|
||||
tuple = {reason, {fun, arity}, stack.module}
|
||||
:elixir_errors.module_error(Helpers.with_span(meta, fun), env, __MODULE__, tuple)
|
||||
end
|
||||
|
||||
defp warn_unused_def({_fun_arity, _kind, false, _}, _used, _env) do
|
||||
:ok
|
||||
end
|
||||
|
||||
defp warn_unused_def({fun_arity, kind, meta, 0}, used, env) do
|
||||
case is_map_key(used, fun_arity) do
|
||||
true -> :ok
|
||||
false -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_def, fun_arity, kind})
|
||||
end
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
defp warn_unused_def({tuple, kind, meta, default}, used, env) when default > 0 do
|
||||
{name, arity} = tuple
|
||||
min = arity - default
|
||||
max = arity
|
||||
|
||||
case min_reachable_default(max, min, :none, name, used) do
|
||||
:none -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_def, tuple, kind})
|
||||
^min -> :ok
|
||||
^max -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_args, tuple})
|
||||
diff -> :elixir_errors.file_warn(meta, env, __MODULE__, {:unused_args, tuple, diff})
|
||||
end
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
defp min_reachable_default(max, min, last, name, used) when max >= min do
|
||||
fun_arity = {name, max}
|
||||
|
||||
case is_map_key(used, fun_arity) do
|
||||
true -> min_reachable_default(max - 1, min, max, name, used)
|
||||
false -> min_reachable_default(max - 1, min, last, name, used)
|
||||
end
|
||||
end
|
||||
|
||||
defp min_reachable_default(_max, _min, last, _name, _used) do
|
||||
last
|
||||
end
|
||||
|
||||
@doc false
|
||||
def warnings(module, file, defs, no_warn_undefined, cache) do
|
||||
context = context()
|
||||
finder = fn fun_arity ->
|
||||
case :lists.keyfind(fun_arity, 1, defs) do
|
||||
{_, _, _, _} = clause -> {:dynamic, clause}
|
||||
false -> false
|
||||
end
|
||||
end
|
||||
|
||||
Enum.flat_map(defs, fn {{fun, arity} = function, kind, meta, clauses} ->
|
||||
stack = stack(with_file_meta(meta, file), module, function, no_warn_undefined, cache)
|
||||
handler = &local_handler(&1, &2, &3, &4, finder)
|
||||
stack = stack(:dynamic, file, module, {:__info__, 1}, no_warn_undefined, cache, handler)
|
||||
|
||||
Enum.flat_map(clauses, fn {meta, args, guards, body} ->
|
||||
try do
|
||||
warnings_from_clause(meta, args, guards, body, stack, context)
|
||||
rescue
|
||||
e ->
|
||||
def_expr = {kind, meta, [guards_to_expr(guards, {fun, [], args}), [do: body]]}
|
||||
|
||||
error =
|
||||
RuntimeError.exception("""
|
||||
found error while checking types for #{Exception.format_mfa(module, fun, arity)}:
|
||||
|
||||
#{Exception.format_banner(:error, e, __STACKTRACE__)}\
|
||||
|
||||
The exception happened while checking this code:
|
||||
|
||||
#{Macro.to_string(def_expr)}
|
||||
|
||||
Please report this bug at: https://github.com/elixir-lang/elixir/issues
|
||||
""")
|
||||
|
||||
reraise error, __STACKTRACE__
|
||||
end
|
||||
context =
|
||||
Enum.reduce(defs, context(), fn {fun_arity, _kind, meta, _clauses} = def, context ->
|
||||
finder = fn _ -> {:dynamic, def} end
|
||||
{_kind, _inferred, context} = local_handler(meta, fun_arity, stack, context, finder)
|
||||
context
|
||||
end)
|
||||
end)
|
||||
|
||||
context = warn_unused_clauses(defs, stack, context)
|
||||
context.warnings
|
||||
end
|
||||
|
||||
defp with_file_meta(meta, file) do
|
||||
case Keyword.fetch(meta, :file) do
|
||||
{:ok, {meta_file, _}} -> meta_file
|
||||
:error -> file
|
||||
defp warn_unused_clauses(defs, stack, context) do
|
||||
for {fun_arity, pending} <- context.local_used,
|
||||
pending != [],
|
||||
{_fun_arity, kind, meta, clauses} = List.keyfind(defs, fun_arity, 0),
|
||||
not Keyword.get(meta, :from_super, false),
|
||||
reduce: context do
|
||||
context ->
|
||||
{_kind, _inferred, mapping} = Map.fetch!(context.local_sigs, fun_arity)
|
||||
|
||||
clauses_indexes =
|
||||
for type_index <- pending, {clause_index, ^type_index} <- mapping, do: clause_index
|
||||
|
||||
Enum.reduce(clauses_indexes, context, fn clause_index, context ->
|
||||
{meta, _args, _guards, _body} = Enum.fetch!(clauses, clause_index)
|
||||
stack = %{stack | function: fun_arity}
|
||||
Helpers.warn(__MODULE__, {:unused_clause, kind, fun_arity}, meta, stack, context)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
defp local_handler(_meta, fun_arity, stack, context, finder) do
|
||||
case context.local_sigs do
|
||||
%{^fun_arity => {kind, inferred, _mapping}} ->
|
||||
{kind, inferred, context}
|
||||
|
||||
%{^fun_arity => kind} when is_atom(kind) ->
|
||||
{kind, :none, context}
|
||||
|
||||
local_sigs ->
|
||||
case finder.(fun_arity) do
|
||||
{mode, {fun_arity, kind, meta, clauses}} ->
|
||||
context = put_in(context.local_sigs, Map.put(local_sigs, fun_arity, kind))
|
||||
|
||||
{inferred, mapping, context} =
|
||||
local_handler(fun_arity, kind, meta, clauses, mode, stack, context)
|
||||
|
||||
context =
|
||||
update_in(context.local_sigs, &Map.put(&1, fun_arity, {kind, inferred, mapping}))
|
||||
|
||||
{kind, inferred, context}
|
||||
|
||||
false ->
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp local_handler({fun, arity} = fun_arity, kind, meta, clauses, mode, stack, context) do
|
||||
expected = List.duplicate(Descr.dynamic(), arity)
|
||||
stack = stack |> fresh_stack(mode, fun_arity) |> with_file_meta(meta)
|
||||
|
||||
{_, _, mapping, clauses_types, clauses_context} =
|
||||
Enum.reduce(clauses, {0, 0, [], [], context}, fn
|
||||
{meta, args, guards, body}, {index, total, mapping, inferred, context} ->
|
||||
context = fresh_context(context)
|
||||
|
||||
try do
|
||||
{args_types, context} =
|
||||
Pattern.of_head(args, guards, expected, :default, meta, stack, context)
|
||||
|
||||
{return_type, context} =
|
||||
Expr.of_expr(body, stack, context)
|
||||
|
||||
{type_index, inferred} =
|
||||
add_inferred(inferred, args_types, return_type, total - 1, [])
|
||||
|
||||
if type_index == -1 do
|
||||
{index + 1, total + 1, [{index, total} | mapping], inferred, context}
|
||||
else
|
||||
{index + 1, total, [{index, type_index} | mapping], inferred, context}
|
||||
end
|
||||
rescue
|
||||
e ->
|
||||
internal_error!(e, __STACKTRACE__, kind, meta, fun, args, guards, body, stack)
|
||||
end
|
||||
end)
|
||||
|
||||
inferred = {:infer, Enum.reverse(clauses_types)}
|
||||
{inferred, mapping, restore_context(context, clauses_context)}
|
||||
end
|
||||
|
||||
# We check for term equality of types as an optimization
|
||||
# to reduce the amount of check we do at runtime.
|
||||
defp add_inferred([{args, existing_return} | tail], args, return, index, acc),
|
||||
do: {index, Enum.reverse(acc, [{args, Descr.union(existing_return, return)} | tail])}
|
||||
|
||||
defp add_inferred([head | tail], args, return, index, acc),
|
||||
do: add_inferred(tail, args, return, index - 1, [head | acc])
|
||||
|
||||
defp add_inferred([], args, return, -1, acc),
|
||||
do: {-1, [{args, return} | Enum.reverse(acc)]}
|
||||
|
||||
defp with_file_meta(stack, meta) do
|
||||
case Keyword.fetch(meta, :file) do
|
||||
{:ok, {meta_file, _}} -> %{stack | file: meta_file}
|
||||
:error -> stack
|
||||
end
|
||||
end
|
||||
|
||||
defp internal_error!(e, trace, kind, meta, fun, args, guards, body, stack) do
|
||||
def_expr = {kind, meta, [guards_to_expr(guards, {fun, [], args}), [do: body]]}
|
||||
|
||||
exception =
|
||||
RuntimeError.exception("""
|
||||
found error while checking types for #{Exception.format_mfa(stack.module, fun, length(args))}:
|
||||
|
||||
#{Exception.format_banner(:error, e, trace)}\
|
||||
|
||||
The exception happened while checking this code:
|
||||
|
||||
#{Macro.to_string(def_expr)}
|
||||
|
||||
Please report this bug at: https://github.com/elixir-lang/elixir/issues
|
||||
""")
|
||||
|
||||
reraise exception, trace
|
||||
end
|
||||
|
||||
defp guards_to_expr([], left) do
|
||||
left
|
||||
end
|
||||
@@ -51,17 +307,9 @@ defmodule Module.Types do
|
||||
guards_to_expr(guards, {:when, [], [left, guard]})
|
||||
end
|
||||
|
||||
defp warnings_from_clause(meta, args, guards, body, stack, context) do
|
||||
with {:ok, _types, context} <- Pattern.of_head(args, guards, meta, stack, context),
|
||||
{:ok, _type, context} <- Expr.of_expr(body, stack, context) do
|
||||
context.warnings
|
||||
else
|
||||
{:error, context} -> context.warnings
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def stack(file, module, function, no_warn_undefined, cache) do
|
||||
def stack(mode, file, module, function, no_warn_undefined, cache, handler)
|
||||
when mode in @modes do
|
||||
%{
|
||||
# The fallback meta used for literals in patterns and guards
|
||||
meta: [],
|
||||
@@ -71,12 +319,14 @@ defmodule Module.Types do
|
||||
module: module,
|
||||
# Current function
|
||||
function: function,
|
||||
# List of calls to not warn on as undefined
|
||||
# List of calls to not warn on as undefined or :all or Macro.Env indicating limited remotes
|
||||
no_warn_undefined: no_warn_undefined,
|
||||
# A list of cached modules received from the parallel compiler
|
||||
# A tuple with cache information (may be nil)
|
||||
cache: cache,
|
||||
# If variable refinements is enabled or not
|
||||
refine: true
|
||||
# The mode to be used, see the @modes attribute
|
||||
mode: mode,
|
||||
# The function for handling local calls
|
||||
local_handler: handler
|
||||
}
|
||||
end
|
||||
|
||||
@@ -85,8 +335,66 @@ defmodule Module.Types do
|
||||
%{
|
||||
# A list of all warnings found so far
|
||||
warnings: [],
|
||||
# Information about all vars and their types
|
||||
vars: %{}
|
||||
# All vars and their types
|
||||
vars: %{},
|
||||
# Variables and arguments from patterns
|
||||
pattern_info: nil,
|
||||
# If type checking has found an error/failure
|
||||
failed: false,
|
||||
# Local signatures used by local handler
|
||||
local_sigs: %{},
|
||||
# Track which clauses have been used across private local calls
|
||||
local_used: %{}
|
||||
}
|
||||
end
|
||||
|
||||
defp fresh_stack(stack, mode, function) when mode in @modes do
|
||||
%{stack | mode: mode, function: function}
|
||||
end
|
||||
|
||||
defp fresh_context(context) do
|
||||
%{context | vars: %{}, failed: false}
|
||||
end
|
||||
|
||||
defp restore_context(%{vars: vars, failed: failed}, later_context) do
|
||||
%{later_context | vars: vars, failed: failed}
|
||||
end
|
||||
|
||||
## Diagnostics
|
||||
|
||||
def format_diagnostic({:unused_clause, kind, {fun, arity}}) do
|
||||
%{
|
||||
message: "this clause of #{kind} #{fun}/#{arity} is never used"
|
||||
}
|
||||
end
|
||||
|
||||
## Module errors
|
||||
|
||||
def format_error({:unused_args, {name, arity}}),
|
||||
do: "default values for the optional arguments in #{name}/#{arity} are never used"
|
||||
|
||||
def format_error({:unused_args, {name, arity}, count}) when arity - count == 1,
|
||||
do: "the default value for the last optional argument in #{name}/#{arity} is never used"
|
||||
|
||||
def format_error({:unused_args, {name, arity}, count}),
|
||||
do:
|
||||
"the default values for the last #{arity - count} optional arguments in #{name}/#{arity} are never used"
|
||||
|
||||
def format_error({:unused_def, {name, arity}, :defp}),
|
||||
do: "function #{name}/#{arity} is unused"
|
||||
|
||||
def format_error({:unused_def, {name, arity}, :defmacrop}),
|
||||
do: "macro #{name}/#{arity} is unused"
|
||||
|
||||
def format_error({:undefined_function, {f, a}, _})
|
||||
when {f, a} in [__info__: 1, behaviour_info: 1, module_info: 1, module_info: 0],
|
||||
do:
|
||||
"undefined function #{f}/#{a} (this function is auto-generated by the compiler and must always be called as a remote, as in __MODULE__.#{f}/#{a})"
|
||||
|
||||
def format_error({:undefined_function, {f, a}, module}),
|
||||
do:
|
||||
"undefined function #{f}/#{a} (expected #{inspect(module)} to define such a function or for it to be imported, but none are available)"
|
||||
|
||||
def format_error({:incorrect_dispatch, {f, a}, _module}),
|
||||
do: "cannot invoke macro #{f}/#{a} before its definition"
|
||||
end
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+1894
-300
File diff suppressed because it is too large
Load Diff
+541
-322
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,8 @@ defmodule Module.Types.Helpers do
|
||||
# AST and enumeration helpers.
|
||||
@moduledoc false
|
||||
|
||||
## AST helpers
|
||||
|
||||
@doc """
|
||||
Guard function to check if an AST node is a variable.
|
||||
"""
|
||||
@@ -14,6 +16,37 @@ defmodule Module.Types.Helpers do
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Unpacks a list into head elements and tail.
|
||||
"""
|
||||
def unpack_list([{:|, _, [head, tail]}], acc), do: {Enum.reverse([head | acc]), tail}
|
||||
def unpack_list([head], acc), do: {Enum.reverse([head | acc]), []}
|
||||
def unpack_list([head | tail], acc), do: unpack_list(tail, [head | acc])
|
||||
|
||||
@doc """
|
||||
Unpacks a match into several matches.
|
||||
"""
|
||||
def unpack_match({:=, _, [left, right]}, acc),
|
||||
do: unpack_match(left, unpack_match(right, acc))
|
||||
|
||||
def unpack_match(node, acc),
|
||||
do: [node | acc]
|
||||
|
||||
@doc """
|
||||
Returns the AST metadata.
|
||||
"""
|
||||
def get_meta({_, meta, _}), do: meta
|
||||
def get_meta(_other), do: []
|
||||
|
||||
@doc """
|
||||
Attaches span information.
|
||||
"""
|
||||
def with_span(meta, name) do
|
||||
:elixir_env.calculate_span(meta, name)
|
||||
end
|
||||
|
||||
## Warnings
|
||||
|
||||
@doc """
|
||||
Formatted hints in typing errors.
|
||||
"""
|
||||
@@ -41,43 +74,161 @@ defmodule Module.Types.Helpers do
|
||||
"""
|
||||
|
||||
#{hint()} when you rescue without specifying exception names, \
|
||||
the variable is assigned a type of a struct but all of its fields are unknown
|
||||
the variable is assigned a type of a struct but all of its fields are unknown. \
|
||||
If you are trying to access an exception's :message key, either specify the \
|
||||
exception names or use `Exception.message/1`.
|
||||
"""
|
||||
end)
|
||||
end
|
||||
|
||||
defp hint, do: :elixir_errors.prefix(:hint)
|
||||
|
||||
@doc """
|
||||
Collect traces from variables in expression.
|
||||
|
||||
This information is exposed to language servers and
|
||||
therefore must remain backwards compatible.
|
||||
"""
|
||||
def collect_traces(expr, %{vars: vars}) do
|
||||
{_, versions} =
|
||||
Macro.prewalk(expr, %{}, fn
|
||||
{var_name, meta, var_context}, versions when is_atom(var_name) and is_atom(var_context) ->
|
||||
version = meta[:version]
|
||||
|
||||
case vars do
|
||||
%{^version => %{off_traces: off_traces, name: name, context: context}} ->
|
||||
{:ok,
|
||||
Map.put(versions, version, %{
|
||||
type: :variable,
|
||||
name: name,
|
||||
context: context,
|
||||
traces: collect_var_traces(off_traces)
|
||||
})}
|
||||
|
||||
_ ->
|
||||
{:ok, versions}
|
||||
end
|
||||
|
||||
node, versions ->
|
||||
{node, versions}
|
||||
end)
|
||||
|
||||
versions
|
||||
|> Map.values()
|
||||
|> Enum.sort_by(& &1.name)
|
||||
end
|
||||
|
||||
defp collect_var_traces(traces) do
|
||||
traces
|
||||
|> Enum.reject(fn {_expr, _file, type, _formatter} ->
|
||||
# As an otimization do not care about dynamic terms
|
||||
type == %{dynamic: :term}
|
||||
end)
|
||||
|> case do
|
||||
[] -> traces
|
||||
filtered -> filtered
|
||||
end
|
||||
|> Enum.reverse()
|
||||
|> Enum.map(fn {expr, file, type, formatter} ->
|
||||
meta = get_meta(expr)
|
||||
|
||||
{formatted_expr, formatter_hints} =
|
||||
case formatter do
|
||||
:default -> {expr_to_string(expr), []}
|
||||
formatter -> formatter.(expr)
|
||||
end
|
||||
|
||||
# This information is exposed to language servers and
|
||||
# therefore must remain backwards compatible.
|
||||
%{
|
||||
file: file,
|
||||
meta: meta,
|
||||
formatted_expr: formatted_expr,
|
||||
formatted_hints: format_hints(formatter_hints ++ expr_hints(expr)),
|
||||
formatted_type: Module.Types.Descr.to_quoted_string(type, collapse_structs: true)
|
||||
}
|
||||
end)
|
||||
|> Enum.sort_by(&{&1.meta[:line], &1.meta[:column]})
|
||||
|> Enum.dedup()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Format previously collected traces.
|
||||
"""
|
||||
def format_traces(traces) do
|
||||
Enum.map(traces, &format_trace/1)
|
||||
end
|
||||
|
||||
defp format_trace(%{type: :variable, name: name, context: context, traces: traces}) do
|
||||
traces =
|
||||
for trace <- traces do
|
||||
location =
|
||||
trace.file
|
||||
|> Path.relative_to_cwd()
|
||||
|> Exception.format_file_line(trace.meta[:line], trace.meta[:column])
|
||||
|> String.replace_suffix(":", "")
|
||||
|
||||
[
|
||||
"""
|
||||
|
||||
# type: #{indent(trace.formatted_type, 4)}
|
||||
# from: #{location}
|
||||
\
|
||||
""",
|
||||
indent(trace.formatted_expr, 4),
|
||||
?\n,
|
||||
trace.formatted_hints
|
||||
]
|
||||
end
|
||||
|
||||
type_or_types = pluralize(traces, "type", "types")
|
||||
["\nwhere #{format_var(name, context)} was given the #{type_or_types}:\n" | traces]
|
||||
end
|
||||
|
||||
@doc """
|
||||
Formats a var for pretty printing.
|
||||
"""
|
||||
def format_var({var, _, context}), do: format_var(var, context)
|
||||
def format_var(var, nil), do: "\"#{var}\""
|
||||
def format_var(var, context), do: "\"#{var}\" (context #{inspect(context)})"
|
||||
|
||||
defp pluralize([_], singular, _plural), do: singular
|
||||
defp pluralize(_, _singular, plural), do: plural
|
||||
|
||||
defp expr_hints({:<<>>, [inferred_bitstring_spec: true] ++ _meta, _}),
|
||||
do: [:inferred_bitstring_spec]
|
||||
|
||||
defp expr_hints(_), do: []
|
||||
|
||||
@doc """
|
||||
Converts the given expression to a string,
|
||||
translating inlined Erlang calls back to Elixir.
|
||||
"""
|
||||
def expr_to_string(expr) do
|
||||
expr
|
||||
|> reverse_rewrite()
|
||||
|> Macro.to_string()
|
||||
end
|
||||
|
||||
defp reverse_rewrite(guard) do
|
||||
Macro.prewalk(guard, fn
|
||||
|> Macro.prewalk(fn
|
||||
{{:., _, [mod, fun]}, meta, args} -> erl_to_ex(mod, fun, args, meta)
|
||||
other -> other
|
||||
end)
|
||||
|> Macro.to_string()
|
||||
end
|
||||
|
||||
defp erl_to_ex(
|
||||
:erlang,
|
||||
:error,
|
||||
[expr, :none, [error_info: {:%{}, _, [module: Exception]}]],
|
||||
meta
|
||||
) do
|
||||
{:raise, meta, [expr]}
|
||||
end
|
||||
|
||||
defp erl_to_ex(mod, fun, args, meta) do
|
||||
case :elixir_rewrite.erl_to_ex(mod, fun, args) do
|
||||
{Kernel, fun, args} -> {fun, meta, args}
|
||||
{mod, fun, args} -> {{:., [], [mod, fun]}, meta, args}
|
||||
{Kernel, fun, args, _} -> {fun, meta, args}
|
||||
{mod, fun, args, _} -> {{:., [], [mod, fun]}, meta, args}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the AST metadata.
|
||||
"""
|
||||
def get_meta({_, meta, _}), do: meta
|
||||
def get_meta(_other), do: []
|
||||
|
||||
@doc """
|
||||
Indents new lines.
|
||||
"""
|
||||
@@ -86,51 +237,43 @@ defmodule Module.Types.Helpers do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Emits a warnings.
|
||||
Emits a warning.
|
||||
"""
|
||||
def warn(module, warning, meta, stack, context) do
|
||||
if Keyword.get(meta, :generated, false) do
|
||||
context
|
||||
else
|
||||
effectively_warn(module, warning, meta, stack, context)
|
||||
end
|
||||
end
|
||||
|
||||
defp effectively_warn(module, warning, meta, stack, context) do
|
||||
{fun, arity} = stack.function
|
||||
location = {stack.file, meta, {stack.module, fun, arity}}
|
||||
%{context | warnings: [{module, warning, location} | context.warnings]}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Like `Enum.reduce/3` but only continues while `fun` returns `{:ok, acc}`
|
||||
and stops on `{:error, reason}`.
|
||||
Emits an error.
|
||||
|
||||
In practice an error is a warning that halts other errors from being collected.
|
||||
"""
|
||||
def reduce_ok(list, acc, fun) do
|
||||
do_reduce_ok(list, acc, fun)
|
||||
end
|
||||
def error(module, warning, meta, stack, context) do
|
||||
case context do
|
||||
%{failed: true} ->
|
||||
context
|
||||
|
||||
defp do_reduce_ok([head | tail], acc, fun) do
|
||||
case fun.(head, acc) do
|
||||
{:ok, acc} ->
|
||||
do_reduce_ok(tail, acc, fun)
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
%{failed: false} ->
|
||||
if Keyword.get(meta, :generated, false) do
|
||||
context
|
||||
else
|
||||
effectively_warn(module, warning, meta, stack, %{context | failed: true})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp do_reduce_ok([], acc, _fun), do: {:ok, acc}
|
||||
|
||||
@doc """
|
||||
Like `Enum.map_reduce/3` but only continues while `fun` returns `{:ok, elem, acc}`
|
||||
and stops on `{:error, reason}`.
|
||||
The type to return when there is an error.
|
||||
"""
|
||||
def map_reduce_ok(list, acc, fun) do
|
||||
do_map_reduce_ok(list, {[], acc}, fun)
|
||||
end
|
||||
|
||||
defp do_map_reduce_ok([head | tail], {list, acc}, fun) do
|
||||
case fun.(head, acc) do
|
||||
{:ok, elem, acc} ->
|
||||
do_map_reduce_ok(tail, {[elem | list], acc}, fun)
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
defp do_map_reduce_ok([], {list, acc}, _fun), do: {:ok, Enum.reverse(list), acc}
|
||||
def error_type, do: Module.Types.Descr.dynamic()
|
||||
end
|
||||
|
||||
+295
-461
@@ -2,8 +2,6 @@ defmodule Module.Types.Of do
|
||||
# Typing functionality shared between Expr and Pattern.
|
||||
# Generic AST and Enum helpers go to Module.Types.Helpers.
|
||||
@moduledoc false
|
||||
|
||||
alias Module.ParallelChecker
|
||||
import Module.Types.{Helpers, Descr}
|
||||
|
||||
@prefix quote(do: ...)
|
||||
@@ -26,6 +24,23 @@ defmodule Module.Types.Of do
|
||||
type
|
||||
end
|
||||
|
||||
@doc """
|
||||
Marks a variable with error.
|
||||
"""
|
||||
def error_var(var, context) do
|
||||
{var_name, meta, var_context} = var
|
||||
version = Keyword.fetch!(meta, :version)
|
||||
|
||||
data = %{
|
||||
type: error_type(),
|
||||
name: var_name,
|
||||
context: var_context,
|
||||
off_traces: []
|
||||
}
|
||||
|
||||
put_in(context.vars[version], data)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Refines the type of a variable.
|
||||
"""
|
||||
@@ -47,8 +62,8 @@ defmodule Module.Types.Of do
|
||||
|
||||
# We need to return error otherwise it leads to cascading errors
|
||||
if empty?(new_type) do
|
||||
{:error,
|
||||
warn(__MODULE__, {:refine_var, old_type, type, var, context}, meta, stack, context)}
|
||||
{:error, error_type(),
|
||||
error({:refine_var, old_type, type, var, context}, meta, stack, context)}
|
||||
else
|
||||
{:ok, new_type, context}
|
||||
end
|
||||
@@ -80,65 +95,94 @@ defmodule Module.Types.Of do
|
||||
def map_fetch(expr, type, field, stack, context) when is_atom(field) do
|
||||
case map_fetch(type, field) do
|
||||
{_optional?, value_type} ->
|
||||
{:ok, value_type, context}
|
||||
{value_type, context}
|
||||
|
||||
reason ->
|
||||
{:ok, dynamic(),
|
||||
warn({reason, expr, type, field, context}, elem(expr, 1), stack, context)}
|
||||
{error_type(), error({reason, expr, type, field, context}, elem(expr, 1), stack, context)}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Builds a closed map.
|
||||
"""
|
||||
def closed_map(pairs, extra \\ [], stack, context, of_fun) do
|
||||
result =
|
||||
reduce_ok(pairs, {true, extra, [], context}, fn
|
||||
{key, value}, {closed?, single, multiple, context} ->
|
||||
with {:ok, keys, context} <- of_finite_key_type(key, stack, context, of_fun),
|
||||
{:ok, value_type, context} <- of_fun.(value, stack, context) do
|
||||
case keys do
|
||||
:none ->
|
||||
{:ok, {false, single, multiple, context}}
|
||||
def closed_map(pairs, stack, context, of_fun) do
|
||||
permutate_map(pairs, stack, context, of_fun, fn fallback, _keys, pairs ->
|
||||
# TODO: Use the fallback type to actually indicate if open or closed.
|
||||
if fallback == none(), do: closed_map(pairs), else: open_map(pairs)
|
||||
end)
|
||||
end
|
||||
|
||||
[key] when multiple == [] ->
|
||||
{:ok, {closed?, [{key, value_type} | single], multiple, context}}
|
||||
@doc """
|
||||
Builds permutation of maps according to the given keys.
|
||||
"""
|
||||
def permutate_map(pairs, %{mode: :traversal} = stack, context, of_fun, _of_map) do
|
||||
context =
|
||||
Enum.reduce(pairs, context, fn {key, value}, context ->
|
||||
{_, context} = of_fun.(key, stack, context)
|
||||
{_, context} = of_fun.(value, stack, context)
|
||||
context
|
||||
end)
|
||||
|
||||
keys ->
|
||||
{:ok, {closed?, single, [{keys, value_type} | multiple], context}}
|
||||
end
|
||||
{dynamic(), context}
|
||||
end
|
||||
|
||||
def permutate_map(pairs, stack, context, of_fun, of_map) do
|
||||
{dynamic?, fallback, single, multiple, assert, context} =
|
||||
Enum.reduce(pairs, {false, none(), [], [], [], context}, fn
|
||||
{key, value}, {dynamic?, fallback, single, multiple, assert, context} ->
|
||||
{dynamic_key?, keys, context} = finite_key_type(key, stack, context, of_fun)
|
||||
{value_type, context} = of_fun.(value, stack, context)
|
||||
dynamic? = dynamic? or dynamic_key? or gradual?(value_type)
|
||||
|
||||
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, context}
|
||||
|
||||
[key] when multiple == [] ->
|
||||
{dynamic?, fallback, [{key, value_type} | single], multiple, assert, context}
|
||||
|
||||
keys ->
|
||||
{dynamic?, fallback, single, [{keys, value_type} | multiple], assert, context}
|
||||
end
|
||||
end)
|
||||
|
||||
with {:ok, {closed?, single, multiple, context}} <- result do
|
||||
map =
|
||||
case Enum.reverse(multiple) do
|
||||
[] ->
|
||||
pairs = Enum.reverse(single)
|
||||
if closed?, do: closed_map(pairs), else: open_map(pairs)
|
||||
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
|
||||
pairs = Enum.reverse(single, [{key, type} | t])
|
||||
if closed?, do: closed_map(pairs), else: open_map(pairs)
|
||||
end
|
||||
|> Enum.reduce(&union/2)
|
||||
end
|
||||
|
||||
{:ok, map, context}
|
||||
end
|
||||
end
|
||||
|
||||
defp of_finite_key_type(key, _stack, context, _of_fun) when is_atom(key) do
|
||||
{:ok, [key], context}
|
||||
end
|
||||
|
||||
defp of_finite_key_type(key, stack, context, of_fun) do
|
||||
with {:ok, key_type, context} <- of_fun.(key, stack, context) do
|
||||
case atom_fetch(key_type) do
|
||||
{:finite, list} -> {:ok, list, context}
|
||||
_ -> {:ok, :none, context}
|
||||
[{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)
|
||||
end
|
||||
|
||||
if dynamic?, do: {dynamic(map), context}, else: {map, context}
|
||||
end
|
||||
|
||||
defp finite_key_type(key, _stack, context, _of_fun) when is_atom(key) do
|
||||
{false, [key], context}
|
||||
end
|
||||
|
||||
defp finite_key_type(key, stack, context, of_fun) do
|
||||
{key_type, context} = of_fun.(key, stack, context)
|
||||
|
||||
case atom_fetch(key_type) do
|
||||
{:finite, list} -> {gradual?(key_type), list, context}
|
||||
_ -> {gradual?(key_type), :none, context}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -153,45 +197,53 @@ defmodule Module.Types.Of do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Handles structs creation.
|
||||
Handles instantiation of a new struct.
|
||||
"""
|
||||
def struct({:%, meta, _}, struct, args, default_handling, stack, context, of_fun)
|
||||
def struct_instance(struct, args, meta, stack, context, of_fun)
|
||||
when is_atom(struct) do
|
||||
# The compiler has already checked the keys are atoms and which ones are required.
|
||||
with {:ok, args_types, context} <-
|
||||
map_reduce_ok(args, context, fn {key, value}, context when is_atom(key) ->
|
||||
with {:ok, type, context} <- of_fun.(value, stack, context) do
|
||||
{:ok, {key, type}, context}
|
||||
end
|
||||
end) do
|
||||
struct(struct, args_types, default_handling, meta, stack, context)
|
||||
{args_types, context} =
|
||||
Enum.map_reduce(args, context, fn {key, value}, context when is_atom(key) ->
|
||||
{type, context} = of_fun.(value, stack, context)
|
||||
{{key, type}, context}
|
||||
end)
|
||||
|
||||
{info, context} = struct_info(struct, meta, stack, context)
|
||||
{struct_type(struct, info, args_types), context}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns `__info__(:struct)` information about a struct.
|
||||
"""
|
||||
def struct_info(struct, meta, stack, context) do
|
||||
case stack.no_warn_undefined do
|
||||
%Macro.Env{} = env ->
|
||||
case :elixir_map.maybe_load_struct_info(meta, struct, [], false, env) do
|
||||
{:ok, info} -> {info, context}
|
||||
{:error, desc} -> raise ArgumentError, List.to_string(:elixir_map.format_error(desc))
|
||||
end
|
||||
|
||||
_ ->
|
||||
# Fetch the signature to validate for warnings.
|
||||
{_, context} = Module.Types.Apply.signature(struct, :__struct__, 0, meta, stack, context)
|
||||
|
||||
info =
|
||||
struct.__info__(:struct) ||
|
||||
raise "expected #{inspect(struct)} to return struct metadata, but got none"
|
||||
|
||||
{info, context}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Struct handling assuming the args have already been converted.
|
||||
Builds a type from the struct info.
|
||||
"""
|
||||
# TODO: Allow structs fields to be defined. If the fields are defined,
|
||||
# then the struct is no longer dynamic. And we need to validate args
|
||||
# against the struct types.
|
||||
# TODO: Use the struct default values to define the default types.
|
||||
def struct(struct, args_types, default_handling, meta, stack, context) do
|
||||
context = remote(struct, :__struct__, 0, meta, stack, context)
|
||||
def struct_type(struct, info, args_types \\ []) do
|
||||
term = term()
|
||||
|
||||
defaults =
|
||||
for %{field: field} <- struct.__info__(:struct), field != :__struct__ do
|
||||
{field, term}
|
||||
end
|
||||
|
||||
pairs =
|
||||
case default_handling do
|
||||
:merge_defaults -> [{:__struct__, atom([struct])} | defaults] ++ args_types
|
||||
:skip_defaults -> [{:__struct__, atom([struct])} | args_types]
|
||||
:only_defaults -> [{:__struct__, atom([struct])} | defaults]
|
||||
end
|
||||
|
||||
{:ok, dynamic(closed_map(pairs)), context}
|
||||
pairs = for %{field: field} <- info, do: {field, term}
|
||||
pairs = [{:__struct__, atom([struct])} | pairs]
|
||||
pairs = if args_types == [], do: pairs, else: pairs ++ args_types
|
||||
closed_map(pairs)
|
||||
end
|
||||
|
||||
## Binary
|
||||
@@ -202,211 +254,122 @@ defmodule Module.Types.Of do
|
||||
In the stack, we add nodes such as <<expr>>, <<..., expr>>, etc,
|
||||
based on the position of the expression within the binary.
|
||||
"""
|
||||
def binary([], _kind, _stack, context, _of_fun) do
|
||||
{:ok, context}
|
||||
def binary([], _kind, _stack, context) do
|
||||
context
|
||||
end
|
||||
|
||||
def binary([head], kind, stack, context, of_fun) do
|
||||
binary_segment(head, kind, [head], stack, context, of_fun)
|
||||
def binary([head], kind, stack, context) do
|
||||
binary_segment(head, kind, [head], stack, context)
|
||||
end
|
||||
|
||||
def binary([head | tail], kind, stack, context, of_fun) do
|
||||
case binary_segment(head, kind, [head, @suffix], stack, context, of_fun) do
|
||||
{:ok, context} -> binary_many(tail, kind, stack, context, of_fun)
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
def binary([head | tail], kind, stack, context) do
|
||||
context = binary_segment(head, kind, [head, @suffix], stack, context)
|
||||
binary_many(tail, kind, stack, context)
|
||||
end
|
||||
|
||||
defp binary_many([last], kind, stack, context, of_fun) do
|
||||
binary_segment(last, kind, [@prefix, last], stack, context, of_fun)
|
||||
defp binary_many([last], kind, stack, context) do
|
||||
binary_segment(last, kind, [@prefix, last], stack, context)
|
||||
end
|
||||
|
||||
defp binary_many([head | tail], kind, stack, context, of_fun) do
|
||||
case binary_segment(head, kind, [@prefix, head, @suffix], stack, context, of_fun) do
|
||||
{:ok, context} -> binary_many(tail, kind, stack, context, of_fun)
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
defp binary_many([head | tail], kind, stack, context) do
|
||||
context = binary_segment(head, kind, [@prefix, head, @suffix], stack, context)
|
||||
binary_many(tail, kind, stack, context)
|
||||
end
|
||||
|
||||
# If the segment is a literal, the compiler has already checked its validity,
|
||||
# so we just skip it.
|
||||
defp binary_segment({:"::", _meta, [left, _right]}, _kind, _args, _stack, context, _of_fun)
|
||||
defp binary_segment({:"::", _meta, [left, _right]}, _kind, _args, _stack, context)
|
||||
when is_binary(left) or is_number(left) do
|
||||
{:ok, context}
|
||||
context
|
||||
end
|
||||
|
||||
defp binary_segment({:"::", meta, [left, right]}, kind, args, stack, context, of_fun) do
|
||||
expected_type = specifier_info(kind, right)
|
||||
defp binary_segment({:"::", meta, [left, right]}, kind, args, stack, context) do
|
||||
type = specifier_type(kind, right)
|
||||
expr = {:<<>>, meta, args}
|
||||
|
||||
with {:ok, _type, context} <- of_fun.(left, {expected_type, expr}, stack, context) do
|
||||
{:ok, context}
|
||||
end
|
||||
{_type, context} =
|
||||
case kind do
|
||||
:match ->
|
||||
Module.Types.Pattern.of_match_var(left, type, expr, stack, context)
|
||||
|
||||
:guard ->
|
||||
Module.Types.Pattern.of_guard(left, type, expr, stack, context)
|
||||
|
||||
:expr ->
|
||||
{actual, context} = Module.Types.Expr.of_expr(left, stack, context)
|
||||
intersect(actual, type, expr, stack, context)
|
||||
end
|
||||
|
||||
specifier_size(kind, right, stack, context)
|
||||
end
|
||||
|
||||
defp specifier_info(kind, {:-, _, [left, _right]}), do: specifier_info(kind, left)
|
||||
defp specifier_info(:expr, {:float, _, _}), do: @integer_or_float
|
||||
defp specifier_info(:expr, {:utf8, _, _}), do: @integer_or_binary
|
||||
defp specifier_info(:expr, {:utf16, _, _}), do: @integer_or_binary
|
||||
defp specifier_info(:expr, {:utf32, _, _}), do: @integer_or_binary
|
||||
defp specifier_info(:pattern, {:utf8, _, _}), do: @integer
|
||||
defp specifier_info(:pattern, {:utf16, _, _}), do: @integer
|
||||
defp specifier_info(:pattern, {:utf32, _, _}), do: @integer
|
||||
defp specifier_info(:pattern, {:float, _, _}), do: @float
|
||||
defp specifier_info(_kind, {:integer, _, _}), do: @integer
|
||||
defp specifier_info(_kind, {:bits, _, _}), do: @binary
|
||||
defp specifier_info(_kind, {:bitstring, _, _}), do: @binary
|
||||
defp specifier_info(_kind, {:bytes, _, _}), do: @binary
|
||||
defp specifier_info(_kind, {:binary, _, _}), do: @binary
|
||||
defp specifier_info(_kind, _specifier), do: @integer
|
||||
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
|
||||
|
||||
## Apply
|
||||
|
||||
def apply(:erlang, name, [left, right], expr, stack, context)
|
||||
when name in [:>=, :"=<", :>, :<, :min, :max] do
|
||||
result = if name in [:min, :max], do: union(left, right), else: boolean()
|
||||
|
||||
cond do
|
||||
match?({false, _}, map_fetch(left, :__struct__)) or
|
||||
match?({false, _}, map_fetch(right, :__struct__)) ->
|
||||
warning = {:struct_comparison, expr, context}
|
||||
{:ok, result, warn(warning, elem(expr, 1), stack, context)}
|
||||
|
||||
number_type?(left) and number_type?(right) ->
|
||||
{:ok, result, context}
|
||||
|
||||
empty?(intersection(left, right)) ->
|
||||
warning = {:mismatched_comparison, expr, context}
|
||||
{:ok, result, warn(warning, elem(expr, 1), stack, context)}
|
||||
|
||||
true ->
|
||||
{:ok, result, context}
|
||||
end
|
||||
defp specifier_size(kind, {:-, _, [left, right]}, stack, context) do
|
||||
specifier_size(kind, right, stack, specifier_size(kind, left, stack, context))
|
||||
end
|
||||
|
||||
def apply(mod, name, args, expr, stack, context) do
|
||||
case :elixir_rewrite.inline(mod, name, length(args)) do
|
||||
{mod, name} -> apply(mod, name, args, expr, stack, context)
|
||||
false -> {:ok, dynamic(), context}
|
||||
end
|
||||
defp specifier_size(:expr, {:size, _, [arg]} = expr, stack, context)
|
||||
when not is_integer(arg) do
|
||||
{actual, context} = Module.Types.Expr.of_expr(arg, stack, context)
|
||||
{_, context} = intersect(actual, integer(), expr, stack, context)
|
||||
context
|
||||
end
|
||||
|
||||
## Remote
|
||||
defp specifier_size(_pattern_or_guard, {:size, _, [arg]} = expr, stack, context)
|
||||
when not is_integer(arg) do
|
||||
{_type, context} = Module.Types.Pattern.of_guard(arg, integer(), expr, stack, context)
|
||||
context
|
||||
end
|
||||
|
||||
def remote(type, fun, arity, hints \\ [], expr, meta, stack, context) do
|
||||
defp specifier_size(_kind, _specifier, _stack, context) do
|
||||
context
|
||||
end
|
||||
|
||||
## Modules
|
||||
|
||||
@doc """
|
||||
Returns modules in a type.
|
||||
|
||||
The call information is used on report reporting.
|
||||
"""
|
||||
def modules(type, fun, arity, hints \\ [], expr, meta, stack, context) do
|
||||
case atom_fetch(type) do
|
||||
{_, mods} ->
|
||||
context =
|
||||
Enum.reduce(mods, context, fn mod, context ->
|
||||
remote(mod, fun, arity, meta, stack, context)
|
||||
end)
|
||||
|
||||
{mods, context}
|
||||
|
||||
:error ->
|
||||
warning = {:badmodule, expr, type, fun, arity, hints, context}
|
||||
{[], warn(warning, meta, stack, context)}
|
||||
{[], error(warning, meta, stack, context)}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks a module is a valid remote.
|
||||
"""
|
||||
def remote(module, fun, arity, meta, stack, context) when is_atom(module) do
|
||||
if Keyword.get(meta, :runtime_module, false) do
|
||||
context
|
||||
else
|
||||
ParallelChecker.preload_module(stack.cache, module)
|
||||
check_export(module, fun, arity, meta, stack, context)
|
||||
end
|
||||
end
|
||||
|
||||
defp check_export(module, fun, arity, meta, stack, context) do
|
||||
case ParallelChecker.fetch_export(stack.cache, module, fun, arity) do
|
||||
{:ok, mode, :def, reason} ->
|
||||
check_deprecated(mode, module, fun, arity, reason, meta, stack, context)
|
||||
|
||||
{:ok, mode, :defmacro, reason} ->
|
||||
context = warn({:unrequired_module, module, fun, arity}, meta, stack, context)
|
||||
check_deprecated(mode, module, fun, arity, reason, meta, stack, context)
|
||||
|
||||
{:error, :module} ->
|
||||
if warn_undefined?(module, fun, arity, stack) do
|
||||
warn({:undefined_module, module, fun, arity}, meta, stack, context)
|
||||
else
|
||||
context
|
||||
end
|
||||
|
||||
{:error, :function} ->
|
||||
if warn_undefined?(module, fun, arity, stack) do
|
||||
exports = ParallelChecker.all_exports(stack.cache, module)
|
||||
warn({:undefined_function, module, fun, arity, exports}, meta, stack, context)
|
||||
else
|
||||
context
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp check_deprecated(:elixir, module, fun, arity, reason, meta, stack, context) do
|
||||
if reason do
|
||||
warn({:deprecated, module, fun, arity, reason}, meta, stack, context)
|
||||
else
|
||||
context
|
||||
end
|
||||
end
|
||||
|
||||
defp check_deprecated(:erlang, module, fun, arity, _reason, meta, stack, context) do
|
||||
case :otp_internal.obsolete(module, fun, arity) do
|
||||
{:deprecated, string} when is_list(string) ->
|
||||
reason = string |> List.to_string() |> :string.titlecase()
|
||||
warn({:deprecated, module, fun, arity, reason}, meta, stack, context)
|
||||
|
||||
{:deprecated, string, removal} when is_list(string) and is_list(removal) ->
|
||||
reason = string |> List.to_string() |> :string.titlecase()
|
||||
reason = "It will be removed in #{removal}. #{reason}"
|
||||
warn({:deprecated, module, fun, arity, reason}, meta, stack, context)
|
||||
|
||||
_ ->
|
||||
context
|
||||
end
|
||||
end
|
||||
|
||||
# The protocol code dispatches to unknown modules, so we ignore them here.
|
||||
#
|
||||
# try do
|
||||
# SomeProtocol.Atom.__impl__
|
||||
# rescue
|
||||
# ...
|
||||
# end
|
||||
#
|
||||
# But for protocols we don't want to traverse the protocol code anyway.
|
||||
# TODO: remove this clause once we no longer traverse the protocol code.
|
||||
defp warn_undefined?(_module, :__impl__, 1, _stack), do: false
|
||||
defp warn_undefined?(_module, :module_info, 0, _stack), do: false
|
||||
defp warn_undefined?(_module, :module_info, 1, _stack), do: false
|
||||
defp warn_undefined?(:erlang, :orelse, 2, _stack), do: false
|
||||
defp warn_undefined?(:erlang, :andalso, 2, _stack), do: false
|
||||
|
||||
defp warn_undefined?(_, _, _, %{no_warn_undefined: :all}) do
|
||||
false
|
||||
end
|
||||
|
||||
defp warn_undefined?(module, fun, arity, stack) do
|
||||
not Enum.any?(stack.no_warn_undefined, &(&1 == module or &1 == {module, fun, arity}))
|
||||
end
|
||||
|
||||
## Warning helpers
|
||||
|
||||
@doc """
|
||||
Intersects two types and emit an incompatible warning if empty.
|
||||
Intersects two types and emit an incompatible error if empty.
|
||||
"""
|
||||
def intersect(actual, {expected, expr}, stack, context) do
|
||||
def intersect(actual, expected, expr, stack, context) do
|
||||
type = intersection(actual, expected)
|
||||
|
||||
if empty?(type) do
|
||||
{:error, incompatible_warn(expr, expected, actual, stack, context)}
|
||||
{error_type(), incompatible_error(expr, expected, actual, stack, context)}
|
||||
else
|
||||
{:ok, type, context}
|
||||
{type, context}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -416,259 +379,130 @@ defmodule Module.Types.Of do
|
||||
This is a generic warning for when the expected/actual types
|
||||
themselves may come from several different circumstances.
|
||||
"""
|
||||
def incompatible_warn(expr, expected_type, actual_type, stack, context) do
|
||||
def incompatible_error(expr, expected_type, actual_type, stack, context) do
|
||||
meta = get_meta(expr) || stack.meta
|
||||
hints = if meta[:inferred_bitstring_spec], do: [:inferred_bitstring_spec], else: []
|
||||
warning = {:incompatible, expr, expected_type, actual_type, hints, context}
|
||||
warn(__MODULE__, warning, meta, stack, context)
|
||||
error(warning, meta, stack, context)
|
||||
end
|
||||
|
||||
defp warn(warning, meta, stack, context) do
|
||||
warn(__MODULE__, warning, meta, stack, context)
|
||||
defp error(warning, meta, stack, context) do
|
||||
error(__MODULE__, warning, meta, stack, context)
|
||||
end
|
||||
|
||||
## Traces
|
||||
|
||||
def format_traces(expr, %{vars: vars}) do
|
||||
{_, versions} =
|
||||
Macro.prewalk(expr, %{}, fn
|
||||
{var_name, meta, var_context}, versions when is_atom(var_name) and is_atom(var_context) ->
|
||||
version = meta[:version]
|
||||
|
||||
case vars do
|
||||
%{^version => data} -> {:ok, Map.put(versions, version, data)}
|
||||
%{} -> {:ok, versions}
|
||||
end
|
||||
|
||||
node, versions ->
|
||||
{node, versions}
|
||||
end)
|
||||
|
||||
versions
|
||||
|> Map.values()
|
||||
|> Enum.sort_by(& &1.name)
|
||||
|> Enum.map(&format_trace/1)
|
||||
end
|
||||
|
||||
defp format_trace(%{off_traces: []}) do
|
||||
[]
|
||||
end
|
||||
|
||||
defp format_trace(%{name: name, context: context, off_traces: traces}) do
|
||||
traces =
|
||||
traces
|
||||
|> Enum.reverse()
|
||||
|> Enum.map(fn {expr, file, type, formatter} ->
|
||||
meta = get_meta(expr)
|
||||
|
||||
location =
|
||||
file
|
||||
|> Path.relative_to_cwd()
|
||||
|> Exception.format_file_line(meta[:line])
|
||||
|> String.replace_suffix(":", "")
|
||||
|
||||
[
|
||||
"""
|
||||
|
||||
# type: #{to_quoted_string(type) |> indent(4)}
|
||||
# from: #{location}
|
||||
\
|
||||
""",
|
||||
case formatter do
|
||||
:default -> [expr |> expr_to_string() |> indent(4), ?\n]
|
||||
formatter -> formatter.(expr)
|
||||
end,
|
||||
format_hints(expr_hints(expr))
|
||||
]
|
||||
end)
|
||||
|
||||
type_or_types = pluralize(traces, "type", "types")
|
||||
["\nwhere #{format_var(name, context)} was given the #{type_or_types}:\n" | traces]
|
||||
end
|
||||
|
||||
defp format_var({var, _, context}), do: format_var(var, context)
|
||||
defp format_var(var, nil), do: "\"#{var}\""
|
||||
defp format_var(var, context), do: "\"#{var}\" (context #{inspect(context)})"
|
||||
|
||||
defp pluralize([_], singular, _plural), do: singular
|
||||
defp pluralize(_, _singular, plural), do: plural
|
||||
|
||||
defp expr_hints({:<<>>, [inferred_bitstring_spec: true] ++ _meta, _}),
|
||||
do: [:inferred_bitstring_spec]
|
||||
|
||||
defp expr_hints(_), do: []
|
||||
|
||||
## Warning formatting
|
||||
|
||||
def format_warning({:refine_var, old_type, new_type, var, context}) do
|
||||
[
|
||||
"""
|
||||
incompatible types assigned to #{format_var(var)}:
|
||||
def format_diagnostic({:refine_var, old_type, new_type, var, context}) do
|
||||
traces = collect_traces(var, context)
|
||||
|
||||
#{to_quoted_string(old_type)} !~ #{to_quoted_string(new_type)}
|
||||
""",
|
||||
format_traces(var, context),
|
||||
"\ntyping violation found at:"
|
||||
]
|
||||
%{
|
||||
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_warning({:incompatible, expr, expected_type, actual_type, hints, context}) do
|
||||
[
|
||||
"""
|
||||
incompatible types in expression:
|
||||
def format_diagnostic({:incompatible, expr, expected_type, actual_type, hints, context}) do
|
||||
traces = collect_traces(expr, context)
|
||||
|
||||
#{expr_to_string(expr) |> indent(4)}
|
||||
%{
|
||||
details: %{typing_traces: traces},
|
||||
message:
|
||||
IO.iodata_to_binary([
|
||||
"""
|
||||
incompatible types in expression:
|
||||
|
||||
expected type:
|
||||
#{expr_to_string(expr) |> indent(4)}
|
||||
|
||||
#{to_quoted_string(expected_type) |> indent(4)}
|
||||
got type:
|
||||
|
||||
but got type:
|
||||
#{to_quoted_string(actual_type) |> indent(4)}
|
||||
|
||||
#{to_quoted_string(actual_type) |> indent(4)}
|
||||
""",
|
||||
format_traces(expr, context),
|
||||
format_hints(hints),
|
||||
"\ntyping violation found at:"
|
||||
]
|
||||
but expected type:
|
||||
|
||||
#{to_quoted_string(expected_type) |> indent(4)}
|
||||
""",
|
||||
format_traces(traces),
|
||||
format_hints(hints)
|
||||
])
|
||||
}
|
||||
end
|
||||
|
||||
def format_warning({:badmap, expr, type, key, context}) do
|
||||
[
|
||||
"""
|
||||
expected a map or struct when accessing .#{key} in expression:
|
||||
def format_diagnostic({:badmodule, expr, type, fun, arity, hints, context}) do
|
||||
traces = collect_traces(expr, context)
|
||||
|
||||
#{expr_to_string(expr) |> indent(4)}
|
||||
""",
|
||||
empty_if(dot_var?(expr), """
|
||||
%{
|
||||
details: %{typing_traces: traces},
|
||||
message:
|
||||
IO.iodata_to_binary([
|
||||
"""
|
||||
expected a module (an atom) when invoking #{fun}/#{arity} in expression:
|
||||
|
||||
but got type:
|
||||
#{expr_to_string(expr) |> indent(4)}
|
||||
""",
|
||||
empty_if(dot_var?(expr), """
|
||||
|
||||
#{to_quoted_string(type) |> indent(4)}
|
||||
"""),
|
||||
format_traces(expr, context),
|
||||
format_hints([:dot]),
|
||||
"\ntyping violation found at:"
|
||||
]
|
||||
but got type:
|
||||
|
||||
#{to_quoted_string(type) |> indent(4)}
|
||||
"""),
|
||||
format_traces(traces),
|
||||
format_hints(hints)
|
||||
])
|
||||
}
|
||||
end
|
||||
|
||||
def format_warning({:badkey, expr, type, key, context}) do
|
||||
[
|
||||
"""
|
||||
unknown key .#{key} in expression:
|
||||
def format_diagnostic({:badmap, expr, type, key, context}) do
|
||||
traces = collect_traces(expr, context)
|
||||
|
||||
#{expr_to_string(expr) |> indent(4)}
|
||||
""",
|
||||
empty_if(dot_var?(expr), """
|
||||
%{
|
||||
details: %{typing_traces: traces},
|
||||
message:
|
||||
IO.iodata_to_binary([
|
||||
"""
|
||||
expected a map or struct when accessing .#{key} in expression:
|
||||
|
||||
the given type does not have the given key:
|
||||
#{expr_to_string(expr) |> indent(4)}
|
||||
""",
|
||||
empty_if(dot_var?(expr), """
|
||||
|
||||
#{to_quoted_string(type) |> indent(4)}
|
||||
"""),
|
||||
format_traces(expr, context),
|
||||
"\ntyping violation found at:"
|
||||
]
|
||||
but got type:
|
||||
|
||||
#{to_quoted_string(type) |> indent(4)}
|
||||
"""),
|
||||
format_traces(traces),
|
||||
format_hints([:dot])
|
||||
])
|
||||
}
|
||||
end
|
||||
|
||||
def format_warning({:badmodule, expr, type, fun, arity, hints, context}) do
|
||||
[
|
||||
"""
|
||||
expected a module (an atom) when invoking #{fun}/#{arity} in expression:
|
||||
def format_diagnostic({:badkey, expr, type, key, context}) do
|
||||
traces = collect_traces(expr, context)
|
||||
|
||||
#{expr_to_string(expr) |> indent(4)}
|
||||
""",
|
||||
empty_if(dot_var?(expr), """
|
||||
%{
|
||||
details: %{typing_traces: traces},
|
||||
span: expr |> get_meta() |> :elixir_env.calculate_span(key) |> Keyword.get(:span),
|
||||
message:
|
||||
IO.iodata_to_binary([
|
||||
"""
|
||||
unknown key .#{key} in expression:
|
||||
|
||||
but got type:
|
||||
#{expr_to_string(expr) |> indent(4)}
|
||||
|
||||
#{to_quoted_string(type) |> indent(4)}
|
||||
"""),
|
||||
format_traces(expr, context),
|
||||
format_hints(hints),
|
||||
"\ntyping violation found at:"
|
||||
]
|
||||
end
|
||||
the given type does not have the given key:
|
||||
|
||||
def format_warning({:undefined_module, module, fun, arity}) do
|
||||
top =
|
||||
if fun == :__struct__ and arity == 0 do
|
||||
"struct #{inspect(module)}"
|
||||
else
|
||||
Exception.format_mfa(module, fun, arity)
|
||||
end
|
||||
|
||||
[
|
||||
top,
|
||||
" is undefined (module ",
|
||||
inspect(module),
|
||||
" is not available or is yet to be defined)"
|
||||
]
|
||||
end
|
||||
|
||||
def format_warning({:undefined_function, module, :__struct__, 0, _exports}) do
|
||||
"struct #{inspect(module)} is undefined (there is such module but it does not define a struct)"
|
||||
end
|
||||
|
||||
def format_warning({:undefined_function, module, fun, arity, exports}) do
|
||||
[
|
||||
Exception.format_mfa(module, fun, arity),
|
||||
" is undefined or private",
|
||||
UndefinedFunctionError.hint_for_loaded_module(module, fun, arity, exports)
|
||||
]
|
||||
end
|
||||
|
||||
def format_warning({:deprecated, module, fun, arity, reason}) do
|
||||
[
|
||||
Exception.format_mfa(module, fun, arity),
|
||||
" is deprecated. ",
|
||||
reason
|
||||
]
|
||||
end
|
||||
|
||||
def format_warning({:unrequired_module, module, fun, arity}) do
|
||||
[
|
||||
"you must require ",
|
||||
inspect(module),
|
||||
" before invoking the macro ",
|
||||
Exception.format_mfa(module, fun, arity)
|
||||
]
|
||||
end
|
||||
|
||||
def format_warning({:mismatched_comparison, expr, context}) do
|
||||
[
|
||||
"""
|
||||
comparison between incompatible types found:
|
||||
|
||||
#{expr_to_string(expr) |> indent(4)}
|
||||
""",
|
||||
format_traces(expr, context),
|
||||
"""
|
||||
|
||||
While Elixir can compare across all types, you are comparing \
|
||||
across types which are always distinct, and the result is either \
|
||||
always true or always false
|
||||
""",
|
||||
"\ntyping violation found at:"
|
||||
]
|
||||
end
|
||||
|
||||
def format_warning({:struct_comparison, expr, context}) do
|
||||
[
|
||||
"""
|
||||
comparison with structs found:
|
||||
|
||||
#{expr_to_string(expr) |> indent(4)}
|
||||
""",
|
||||
format_traces(expr, context),
|
||||
"""
|
||||
|
||||
Comparison operators (>, <, >=, <=, min, and max) perform structural \
|
||||
and not semantic comparison. Comparing with a struct won't give meaningful \
|
||||
results. Struct that can be compared typically define a compare/2 function \
|
||||
within their modules that can be used for semantic comparison
|
||||
""",
|
||||
"\ntyping violation found at:"
|
||||
]
|
||||
#{to_quoted_string(type) |> indent(4)}
|
||||
""",
|
||||
format_traces(traces)
|
||||
])
|
||||
}
|
||||
end
|
||||
|
||||
defp dot_var?(expr) do
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -865,7 +865,13 @@ defmodule OptionParser do
|
||||
|
||||
defp format_error({option, nil}, opts, types) do
|
||||
if type = get_type(option, opts, types) do
|
||||
"#{option} : Missing argument of type #{type}"
|
||||
if String.contains?(option, "_") do
|
||||
msg = "#{option} : Unknown option"
|
||||
|
||||
msg <> ". Did you mean #{String.replace(option, "_", "-")}?"
|
||||
else
|
||||
"#{option} : Missing argument of type #{type}"
|
||||
end
|
||||
else
|
||||
msg = "#{option} : Unknown option"
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ defmodule PartitionSupervisor do
|
||||
|
||||
DynamicSupervisor.start_child(MyApp.DynamicSupervisor, {Agent, fn -> %{} end})
|
||||
|
||||
You can do start the dynamic supervisors under a `PartitionSupervisor`:
|
||||
You can start the dynamic supervisors under a `PartitionSupervisor`:
|
||||
|
||||
children = [
|
||||
{PartitionSupervisor,
|
||||
@@ -151,7 +151,7 @@ defmodule PartitionSupervisor do
|
||||
@doc false
|
||||
def child_spec(opts) when is_list(opts) do
|
||||
id =
|
||||
case Keyword.get(opts, :name, DynamicSupervisor) do
|
||||
case Keyword.get(opts, :name, PartitionSupervisor) do
|
||||
name when is_atom(name) -> name
|
||||
{:via, _module, name} -> name
|
||||
end
|
||||
@@ -228,26 +228,26 @@ defmodule PartitionSupervisor do
|
||||
def start_link(opts) when is_list(opts) do
|
||||
name = opts[:name]
|
||||
|
||||
unless name do
|
||||
if !name do
|
||||
raise ArgumentError, "the :name option must be given to PartitionSupervisor"
|
||||
end
|
||||
|
||||
{child_spec, opts} = Keyword.pop(opts, :child_spec)
|
||||
|
||||
unless child_spec do
|
||||
if !child_spec do
|
||||
raise ArgumentError, "the :child_spec option must be given to PartitionSupervisor"
|
||||
end
|
||||
|
||||
{partitions, opts} = Keyword.pop(opts, :partitions, System.schedulers_online())
|
||||
|
||||
unless is_integer(partitions) and partitions >= 1 do
|
||||
if not (is_integer(partitions) and partitions >= 1) do
|
||||
raise ArgumentError,
|
||||
"the :partitions option must be a positive integer, got: #{inspect(partitions)}"
|
||||
end
|
||||
|
||||
{with_arguments, opts} = Keyword.pop(opts, :with_arguments, fn args, _partition -> args end)
|
||||
|
||||
unless is_function(with_arguments, 2) do
|
||||
if not is_function(with_arguments, 2) do
|
||||
raise ArgumentError,
|
||||
"the :with_arguments option must be a function that receives two arguments, " <>
|
||||
"the current call arguments and the partition, got: #{inspect(with_arguments)}"
|
||||
@@ -260,17 +260,17 @@ defmodule PartitionSupervisor do
|
||||
for partition <- 0..(partitions - 1) do
|
||||
args = with_arguments.(args, partition)
|
||||
|
||||
unless is_list(args) do
|
||||
if not is_list(args) do
|
||||
raise "the call to the function in :with_arguments must return a list, got: #{inspect(args)}"
|
||||
end
|
||||
|
||||
start = {__MODULE__, :start_child, [mod, fun, args, name, partition]}
|
||||
start = {__MODULE__, :start_child, [mod, fun, args, partition]}
|
||||
Map.merge(map, %{id: partition, start: start, modules: modules})
|
||||
end
|
||||
|
||||
auto_shutdown = Keyword.get(opts, :auto_shutdown, :never)
|
||||
|
||||
unless auto_shutdown == :never do
|
||||
if auto_shutdown != :never do
|
||||
raise ArgumentError,
|
||||
"the :auto_shutdown option must be :never, got: #{inspect(auto_shutdown)}"
|
||||
end
|
||||
@@ -282,14 +282,14 @@ defmodule PartitionSupervisor do
|
||||
end
|
||||
|
||||
@doc false
|
||||
def start_child(mod, fun, args, name, partition) do
|
||||
def start_child(mod, fun, args, partition) do
|
||||
case apply(mod, fun, args) do
|
||||
{:ok, pid} ->
|
||||
register_child(name, partition, pid)
|
||||
register_child(partition, pid)
|
||||
{:ok, pid}
|
||||
|
||||
{:ok, pid, info} ->
|
||||
register_child(name, partition, pid)
|
||||
register_child(partition, pid)
|
||||
{:ok, pid, info}
|
||||
|
||||
other ->
|
||||
@@ -297,33 +297,97 @@ defmodule PartitionSupervisor do
|
||||
end
|
||||
end
|
||||
|
||||
defp register_child(name, partition, pid) when is_atom(name) do
|
||||
:ets.insert(name, {partition, pid})
|
||||
end
|
||||
|
||||
defp register_child({:via, _, _}, partition, pid) do
|
||||
Registry.register(@registry, {self(), partition}, pid)
|
||||
defp register_child(partition, pid) do
|
||||
:ets.insert(Process.get(:ets_table), {partition, pid})
|
||||
end
|
||||
|
||||
@impl true
|
||||
def init({name, partitions, children, init_opts}) do
|
||||
init_partitions(name, partitions)
|
||||
table = init_table(name)
|
||||
:ets.insert(table, {:partitions, partitions, partitions})
|
||||
Process.put(:ets_table, table)
|
||||
Supervisor.init(children, Keyword.put_new(init_opts, :strategy, :one_for_one))
|
||||
end
|
||||
|
||||
defp init_partitions(name, partitions) when is_atom(name) do
|
||||
:ets.new(name, [:set, :named_table, :protected, read_concurrency: true])
|
||||
:ets.insert(name, {:partitions, partitions})
|
||||
defp init_table(name) when is_atom(name) do
|
||||
:ets.new(name, [:set, :named_table, :public, read_concurrency: true])
|
||||
end
|
||||
|
||||
defp init_partitions({:via, _, _}, partitions) do
|
||||
child_spec = {Registry, keys: :unique, name: @registry}
|
||||
defp init_table({:via, _, _}) do
|
||||
table = :ets.new(__MODULE__, [:set, :public, read_concurrency: true])
|
||||
ensure_registry()
|
||||
Registry.register(@registry, self(), table)
|
||||
table
|
||||
end
|
||||
|
||||
unless Process.whereis(@registry) do
|
||||
Supervisor.start_child(:elixir_sup, child_spec)
|
||||
defp ensure_registry do
|
||||
if Process.whereis(@registry) == nil do
|
||||
Supervisor.start_child(:elixir_sup, {Registry, keys: :unique, name: @registry})
|
||||
end
|
||||
end
|
||||
|
||||
Registry.register(@registry, self(), partitions)
|
||||
@doc """
|
||||
Resizes the number of partitions in the PartitionSupervisor.
|
||||
|
||||
This is done by starting or stopping a given number of
|
||||
partitions in the supervisor. All of the child specifications
|
||||
are kept in the `PartitionSupervisor` itself.
|
||||
|
||||
The final number of partitions cannot be less than zero and
|
||||
cannot be more than the number of partitions the supervisor
|
||||
started with.
|
||||
"""
|
||||
@doc since: "1.18.0"
|
||||
@spec resize!(name(), non_neg_integer()) :: non_neg_integer()
|
||||
def resize!(name, partitions) when is_integer(partitions) do
|
||||
supervisor =
|
||||
GenServer.whereis(name) || exit({:noproc, {__MODULE__, :resize!, [name, partitions]}})
|
||||
|
||||
table = table(name)
|
||||
ensure_registry()
|
||||
|
||||
Registry.lock(@registry, supervisor, fn ->
|
||||
case :ets.lookup(table, :partitions) do
|
||||
[{:partitions, _current, max}] when partitions not in 0..max//1 ->
|
||||
raise ArgumentError,
|
||||
"the number of partitions to resize to must be a number between 0 and #{max}, got: #{partitions}"
|
||||
|
||||
[{:partitions, current, max}] when partitions > current ->
|
||||
for id <- current..(partitions - 1) do
|
||||
case Supervisor.restart_child(supervisor, id) do
|
||||
{:ok, _} ->
|
||||
:ok
|
||||
|
||||
{:ok, _, _} ->
|
||||
:ok
|
||||
|
||||
{:error, reason} ->
|
||||
raise "cannot restart partition #{id} of PartitionSupervisor #{inspect(name)} due to reason #{inspect(reason)}"
|
||||
end
|
||||
end
|
||||
|
||||
:ets.insert(table, {:partitions, partitions, max})
|
||||
current
|
||||
|
||||
[{:partitions, current, max}] when partitions < current ->
|
||||
:ets.insert(table, {:partitions, partitions, max})
|
||||
|
||||
for id <- partitions..(current - 1) do
|
||||
case Supervisor.terminate_child(supervisor, id) do
|
||||
:ok ->
|
||||
:ok
|
||||
|
||||
{:error, reason} ->
|
||||
raise "cannot terminate partition #{id} of PartitionSupervisor #{inspect(name)} due to reason #{inspect(reason)}"
|
||||
end
|
||||
end
|
||||
|
||||
current
|
||||
|
||||
[{:partitions, current, _max}] ->
|
||||
current
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -332,24 +396,27 @@ defmodule PartitionSupervisor do
|
||||
@doc since: "1.14.0"
|
||||
@spec partitions(name()) :: pos_integer()
|
||||
def partitions(name) do
|
||||
{_name, partitions} = name_partitions(name)
|
||||
partitions
|
||||
name |> table() |> partitions(name)
|
||||
end
|
||||
|
||||
# For whereis_name, we want to lookup on GenServer.whereis/1
|
||||
# just once, so we lookup the name and partitions together.
|
||||
defp name_partitions(name) when is_atom(name) do
|
||||
defp partitions(table, name) do
|
||||
try do
|
||||
{name, :ets.lookup_element(name, :partitions, 2)}
|
||||
:ets.lookup_element(table, :partitions, 2)
|
||||
rescue
|
||||
_ -> exit({:noproc, {__MODULE__, :partitions, [name]}})
|
||||
end
|
||||
end
|
||||
|
||||
defp name_partitions(name) when is_tuple(name) do
|
||||
defp table(name) when is_atom(name) do
|
||||
name
|
||||
end
|
||||
|
||||
# For whereis_name, we want to lookup on GenServer.whereis/1
|
||||
# just once, so we lookup the name and partitions together.
|
||||
defp table(name) when is_tuple(name) do
|
||||
with pid when is_pid(pid) <- GenServer.whereis(name),
|
||||
[name_partitions] <- Registry.lookup(@registry, pid) do
|
||||
name_partitions
|
||||
[{_, table}] <- Registry.lookup(@registry, pid) do
|
||||
table
|
||||
else
|
||||
_ -> exit({:noproc, {__MODULE__, :partitions, [name]}})
|
||||
end
|
||||
@@ -374,7 +441,7 @@ defmodule PartitionSupervisor do
|
||||
@doc since: "1.14.0"
|
||||
@spec which_children(name()) :: [
|
||||
# Inlining [module()] | :dynamic here because :supervisor.modules() is not exported
|
||||
{:undefined, pid | :restarting, :worker | :supervisor, [module()] | :dynamic}
|
||||
{integer(), pid | :restarting, :worker | :supervisor, [module()] | :dynamic}
|
||||
]
|
||||
def which_children(name) when is_atom(name) or elem(name, 0) == :via do
|
||||
Supervisor.which_children(name)
|
||||
@@ -428,22 +495,17 @@ defmodule PartitionSupervisor do
|
||||
|
||||
@doc false
|
||||
def whereis_name({name, key}) when is_atom(name) or is_tuple(name) do
|
||||
{name, partitions} = name_partitions(name)
|
||||
table = table(name)
|
||||
partitions = partitions(table, name)
|
||||
|
||||
if partitions == 0 do
|
||||
raise ArgumentError, "PartitionSupervisor #{inspect(name)} has zero partitions"
|
||||
end
|
||||
|
||||
partition =
|
||||
if is_integer(key), do: rem(abs(key), partitions), else: :erlang.phash2(key, partitions)
|
||||
|
||||
whereis_name(name, partition)
|
||||
end
|
||||
|
||||
defp whereis_name(name, partition) when is_atom(name) do
|
||||
:ets.lookup_element(name, partition, 2)
|
||||
end
|
||||
|
||||
defp whereis_name(name, partition) when is_pid(name) do
|
||||
@registry
|
||||
|> Registry.values({name, partition}, name)
|
||||
|> List.first(:undefined)
|
||||
:ets.lookup_element(table, partition, 2)
|
||||
end
|
||||
|
||||
@doc false
|
||||
|
||||
@@ -884,7 +884,7 @@ defmodule Path do
|
||||
* A `..` component would make it so that the path would traverse up above
|
||||
the root of `relative_to`.
|
||||
|
||||
* A symbolic link in the path points to something above the root of `cwd`.
|
||||
* A symbolic link in the path points to something above the root of `relative_to`.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -906,10 +906,10 @@ defmodule Path do
|
||||
"""
|
||||
@doc since: "1.14.0"
|
||||
@spec safe_relative(t, t) :: {:ok, binary} | :error
|
||||
def safe_relative(path, cwd \\ File.cwd!()) do
|
||||
def safe_relative(path, relative_to \\ File.cwd!()) do
|
||||
path = IO.chardata_to_string(path)
|
||||
|
||||
case :filelib.safe_relative_path(path, cwd) do
|
||||
case :filelib.safe_relative_path(path, relative_to) do
|
||||
:unsafe -> :error
|
||||
relative_path -> {:ok, IO.chardata_to_string(relative_path)}
|
||||
end
|
||||
|
||||
@@ -88,6 +88,9 @@ defmodule Process do
|
||||
`false`.
|
||||
|
||||
`pid` must refer to a process running on the local node or `ArgumentError` is raised.
|
||||
To check whether a process on any node is alive you can use the [`:erpc`](`:erpc`) module.
|
||||
|
||||
:erpc.call(node(pid), Process, :alive?, [pid])
|
||||
|
||||
Inlined by the compiler.
|
||||
"""
|
||||
@@ -233,6 +236,12 @@ defmodule Process do
|
||||
the current process will sleep forever, and not
|
||||
consume or reply to messages.
|
||||
|
||||
> #### Sleeping limit {: .info }
|
||||
>
|
||||
> Before Elixir v1.18, `sleep/1` did not accept integer timeout values greater
|
||||
> than `16#ffffffff`, that is, `2^32-1`. Since Elixir v1.18, arbitrarily-high integer
|
||||
> values are accepted.
|
||||
|
||||
**Use this function with extreme care**. For almost all situations
|
||||
where you would use `sleep/1` in Elixir, there is likely a
|
||||
more correct, faster and precise way of achieving the same with
|
||||
@@ -299,7 +308,15 @@ defmodule Process do
|
||||
end
|
||||
|
||||
"""
|
||||
|
||||
# Max value for a receive's after clause
|
||||
@max_receive_after 0xFFFFFFFF
|
||||
|
||||
@spec sleep(timeout) :: :ok
|
||||
def sleep(timeout) when is_integer(timeout) and timeout > @max_receive_after do
|
||||
receive after: (@max_receive_after -> sleep(timeout - @max_receive_after))
|
||||
end
|
||||
|
||||
def sleep(timeout)
|
||||
when is_integer(timeout) and timeout >= 0
|
||||
when timeout == :infinity do
|
||||
@@ -552,10 +569,15 @@ defmodule Process do
|
||||
|
||||
send(pid, {:ping, ref_and_alias})
|
||||
|
||||
receive do: msg -> msg
|
||||
receive do: (msg -> msg)
|
||||
#=> :pong
|
||||
|
||||
receive do: msg -> msg
|
||||
ref_and_alias = Process.monitor(pid, alias: :reply_demonitor)
|
||||
#=> #Reference<0.906660723.3006791681.40191>
|
||||
|
||||
send(pid, {:ping, ref_and_alias})
|
||||
|
||||
receive do: (msg -> msg)
|
||||
#=> {:DOWN, #Reference<0.906660723.3006791681.40191>, :process, #PID<0.118.0>, :noproc}
|
||||
|
||||
"""
|
||||
|
||||
+114
-125
@@ -6,7 +6,7 @@ defmodule Protocol do
|
||||
implementations. A protocol is defined with `Kernel.defprotocol/2`
|
||||
and its implementations with `Kernel.defimpl/3`.
|
||||
|
||||
## A real case
|
||||
## Example
|
||||
|
||||
In Elixir, we have two nouns for checking how many items there
|
||||
are in a data structure: `length` and `size`. `length` means the
|
||||
@@ -102,8 +102,7 @@ defmodule Protocol do
|
||||
invoking the protocol will raise unless it is configured to
|
||||
fall back to `Any`. Conveniences for building implementations
|
||||
on top of existing ones are also available, look at `defstruct/1`
|
||||
for more information about deriving
|
||||
protocols.
|
||||
for more information about deriving protocols.
|
||||
|
||||
## Fallback to `Any`
|
||||
|
||||
@@ -165,56 +164,16 @@ defmodule Protocol do
|
||||
The `@spec` above expresses that all types allowed to implement the
|
||||
given protocol are valid argument types for the given function.
|
||||
|
||||
## Reflection
|
||||
## Configuration
|
||||
|
||||
Any protocol module contains three extra functions:
|
||||
The following module attributes are available to configure a protocol:
|
||||
|
||||
* `__protocol__/1` - returns the protocol information. The function takes
|
||||
one of the following atoms:
|
||||
* `@fallback_to_any` - when true, enables protocol dispatch to
|
||||
fallback to any
|
||||
|
||||
* `:consolidated?` - returns whether the protocol is consolidated
|
||||
|
||||
* `:functions` - returns a keyword list of protocol functions and their arities
|
||||
|
||||
* `:impls` - if consolidated, returns `{:consolidated, modules}` with the list of modules
|
||||
implementing the protocol, otherwise `:not_consolidated`
|
||||
|
||||
* `:module` - the protocol module atom name
|
||||
|
||||
* `impl_for/1` - returns the module that implements the protocol for the given argument,
|
||||
`nil` otherwise
|
||||
|
||||
* `impl_for!/1` - same as above but raises `Protocol.UndefinedError` if an implementation is
|
||||
not found
|
||||
|
||||
For example, for the `Enumerable` protocol we have:
|
||||
|
||||
iex> Enumerable.__protocol__(:functions)
|
||||
[count: 1, member?: 2, reduce: 3, slice: 1]
|
||||
|
||||
iex> Enumerable.impl_for([])
|
||||
Enumerable.List
|
||||
|
||||
iex> Enumerable.impl_for(42)
|
||||
nil
|
||||
|
||||
In addition, every protocol implementation module contains the `__impl__/1`
|
||||
function. The function takes one of the following atoms:
|
||||
|
||||
* `:for` - returns the module responsible for the data structure of the
|
||||
protocol implementation
|
||||
|
||||
* `:protocol` - returns the protocol module for which this implementation
|
||||
is provided
|
||||
|
||||
For example, the module implementing the `Enumerable` protocol for lists is
|
||||
`Enumerable.List`. Therefore, we can invoke `__impl__/1` on this module:
|
||||
|
||||
iex(1)> Enumerable.List.__impl__(:for)
|
||||
List
|
||||
|
||||
iex(2)> Enumerable.List.__impl__(:protocol)
|
||||
Enumerable
|
||||
* `@undefined_impl_description` - a string with additional description
|
||||
to be used on `Protocol.UndefinedError` when looking up the implementation
|
||||
fails. This option is only applied if `@fallback_to_any` is not set to true
|
||||
|
||||
## Consolidation
|
||||
|
||||
@@ -222,7 +181,8 @@ defmodule Protocol do
|
||||
are known up-front, typically after all Elixir code in a project is compiled,
|
||||
Elixir provides a feature called *protocol consolidation*. Consolidation directly
|
||||
links protocols to their implementations in a way that invoking a function from a
|
||||
consolidated protocol is equivalent to invoking two remote functions.
|
||||
consolidated protocol is equivalent to invoking two remote functions - one to
|
||||
identify the correct implementation, and another to call the implementation.
|
||||
|
||||
Protocol consolidation is applied by default to all Mix projects during compilation.
|
||||
This may be an issue during test. For instance, if you want to implement a protocol
|
||||
@@ -268,6 +228,41 @@ defmodule Protocol do
|
||||
globally set.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
A function available in all protocol definitions that returns protocol metadata.
|
||||
"""
|
||||
@callback __protocol__(:consolidated?) :: boolean()
|
||||
@callback __protocol__(:functions) :: [{atom(), arity()}]
|
||||
@callback __protocol__(:impls) :: {:consolidated, [module()]} | :not_consolidated
|
||||
@callback __protocol__(:module) :: module()
|
||||
|
||||
@doc """
|
||||
A function available in all protocol definitions that returns the implementation
|
||||
for the given `term` or nil.
|
||||
|
||||
If `@fallback_to_any` is true, `nil` is never returned.
|
||||
"""
|
||||
@callback impl_for(term) :: module() | nil
|
||||
|
||||
@doc """
|
||||
A function available in all protocol definitions that returns the implementation
|
||||
for the given `term` or raises.
|
||||
|
||||
If `@fallback_to_any` is true, it never raises.
|
||||
"""
|
||||
@callback impl_for!(term) :: module()
|
||||
|
||||
@doc """
|
||||
An optional callback to be implemented by protocol authors for custom deriving.
|
||||
|
||||
It must return a quoted expression that implements the protocol for the given module.
|
||||
|
||||
See `Protocol.derive/3` for an example.
|
||||
"""
|
||||
@macrocallback __deriving__(module(), term()) :: Macro.t()
|
||||
|
||||
@optional_callbacks __deriving__: 2
|
||||
|
||||
@doc false
|
||||
defmacro def(signature)
|
||||
|
||||
@@ -380,52 +375,50 @@ defmodule Protocol do
|
||||
@doc """
|
||||
Derives the `protocol` for `module` with the given options.
|
||||
|
||||
If your implementation passes options or if you are generating
|
||||
custom code based on the struct, you will also need to implement
|
||||
a macro defined as `__deriving__(module, struct, options)`
|
||||
to get the options that were passed.
|
||||
Every time you derive a protocol, Elixir will verify if the protocol
|
||||
has implemented the `c:Protocol.__deriving__/2` callback. If so,
|
||||
the callback will be invoked and it should define the implementation
|
||||
module. Otherwise an implementation that simply points to the `Any`
|
||||
implementation is automatically derived.
|
||||
|
||||
## Examples
|
||||
|
||||
defprotocol Derivable do
|
||||
def ok(arg)
|
||||
end
|
||||
@impl true
|
||||
defmacro __deriving__(module, options) do
|
||||
# If you need to load struct metadata, you may call:
|
||||
# struct_info = Macro.struct_info!(module, __CALLER__)
|
||||
|
||||
defimpl Derivable, for: Any do
|
||||
defmacro __deriving__(module, struct, options) do
|
||||
quote do
|
||||
defimpl Derivable, for: unquote(module) do
|
||||
def ok(arg) do
|
||||
{:ok, arg, unquote(Macro.escape(struct)), unquote(options)}
|
||||
{:ok, arg, unquote(options)}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def ok(arg) do
|
||||
{:ok, arg}
|
||||
end
|
||||
def ok(arg)
|
||||
end
|
||||
|
||||
Once the protocol is defined, there are two ways it can be
|
||||
derived. The first is by using the `@derive` module attribute
|
||||
by the time you define the struct:
|
||||
|
||||
defmodule ImplStruct do
|
||||
@derive [Derivable]
|
||||
defstruct a: 0, b: 0
|
||||
end
|
||||
|
||||
Derivable.ok(%ImplStruct{})
|
||||
#=> {:ok, %ImplStruct{a: 0, b: 0}, %ImplStruct{a: 0, b: 0}, []}
|
||||
#=> {:ok, %ImplStruct{a: 0, b: 0}, []}
|
||||
|
||||
Explicit derivations can now be called via `__deriving__/3`:
|
||||
If the struct has already been defined, you can call this macro:
|
||||
|
||||
# Explicitly derived via `__deriving__/3`
|
||||
Derivable.ok(%ImplStruct{a: 1, b: 1})
|
||||
#=> {:ok, %ImplStruct{a: 1, b: 1}, %ImplStruct{a: 0, b: 0}, []}
|
||||
|
||||
# Explicitly derived by API via `__deriving__/3`
|
||||
require Protocol
|
||||
Protocol.derive(Derivable, ImplStruct, :oops)
|
||||
Derivable.ok(%ImplStruct{a: 1, b: 1})
|
||||
#=> {:ok, %ImplStruct{a: 1, b: 1}, %ImplStruct{a: 0, b: 0}, :oops}
|
||||
#=> {:ok, %ImplStruct{a: 1, b: 1}, :oops}
|
||||
|
||||
"""
|
||||
defmacro derive(protocol, module, options \\ []) do
|
||||
@@ -497,7 +490,8 @@ defmodule Protocol do
|
||||
|
||||
defp extract_matching_by_attribute(paths, prefix, callback) do
|
||||
for path <- paths,
|
||||
path = to_charlist(path),
|
||||
# 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),
|
||||
mod = extract_from_file(path, file, prefix, callback),
|
||||
do: mod
|
||||
@@ -699,25 +693,14 @@ defmodule Protocol do
|
||||
def __protocol__(name, do: block) do
|
||||
quote do
|
||||
defmodule unquote(name) do
|
||||
@behaviour Protocol
|
||||
@before_compile Protocol
|
||||
|
||||
# We don't allow function definition inside protocols
|
||||
import Kernel,
|
||||
except: [
|
||||
def: 1,
|
||||
def: 2,
|
||||
defp: 1,
|
||||
defp: 2,
|
||||
defdelegate: 2,
|
||||
defguard: 1,
|
||||
defguardp: 1,
|
||||
defmacro: 1,
|
||||
defmacro: 2,
|
||||
defmacrop: 1,
|
||||
defmacrop: 2
|
||||
]
|
||||
except: [def: 1, def: 2, defdelegate: 2, defguard: 1, defguardp: 1]
|
||||
|
||||
# Import the new dsl that holds the new def
|
||||
# Import the new `def` that is used by protocols
|
||||
import Protocol, only: [def: 1]
|
||||
|
||||
# Compile with debug info for consolidation
|
||||
@@ -736,19 +719,17 @@ defmodule Protocol do
|
||||
end
|
||||
end
|
||||
|
||||
defp callback_ast_to_fa({kind, {:"::", meta, [{name, _, args}, _return]}, _pos})
|
||||
when kind in [:callback, :macrocallback] do
|
||||
defp callback_ast_to_fa({_kind, {:"::", meta, [{name, _, args}, _return]}, _pos}) do
|
||||
[{{name, length(List.wrap(args))}, meta}]
|
||||
end
|
||||
|
||||
defp callback_ast_to_fa(
|
||||
{kind, {:when, _, [{:"::", meta, [{name, _, args}, _return]}, _vars]}, _pos}
|
||||
)
|
||||
when kind in [:callback, :macrocallback] do
|
||||
{_kind, {:when, _, [{:"::", meta, [{name, _, args}, _return]}, _vars]}, _pos}
|
||||
) do
|
||||
[{{name, length(List.wrap(args))}, meta}]
|
||||
end
|
||||
|
||||
defp callback_ast_to_fa({kind, _, _pos}) when kind in [:callback, :macrocallback] do
|
||||
defp callback_ast_to_fa({_kind, _clause, _pos}) do
|
||||
[]
|
||||
end
|
||||
|
||||
@@ -766,13 +747,10 @@ defmodule Protocol do
|
||||
end
|
||||
|
||||
defp warn(message, env, line) when is_integer(line) do
|
||||
stacktrace = :maps.update(:line, line, env) |> Macro.Env.stacktrace()
|
||||
IO.warn(message, stacktrace)
|
||||
IO.warn(message, %{env | line: line})
|
||||
end
|
||||
|
||||
def __before_compile__(env) do
|
||||
callback_metas = callback_metas(env.module, :callback)
|
||||
callbacks = :maps.keys(callback_metas)
|
||||
functions = Module.get_attribute(env.module, :__functions__)
|
||||
|
||||
if functions == [] do
|
||||
@@ -783,8 +761,11 @@ defmodule Protocol do
|
||||
)
|
||||
end
|
||||
|
||||
callback_metas = callback_metas(env.module, :callback)
|
||||
callbacks = :maps.keys(callback_metas)
|
||||
|
||||
# TODO: Convert the following warnings into errors in future Elixir versions
|
||||
:lists.map(
|
||||
:lists.foreach(
|
||||
fn {name, arity} = fa ->
|
||||
warn(
|
||||
"cannot define @callback #{name}/#{arity} inside protocol, use def/1 to outline your protocol definition",
|
||||
@@ -799,7 +780,7 @@ defmodule Protocol do
|
||||
macrocallback_metas = callback_metas(env.module, :macrocallback)
|
||||
macrocallbacks = :maps.keys(macrocallback_metas)
|
||||
|
||||
:lists.map(
|
||||
:lists.foreach(
|
||||
fn {name, arity} = fa ->
|
||||
warn(
|
||||
"cannot define @macrocallback #{name}/#{arity} inside protocol, use def/1 to outline your protocol definition",
|
||||
@@ -826,7 +807,7 @@ defmodule Protocol do
|
||||
quote bind_quoted: [built_in: __built_in__()] do
|
||||
any_impl_for =
|
||||
if @fallback_to_any do
|
||||
quote do: unquote(__MODULE__.Any)
|
||||
__MODULE__.Any
|
||||
else
|
||||
nil
|
||||
end
|
||||
@@ -872,6 +853,9 @@ defmodule Protocol do
|
||||
unquote(any_impl_for)
|
||||
end
|
||||
|
||||
undefined_impl_description =
|
||||
Module.get_attribute(__MODULE__, :undefined_impl_description, "")
|
||||
|
||||
@doc false
|
||||
@spec impl_for!(term) :: atom
|
||||
if any_impl_for do
|
||||
@@ -880,23 +864,27 @@ defmodule Protocol do
|
||||
end
|
||||
else
|
||||
Kernel.def impl_for!(data) do
|
||||
impl_for(data) || raise(Protocol.UndefinedError, protocol: __MODULE__, value: data)
|
||||
impl_for(data) ||
|
||||
raise(Protocol.UndefinedError,
|
||||
protocol: __MODULE__,
|
||||
value: data,
|
||||
description: unquote(undefined_impl_description)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
# Internal handler for Structs
|
||||
Kernel.defp struct_impl_for(struct) do
|
||||
target =
|
||||
case Code.ensure_compiled(Module.concat(__MODULE__, struct)) do
|
||||
{:module, module} -> module
|
||||
{:error, _} -> unquote(any_impl_for)
|
||||
end
|
||||
case Code.ensure_compiled(Module.concat(__MODULE__, struct)) do
|
||||
{:module, module} -> module
|
||||
{:error, _} -> unquote(any_impl_for)
|
||||
end
|
||||
end
|
||||
|
||||
# Inline struct implementation for performance
|
||||
@compile {:inline, struct_impl_for: 1}
|
||||
|
||||
unless Module.defines_type?(__MODULE__, {:t, 0}) do
|
||||
if not Module.defines_type?(__MODULE__, {:t, 0}) do
|
||||
@typedoc """
|
||||
All the types that implement this protocol.
|
||||
"""
|
||||
@@ -910,9 +898,9 @@ defmodule Protocol do
|
||||
|
||||
@doc false
|
||||
@spec __protocol__(:module) :: __MODULE__
|
||||
@spec __protocol__(:functions) :: unquote(Protocol.__functions_spec__(@__functions__))
|
||||
@spec __protocol__(:consolidated?) :: boolean
|
||||
@spec __protocol__(:impls) :: :not_consolidated | {:consolidated, [module]}
|
||||
@spec __protocol__(:functions) :: [{atom(), arity()}]
|
||||
@spec __protocol__(:consolidated?) :: boolean()
|
||||
@spec __protocol__(:impls) :: :not_consolidated | {:consolidated, [module()]}
|
||||
Kernel.def(__protocol__(:module), do: __MODULE__)
|
||||
Kernel.def(__protocol__(:functions), do: unquote(:lists.sort(@__functions__)))
|
||||
Kernel.def(__protocol__(:consolidated?), do: false)
|
||||
@@ -920,12 +908,6 @@ defmodule Protocol do
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def __functions_spec__([]), do: []
|
||||
|
||||
def __functions_spec__([head | tail]),
|
||||
do: [:lists.foldl(&{:|, [], [&1, &2]}, head, tail), quote(do: ...)]
|
||||
|
||||
@doc false
|
||||
def __impl__(protocol, opts, do_block, env) do
|
||||
opts = Keyword.merge(opts, do_block)
|
||||
@@ -989,14 +971,12 @@ defmodule Protocol do
|
||||
|
||||
@doc false
|
||||
def __derive__(derives, for, %Macro.Env{} = env) when is_atom(for) do
|
||||
struct = Macro.struct!(for, env)
|
||||
|
||||
foreach = fn
|
||||
proto when is_atom(proto) ->
|
||||
derive(proto, for, struct, [], env)
|
||||
derive(proto, for, [], env)
|
||||
|
||||
{proto, opts} when is_atom(proto) ->
|
||||
derive(proto, for, struct, opts, env)
|
||||
derive(proto, for, opts, env)
|
||||
end
|
||||
|
||||
:lists.foreach(foreach, :lists.flatten(derives))
|
||||
@@ -1004,21 +984,30 @@ defmodule Protocol do
|
||||
:ok
|
||||
end
|
||||
|
||||
defp derive(protocol, for, struct, opts, env) do
|
||||
defp derive(protocol, for, opts, env) do
|
||||
extra = ", cannot derive #{inspect(protocol)} for #{inspect(for)}"
|
||||
assert_protocol!(protocol, extra)
|
||||
__ensure_defimpl__(protocol, for, env)
|
||||
assert_impl!(protocol, Any, extra)
|
||||
|
||||
{mod, args} =
|
||||
if macro_exported?(protocol, :__deriving__, 2) do
|
||||
{protocol, [for, opts]}
|
||||
else
|
||||
# TODO: Deprecate this on Elixir v1.22+
|
||||
assert_impl!(protocol, Any, extra)
|
||||
{Module.concat(protocol, Any), [for, Macro.struct!(for, env), opts]}
|
||||
end
|
||||
|
||||
# Clean up variables from eval context
|
||||
env = :elixir_env.reset_vars(env)
|
||||
args = [for, struct, opts]
|
||||
impl = Module.concat(protocol, Any)
|
||||
|
||||
:elixir_module.expand_callback(env.line, impl, :__deriving__, args, env, fn mod, fun, args ->
|
||||
:elixir_module.expand_callback(env.line, mod, :__deriving__, args, env, fn mod, fun, args ->
|
||||
if function_exported?(mod, fun, length(args)) do
|
||||
apply(mod, fun, args)
|
||||
else
|
||||
__ensure_defimpl__(protocol, for, env)
|
||||
assert_impl!(protocol, Any, extra)
|
||||
impl = Module.concat(protocol, Any)
|
||||
|
||||
funs =
|
||||
for {fun, arity} <- protocol.__protocol__(:functions) do
|
||||
args = Macro.generate_arguments(arity, nil)
|
||||
|
||||
+16
-2
@@ -184,8 +184,22 @@ defmodule Range do
|
||||
|
||||
@spec new(limit, limit) :: t
|
||||
def new(first, last) when is_integer(first) and is_integer(last) do
|
||||
# TODO: Deprecate inferring a range with a step of -1 on Elixir v1.18
|
||||
step = if first <= last, do: 1, else: -1
|
||||
step =
|
||||
if first <= last do
|
||||
1
|
||||
else
|
||||
# TODO: Remove me on v2.0
|
||||
IO.warn_once(
|
||||
{__MODULE__, :new},
|
||||
fn ->
|
||||
"Range.new/2 and first..last default to a step of -1 when last < first. Use Range.new(first, last, -1) or first..last//-1, or pass 1 if that was your intention"
|
||||
end,
|
||||
3
|
||||
)
|
||||
|
||||
-1
|
||||
end
|
||||
|
||||
%Range{first: first, last: last, step: step}
|
||||
end
|
||||
|
||||
|
||||
@@ -36,10 +36,9 @@ defmodule Record do
|
||||
|
||||
## Reflection
|
||||
|
||||
A list of all records in a module, if any, can be retrieved by reading the
|
||||
`@__records__` module attribute. It returns a list of maps with the record
|
||||
kind, name, tag, and fields. The attribute is only available inside the
|
||||
module definition.
|
||||
The record tag and its fields are stored as metadata in the "Docs" chunk
|
||||
of the record definition macro. You can retrieve the documentation for
|
||||
a module by calling `Code.fetch_docs/1`.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
@@ -253,6 +252,7 @@ defmodule Record do
|
||||
tag = tag || name
|
||||
fields = Record.__record__(__MODULE__, :defrecord, name, tag, kv)
|
||||
|
||||
@doc record: {tag, fields}
|
||||
defmacro unquote(name)(args \\ []) do
|
||||
Record.__access__(unquote(tag), unquote(fields), args, __CALLER__)
|
||||
end
|
||||
@@ -312,6 +312,8 @@ defmodule Record do
|
||||
error_on_duplicate_record(module, name)
|
||||
|
||||
fields = fields(kind, kv)
|
||||
|
||||
# TODO: Remove me on Elixir v2.0
|
||||
Module.register_attribute(module, :__records__, accumulate: true)
|
||||
|
||||
Module.put_attribute(module, :__records__, %{
|
||||
|
||||
+17
-42
@@ -186,27 +186,7 @@ defmodule Regex do
|
||||
|
||||
defstruct re_pattern: nil, source: "", opts: [], re_version: ""
|
||||
|
||||
@type re_option ::
|
||||
:unicode
|
||||
| :caseless
|
||||
| :dotall
|
||||
| :multiline
|
||||
| :extended
|
||||
| :firstline
|
||||
| :ungreedy
|
||||
| :anchored
|
||||
| :dollar_endonly
|
||||
| :no_auto_capture
|
||||
| :newline
|
||||
|
||||
@type t :: %__MODULE__{re_pattern: term, source: binary, opts: binary | [re_option()]}
|
||||
|
||||
@type capture_option ::
|
||||
:all | :first | :all_but_first | :none | :all_names | [binary() | atom()]
|
||||
@type run_option ::
|
||||
{:return, :binary | :index}
|
||||
| {:capture, capture_option()}
|
||||
| {:offset, non_neg_integer()}
|
||||
@type t :: %__MODULE__{re_pattern: term, source: binary, opts: binary | [term]}
|
||||
|
||||
defmodule CompileError do
|
||||
@moduledoc """
|
||||
@@ -242,7 +222,7 @@ defmodule Regex do
|
||||
{:ok, Regex.compile!("foo", [:caseless])}
|
||||
|
||||
"""
|
||||
@spec compile(binary, binary | [re_option()]) :: {:ok, t} | {:error, any}
|
||||
@spec compile(binary, binary | [term]) :: {:ok, t} | {:error, term}
|
||||
def compile(source, opts \\ "") when is_binary(source) do
|
||||
compile(source, opts, version())
|
||||
end
|
||||
@@ -270,7 +250,7 @@ defmodule Regex do
|
||||
@doc """
|
||||
Compiles the regular expression and raises `Regex.CompileError` in case of errors.
|
||||
"""
|
||||
@spec compile!(binary, binary | [re_option()]) :: t
|
||||
@spec compile!(binary, binary | [term]) :: t
|
||||
def compile!(source, options \\ "") when is_binary(source) do
|
||||
case compile(source, options) do
|
||||
{:ok, regex} -> regex
|
||||
@@ -377,7 +357,7 @@ defmodule Regex do
|
||||
["d", ""]
|
||||
|
||||
"""
|
||||
@spec run(t, binary, [run_option()]) :: nil | [binary] | [{integer, integer}]
|
||||
@spec run(t, binary, [term]) :: nil | [binary] | [{integer, integer}]
|
||||
def run(regex, string, options \\ [])
|
||||
|
||||
def run(%Regex{} = regex, string, options) when is_binary(string) do
|
||||
@@ -412,7 +392,7 @@ defmodule Regex do
|
||||
nil
|
||||
|
||||
"""
|
||||
@spec named_captures(t, String.t(), [{:return, :binary | :index}]) :: map | nil
|
||||
@spec named_captures(t, String.t(), [term]) :: map | nil
|
||||
def named_captures(regex, string, options \\ []) when is_binary(string) do
|
||||
names = names(regex)
|
||||
options = Keyword.put(options, :capture, names)
|
||||
@@ -456,7 +436,7 @@ defmodule Regex do
|
||||
[:caseless]
|
||||
|
||||
"""
|
||||
@spec opts(t) :: [re_option()]
|
||||
@spec opts(t) :: [term]
|
||||
def opts(%Regex{opts: opts}) do
|
||||
opts
|
||||
end
|
||||
@@ -528,7 +508,7 @@ defmodule Regex do
|
||||
[["cd"], ["ce"]]
|
||||
|
||||
"""
|
||||
@spec scan(t(), String.t(), [run_option()]) :: [[String.t()]] | [[{integer(), integer()}]]
|
||||
@spec scan(t(), String.t(), [term()]) :: [[String.t()]] | [[{integer(), integer()}]]
|
||||
def scan(regex, string, options \\ [])
|
||||
|
||||
def scan(%Regex{} = regex, string, options) when is_binary(string) do
|
||||
@@ -587,40 +567,35 @@ defmodule Regex do
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Regex.split(~r{-}, "a-b-c")
|
||||
iex> Regex.split(~r/-/, "a-b-c")
|
||||
["a", "b", "c"]
|
||||
|
||||
iex> Regex.split(~r{-}, "a-b-c", parts: 2)
|
||||
iex> Regex.split(~r/-/, "a-b-c", parts: 2)
|
||||
["a", "b-c"]
|
||||
|
||||
iex> Regex.split(~r{-}, "abc")
|
||||
iex> Regex.split(~r/-/, "abc")
|
||||
["abc"]
|
||||
|
||||
iex> Regex.split(~r{}, "abc")
|
||||
iex> Regex.split(~r//, "abc")
|
||||
["", "a", "b", "c", ""]
|
||||
|
||||
iex> Regex.split(~r{a(?<second>b)c}, "abc")
|
||||
iex> Regex.split(~r/a(?<second>b)c/, "abc")
|
||||
["", ""]
|
||||
|
||||
iex> Regex.split(~r{a(?<second>b)c}, "abc", on: [:second])
|
||||
iex> Regex.split(~r/a(?<second>b)c/, "abc", on: [:second])
|
||||
["a", "c"]
|
||||
|
||||
iex> Regex.split(~r{(x)}, "Elixir", include_captures: true)
|
||||
iex> Regex.split(~r/(x)/, "Elixir", include_captures: true)
|
||||
["Eli", "x", "ir"]
|
||||
|
||||
iex> Regex.split(~r{a(?<second>b)c}, "abc", on: [:second], include_captures: true)
|
||||
iex> Regex.split(~r/a(?<second>b)c/, "abc", on: [:second], include_captures: true)
|
||||
["a", "b", "c"]
|
||||
|
||||
iex> Regex.split(~r{-}, "-a-b--c", trim: true)
|
||||
iex> Regex.split(~r/-/, "-a-b--c", trim: true)
|
||||
["a", "b", "c"]
|
||||
|
||||
"""
|
||||
@spec split(t, String.t(),
|
||||
parts: pos_integer() | :infinity,
|
||||
trim: boolean(),
|
||||
on: capture_option(),
|
||||
include_captures: boolean()
|
||||
) :: [String.t()]
|
||||
@spec split(t, String.t(), [term]) :: [String.t()]
|
||||
def split(regex, string, options \\ [])
|
||||
|
||||
def split(%Regex{}, "", opts) do
|
||||
|
||||
+162
-38
@@ -331,7 +331,7 @@ defmodule Registry do
|
||||
def start_link(options) do
|
||||
keys = Keyword.get(options, :keys)
|
||||
|
||||
unless keys in @keys do
|
||||
if keys not in @keys do
|
||||
raise ArgumentError,
|
||||
"expected :keys to be given and be one of :unique or :duplicate, got: #{inspect(keys)}"
|
||||
end
|
||||
@@ -350,27 +350,27 @@ defmodule Registry do
|
||||
|
||||
meta = Keyword.get(options, :meta, [])
|
||||
|
||||
unless Keyword.keyword?(meta) do
|
||||
if not Keyword.keyword?(meta) do
|
||||
raise ArgumentError, "expected :meta to be a keyword list, got: #{inspect(meta)}"
|
||||
end
|
||||
|
||||
partitions = Keyword.get(options, :partitions, 1)
|
||||
|
||||
unless is_integer(partitions) and partitions >= 1 do
|
||||
if not (is_integer(partitions) and partitions >= 1) do
|
||||
raise ArgumentError,
|
||||
"expected :partitions to be a positive integer, got: #{inspect(partitions)}"
|
||||
end
|
||||
|
||||
listeners = Keyword.get(options, :listeners, [])
|
||||
|
||||
unless is_list(listeners) and Enum.all?(listeners, &is_atom/1) do
|
||||
if not (is_list(listeners) and Enum.all?(listeners, &is_atom/1)) do
|
||||
raise ArgumentError,
|
||||
"expected :listeners to be a list of named processes, got: #{inspect(listeners)}"
|
||||
end
|
||||
|
||||
compressed = Keyword.get(options, :compressed, false)
|
||||
|
||||
unless is_boolean(compressed) do
|
||||
if not is_boolean(compressed) do
|
||||
raise ArgumentError,
|
||||
"expected :compressed to be a boolean, got: #{inspect(compressed)}"
|
||||
end
|
||||
@@ -471,7 +471,7 @@ defmodule Registry do
|
||||
"""
|
||||
@doc since: "1.4.0"
|
||||
@spec dispatch(registry, key, dispatcher, keyword) :: :ok
|
||||
when dispatcher: (entries :: [{pid, value}] -> term) | {module(), atom(), [any()]}
|
||||
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)
|
||||
when is_atom(registry) and tuple_size(mfa_or_fun) == 3 do
|
||||
@@ -609,6 +609,65 @@ defmodule Registry do
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Out-of-band locking of the given `lock_key` for the duration of `function`.
|
||||
|
||||
Only one function can execute under the same `lock_key` at a given
|
||||
time. The given function always runs in the caller process.
|
||||
|
||||
The `lock_key` has its own namespace and therefore does not clash or
|
||||
overlap with the regular registry keys. In other words, locking works
|
||||
out-of-band from the regular Registry operations. See the "Use cases"
|
||||
section below.
|
||||
|
||||
Locking behaves the same regardless of the registry type.
|
||||
|
||||
## Use cases
|
||||
|
||||
The Registry is safe and concurrent out-of-the-box. You are not required
|
||||
to use this function when interacting with the Registry. Furthermore,
|
||||
`Registry` with `:unique` keys can already act as a process-lock for any
|
||||
given key. For example, you can ensure only one process runs at a given
|
||||
time for a given `:key` by doing:
|
||||
|
||||
name = {:via, Registry, {MyApp.Registry, :key, :value}}
|
||||
|
||||
# Do not attempt to start if we are already running
|
||||
if pid = GenServer.whereis(name) do
|
||||
pid
|
||||
else
|
||||
case GenServer.start_link(__MODULE__, :ok, name: name) do
|
||||
{:ok, pid} -> pid
|
||||
{:error, {:already_started, pid}} -> pid
|
||||
end
|
||||
end
|
||||
|
||||
Process locking gives you plenty of flexibility and fault isolation and
|
||||
is enough for most cases.
|
||||
|
||||
This function is useful only when spawning processes is not an option,
|
||||
for example, when copying the data to another process could be too
|
||||
expensive. Or when the work must be done within the current process
|
||||
for other reasons. In such cases, this function provides a scalable
|
||||
mechanism for managing locks on top of the registry's infrastructure.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Registry.start_link(keys: :unique, name: Registry.LockTest)
|
||||
iex> Registry.lock(Registry.LockTest, :hello, fn -> :ok end)
|
||||
:ok
|
||||
iex> Registry.lock(Registry.LockTest, :world, fn -> self() end)
|
||||
self()
|
||||
|
||||
"""
|
||||
@doc since: "1.18.0"
|
||||
def lock(registry, lock_key, function)
|
||||
when is_atom(registry) and is_function(function, 0) do
|
||||
{_kind, partitions, _, pid_ets, _} = info!(registry)
|
||||
{pid_server, _pid_ets} = pid_ets || pid_ets!(registry, lock_key, partitions)
|
||||
Registry.Partition.lock(pid_server, lock_key, function)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns `{pid, value}` pairs under the given `key` in `registry` that match `pattern`.
|
||||
|
||||
@@ -757,28 +816,28 @@ defmodule Registry do
|
||||
In the example below we register the current process and look it up
|
||||
both from itself and other processes:
|
||||
|
||||
iex> Registry.start_link(keys: :unique, name: Registry.UniqueLookupTest)
|
||||
iex> Registry.values(Registry.UniqueLookupTest, "hello", self())
|
||||
iex> Registry.start_link(keys: :unique, name: Registry.UniqueValuesTest)
|
||||
iex> Registry.values(Registry.UniqueValuesTest, "hello", self())
|
||||
[]
|
||||
iex> {:ok, _} = Registry.register(Registry.UniqueLookupTest, "hello", :world)
|
||||
iex> Registry.values(Registry.UniqueLookupTest, "hello", self())
|
||||
iex> {:ok, _} = Registry.register(Registry.UniqueValuesTest, "hello", :world)
|
||||
iex> Registry.values(Registry.UniqueValuesTest, "hello", self())
|
||||
[:world]
|
||||
iex> Task.async(fn -> Registry.values(Registry.UniqueLookupTest, "hello", self()) end) |> Task.await()
|
||||
iex> Task.async(fn -> Registry.values(Registry.UniqueValuesTest, "hello", self()) end) |> Task.await()
|
||||
[]
|
||||
iex> parent = self()
|
||||
iex> Task.async(fn -> Registry.values(Registry.UniqueLookupTest, "hello", parent) end) |> Task.await()
|
||||
iex> Task.async(fn -> Registry.values(Registry.UniqueValuesTest, "hello", parent) end) |> Task.await()
|
||||
[:world]
|
||||
|
||||
The same applies to duplicate registries:
|
||||
|
||||
iex> Registry.start_link(keys: :duplicate, name: Registry.DuplicateLookupTest)
|
||||
iex> Registry.values(Registry.DuplicateLookupTest, "hello", self())
|
||||
iex> Registry.start_link(keys: :duplicate, name: Registry.DuplicateValuesTest)
|
||||
iex> Registry.values(Registry.DuplicateValuesTest, "hello", self())
|
||||
[]
|
||||
iex> {:ok, _} = Registry.register(Registry.DuplicateLookupTest, "hello", :world)
|
||||
iex> Registry.values(Registry.DuplicateLookupTest, "hello", self())
|
||||
iex> {:ok, _} = Registry.register(Registry.DuplicateValuesTest, "hello", :world)
|
||||
iex> Registry.values(Registry.DuplicateValuesTest, "hello", self())
|
||||
[:world]
|
||||
iex> {:ok, _} = Registry.register(Registry.DuplicateLookupTest, "hello", :another)
|
||||
iex> Enum.sort(Registry.values(Registry.DuplicateLookupTest, "hello", self()))
|
||||
iex> {:ok, _} = Registry.register(Registry.DuplicateValuesTest, "hello", :another)
|
||||
iex> Enum.sort(Registry.values(Registry.DuplicateValuesTest, "hello", self()))
|
||||
[:another, :world]
|
||||
|
||||
"""
|
||||
@@ -1182,8 +1241,8 @@ defmodule Registry do
|
||||
def count(registry) when is_atom(registry) do
|
||||
case key_info!(registry) do
|
||||
{_kind, partitions, nil} ->
|
||||
Enum.reduce(0..(partitions - 1), 0, fn partition_index, acc ->
|
||||
acc + safe_size(key_ets!(registry, partition_index))
|
||||
Enum.sum_by(0..(partitions - 1), fn partition_index ->
|
||||
safe_size(key_ets!(registry, partition_index))
|
||||
end)
|
||||
|
||||
{_kind, 1, key_ets} ->
|
||||
@@ -1258,9 +1317,8 @@ defmodule Registry do
|
||||
:ets.select_count(key_ets, spec)
|
||||
|
||||
{:duplicate, partitions, _key_ets} ->
|
||||
Enum.reduce(0..(partitions - 1), 0, fn partition_index, acc ->
|
||||
count = :ets.select_count(key_ets!(registry, partition_index), spec)
|
||||
acc + count
|
||||
Enum.sum_by(0..(partitions - 1), fn partition_index ->
|
||||
:ets.select_count(key_ets!(registry, partition_index), spec)
|
||||
end)
|
||||
end
|
||||
end
|
||||
@@ -1304,12 +1362,12 @@ defmodule Registry do
|
||||
iex> Registry.select(Registry.SelectAllTest, [{{:"$1", :"$2", :"$3"}, [], [{{:"$1", :"$2", :"$3"}}]}]) |> Enum.sort()
|
||||
[{"hello", self(), :value}, {"world", self(), :value}]
|
||||
|
||||
Get all keys in the registry:
|
||||
If you want to get keys, you can pass a separate selector:
|
||||
|
||||
iex> Registry.start_link(keys: :unique, name: Registry.SelectAllTest)
|
||||
iex> {:ok, _} = Registry.register(Registry.SelectAllTest, "hello", :value)
|
||||
iex> {:ok, _} = Registry.register(Registry.SelectAllTest, "world", :value)
|
||||
iex> Registry.select(Registry.SelectAllTest, [{{:"$1", :_, :_}, [], [:"$1"]}]) |> Enum.sort()
|
||||
iex> Registry.start_link(keys: :unique, name: Registry.SelectKeysTest)
|
||||
iex> {:ok, _} = Registry.register(Registry.SelectKeysTest, "hello", :value)
|
||||
iex> {:ok, _} = Registry.register(Registry.SelectKeysTest, "world", :value)
|
||||
iex> Registry.select(Registry.SelectKeysTest, [{{:"$1", :_, :_}, [], [:"$1"]}]) |> Enum.sort()
|
||||
["hello", "world"]
|
||||
|
||||
"""
|
||||
@@ -1352,9 +1410,8 @@ defmodule Registry do
|
||||
|
||||
case key_info!(registry) do
|
||||
{_kind, partitions, nil} ->
|
||||
Enum.reduce(0..(partitions - 1), 0, fn partition_index, acc ->
|
||||
count = :ets.select_count(key_ets!(registry, partition_index), spec)
|
||||
acc + count
|
||||
Enum.sum_by(0..(partitions - 1), fn partition_index ->
|
||||
:ets.select_count(key_ets!(registry, partition_index), spec)
|
||||
end)
|
||||
|
||||
{_kind, 1, key_ets} ->
|
||||
@@ -1435,7 +1492,7 @@ defmodule Registry do
|
||||
end
|
||||
|
||||
defp unlink_if_unregistered(pid_server, pid_ets, self) do
|
||||
unless :ets.member(pid_ets, self) do
|
||||
if not :ets.member(pid_ets, self) do
|
||||
Process.unlink(pid_server)
|
||||
end
|
||||
end
|
||||
@@ -1502,7 +1559,7 @@ defmodule Registry.Partition do
|
||||
@moduledoc false
|
||||
|
||||
# This process owns the equivalent key and pid ETS tables
|
||||
# and is responsible for monitoring processes that map to
|
||||
# and is responsible for linking to processes that map to
|
||||
# its own pid table.
|
||||
use GenServer
|
||||
@all_info -1
|
||||
@@ -1527,13 +1584,25 @@ defmodule Registry.Partition do
|
||||
@doc """
|
||||
Starts the registry partition.
|
||||
|
||||
The process is only responsible for monitoring, demonitoring
|
||||
and cleaning up when monitored processes crash.
|
||||
The process is only responsible for linking and cleaning up when processes crash.
|
||||
"""
|
||||
def start_link(registry, arg) do
|
||||
GenServer.start_link(__MODULE__, arg, name: registry)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Runs function with a lock.
|
||||
"""
|
||||
def lock(pid, key, lock) do
|
||||
ref = GenServer.call(pid, {:lock, key})
|
||||
|
||||
try do
|
||||
lock.()
|
||||
after
|
||||
send(pid, {:unlock, key, ref})
|
||||
end
|
||||
end
|
||||
|
||||
## Callbacks
|
||||
|
||||
def init({kind, registry, i, partitions, key_partition, pid_partition, listeners, compressed}) do
|
||||
@@ -1554,7 +1623,7 @@ defmodule Registry.Partition do
|
||||
true = :ets.insert(registry, {i, key_ets, {self(), pid_ets}})
|
||||
end
|
||||
|
||||
{:ok, pid_ets}
|
||||
{:ok, {pid_ets, %{}}}
|
||||
end
|
||||
|
||||
# The key partition is a set for unique keys,
|
||||
@@ -1588,7 +1657,21 @@ defmodule Registry.Partition do
|
||||
{:reply, :ok, state}
|
||||
end
|
||||
|
||||
def handle_info({:EXIT, pid, _reason}, ets) do
|
||||
def handle_call({:lock, key}, from, {ets, lock}) do
|
||||
lock =
|
||||
case lock do
|
||||
%{^key => queue} ->
|
||||
Map.put(lock, key, :queue.in(from, queue))
|
||||
|
||||
%{} ->
|
||||
go(from, key)
|
||||
Map.put(lock, key, :queue.new())
|
||||
end
|
||||
|
||||
{:noreply, {ets, lock}}
|
||||
end
|
||||
|
||||
def handle_info({:EXIT, pid, _reason}, {ets, lock}) do
|
||||
entries = :ets.take(ets, pid)
|
||||
|
||||
for {_pid, key, key_ets, _counter} <- entries do
|
||||
@@ -1609,6 +1692,47 @@ defmodule Registry.Partition do
|
||||
end
|
||||
end
|
||||
|
||||
{:noreply, ets}
|
||||
{:noreply, {ets, lock}}
|
||||
end
|
||||
|
||||
def handle_info({{:unlock, key}, _ref, :process, _pid, _reason}, state) do
|
||||
unlock(key, state)
|
||||
end
|
||||
|
||||
def handle_info({:unlock, key, ref}, state) do
|
||||
Process.demonitor(ref, [:flush])
|
||||
unlock(key, state)
|
||||
end
|
||||
|
||||
defp unlock(key, {ets, lock}) do
|
||||
%{^key => queue} = lock
|
||||
|
||||
lock =
|
||||
case dequeue(queue, key) do
|
||||
:empty -> Map.delete(lock, key)
|
||||
{:not_empty, queue} -> Map.put(lock, key, queue)
|
||||
end
|
||||
|
||||
{:noreply, {ets, lock}}
|
||||
end
|
||||
|
||||
defp dequeue(queue, key) do
|
||||
case :queue.out(queue) do
|
||||
{:empty, _} ->
|
||||
:empty
|
||||
|
||||
{{:value, {pid, _tag} = from}, queue} ->
|
||||
if node(pid) != node() or Process.alive?(pid) do
|
||||
go(from, key)
|
||||
{:not_empty, queue}
|
||||
else
|
||||
dequeue(queue, key)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp go({pid, _tag} = from, key) do
|
||||
ref = Process.monitor(pid, tag: {:unlock, key})
|
||||
GenServer.reply(from, ref)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -90,7 +90,7 @@ defmodule Set do
|
||||
if target1 == target2 do
|
||||
target1.intersection(set1, set2)
|
||||
else
|
||||
Enumerable.reduce(set1, {:cont, target1.new}, fn v, acc ->
|
||||
Enumerable.reduce(set1, {:cont, target1.new()}, fn v, acc ->
|
||||
{:cont, if(target2.member?(set2, v), do: target1.put(acc, v), else: acc)}
|
||||
end)
|
||||
|> elem(1)
|
||||
|
||||
+27
-145
@@ -31,8 +31,11 @@ defmodule Stream do
|
||||
Due to their laziness, streams are useful when working with large
|
||||
(or even infinite) collections. When chaining many operations with `Enum`,
|
||||
intermediate lists are created, while `Stream` creates a recipe of
|
||||
computations that are executed at a later moment. Let's see another
|
||||
example:
|
||||
computations that are executed at a later moment. Then when the
|
||||
stream is consumed later on, most commonly by using a function in
|
||||
the `Enum` module, the stream will emit its elements one by one.
|
||||
|
||||
Let's see another example:
|
||||
|
||||
1..3
|
||||
|> Enum.map(&IO.inspect(&1))
|
||||
@@ -90,11 +93,21 @@ defmodule Stream do
|
||||
This module also provides many convenience functions for creating streams,
|
||||
like `Stream.cycle/1`, `Stream.unfold/2`, `Stream.resource/3` and more.
|
||||
|
||||
Note the functions in this module are guaranteed to return enumerables.
|
||||
Since enumerables can have different shapes (structs, anonymous functions,
|
||||
and so on), the functions in this module may return any of those shapes
|
||||
and this may change at any time. For example, a function that today
|
||||
returns an anonymous function may return a struct in future releases.
|
||||
> #### Do not check for `Stream` structs
|
||||
>
|
||||
> While some functions in this module may return the `Stream` struct,
|
||||
> you must never explicitly check for the `Stream` struct, as streams
|
||||
> may come in several shapes, such as `IO.Stream`, `File.Stream`, or
|
||||
> even `Range`s.
|
||||
>
|
||||
> The functions in this module only guarantee to return enumerables
|
||||
> and their implementation (structs, anonymous functions, etc) may
|
||||
> change at any time. For example, a function that returns an anonymous
|
||||
> function today may return a struct in future releases.
|
||||
>
|
||||
> Instead of checking for a particular type, you must instead write
|
||||
> assertive code that assumes you have an enumerable, using the functions
|
||||
> in the `Enum` or `Stream` module accordingly.
|
||||
"""
|
||||
|
||||
@doc false
|
||||
@@ -868,7 +881,7 @@ defmodule Stream do
|
||||
"""
|
||||
@spec transform(Enumerable.t(), acc, fun) :: Enumerable.t()
|
||||
when fun: (element, acc -> {Enumerable.t(), acc} | {:halt, acc}),
|
||||
acc: any
|
||||
acc: term
|
||||
def transform(enum, acc, reducer) when is_function(reducer, 2) do
|
||||
&do_transform(enum, fn -> acc end, reducer, &1, &2, nil, fn acc -> acc end)
|
||||
end
|
||||
@@ -883,7 +896,7 @@ defmodule Stream do
|
||||
when start_fun: (-> acc),
|
||||
reducer: (element, acc -> {Enumerable.t(), acc} | {:halt, acc}),
|
||||
after_fun: (acc -> term),
|
||||
acc: any
|
||||
acc: term
|
||||
def transform(enum, start_fun, reducer, after_fun)
|
||||
when is_function(start_fun, 0) and is_function(reducer, 2) and is_function(after_fun, 1) do
|
||||
&do_transform(enum, start_fun, reducer, &1, &2, nil, after_fun)
|
||||
@@ -910,7 +923,7 @@ defmodule Stream do
|
||||
reducer: (element, acc -> {Enumerable.t(), acc} | {:halt, acc}),
|
||||
last_fun: (acc -> {Enumerable.t(), acc} | {:halt, acc}),
|
||||
after_fun: (acc -> term),
|
||||
acc: any
|
||||
acc: term
|
||||
def transform(enum, start_fun, reducer, last_fun, after_fun)
|
||||
when is_function(start_fun, 0) and is_function(reducer, 2) and is_function(last_fun, 1) and
|
||||
is_function(after_fun, 1) do
|
||||
@@ -950,6 +963,8 @@ defmodule Stream do
|
||||
do_transform_user(:lists.reverse(vals), user_acc, :cont, next, inner_acc, funs)
|
||||
|
||||
{_, vals} ->
|
||||
# Do not attempt to call the resource again, it has either done or halted
|
||||
next = fn _ -> {:done, []} end
|
||||
do_transform_user(:lists.reverse(vals), user_acc, :last, next, inner_acc, funs)
|
||||
end
|
||||
end
|
||||
@@ -962,7 +977,6 @@ defmodule Stream do
|
||||
last_fun.(user_acc)
|
||||
catch
|
||||
kind, reason ->
|
||||
next.({:halt, []})
|
||||
after_fun.(user_acc)
|
||||
:erlang.raise(kind, reason, __STACKTRACE__)
|
||||
else
|
||||
@@ -1361,140 +1375,8 @@ defmodule Stream do
|
||||
@doc since: "1.12.0"
|
||||
@spec zip_with(enumerables, (Enumerable.t() -> term)) :: Enumerable.t()
|
||||
when enumerables: [Enumerable.t()] | Enumerable.t()
|
||||
def zip_with(enumerables, zip_fun) when is_function(zip_fun, 1) do
|
||||
if is_list(enumerables) and :lists.all(&is_list/1, enumerables) do
|
||||
&zip_list(enumerables, &1, &2, zip_fun)
|
||||
else
|
||||
&zip_enum(enumerables, &1, &2, zip_fun)
|
||||
end
|
||||
end
|
||||
|
||||
defp zip_list(_enumerables, {:halt, acc}, _fun, _zip_fun) do
|
||||
{:halted, acc}
|
||||
end
|
||||
|
||||
defp zip_list(enumerables, {:suspend, acc}, fun, zip_fun) do
|
||||
{:suspended, acc, &zip_list(enumerables, &1, fun, zip_fun)}
|
||||
end
|
||||
|
||||
defp zip_list([], {:cont, acc}, _fun, _zip_fun), do: {:done, acc}
|
||||
|
||||
defp zip_list(enumerables, {:cont, acc}, fun, zip_fun) do
|
||||
case zip_list_heads_tails(enumerables, [], []) do
|
||||
{heads, tails} -> zip_list(tails, fun.(zip_fun.(heads), acc), fun, zip_fun)
|
||||
:error -> {:done, acc}
|
||||
end
|
||||
end
|
||||
|
||||
defp zip_list_heads_tails([[head | tail] | rest], heads, tails) do
|
||||
zip_list_heads_tails(rest, [head | heads], [tail | tails])
|
||||
end
|
||||
|
||||
defp zip_list_heads_tails([[] | _rest], _heads, _tails) do
|
||||
:error
|
||||
end
|
||||
|
||||
defp zip_list_heads_tails([], heads, tails) do
|
||||
{:lists.reverse(heads), :lists.reverse(tails)}
|
||||
end
|
||||
|
||||
defp zip_enum(enumerables, acc, fun, zip_fun) do
|
||||
step = fn x, acc ->
|
||||
{:suspend, :lists.reverse([x | acc])}
|
||||
end
|
||||
|
||||
enum_funs =
|
||||
Enum.map(enumerables, fn enum ->
|
||||
{&Enumerable.reduce(enum, &1, step), [], :cont}
|
||||
end)
|
||||
|
||||
do_zip_enum(enum_funs, acc, fun, zip_fun)
|
||||
end
|
||||
|
||||
# This implementation of do_zip_enum/4 works for any number of streams to zip
|
||||
defp do_zip_enum(zips, {:halt, acc}, _fun, _zip_fun) do
|
||||
do_zip_close(zips)
|
||||
{:halted, acc}
|
||||
end
|
||||
|
||||
defp do_zip_enum(zips, {:suspend, acc}, fun, zip_fun) do
|
||||
{:suspended, acc, &do_zip_enum(zips, &1, fun, zip_fun)}
|
||||
end
|
||||
|
||||
defp do_zip_enum([], {:cont, acc}, _callback, _zip_fun) do
|
||||
{:done, acc}
|
||||
end
|
||||
|
||||
defp do_zip_enum(zips, {:cont, acc}, callback, zip_fun) do
|
||||
try do
|
||||
do_zip_next(zips, acc, callback, [], [], zip_fun)
|
||||
catch
|
||||
kind, reason ->
|
||||
do_zip_close(zips)
|
||||
:erlang.raise(kind, reason, __STACKTRACE__)
|
||||
else
|
||||
{:next, buffer, acc} ->
|
||||
do_zip_enum(buffer, acc, callback, zip_fun)
|
||||
|
||||
{:done, _acc} = other ->
|
||||
other
|
||||
end
|
||||
end
|
||||
|
||||
# do_zip_next/6 computes the next tuple formed by
|
||||
# the next element of each zipped stream.
|
||||
defp do_zip_next(
|
||||
[{_, [], :halt} | zips],
|
||||
acc,
|
||||
_callback,
|
||||
_yielded_elems,
|
||||
buffer,
|
||||
_zip_fun
|
||||
) do
|
||||
do_zip_close(:lists.reverse(buffer, zips))
|
||||
{:done, acc}
|
||||
end
|
||||
|
||||
defp do_zip_next([{fun, [], :cont} | zips], acc, callback, yielded_elems, buffer, zip_fun) do
|
||||
case fun.({:cont, []}) do
|
||||
{:suspended, [elem | next_acc], fun} ->
|
||||
next_buffer = [{fun, next_acc, :cont} | buffer]
|
||||
do_zip_next(zips, acc, callback, [elem | yielded_elems], next_buffer, zip_fun)
|
||||
|
||||
{_, [elem | next_acc]} ->
|
||||
next_buffer = [{fun, next_acc, :halt} | buffer]
|
||||
do_zip_next(zips, acc, callback, [elem | yielded_elems], next_buffer, zip_fun)
|
||||
|
||||
{_, []} ->
|
||||
# The current zipped stream terminated, so we close all the streams
|
||||
# and return {:halted, acc} (which is returned as is by do_zip/3).
|
||||
do_zip_close(:lists.reverse(buffer, zips))
|
||||
{:done, acc}
|
||||
end
|
||||
end
|
||||
|
||||
defp do_zip_next(
|
||||
[{fun, zip_acc, zip_op} | zips],
|
||||
acc,
|
||||
callback,
|
||||
yielded_elems,
|
||||
buffer,
|
||||
zip_fun
|
||||
) do
|
||||
[elem | rest] = zip_acc
|
||||
next_buffer = [{fun, rest, zip_op} | buffer]
|
||||
do_zip_next(zips, acc, callback, [elem | yielded_elems], next_buffer, zip_fun)
|
||||
end
|
||||
|
||||
defp do_zip_next([] = _zips, acc, callback, yielded_elems, buffer, zip_fun) do
|
||||
# "yielded_elems" is a reversed list of results for the current iteration of
|
||||
# zipping. That is to say, the nth element from each of the enums being zipped.
|
||||
# It needs to be reversed and passed to the zipping function so it can do it's thing.
|
||||
{:next, :lists.reverse(buffer), callback.(zip_fun.(:lists.reverse(yielded_elems)), acc)}
|
||||
end
|
||||
|
||||
defp do_zip_close(zips) do
|
||||
:lists.foreach(fn {fun, _, _} -> fun.({:halt, []}) end, zips)
|
||||
def zip_with(enumerables, zip_fun) do
|
||||
R.zip_with(enumerables, zip_fun)
|
||||
end
|
||||
|
||||
## Sources
|
||||
|
||||
@@ -2,6 +2,142 @@ defmodule Stream.Reducers do
|
||||
# Collection of reducers and utilities shared by Enum and Stream.
|
||||
@moduledoc false
|
||||
|
||||
def zip_with(enumerables, zip_fun) when is_function(zip_fun, 1) do
|
||||
if is_list(enumerables) and :lists.all(&is_list/1, enumerables) do
|
||||
&zip_list(enumerables, &1, &2, zip_fun)
|
||||
else
|
||||
&zip_enum(enumerables, &1, &2, zip_fun)
|
||||
end
|
||||
end
|
||||
|
||||
defp zip_list(_enumerables, {:halt, acc}, _fun, _zip_fun) do
|
||||
{:halted, acc}
|
||||
end
|
||||
|
||||
defp zip_list(enumerables, {:suspend, acc}, fun, zip_fun) do
|
||||
{:suspended, acc, &zip_list(enumerables, &1, fun, zip_fun)}
|
||||
end
|
||||
|
||||
defp zip_list([], {:cont, acc}, _fun, _zip_fun), do: {:done, acc}
|
||||
|
||||
defp zip_list(enumerables, {:cont, acc}, fun, zip_fun) do
|
||||
case zip_list_heads_tails(enumerables, [], []) do
|
||||
{heads, tails} -> zip_list(tails, fun.(zip_fun.(heads), acc), fun, zip_fun)
|
||||
:error -> {:done, acc}
|
||||
end
|
||||
end
|
||||
|
||||
defp zip_list_heads_tails([[head | tail] | rest], heads, tails) do
|
||||
zip_list_heads_tails(rest, [head | heads], [tail | tails])
|
||||
end
|
||||
|
||||
defp zip_list_heads_tails([[] | _rest], _heads, _tails) do
|
||||
:error
|
||||
end
|
||||
|
||||
defp zip_list_heads_tails([], heads, tails) do
|
||||
{:lists.reverse(heads), :lists.reverse(tails)}
|
||||
end
|
||||
|
||||
defp zip_enum(enumerables, acc, fun, zip_fun) do
|
||||
step = fn x, acc ->
|
||||
{:suspend, :lists.reverse([x | acc])}
|
||||
end
|
||||
|
||||
enum_funs =
|
||||
Enum.map(enumerables, fn enum ->
|
||||
{&Enumerable.reduce(enum, &1, step), [], :cont}
|
||||
end)
|
||||
|
||||
do_zip_enum(enum_funs, acc, fun, zip_fun)
|
||||
end
|
||||
|
||||
# This implementation of do_zip_enum/4 works for any number of streams to zip
|
||||
defp do_zip_enum(zips, {:halt, acc}, _fun, _zip_fun) do
|
||||
do_zip_close(zips)
|
||||
{:halted, acc}
|
||||
end
|
||||
|
||||
defp do_zip_enum(zips, {:suspend, acc}, fun, zip_fun) do
|
||||
{:suspended, acc, &do_zip_enum(zips, &1, fun, zip_fun)}
|
||||
end
|
||||
|
||||
defp do_zip_enum([], {:cont, acc}, _callback, _zip_fun) do
|
||||
{:done, acc}
|
||||
end
|
||||
|
||||
defp do_zip_enum(zips, {:cont, acc}, callback, zip_fun) do
|
||||
try do
|
||||
do_zip_next(zips, acc, callback, [], [], zip_fun)
|
||||
catch
|
||||
kind, reason ->
|
||||
do_zip_close(zips)
|
||||
:erlang.raise(kind, reason, __STACKTRACE__)
|
||||
else
|
||||
{:next, buffer, acc} ->
|
||||
do_zip_enum(buffer, acc, callback, zip_fun)
|
||||
|
||||
{:done, _acc} = other ->
|
||||
other
|
||||
end
|
||||
end
|
||||
|
||||
# do_zip_next/6 computes the next tuple formed by
|
||||
# the next element of each zipped stream.
|
||||
defp do_zip_next(
|
||||
[{_, [], :halt} | zips],
|
||||
acc,
|
||||
_callback,
|
||||
_yielded_elems,
|
||||
buffer,
|
||||
_zip_fun
|
||||
) do
|
||||
do_zip_close(:lists.reverse(buffer, zips))
|
||||
{:done, acc}
|
||||
end
|
||||
|
||||
defp do_zip_next([{fun, [], :cont} | zips], acc, callback, yielded_elems, buffer, zip_fun) do
|
||||
case fun.({:cont, []}) do
|
||||
{:suspended, [elem | next_acc], fun} ->
|
||||
next_buffer = [{fun, next_acc, :cont} | buffer]
|
||||
do_zip_next(zips, acc, callback, [elem | yielded_elems], next_buffer, zip_fun)
|
||||
|
||||
{_, [elem | next_acc]} ->
|
||||
next_buffer = [{fun, next_acc, :halt} | buffer]
|
||||
do_zip_next(zips, acc, callback, [elem | yielded_elems], next_buffer, zip_fun)
|
||||
|
||||
{_, []} ->
|
||||
# The current zipped stream terminated, so we close all the streams
|
||||
# and return {:halted, acc} (which is returned as is by do_zip/3).
|
||||
do_zip_close(:lists.reverse(buffer, zips))
|
||||
{:done, acc}
|
||||
end
|
||||
end
|
||||
|
||||
defp do_zip_next(
|
||||
[{fun, zip_acc, zip_op} | zips],
|
||||
acc,
|
||||
callback,
|
||||
yielded_elems,
|
||||
buffer,
|
||||
zip_fun
|
||||
) do
|
||||
[elem | rest] = zip_acc
|
||||
next_buffer = [{fun, rest, zip_op} | buffer]
|
||||
do_zip_next(zips, acc, callback, [elem | yielded_elems], next_buffer, zip_fun)
|
||||
end
|
||||
|
||||
defp do_zip_next([] = _zips, acc, callback, yielded_elems, buffer, zip_fun) do
|
||||
# "yielded_elems" is a reversed list of results for the current iteration of
|
||||
# zipping. That is to say, the nth element from each of the enums being zipped.
|
||||
# It needs to be reversed and passed to the zipping function so it can do it's thing.
|
||||
{:next, :lists.reverse(buffer), callback.(zip_fun.(:lists.reverse(yielded_elems)), acc)}
|
||||
end
|
||||
|
||||
defp do_zip_close(zips) do
|
||||
:lists.foreach(fn {fun, _, _} -> fun.({:halt, []}) end, zips)
|
||||
end
|
||||
|
||||
def chunk_every(chunk_by, enumerable, count, step, leftover) do
|
||||
limit = :erlang.max(count, step)
|
||||
|
||||
@@ -151,10 +287,10 @@ defmodule Stream.Reducers do
|
||||
defmacro reject(callback, fun \\ nil) do
|
||||
quote do
|
||||
fn entry, acc ->
|
||||
unless unquote(callback).(entry) do
|
||||
next(unquote(fun), entry, acc)
|
||||
else
|
||||
if unquote(callback).(entry) do
|
||||
skip(acc)
|
||||
else
|
||||
next(unquote(fun), entry, acc)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
+32
-15
@@ -18,7 +18,7 @@ defmodule String do
|
||||
"hello world"
|
||||
|
||||
The functions in this module act according to
|
||||
[The Unicode Standard, Version 15.1.0](http://www.unicode.org/versions/Unicode15.1.0/).
|
||||
[The Unicode Standard, Version 16.0.0](http://www.unicode.org/versions/Unicode16.0.0/).
|
||||
|
||||
## Interpolation
|
||||
|
||||
@@ -326,7 +326,7 @@ defmodule String do
|
||||
recur_printable?(string, character_limit)
|
||||
end
|
||||
|
||||
defp recur_printable?(_string, 0), do: true
|
||||
defp recur_printable?(<<_::binary>>, 0), do: true
|
||||
defp recur_printable?(<<>>, _character_limit), do: true
|
||||
|
||||
for char <- 0x20..0x7E do
|
||||
@@ -357,9 +357,10 @@ defmodule String do
|
||||
|
||||
@doc ~S"""
|
||||
Divides a string into substrings at each Unicode whitespace
|
||||
occurrence with leading and trailing whitespace ignored. Groups
|
||||
of whitespace are treated as a single occurrence. Divisions do
|
||||
not occur on non-breaking whitespace.
|
||||
occurrence with leading and trailing whitespace ignored.
|
||||
|
||||
Groups of whitespace are treated as a single occurrence.
|
||||
Divisions do not occur on non-breaking whitespace.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -375,6 +376,11 @@ defmodule String do
|
||||
iex> String.split("no\u00a0break")
|
||||
["no\u00a0break"]
|
||||
|
||||
Removes empty strings, like when using `trim: true` in `String.split/3`.
|
||||
|
||||
iex> String.split(" ")
|
||||
[]
|
||||
|
||||
"""
|
||||
@spec split(t) :: [t]
|
||||
defdelegate split(binary), to: String.Break
|
||||
@@ -640,10 +646,12 @@ defmodule String do
|
||||
The offset is capped to the length of the string. Returns a tuple with
|
||||
two elements.
|
||||
|
||||
Note: keep in mind this function splits on graphemes and for such it
|
||||
has to linearly traverse the string. If you want to split a string or
|
||||
a binary based on the number of bytes, use `Kernel.binary_part/3`
|
||||
instead.
|
||||
> #### Linear Access {: .warning}
|
||||
>
|
||||
> This function splits on graphemes and for such it has to linearly traverse
|
||||
> the string.
|
||||
> If you want to split a string or a binary based on the number of bytes,
|
||||
> use `Kernel.binary_part/3` instead.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -2225,6 +2233,12 @@ defmodule String do
|
||||
Returns the grapheme at the `position` of the given UTF-8 `string`.
|
||||
If `position` is greater than `string` length, then it returns `nil`.
|
||||
|
||||
> #### Linear Access {: .warning}
|
||||
>
|
||||
> This function has to linearly traverse the string.
|
||||
> If you want to access a string or a binary in constant time based on the
|
||||
> number of bytes, use `Kernel.binary_slice/3` or `:binary.at/2` instead.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> String.at("elixir", 0)
|
||||
@@ -2961,9 +2975,9 @@ defmodule String do
|
||||
@doc """
|
||||
Returns a float whose text representation is `string`.
|
||||
|
||||
`string` must be the string representation of a float including a decimal point.
|
||||
In order to parse a string without decimal point as a float then `Float.parse/1`
|
||||
should be used. Otherwise, an `ArgumentError` will be raised.
|
||||
`string` must be the string representation of a float including leading digits and a decimal
|
||||
point. To parse a string without decimal point as a float, refer to `Float.parse/1`. Otherwise,
|
||||
an `ArgumentError` will be raised.
|
||||
|
||||
Inlined by the compiler.
|
||||
|
||||
@@ -2978,6 +2992,9 @@ defmodule String do
|
||||
String.to_float("3")
|
||||
** (ArgumentError) argument error
|
||||
|
||||
String.to_float(".3")
|
||||
** (ArgumentError) argument error
|
||||
|
||||
"""
|
||||
@spec to_float(String.t()) :: float
|
||||
def to_float(string) when is_binary(string) do
|
||||
@@ -3043,10 +3060,10 @@ defmodule String do
|
||||
end
|
||||
|
||||
defp bag_difference(bag1, bag2) do
|
||||
Enum.reduce(bag1, 0, fn {char, count1}, sum ->
|
||||
Enum.sum_by(bag1, fn {char, count1} ->
|
||||
case bag2 do
|
||||
%{^char => count2} -> sum + max(count1 - count2, 0)
|
||||
%{} -> sum + count1
|
||||
%{^char => count2} -> max(count1 - count2, 0)
|
||||
%{} -> count1
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
@@ -57,6 +57,6 @@ end
|
||||
|
||||
defimpl String.Chars, for: Float do
|
||||
def to_string(term) do
|
||||
:erlang.float_to_binary(term, [:short])
|
||||
Float.to_string(term)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -188,6 +188,12 @@ defmodule StringIO do
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
# Fail fast if someone tries to use it with a File API
|
||||
def handle_info({:file_request, from, reply_as, _req}, state) do
|
||||
send(from, {:file_reply, reply_as, {:error, :enotsup}})
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
def handle_info({:DOWN, _, _, _, _}, state) do
|
||||
{:stop, :shutdown, state}
|
||||
end
|
||||
|
||||
@@ -525,7 +525,7 @@ defmodule Supervisor do
|
||||
import Supervisor.Spec
|
||||
@behaviour Supervisor
|
||||
|
||||
unless Module.has_attribute?(__MODULE__, :doc) do
|
||||
if not Module.has_attribute?(__MODULE__, :doc) do
|
||||
@doc """
|
||||
Returns a specification to start this module under a supervisor.
|
||||
|
||||
@@ -595,7 +595,7 @@ defmodule Supervisor do
|
||||
@typedoc "The supervisor reference."
|
||||
@type supervisor :: pid | name | {atom, node}
|
||||
|
||||
@typedoc "Options given to `start_link/2` and `c:init/1`."
|
||||
@typedoc "Options given to `start_link/2` and `init/2`."
|
||||
@type init_option ::
|
||||
{:strategy, strategy}
|
||||
| {:max_restarts, non_neg_integer}
|
||||
|
||||
@@ -168,7 +168,7 @@ defmodule Supervisor.Spec do
|
||||
) :: {:ok, tuple}
|
||||
@deprecated "Use the new child specifications outlined in the Supervisor module instead"
|
||||
def supervise(children, options) do
|
||||
unless strategy = options[:strategy] do
|
||||
if !(strategy = options[:strategy]) do
|
||||
raise ArgumentError, "expected :strategy option to be given"
|
||||
end
|
||||
|
||||
|
||||
@@ -1054,7 +1054,7 @@ defmodule System do
|
||||
* `:arg0` - sets the command arg0
|
||||
|
||||
* `:stderr_to_stdout` - redirects stderr to stdout when `true`, no effect
|
||||
if `use_stdio` is `false``.
|
||||
if `use_stdio` is `false`.
|
||||
|
||||
* `:use_stdio` - `true` by default, setting it to false allows direct
|
||||
interaction with the terminal from the callee
|
||||
@@ -1101,7 +1101,7 @@ defmodule System do
|
||||
def cmd(command, args, opts \\ []) when is_binary(command) and is_list(args) do
|
||||
assert_no_null_byte!(command, "System.cmd/3")
|
||||
|
||||
unless Enum.all?(args, &is_binary/1) do
|
||||
if not Enum.all?(args, &is_binary/1) do
|
||||
raise ArgumentError, "all arguments for System.cmd/3 must be binaries"
|
||||
end
|
||||
|
||||
@@ -1239,7 +1239,7 @@ defmodule System do
|
||||
This time is monotonically increasing and starts in an unspecified
|
||||
point in time.
|
||||
"""
|
||||
@spec monotonic_time(time_unit) :: integer
|
||||
@spec monotonic_time(time_unit | :native) :: integer
|
||||
def monotonic_time(unit) do
|
||||
:erlang.monotonic_time(normalize_time_unit(unit))
|
||||
end
|
||||
@@ -1265,7 +1265,7 @@ defmodule System do
|
||||
case of time warps although the VM works towards aligning
|
||||
them. This time is not monotonic.
|
||||
"""
|
||||
@spec system_time(time_unit) :: integer
|
||||
@spec system_time(time_unit | :native) :: integer
|
||||
def system_time(unit) do
|
||||
:erlang.system_time(normalize_time_unit(unit))
|
||||
end
|
||||
@@ -1316,7 +1316,7 @@ defmodule System do
|
||||
`monotonic_time/1`), gives the Erlang system time that corresponds
|
||||
to that monotonic time.
|
||||
"""
|
||||
@spec time_offset(time_unit) :: integer
|
||||
@spec time_offset(time_unit | :native) :: integer
|
||||
def time_offset(unit) do
|
||||
:erlang.time_offset(normalize_time_unit(unit))
|
||||
end
|
||||
|
||||
@@ -329,7 +329,7 @@ defmodule Task do
|
||||
@doc false
|
||||
defmacro __using__(opts) do
|
||||
quote location: :keep, bind_quoted: [opts: opts] do
|
||||
unless Module.has_attribute?(__MODULE__, :doc) do
|
||||
if not Module.has_attribute?(__MODULE__, :doc) do
|
||||
@doc """
|
||||
Returns a specification to start this module under a supervisor.
|
||||
|
||||
@@ -610,7 +610,7 @@ defmodule Task do
|
||||
* `:zip_input_on_exit` - (since v1.14.0) adds the original
|
||||
input to `:exit` tuples. The value emitted for that task is
|
||||
`{:exit, {input, reason}}`, where `input` is the collection element
|
||||
that caused an exited during processing. Defaults to `false`.
|
||||
that caused an exit during processing. Defaults to `false`.
|
||||
|
||||
## Example
|
||||
|
||||
@@ -716,7 +716,7 @@ defmodule Task do
|
||||
|
||||
iex> strings = ["long string", "longer string", "there are many of these"]
|
||||
iex> stream = Task.async_stream(strings, fn text -> text |> String.codepoints() |> Enum.count() end)
|
||||
iex> Enum.reduce(stream, 0, fn {:ok, num}, acc -> num + acc end)
|
||||
iex> Enum.sum_by(stream, fn {:ok, num} -> num end)
|
||||
47
|
||||
|
||||
See `async_stream/5` for discussion, options, and more examples.
|
||||
@@ -842,14 +842,14 @@ defmodule Task do
|
||||
Process.demonitor(ref, [:flush])
|
||||
|
||||
{url, state} = pop_in(state.tasks[ref])
|
||||
IO.puts "Got #{inspect(result)} for URL #{inspect url}"
|
||||
IO.puts("Got #{inspect(result)} for URL #{inspect url}")
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
# If the task fails...
|
||||
def handle_info({:DOWN, ref, _, _, reason}, state) do
|
||||
{url, state} = pop_in(state.tasks[ref])
|
||||
IO.puts "URL #{inspect url} failed with reason #{inspect(reason)}"
|
||||
IO.puts("URL #{inspect url} failed with reason #{inspect(reason)}")
|
||||
{:noreply, state}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -209,15 +209,15 @@ defmodule Task.Supervised do
|
||||
ordered = Keyword.get(options, :ordered, true)
|
||||
zip_input_on_exit = Keyword.get(options, :zip_input_on_exit, false)
|
||||
|
||||
unless is_integer(max_concurrency) and max_concurrency > 0 do
|
||||
if not (is_integer(max_concurrency) and max_concurrency > 0) do
|
||||
raise ArgumentError, ":max_concurrency must be an integer greater than zero"
|
||||
end
|
||||
|
||||
unless on_timeout in [:exit, :kill_task] do
|
||||
if on_timeout not in [:exit, :kill_task] do
|
||||
raise ArgumentError, ":on_timeout must be either :exit or :kill_task"
|
||||
end
|
||||
|
||||
unless (is_integer(timeout) and timeout >= 0) or timeout == :infinity do
|
||||
if not ((is_integer(timeout) and timeout >= 0) or timeout == :infinity) do
|
||||
raise ArgumentError, ":timeout must be either a positive integer or :infinity"
|
||||
end
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ defmodule Task.Supervisor do
|
||||
|
||||
@typedoc "Option values used by `start_link`"
|
||||
@type option ::
|
||||
DynamicSupervisor.option()
|
||||
GenServer.option()
|
||||
| DynamicSupervisor.init_option()
|
||||
|
||||
@typedoc """
|
||||
@@ -498,7 +498,7 @@ defmodule Task.Supervisor do
|
||||
Starts a task as a child of the given `supervisor`.
|
||||
|
||||
Task.Supervisor.start_child(MyTaskSupervisor, fn ->
|
||||
IO.puts "I am running in a task"
|
||||
IO.puts("I am running in a task")
|
||||
end)
|
||||
|
||||
Note that the spawned process is not linked to the caller, but
|
||||
@@ -614,7 +614,7 @@ defmodule Task.Supervisor do
|
||||
defp build_stream(supervisor, link_type, enumerable, fun, options) do
|
||||
shutdown = Keyword.get(options, :shutdown, 5000)
|
||||
|
||||
unless (is_integer(shutdown) and shutdown >= 0) or shutdown == :brutal_kill do
|
||||
if not ((is_integer(shutdown) and shutdown >= 0) or shutdown == :brutal_kill) do
|
||||
raise ArgumentError, ":shutdown must be either a positive integer or :brutal_kill"
|
||||
end
|
||||
|
||||
|
||||
+2
-16
@@ -87,22 +87,8 @@ defmodule Tuple do
|
||||
:erlang.insert_element(index + 1, tuple, value)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Inserts an element at the end of a tuple.
|
||||
|
||||
Returns a new tuple with the element appended at the end, and contains
|
||||
the elements in `tuple` followed by `value` as the last element.
|
||||
|
||||
Inlined by the compiler.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> tuple = {:foo, :bar}
|
||||
iex> Tuple.append(tuple, :baz)
|
||||
{:foo, :bar, :baz}
|
||||
|
||||
"""
|
||||
@spec append(tuple, term) :: tuple
|
||||
@doc false
|
||||
@deprecated "Use insert_at instead"
|
||||
def append(tuple, value) do
|
||||
:erlang.append_element(tuple, value)
|
||||
end
|
||||
|
||||
+21
-16
@@ -367,21 +367,27 @@ defmodule URI do
|
||||
@doc """
|
||||
Percent-encodes all characters that require escaping in `string`.
|
||||
|
||||
By default, this function is meant to escape the whole URI, and
|
||||
therefore it will only escape characters which are foreign in
|
||||
all parts of a URI. Reserved characters (such as `:` and `/`)
|
||||
or unreserved (such as letters and numbers) are not escaped.
|
||||
The optional `predicate` argument specifies a function used to detect whether
|
||||
a byte in the `string` should be escaped:
|
||||
|
||||
Because different components of a URI require different escaping
|
||||
rules, this function also accepts a `predicate` function as an optional
|
||||
argument. If passed, this function will be called with each byte
|
||||
in `string` as its argument and should return a truthy value (anything other
|
||||
than `false` or `nil`) if the given byte should be left as is, or
|
||||
return a falsy value (`false` or `nil`) if the character should be
|
||||
escaped. Defaults to `URI.char_unescaped?/1`.
|
||||
* if the function returns a truthy value, the byte should be kept as-is.
|
||||
* if the function returns a falsy value, the byte should be escaped.
|
||||
|
||||
See `encode_www_form/1` if you are interested in escaping reserved
|
||||
characters too.
|
||||
The `predicate` argument can use some built-in functions:
|
||||
|
||||
* `URI.char_unescaped?/1` (default) - reserved characters (such as `:`
|
||||
and `/`) or unreserved (such as letters and numbers) are kept as-is.
|
||||
It's typically used to encode the whole URI.
|
||||
* `URI.char_unreserved?/1` - unreserved characters (such as letters and
|
||||
numbers) are kept as-is. It's typically used to encode components in
|
||||
a URI, such as query or fragment.
|
||||
* `URI.char_reserved?/1` - Reserved characters (such as `:` and `/`) are
|
||||
kept as-is.
|
||||
|
||||
And, you can also use custom functions.
|
||||
|
||||
See `encode_www_form/1` if you are interested in encoding `string` as
|
||||
"x-www-form-urlencoded".
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -693,8 +699,7 @@ defmodule URI do
|
||||
|
||||
> #### `:authority` field {: .info}
|
||||
>
|
||||
> This function sets the field `:authority` for backwards-compatibility reasons
|
||||
> but it is deprecated.
|
||||
> This function sets the deprecated field `:authority` for backwards-compatibility reasons.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -902,7 +907,7 @@ defmodule URI do
|
||||
%{rel | path: remove_dot_segments_from_path(rel.path)}
|
||||
end
|
||||
|
||||
def merge(base, %URI{host: host} = rel) when host != nil do
|
||||
def merge(%URI{} = base, %URI{host: host} = rel) when host != nil do
|
||||
%{rel | scheme: base.scheme, path: remove_dot_segments_from_path(rel.path)}
|
||||
end
|
||||
|
||||
|
||||
@@ -686,7 +686,7 @@ end
|
||||
|
||||
defimpl Inspect, for: Version.Requirement do
|
||||
def inspect(%Version.Requirement{source: source}, opts) do
|
||||
colorized = Inspect.Algebra.color("\"" <> source <> "\"", :string, opts)
|
||||
colorized = Inspect.Algebra.color_doc("\"" <> source <> "\"", :string, opts)
|
||||
|
||||
Inspect.Algebra.concat(["Version.parse_requirement!(", colorized, ")"])
|
||||
end
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user