Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c5259826f | ||
|
|
ccdefd904f | ||
|
|
ff95a59ec0 | ||
|
|
4081590742 | ||
|
|
693df5fe8c | ||
|
|
b4605374b5 | ||
|
|
36ec08e0c8 | ||
|
|
ae0ac9b3c9 | ||
|
|
e3479831ba | ||
|
|
ffbcc004ec | ||
|
|
e0d61c7875 | ||
|
|
b2059031a6 | ||
|
|
ed2829d8ea | ||
|
|
081f99cd37 | ||
|
|
7837cc5c59 | ||
|
|
b96fc39422 | ||
|
|
e1cb898198 | ||
|
|
88e2894925 | ||
|
|
f7cb5ae053 | ||
|
|
b4845e6854 | ||
|
|
0f1200f99b | ||
|
|
cec1a7190b | ||
|
|
53d1b98a05 | ||
|
|
4828f11cd2 | ||
|
|
501271cad0 | ||
|
|
fd4a6ad5cf | ||
|
|
196aab87ca | ||
|
|
59b266506a | ||
|
|
1c8e625d77 | ||
|
|
f9d7adbfdf | ||
|
|
f1721c8a58 | ||
|
|
57abca4c16 | ||
|
|
8e9ece0d62 | ||
|
|
54ba096d34 | ||
|
|
6042fd0bcc | ||
|
|
475b73b46f | ||
|
|
3cf6069ad9 | ||
|
|
48cee5aaa5 | ||
|
|
94160c0ed1 | ||
|
|
bc542dcc2a | ||
|
|
5e5f00c6fe | ||
|
|
a59fe07529 | ||
|
|
0c3aebbc47 | ||
|
|
e7555da79f | ||
|
|
98cfa7b738 | ||
|
|
556bd254db | ||
|
|
4b9a2bb8dc | ||
|
|
3a876ca053 | ||
|
|
3ab4623657 | ||
|
|
5d9eb80874 | ||
|
|
8167bb65d1 | ||
|
|
ec8cde34d0 | ||
|
|
0d18ecd286 | ||
|
|
301711af12 | ||
|
|
cc9f4b8292 | ||
|
|
b52ddb30e0 | ||
|
|
d65994921a | ||
|
|
42e24569f4 | ||
|
|
2de183ede6 | ||
|
|
cce861eec7 | ||
|
|
9b388a58a1 | ||
|
|
29858c9d1e | ||
|
|
2a53036053 | ||
|
|
ef0f17dfe5 | ||
|
|
1a6667d551 | ||
|
|
1fca074184 | ||
|
|
749459a32f | ||
|
|
af11c51b72 | ||
|
|
df1d9a6b90 | ||
|
|
8f95fdadaf | ||
|
|
cf9fa416f8 | ||
|
|
67cbd59d03 | ||
|
|
76bfcfa871 | ||
|
|
bc5bb85a17 | ||
|
|
7eb29f5905 | ||
|
|
bad2b253d7 | ||
|
|
8e20b02a71 | ||
|
|
85910e7385 | ||
|
|
f6c67c3d49 | ||
|
|
4019f7acf1 | ||
|
|
095705d8e3 | ||
|
|
8df88f9ac2 | ||
|
|
94b1df3ef4 | ||
|
|
607110b39b | ||
|
|
2d9762f3f5 | ||
|
|
46ff8ccbc4 | ||
|
|
273a929001 | ||
|
|
cf9ddd9194 | ||
|
|
82dca5537a | ||
|
|
6c327d025a | ||
|
|
65bf11d51f | ||
|
|
e3344123c2 | ||
|
|
1995478163 | ||
|
|
74f09347d5 | ||
|
|
82c4c3616b | ||
|
|
2d6e7a6364 | ||
|
|
f11ca077ec | ||
|
|
0b51e0129e | ||
|
|
0914d1d312 | ||
|
|
403a5c1725 | ||
|
|
a0cef3bdd7 | ||
|
|
df449805c8 | ||
|
|
bc73ad46ef | ||
|
|
e1e8f9afec | ||
|
|
0f61583978 | ||
|
|
afa6d0c177 | ||
|
|
c260957de5 | ||
|
|
94fa4e5d19 | ||
|
|
11ce4999c5 | ||
|
|
1c23624489 | ||
|
|
d332c78e3b | ||
|
|
ff9c76c95a | ||
|
|
18515152d3 | ||
|
|
70bcc1494d | ||
|
|
3b80c1b1e0 | ||
|
|
ba1e45a08d | ||
|
|
a19808942f | ||
|
|
4fa9076e52 | ||
|
|
21f7a719d8 | ||
|
|
bfefb5590b | ||
|
|
0fb465bcc6 | ||
|
|
9a12013ed0 | ||
|
|
cebfb83a7e | ||
|
|
9cd30ce651 | ||
|
|
c025240fb0 | ||
|
|
65deef3907 | ||
|
|
0a0ff7fa9a | ||
|
|
97e8b0242d | ||
|
|
afb6e207a4 | ||
|
|
562fd7cc2c | ||
|
|
c1a538e4a8 | ||
|
|
2013b4dc34 | ||
|
|
760cf2ccb6 | ||
|
|
512594c08a | ||
|
|
660fc86ede | ||
|
|
7b27ffa7de | ||
|
|
b7756f875c | ||
|
|
c56cc3df49 | ||
|
|
b0b0f8e0fd | ||
|
|
d38a9b8112 | ||
|
|
72f70de5fd | ||
|
|
5834b14433 | ||
|
|
d0ec4296ba | ||
|
|
4822cb230c | ||
|
|
323dac093b | ||
|
|
71503e07bd | ||
|
|
983ee5c640 | ||
|
|
2b6d3f8031 | ||
|
|
92cf894756 | ||
|
|
e798fd7762 | ||
|
|
854db25440 | ||
|
|
ebc57cdd16 | ||
|
|
fc6bf59462 | ||
|
|
e764de29f6 | ||
|
|
480cbf66c3 | ||
|
|
64afd6bebf | ||
|
|
f80fedde81 | ||
|
|
6228ade7d2 | ||
|
|
08706cc490 | ||
|
|
5e591ebfc6 | ||
|
|
1c83baae63 | ||
|
|
4a7f19b97c | ||
|
|
41159e562f | ||
|
|
35f6ff827c | ||
|
|
295f4a7052 | ||
|
|
98f2201e6d | ||
|
|
5fa00e5260 | ||
|
|
b04089d9b9 | ||
|
|
5196793ec3 | ||
|
|
9082f70a27 | ||
|
|
d135c7f8ca | ||
|
|
9f6e39d1b2 | ||
|
|
c192083726 | ||
|
|
9413dcc573 | ||
|
|
8b5d6bdd4f | ||
|
|
4d44dac703 | ||
|
|
12001c6c28 | ||
|
|
3dd567c362 | ||
|
|
b3d6aff7b6 | ||
|
|
7aa93b188c | ||
|
|
a7e1e8b20f | ||
|
|
e427599eaf | ||
|
|
dc3a261761 | ||
|
|
dce3f6eb6e | ||
|
|
2885c01da8 | ||
|
|
ba0b09f0fb | ||
|
|
cf77f5f5da | ||
|
|
cf36a9aaef | ||
|
|
9cbd6dcda7 | ||
|
|
faec14746f | ||
|
|
a122c512a9 | ||
|
|
de2010649d | ||
|
|
4d2c491e19 | ||
|
|
67c30abb4a | ||
|
|
85968b6009 | ||
|
|
3b9d262dec | ||
|
|
b2270699e1 | ||
|
|
b41c3fc65d | ||
|
|
801f52fc5d | ||
|
|
9ff0985599 | ||
|
|
dccbbf3731 | ||
|
|
b356a2f20d | ||
|
|
5aabde9b8e | ||
|
|
89016215e7 | ||
|
|
5e39868303 | ||
|
|
284881c370 | ||
|
|
9699ec1c0e | ||
|
|
b792ae50c3 | ||
|
|
11c3c663bc | ||
|
|
d8785b146c | ||
|
|
402c3c51e6 | ||
|
|
1b7c4c3944 | ||
|
|
9a91cd5cb0 | ||
|
|
e2de345c26 | ||
|
|
f118e58254 | ||
|
|
99622c6ab3 | ||
|
|
2ccef8c503 | ||
|
|
089d0cd6b6 | ||
|
|
8895603c21 | ||
|
|
a948b42e0e | ||
|
|
b4040b99d7 | ||
|
|
522af0821b | ||
|
|
76a59a7ab4 | ||
|
|
f6cd75b978 | ||
|
|
1ccfdcfe6b | ||
|
|
4a2047af1f | ||
|
|
739a41f54f | ||
|
|
0133ccd692 | ||
|
|
c352e4f562 | ||
|
|
b57852d49f | ||
|
|
cafb4c2ae2 | ||
|
|
16d3522a6c | ||
|
|
d89c31c056 | ||
|
|
bcdeeb3e96 | ||
|
|
ea885f293c | ||
|
|
a00178092e | ||
|
|
e935cf0784 | ||
|
|
6db8897230 | ||
|
|
c091f9bab7 | ||
|
|
72adfa330d | ||
|
|
fb4474de0a | ||
|
|
b5266b0fd0 | ||
|
|
46bd011fef | ||
|
|
2d9a64f147 | ||
|
|
ddf1e1452c | ||
|
|
6db550fd3a | ||
|
|
e6e5d2a98d | ||
|
|
89cec027ba | ||
|
|
319ba7157e | ||
|
|
51755e2ee9 | ||
|
|
aace60b863 | ||
|
|
0ef18f9892 | ||
|
|
74b33b752a | ||
|
|
0166ee82a1 | ||
|
|
e51cc8b88c | ||
|
|
16fafe2117 | ||
|
|
8b4ec95a99 | ||
|
|
2b2587c68c | ||
|
|
3c6891cb7d | ||
|
|
9a6db666a1 | ||
|
|
71f690543a | ||
|
|
37f11279e3 | ||
|
|
3ed1a66432 | ||
|
|
6666b7bb31 | ||
|
|
2b264cbd32 | ||
|
|
bdaf42bf15 | ||
|
|
6a8867c66f | ||
|
|
69df6543fb | ||
|
|
92756bd679 | ||
|
|
75e5609364 | ||
|
|
83ddb923d4 | ||
|
|
f1151b27c5 | ||
|
|
5725832003 | ||
|
|
53d66875d7 | ||
|
|
18540250e2 | ||
|
|
8744a3cde8 | ||
|
|
4bc45a7b63 | ||
|
|
b56c262db3 | ||
|
|
ed63846b4d | ||
|
|
83374a88c9 | ||
|
|
3d1c1b4e23 | ||
|
|
cc138f37f2 | ||
|
|
0df37749af | ||
|
|
8148d079bf | ||
|
|
091bb1b5b5 | ||
|
|
b76e73abbc | ||
|
|
3757e36ed7 | ||
|
|
9c99bc28a3 | ||
|
|
d2cb4f4063 | ||
|
|
5257bbe548 | ||
|
|
5cee48fdc8 | ||
|
|
ef2785326c | ||
|
|
0bbcb8f280 | ||
|
|
cfba0c8f34 | ||
|
|
779ccdb756 | ||
|
|
d9e01a61c9 | ||
|
|
5d6da17e29 | ||
|
|
76c80e9552 | ||
|
|
58483e0445 | ||
|
|
1a1dc609c0 | ||
|
|
bdd2218b61 | ||
|
|
b9209ad1ac | ||
|
|
d155966074 | ||
|
|
7456b5789b | ||
|
|
7c8ca5a3af | ||
|
|
291ebf7458 | ||
|
|
70da709881 | ||
|
|
06e8bfacc0 | ||
|
|
10fdb8e2d4 | ||
|
|
44061c8499 | ||
|
|
50995e6640 | ||
|
|
ce05be75eb | ||
|
|
df58c00e20 | ||
|
|
61910595d3 | ||
|
|
1f80700c10 | ||
|
|
fa76ac65db | ||
|
|
b028ac88a8 | ||
|
|
c592d1300d | ||
|
|
479b2890ce | ||
|
|
608a023fa7 | ||
|
|
42af53f094 | ||
|
|
1d8521d148 | ||
|
|
ebb0e84897 | ||
|
|
5c05d1ab22 | ||
|
|
f44a7fabd9 | ||
|
|
1b1d365443 | ||
|
|
c12a130e7c | ||
|
|
533f2f7d2f | ||
|
|
f89d809d0b | ||
|
|
fc9f7690c1 | ||
|
|
c89b16a98a | ||
|
|
68aa544d90 | ||
|
|
1f6e7093cf | ||
|
|
f837246027 | ||
|
|
181c38f16e | ||
|
|
655ce6d3b4 | ||
|
|
7f174f1fff | ||
|
|
fe77f54b5f | ||
|
|
c040270bd7 | ||
|
|
9235f8e72a | ||
|
|
285097b41f | ||
|
|
6a0973b091 | ||
|
|
f4539c3fa3 | ||
|
|
99c587fff5 | ||
|
|
f1b0da335e | ||
|
|
65ea09dcb8 | ||
|
|
f86d8197fe | ||
|
|
f3c4aae804 | ||
|
|
2dacb59d36 | ||
|
|
3fd68ef62a | ||
|
|
bdde9e619b | ||
|
|
1fd15774a4 | ||
|
|
57181bfc77 | ||
|
|
3be3afb920 | ||
|
|
199acd1c2f | ||
|
|
276a8bf82d | ||
|
|
4fea9cc942 | ||
|
|
381bd68624 | ||
|
|
8bcbfe9a87 | ||
|
|
86534cf7ec | ||
|
|
295ebf4018 | ||
|
|
d015598d71 | ||
|
|
80646cf56e | ||
|
|
ad8a69dee0 | ||
|
|
c9e1f85572 | ||
|
|
e72d4d1cdc | ||
|
|
abb6e10a10 | ||
|
|
986bd30072 | ||
|
|
14be5e4473 | ||
|
|
e8cb3489c9 | ||
|
|
e7139df2bf | ||
|
|
a726fcb546 | ||
|
|
b6f0604880 | ||
|
|
f2a4b55fdb | ||
|
|
8770a61769 | ||
|
|
cd88b69dbe | ||
|
|
9dc480d3ba | ||
|
|
3fe3accc05 | ||
|
|
0eb3321c10 | ||
|
|
2b8c2210b0 | ||
|
|
3825c79dda | ||
|
|
4da2b27863 | ||
|
|
14f843bc8b | ||
|
|
ec13373031 | ||
|
|
7094e9754c | ||
|
|
7c19db6727 | ||
|
|
bdb5072925 | ||
|
|
f1704d42bc | ||
|
|
8ec9860a4e | ||
|
|
2c8fe38a07 | ||
|
|
2627fa49c5 | ||
|
|
c3dc9ddd57 | ||
|
|
ac9dec8daa | ||
|
|
775eed1567 | ||
|
|
5ea927e081 | ||
|
|
7be4c5051c | ||
|
|
1387035845 | ||
|
|
89f733e258 | ||
|
|
9b5cafe72a | ||
|
|
138c1bd9ed | ||
|
|
b79a97a6b6 | ||
|
|
29f29419a2 | ||
|
|
48ef163566 | ||
|
|
1ce201aa1e | ||
|
|
2d20e30263 | ||
|
|
80c45ed673 | ||
|
|
62db05ded2 | ||
|
|
f30ee1fec4 | ||
|
|
cb1cfdd991 | ||
|
|
20ff5114a1 | ||
|
|
4e8ff12bea | ||
|
|
e30525bdbb | ||
|
|
fc12dce7c3 | ||
|
|
10a0fd8144 | ||
|
|
1c157bdc54 | ||
|
|
110dfe67aa | ||
|
|
3300016a85 | ||
|
|
ee9667848a | ||
|
|
8742096f77 | ||
|
|
3be6098300 | ||
|
|
3d3e233255 | ||
|
|
c257b13291 | ||
|
|
68ad287a58 | ||
|
|
101a801bdd | ||
|
|
aa0a9b3f4f | ||
|
|
9306ca9ea4 | ||
|
|
5e3ef21c24 | ||
|
|
fd98a3f3cb | ||
|
|
97aef3c5db | ||
|
|
acf507f821 | ||
|
|
8491f0dc74 | ||
|
|
6db1b1258e | ||
|
|
2701169b1e | ||
|
|
1aea878a7a | ||
|
|
f3806f82af | ||
|
|
d81260ebe7 | ||
|
|
cb916e31cd | ||
|
|
1854bbbe97 | ||
|
|
35a5a72a2c | ||
|
|
68c1ae2545 | ||
|
|
9a9b1f48bc | ||
|
|
6570d782e4 | ||
|
|
0d5ca47fda | ||
|
|
d54b2ee850 | ||
|
|
a967dc8189 | ||
|
|
644bc7c5d7 | ||
|
|
6525df8c8a | ||
|
|
b2270d7d3d | ||
|
|
ec7622d264 | ||
|
|
72cf0da6fb | ||
|
|
e3a0f19fff | ||
|
|
1c03f7990a | ||
|
|
d191085117 | ||
|
|
1a4efb79a1 | ||
|
|
c36090f3a1 | ||
|
|
593b3083c4 | ||
|
|
cb2167600a | ||
|
|
d375989b28 | ||
|
|
862b6ebfee | ||
|
|
bc9688f1bb | ||
|
|
7d777ca739 | ||
|
|
e19c059a1b | ||
|
|
94a8c9ad2a | ||
|
|
a4e909fe1a | ||
|
|
9550632e2c | ||
|
|
fa9bca45fb | ||
|
|
ea61a70953 | ||
|
|
31818a36e6 | ||
|
|
6055db482a | ||
|
|
4bd3c9936b | ||
|
|
7052f2b31d | ||
|
|
a80929c209 | ||
|
|
c13b5822dd | ||
|
|
c32b5ac837 | ||
|
|
d462795cb8 | ||
|
|
561338d5e6 | ||
|
|
6963bc9110 | ||
|
|
3cef9a8bd9 | ||
|
|
a43d0a0656 | ||
|
|
1efda76c7f | ||
|
|
ead8cf6101 | ||
|
|
9784707524 | ||
|
|
cf56af4e05 | ||
|
|
c0a0f2dd70 | ||
|
|
4a75716625 | ||
|
|
2ce4e761c5 | ||
|
|
49cd3c13bd | ||
|
|
ea47f8e31f | ||
|
|
651375ddb2 | ||
|
|
c23de2c56a | ||
|
|
78c4192d31 | ||
|
|
9d56a019ee | ||
|
|
6bc9b3459e | ||
|
|
18bf8165be | ||
|
|
2e1c4625c0 | ||
|
|
e208e1066e | ||
|
|
c0ffc15b76 | ||
|
|
339759c8f1 | ||
|
|
6c9a8495a1 | ||
|
|
351cff9d95 | ||
|
|
184b71956d | ||
|
|
ca33a6c8a9 | ||
|
|
0008320ac6 | ||
|
|
3c9e004ded | ||
|
|
da9b901457 | ||
|
|
1a2f5e5828 | ||
|
|
276bd54f45 | ||
|
|
610770d227 | ||
|
|
413f608ed4 | ||
|
|
a07c39b277 | ||
|
|
a5effa54a3 | ||
|
|
182f16b76f | ||
|
|
0f85b397da | ||
|
|
af4d6c3731 | ||
|
|
7962c6859a | ||
|
|
9d74c6a718 | ||
|
|
4de2cd7d95 | ||
|
|
fa686ee858 | ||
|
|
26adc8db3d | ||
|
|
a188258633 | ||
|
|
96d63a1ec9 | ||
|
|
711f346a44 | ||
|
|
628bf60c55 | ||
|
|
48b9418307 | ||
|
|
5984c6cc29 | ||
|
|
6aa5e4b2a9 | ||
|
|
30c5f49647 | ||
|
|
4cfad26915 | ||
|
|
ad728a48ed | ||
|
|
3b1a14190e | ||
|
|
d490c1297d | ||
|
|
75e58e3ddc | ||
|
|
4f1f5b8a47 | ||
|
|
27bd9ffcc6 | ||
|
|
cc8093aad9 | ||
|
|
b2ee70ee74 | ||
|
|
f2cb13c39b | ||
|
|
4abd1128fe | ||
|
|
1fa3c45d1b | ||
|
|
6311832079 | ||
|
|
840f52077a | ||
|
|
5e0a47f936 | ||
|
|
cd97da3c41 | ||
|
|
acd9bf6a94 | ||
|
|
10ef56fdfa | ||
|
|
8cdaeb049a | ||
|
|
b481ed6668 | ||
|
|
480c3a30be | ||
|
|
3ea9abaca0 | ||
|
|
31f8291895 | ||
|
|
aa55a5bfd6 | ||
|
|
fe09be743f | ||
|
|
35665e7aac | ||
|
|
53dbf7083e | ||
|
|
a1103711db | ||
|
|
4015986310 | ||
|
|
0d32ea6bc4 | ||
|
|
b9e2d38bb6 | ||
|
|
2532c37d15 | ||
|
|
c31958aa17 | ||
|
|
02abfb75fc | ||
|
|
49fcc6522c | ||
|
|
1ce5b6a311 | ||
|
|
8715a7215e | ||
|
|
e5a85b5fb8 | ||
|
|
fc5afad35d | ||
|
|
89bc4aff6c | ||
|
|
0a93a6569e | ||
|
|
66f13b7051 | ||
|
|
534d8eac6b | ||
|
|
172fc603ed | ||
|
|
9f2962253f | ||
|
|
41b211c0db | ||
|
|
9b4b1c9712 | ||
|
|
ea42a4077c | ||
|
|
b62a23d521 | ||
|
|
4460b53e1f | ||
|
|
12c91a2780 | ||
|
|
806204c732 | ||
|
|
77801a9eb7 | ||
|
|
93a3c5f958 | ||
|
|
20d4eb1c44 | ||
|
|
25474c6c07 | ||
|
|
0098f21243 | ||
|
|
caf642f311 | ||
|
|
0f2c0b3fd2 | ||
|
|
6092ee1ff1 | ||
|
|
35669c9f71 | ||
|
|
5918b24d72 | ||
|
|
f1abf15a1a | ||
|
|
e6ac381e07 | ||
|
|
18167b811c | ||
|
|
43e3b6ebee | ||
|
|
1d7af251ad | ||
|
|
391f8c9e8b | ||
|
|
b2de6d8cd8 | ||
|
|
86df142ed0 | ||
|
|
ed87925a68 | ||
|
|
ef61614f60 | ||
|
|
2cb4b77f40 | ||
|
|
58a551f338 | ||
|
|
a4450fa8b2 | ||
|
|
7287ae758a | ||
|
|
81f59ec775 | ||
|
|
21ed5f20e9 | ||
|
|
0b3da17c11 | ||
|
|
577e3b997b | ||
|
|
1c8e3f3cec | ||
|
|
331df3587b | ||
|
|
94d9ea29a4 | ||
|
|
52ee3ac163 | ||
|
|
d8d8eafc52 | ||
|
|
8a144112cc | ||
|
|
14f7f797be | ||
|
|
09ad598a12 | ||
|
|
c5f735c989 | ||
|
|
829fe98403 | ||
|
|
a189bb24f1 | ||
|
|
aaae97992d | ||
|
|
216bf94577 | ||
|
|
925cba060c | ||
|
|
b75f5899e1 | ||
|
|
4436bc4b19 | ||
|
|
8364149953 | ||
|
|
45fd2a6fac | ||
|
|
b78d68db32 | ||
|
|
e8c49a446a | ||
|
|
c7f0295d6d | ||
|
|
6fd3f52f05 | ||
|
|
e69eb8f172 | ||
|
|
9a627b2f5e | ||
|
|
9a1d52cb4f | ||
|
|
7a159a049c | ||
|
|
b6f4f4f2f3 | ||
|
|
c4db8841ab | ||
|
|
80ae2d4980 | ||
|
|
808ccb93cf | ||
|
|
2cab7ec926 | ||
|
|
06c2ab093d | ||
|
|
d002f2fd10 | ||
|
|
76b804f725 |
+3
-122
@@ -2,139 +2,21 @@ env:
|
||||
CIRRUS_CLONE_DEPTH: 50
|
||||
ELIXIR_ASSERT_TIMEOUT: 2000
|
||||
ELIXIRC_OPTS: "--warnings-as-errors"
|
||||
ERLC_OPTS: "+warnings_as_errors"
|
||||
ERLC_OPTS: "warnings_as_errors"
|
||||
LANG: C.UTF-8
|
||||
|
||||
test_template: &DEFAULT_TEST_SETTINGS
|
||||
# don't cancel the task execution if it's master or a release branch
|
||||
auto_cancellation: $CIRRUS_BRANCH != 'master' && $CIRRUS_BRANCH !=~ 'v\d+\.\d+.*'
|
||||
|
||||
test_linux_task:
|
||||
<<: *DEFAULT_TEST_SETTINGS
|
||||
|
||||
container:
|
||||
image: buildpack-deps:trusty
|
||||
cpu: 8
|
||||
memory: 1536Mi
|
||||
|
||||
env:
|
||||
PATH: "${CIRRUS_WORKING_DIR}/otp/bin:${PATH}"
|
||||
|
||||
matrix:
|
||||
- name: Linux, ${OTP_RELEASE}, Ubuntu 14.04
|
||||
alias: Linux Stable
|
||||
matrix:
|
||||
- env:
|
||||
CHECK_POSIX_COMPLIANT: true
|
||||
CHECK_REPRODUCIBLE: true
|
||||
OTP_RELEASE: OTP-22.1
|
||||
- env:
|
||||
OTP_RELEASE: OTP-22.0
|
||||
- env:
|
||||
OTP_RELEASE: OTP-21.3.8
|
||||
- env:
|
||||
OTP_RELEASE: OTP-21.2
|
||||
- env:
|
||||
OTP_RELEASE: OTP-21.1
|
||||
- env:
|
||||
OTP_RELEASE: OTP-21.0
|
||||
|
||||
- name: Linux, OTP-${OTP_RELEASE}, development, Ubuntu 14.04
|
||||
alias: Linux Development
|
||||
allow_failures: true
|
||||
skip_notifications: true
|
||||
depends_on:
|
||||
- Linux Stable
|
||||
- FreeBSD Stable
|
||||
matrix:
|
||||
- env:
|
||||
OTP_RELEASE: master
|
||||
- env:
|
||||
OTP_RELEASE: maint
|
||||
|
||||
install_script:
|
||||
- wget -O otp.tar.gz https://repo.hex.pm/builds/otp/ubuntu-14.04/${OTP_RELEASE}.tar.gz
|
||||
- mkdir -p otp
|
||||
- tar zxf otp.tar.gz -C otp --strip-components=1
|
||||
- otp/Install -minimal ${CIRRUS_WORKING_DIR}/otp
|
||||
- rm -rf .git
|
||||
- make compile
|
||||
|
||||
build_info_script: bin/elixir --version
|
||||
|
||||
test_formatted_script:
|
||||
- make test_formatted &&
|
||||
echo "All Elixir source code files are properly formatted."
|
||||
|
||||
dialyzer_script: dialyzer -pa lib/elixir/ebin --build_plt --output_plt elixir.plt --apps lib/elixir/ebin/elixir.beam lib/elixir/ebin/Elixir.Kernel.beam
|
||||
|
||||
test_erlang_script: make test_erlang
|
||||
|
||||
test_elixir_script: make test_elixir
|
||||
|
||||
check_posix_compliant_script: |
|
||||
if [ -n "$CHECK_POSIX_COMPLIANT" ]; then
|
||||
apt update
|
||||
apt install -y shellcheck
|
||||
shellcheck -e SC2039,2086 bin/elixir && echo "bin/elixir is POSIX compliant"
|
||||
shellcheck bin/elixirc && echo "bin/elixirc is POSIX compliant"
|
||||
shellcheck bin/iex && echo "bin/iex is POSIX compliant"
|
||||
else
|
||||
echo "The format of the shell scripts is only checked in the last stable Erlang/OTP version."
|
||||
fi
|
||||
|
||||
check_reproducible_script: |
|
||||
if [ -n "$CHECK_REPRODUCIBLE" ]; then
|
||||
make check_reproducible
|
||||
else
|
||||
echo "The reproducibility of the build is only checked in the last stable Erlang/OTP version."
|
||||
fi
|
||||
|
||||
|
||||
test_windows_task:
|
||||
<<: *DEFAULT_TEST_SETTINGS
|
||||
|
||||
name: Windows, OTP-${OTP_RELEASE}, Windows Server 2019
|
||||
alias: Windows Stable
|
||||
|
||||
matrix:
|
||||
- env:
|
||||
OS_VERSION: 2019
|
||||
OTP_RELEASE: 22.0
|
||||
|
||||
- env:
|
||||
OS_VERSION: 2019
|
||||
OTP_RELEASE: 21.0.1
|
||||
|
||||
windows_container:
|
||||
image: fertapric/elixir-ci:otp-win64-${OTP_RELEASE}
|
||||
os_version: ${OS_VERSION}
|
||||
cpu: 4
|
||||
memory: 3840Mi
|
||||
|
||||
install_script:
|
||||
- rmdir /s /q .git
|
||||
- make compile
|
||||
|
||||
build_info_script: bin/elixir --version
|
||||
|
||||
test_formatted_script:
|
||||
- make test_formatted &&
|
||||
echo "All Elixir source code files are properly formatted."
|
||||
|
||||
test_erlang_script: make --keep-going test_erlang
|
||||
|
||||
test_elixir_script: make --keep-going test_elixir
|
||||
|
||||
|
||||
test_freebsd_task:
|
||||
<<: *DEFAULT_TEST_SETTINGS
|
||||
|
||||
name: FreeBSD 12.0
|
||||
name: FreeBSD 12.1
|
||||
alias: FreeBSD Stable
|
||||
|
||||
freebsd_instance:
|
||||
image_family: freebsd-12-0
|
||||
image_family: freebsd-12-1
|
||||
cpu: 8
|
||||
memory: 7424Mi
|
||||
|
||||
@@ -143,7 +25,6 @@ test_freebsd_task:
|
||||
LC_ALL: en_US.UTF-8
|
||||
|
||||
install_script:
|
||||
- sudo pkg update
|
||||
- pkg install -y erlang git gmake
|
||||
- rm -rf .git
|
||||
- gmake compile
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
lib/elixir/test/elixir/fixtures/*.txt text eol=lf
|
||||
*.ex diff=elixir
|
||||
*.exs diff=elixir
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
name: CI
|
||||
|
||||
on: [pull_request, push]
|
||||
|
||||
env:
|
||||
ELIXIR_ASSERT_TIMEOUT: 2000
|
||||
ELIXIRC_OPTS: "--warnings-as-errors"
|
||||
ERLC_OPTS: "warnings_as_errors"
|
||||
LANG: C.UTF-8
|
||||
|
||||
jobs:
|
||||
test_linux:
|
||||
name: Linux, ${{ matrix.otp_release }}, Ubuntu 16.04
|
||||
continue-on-error: ${{ matrix.development }}
|
||||
strategy:
|
||||
matrix:
|
||||
otp_release: ['OTP-23.0', 'OTP-22.3', 'OTP-22.0', 'OTP-21.3.8', 'OTP-21.0']
|
||||
development: [false]
|
||||
include:
|
||||
- otp_release: master
|
||||
development: true
|
||||
- otp_release: maint
|
||||
development: true
|
||||
runs-on: ubuntu-16.04
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
with:
|
||||
fetch-depth: 50
|
||||
- name: Install Erlang/OTP
|
||||
run: |
|
||||
cd $RUNNER_TEMP
|
||||
wget -O otp.tar.gz https://repo.hex.pm/builds/otp/ubuntu-14.04/${{ matrix.otp_release }}.tar.gz
|
||||
mkdir -p otp
|
||||
tar zxf otp.tar.gz -C otp --strip-components=1
|
||||
otp/Install -minimal $(pwd)/otp
|
||||
echo "::add-path::$(pwd)/otp/bin"
|
||||
- name: Compile Elixir
|
||||
run: |
|
||||
rm -rf .git
|
||||
make compile
|
||||
- name: Build info
|
||||
run: bin/elixir --version
|
||||
- name: Check format
|
||||
run: make test_formatted && echo "All Elixir source code files are properly formatted."
|
||||
- name: Dyalizer
|
||||
run: dialyzer -pa lib/elixir/ebin --build_plt --output_plt elixir.plt --apps lib/elixir/ebin/elixir.beam lib/elixir/ebin/Elixir.Kernel.beam
|
||||
- name: Erlang test suite
|
||||
run: make test_erlang
|
||||
- name: Elixir test suite
|
||||
run: make test_elixir
|
||||
- name: Check reproducible builds
|
||||
run: taskset 1 make check_reproducible
|
||||
if: matrix.otp_release == 'OTP-23.0'
|
||||
|
||||
test_windows:
|
||||
name: Windows, OTP-${{ matrix.otp_release }}, Windows Server 2019
|
||||
strategy:
|
||||
matrix:
|
||||
otp_release: ['22.0']
|
||||
runs-on: windows-2019
|
||||
steps:
|
||||
- name: Configure Git
|
||||
run: git config --global core.autocrlf input
|
||||
- uses: actions/checkout@v1
|
||||
with:
|
||||
fetch-depth: 50
|
||||
- name: Cache Erlang/OTP package
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: C:\Users\runneradmin\AppData\Local\Temp\chocolatey\erlang
|
||||
key: OTP-${{ matrix.otp_release }}-windows-2019
|
||||
- name: Install Erlang/OTP
|
||||
run: choco install -y erlang --version ${{ matrix.otp_release }}
|
||||
- name: Compile Elixir
|
||||
run: |
|
||||
remove-item '.git' -recurse -force
|
||||
make compile
|
||||
- name: Build info
|
||||
run: bin/elixir --version
|
||||
- name: Check format
|
||||
run: make test_formatted && echo "All Elixir source code files are properly formatted."
|
||||
- name: Erlang test suite
|
||||
run: make --keep-going test_erlang
|
||||
- name: Elixir test suite
|
||||
run: make --keep-going test_elixir
|
||||
|
||||
check_posix_compliant:
|
||||
name: Check POSIX-compliant
|
||||
runs-on: ubuntu-16.04
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
with:
|
||||
fetch-depth: 50
|
||||
- name: Install Shellcheck
|
||||
run: |
|
||||
sudo apt update
|
||||
sudo apt install -y shellcheck
|
||||
- name: Check POSIX-compliant
|
||||
run: |
|
||||
shellcheck -e SC2039,2086 bin/elixir && echo "bin/elixir is POSIX compliant"
|
||||
shellcheck bin/elixirc && echo "bin/elixirc is POSIX compliant"
|
||||
shellcheck bin/iex && echo "bin/iex is POSIX compliant"
|
||||
@@ -1,17 +0,0 @@
|
||||
on: check_suite
|
||||
name: CI email
|
||||
jobs:
|
||||
sendEmail:
|
||||
name: Send email
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Send email
|
||||
# Source: https://github.com/elixir-lang/elixir-ci
|
||||
uses: docker://fertapric/elixir-ci-email:latest
|
||||
env:
|
||||
APP_NAME: Cirrus CI
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
MAIL_FROM: ci@elixir-lang.org
|
||||
MAIL_HOST: smtp.sendgrid.net
|
||||
MAIL_PASSWORD: ${{ secrets.CI_EMAIL_PASSWORD }}
|
||||
MAIL_USERNAME: ${{ secrets.CI_EMAIL_USERNAME }}
|
||||
+347
-135
@@ -1,222 +1,434 @@
|
||||
# Changelog for Elixir v1.10
|
||||
# Changelog for Elixir v1.11
|
||||
|
||||
## Support for Erlang/OTP 21+
|
||||
Over the last releases, the Elixir team has been focusing on the compiler, both in terms of catching more mistakes at compilation time and making it faster. Elixir v1.11 has made excellent progress on both fronts. This release also includes many other goodies, such as tighter Erlang integration, support for more guard expressions, built-in datetime formatting, and other calendar enhancements.
|
||||
|
||||
Elixir v1.10 requires Erlang/OTP 21+, allowing Elixir to integrate with Erlang/OTP's new logger. Currently, this means that the logger level, logger metadata, as well as all log messages are now shared between Erlang and Elixir APIs.
|
||||
## Tighter Erlang integration
|
||||
|
||||
We will continue improving the relationship between the logging systems in future releases. In particular, we plan to expose all log levels and runtime filtering functionalities available in Erlang directly into Elixir in the next Elixir version.
|
||||
Following Elixir v1.10, we have further integrated with Erlang's new logger by adding four new log levels: `notice`, `critical`, `alert`, and `emergency`, matching all log levels found in the Syslog standard. The `Logger` module now supports structured logging by passing maps and keyword lists to its various functions. It is also possible to specify the log level per module, via the `Logger.put_module_level/2` function. Log levels per application will be added in future releases.
|
||||
|
||||
This release also adds two new guards, `is_struct/1` and `is_map_key/2`, thanks to the strict requirement on Erlang/OTP 21+.
|
||||
IEx also has been improved to show the documentation for Erlang modules directly from your Elixir terminal. This works with Erlang/OTP 23+ and requires Erlang modules to have been compiled with documentation chunks.
|
||||
|
||||
## Releases improvements
|
||||
## Compiler checks: application boundaries
|
||||
|
||||
Elixir v1.9 introduced releases as a mechanism to package self-contained applications. Elixir v1.10 further improves releases with bug fixes and new enhancements based on feedback we got from the community. The highlights are:
|
||||
Elixir v1.11 builds on top of the recently added compilation tracers to track application boundaries. From this release, Elixir will warn if you invoke a function from an existing module but this module does not belong to any of your listed dependencies.
|
||||
|
||||
* Allow the dual boot system of releases to be disabled on environments that are boot-time sensitive, such as embedded devices
|
||||
These two conditions may seem contradictory. After all, if a module is available, it must have come from a dependency. This is not true in two scenarios:
|
||||
|
||||
* Track and raise if compile-time configuration is set or changes at runtime (more in the next section)
|
||||
* Modules from Elixir and Erlang/OTP are always available - even if their applications are not explicitly listed as a dependency
|
||||
|
||||
* Support for easily adding extra files to releases via overlays
|
||||
* In an umbrella project, because all child applications are compiled within the same VM, you may have a module from a sibling project available, even if you don't depend on said sibling
|
||||
|
||||
* Allow `RELEASE_DISTRIBUTION` to be set to `none` in order to fully disable it
|
||||
This new compiler check makes sure that all modules that you invoke are listed as part of your dependencies, emitting a warning like below otherwise:
|
||||
|
||||
* Add a built-in `:tar` step that automatically packages releases
|
||||
```text
|
||||
:ssl.connect/2 defined in application :ssl is used by the current
|
||||
application but the current application does not directly depend
|
||||
on :ssl. To fix this, you must do one of:
|
||||
|
||||
See the full CHANGELOG for more improvements.
|
||||
1. If :ssl is part of Erlang/Elixir, you must include it under
|
||||
:extra_applications inside "def application" in your mix.exs
|
||||
|
||||
## Improvements to sort-based APIs in Enum
|
||||
2. If :ssl is a dependency, make sure it is listed under "def deps"
|
||||
in your mix.exs
|
||||
|
||||
`Enum.sort/1` in Elixir always sorts from lowest to highest. If you want to sort from highest to lowest, you need to call `Enum.sort/2` with a custom sorting function, such as `Enum.sort(collection, &>=/2)`, which is not immediately obvious to someone reading the code.
|
||||
3. In case you don't want to add a requirement to :ssl, you may
|
||||
optionally skip this warning by adding [xref: [exclude: :ssl]
|
||||
to your "def project" in mix.exs
|
||||
```
|
||||
|
||||
To make matters worse, comparison operators, such as `<=` and `>=`, perform structural sorting, instead of a semantic one. For example, using `>=` to sort dates descendingly won't yield the correct result. Therefore, to sort dates from more recent to oldest, one has to write `Enum.sort(dates, &(Date.compare(&1, &2) != :lt))`.
|
||||
This comes with extra benefits in umbrella projects, as it requires child applications to explicitly list their dependencies, completely rejecting cyclic dependencies between siblings.
|
||||
|
||||
Elixir v1.10 streamlines the sorting functions by introducing both `:asc` and `:desc` shortcuts:
|
||||
## Compiler checks: data constructors
|
||||
|
||||
Enum.sort(collection, :asc) # the default
|
||||
Enum.sort(collection, :desc) # in reverse
|
||||
In Elixir v1.11, the compiler also tracks structs and maps fields across a function body. For example, imagine you wanted to write this code:
|
||||
|
||||
Furthermore, if you want to perform semantic comparison, you can pass a module that provides the relevant comparison function. For example, to sort dates:
|
||||
def drive?(%User{age: age}), do: age >= 18
|
||||
|
||||
Enum.sort(birth_dates, Date)
|
||||
Enum.sort(birth_dates, {:asc, Date})
|
||||
Enum.sort(birth_dates, {:desc, Date})
|
||||
If there is either a typo on the `:age` field or the `:age` field was not yet defined, the compiler will fail accordingly. However, if you wrote this code:
|
||||
|
||||
This new API has also been added to `Enum.sort_by`, `Enum.min_by`, `Enum.max_by`, and friends.
|
||||
def drive?(%User{} = user), do: user.age >= 18
|
||||
|
||||
### Tracking of compile-time configuration
|
||||
The compiler would not catch the missing field and an error would only be raised at runtime. With v1.11, Elixir will track the usage of all maps and struct fields within the same function, emitting warnings for cases like above:
|
||||
|
||||
All applications in Elixir come with an application environment. This environment is a key-value store that allows us to configure said application. While reading the application environment at runtime is the preferred approach, in some rare occasions you may want to use the application environment to configure the compilation of a certain project. This is often done by calling `Application.get_env/3` outside of a function:
|
||||
```text
|
||||
warning: undefined field `age` in expression:
|
||||
|
||||
defmodule MyApp.DBClient do
|
||||
@db_host Application.get_env(:my_app, :db_host, "db.local")
|
||||
# example.exs:7
|
||||
user.age
|
||||
|
||||
def start_link() do
|
||||
SomeLib.DBClient.start_link(host: @db_host)
|
||||
end
|
||||
expected one of the following fields: name, address
|
||||
|
||||
where "user" was given the type %User{} in:
|
||||
|
||||
# example.exs:7
|
||||
%User{} = user
|
||||
|
||||
Conflict found at
|
||||
example.exs:7: Check.drive?/1
|
||||
```
|
||||
|
||||
The compiler also checks binary constructors. Consider you have to send a string over the wire with length-based encoding, where the string is prefixed by its length, up to 4MBs. Your initial attempt may be this:
|
||||
|
||||
def run_length(string) when is_binary(string) do
|
||||
<<byte_size(string)::32, string>>
|
||||
end
|
||||
|
||||
This approach has one big limitation: if you change the value of the application environment after the code is compiled, the value used at runtime is not going to change! For example, if you are using `mix release` and your `config/releases.exs` has:
|
||||
However, the code above has a bug. Each segment given between `<<>>` must be an integer, unless specified otherwise. With Elixir v1.11, the compiler will let you know so:
|
||||
|
||||
config :my_app, :db_host, "db.production"
|
||||
```text
|
||||
warning: incompatible types:
|
||||
|
||||
The new value will have no effect as the code was compiled to connect to "db.local", which is mostly likely unavailable in the production environment.
|
||||
binary() !~ integer()
|
||||
|
||||
For those reasons, reading the application environment at runtime should be the first choice. However, if you really have to read the application environment during compilation, Elixir v1.10 introduces a `Application.compile_env/3` function:
|
||||
in expression:
|
||||
|
||||
@db_host Application.compile_env(:my_app, :db_host, "db.local")
|
||||
<<byte_size(string)::integer()-size(32), string>>
|
||||
|
||||
By using `compile_env/3`, Elixir will store the values used during compilation and compare the compilation values with the runtime values whenever your system starts, raising an error in case they differ. This helps developers ensure they are running their production systems with the configuration they intend to.
|
||||
where "string" was given the type integer() in:
|
||||
|
||||
### Compiler tracing
|
||||
# foo.exs:4
|
||||
<<byte_size(string)::integer()-size(32), string>>
|
||||
|
||||
This release brings enhancements to the Elixir compiler and adds new capabilities for developers to listen to compilation events.
|
||||
where "string" was given the type binary() in:
|
||||
|
||||
In previous Elixir releases, Elixir would compile a database of cross references between modules (such as function calls, references, structs, etc) for each project. Although developers could traverse this database, they often requested more events or more information to be made available.
|
||||
# foo.exs:3
|
||||
is_binary(string)
|
||||
|
||||
In Elixir v1.10, we have replaced this database by compiler tracing. This means that developers can now directly listen to events emitted by the compiler to store and collect all the information they need (and only the information they need).
|
||||
HINT: all expressions given to binaries are assumed to be of type integer()
|
||||
unless said otherwise. For example, <<expr>> assumes "expr" is an integer.
|
||||
Pass a modifier, such as <<expr::float>> or <<expr::binary>>, to change the
|
||||
default behaviour.
|
||||
|
||||
Elixir itself is already using the new compiler tracing to provide new functionality. In particular, the compiler now checks for undefined function warnings more consistently. In previous versions, we would emit undefined function warnings only for files in `lib`, skipping test files and scripts.
|
||||
Conflict found at
|
||||
foo.exs:4: Check.run_length/1
|
||||
```
|
||||
|
||||
Furthermore, in Elixir v1.10 developers can now disable undefined function warnings directly on the callsite. For example, imagine you have an optional dependency which may not be available in some cases. You can tell the compiler to skip warning on calls to optional modules with:
|
||||
Which can be fixed by adding `::binary` to the second component:
|
||||
|
||||
@compile {:no_warn_undefined, OptionalDependency}
|
||||
defdelegate my_function_call(arg), to: OptionalDependency
|
||||
def run_length(string) when is_binary(string) do
|
||||
<<byte_size(string)::32, string::binary>>
|
||||
end
|
||||
|
||||
Finally, as consequence of these improvements, some functionality related to `xref` (our previous database), has been deprecated in favor of the new compiler tracing.
|
||||
While some of those warnings could be automatically fixed by the compiler, future versions will also perform those checks across functions and potentially across modules, where automatic fixes wouldn't be desired (nor possible).
|
||||
|
||||
### Other enhancements
|
||||
## Compilation time improvements
|
||||
|
||||
The calendar data types got many improvements, such as sigil support for third-party calendars, as well as the additions of `DateTime.now!/2`, `DateTime.shift_zone!/3`, and `NaiveDateTime.local_now/0`.
|
||||
Elixir v1.11 features many improvements to how the compiler tracks file dependencies, such that touching one file causes less files to be recompiled. In previous versions, Elixir tracked three types of dependencies:
|
||||
|
||||
There are many improvements related to the Elixir AST in this release too. First of all, `Code.string_to_quoted/2` has two new options, `:token_metadata` and `:literal_encoder`, that give more control over Elixir's parser. This information has already been available to the Elixir formatter for a couple versions and has now been made public. Furthermore, all public metadata entries in the AST nodes have been extensively documented. These changes alongside the compiler improvements from previous section means tools like Credo and Boundary now have a better foundation to analyze the source code.
|
||||
* compile time dependencies - if A depends on B at compile time, such as by using a macro, whenever B changes, A is recompiled
|
||||
* struct dependencies - if A depends on B's struct, whenever B's struct definition changed, A is recompiled
|
||||
* runtime dependencies - if A depends on B at runtime, A is never recompiled
|
||||
|
||||
Finally, ExUnit comes with two small but important improvements: `ExUnit.CaptureIO` can now be used in tests that run asynchronously and we have added "data-structure diffing" when performing assertions with pattern matching. So now, whenever an assertion such `assert %{field: value} = expression()` fails, ExUnit will show both left-hand and right-hand sides, highlighting the parts that did not match in red.
|
||||
However, because dependencies are transitive, if A depends on B at compile time and B depends on C at runtime, A would depend on C at compile time. Therefore, it is very important to reduce the amount of compile time dependencies.
|
||||
|
||||
## v1.10.0
|
||||
Elixir v1.11 replaces "struct dependencies" by "exports dependencies". In other words, if A depends on B, whenever B public's interface changes, A is recompiled. B's public interface is made by its struct definition and all of its public functions and macros.
|
||||
|
||||
### 1. Enhancements
|
||||
This change allows us to mark `import`s and `require`s as "exports dependencies" instead of "compile time" dependencies. This simplifies the dependency graph considerably. For example, [in the Hex.pm project](https://github.com/hexpm/hexpm), changing the `user.ex` file in Elixir v1.10 would emit this:
|
||||
|
||||
```text
|
||||
$ touch lib/hexpm/accounts/user.ex && mix compile
|
||||
Compiling 90 files (.ex)
|
||||
```
|
||||
|
||||
In Elixir v1.11, we now get:
|
||||
|
||||
```text
|
||||
$ touch lib/hexpm/accounts/user.ex && mix compile
|
||||
Compiling 16 files (.ex)
|
||||
```
|
||||
|
||||
To make things even better, Elixir v1.11 also introduces a more granular file tracking for path dependencies. In previous versions, a module from a path dependency would always be treated as a compile time dependency. This often meant that if you have an umbrella project, changing an application would cause many modules in sibling applications to recompile. Fortunately, Elixir v1.11 will tag modules from dependencies as exports if appropriate, yielding dramatic improvements to those using path dependencies.
|
||||
|
||||
To round up the list of compiler enhancements, the `--profile=time` option added in Elixir v1.10 now also includes the time to compile each individual file. For example, in the Plug project, one can now get:
|
||||
|
||||
```text
|
||||
[profile] lib/plug/conn.ex compiled in 935ms
|
||||
[profile] lib/plug/ssl.ex compiled in 147ms (plus 744ms waiting)
|
||||
[profile] lib/plug/static.ex compiled in 238ms (plus 654ms waiting)
|
||||
[profile] lib/plug/csrf_protection.ex compiled in 237ms (plus 790ms waiting)
|
||||
[profile] lib/plug/debugger.ex compiled in 719ms (plus 947ms waiting)
|
||||
[profile] Finished compilation cycle of 60 modules in 1802ms
|
||||
[profile] Finished group pass check of 60 modules in 75ms
|
||||
```
|
||||
|
||||
While implementing those features, we have also made the `--long-compilation-threshold` flag more precise. In previous versions, `--long-compilation-threshold` would consider both the time a file spent to compile and the time spent waiting on other files. In Elixir v1.11, it considers only the compilation time. This means less false positives and you can now effectively get all files that take longer than 2s to compile by passing `--long-compilation-threshold 2`.
|
||||
|
||||
## `mix xref graph` improvements
|
||||
|
||||
To bring visibility to the compiler tracking improvements described in the previous section, we have also added new features to `mix xref`. `mix xref` is a task that describes cross-references between files in your projects. The `mix xref graph` subsection focuses on the dependency graph between them.
|
||||
|
||||
First we have made the existing `--label` flag to consider transitive dependencies. Using `--sink FILE` and `--label compile` can be a powerful combo to find out which files will change whenever the given `FILE` changes. For example, in the Hex.pm project, we get:
|
||||
|
||||
```text
|
||||
$ mix xref graph --sink lib/hexpm/accounts/user.ex --label compile
|
||||
lib/hexpm/billing/hexpm.ex
|
||||
└── lib/hexpm/billing/billing.ex (compile)
|
||||
lib/hexpm/billing/local.ex
|
||||
└── lib/hexpm/billing/billing.ex (compile)
|
||||
lib/hexpm/emails/bamboo.ex
|
||||
├── lib/hexpm/accounts/email.ex (compile)
|
||||
└── lib/hexpm/accounts/user.ex (compile)
|
||||
lib/hexpm/emails/emails.ex
|
||||
└── lib/hexpm_web/views/email_view.ex (compile)
|
||||
lib/hexpm_web/controllers/api/docs_controller.ex
|
||||
└── lib/hexpm_web/controllers/auth_helpers.ex (compile)
|
||||
lib/hexpm_web/controllers/api/key_controller.ex
|
||||
└── lib/hexpm_web/controllers/auth_helpers.ex (compile)
|
||||
lib/hexpm_web/controllers/api/organization_controller.ex
|
||||
└── lib/hexpm_web/controllers/auth_helpers.ex (compile)
|
||||
lib/hexpm_web/controllers/api/organization_user_controller.ex
|
||||
└── lib/hexpm_web/controllers/auth_helpers.ex (compile)
|
||||
lib/hexpm_web/controllers/api/owner_controller.ex
|
||||
└── lib/hexpm_web/controllers/auth_helpers.ex (compile)
|
||||
lib/hexpm_web/controllers/api/package_controller.ex
|
||||
└── lib/hexpm_web/controllers/auth_helpers.ex (compile)
|
||||
lib/hexpm_web/controllers/api/release_controller.ex
|
||||
└── lib/hexpm_web/controllers/auth_helpers.ex (compile)
|
||||
lib/hexpm_web/controllers/api/repository_controller.ex
|
||||
└── lib/hexpm_web/controllers/auth_helpers.ex (compile)
|
||||
lib/hexpm_web/controllers/api/retirement_controller.ex
|
||||
└── lib/hexpm_web/controllers/auth_helpers.ex (compile)
|
||||
lib/hexpm_web/controllers/blog_controller.ex
|
||||
└── lib/hexpm_web/views/blog_view.ex (compile)
|
||||
lib/hexpm_web/endpoint.ex
|
||||
├── lib/hexpm_web/plug_parser.ex (compile)
|
||||
└── lib/hexpm_web/session.ex (compile)
|
||||
```
|
||||
|
||||
All the files at the root will recompile if `lib/hexpm/accounts/user.ex` changes. Their children describe the *why*. For example, the `repository_controller.ex` file will recompile if user changes because it has a compile time dependency on `auth_helpers.ex`, which depends on `user.ex`. This indirect compile time dependency is often the source of recompilations and Elixir v1.11 now makes it trivial to spot them, so they can be eventually addressed.
|
||||
|
||||
Another improvement to `mix xref graph` is the addition of `--format cycles`, which will print all cycles in your compilation dependency graph. A `--min-cycle-size` flag can be used if you want to discard short cycles.
|
||||
|
||||
## `config/runtime.exs` and `mix app.config`
|
||||
|
||||
Elixir v1.9 introduced a new configuration file, specific to releases, called `config/releases.exs`. A release is a self-contained artifact with the Erlang VM, Elixir and your application, ready to run in production.
|
||||
|
||||
The addition of `config/releases.exs` has been a very useful one but, unfortunately, it applies only to releases. Developers not using releases must use the `config/config.exs` file, which often loaded too early at compilation time. For any dynamic configuration, developers had to resort to third-party tools or workarounds to achieve the desired results.
|
||||
|
||||
Elixir v1.11 addresses this issue by introducing a new configuration file, called `config/runtime.exs`. This new configuration file is loaded exactly before your application starts, when the code is already fully compiled. It is loaded in development, test, and production, regardless if you are using Mix or releases. Therefore it provides a unified API for runtime configuration in Elixir.
|
||||
|
||||
`config/runtime.exs` works the same as any other configuration file. However, given `config/runtime.exs` is meant to run with or without Mix, developers must not use `Mix.env()` or `Mix.target()` in `config/runtime.exs`. Instead, they must use the new `config_env()` and `config_target()`, which have been added to the `Config` module.
|
||||
|
||||
While `config/releases.exs` will continue to be supported, developers can migrate to `config/runtime.exs` without loss of functionality. For example, a `config/releases.exs` file such as this one
|
||||
|
||||
```elixir
|
||||
# config/releases.exs
|
||||
import Config
|
||||
|
||||
config :foo, ...
|
||||
config :bar, ...
|
||||
```
|
||||
|
||||
could run as is as `config/runtime.exs`. However, given `config/runtime.exs` runs in all environments, you may want to restrict part of your configuration to the `:prod` environment:
|
||||
|
||||
```elixir
|
||||
# config/runtime.exs
|
||||
import Config
|
||||
|
||||
if config_env() == :prod do
|
||||
config :foo, ...
|
||||
config :bar, ...
|
||||
end
|
||||
```
|
||||
|
||||
If both files are available, releases will pick the now preferred `config/runtime.exs` instead of `config/releases.exs`.
|
||||
|
||||
To wrap it all up, `Mix` also includes a new task called `mix app.config`. This task loads all applications and configures them, without starting them. Whenever you write your own Mix tasks, you will typically want to invoke either `mix app.start` or `mix app.config` before running your own code. Which one is better depends if you want your applications running or only configured.
|
||||
|
||||
## Other improvements
|
||||
|
||||
Elixir v1.11 adds the `is_struct/2`, `is_exception/1`, and `is_exception/2` guards. It also adds support for the `map.field` syntax in guards.
|
||||
|
||||
The Calendar module ships with a new `Calendar.strftime/3` function, which provides datetime formatting based on the `strftime` format. The `Date` module got new functions for working with weeks and months, such as `Date.beginning_of_month/1` and `Date.end_of_week/2`. Finally, all calendar types got conversion functions from and to gregorian timestamps, such as `Date.from_gregorian_days/2` and `NaiveDateTime.to_gregorian_seconds/1`.
|
||||
|
||||
Mix also includes two new tasks: `mix app.config`, for application runtime configuration, and `mix test.coverage`, which generates aggregated coverage reports for umbrella projects and for test suites partitioned across processes.
|
||||
|
||||
## v1.11.1 (2020-10-16)
|
||||
|
||||
### 1. Bug fixes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Application] Add `Application.compile_env/3` and `Application.compile_env!/2` for reading values at compilation time and tracking if they accidentally change during runtime
|
||||
* [Calendar] Allow custom calendar representations in calendar sigils
|
||||
* [Calendar] Add `c:Calendar.parse_time/1`, `c:Calendar.parse_date/1`, `c:Calendar.parse_naive_datetime/1` and `c:Calendar.parse_utc_datetime/1` callbacks to calendar behaviour
|
||||
* [CLI] Add support for `NO_COLOR` environment variable
|
||||
* [Code] Add `:token_metadata` and `:literal_encoder` support to `Code.string_to_quoted/2`
|
||||
* [Code] Add compiler tracing to lift events done by the compiler
|
||||
* [Code] Return `{:error, :unavailable}` in `Code.ensure_compiled/1` if module is in a deadlock
|
||||
* [DateTime] Add `DateTime.now!/2` and `DateTime.shift_zone!/3`
|
||||
* [Enum] Speed up getting one random element from enumerables
|
||||
* [Enum] Add `Enum.frequencies/1`, `Enum.frequencies_by/2`, and `Enum.map_intersperse/2`
|
||||
* [Enum] Allow a sorting function on `Enum.min/max/min_by/max_by`
|
||||
* [Enum] Add `asc/desc` and `compare/1` support to `Enum.sort/2`
|
||||
* [Exception] Add version alongside app names in stacktraces
|
||||
* [Function] Add `Function.identity/1`
|
||||
* [Kernel] Add `Kernel.is_struct/1` and `Kernel.is_map_key/2`
|
||||
* [Kernel] Warn when function head comes immediately after the implementation instead of before the implementation
|
||||
* [Kernel] Warn if duplicate key is found in struct declaration
|
||||
* [Kernel] Print all undefined functions as warnings and then raise. This allows users to see all undefined calls at once, when it would otherwise require them to compile the code multiple times
|
||||
* [Keyword] Add `Keyword.pop!/2` and `Keyword.pop_values/2`
|
||||
* [Map] Add `Map.pop!/2`
|
||||
* [MapSet] Optimize multiple operations
|
||||
* [Module] Add `Module.has_attribute?/2`
|
||||
* [Module] Add `@compile {:no_warn_undefined, mfa_or_module}` to turn off undefined function warnings
|
||||
* [NaiveDateTime] Add `NaiveDateTime.local_now/0`
|
||||
* [Record] Warn if duplicate key is found in record declaration
|
||||
* [String] Update to Unicode 12.1
|
||||
* [StringIO] Add `:encoding` option to StringIO and optimize `get_chars` operation
|
||||
|
||||
#### ExUnit
|
||||
|
||||
* [ExUnit.Assertions] Support diffs in pattern matching and in `assert_receive`
|
||||
* [ExUnit.CaptureIO] Supports capturing named devices in asynchronous tests
|
||||
* [Code] Ignore tracers if lexical tracker is dead or explicitly nil when evaling code with an environment
|
||||
* [GenServer] Do not show warning when using `super` in `GenServer.child_spec/1`
|
||||
* [Kernel] Do not crash when :reduce is set to `nil` in comprehensions
|
||||
* [Kernel] Fix a scenario where undefined function warnings were not being emitted
|
||||
|
||||
#### IEx
|
||||
|
||||
* [IEx] Warn on circular file imports when loading default `.iex.exs`
|
||||
* [IEx] Allow customization of the continuation prompt on IEx
|
||||
* [IEx.Helpers] Properly handle tags inside typespec when showing Erlang docs
|
||||
|
||||
#### Logger
|
||||
|
||||
* [Logger] Allow `start_options` to be configured on Logger's GenEvent
|
||||
* [Logger] Integrate Elixir's Logger with Erlang/OTP 21+'s logger. This means setting up the logger level in Elixir will automatically change the logger level for Erlang and vice-versa
|
||||
* [Logger] Do not deadlock Logger if handler crashes on sync mode
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix compile] Add `--profile time` flag to profile compilation steps
|
||||
* [mix deps.compile] Add `--skip-umbrella-apps` flag. The new flag does not compile umbrella apps. This is useful for building caches in CD/CI pipelines
|
||||
* [mix deps.unlock] Add `--check-unused` flag. The new flag raises if there are any unused dependencies in the lock file
|
||||
* [mix release] Allow `RELEASE_DISTRIBUTION` to be set to `none`
|
||||
* [mix release] Support overlays in `rel/overlays`
|
||||
* [mix release] Allow configuration reboot to be disabled in releases
|
||||
* [mix test] Add support for simple round-robin test partitioning across multiple machines
|
||||
* [Mix.Project] Add `MIX_DEPS_PATH` environment variable for setting `:deps_path`
|
||||
* [Mix.Project] Add `Mix.Project.deps_scms/1` that returns deps with their SCMs
|
||||
* [Mix.Task] Add `Mix.Task.Compiler.after_compiler/2` callback, to simplify compilers that may need to run something at multiple steps
|
||||
* [Mix] Add inet6 fallback to Mix usage of httpc
|
||||
* [mix compile.app] Do not list apps that do not match the current target
|
||||
|
||||
## v1.11.0 (2020-10-06)
|
||||
|
||||
### 1. Enhancements
|
||||
|
||||
#### EEx
|
||||
|
||||
* [EEx] Track column information in EEx templates when enabled in the compiler
|
||||
* [EEx] Show column information in EEx error messages
|
||||
* [EEx] Support `:indentation` option when compiling EEx templates for proper column tracking
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Access] Add `Access.at!/1`
|
||||
* [Calendar] Add `Calendar.strftime/3` for datetime formatting
|
||||
* [Calendar] Add linear integer representations to Calendar modules: `Date.from_gregorian_days/2`, `Date.to_gregorian_days/1`, `NaiveDateTime.from_gregorian_seconds/3`, `NaiveDateTime.to_gregorian_seconds/1`, `Time.from_seconds_after_midnight/1`, and `Time.to_seconds_after_midnight/1`
|
||||
* [Calendar] Add `new!` to Date/Time/NaiveDateTime/DateTime (`new` has also been added to `DateTime` for completeness)
|
||||
* [Calendar] Support custom starting day of the week in `Date.day_of_week/2`
|
||||
* [Calendar] Add `Date.beginning_of_month/1` and `Date.end_of_month/1`
|
||||
* [Calendar] Add `Date.beginning_of_week/2` and `Date.end_of_week/2`
|
||||
* [Code] Add `:column` to `Code.string_to_quoted*/2`
|
||||
* [Code] Add `Code.can_await_module_compilation?/0` to check if the parallel compiler is enabled and it can await for other modules to be compiled
|
||||
* [Config] Support `config_env/0` and `config_target/0` in `config` files
|
||||
* [Config] Allow `import_config` to be disabled for some configuration files
|
||||
* [Enum] Allow a sorting function on `Enum.min_max_by/3,4`, including the new `compare/2` conventions
|
||||
* [Kernel] Add `is_struct/2` guard
|
||||
* [Kernel] Add `is_exception/1` and `is_exception/2` guards
|
||||
* [Kernel] Support `map.field` syntax in guards
|
||||
* [Kernel] Add `+++` and `---` with right associativity to the list of custom operators
|
||||
* [Kernel] Warn if a variable that looks like a compiler variable (such as `__MODULE__`) is unused
|
||||
* [Kernel.ParallelCompiler] Report individual file compilation times when `profile: :time` is given
|
||||
* [Kernel.ParallelCompiler] Improve precision of `:long_compilation_threshold` so it takes only compilation times into account (and not waiting times)
|
||||
* [Registry] Add `Registry.delete_meta/2`
|
||||
* [Task] Add `Task.await_many/2`
|
||||
|
||||
#### ExUnit
|
||||
|
||||
* [ExUnit] Add support for coloring on Windows 10 consoles/shells
|
||||
* [ExUnit] Add `ExUnit.fetch_test_supervisor/0`
|
||||
* [ExUnit] Add `@tag :tmp_dir` support to ExUnit. The temporary directory is automatically created and pruned before each test
|
||||
* [ExUnit] Add file and line to ExUnit's `--trace`
|
||||
* [ExUnit.Assertion] Allow receive timeouts to be computed at runtime
|
||||
* [ExUnit.Case] Add `register_test/6` to speed up compilation of custom tests
|
||||
* [ExUnit.Doctest] Allow users to add tags to doctests
|
||||
|
||||
#### IEx
|
||||
|
||||
* [IEx] Add support for coloring on Windows 10 consoles/shells
|
||||
* [IEx.Helpers] Show docs from Erlang modules that have been compiled with the docs chunk
|
||||
|
||||
#### Logger
|
||||
|
||||
* [Logger] Add `notice`, `critical`, `alert`, and `emergency` log levels
|
||||
* [Logger] Support structured logging by logging maps or keyword lists
|
||||
* [Logger] Allow level to be set per module with `Logger.put_module_level/2`
|
||||
* [Logger] Include `erl_level` in Logger's metadata
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix] Add `MIX_BUILD_ROOT` to config `_build` dir
|
||||
* [mix] Introduce `MIX_XDG` as a simpler mechanism to opt-in to the XDG specification
|
||||
* [mix] Allow requirements for a Mix task to be listed via the `@requirements` module attribute
|
||||
* [mix] Allow optional dependencies to be defined in `:extra_applications` and `:applications`
|
||||
* [mix app.config] Add new `mix app.config` task that compiles applications and loads runtime configuration
|
||||
* [mix archive.install] Support `--repo` option on Hex packages
|
||||
* [mix compile] Support the `__mix_recompile__?/0` callback for custom behaviour on when Mix should recompile a given module
|
||||
* [mix compile.elixir] Mark modules for path dependencies as "Export dependencies" if they changed but their public interface is the same
|
||||
* [mix compile.elixir] Track application boundaries in the Elixir compiler. If you invoke code from Erlang or Elixir standard libraries and you don't depend on the proper applications, a warning will be emitted. A warning will also be emitted if you invoke code from an umbrella sibling that you don't depend on - effectively forbidding cyclic dependencies between apps
|
||||
* [mix deps] Sort the dependencies alphabetically before printing
|
||||
* [mix deps] Use `origin/HEAD` as the default Git ref in dependencies
|
||||
* [mix deps] Redact Git `username`/`password` in output log
|
||||
* [mix deps] Support rebar3's `git_subdir` resource type
|
||||
* [mix deps.compile] Allow local deps to be skipped on `mix deps.compile`
|
||||
* [mix deps.unlock] Print which dependencies get unlocked when using the `--unused` flag
|
||||
* [mix escript.install] Support `--repo` option on Hex packages
|
||||
* [mix new] Add `@impl` to application generated by `mix new --sup`
|
||||
* [mix release] Enable overriding `sys.config` location via `RELEASE_SYS_CONFIG` env var
|
||||
* [mix release] Boot a release under configuration in interactive mode and then swap to embedded mode (if running on Erlang/OTP 23+)
|
||||
* [mix release] Add `rel_templates_path` to configure the source of template files such as "env.sh.eex", "vm.args.eex" and "overlays"
|
||||
* [mix release] Allow some chunks to be kept in the `:strip_beams` config
|
||||
* [mix test] Allow `:ignore_modules` inside `:test_coverage` option
|
||||
* [mix test.coverage] Add `mix test.coverage` that aggregates coverage results from umbrellas and OS partitioning
|
||||
* [mix xref] Make the `--label` option for `mix xref graph` transitive by default and add `--only-direct` for only direct dependencies
|
||||
* [mix xref] Add `--format cycles` support for `mix xref graph`
|
||||
* [mix xref] Add support to `mix xref graph` for using `--source` and `--sink` at the same time
|
||||
|
||||
### 2. Bug fixes
|
||||
|
||||
#### EEx
|
||||
|
||||
* [EEx] Ensure multiline do/end with no spaces compile under trim mode
|
||||
* [EEx] Make trimming behaviour via the `:trim` option more consistent
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Enum] Allow positive range slices on infinite streams given to `Enum.slice/2`
|
||||
* [Kernel] Raise error on functions/guards without implementation
|
||||
* [Keyword] Ensure keyword replace and update preserve order
|
||||
* [Module] Raise instead of silently failing when performing a write module operation during after-compile
|
||||
* [Module] Fix `@macrocallback` definitions with a `when` clause
|
||||
* [Stream] Close with correct accumulator in `Stream.resource/3` when called for a single-element list
|
||||
* [Stream] Allow `Stream.cycle/1` to be double nested inside `Stream.cycle/1`
|
||||
* [URI] Preserve slashes in URIs without authority
|
||||
* [Application] Warn if non-atom keys are given to `put_env`, `get_env`, `fetch_env`, and `delete_env`
|
||||
* [Code] Do not send language keyword through the `:static_atoms_encoder` in `Code.string_to_quoted`
|
||||
* [Kernel] Validate values given to `:line` in quote to avoid emitting invalid ASTs
|
||||
* [Kernel] Report the correct line number when raising inside a macro
|
||||
* [Kernel] Fix an issue where `elixirc` would not accept paths with backslash (`\`) separators on Windows
|
||||
* [Kernel] Properly parse `&//2` (i.e. the capture of the division operator)
|
||||
* [Kernel] Raise `CompileError` when trying to define reserved types
|
||||
* [Kernel] Improve compiler error message when using `|` in a `def` signature
|
||||
* [Kernel] Improve error message when trying to use invalid list operators in guards
|
||||
* [Kernel.SpecialForms] Add `|/2` to the list of special forms to avoid inconsistent behaviour on overrides
|
||||
* [Keyword] Enforce keys to be atoms in `Keyword.keys/1`
|
||||
* [Record] Keep lexical ordering when creating records
|
||||
* [Registry] Do not crash when a process with key-value has been registered using `:via` and it fails to start on `init`
|
||||
* [URI] `URI.decode_query/2` emits an empty string for parameters without values, according to [URL's living standard](https://url.spec.whatwg.org/#application/x-www-form-urlencoded) - note this behaviour is not specified in the spec implemented by the URI module, so the living standard was chosen
|
||||
* [Version] Add defaults and enforce keys in `Version` struct
|
||||
|
||||
#### ExUnit
|
||||
|
||||
* [ExUnit.CaptureIO] Fix race condition where a dead capture would still be considered as active
|
||||
* [ExUnit.Diff] Do not crash when failing to eval/inspect struct
|
||||
* [ExUnit.Diff] Properly diff numbers in respect to `==` and `===` operators
|
||||
|
||||
#### IEx
|
||||
|
||||
* [IEx] Exit IEx session if the group leader exits
|
||||
* [IEx] Allow `pry` to be used in non-tty terminals
|
||||
* [IEx] Fix tokenizer emitting repeated warnings in the REPL
|
||||
* [IEx] Ensure `--dot-iex` is preserved when restarting the evaluator and after shell respawn
|
||||
* [IEx.Pry] Ensure `IEx.pry` can be triggered more than twice when invoked from the same process
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix compile] Do not filter out warning for external files from diagnostics
|
||||
* [Mix.Project] Ensure user given `:manager` to dependencies has higher precedence than the SCM one
|
||||
* [Mix.Project] Recompile umbrella children when config files change and `mix compile` is called from the umbrella root
|
||||
* [Mix.Task] Always recompile before running tasks from dependencies
|
||||
* [Mix.Task] Ensure project's Logger config is used when running Mix tasks
|
||||
* [mix cmd] Fix a bug where only the first --app option would be executed
|
||||
* [mix compile] Fix an issue where new protocol implementations would not propagate when running `mix compile` from an umbrella root
|
||||
* [mix deps.compile] Use `gmake` instead of `make` when compiling deps on NetBSD/DragonFlyBSD
|
||||
* [mix release] Load `.app` from dependencies path when it is a project dependency
|
||||
* [mix release] Always include "rel/overlays" in the list of overlays directories if available
|
||||
* [mix release] Change `erts/bin/erl` binary mode to `0o755`
|
||||
* [mix test] Compare to test coverage threshold inclusively
|
||||
|
||||
#### Logger
|
||||
|
||||
* [Logger] Print metadata for all types that implement String.Chars
|
||||
|
||||
### 3. Soft-deprecations (no warnings emitted)
|
||||
|
||||
#### Elixir
|
||||
### Elixir
|
||||
|
||||
* [Code] `compiler_options/0` is deprecated in favor of `compiler_option/1`
|
||||
* [Exception] `Exception.exception?/1` is deprecated in favor of `Kernel.is_exception/1`
|
||||
* [Regex] `Regex.regex?/1` is deprecated in favor of `Kernel.is_struct/2`
|
||||
|
||||
#### Mix
|
||||
### Logger
|
||||
|
||||
* [mix xref] `calls/0` is deprecated in favor of compiler tracer
|
||||
* [mix xref] The `xref.exclude` option has been moved to `elixirc_options.no_warn_undefined` as the `xref` pass has been moved into the compiler
|
||||
* [Logger] `warn` log level is deprecated in favor of `warning`
|
||||
|
||||
### Mix
|
||||
|
||||
* [mix release] `config/releases.exs` is deprecated in favor of a more general purpose `config/runtime.exs`
|
||||
|
||||
### 4. Hard-deprecations
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Code] `Code.load_file/2` has been deprecated in favor of `Code.require_file/2` or `Code.compile_file/2`
|
||||
* [Code] `Code.loaded_files/0` and `Code.unload_file/1` have been deprecated in favor of `Code.required_files/0` and `Code.unrequire_file/1` respectively
|
||||
* [Code] `Code.ensure_compiled?/1` is deprecated in favor of `Code.ensure_compiled/1`
|
||||
* [String] `String.normalize/2` has been deprecated in favor of `:unicode.characters_to_nfc_binary/1` or `:unicode.characters_to_nfd_binary/1` which ship as part of Erlang/OTP 20+
|
||||
* [Supervisor] `Supervisor.Spec.supervise/2` has been deprecated in favor of the new Supervisor child specification
|
||||
* [Supervisor] The `:simple_one_for_one` strategy in `Supervisor` has been deprecated in favor of `DynamicSupervisor`
|
||||
|
||||
#### Logger
|
||||
|
||||
* [Logger] `:compile_time_purge_level` application environment configuration has been deprecated in favor of the more general `:compile_time_purge_matching` config
|
||||
* [Logger] Deprecate logging non-chardata values
|
||||
* [Supervisor] Deprecate `Supervisor.start_child/2` and `Supervisor.terminate_child/2` in favor of `DynamicSupervisor`
|
||||
* [Supervisor.Spec] Deprecate `Supervisor.Spec.worker/3` and `Supervisor.Spec.supervisor/3` in favor of the new typespecs
|
||||
* [System] Deprecate `System.stacktrace/0` in favor of `__STACKTRACE__`
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix compile.xref] This check has been moved into the compiler and has no effect now
|
||||
* [mix xref deprecations] This check has been moved into the compiler and has no effect now
|
||||
* [mix xref unreachable] This check has been moved into the compiler and has no effect now
|
||||
* [Mix.Project] Deprecate `Mix.Project.compile/2` in favor of `Mix.Task.run("compile", args)`
|
||||
|
||||
## v1.9
|
||||
## v1.10
|
||||
|
||||
The CHANGELOG for v1.9 releases can be found [in the v1.9 branch](https://github.com/elixir-lang/elixir/blob/v1.9/CHANGELOG.md).
|
||||
The CHANGELOG for v1.10 releases can be found [in the v1.10 branch](https://github.com/elixir-lang/elixir/blob/v1.10/CHANGELOG.md).
|
||||
|
||||
@@ -2,9 +2,10 @@ PREFIX ?= /usr/local
|
||||
TEST_FILES ?= "*_test.exs"
|
||||
SHARE_PREFIX ?= $(PREFIX)/share
|
||||
MAN_PREFIX ?= $(SHARE_PREFIX)/man
|
||||
CANONICAL := master/ # master/ or vMAJOR.MINOR/
|
||||
ELIXIRC := bin/elixirc --verbose --ignore-module-conflict $(ELIXIRC_OPTS)
|
||||
ERLC := erlc -I lib/elixir/include $(ERLC_OPTS)
|
||||
CANONICAL := v1.11/
|
||||
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 := erl -I lib/elixir/include -noshell -pa lib/elixir/ebin
|
||||
GENERATE_APP := $(CURDIR)/lib/elixir/generate_app.escript
|
||||
VERSION := $(strip $(shell cat VERSION))
|
||||
@@ -74,7 +75,7 @@ compile: erlang $(APP) elixir
|
||||
|
||||
erlang: $(PARSER)
|
||||
$(Q) if [ ! -f $(APP) ]; then $(call CHECK_ERLANG_RELEASE); fi
|
||||
$(Q) cd lib/elixir && mkdir -p ebin && erl -make
|
||||
$(Q) cd lib/elixir && mkdir -p ebin && $(ERL_MAKE)
|
||||
|
||||
$(PARSER): lib/elixir/src/elixir_parser.yrl
|
||||
$(Q) erlc -o $@ +'{verbose,true}' +'{report,true}' $<
|
||||
@@ -145,12 +146,12 @@ check_reproducible: compile
|
||||
$(Q) mv lib/mix/ebin/* lib/mix/tmp/ebin_reproducible/
|
||||
SOURCE_DATE_EPOCH=$(call READ_SOURCE_DATE_EPOCH) $(MAKE) compile
|
||||
$(Q) echo "Diffing..."
|
||||
$(Q) diff -r lib/elixir/ebin/ lib/elixir/tmp/ebin_reproducible/
|
||||
$(Q) diff -r lib/eex/ebin/ lib/eex/tmp/ebin_reproducible/
|
||||
$(Q) diff -r lib/ex_unit/ebin/ lib/ex_unit/tmp/ebin_reproducible/
|
||||
$(Q) diff -r lib/iex/ebin/ lib/iex/tmp/ebin_reproducible/
|
||||
$(Q) diff -r lib/logger/ebin/ lib/logger/tmp/ebin_reproducible/
|
||||
$(Q) diff -r lib/mix/ebin/ lib/mix/tmp/ebin_reproducible/
|
||||
$(Q) bin/elixir lib/elixir/diff.exs lib/elixir/ebin/ lib/elixir/tmp/ebin_reproducible/
|
||||
$(Q) bin/elixir lib/elixir/diff.exs lib/eex/ebin/ lib/eex/tmp/ebin_reproducible/
|
||||
$(Q) bin/elixir lib/elixir/diff.exs lib/ex_unit/ebin/ lib/ex_unit/tmp/ebin_reproducible/
|
||||
$(Q) bin/elixir lib/elixir/diff.exs lib/iex/ebin/ lib/iex/tmp/ebin_reproducible/
|
||||
$(Q) bin/elixir lib/elixir/diff.exs lib/logger/ebin/ lib/logger/tmp/ebin_reproducible/
|
||||
$(Q) bin/elixir lib/elixir/diff.exs lib/mix/ebin/ lib/mix/tmp/ebin_reproducible/
|
||||
$(Q) echo "Builds are reproducible"
|
||||
|
||||
clean:
|
||||
@@ -294,7 +295,7 @@ PLT = .elixir.plt
|
||||
|
||||
$(PLT):
|
||||
@ echo "==> Building PLT with Elixir's dependencies..."
|
||||
$(Q) dialyzer --output_plt $(PLT) --build_plt --apps erts kernel stdlib compiler syntax_tools parsetools tools ssl inets
|
||||
$(Q) dialyzer --output_plt $(PLT) --build_plt --apps erts kernel stdlib compiler syntax_tools parsetools tools ssl inets crypto runtime_tools ftp tftp mnesia public_key asn1 hipe sasl
|
||||
|
||||
clean_plt:
|
||||
$(Q) rm -f $(PLT)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||

|
||||
=========
|
||||
[](https://cirrus-ci.com/github/elixir-lang/elixir)
|
||||
# 
|
||||
|
||||
[](https://github.com/elixir-lang/elixir/actions?query=branch%3Amaster+workflow%3ACI) [](https://cirrus-ci.com/github/elixir-lang/elixir)
|
||||
|
||||
Elixir is a dynamic, functional language designed for building scalable
|
||||
and maintainable applications.
|
||||
|
||||
+3
-4
@@ -6,12 +6,11 @@ Elixir applies bug fixes only to the latest minor branch. Security patches are a
|
||||
|
||||
| Elixir version | Support
|
||||
| -------------- | ------------------------------
|
||||
| 1.10 | Development
|
||||
| 1.9 | Bug fixes and security patches
|
||||
| 1.11 | Bug fixes and security patches
|
||||
| 1.10 | Security patches only
|
||||
| 1.9 | Security patches only
|
||||
| 1.8 | Security patches only
|
||||
| 1.7 | Security patches only
|
||||
| 1.6 | Security patches only
|
||||
| 1.5 | Security patches only
|
||||
|
||||
## Announcements
|
||||
|
||||
|
||||
@@ -152,6 +152,10 @@ for /d %%d in ("!SCRIPT_PATH!..\lib\*.") do (
|
||||
)
|
||||
|
||||
:run
|
||||
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 !runMode! == "iex" (
|
||||
set beforeExtra=-noshell -s elixir start_cli !beforeExtra!
|
||||
)
|
||||
|
||||
+13
-11
@@ -1,9 +1,9 @@
|
||||
defmodule EEx.SyntaxError do
|
||||
defexception [:message, :file, :line]
|
||||
defexception [:message, :file, :line, :column]
|
||||
|
||||
@impl true
|
||||
def message(exception) do
|
||||
"#{exception.file}:#{exception.line}: #{exception.message}"
|
||||
"#{exception.file}:#{exception.line}:#{exception.column}: #{exception.message}"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -19,17 +19,17 @@ defmodule EEx do
|
||||
|
||||
This module provides 3 main APIs for you to use:
|
||||
|
||||
1. Evaluate a string (`eval_string`) or a file (`eval_file`)
|
||||
1. Evaluate a string (`eval_string/3`) or a file (`eval_file/3`)
|
||||
directly. This is the simplest API to use but also the
|
||||
slowest, since the code is evaluated and not compiled before.
|
||||
|
||||
2. Define a function from a string (`function_from_string`)
|
||||
or a file (`function_from_file`). This allows you to embed
|
||||
2. Define a function from a string (`function_from_string/5`)
|
||||
or a file (`function_from_file/5`). This allows you to embed
|
||||
the template as a function inside a module which will then
|
||||
be compiled. This is the preferred API if you have access
|
||||
to the template at compilation time.
|
||||
|
||||
3. Compile a string (`compile_string`) or a file (`compile_file`)
|
||||
3. Compile a string (`compile_string/2`) or a file (`compile_file/2`)
|
||||
into Elixir syntax tree. This is the API used by both functions
|
||||
above and is available to you if you want to provide your own
|
||||
ways of handling the compiled template.
|
||||
@@ -39,12 +39,14 @@ defmodule EEx do
|
||||
All functions in this module accept EEx-related options.
|
||||
They are:
|
||||
|
||||
* `:line` - the line to be used as the template start. Defaults to 1.
|
||||
* `:file` - the file to be used in the template. Defaults to the given
|
||||
file the template is read from or to "nofile" when compiling from a string.
|
||||
* `:line` - the line to be used as the template start. Defaults to 1.
|
||||
* `:indentation` - (since v1.11.0) an integer added to the column after every
|
||||
new line. Defaults to 0.
|
||||
* `:engine` - the EEx engine to be used for compilation.
|
||||
* `:trim` - trims whitespace left/right of quotation tags. If a quotation
|
||||
tag appears on its own in a given line, line endings are also removed.
|
||||
* `:trim` - if true, trims whitespace left/right of quotation tags up until
|
||||
newlines. At least one newline is retained. Defaults to false.
|
||||
|
||||
## Engine
|
||||
|
||||
@@ -67,7 +69,7 @@ defmodule EEx do
|
||||
**must** use the equals sign (`=`). Since everything in
|
||||
Elixir is an expression, there are no exceptions for this rule.
|
||||
For example, while some template languages would special-case
|
||||
`if/2` clauses, they are treated the same in EEx and
|
||||
`if` clauses, they are treated the same in EEx and
|
||||
also require `=` in order to have their result printed:
|
||||
|
||||
<%= if true do %>
|
||||
@@ -82,7 +84,7 @@ defmodule EEx do
|
||||
|
||||
will be rendered as `<%= x + 3 %>`.
|
||||
|
||||
Notice that different engines may have different rules
|
||||
Note that different engines may have different rules
|
||||
for each tag. Other tags may be added in future versions.
|
||||
|
||||
### Macros
|
||||
|
||||
+57
-25
@@ -13,23 +13,28 @@ defmodule EEx.Compiler do
|
||||
def compile(source, opts) when is_binary(source) and is_list(opts) do
|
||||
file = opts[:file] || "nofile"
|
||||
line = opts[:line] || 1
|
||||
column = 1
|
||||
indentation = opts[:indentation] || 0
|
||||
trim = opts[:trim] || false
|
||||
tokenizer_options = %{trim: trim, indentation: indentation}
|
||||
|
||||
case EEx.Tokenizer.tokenize(source, line, trim: trim) do
|
||||
case EEx.Tokenizer.tokenize(source, line, column, tokenizer_options) do
|
||||
{:ok, tokens} ->
|
||||
state = %{
|
||||
engine: opts[:engine] || @default_engine,
|
||||
file: file,
|
||||
line: line,
|
||||
quoted: [],
|
||||
start_line: nil
|
||||
start_line: nil,
|
||||
start_column: nil,
|
||||
parser_options: Code.get_compiler_option(:parser_options)
|
||||
}
|
||||
|
||||
init = state.engine.init(opts)
|
||||
generate_buffer(tokens, init, [], state)
|
||||
|
||||
{:error, line, message} ->
|
||||
raise EEx.SyntaxError, line: line, file: file, message: message
|
||||
{:error, line, column, message} ->
|
||||
raise EEx.SyntaxError, file: file, line: line, column: column, message: message
|
||||
end
|
||||
end
|
||||
|
||||
@@ -41,13 +46,19 @@ defmodule EEx.Compiler do
|
||||
generate_buffer(rest, buffer, scope, state)
|
||||
end
|
||||
|
||||
defp generate_buffer([{:expr, line, mark, chars, _} | rest], buffer, scope, state) do
|
||||
expr = Code.string_to_quoted!(chars, line: line, file: state.file)
|
||||
defp generate_buffer([{:expr, line, column, mark, chars} | rest], buffer, scope, state) do
|
||||
options = [file: state.file, line: line, column: column(column, mark)] ++ state.parser_options
|
||||
expr = Code.string_to_quoted!(chars, options)
|
||||
buffer = state.engine.handle_expr(buffer, IO.chardata_to_string(mark), expr)
|
||||
generate_buffer(rest, buffer, scope, state)
|
||||
end
|
||||
|
||||
defp generate_buffer([{:start_expr, start_line, mark, chars, _} | rest], buffer, scope, state) do
|
||||
defp generate_buffer(
|
||||
[{:start_expr, start_line, start_column, mark, chars} | rest],
|
||||
buffer,
|
||||
scope,
|
||||
state
|
||||
) do
|
||||
{contents, line, rest} = look_ahead_middle(rest, start_line, chars)
|
||||
|
||||
{contents, rest} =
|
||||
@@ -55,7 +66,13 @@ defmodule EEx.Compiler do
|
||||
rest,
|
||||
state.engine.handle_begin(buffer),
|
||||
[contents | scope],
|
||||
%{state | quoted: [], line: line, start_line: start_line}
|
||||
%{
|
||||
state
|
||||
| quoted: [],
|
||||
line: line,
|
||||
start_line: start_line,
|
||||
start_column: column(start_column, mark)
|
||||
}
|
||||
)
|
||||
|
||||
buffer = state.engine.handle_expr(buffer, IO.chardata_to_string(mark), contents)
|
||||
@@ -63,7 +80,7 @@ defmodule EEx.Compiler do
|
||||
end
|
||||
|
||||
defp generate_buffer(
|
||||
[{:middle_expr, line, '', chars, _} | rest],
|
||||
[{:middle_expr, line, _column, '', chars} | rest],
|
||||
buffer,
|
||||
[current | scope],
|
||||
state
|
||||
@@ -74,7 +91,7 @@ defmodule EEx.Compiler do
|
||||
end
|
||||
|
||||
defp generate_buffer(
|
||||
[{:middle_expr, line, modifier, chars, trimmed?} | t],
|
||||
[{:middle_expr, line, column, modifier, chars} | t],
|
||||
buffer,
|
||||
[_ | _] = scope,
|
||||
state
|
||||
@@ -84,27 +101,35 @@ defmodule EEx.Compiler do
|
||||
"please remove \"#{modifier}\" accordingly"
|
||||
|
||||
:elixir_errors.erl_warn(line, state.file, message)
|
||||
generate_buffer([{:middle_expr, line, '', chars, trimmed?} | t], buffer, scope, state)
|
||||
generate_buffer([{:middle_expr, line, column, '', chars} | t], buffer, scope, state)
|
||||
# TODO: Make this an error on Elixir v2.0 since it accidentally worked previously.
|
||||
# raise EEx.SyntaxError, message: message, file: state.file, line: line
|
||||
end
|
||||
|
||||
defp generate_buffer([{:middle_expr, line, _, chars, _} | _], _buffer, [], state) do
|
||||
defp generate_buffer([{:middle_expr, line, column, _, chars} | _], _buffer, [], state) do
|
||||
raise EEx.SyntaxError,
|
||||
message: "unexpected middle of expression <%#{chars}%>",
|
||||
file: state.file,
|
||||
line: line
|
||||
line: line,
|
||||
column: column
|
||||
end
|
||||
|
||||
defp generate_buffer([{:end_expr, line, '', chars, _} | rest], buffer, [current | _], state) do
|
||||
defp generate_buffer(
|
||||
[{:end_expr, line, _column, '', chars} | rest],
|
||||
buffer,
|
||||
[current | _],
|
||||
state
|
||||
) do
|
||||
{wrapped, state} = wrap_expr(current, line, buffer, chars, state)
|
||||
tuples = Code.string_to_quoted!(wrapped, line: state.start_line, file: state.file)
|
||||
column = state.start_column
|
||||
options = [file: state.file, line: state.start_line, column: column] ++ state.parser_options
|
||||
tuples = Code.string_to_quoted!(wrapped, options)
|
||||
buffer = insert_quoted(tuples, state.quoted)
|
||||
{buffer, rest}
|
||||
end
|
||||
|
||||
defp generate_buffer(
|
||||
[{:end_expr, line, modifier, chars, trimmed?} | t],
|
||||
[{:end_expr, line, column, modifier, chars} | t],
|
||||
buffer,
|
||||
[_ | _] = scope,
|
||||
state
|
||||
@@ -114,27 +139,29 @@ defmodule EEx.Compiler do
|
||||
"expression \"<%#{modifier}#{chars}%>\", please remove \"#{modifier}\" accordingly"
|
||||
|
||||
:elixir_errors.erl_warn(line, state.file, message)
|
||||
generate_buffer([{:end_expr, line, '', chars, trimmed?} | t], buffer, scope, state)
|
||||
generate_buffer([{:end_expr, line, column, '', chars} | t], buffer, scope, state)
|
||||
# TODO: Make this an error on Elixir v2.0 since it accidentally worked previously.
|
||||
# raise EEx.SyntaxError, message: message, file: state.file, line: line
|
||||
# raise EEx.SyntaxError, message: message, file: state.file, line: line, column: column
|
||||
end
|
||||
|
||||
defp generate_buffer([{:end_expr, line, _, chars, _} | _], _buffer, [], state) do
|
||||
defp generate_buffer([{:end_expr, line, column, _, chars} | _], _buffer, [], state) do
|
||||
raise EEx.SyntaxError,
|
||||
message: "unexpected end of expression <%#{chars}%>",
|
||||
file: state.file,
|
||||
line: line
|
||||
line: line,
|
||||
column: column
|
||||
end
|
||||
|
||||
defp generate_buffer([], buffer, [], state) do
|
||||
defp generate_buffer([{:eof, _, _}], buffer, [], state) do
|
||||
state.engine.handle_body(buffer)
|
||||
end
|
||||
|
||||
defp generate_buffer([], _buffer, _scope, state) do
|
||||
defp generate_buffer([{:eof, line, column}], _buffer, _scope, state) do
|
||||
raise EEx.SyntaxError,
|
||||
message: "unexpected end of string, expected a closing '<% end %>'",
|
||||
file: state.file,
|
||||
line: state.line
|
||||
line: line,
|
||||
column: column
|
||||
end
|
||||
|
||||
# Creates a placeholder and wrap it inside the expression block
|
||||
@@ -152,7 +179,7 @@ defmodule EEx.Compiler do
|
||||
# Look middle expressions that immediately follow a start_expr
|
||||
|
||||
defp look_ahead_middle(
|
||||
[{:text, text}, {:middle_expr, line, _, chars, _} | rest] = tokens,
|
||||
[{:text, text}, {:middle_expr, line, _column, _, chars} | rest] = tokens,
|
||||
start,
|
||||
contents
|
||||
) do
|
||||
@@ -163,7 +190,7 @@ defmodule EEx.Compiler do
|
||||
end
|
||||
end
|
||||
|
||||
defp look_ahead_middle([{:middle_expr, line, _, chars, _} | rest], _start, contents) do
|
||||
defp look_ahead_middle([{:middle_expr, line, _column, _, chars} | rest], _start, contents) do
|
||||
{contents ++ chars, line, rest}
|
||||
end
|
||||
|
||||
@@ -197,4 +224,9 @@ defmodule EEx.Compiler do
|
||||
defp insert_quoted(other, _quoted) do
|
||||
other
|
||||
end
|
||||
|
||||
defp column(column, mark) do
|
||||
# length('<%') == 2
|
||||
column + 2 + length(mark)
|
||||
end
|
||||
end
|
||||
|
||||
+111
-145
@@ -3,14 +3,14 @@ defmodule EEx.Tokenizer do
|
||||
|
||||
@type content :: IO.chardata()
|
||||
@type line :: non_neg_integer
|
||||
@type column :: non_neg_integer
|
||||
@type marker :: '=' | '/' | '|' | ''
|
||||
@type trimmed? :: boolean
|
||||
@type token ::
|
||||
{:text, content}
|
||||
| {:expr | :start_expr | :middle_expr | :end_expr, line, marker, content, trimmed?}
|
||||
| {:expr | :start_expr | :middle_expr | :end_expr, line, column, marker, content}
|
||||
| {:eof, line, column}
|
||||
|
||||
@spaces [?\s, ?\t]
|
||||
@closing_brackets ')]}'
|
||||
|
||||
@doc """
|
||||
Tokenizes the given charlist or binary.
|
||||
@@ -18,68 +18,79 @@ defmodule EEx.Tokenizer do
|
||||
It returns {:ok, list} with the following tokens:
|
||||
|
||||
* `{:text, content}`
|
||||
* `{:expr, line, marker, content, trimmed?}`
|
||||
* `{:start_expr, line, marker, content, trimmed?}`
|
||||
* `{:middle_expr, line, marker, content, trimmed?}`
|
||||
* `{:end_expr, line, marker, content, trimmed?}`
|
||||
* `{:expr, line, column, marker, content}`
|
||||
* `{:start_expr, line, column, marker, content}`
|
||||
* `{:middle_expr, line, column, marker, content}`
|
||||
* `{:end_expr, line, column, marker, content}`
|
||||
* `{:eof, line, column}`
|
||||
|
||||
Or `{:error, line, error}` in case of errors.
|
||||
Or `{:error, line, column, message}` in case of errors.
|
||||
"""
|
||||
@spec tokenize(binary | charlist, line, keyword) :: {:ok, [token]} | {:error, line, String.t()}
|
||||
def tokenize(bin, line, opts \\ [])
|
||||
@spec tokenize(binary | charlist, line, column, map) ::
|
||||
{:ok, [token]} | {:error, line, column, String.t()}
|
||||
|
||||
def tokenize(bin, line, opts)
|
||||
when is_binary(bin) and is_integer(line) and line >= 0 and is_list(opts) do
|
||||
tokenize(String.to_charlist(bin), line, opts)
|
||||
def tokenize(bin, line, column, opts) when is_binary(bin) do
|
||||
tokenize(String.to_charlist(bin), line, column, opts)
|
||||
end
|
||||
|
||||
def tokenize(list, line, opts)
|
||||
when is_list(list) and is_integer(line) and line >= 0 and is_list(opts) do
|
||||
tokenize(list, line, opts, [], [])
|
||||
def tokenize(list, line, column, opts)
|
||||
when is_list(list) and is_integer(line) and line >= 0 and is_integer(column) and column >= 0 do
|
||||
column = opts.indentation + column
|
||||
{list, line, column} = (opts.trim && trim_init(list, line, column)) || {list, line, column}
|
||||
tokenize(list, line, column, opts, [], [])
|
||||
end
|
||||
|
||||
defp tokenize('<%%' ++ t, line, opts, buffer, acc) do
|
||||
tokenize(t, line, opts, [?%, ?< | buffer], acc)
|
||||
defp tokenize('<%%' ++ t, line, column, opts, buffer, acc) do
|
||||
tokenize(t, line, column + 3, opts, [?%, ?< | buffer], acc)
|
||||
end
|
||||
|
||||
defp tokenize('<%#' ++ t, line, opts, buffer, acc) do
|
||||
case expr(t, line, []) do
|
||||
{:error, _, _} = error ->
|
||||
defp tokenize('<%#' ++ t, line, column, opts, buffer, acc) do
|
||||
case expr(t, line, column + 3, opts, []) do
|
||||
{:error, _, _, _} = error ->
|
||||
error
|
||||
|
||||
{:ok, _, new_line, rest} ->
|
||||
{_, rest, new_line, buffer} = trim_if_needed(rest, new_line, opts, buffer, acc)
|
||||
tokenize(rest, new_line, opts, buffer, acc)
|
||||
{:ok, _, new_line, new_column, rest} ->
|
||||
{rest, new_line, new_column, buffer} =
|
||||
trim_if_needed(rest, new_line, new_column, opts, buffer)
|
||||
|
||||
tokenize(rest, new_line, new_column, opts, buffer, acc)
|
||||
end
|
||||
end
|
||||
|
||||
defp tokenize('<%' ++ t, line, opts, buffer, acc) do
|
||||
defp tokenize('<%' ++ t, line, column, opts, buffer, acc) do
|
||||
{marker, t} = retrieve_marker(t)
|
||||
|
||||
case expr(t, line, []) do
|
||||
{:error, _, _} = error ->
|
||||
case expr(t, line, column + 2 + length(marker), opts, []) do
|
||||
{:error, _, _, _} = error ->
|
||||
error
|
||||
|
||||
{:ok, expr, new_line, rest} ->
|
||||
token = token_name(expr)
|
||||
{trimmed?, rest, new_line, buffer} = trim_if_needed(rest, new_line, opts, buffer, acc)
|
||||
expr = pad_if_needed(token, expr, trimmed?)
|
||||
{:ok, expr, new_line, new_column, rest} ->
|
||||
{key, expr} =
|
||||
case :elixir_tokenizer.tokenize(expr, 1, file: "eex", check_terminators: false) do
|
||||
{:ok, tokens} -> token_key(tokens, expr)
|
||||
{:error, _, _, _} -> {:expr, expr}
|
||||
end
|
||||
|
||||
{rest, new_line, new_column, buffer} =
|
||||
trim_if_needed(rest, new_line, new_column, opts, buffer)
|
||||
|
||||
acc = tokenize_text(buffer, acc)
|
||||
final = {token, line, marker, Enum.reverse(expr), trimmed?}
|
||||
tokenize(rest, new_line, opts, [], [final | acc])
|
||||
final = {key, line, column, marker, expr}
|
||||
tokenize(rest, new_line, new_column, opts, [], [final | acc])
|
||||
end
|
||||
end
|
||||
|
||||
defp tokenize('\n' ++ t, line, opts, buffer, acc) do
|
||||
tokenize(t, line + 1, opts, [?\n | buffer], acc)
|
||||
defp tokenize('\n' ++ t, line, _column, opts, buffer, acc) do
|
||||
tokenize(t, line + 1, opts.indentation + 1, opts, [?\n | buffer], acc)
|
||||
end
|
||||
|
||||
defp tokenize([h | t], line, opts, buffer, acc) do
|
||||
tokenize(t, line, opts, [h | buffer], acc)
|
||||
defp tokenize([h | t], line, column, opts, buffer, acc) do
|
||||
tokenize(t, line, column + 1, opts, [h | buffer], acc)
|
||||
end
|
||||
|
||||
defp tokenize([], _line, _opts, buffer, acc) do
|
||||
{:ok, Enum.reverse(tokenize_text(buffer, acc))}
|
||||
defp tokenize([], line, column, _opts, buffer, acc) do
|
||||
eof = {:eof, line, column}
|
||||
{:ok, Enum.reverse([eof | tokenize_text(buffer, acc)])}
|
||||
end
|
||||
|
||||
# Retrieve marker for <%
|
||||
@@ -94,102 +105,61 @@ defmodule EEx.Tokenizer do
|
||||
|
||||
# Tokenize an expression until we find %>
|
||||
|
||||
defp expr([?%, ?> | t], line, buffer) do
|
||||
{:ok, buffer, line, t}
|
||||
defp expr([?%, ?> | t], line, column, _opts, buffer) do
|
||||
{:ok, Enum.reverse(buffer), line, column + 2, t}
|
||||
end
|
||||
|
||||
defp expr('\n' ++ t, line, buffer) do
|
||||
expr(t, line + 1, [?\n | buffer])
|
||||
defp expr('\n' ++ t, line, _column, opts, buffer) do
|
||||
expr(t, line + 1, opts.indentation + 1, opts, [?\n | buffer])
|
||||
end
|
||||
|
||||
defp expr([h | t], line, buffer) do
|
||||
expr(t, line, [h | buffer])
|
||||
defp expr([h | t], line, column, opts, buffer) do
|
||||
expr(t, line, column + 1, opts, [h | buffer])
|
||||
end
|
||||
|
||||
defp expr([], line, _buffer) do
|
||||
{:error, line, "missing token '%>'"}
|
||||
defp expr([], line, column, _opts, _buffer) do
|
||||
{:error, line, column, "missing token '%>'"}
|
||||
end
|
||||
|
||||
# Receive an expression content and check
|
||||
# if it is a start, middle or an end token.
|
||||
#
|
||||
# Start tokens finish with "do" and "fn ->"
|
||||
# Middle tokens are marked with "->" or keywords
|
||||
# End tokens contain only the end word and optionally
|
||||
# combinations of ")", "]" and "}".
|
||||
# Receives tokens and check if it is a start, middle or an end token.
|
||||
defp token_key(tokens, expr) do
|
||||
case {tokens, Enum.reverse(tokens)} do
|
||||
{[{:end, _} | _], [{:do, _} | _]} ->
|
||||
{:middle_expr, expr}
|
||||
|
||||
defp token_name([h | t]) when h in @spaces do
|
||||
token_name(t)
|
||||
end
|
||||
{_, [{:do, _} | _]} ->
|
||||
{:start_expr, maybe_append_space(expr)}
|
||||
|
||||
defp token_name('od' ++ [h | rest]) when h in @spaces or h in @closing_brackets do
|
||||
case tokenize_rest(rest) do
|
||||
{:ok, [{:end, _} | _]} -> :middle_expr
|
||||
_ -> :start_expr
|
||||
end
|
||||
end
|
||||
{_, [{:block_identifier, _, _} | _]} ->
|
||||
{:middle_expr, maybe_append_space(expr)}
|
||||
|
||||
defp token_name('>-' ++ rest) do
|
||||
case tokenize_rest(rest) do
|
||||
{:ok, [{:end, _} | _]} ->
|
||||
:middle_expr
|
||||
{[{:end, _} | _], [{:stab_op, _, _} | _]} ->
|
||||
{:middle_expr, expr}
|
||||
|
||||
# Check if there is a "fn" token and, if so, it is not
|
||||
# followed by an "end" token. If this is the case, we
|
||||
# are on a start expr.
|
||||
{:ok, tokens} ->
|
||||
tokens = Enum.reverse(tokens)
|
||||
fn_index = fn_index(tokens)
|
||||
{_, [{:stab_op, _, _} | reverse_tokens]} ->
|
||||
fn_index = Enum.find_index(reverse_tokens, &match?({:fn, _}, &1)) || :infinity
|
||||
end_index = Enum.find_index(reverse_tokens, &match?({:end, _}, &1)) || :infinity
|
||||
|
||||
if fn_index && end_index(tokens) > fn_index do
|
||||
:start_expr
|
||||
if end_index > fn_index do
|
||||
{:start_expr, expr}
|
||||
else
|
||||
:middle_expr
|
||||
{:middle_expr, expr}
|
||||
end
|
||||
|
||||
_error ->
|
||||
:middle_expr
|
||||
{tokens, _} ->
|
||||
case Enum.drop_while(tokens, &closing_bracket?/1) do
|
||||
[{:end, _} | _] -> {:end_expr, expr}
|
||||
_ -> {:expr, expr}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp token_name('esle' ++ t), do: check_spaces(t, :middle_expr)
|
||||
defp token_name('retfa' ++ t), do: check_spaces(t, :middle_expr)
|
||||
defp token_name('hctac' ++ t), do: check_spaces(t, :middle_expr)
|
||||
defp token_name('eucser' ++ t), do: check_spaces(t, :middle_expr)
|
||||
defp maybe_append_space([?\s]), do: [?\s]
|
||||
defp maybe_append_space([h]), do: [h, ?\s]
|
||||
defp maybe_append_space([h | t]), do: [h | maybe_append_space(t)]
|
||||
|
||||
defp token_name(rest) do
|
||||
case Enum.drop_while(rest, &(&1 in @spaces or &1 in @closing_brackets)) do
|
||||
'dne' ++ t -> check_spaces(t, :end_expr)
|
||||
_ -> :expr
|
||||
end
|
||||
end
|
||||
|
||||
# Tokenize the remaining passing check_terminators as false,
|
||||
# which relax the tokenizer to not error on unmatched pairs.
|
||||
# If the tokens start with an "end" we have a middle expr.
|
||||
defp tokenize_rest(rest) do
|
||||
:elixir_tokenizer.tokenize(Enum.reverse(rest), 1, file: "eex", check_terminators: false)
|
||||
end
|
||||
|
||||
defp fn_index(tokens) do
|
||||
Enum.find_index(tokens, fn
|
||||
{:fn_paren, _} -> true
|
||||
{:fn, _} -> true
|
||||
_ -> false
|
||||
end)
|
||||
end
|
||||
|
||||
defp end_index(tokens) do
|
||||
Enum.find_index(tokens, &match?({:end, _}, &1)) || :infinity
|
||||
end
|
||||
|
||||
defp check_spaces(string, token) do
|
||||
if Enum.all?(string, &(&1 in @spaces)) do
|
||||
token
|
||||
else
|
||||
:expr
|
||||
end
|
||||
end
|
||||
defp closing_bracket?({closing, _}) when closing in ~w"( [ {"a, do: true
|
||||
defp closing_bracket?(_), do: false
|
||||
|
||||
# Tokenize the buffered text by appending
|
||||
# it to the given accumulator.
|
||||
@@ -202,45 +172,41 @@ defmodule EEx.Tokenizer do
|
||||
[{:text, Enum.reverse(buffer)} | acc]
|
||||
end
|
||||
|
||||
# If trim mode is enabled and the token is on a line with
|
||||
# only itself and whitespace, trim the whitespace around it,
|
||||
# including the line break following it if there is one.
|
||||
defp trim_if_needed(rest, line, opts, buffer, acc) do
|
||||
with true <- opts[:trim],
|
||||
{true, new_buffer} <- trim_left(buffer, acc),
|
||||
{true, new_rest, new_line} <- trim_right(rest, line) do
|
||||
{true, new_rest, new_line, new_buffer}
|
||||
defp trim_if_needed(rest, line, column, opts, buffer) do
|
||||
if opts.trim do
|
||||
buffer = trim_left(buffer, 0)
|
||||
{rest, line, column} = trim_right(rest, line, column, 0)
|
||||
{rest, line, column, buffer}
|
||||
else
|
||||
_ -> {false, rest, line, buffer}
|
||||
{rest, line, column, buffer}
|
||||
end
|
||||
end
|
||||
|
||||
defp trim_left(buffer, acc) do
|
||||
case {trim_whitespace(buffer), acc} do
|
||||
{[?\n | _] = trimmed_buffer, _} -> {true, trimmed_buffer}
|
||||
{[], [{_, _, _, _, true} | _]} -> {true, []}
|
||||
{[], []} -> {true, []}
|
||||
_ -> {false, buffer}
|
||||
defp trim_init([h | t], line, column) when h in @spaces, do: trim_init(t, line, column + 1)
|
||||
defp trim_init([?\r, ?\n | t], line, _column), do: trim_init(t, line + 1, 1)
|
||||
defp trim_init([?\n | t], line, _column), do: trim_init(t, line + 1, 1)
|
||||
defp trim_init([?<, ?% | _] = rest, line, column), do: {rest, line, column}
|
||||
defp trim_init(_, _, _), do: false
|
||||
|
||||
defp trim_left(buffer, count) do
|
||||
case trim_whitespace(buffer) do
|
||||
[?\n, ?\r | rest] -> trim_left(rest, count + 1)
|
||||
[?\n | rest] -> trim_left(rest, count + 1)
|
||||
_ when count > 0 -> [?\n | buffer]
|
||||
_ -> buffer
|
||||
end
|
||||
end
|
||||
|
||||
defp trim_right(rest, line) do
|
||||
defp trim_right(rest, line, column, count) do
|
||||
case trim_whitespace(rest) do
|
||||
[?\r, ?\n | trimmed_rest] -> {true, trimmed_rest, line + 1}
|
||||
[?\n | trimmed_rest] -> {true, trimmed_rest, line + 1}
|
||||
[] -> {true, [], line}
|
||||
_ -> {false, rest, line}
|
||||
[?\r, ?\n | rest] -> trim_right(rest, line + 1, 1, count + 1)
|
||||
[?\n | rest] -> trim_right(rest, line + 1, 1, count + 1)
|
||||
[] -> {[], line, column + length(rest)}
|
||||
_ when count > 0 -> {[?\n | rest], line - 1, column}
|
||||
_ -> {rest, line, column}
|
||||
end
|
||||
end
|
||||
|
||||
defp trim_whitespace([h | t]) when h in @spaces do
|
||||
trim_whitespace(t)
|
||||
end
|
||||
|
||||
defp trim_whitespace(list) do
|
||||
list
|
||||
end
|
||||
|
||||
defp pad_if_needed(:start_expr, [h | _] = expr, true) when h not in @spaces, do: [?\s | expr]
|
||||
defp pad_if_needed(_, expr, _), do: expr
|
||||
defp trim_whitespace([h | t]) when h in @spaces, do: trim_whitespace(t)
|
||||
defp trim_whitespace(list), do: list
|
||||
end
|
||||
|
||||
@@ -4,37 +4,39 @@ defmodule EEx.TokenizerTest do
|
||||
use ExUnit.Case, async: true
|
||||
require EEx.Tokenizer, as: T
|
||||
|
||||
@opts %{indentation: 0, trim: false}
|
||||
|
||||
test "simple chars lists" do
|
||||
assert T.tokenize('foo', 1) == {:ok, [{:text, 'foo'}]}
|
||||
assert T.tokenize('foo', 1, 1, @opts) == {:ok, [{:text, 'foo'}, {:eof, 1, 4}]}
|
||||
end
|
||||
|
||||
test "simple strings" do
|
||||
assert T.tokenize("foo", 1) == {:ok, [{:text, 'foo'}]}
|
||||
assert T.tokenize("foo", 1, 1, @opts) == {:ok, [{:text, 'foo'}, {:eof, 1, 4}]}
|
||||
end
|
||||
|
||||
test "strings with embedded code" do
|
||||
assert T.tokenize('foo <% bar %>', 1) ==
|
||||
{:ok, [{:text, 'foo '}, {:expr, 1, '', ' bar ', false}]}
|
||||
assert T.tokenize('foo <% bar %>', 1, 1, @opts) ==
|
||||
{:ok, [{:text, 'foo '}, {:expr, 1, 5, '', ' bar '}, {:eof, 1, 14}]}
|
||||
end
|
||||
|
||||
test "strings with embedded equals code" do
|
||||
assert T.tokenize('foo <%= bar %>', 1) ==
|
||||
{:ok, [{:text, 'foo '}, {:expr, 1, '=', ' bar ', false}]}
|
||||
assert T.tokenize('foo <%= bar %>', 1, 1, @opts) ==
|
||||
{:ok, [{:text, 'foo '}, {:expr, 1, 5, '=', ' bar '}, {:eof, 1, 15}]}
|
||||
end
|
||||
|
||||
test "strings with embedded slash code" do
|
||||
assert T.tokenize('foo <%/ bar %>', 1) ==
|
||||
{:ok, [{:text, 'foo '}, {:expr, 1, '/', ' bar ', false}]}
|
||||
assert T.tokenize('foo <%/ bar %>', 1, 1, @opts) ==
|
||||
{:ok, [{:text, 'foo '}, {:expr, 1, 5, '/', ' bar '}, {:eof, 1, 15}]}
|
||||
end
|
||||
|
||||
test "strings with embedded pipe code" do
|
||||
assert T.tokenize('foo <%| bar %>', 1) ==
|
||||
{:ok, [{:text, 'foo '}, {:expr, 1, '|', ' bar ', false}]}
|
||||
assert T.tokenize('foo <%| bar %>', 1, 1, @opts) ==
|
||||
{:ok, [{:text, 'foo '}, {:expr, 1, 5, '|', ' bar '}, {:eof, 1, 15}]}
|
||||
end
|
||||
|
||||
test "strings with more than one line" do
|
||||
assert T.tokenize('foo\n<%= bar %>', 1) ==
|
||||
{:ok, [{:text, 'foo\n'}, {:expr, 2, '=', ' bar ', false}]}
|
||||
assert T.tokenize('foo\n<%= bar %>', 1, 1, @opts) ==
|
||||
{:ok, [{:text, 'foo\n'}, {:expr, 2, 1, '=', ' bar '}, {:eof, 2, 11}]}
|
||||
end
|
||||
|
||||
test "strings with more than one line and expression with more than one line" do
|
||||
@@ -47,167 +49,208 @@ defmodule EEx.TokenizerTest do
|
||||
|
||||
exprs = [
|
||||
{:text, 'foo '},
|
||||
{:expr, 1, '=', ' bar\n\nbaz ', false},
|
||||
{:expr, 1, 5, '=', ' bar\n\nbaz '},
|
||||
{:text, '\n'},
|
||||
{:expr, 4, '', ' foo ', false},
|
||||
{:text, '\n'}
|
||||
{:expr, 4, 1, '', ' foo '},
|
||||
{:text, '\n'},
|
||||
{:eof, 5, 1}
|
||||
]
|
||||
|
||||
assert T.tokenize(string, 1) == {:ok, exprs}
|
||||
assert T.tokenize(string, 1, 1, @opts) == {:ok, exprs}
|
||||
end
|
||||
|
||||
test "quotation" do
|
||||
assert T.tokenize('foo <%% true %>', 1) == {:ok, [{:text, 'foo <% true %>'}]}
|
||||
assert T.tokenize('foo <%% true %>', 1, 1, @opts) ==
|
||||
{:ok, [{:text, 'foo <% true %>'}, {:eof, 1, 16}]}
|
||||
end
|
||||
|
||||
test "quotation with do/end" do
|
||||
assert T.tokenize('foo <%% true do %>bar<%% end %>', 1) ==
|
||||
{:ok, [{:text, 'foo <% true do %>bar<% end %>'}]}
|
||||
assert T.tokenize('foo <%% true do %>bar<%% end %>', 1, 1, @opts) ==
|
||||
{:ok, [{:text, 'foo <% true do %>bar<% end %>'}, {:eof, 1, 32}]}
|
||||
end
|
||||
|
||||
test "quotation with interpolation" do
|
||||
exprs = [
|
||||
{:text, 'a <% b '},
|
||||
{:expr, 1, '=', ' c ', false},
|
||||
{:expr, 1, 9, '=', ' c '},
|
||||
{:text, ' '},
|
||||
{:expr, 1, '=', ' d ', false},
|
||||
{:text, ' e %> f'}
|
||||
{:expr, 1, 18, '=', ' d '},
|
||||
{:text, ' e %> f'},
|
||||
{:eof, 1, 33}
|
||||
]
|
||||
|
||||
assert T.tokenize('a <%% b <%= c %> <%= d %> e %> f', 1) == {:ok, exprs}
|
||||
assert T.tokenize('a <%% b <%= c %> <%= d %> e %> f', 1, 1, @opts) == {:ok, exprs}
|
||||
end
|
||||
|
||||
test "improperly formatted quotation with interpolation" do
|
||||
exprs = [
|
||||
{:text, '<%% a <%= b %> c %>'}
|
||||
{:text, '<%% a <%= b %> c %>'},
|
||||
{:eof, 1, 22}
|
||||
]
|
||||
|
||||
assert T.tokenize('<%%% a <%%= b %> c %>', 1) == {:ok, exprs}
|
||||
assert T.tokenize('<%%% a <%%= b %> c %>', 1, 1, @opts) == {:ok, exprs}
|
||||
end
|
||||
|
||||
test "comments" do
|
||||
test "eex comments" do
|
||||
exprs = [
|
||||
{:text, 'foo '}
|
||||
{:text, 'foo '},
|
||||
{:eof, 1, 16}
|
||||
]
|
||||
|
||||
assert T.tokenize('foo <%# true %>', 1) == {:ok, exprs}
|
||||
assert T.tokenize('foo <%# true %>', 1, 1, @opts) == {:ok, exprs}
|
||||
end
|
||||
|
||||
test "comments with do/end" do
|
||||
test "eex comments with do/end" do
|
||||
exprs = [
|
||||
{:text, 'foo bar'}
|
||||
{:text, 'foo bar'},
|
||||
{:eof, 1, 32}
|
||||
]
|
||||
|
||||
assert T.tokenize('foo <%# true do %>bar<%# end %>', 1) == {:ok, exprs}
|
||||
assert T.tokenize('foo <%# true do %>bar<%# end %>', 1, 1, @opts) == {:ok, exprs}
|
||||
end
|
||||
|
||||
test "elixir comments" do
|
||||
exprs = [
|
||||
{:text, 'foo '},
|
||||
{:expr, 1, 5, [], ' true # this is a boolean '},
|
||||
{:eof, 1, 35}
|
||||
]
|
||||
|
||||
assert T.tokenize('foo <% true # this is a boolean %>', 1, 1, @opts) == {:ok, exprs}
|
||||
end
|
||||
|
||||
test "elixir comments with do/end" do
|
||||
exprs = [
|
||||
{:start_expr, 1, 1, [], ' if true do # startif '},
|
||||
{:text, 'text'},
|
||||
{:end_expr, 1, 31, [], ' end # closeif '},
|
||||
{:eof, 1, 50}
|
||||
]
|
||||
|
||||
assert T.tokenize('<% if true do # startif %>text<% end # closeif %>', 1, 1, @opts) ==
|
||||
{:ok, exprs}
|
||||
end
|
||||
|
||||
test "strings with embedded do end" do
|
||||
exprs = [
|
||||
{:text, 'foo '},
|
||||
{:start_expr, 1, '', ' if true do ', false},
|
||||
{:start_expr, 1, 5, '', ' if true do '},
|
||||
{:text, 'bar'},
|
||||
{:end_expr, 1, '', ' end ', false}
|
||||
{:end_expr, 1, 24, '', ' end '},
|
||||
{:eof, 1, 33}
|
||||
]
|
||||
|
||||
assert T.tokenize('foo <% if true do %>bar<% end %>', 1) == {:ok, exprs}
|
||||
assert T.tokenize('foo <% if true do %>bar<% end %>', 1, 1, @opts) == {:ok, exprs}
|
||||
end
|
||||
|
||||
test "strings with embedded -> end" do
|
||||
exprs = [
|
||||
{:text, 'foo '},
|
||||
{:start_expr, 1, '', ' cond do ', false},
|
||||
{:middle_expr, 1, '', ' false -> ', false},
|
||||
{:start_expr, 1, 5, '', ' cond do '},
|
||||
{:middle_expr, 1, 18, '', ' false -> '},
|
||||
{:text, 'bar'},
|
||||
{:middle_expr, 1, '', ' true -> ', false},
|
||||
{:middle_expr, 1, 35, '', ' true -> '},
|
||||
{:text, 'baz'},
|
||||
{:end_expr, 1, '', ' end ', false}
|
||||
{:end_expr, 1, 51, '', ' end '},
|
||||
{:eof, 1, 60}
|
||||
]
|
||||
|
||||
assert T.tokenize('foo <% cond do %><% false -> %>bar<% true -> %>baz<% end %>', 1) ==
|
||||
assert T.tokenize('foo <% cond do %><% false -> %>bar<% true -> %>baz<% end %>', 1, 1, @opts) ==
|
||||
{:ok, exprs}
|
||||
end
|
||||
|
||||
test "strings with multiple callbacks" do
|
||||
exprs = [
|
||||
{:start_expr, 1, '=', ' a fn -> ', false},
|
||||
{:start_expr, 1, 1, '=', ' a fn -> '},
|
||||
{:text, 'foo'},
|
||||
{:middle_expr, 1, '', ' end, fn -> ', false},
|
||||
{:middle_expr, 1, 18, '', ' end, fn -> '},
|
||||
{:text, 'bar'},
|
||||
{:end_expr, 1, '', ' end ', false}
|
||||
{:end_expr, 1, 37, '', ' end '},
|
||||
{:eof, 1, 46}
|
||||
]
|
||||
|
||||
assert T.tokenize('<%= a fn -> %>foo<% end, fn -> %>bar<% end %>', 1) == {:ok, exprs}
|
||||
assert T.tokenize('<%= a fn -> %>foo<% end, fn -> %>bar<% end %>', 1, 1, @opts) ==
|
||||
{:ok, exprs}
|
||||
end
|
||||
|
||||
test "strings with callback followed by do block" do
|
||||
exprs = [
|
||||
{:start_expr, 1, '=', ' a fn -> ', false},
|
||||
{:start_expr, 1, 1, '=', ' a fn -> '},
|
||||
{:text, 'foo'},
|
||||
{:middle_expr, 1, '', ' end do ', false},
|
||||
{:middle_expr, 1, 18, '', ' end do '},
|
||||
{:text, 'bar'},
|
||||
{:end_expr, 1, '', ' end ', false}
|
||||
{:end_expr, 1, 33, '', ' end '},
|
||||
{:eof, 1, 42}
|
||||
]
|
||||
|
||||
assert T.tokenize('<%= a fn -> %>foo<% end do %>bar<% end %>', 1) == {:ok, exprs}
|
||||
assert T.tokenize('<%= a fn -> %>foo<% end do %>bar<% end %>', 1, 1, @opts) == {:ok, exprs}
|
||||
end
|
||||
|
||||
test "strings with embedded keywords blocks" do
|
||||
exprs = [
|
||||
{:text, 'foo '},
|
||||
{:start_expr, 1, '', ' if true do ', false},
|
||||
{:start_expr, 1, 5, '', ' if true do '},
|
||||
{:text, 'bar'},
|
||||
{:middle_expr, 1, '', ' else ', false},
|
||||
{:middle_expr, 1, 24, '', ' else '},
|
||||
{:text, 'baz'},
|
||||
{:end_expr, 1, '', ' end ', false}
|
||||
{:end_expr, 1, 37, '', ' end '},
|
||||
{:eof, 1, 46}
|
||||
]
|
||||
|
||||
assert T.tokenize('foo <% if true do %>bar<% else %>baz<% end %>', 1) == {:ok, exprs}
|
||||
assert T.tokenize('foo <% if true do %>bar<% else %>baz<% end %>', 1, 1, @opts) ==
|
||||
{:ok, exprs}
|
||||
end
|
||||
|
||||
test "trim mode" do
|
||||
template = '\t<%= if true do %> \n TRUE \n <% else %>\n FALSE \n <% end %> '
|
||||
template = '\t<%= if true do %> \n TRUE \n <% else %>\n FALSE \n <% end %> \n\n '
|
||||
|
||||
exprs = [
|
||||
{:start_expr, 1, '=', ' if true do ', true},
|
||||
{:text, ' TRUE \n'},
|
||||
{:middle_expr, 3, '', ' else ', true},
|
||||
{:text, ' FALSE \n'},
|
||||
{:end_expr, 5, '', ' end ', true}
|
||||
{:start_expr, 1, 2, '=', ' if true do '},
|
||||
{:text, '\n TRUE \n'},
|
||||
{:middle_expr, 3, 3, '', ' else '},
|
||||
{:text, '\n FALSE \n'},
|
||||
{:end_expr, 5, 3, '', ' end '},
|
||||
{:eof, 7, 3}
|
||||
]
|
||||
|
||||
assert T.tokenize(template, 1, trim: true) == {:ok, exprs}
|
||||
assert T.tokenize(template, 1, 1, %{@opts | trim: true}) == {:ok, exprs}
|
||||
end
|
||||
|
||||
test "trim mode with comment" do
|
||||
exprs = [
|
||||
{:text, '123'}
|
||||
{:text, '\n123'},
|
||||
{:eof, 2, 4}
|
||||
]
|
||||
|
||||
assert T.tokenize(' <%# comment %> \n123', 1, trim: true) == {:ok, exprs}
|
||||
assert T.tokenize(' <%# comment %> \n123', 1, 1, %{@opts | trim: true}) == {:ok, exprs}
|
||||
end
|
||||
|
||||
test "trim mode with CRLF" do
|
||||
exprs = [
|
||||
{:text, '0\r\n'},
|
||||
{:expr, 2, '=', ' 12 ', true},
|
||||
{:text, '34'}
|
||||
{:text, '0\n'},
|
||||
{:expr, 2, 3, '=', ' 12 '},
|
||||
{:text, '\n34'},
|
||||
{:eof, 3, 3}
|
||||
]
|
||||
|
||||
assert T.tokenize('0\r\n <%= 12 %> \r\n34', 1, trim: true) == {:ok, exprs}
|
||||
assert T.tokenize('0\r\n <%= 12 %> \r\n34', 1, 1, %{@opts | trim: true}) == {:ok, exprs}
|
||||
end
|
||||
|
||||
test "trim mode set to false" do
|
||||
exprs = [
|
||||
{:text, ' '},
|
||||
{:expr, 1, '=', ' 12 ', false},
|
||||
{:text, ' \n'}
|
||||
{:expr, 1, 2, '=', ' 12 '},
|
||||
{:text, ' \n'},
|
||||
{:eof, 2, 1}
|
||||
]
|
||||
|
||||
assert T.tokenize(' <%= 12 %> \n', 1, trim: false) == {:ok, exprs}
|
||||
assert T.tokenize(' <%= 12 %> \n', 1, 1, %{@opts | trim: false}) == {:ok, exprs}
|
||||
end
|
||||
|
||||
test "trim mode no false positives" do
|
||||
assert_not_trimmed = fn x -> assert T.tokenize(x, 1, trim: true) == T.tokenize(x, 1) end
|
||||
assert_not_trimmed = fn x ->
|
||||
assert T.tokenize(x, 1, 1, %{@opts | trim: false}) == T.tokenize(x, 1, 1, @opts)
|
||||
end
|
||||
|
||||
assert_not_trimmed.('foo <%= "bar" %> ')
|
||||
assert_not_trimmed.('\n <%= "foo" %>bar')
|
||||
@@ -215,8 +258,13 @@ defmodule EEx.TokenizerTest do
|
||||
assert_not_trimmed.(' <%= 01 %><%= 23 %>\n')
|
||||
end
|
||||
|
||||
test "raise syntax error when there is start mark and no end mark" do
|
||||
assert T.tokenize('foo <% :bar', 1) == {:error, 1, "missing token '%>'"}
|
||||
assert T.tokenize('<%# true ', 1) == {:error, 1, "missing token '%>'"}
|
||||
test "returns error when there is start mark and no end mark" do
|
||||
assert T.tokenize('foo <% :bar', 1, 1, @opts) == {:error, 1, 12, "missing token '%>'"}
|
||||
assert T.tokenize('<%# true ', 1, 1, @opts) == {:error, 1, 10, "missing token '%>'"}
|
||||
end
|
||||
|
||||
test "marks invalid expressions as regular expressions" do
|
||||
assert T.tokenize('<% 1 $ 2 %>', 1, 1, @opts) ==
|
||||
{:ok, [{:expr, 1, 1, [], ' 1 $ 2 '}, {:eof, 1, 12}]}
|
||||
end
|
||||
end
|
||||
|
||||
+77
-13
@@ -79,8 +79,24 @@ defmodule EExTest do
|
||||
end
|
||||
|
||||
test "trim mode" do
|
||||
string = "<%= 123 %> \n \n <%= 789 %>"
|
||||
expected = "123\n789"
|
||||
assert_eval(expected, string, [], trim: true)
|
||||
|
||||
string = "<%= 123 %> \n456\n <%= 789 %>"
|
||||
expected = "123456\n789"
|
||||
expected = "123\n456\n789"
|
||||
assert_eval(expected, string, [], trim: true)
|
||||
|
||||
string = "<%= 123 %> \n <%= 456 %> \n <%= 789 %>"
|
||||
expected = "123\n456\n789"
|
||||
assert_eval(expected, string, [], trim: true)
|
||||
|
||||
string = "\n <%= 123 %> \n <%= 456 %> \n <%= 789 %> \n"
|
||||
expected = "123\n456\n789"
|
||||
assert_eval(expected, string, [], trim: true)
|
||||
|
||||
string = "\r\n <%= 123 %> \r\n <%= 456 %> \r\n <%= 789 %> \r\n"
|
||||
expected = "123\n456\n789"
|
||||
assert_eval(expected, string, [], trim: true)
|
||||
end
|
||||
|
||||
@@ -94,7 +110,7 @@ defmodule EExTest do
|
||||
<% end %>
|
||||
"""
|
||||
|
||||
expected = " that\n"
|
||||
expected = "\n that\n"
|
||||
assert_eval(expected, string, [], trim: true)
|
||||
end
|
||||
|
||||
@@ -106,11 +122,22 @@ defmodule EExTest do
|
||||
<%= "Fourth line" %>
|
||||
"""
|
||||
|
||||
expected = "First lineSecond lineThird lineFourth line"
|
||||
expected = "First line\nSecond line\nThird line\nFourth line"
|
||||
assert_eval(expected, string, [], trim: true)
|
||||
end
|
||||
|
||||
test "trim mode with no spaces" do
|
||||
string = """
|
||||
<%=if true do%>
|
||||
this
|
||||
<%else%>
|
||||
that
|
||||
<%end%>
|
||||
"""
|
||||
|
||||
expected = "\n this\n"
|
||||
assert_eval(expected, string, [], trim: true)
|
||||
|
||||
string = """
|
||||
<%=cond do%>
|
||||
<%false ->%>
|
||||
@@ -120,7 +147,7 @@ defmodule EExTest do
|
||||
<%end%>
|
||||
"""
|
||||
|
||||
expected = " that\n"
|
||||
expected = "\n that\n"
|
||||
assert_eval(expected, string, [], trim: true)
|
||||
end
|
||||
|
||||
@@ -220,25 +247,27 @@ defmodule EExTest do
|
||||
|
||||
describe "raises syntax errors" do
|
||||
test "when the token is invalid" do
|
||||
assert_raise EEx.SyntaxError, "nofile:1: missing token '%>'", fn ->
|
||||
assert_raise EEx.SyntaxError, "nofile:1:12: missing token '%>'", fn ->
|
||||
EEx.compile_string("foo <%= bar")
|
||||
end
|
||||
end
|
||||
|
||||
test "when middle expression is found without a start expression" do
|
||||
assert_raise EEx.SyntaxError, "nofile:1: unexpected middle of expression <% else %>", fn ->
|
||||
EEx.compile_string("<% if true %> foo<% else %>bar<% end %>")
|
||||
end
|
||||
assert_raise EEx.SyntaxError,
|
||||
"nofile:1:18: unexpected middle of expression <% else %>",
|
||||
fn ->
|
||||
EEx.compile_string("<% if true %> foo<% else %>bar<% end %>")
|
||||
end
|
||||
end
|
||||
|
||||
test "when end expression is found without a start expression" do
|
||||
assert_raise EEx.SyntaxError, "nofile:1: unexpected end of expression <% end %>", fn ->
|
||||
assert_raise EEx.SyntaxError, "nofile:1:5: unexpected end of expression <% end %>", fn ->
|
||||
EEx.compile_string("foo <% end %>")
|
||||
end
|
||||
end
|
||||
|
||||
test "when start expression is found without an end expression" do
|
||||
msg = "nofile:2: unexpected end of string, expected a closing '<% end %>'"
|
||||
msg = "nofile:2:17: unexpected end of string, expected a closing '<% end %>'"
|
||||
|
||||
assert_raise EEx.SyntaxError, msg, fn ->
|
||||
EEx.compile_string("foo\n<% if true do %>")
|
||||
@@ -246,7 +275,7 @@ defmodule EExTest do
|
||||
end
|
||||
|
||||
test "when nested end expression is found without a start expression" do
|
||||
assert_raise EEx.SyntaxError, "nofile:1: unexpected end of expression <% end %>", fn ->
|
||||
assert_raise EEx.SyntaxError, "nofile:1:30: unexpected end of expression <% end %>", fn ->
|
||||
EEx.compile_string("foo <% if true do %><% end %><% end %>")
|
||||
end
|
||||
end
|
||||
@@ -285,13 +314,13 @@ defmodule EExTest do
|
||||
|
||||
describe "error messages" do
|
||||
test "honor line numbers" do
|
||||
assert_raise EEx.SyntaxError, "nofile:99: missing token '%>'", fn ->
|
||||
assert_raise EEx.SyntaxError, "nofile:99:12: missing token '%>'", fn ->
|
||||
EEx.compile_string("foo <%= bar", line: 99)
|
||||
end
|
||||
end
|
||||
|
||||
test "honor file names" do
|
||||
assert_raise EEx.SyntaxError, "my_file.eex:1: missing token '%>'", fn ->
|
||||
assert_raise EEx.SyntaxError, "my_file.eex:1:12: missing token '%>'", fn ->
|
||||
EEx.compile_string("foo <%= bar", file: "my_file.eex")
|
||||
end
|
||||
end
|
||||
@@ -502,6 +531,41 @@ defmodule EExTest do
|
||||
|
||||
assert_eval("\n\n Good\n \n", string)
|
||||
end
|
||||
|
||||
test "line and column meta" do
|
||||
parser_options = Code.get_compiler_option(:parser_options)
|
||||
Code.put_compiler_option(:parser_options, columns: true)
|
||||
|
||||
try do
|
||||
indentation = 12
|
||||
|
||||
ast =
|
||||
EEx.compile_string(
|
||||
"""
|
||||
<%= f() %> <% f() %>
|
||||
<%= f fn -> %>
|
||||
<%= f() %>
|
||||
<% end %>
|
||||
""",
|
||||
indentation: indentation
|
||||
)
|
||||
|
||||
{_, calls} =
|
||||
Macro.prewalk(ast, [], fn
|
||||
{:f, meta, _args} = expr, acc -> {expr, [meta | acc]}
|
||||
other, acc -> {other, acc}
|
||||
end)
|
||||
|
||||
assert Enum.reverse(calls) == [
|
||||
[line: 1, column: indentation + 5],
|
||||
[line: 1, column: indentation + 15],
|
||||
[line: 2, column: indentation + 7],
|
||||
[line: 3, column: indentation + 9]
|
||||
]
|
||||
after
|
||||
Code.put_compiler_option(:parser_options, parser_options)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "buffers" do
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
warn_exported_vars,
|
||||
%% warn_missing_spec,
|
||||
%% warn_untyped_record,
|
||||
warnings_as_errors,
|
||||
debug_info,
|
||||
{outdir, "ebin/"}
|
||||
]}.
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
defmodule Diff do
|
||||
@moduledoc """
|
||||
Utilities for comparing build artifacts.
|
||||
"""
|
||||
|
||||
@known_chunks ~w(
|
||||
abstract_code
|
||||
debug_info
|
||||
attributes
|
||||
compile_info
|
||||
exports
|
||||
labeled_exports
|
||||
imports
|
||||
indexed_imports
|
||||
locals
|
||||
labeled_locals
|
||||
atoms
|
||||
)a
|
||||
|
||||
@doc """
|
||||
Compares the build artifacts of two build directories.
|
||||
"""
|
||||
@spec compare_dirs(Path.t(), Path.t()) ::
|
||||
{
|
||||
only1_paths :: list(Path.t()),
|
||||
only2_paths :: list(Path.t()),
|
||||
diff :: list({Path.t(), diff :: String.t()})
|
||||
}
|
||||
def compare_dirs(dir1, dir2) do
|
||||
dir1 = Path.expand(dir1)
|
||||
dir2 = Path.expand(dir2)
|
||||
|
||||
assert_dir!(dir1)
|
||||
assert_dir!(dir2)
|
||||
|
||||
dir1_paths = relative_paths(dir1)
|
||||
dir2_paths = relative_paths(dir2)
|
||||
|
||||
only1_paths = dir1_paths -- dir2_paths
|
||||
only2_paths = dir2_paths -- dir1_paths
|
||||
common_paths = dir1_paths -- only1_paths
|
||||
common_files = Enum.reject(common_paths, &File.dir?/1)
|
||||
|
||||
diff =
|
||||
Enum.flat_map(common_files, fn path ->
|
||||
file1 = Path.join(dir1, path)
|
||||
file2 = Path.join(dir2, path)
|
||||
|
||||
case compare_files(file1, file2) do
|
||||
:eq -> []
|
||||
{:diff, diff} -> [{path, diff}]
|
||||
end
|
||||
end)
|
||||
|
||||
{only1_paths, only2_paths, diff}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Compares the contents of two files.
|
||||
|
||||
If the files are BEAM files, it performs a more human-friendly
|
||||
"BEAM-diff".
|
||||
"""
|
||||
@spec compare_files(Path.t(), Path.t()) :: :eq | {:diff, diff :: String.t()}
|
||||
def compare_files(file1, file2) do
|
||||
content1 = File.read!(file1)
|
||||
content2 = File.read!(file2)
|
||||
|
||||
if content1 == content2 do
|
||||
:eq
|
||||
else
|
||||
diff =
|
||||
if String.ends_with?(file1, ".beam") do
|
||||
beam_diff(file1, content1, file2, content2)
|
||||
else
|
||||
file_diff(file1, file2)
|
||||
end
|
||||
|
||||
{:diff, diff}
|
||||
end
|
||||
end
|
||||
|
||||
defp beam_diff(file1, content1, file2, content2) do
|
||||
with {:ok, {module, chunks1}} <- :beam_lib.chunks(content1, @known_chunks),
|
||||
{:ok, {^module, chunks2}} <- :beam_lib.chunks(content2, @known_chunks),
|
||||
true <- chunks1 != chunks2 do
|
||||
for {chunk1, chunk2} <- Enum.zip(chunks1, chunks2), chunk1 != chunk2 do
|
||||
tmp_file1 =
|
||||
chunk1
|
||||
|> inspect(pretty: true, limit: :infinity)
|
||||
|> write_tmp()
|
||||
|
||||
tmp_file2 =
|
||||
chunk2
|
||||
|> inspect(pretty: true, limit: :infinity)
|
||||
|> write_tmp()
|
||||
|
||||
file_diff(tmp_file1, tmp_file2)
|
||||
end
|
||||
else
|
||||
_ ->
|
||||
file_diff(file1, file2)
|
||||
end
|
||||
end
|
||||
|
||||
defp file_diff(file1, file2) do
|
||||
{diff, _} = System.cmd("diff", [file1, file2])
|
||||
diff
|
||||
end
|
||||
|
||||
defp relative_paths(dir) do
|
||||
dir
|
||||
|> Path.join("**")
|
||||
|> Path.wildcard()
|
||||
|> Enum.map(&Path.relative_to(&1, dir))
|
||||
end
|
||||
|
||||
defp assert_dir!(dir) do
|
||||
unless File.dir?(dir) do
|
||||
raise ArgumentError, "#{inspect(dir)} is not a directory"
|
||||
end
|
||||
end
|
||||
|
||||
defp write_tmp(content) do
|
||||
filename = generate_tmp_filename()
|
||||
File.mkdir_p!("tmp")
|
||||
File.write!(Path.join("tmp", filename), content)
|
||||
Path.join("tmp", filename)
|
||||
end
|
||||
|
||||
defp generate_tmp_filename do
|
||||
sec = :os.system_time(:second)
|
||||
rand = :rand.uniform(999_999_999)
|
||||
scheduler_id = :erlang.system_info(:scheduler_id)
|
||||
"tmp-#{sec}-#{rand}-#{scheduler_id}"
|
||||
end
|
||||
end
|
||||
|
||||
case System.argv() do
|
||||
[dir1, dir2] ->
|
||||
case Diff.compare_dirs(dir1, dir2) do
|
||||
{[], [], []} ->
|
||||
IO.puts("#{inspect(dir1)} and #{inspect(dir2)} are equal")
|
||||
|
||||
{only1, only2, diff} ->
|
||||
for path <- only1, do: IO.puts("Only in #{dir1}: #{path}")
|
||||
for path <- only2, do: IO.puts("Only in #{dir2}: #{path}")
|
||||
for {path, diff} <- diff, do: IO.puts("Diff #{path}:\n#{diff}")
|
||||
|
||||
System.halt(1)
|
||||
end
|
||||
|
||||
_ ->
|
||||
IO.puts("Please, provide two directories as arguments")
|
||||
System.halt(1)
|
||||
end
|
||||
+14
-11
@@ -1,10 +1,10 @@
|
||||
# Returns config for Elixir docs
|
||||
[
|
||||
extras: Path.wildcard("lib/elixir/pages/*.md"),
|
||||
extras: Path.wildcard("lib/elixir/pages/*.md") ++ ["CHANGELOG.md"],
|
||||
groups_for_functions: [
|
||||
Guards: & &1[:guard] == true
|
||||
],
|
||||
skip_undefined_reference_warnings_on: ["compatibility-and-deprecations"],
|
||||
skip_undefined_reference_warnings_on: ["lib/elixir/pages/compatibility-and-deprecations.md"],
|
||||
groups_for_modules: [
|
||||
# [Kernel, Kernel.SpecialForms],
|
||||
|
||||
@@ -89,15 +89,18 @@
|
||||
Kernel.ParallelCompiler,
|
||||
Macro,
|
||||
Macro.Env
|
||||
],
|
||||
Deprecated: [
|
||||
Behaviour,
|
||||
Dict,
|
||||
GenEvent,
|
||||
HashDict,
|
||||
HashSet,
|
||||
Set,
|
||||
Supervisor.Spec
|
||||
]
|
||||
|
||||
## Automatically detected groups
|
||||
|
||||
# Deprecated: [
|
||||
# Behaviour,
|
||||
# Dict,
|
||||
# GenEvent,
|
||||
# HashDict,
|
||||
# HashSet,
|
||||
# Set,
|
||||
# Supervisor.Spec
|
||||
# ]
|
||||
]
|
||||
]
|
||||
|
||||
+43
-11
@@ -6,8 +6,8 @@ defmodule Access do
|
||||
keys of any type in a data structure via the `data[key]` syntax.
|
||||
|
||||
`Access` supports keyword lists (`Keyword`) and maps (`Map`) out
|
||||
of the box. The key can be of any type and it returns `nil` if
|
||||
the key does not exist:
|
||||
of the box. Keywords supports only atoms keys, keys for maps can
|
||||
be of any type. Both returns `nil` if the key does not exist:
|
||||
|
||||
iex> keywords = [a: 1, b: 2]
|
||||
iex> keywords[:a]
|
||||
@@ -683,36 +683,68 @@ defmodule Access do
|
||||
end
|
||||
|
||||
defp at(:get_and_update, data, index, next) when is_list(data) do
|
||||
get_and_update_at(data, index, next, [])
|
||||
get_and_update_at(data, index, next, [], fn -> nil end)
|
||||
end
|
||||
|
||||
defp at(_op, data, _index, _next) do
|
||||
raise "Access.at/1 expected a list, got: #{inspect(data)}"
|
||||
end
|
||||
|
||||
defp get_and_update_at([head | rest], 0, next, updates) do
|
||||
defp get_and_update_at([head | rest], 0, next, updates, _default_fun) do
|
||||
case next.(head) do
|
||||
{get, update} -> {get, :lists.reverse([update | updates], rest)}
|
||||
:pop -> {head, :lists.reverse(updates, rest)}
|
||||
end
|
||||
end
|
||||
|
||||
defp get_and_update_at(list, index, next, updates) when index < 0 do
|
||||
defp get_and_update_at([_ | _] = list, index, next, updates, default_fun) when index < 0 do
|
||||
list_length = length(list)
|
||||
|
||||
if list_length + index >= 0 do
|
||||
get_and_update_at(list, list_length + index, next, updates)
|
||||
get_and_update_at(list, list_length + index, next, updates, default_fun)
|
||||
else
|
||||
{nil, list}
|
||||
{default_fun.(), list}
|
||||
end
|
||||
end
|
||||
|
||||
defp get_and_update_at([head | rest], index, next, updates) when index > 0 do
|
||||
get_and_update_at(rest, index - 1, next, [head | updates])
|
||||
defp get_and_update_at([head | rest], index, next, updates, default_fun) when index > 0 do
|
||||
get_and_update_at(rest, index - 1, next, [head | updates], default_fun)
|
||||
end
|
||||
|
||||
defp get_and_update_at([], _index, _next, updates) do
|
||||
{nil, :lists.reverse(updates)}
|
||||
defp get_and_update_at([], _index, _next, updates, default_fun) do
|
||||
{default_fun.(), :lists.reverse(updates)}
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
Same as `at/1` except that it raises `Enum.OutOfBoundsError`
|
||||
if the given index is out of bounds.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> get_in([:a, :b, :c], [Access.at!(2)])
|
||||
:c
|
||||
iex> get_in([:a, :b, :c], [Access.at!(3)])
|
||||
** (Enum.OutOfBoundsError) out of bounds error
|
||||
|
||||
"""
|
||||
@spec at!(integer) :: access_fun(data :: list, get_value :: term)
|
||||
def at!(index) when is_integer(index) do
|
||||
fn op, data, next -> at!(op, data, index, next) end
|
||||
end
|
||||
|
||||
defp at!(:get, data, index, next) when is_list(data) do
|
||||
case Enum.fetch(data, index) do
|
||||
{:ok, value} -> next.(value)
|
||||
:error -> raise Enum.OutOfBoundsError
|
||||
end
|
||||
end
|
||||
|
||||
defp at!(:get_and_update, data, index, next) when is_list(data) do
|
||||
get_and_update_at(data, index, next, [], fn -> raise Enum.OutOfBoundsError end)
|
||||
end
|
||||
|
||||
defp at!(_op, data, _index, _next) do
|
||||
raise "Access.at!/1 expected a list, got: #{inspect(data)}"
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
|
||||
@@ -7,7 +7,7 @@ defmodule Application do
|
||||
programming languages, but with some additional characteristics.
|
||||
|
||||
An application is a component implementing some specific functionality, with a
|
||||
standardized directory structure, configuration, and lifecycle. Applications
|
||||
standardized directory structure, configuration, and life cycle. Applications
|
||||
are *loaded*, *started*, and *stopped*. Each application also has its own
|
||||
environment, which provides a unified API for configuring each application.
|
||||
|
||||
@@ -84,8 +84,8 @@ defmodule Application do
|
||||
|
||||
This approach has one big limitation: if you change the value of the
|
||||
application environment after the code is compiled, the value used at
|
||||
runtime is not going to change! For example, if you are using `mix release`
|
||||
and your `config/releases.exs` has:
|
||||
runtime is not going to change! For example, if your `config/runtime.exs`
|
||||
has:
|
||||
|
||||
config :my_app, :db_host, "db.production"
|
||||
|
||||
@@ -171,7 +171,7 @@ defmodule Application do
|
||||
the documentation of `Mix.Tasks.Compile.App`, available as well by running
|
||||
`mix help compile.app`.
|
||||
|
||||
## The application lifecycle
|
||||
## The application life cycle
|
||||
|
||||
### Loading applications
|
||||
|
||||
@@ -210,7 +210,7 @@ defmodule Application do
|
||||
dependencies for you.
|
||||
|
||||
If the application does not have a callback module configured, starting is
|
||||
done at this point. Otherwise, its `c:start/2` callback if invoked. The PID of
|
||||
done at this point. Otherwise, its `c:start/2` callback is invoked. The PID of
|
||||
the top-level supervisor returned by this function is stored by the runtime
|
||||
for later use, and the returned application state is saved too, if any.
|
||||
|
||||
@@ -499,7 +499,7 @@ defmodule Application do
|
||||
Giving a path is useful to let Elixir know that only certain paths
|
||||
in a large configuration are compile time dependent.
|
||||
"""
|
||||
# TODO: Warn if get_env/fetch_env/fetch_env! is used at compile time instead of compile_env
|
||||
# TODO: Warn on v1.14 if get_env/fetch_env/fetch_env! is used at compile time instead of compile_env
|
||||
@doc since: "1.10.0"
|
||||
@spec compile_env(app, key | list, value) :: value
|
||||
defmacro compile_env(app, key_or_path, default \\ nil) when is_atom(app) do
|
||||
@@ -524,7 +524,7 @@ defmodule Application do
|
||||
Reads the application environment at compilation time or raises.
|
||||
|
||||
This is the same as `compile_env/3` but it raises an
|
||||
ArgumentError if the configuration is not available.
|
||||
`ArgumentError` if the configuration is not available.
|
||||
"""
|
||||
@doc since: "1.10.0"
|
||||
@spec compile_env!(app, key | list) :: value
|
||||
@@ -584,7 +584,7 @@ defmodule Application do
|
||||
**Important:** 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.html).
|
||||
read our [library guidelines](library-guidelines.md).
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -618,6 +618,7 @@ defmodule Application do
|
||||
"""
|
||||
@spec get_env(app, key, value) :: value
|
||||
def get_env(app, key, default \\ nil) when is_atom(app) do
|
||||
maybe_warn_on_app_env_key(app, key)
|
||||
:application.get_env(app, key, default)
|
||||
end
|
||||
|
||||
@@ -628,6 +629,8 @@ defmodule Application do
|
||||
"""
|
||||
@spec fetch_env(app, key) :: {:ok, value} | :error
|
||||
def fetch_env(app, key) when is_atom(app) do
|
||||
maybe_warn_on_app_env_key(app, key)
|
||||
|
||||
case :application.get_env(app, key) do
|
||||
{:ok, value} -> {:ok, value}
|
||||
:undefined -> :error
|
||||
@@ -664,9 +667,7 @@ defmodule Application do
|
||||
"because configuration at #{inspect(key)} was not set"
|
||||
|
||||
:undefined ->
|
||||
"because the application was not loaded/started. If your application " <>
|
||||
"depends on #{inspect(app)} at runtime, make sure to load/start it or " <>
|
||||
"list it under :extra_applications in your mix.exs file"
|
||||
"because the application was not loaded nor configured"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -689,6 +690,7 @@ defmodule Application do
|
||||
"""
|
||||
@spec put_env(app, key, value, timeout: timeout, persistent: boolean) :: :ok
|
||||
def put_env(app, key, value, opts \\ []) when is_atom(app) do
|
||||
maybe_warn_on_app_env_key(app, key)
|
||||
:application.set_env(app, key, value, opts)
|
||||
end
|
||||
|
||||
@@ -734,9 +736,18 @@ defmodule Application do
|
||||
"""
|
||||
@spec delete_env(app, key, timeout: timeout, persistent: boolean) :: :ok
|
||||
def delete_env(app, key, opts \\ []) when is_atom(app) do
|
||||
maybe_warn_on_app_env_key(app, key)
|
||||
:application.unset_env(app, key, opts)
|
||||
end
|
||||
|
||||
defp maybe_warn_on_app_env_key(_app, key) when is_atom(key),
|
||||
do: :ok
|
||||
|
||||
defp maybe_warn_on_app_env_key(app, key) do
|
||||
message = "passing non-atom as application env key is deprecated, got: #{inspect(key)}"
|
||||
IO.warn_once({Application, :key, app, key}, message, _stacktrace_drop_levels = 2)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Ensures the given `app` is started.
|
||||
|
||||
|
||||
@@ -111,9 +111,9 @@ defmodule Behaviour do
|
||||
end
|
||||
end
|
||||
|
||||
defp __behaviour__doc_value(:none), do: nil
|
||||
defp __behaviour__doc_value(:hidden), do: false
|
||||
defp __behaviour__doc_value(%{"en" => doc}), do: doc
|
||||
defp __behaviour__doc_value(:hidden), do: false
|
||||
defp __behaviour__doc_value(_), do: nil
|
||||
|
||||
import unquote(__MODULE__)
|
||||
end
|
||||
|
||||
@@ -61,7 +61,7 @@ defmodule Bitwise do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Calculates the bitwise NOT of its argument.
|
||||
Calculates the bitwise NOT of the argument.
|
||||
|
||||
Allowed in guard tests. Inlined by the compiler.
|
||||
|
||||
@@ -81,7 +81,9 @@ defmodule Bitwise do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Prefix (unary) operator; calculates the bitwise NOT of its argument.
|
||||
Bitwise NOT unary operator.
|
||||
|
||||
Calculates the bitwise NOT of the argument.
|
||||
|
||||
Allowed in guard tests. Inlined by the compiler.
|
||||
|
||||
@@ -118,7 +120,9 @@ defmodule Bitwise do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Infix operator; calculates the bitwise AND of its arguments.
|
||||
Bitwise AND operator.
|
||||
|
||||
Calculates the bitwise AND of its arguments.
|
||||
|
||||
Allowed in guard tests. Inlined by the compiler.
|
||||
|
||||
@@ -152,7 +156,9 @@ defmodule Bitwise do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Infix operator; calculates the bitwise OR of its arguments.
|
||||
Bitwise OR operator.
|
||||
|
||||
Calculates the bitwise OR of its arguments.
|
||||
|
||||
Allowed in guard tests. Inlined by the compiler.
|
||||
|
||||
@@ -186,7 +192,9 @@ defmodule Bitwise do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Infix operator; calculates the bitwise XOR of its arguments.
|
||||
Bitwise XOR operator.
|
||||
|
||||
Calculates the bitwise XOR of its arguments.
|
||||
|
||||
Allowed in guard tests. Inlined by the compiler.
|
||||
|
||||
@@ -229,7 +237,9 @@ defmodule Bitwise do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Infix operator; calculates the result of an arithmetic left bitshift.
|
||||
Arithmetic left bitshift operator.
|
||||
|
||||
Calculates the result of an arithmetic left bitshift.
|
||||
|
||||
Allowed in guard tests. Inlined by the compiler.
|
||||
|
||||
@@ -281,7 +291,9 @@ defmodule Bitwise do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Infix operator; calculates the result of an arithmetic right bitshift.
|
||||
Arithmetic right bitshift operator.
|
||||
|
||||
Calculates the result of an arithmetic right bitshift.
|
||||
|
||||
Allowed in guard tests. Inlined by the compiler.
|
||||
|
||||
|
||||
+520
-3
@@ -68,10 +68,18 @@ defmodule Calendar do
|
||||
@typedoc "The time zone abbreviation (for example, CET or CEST or BST, and such)"
|
||||
@type zone_abbr :: String.t()
|
||||
|
||||
@typedoc "The time zone UTC offset in seconds"
|
||||
@typedoc """
|
||||
The time zone UTC offset in seconds for standard time.
|
||||
|
||||
See also `t:std_offset/0`.
|
||||
"""
|
||||
@type utc_offset :: integer
|
||||
|
||||
@typedoc "The time zone standard offset in seconds (not zero in summer times)"
|
||||
@typedoc """
|
||||
The time zone standard offset in seconds (typically not zero in summer times).
|
||||
|
||||
It must be added to `t:utc_offset/0` to get the total offset from UTC used for "wall time".
|
||||
"""
|
||||
@type std_offset :: integer
|
||||
|
||||
@typedoc "Any map/struct that contains the date fields"
|
||||
@@ -159,8 +167,14 @@ defmodule Calendar do
|
||||
|
||||
@doc """
|
||||
Calculates the day of the week from the given `year`, `month`, and `day`.
|
||||
|
||||
The `starting_on` represents the starting day of the week. All
|
||||
calendars must support at least the `:default` value. They may
|
||||
also support other values representing their days of the week.
|
||||
"""
|
||||
@callback day_of_week(year, month, day) :: day_of_week()
|
||||
@callback day_of_week(year, month, day, starting_on :: :default | atom) ::
|
||||
{day_of_week(), first_day_of_week :: non_neg_integer(),
|
||||
last_day_of_week :: non_neg_integer()}
|
||||
|
||||
@doc """
|
||||
Calculates the day of the year from the given `year`, `month`, and `day`.
|
||||
@@ -363,4 +377,507 @@ defmodule Calendar do
|
||||
def get_time_zone_database() do
|
||||
Application.get_env(:elixir, :time_zone_database, Calendar.UTCOnlyTimeZoneDatabase)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Formats received datetime into a string.
|
||||
|
||||
The datetime can be any of the Calendar types (`Time`, `Date`,
|
||||
`NaiveDateTime`, and `DateTime`) or any map, as long as they
|
||||
contain all of the relevant fields necessary for formatting.
|
||||
For example, if you use `%Y` to format the year, the datetime
|
||||
must have the `:year` field. Therefore, if you pass a `Time`,
|
||||
or a map without the `:year` field to a format that expects `%Y`,
|
||||
an error will be raised.
|
||||
|
||||
## Options
|
||||
|
||||
* `:preferred_datetime` - a string for the preferred format to show datetimes,
|
||||
it can't contain the `%c` format and defaults to `"%Y-%m-%d %H:%M:%S"`
|
||||
if the option is not received
|
||||
|
||||
* `:preferred_date` - a string for the preferred format to show dates,
|
||||
it can't contain the `%x` format and defaults to `"%Y-%m-%d"`
|
||||
if the option is not received
|
||||
|
||||
* `:preferred_time` - a string for the preferred format to show times,
|
||||
it can't contain the `%X` format and defaults to `"%H:%M:%S"`
|
||||
if the option is not received
|
||||
|
||||
* `:am_pm_names` - a function that receives either `:am` or `:pm` and returns
|
||||
the name of the period of the day, if the option is not received it defaults
|
||||
to a function that returns `"am"` and `"pm"`, respectively
|
||||
|
||||
* `:month_names` - a function that receives a number and returns the name of
|
||||
the corresponding month, if the option is not received it defaults to a
|
||||
function that returns the month names in English
|
||||
|
||||
* `:abbreviated_month_names` - a function that receives a number and returns the
|
||||
abbreviated name of the corresponding month, if the option is not received it
|
||||
defaults to a function that returns the abbreviated month names in English
|
||||
|
||||
* `:day_of_week_names` - a function that receives a number and returns the name of
|
||||
the corresponding day of week, if the option is not received it defaults to a
|
||||
function that returns the day of week names in English
|
||||
|
||||
* `:abbreviated_day_of_week_names` - a function that receives a number and returns
|
||||
the abbreviated name of the corresponding day of week, if the option is not received
|
||||
it defaults to a function that returns the abbreviated day of week names in English
|
||||
|
||||
## Formatting syntax
|
||||
|
||||
The formatting syntax for strftime is a sequence of characters in the following format:
|
||||
|
||||
%<padding><width><format>
|
||||
|
||||
where:
|
||||
|
||||
* `%`: indicates the start of a formatted section
|
||||
* `<padding>`: set the padding (see below)
|
||||
* `<width>`: a number indicating the minimum size of the formatted section
|
||||
* `<format>`: the format itself (see below)
|
||||
|
||||
### Accepted padding options
|
||||
|
||||
* `-`: no padding, removes all padding from the format
|
||||
* `_`: pad with spaces
|
||||
* `0`: pad with zeroes
|
||||
|
||||
### Accepted formats
|
||||
|
||||
The accepted formats are:
|
||||
|
||||
Format | Description | Examples (in ISO)
|
||||
:----- | :-----------------------------------------------------------------------| :------------------------
|
||||
a | Abbreviated name of day | Mon
|
||||
A | Full name of day | Monday
|
||||
b | Abbreviated month name | Jan
|
||||
B | Full month name | January
|
||||
c | Preferred date+time representation | 2018-10-17 12:34:56
|
||||
d | Day of the month | 01, 12
|
||||
f | Microseconds *(does not support width and padding modifiers)* | 000000, 999999, 0123
|
||||
H | Hour using a 24-hour clock | 00, 23
|
||||
I | Hour using a 12-hour clock | 01, 12
|
||||
j | Day of the year | 001, 366
|
||||
m | Month | 01, 12
|
||||
M | Minute | 00, 59
|
||||
p | "AM" or "PM" (noon is "PM", midnight as "AM") | AM, PM
|
||||
P | "am" or "pm" (noon is "pm", midnight as "am") | am, pm
|
||||
q | Quarter | 1, 2, 3, 4
|
||||
S | Second | 00, 59, 60
|
||||
u | Day of the week | 1 (Monday), 7 (Sunday)
|
||||
x | Preferred date (without time) representation | 2018-10-17
|
||||
X | Preferred time (without date) representation | 12:34:56
|
||||
y | Year as 2-digits | 01, 01, 86, 18
|
||||
Y | Year | -0001, 0001, 1986
|
||||
z | +hhmm/-hhmm time zone offset from UTC (empty string if naive) | +0300, -0530
|
||||
Z | Time zone abbreviation (empty string if naive) | CET, BRST
|
||||
% | Literal "%" character | %
|
||||
|
||||
Any other character will be interpreted as an invalid format and raise an error
|
||||
|
||||
## Examples
|
||||
|
||||
Without options:
|
||||
|
||||
iex> Calendar.strftime(~U[2019-08-26 13:52:06.0Z], "%y-%m-%d %I:%M:%S %p")
|
||||
"19-08-26 01:52:06 PM"
|
||||
|
||||
iex> Calendar.strftime(~U[2019-08-26 13:52:06.0Z], "%a, %B %d %Y")
|
||||
"Mon, August 26 2019"
|
||||
|
||||
iex> Calendar.strftime(~U[2019-08-26 13:52:06.0Z], "%c")
|
||||
"2019-08-26 13:52:06"
|
||||
|
||||
With options:
|
||||
|
||||
iex> Calendar.strftime(~U[2019-08-26 13:52:06.0Z], "%c", preferred_datetime: "%H:%M:%S %d-%m-%y")
|
||||
"13:52:06 26-08-19"
|
||||
|
||||
iex> Calendar.strftime(
|
||||
...> ~U[2019-08-26 13:52:06.0Z],
|
||||
...> "%A",
|
||||
...> day_of_week_names: fn day_of_week ->
|
||||
...> {"segunda-feira", "terça-feira", "quarta-feira", "quinta-feira",
|
||||
...> "sexta-feira", "sábado", "domingo"}
|
||||
...> |> elem(day_of_week - 1)
|
||||
...> end
|
||||
...>)
|
||||
"segunda-feira"
|
||||
|
||||
iex> Calendar.strftime(
|
||||
...> ~U[2019-08-26 13:52:06.0Z],
|
||||
...> "%B",
|
||||
...> month_names: fn month ->
|
||||
...> {"январь", "февраль", "март", "апрель", "май", "июнь",
|
||||
...> "июль", "август", "сентябрь", "октябрь", "ноябрь", "декабрь"}
|
||||
...> |> elem(month - 1)
|
||||
...> end
|
||||
...>)
|
||||
"август"
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
@spec strftime(map(), String.t(), keyword()) :: String.t()
|
||||
def strftime(date_or_time_or_datetime, string_format, user_options \\ [])
|
||||
when is_map(date_or_time_or_datetime) and is_binary(string_format) do
|
||||
parse(
|
||||
string_format,
|
||||
date_or_time_or_datetime,
|
||||
options(user_options),
|
||||
[]
|
||||
)
|
||||
|> IO.iodata_to_binary()
|
||||
end
|
||||
|
||||
defp parse("", _datetime, _format_options, acc),
|
||||
do: Enum.reverse(acc)
|
||||
|
||||
defp parse("%" <> rest, datetime, format_options, acc),
|
||||
do: parse_modifiers(rest, nil, nil, {datetime, format_options, acc})
|
||||
|
||||
defp parse(<<char, rest::binary>>, datetime, format_options, acc),
|
||||
do: parse(rest, datetime, format_options, [char | acc])
|
||||
|
||||
defp parse_modifiers("-" <> rest, width, nil, parser_data) do
|
||||
parse_modifiers(rest, width, "", parser_data)
|
||||
end
|
||||
|
||||
defp parse_modifiers("0" <> rest, width, nil, parser_data) do
|
||||
parse_modifiers(rest, width, ?0, parser_data)
|
||||
end
|
||||
|
||||
defp parse_modifiers("_" <> rest, width, nil, parser_data) do
|
||||
parse_modifiers(rest, width, ?\s, parser_data)
|
||||
end
|
||||
|
||||
defp parse_modifiers(<<digit, rest::binary>>, width, pad, parser_data) when digit in ?0..?9 do
|
||||
new_width = (width || 0) * 10 + (digit - ?0)
|
||||
|
||||
parse_modifiers(rest, new_width, pad, parser_data)
|
||||
end
|
||||
|
||||
# set default padding if none was specified
|
||||
defp parse_modifiers(<<format, _::binary>> = rest, width, nil, parser_data) do
|
||||
parse_modifiers(rest, width, default_pad(format), parser_data)
|
||||
end
|
||||
|
||||
# set default width if none was specified
|
||||
defp parse_modifiers(<<format, _::binary>> = rest, nil, pad, parser_data) do
|
||||
parse_modifiers(rest, default_width(format), pad, parser_data)
|
||||
end
|
||||
|
||||
defp parse_modifiers(rest, width, pad, {datetime, format_options, acc}) do
|
||||
format_modifiers(rest, width, pad, datetime, format_options, acc)
|
||||
end
|
||||
|
||||
defp am_pm(hour, format_options) when hour > 11 do
|
||||
format_options.am_pm_names.(:pm)
|
||||
end
|
||||
|
||||
defp am_pm(hour, format_options) when hour <= 11 do
|
||||
format_options.am_pm_names.(:am)
|
||||
end
|
||||
|
||||
defp default_pad(format) when format in 'aAbBpPZ', do: ?\s
|
||||
defp default_pad(_format), do: ?0
|
||||
|
||||
defp default_width(format) when format in 'dHImMSy', do: 2
|
||||
defp default_width(?j), do: 3
|
||||
defp default_width(format) when format in 'Yz', do: 4
|
||||
defp default_width(_format), do: 0
|
||||
|
||||
# Literally just %
|
||||
defp format_modifiers("%" <> rest, width, pad, datetime, format_options, acc) do
|
||||
parse(rest, datetime, format_options, [pad_leading("%", width, pad) | acc])
|
||||
end
|
||||
|
||||
# Abbreviated name of day
|
||||
defp format_modifiers("a" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result =
|
||||
datetime
|
||||
|> Date.day_of_week()
|
||||
|> format_options.abbreviated_day_of_week_names.()
|
||||
|> pad_leading(width, pad)
|
||||
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Full name of day
|
||||
defp format_modifiers("A" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result =
|
||||
datetime
|
||||
|> Date.day_of_week()
|
||||
|> format_options.day_of_week_names.()
|
||||
|> pad_leading(width, pad)
|
||||
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Abbreviated month name
|
||||
defp format_modifiers("b" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result =
|
||||
datetime.month
|
||||
|> format_options.abbreviated_month_names.()
|
||||
|> pad_leading(width, pad)
|
||||
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Full month name
|
||||
defp format_modifiers("B" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result = datetime.month |> format_options.month_names.() |> pad_leading(width, pad)
|
||||
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Preferred date+time representation
|
||||
defp format_modifiers(
|
||||
"c" <> _rest,
|
||||
_width,
|
||||
_pad,
|
||||
_datetime,
|
||||
%{preferred_datetime_invoked: true},
|
||||
_acc
|
||||
) do
|
||||
raise ArgumentError,
|
||||
"tried to format preferred_datetime within another preferred_datetime format"
|
||||
end
|
||||
|
||||
defp format_modifiers("c" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result =
|
||||
format_options.preferred_datetime
|
||||
|> parse(datetime, %{format_options | preferred_datetime_invoked: true}, [])
|
||||
|> pad_preferred(width, pad)
|
||||
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Day of the month
|
||||
defp format_modifiers("d" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result = datetime.day |> Integer.to_string() |> pad_leading(width, pad)
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Microseconds
|
||||
defp format_modifiers("f" <> rest, _width, _pad, datetime, format_options, acc) do
|
||||
{microsecond, precision} = datetime.microsecond
|
||||
|
||||
result =
|
||||
microsecond
|
||||
|> Integer.to_string()
|
||||
|> String.pad_leading(6, "0")
|
||||
|> binary_part(0, max(precision, 1))
|
||||
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Hour using a 24-hour clock
|
||||
defp format_modifiers("H" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result = datetime.hour |> Integer.to_string() |> pad_leading(width, pad)
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Hour using a 12-hour clock
|
||||
defp format_modifiers("I" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result = (rem(datetime.hour() + 23, 12) + 1) |> Integer.to_string() |> pad_leading(width, pad)
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Day of the year
|
||||
defp format_modifiers("j" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result = datetime |> Date.day_of_year() |> Integer.to_string() |> pad_leading(width, pad)
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Month
|
||||
defp format_modifiers("m" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result = datetime.month |> Integer.to_string() |> pad_leading(width, pad)
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Minute
|
||||
defp format_modifiers("M" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result = datetime.minute |> Integer.to_string() |> pad_leading(width, pad)
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# "AM" or "PM" (noon is "PM", midnight as "AM")
|
||||
defp format_modifiers("p" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result = datetime.hour |> am_pm(format_options) |> String.upcase() |> pad_leading(width, pad)
|
||||
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# "am" or "pm" (noon is "pm", midnight as "am")
|
||||
defp format_modifiers("P" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result =
|
||||
datetime.hour
|
||||
|> am_pm(format_options)
|
||||
|> String.downcase()
|
||||
|> pad_leading(width, pad)
|
||||
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Quarter
|
||||
defp format_modifiers("q" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result = datetime |> Date.quarter_of_year() |> Integer.to_string() |> pad_leading(width, pad)
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Second
|
||||
defp format_modifiers("S" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result = datetime.second |> Integer.to_string() |> pad_leading(width, pad)
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Day of the week
|
||||
defp format_modifiers("u" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result = datetime |> Date.day_of_week() |> Integer.to_string() |> pad_leading(width, pad)
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Preferred date (without time) representation
|
||||
defp format_modifiers(
|
||||
"x" <> _rest,
|
||||
_width,
|
||||
_pad,
|
||||
_datetime,
|
||||
%{preferred_date_invoked: true},
|
||||
_acc
|
||||
) do
|
||||
raise ArgumentError,
|
||||
"tried to format preferred_date within another preferred_date format"
|
||||
end
|
||||
|
||||
defp format_modifiers("x" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result =
|
||||
format_options.preferred_date
|
||||
|> parse(datetime, %{format_options | preferred_date_invoked: true}, [])
|
||||
|> pad_preferred(width, pad)
|
||||
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Preferred time (without date) representation
|
||||
defp format_modifiers(
|
||||
"X" <> _rest,
|
||||
_width,
|
||||
_pad,
|
||||
_datetime,
|
||||
%{preferred_time_invoked: true},
|
||||
_acc
|
||||
) do
|
||||
raise ArgumentError,
|
||||
"tried to format preferred_time within another preferred_time format"
|
||||
end
|
||||
|
||||
defp format_modifiers("X" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result =
|
||||
format_options.preferred_time
|
||||
|> parse(datetime, %{format_options | preferred_time_invoked: true}, [])
|
||||
|> pad_preferred(width, pad)
|
||||
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Year as 2-digits
|
||||
defp format_modifiers("y" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result = datetime.year |> rem(100) |> Integer.to_string() |> pad_leading(width, pad)
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# Year
|
||||
defp format_modifiers("Y" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result = datetime.year |> Integer.to_string() |> pad_leading(width, pad)
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
# +hhmm/-hhmm time zone offset from UTC (empty string if naive)
|
||||
defp format_modifiers(
|
||||
"z" <> rest,
|
||||
width,
|
||||
pad,
|
||||
datetime = %{utc_offset: utc_offset, std_offset: std_offset},
|
||||
format_options,
|
||||
acc
|
||||
) do
|
||||
absolute_offset = abs(utc_offset + std_offset)
|
||||
|
||||
offset_number =
|
||||
Integer.to_string(div(absolute_offset, 3600) * 100 + rem(div(absolute_offset, 60), 60))
|
||||
|
||||
sign = if utc_offset + std_offset >= 0, do: "+", else: "-"
|
||||
result = "#{sign}#{pad_leading(offset_number, width, pad)}"
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
defp format_modifiers("z" <> rest, _width, _pad, datetime, format_options, acc) do
|
||||
parse(rest, datetime, format_options, ["" | acc])
|
||||
end
|
||||
|
||||
# Time zone abbreviation (empty string if naive)
|
||||
defp format_modifiers("Z" <> rest, width, pad, datetime, format_options, acc) do
|
||||
result = datetime |> Map.get(:zone_abbr, "") |> pad_leading(width, pad)
|
||||
parse(rest, datetime, format_options, [result | acc])
|
||||
end
|
||||
|
||||
defp format_modifiers(rest, _width, _pad, _datetime, _format_options, _acc) do
|
||||
{next, _rest} = String.next_grapheme(rest) || {"", ""}
|
||||
raise ArgumentError, "invalid strftime format: %#{next}"
|
||||
end
|
||||
|
||||
defp pad_preferred(result, width, pad) when length(result) < width do
|
||||
pad_preferred([pad | result], width, pad)
|
||||
end
|
||||
|
||||
defp pad_preferred(result, _width, _pad), do: result
|
||||
|
||||
defp pad_leading(string, count, padding) do
|
||||
to_pad = count - byte_size(string)
|
||||
if to_pad > 0, do: do_pad_leading(to_pad, padding, string), else: string
|
||||
end
|
||||
|
||||
defp do_pad_leading(0, _, acc), do: acc
|
||||
|
||||
defp do_pad_leading(count, padding, acc),
|
||||
do: do_pad_leading(count - 1, padding, [padding | acc])
|
||||
|
||||
defp options(user_options) do
|
||||
default_options = %{
|
||||
preferred_date: "%Y-%m-%d",
|
||||
preferred_time: "%H:%M:%S",
|
||||
preferred_datetime: "%Y-%m-%d %H:%M:%S",
|
||||
am_pm_names: fn
|
||||
:am -> "am"
|
||||
:pm -> "pm"
|
||||
end,
|
||||
month_names: fn month ->
|
||||
{"January", "February", "March", "April", "May", "June", "July", "August", "September",
|
||||
"October", "November", "December"}
|
||||
|> elem(month - 1)
|
||||
end,
|
||||
day_of_week_names: fn day_of_week ->
|
||||
{"Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"}
|
||||
|> elem(day_of_week - 1)
|
||||
end,
|
||||
abbreviated_month_names: fn month ->
|
||||
{"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}
|
||||
|> elem(month - 1)
|
||||
end,
|
||||
abbreviated_day_of_week_names: fn day_of_week ->
|
||||
{"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"} |> elem(day_of_week - 1)
|
||||
end,
|
||||
preferred_datetime_invoked: false,
|
||||
preferred_date_invoked: false,
|
||||
preferred_time_invoked: false
|
||||
}
|
||||
|
||||
Enum.reduce(user_options, default_options, fn {key, value}, acc ->
|
||||
if Map.has_key?(acc, key) do
|
||||
%{acc | key => value}
|
||||
else
|
||||
raise ArgumentError, "unknown option #{inspect(key)} given to Calendar.strftime/3"
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -88,20 +88,20 @@ defmodule Date do
|
||||
|
||||
"""
|
||||
@doc since: "1.5.0"
|
||||
@spec range(Date.t(), Date.t()) :: Date.Range.t()
|
||||
def range(%Date{calendar: calendar} = first, %Date{calendar: calendar} = last) do
|
||||
@spec range(Calendar.date(), Calendar.date()) :: Date.Range.t()
|
||||
def range(%{calendar: calendar} = first, %{calendar: calendar} = last) do
|
||||
{first_days, _} = to_iso_days(first)
|
||||
{last_days, _} = to_iso_days(last)
|
||||
|
||||
%Date.Range{
|
||||
first: first,
|
||||
last: last,
|
||||
first: %Date{calendar: calendar, year: first.year, month: first.month, day: first.day},
|
||||
last: %Date{calendar: calendar, year: last.year, month: last.month, day: last.day},
|
||||
first_in_iso_days: first_days,
|
||||
last_in_iso_days: last_days
|
||||
}
|
||||
end
|
||||
|
||||
def range(%Date{}, %Date{}) do
|
||||
def range(%{calendar: _, year: _, month: _, day: _}, %{calendar: _, year: _, month: _, day: _}) do
|
||||
raise ArgumentError, "both dates must have matching calendars"
|
||||
end
|
||||
|
||||
@@ -224,6 +224,33 @@ defmodule Date do
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Builds a new ISO date.
|
||||
|
||||
Expects all values to be integers. Returns `date` if each
|
||||
entry fits its appropriate range, raises if the date is invalid.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Date.new!(2000, 1, 1)
|
||||
~D[2000-01-01]
|
||||
iex> Date.new!(2000, 13, 1)
|
||||
** (ArgumentError) cannot build date, reason: :invalid_date
|
||||
iex> Date.new!(2000, 2, 29)
|
||||
~D[2000-02-29]
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
@spec new!(Calendar.year(), Calendar.month(), Calendar.day(), Calendar.calendar()) :: t
|
||||
def new!(year, month, day, calendar \\ Calendar.ISO) do
|
||||
case new(year, month, day, calendar) do
|
||||
{:ok, value} ->
|
||||
value
|
||||
|
||||
{:error, reason} ->
|
||||
raise ArgumentError, "cannot build date, reason: #{inspect(reason)}"
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts the given date to a string according to its calendar.
|
||||
|
||||
@@ -398,6 +425,45 @@ defmodule Date do
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts a number of gregorian days to a `Date` struct.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Date.from_gregorian_days(1)
|
||||
~D[0000-01-02]
|
||||
iex> Date.from_gregorian_days(730_485)
|
||||
~D[2000-01-01]
|
||||
iex> Date.from_gregorian_days(-1)
|
||||
~D[-0001-12-31]
|
||||
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
@spec from_gregorian_days(integer(), Calendar.calendar()) :: t
|
||||
def from_gregorian_days(days, calendar \\ Calendar.ISO) when is_integer(days) do
|
||||
from_iso_days({days, 0}, calendar)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts a `date` struct to a number of gregorian days.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Date.to_gregorian_days(~D[0000-01-02])
|
||||
1
|
||||
iex> Date.to_gregorian_days(~D[2000-01-01])
|
||||
730_485
|
||||
iex> Date.to_gregorian_days(~N[2000-01-01 00:00:00])
|
||||
730_485
|
||||
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
@spec to_gregorian_days(Calendar.date()) :: integer()
|
||||
def to_gregorian_days(date) do
|
||||
{days, _} = to_iso_days(date)
|
||||
days
|
||||
end
|
||||
|
||||
@doc """
|
||||
Compares two date structs.
|
||||
|
||||
@@ -618,6 +684,11 @@ defmodule Date do
|
||||
calendar (the default), it is an integer from 1 to 7, where
|
||||
1 is Monday and 7 is Sunday.
|
||||
|
||||
An optional `starting_on` value may be supplied, which
|
||||
configures the weekday the week starts on. The default value
|
||||
for it is `:default`, which translates to `:monday` for the
|
||||
built-in ISO calendar. Any other weekday may be given to.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Date.day_of_week(~D[2016-10-31])
|
||||
@@ -629,13 +700,131 @@ defmodule Date do
|
||||
iex> Date.day_of_week(~D[-0015-10-30])
|
||||
3
|
||||
|
||||
iex> Date.day_of_week(~D[2016-10-31], :sunday)
|
||||
2
|
||||
iex> Date.day_of_week(~D[2016-11-01], :sunday)
|
||||
3
|
||||
iex> Date.day_of_week(~N[2016-11-01 01:23:45], :sunday)
|
||||
3
|
||||
iex> Date.day_of_week(~D[-0015-10-30], :sunday)
|
||||
4
|
||||
|
||||
"""
|
||||
@doc since: "1.4.0"
|
||||
@spec day_of_week(Calendar.date()) :: Calendar.day()
|
||||
def day_of_week(date)
|
||||
@spec day_of_week(Calendar.date(), starting_on :: :default | atom) :: Calendar.day_of_week()
|
||||
def day_of_week(date, starting_on \\ :default)
|
||||
|
||||
def day_of_week(%{calendar: calendar, year: year, month: month, day: day}) do
|
||||
calendar.day_of_week(year, month, day)
|
||||
def day_of_week(%{calendar: calendar, year: year, month: month, day: day}, starting_on) do
|
||||
{day_of_week, _first, _last} = calendar.day_of_week(year, month, day, starting_on)
|
||||
day_of_week
|
||||
end
|
||||
|
||||
@doc """
|
||||
Calculates a date that is the first day of the week for the given `date`.
|
||||
|
||||
If the day is already the first day of the week, it returns the
|
||||
day itself. For the built-in ISO calendar, the week starts on Monday.
|
||||
A weekday rather than `:default` can be given as `starting_on`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Date.beginning_of_week(~D[2020-07-11])
|
||||
~D[2020-07-06]
|
||||
iex> Date.beginning_of_week(~D[2020-07-06])
|
||||
~D[2020-07-06]
|
||||
iex> Date.beginning_of_week(~D[2020-07-11], :sunday)
|
||||
~D[2020-07-05]
|
||||
iex> Date.beginning_of_week(~D[2020-07-11], :saturday)
|
||||
~D[2020-07-11]
|
||||
iex> Date.beginning_of_week(~N[2020-07-11 01:23:45])
|
||||
~D[2020-07-06]
|
||||
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
@spec beginning_of_week(Calendar.date(), starting_on :: :default | atom) :: Date.t()
|
||||
def beginning_of_week(date, starting_on \\ :default)
|
||||
|
||||
def beginning_of_week(%{calendar: Calendar.ISO} = date, starting_on) do
|
||||
%{year: year, month: month, day: day} = date
|
||||
iso_days = Calendar.ISO.date_to_iso_days(year, month, day)
|
||||
|
||||
{year, month, day} =
|
||||
case Calendar.ISO.iso_days_to_day_of_week(iso_days, starting_on) do
|
||||
1 ->
|
||||
{year, month, day}
|
||||
|
||||
day_of_week ->
|
||||
Calendar.ISO.date_from_iso_days(iso_days - day_of_week + 1)
|
||||
end
|
||||
|
||||
%Date{calendar: Calendar.ISO, year: year, month: month, day: day}
|
||||
end
|
||||
|
||||
def beginning_of_week(%{calendar: calendar} = date, starting_on) do
|
||||
%{year: year, month: month, day: day} = date
|
||||
|
||||
case calendar.day_of_week(year, month, day, starting_on) do
|
||||
{day_of_week, day_of_week, _} ->
|
||||
%Date{calendar: calendar, year: year, month: month, day: day}
|
||||
|
||||
{day_of_week, first_day_of_week, _} ->
|
||||
add(date, -(day_of_week - first_day_of_week))
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Calculates a date that is the last day of the week for the given `date`.
|
||||
|
||||
If the day is already the last day of the week, it returns the
|
||||
day itself. For the built-in ISO calendar, the week ends on Sunday.
|
||||
A weekday rather than `:default` can be given as `starting_on`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Date.end_of_week(~D[2020-07-11])
|
||||
~D[2020-07-12]
|
||||
iex> Date.end_of_week(~D[2020-07-05])
|
||||
~D[2020-07-05]
|
||||
iex> Date.end_of_week(~D[2020-07-06], :sunday)
|
||||
~D[2020-07-11]
|
||||
iex> Date.end_of_week(~D[2020-07-06], :sunday)
|
||||
~D[2020-07-11]
|
||||
iex> Date.end_of_week(~D[2020-07-06], :saturday)
|
||||
~D[2020-07-10]
|
||||
iex> Date.end_of_week(~N[2020-07-11 01:23:45])
|
||||
~D[2020-07-12]
|
||||
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
@spec end_of_week(Calendar.date(), starting_on :: :default | atom) :: Date.t()
|
||||
def end_of_week(date, starting_on \\ :default)
|
||||
|
||||
def end_of_week(%{calendar: Calendar.ISO} = date, starting_on) do
|
||||
%{year: year, month: month, day: day} = date
|
||||
iso_days = Calendar.ISO.date_to_iso_days(year, month, day)
|
||||
|
||||
{year, month, day} =
|
||||
case Calendar.ISO.iso_days_to_day_of_week(iso_days, starting_on) do
|
||||
7 ->
|
||||
{year, month, day}
|
||||
|
||||
day_of_week ->
|
||||
Calendar.ISO.date_from_iso_days(iso_days + 7 - day_of_week)
|
||||
end
|
||||
|
||||
%Date{calendar: Calendar.ISO, year: year, month: month, day: day}
|
||||
end
|
||||
|
||||
def end_of_week(%{calendar: calendar} = date, starting_on) do
|
||||
%{year: year, month: month, day: day} = date
|
||||
|
||||
case calendar.day_of_week(year, month, day, starting_on) do
|
||||
{day_of_week, _, day_of_week} ->
|
||||
%Date{calendar: calendar, year: year, month: month, day: day}
|
||||
|
||||
{day_of_week, _, last_day_of_week} ->
|
||||
add(date, last_day_of_week - day_of_week)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -739,6 +928,45 @@ defmodule Date do
|
||||
calendar.day_of_era(year, month, day)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Calculates a date that is the first day of the month for the given `date`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Date.beginning_of_month(~D[2000-01-31])
|
||||
~D[2000-01-01]
|
||||
iex> Date.beginning_of_month(~D[2000-01-01])
|
||||
~D[2000-01-01]
|
||||
iex> Date.beginning_of_month(~N[2000-01-31 01:23:45])
|
||||
~D[2000-01-01]
|
||||
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
@spec beginning_of_month(Calendar.date()) :: t()
|
||||
def beginning_of_month(%{year: year, month: month, calendar: calendar}) do
|
||||
%Date{year: year, month: month, day: 1, calendar: calendar}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Calculates a date that is the last day of the month for the given `date`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Date.end_of_month(~D[2000-01-01])
|
||||
~D[2000-01-31]
|
||||
iex> Date.end_of_month(~D[2000-01-31])
|
||||
~D[2000-01-31]
|
||||
iex> Date.end_of_month(~N[2000-01-01 01:23:45])
|
||||
~D[2000-01-31]
|
||||
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
@spec end_of_month(Calendar.date()) :: t()
|
||||
def end_of_month(%{year: year, month: month, calendar: calendar} = date) do
|
||||
day = Date.days_in_month(date)
|
||||
%Date{year: year, month: month, day: day, calendar: calendar}
|
||||
end
|
||||
|
||||
## Helpers
|
||||
|
||||
defimpl String.Chars do
|
||||
|
||||
@@ -25,10 +25,14 @@ defmodule DateTime do
|
||||
datetimes and returns `{:error, :utc_only_time_zone_database}`
|
||||
for any other time zone.
|
||||
|
||||
Other time zone databases can also be configured. For example, to use the
|
||||
[tzdata](https://hexdocs.pm/tzdata/) database, first make sure it is added as
|
||||
a dependency in `mix.exs`. It can then be configured either via
|
||||
configuration:
|
||||
Other time zone databases can also be configured. For example,
|
||||
two of the available options are:
|
||||
|
||||
* [`tz`](https://hexdocs.pm/tz/)
|
||||
* [`tzdata`](https://hexdocs.pm/tzdata/)
|
||||
|
||||
To use them, first make sure it is added as a dependency in `mix.exs`.
|
||||
It can then be configured either via configuration:
|
||||
|
||||
config :elixir, :time_zone_database, Tzdata.TimeZoneDatabase
|
||||
|
||||
@@ -36,6 +40,7 @@ defmodule DateTime do
|
||||
|
||||
Calendar.put_time_zone_database(Tzdata.TimeZoneDatabase)
|
||||
|
||||
See the proper names in the library installation instructions.
|
||||
"""
|
||||
|
||||
@enforce_keys [:year, :month, :day, :hour, :minute, :second] ++
|
||||
@@ -72,6 +77,7 @@ defmodule DateTime do
|
||||
}
|
||||
|
||||
@unix_days :calendar.date_to_gregorian_days({1970, 1, 1})
|
||||
@seconds_per_day 24 * 60 * 60
|
||||
|
||||
@doc """
|
||||
Returns the current datetime in UTC.
|
||||
@@ -88,12 +94,165 @@ defmodule DateTime do
|
||||
System.os_time() |> from_unix!(:native, calendar)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Builds a datetime from date and time structs.
|
||||
|
||||
It expects a time zone to put the `DateTime` in.
|
||||
If the time zone is not passed it will default to `"Etc/UTC"`,
|
||||
which always succeeds. Otherwise, the `DateTime` is checked against the time zone database
|
||||
given as `time_zone_database`. See the "Time zone database"
|
||||
section in the module documentation.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> DateTime.new(~D[2016-05-24], ~T[13:26:08.003], "Etc/UTC")
|
||||
{:ok, ~U[2016-05-24 13:26:08.003Z]}
|
||||
|
||||
When the datetime is ambiguous - for instance during changing from summer
|
||||
to winter time - the two possible valid datetimes are returned. First the one
|
||||
that happens first, then the one that happens after.
|
||||
|
||||
iex> {:ambiguous, first_dt, second_dt} = DateTime.new(~D[2018-10-28], ~T[02:30:00], "Europe/Copenhagen", FakeTimeZoneDatabase)
|
||||
iex> first_dt
|
||||
#DateTime<2018-10-28 02:30:00+02:00 CEST Europe/Copenhagen>
|
||||
iex> second_dt
|
||||
#DateTime<2018-10-28 02:30:00+01:00 CET Europe/Copenhagen>
|
||||
|
||||
When there is a gap in wall time - for instance in spring when the clocks are
|
||||
turned forward - the latest valid datetime just before the gap and the first
|
||||
valid datetime just after the gap.
|
||||
|
||||
iex> {:gap, just_before, just_after} = DateTime.new(~D[2019-03-31], ~T[02:30:00], "Europe/Copenhagen", FakeTimeZoneDatabase)
|
||||
iex> just_before
|
||||
#DateTime<2019-03-31 01:59:59.999999+01:00 CET Europe/Copenhagen>
|
||||
iex> just_after
|
||||
#DateTime<2019-03-31 03:00:00+02:00 CEST Europe/Copenhagen>
|
||||
|
||||
Most of the time there is one, and just one, valid datetime for a certain
|
||||
date and time in a certain time zone.
|
||||
|
||||
iex> {:ok, datetime} = DateTime.new(~D[2018-07-28], ~T[12:30:00], "Europe/Copenhagen", FakeTimeZoneDatabase)
|
||||
iex> datetime
|
||||
#DateTime<2018-07-28 12:30:00+02:00 CEST Europe/Copenhagen>
|
||||
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
@spec new(Date.t(), Time.t(), Calendar.time_zone(), Calendar.time_zone_database()) ::
|
||||
{:ok, t}
|
||||
| {:ambiguous, t, t}
|
||||
| {:gap, t, t}
|
||||
| {:error,
|
||||
:incompatible_calendars | :time_zone_not_found | :utc_only_time_zone_database}
|
||||
def new(
|
||||
date,
|
||||
time,
|
||||
time_zone \\ "Etc/UTC",
|
||||
time_zone_database \\ Calendar.get_time_zone_database()
|
||||
)
|
||||
|
||||
def new(%Date{calendar: calendar} = date, %Time{calendar: calendar} = time, "Etc/UTC", _db) do
|
||||
%{year: year, month: month, day: day} = date
|
||||
%{hour: hour, minute: minute, second: second, microsecond: microsecond} = time
|
||||
|
||||
datetime = %DateTime{
|
||||
calendar: calendar,
|
||||
year: year,
|
||||
month: month,
|
||||
day: day,
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
second: second,
|
||||
microsecond: microsecond,
|
||||
std_offset: 0,
|
||||
utc_offset: 0,
|
||||
zone_abbr: "UTC",
|
||||
time_zone: "Etc/UTC"
|
||||
}
|
||||
|
||||
{:ok, datetime}
|
||||
end
|
||||
|
||||
def new(date, time, time_zone, time_zone_database) do
|
||||
with {:ok, naive_datetime} <- NaiveDateTime.new(date, time) do
|
||||
from_naive(naive_datetime, time_zone, time_zone_database)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Builds a datetime from date and time structs, raising on errors.
|
||||
|
||||
It expects a time zone to put the `DateTime` in.
|
||||
If the time zone is not passed it will default to `"Etc/UTC"`,
|
||||
which always succeeds. Otherwise, the DateTime is checked against the time zone database
|
||||
given as `time_zone_database`. See the "Time zone database"
|
||||
section in the module documentation.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> DateTime.new!(~D[2016-05-24], ~T[13:26:08.003], "Etc/UTC")
|
||||
~U[2016-05-24 13:26:08.003Z]
|
||||
|
||||
When the datetime is ambiguous - for instance during changing from summer
|
||||
to winter time - an error will be raised.
|
||||
|
||||
iex> DateTime.new!(~D[2018-10-28], ~T[02:30:00], "Europe/Copenhagen", FakeTimeZoneDatabase)
|
||||
** (ArgumentError) cannot build datetime with ~D[2018-10-28] and ~T[02:30:00] because such instant is ambiguous in time zone Europe/Copenhagen as there is an overlap between #DateTime<2018-10-28 02:30:00+02:00 CEST Europe/Copenhagen> and #DateTime<2018-10-28 02:30:00+01:00 CET Europe/Copenhagen>
|
||||
|
||||
When there is a gap in wall time - for instance in spring when the clocks are
|
||||
turned forward - an error will be raised.
|
||||
|
||||
iex> DateTime.new!(~D[2019-03-31], ~T[02:30:00], "Europe/Copenhagen", FakeTimeZoneDatabase)
|
||||
** (ArgumentError) cannot build datetime with ~D[2019-03-31] and ~T[02:30:00] because such instant does not exist in time zone Europe/Copenhagen as there is a gap between #DateTime<2019-03-31 01:59:59.999999+01:00 CET Europe/Copenhagen> and #DateTime<2019-03-31 03:00:00+02:00 CEST Europe/Copenhagen>
|
||||
|
||||
Most of the time there is one, and just one, valid datetime for a certain
|
||||
date and time in a certain time zone.
|
||||
|
||||
iex> datetime = DateTime.new!(~D[2018-07-28], ~T[12:30:00], "Europe/Copenhagen", FakeTimeZoneDatabase)
|
||||
iex> datetime
|
||||
#DateTime<2018-07-28 12:30:00+02:00 CEST Europe/Copenhagen>
|
||||
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
@spec new!(Date.t(), Time.t(), Calendar.time_zone(), Calendar.time_zone_database()) :: t
|
||||
def new!(
|
||||
date,
|
||||
time,
|
||||
time_zone \\ "Etc/UTC",
|
||||
time_zone_database \\ Calendar.get_time_zone_database()
|
||||
)
|
||||
|
||||
def new!(date, time, time_zone, time_zone_database) do
|
||||
case new(date, time, time_zone, time_zone_database) do
|
||||
{:ok, datetime} ->
|
||||
datetime
|
||||
|
||||
{:ambiguous, dt1, dt2} ->
|
||||
raise ArgumentError,
|
||||
"cannot build datetime with #{inspect(date)} and #{inspect(time)} because such " <>
|
||||
"instant is ambiguous in time zone #{time_zone} as there is an overlap " <>
|
||||
"between #{inspect(dt1)} and #{inspect(dt2)}"
|
||||
|
||||
{:gap, dt1, dt2} ->
|
||||
raise ArgumentError,
|
||||
"cannot build datetime with #{inspect(date)} and #{inspect(time)} because such " <>
|
||||
"instant does not exist in time zone #{time_zone} as there is a gap " <>
|
||||
"between #{inspect(dt1)} and #{inspect(dt2)}"
|
||||
|
||||
{:error, reason} ->
|
||||
raise ArgumentError,
|
||||
"cannot build datetime with #{inspect(date)} and #{inspect(time)}, reason: #{
|
||||
inspect(reason)
|
||||
}"
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts the given Unix time to `DateTime`.
|
||||
|
||||
The integer can be given in different unit
|
||||
according to `System.convert_time_unit/3` and it will
|
||||
be converted to microseconds internally.
|
||||
be converted to microseconds internally. Up to
|
||||
253402300799 seconds is supported.
|
||||
|
||||
Unix times are always in UTC and therefore the DateTime
|
||||
will be returned in UTC.
|
||||
@@ -108,14 +267,26 @@ defmodule DateTime do
|
||||
iex> datetime
|
||||
~U[2015-05-25 13:26:08.868569Z]
|
||||
|
||||
iex> {:ok, datetime} = DateTime.from_unix(253_402_300_799)
|
||||
iex> datetime
|
||||
~U[9999-12-31 23:59:59Z]
|
||||
|
||||
iex> {:error, :invalid_unix_time} = DateTime.from_unix(253_402_300_800)
|
||||
|
||||
The unit can also be an integer as in `t:System.time_unit/0`:
|
||||
|
||||
iex> {:ok, datetime} = DateTime.from_unix(143_256_036_886_856, 1024)
|
||||
iex> datetime
|
||||
~U[6403-03-17 07:05:22.320312Z]
|
||||
|
||||
Negative Unix times are supported, up to -62167219200 seconds,
|
||||
which is equivalent to "0000-01-01T00:00:00Z" or 0 Gregorian seconds.
|
||||
Negative Unix times are supported up to -377705116800 seconds:
|
||||
|
||||
iex> {:ok, datetime} = DateTime.from_unix(-377_705_116_800)
|
||||
iex> datetime
|
||||
~U[-9999-01-01 00:00:00Z]
|
||||
|
||||
iex> {:error, :invalid_unix_time} = DateTime.from_unix(-377_705_116_801)
|
||||
|
||||
"""
|
||||
@spec from_unix(integer, :native | System.time_unit(), Calendar.calendar()) ::
|
||||
{:ok, t} | {:error, atom}
|
||||
@@ -582,6 +753,10 @@ defmodule DateTime do
|
||||
It will return the integer with the given unit,
|
||||
according to `System.convert_time_unit/3`.
|
||||
|
||||
If you want to get the current time in Unix seconds,
|
||||
do not do `DateTime.utc_now() |> DateTime.to_unix()`.
|
||||
Simply call `System.os_time(:second)` instead.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> 1_464_096_368 |> DateTime.from_unix!() |> DateTime.to_unix()
|
||||
@@ -863,6 +1038,93 @@ defmodule DateTime do
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts a number of gregorian seconds to a `DateTime` struct.
|
||||
|
||||
The returned `DateTime` will have `UTC` timezone, if you want other timezone, please use
|
||||
`DateTime.shift_zone/3`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> DateTime.from_gregorian_seconds(1)
|
||||
~U[0000-01-01 00:00:01Z]
|
||||
iex> DateTime.from_gregorian_seconds(63_755_511_991, {5000, 3})
|
||||
~U[2020-05-01 00:26:31.005Z]
|
||||
iex> DateTime.from_gregorian_seconds(-1)
|
||||
~U[-0001-12-31 23:59:59Z]
|
||||
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
@spec from_gregorian_seconds(integer(), Calendar.microsecond(), Calendar.calendar()) :: t
|
||||
def from_gregorian_seconds(
|
||||
seconds,
|
||||
{microsecond, precision} \\ {0, 0},
|
||||
calendar \\ Calendar.ISO
|
||||
)
|
||||
when is_integer(seconds) do
|
||||
iso_days = Calendar.ISO.gregorian_seconds_to_iso_days(seconds, microsecond)
|
||||
|
||||
{year, month, day, hour, minute, second, {microsecond, _}} =
|
||||
calendar.naive_datetime_from_iso_days(iso_days)
|
||||
|
||||
%DateTime{
|
||||
calendar: calendar,
|
||||
year: year,
|
||||
month: month,
|
||||
day: day,
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
second: second,
|
||||
microsecond: {microsecond, precision},
|
||||
std_offset: 0,
|
||||
utc_offset: 0,
|
||||
zone_abbr: "UTC",
|
||||
time_zone: "Etc/UTC"
|
||||
}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts a `DateTime` struct to a number of gregorian seconds and microseconds.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> dt = %DateTime{year: 0000, month: 1, day: 1, zone_abbr: "UTC",
|
||||
...> hour: 0, minute: 0, second: 1, microsecond: {0, 0},
|
||||
...> utc_offset: 0, std_offset: 0, time_zone: "Etc/UTC"}
|
||||
iex> DateTime.to_gregorian_seconds(dt)
|
||||
{1, 0}
|
||||
|
||||
iex> dt = %DateTime{year: 2020, month: 5, day: 1, zone_abbr: "UTC",
|
||||
...> hour: 0, minute: 26, second: 31, microsecond: {5000, 0},
|
||||
...> utc_offset: 0, std_offset: 0, time_zone: "Etc/UTC"}
|
||||
iex> DateTime.to_gregorian_seconds(dt)
|
||||
{63_755_511_991, 5000}
|
||||
|
||||
iex> dt = %DateTime{year: 2020, month: 5, day: 1, zone_abbr: "CET",
|
||||
...> hour: 1, minute: 26, second: 31, microsecond: {5000, 0},
|
||||
...> utc_offset: 3600, std_offset: 0, time_zone: "Europe/Warsaw"}
|
||||
iex> DateTime.to_gregorian_seconds(dt)
|
||||
{63_755_511_991, 5000}
|
||||
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
@spec to_gregorian_seconds(Calendar.datetime()) :: {integer(), non_neg_integer()}
|
||||
def to_gregorian_seconds(
|
||||
%{
|
||||
std_offset: std_offset,
|
||||
utc_offset: utc_offset,
|
||||
microsecond: {microsecond, _}
|
||||
} = datetime
|
||||
) do
|
||||
{days, day_fraction} =
|
||||
datetime
|
||||
|> to_iso_days()
|
||||
|> apply_tz_offset(utc_offset + std_offset)
|
||||
|
||||
seconds_in_day = seconds_from_day_fraction(day_fraction)
|
||||
{days * @seconds_per_day + seconds_in_day, microsecond}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts the given `datetime` to a string according to its calendar.
|
||||
|
||||
@@ -1263,6 +1525,12 @@ defmodule DateTime do
|
||||
}
|
||||
end
|
||||
|
||||
defp seconds_from_day_fraction({parts_in_day, @seconds_per_day}),
|
||||
do: parts_in_day
|
||||
|
||||
defp seconds_from_day_fraction({parts_in_day, parts_per_day}),
|
||||
do: div(parts_in_day * @seconds_per_day, parts_per_day)
|
||||
|
||||
defimpl String.Chars do
|
||||
def to_string(datetime) do
|
||||
%{
|
||||
|
||||
@@ -8,6 +8,12 @@ defmodule Calendar.ISO do
|
||||
applied for all time, consequently the dates give different results
|
||||
before the year 1583 from when the Gregorian calendar was adopted.
|
||||
|
||||
Given this is the default calendar used by Elixir, it has one
|
||||
difference compared to the ISO8601 specification in that it allows
|
||||
a whitespace instead of `T` as a seperator between date and times
|
||||
both when parsing and formatting. Strict formatting can be done
|
||||
by using the `to_iso8601` found in `NaiveDateTime` and `DateTime`.
|
||||
|
||||
Note that while ISO 8601 allows times and datetimes to specify
|
||||
24:00:00 as the zero hour of the next day, this notation is not
|
||||
supported by Elixir.
|
||||
@@ -44,6 +50,7 @@ defmodule Calendar.ISO do
|
||||
@type hour :: 0..23
|
||||
@type minute :: 0..59
|
||||
@type second :: 0..59
|
||||
@type weekday :: :monday | :tuesday | :wednesday | :thursday | :friday | :saturday | :sunday
|
||||
|
||||
@typedoc """
|
||||
Microseconds with stored precision.
|
||||
@@ -493,12 +500,12 @@ defmodule Calendar.ISO do
|
||||
total_microseconds = divide_by_parts_per_day(parts_in_day, parts_per_day)
|
||||
|
||||
{hours, rest_microseconds1} =
|
||||
div_mod(total_microseconds, @seconds_per_hour * @microseconds_per_second)
|
||||
div_rem(total_microseconds, @seconds_per_hour * @microseconds_per_second)
|
||||
|
||||
{minutes, rest_microseconds2} =
|
||||
div_mod(rest_microseconds1, @seconds_per_minute * @microseconds_per_second)
|
||||
div_rem(rest_microseconds1, @seconds_per_minute * @microseconds_per_second)
|
||||
|
||||
{seconds, microseconds} = div_mod(rest_microseconds2, @microseconds_per_second)
|
||||
{seconds, microseconds} = div_rem(rest_microseconds2, @microseconds_per_second)
|
||||
{hours, minutes, seconds, {microseconds, 6}}
|
||||
end
|
||||
|
||||
@@ -533,7 +540,7 @@ defmodule Calendar.ISO do
|
||||
{year, month, day_in_month + 1}
|
||||
end
|
||||
|
||||
defp div_mod(int1, int2) do
|
||||
defp div_rem(int1, int2) do
|
||||
div = div(int1, int2)
|
||||
rem = int1 - div * int2
|
||||
|
||||
@@ -619,43 +626,83 @@ defmodule Calendar.ISO do
|
||||
rem(year, 4) === 0 and (rem(year, 100) !== 0 or rem(year, 400) === 0)
|
||||
end
|
||||
|
||||
# TODO: Deprecate me on v1.15
|
||||
@doc false
|
||||
def day_of_week(year, month, day) do
|
||||
day_of_week(year, month, day, :default) |> elem(0)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Calculates the day of the week from the given `year`, `month`, and `day`.
|
||||
|
||||
It is an integer from 1 to 7, where 1 is Monday and 7 is Sunday.
|
||||
It is an integer from 1 to 7, where 1 is the given `starting_on` weekday.
|
||||
For example, if `starting_on` is set to `:monday`, then 1 is Monday and
|
||||
7 is Sunday.
|
||||
|
||||
`starting_on` can also be `:default`, which is equivalent to `:monday`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Calendar.ISO.day_of_week(2016, 10, 31)
|
||||
1
|
||||
iex> Calendar.ISO.day_of_week(2016, 11, 1)
|
||||
2
|
||||
iex> Calendar.ISO.day_of_week(2016, 11, 2)
|
||||
3
|
||||
iex> Calendar.ISO.day_of_week(2016, 11, 3)
|
||||
4
|
||||
iex> Calendar.ISO.day_of_week(2016, 11, 4)
|
||||
5
|
||||
iex> Calendar.ISO.day_of_week(2016, 11, 5)
|
||||
6
|
||||
iex> Calendar.ISO.day_of_week(2016, 11, 6)
|
||||
7
|
||||
iex> Calendar.ISO.day_of_week(-99, 1, 31)
|
||||
4
|
||||
iex> Calendar.ISO.day_of_week(2016, 10, 31, :monday)
|
||||
{1, 1, 7}
|
||||
iex> Calendar.ISO.day_of_week(2016, 11, 1, :monday)
|
||||
{2, 1, 7}
|
||||
iex> Calendar.ISO.day_of_week(2016, 11, 2, :monday)
|
||||
{3, 1, 7}
|
||||
iex> Calendar.ISO.day_of_week(2016, 11, 3, :monday)
|
||||
{4, 1, 7}
|
||||
iex> Calendar.ISO.day_of_week(2016, 11, 4, :monday)
|
||||
{5, 1, 7}
|
||||
iex> Calendar.ISO.day_of_week(2016, 11, 5, :monday)
|
||||
{6, 1, 7}
|
||||
iex> Calendar.ISO.day_of_week(2016, 11, 6, :monday)
|
||||
{7, 1, 7}
|
||||
iex> Calendar.ISO.day_of_week(-99, 1, 31, :monday)
|
||||
{4, 1, 7}
|
||||
|
||||
iex> Calendar.ISO.day_of_week(2016, 10, 31, :sunday)
|
||||
{2, 1, 7}
|
||||
iex> Calendar.ISO.day_of_week(2016, 11, 1, :sunday)
|
||||
{3, 1, 7}
|
||||
iex> Calendar.ISO.day_of_week(2016, 11, 2, :sunday)
|
||||
{4, 1, 7}
|
||||
iex> Calendar.ISO.day_of_week(2016, 11, 3, :sunday)
|
||||
{5, 1, 7}
|
||||
iex> Calendar.ISO.day_of_week(2016, 11, 4, :sunday)
|
||||
{6, 1, 7}
|
||||
iex> Calendar.ISO.day_of_week(2016, 11, 5, :sunday)
|
||||
{7, 1, 7}
|
||||
iex> Calendar.ISO.day_of_week(2016, 11, 6, :sunday)
|
||||
{1, 1, 7}
|
||||
iex> Calendar.ISO.day_of_week(-99, 1, 31, :sunday)
|
||||
{5, 1, 7}
|
||||
|
||||
iex> Calendar.ISO.day_of_week(2016, 10, 31, :saturday)
|
||||
{3, 1, 7}
|
||||
|
||||
"""
|
||||
@doc since: "1.4.0"
|
||||
@spec day_of_week(year, month, day) :: day_of_week()
|
||||
@doc since: "1.11.0"
|
||||
@spec day_of_week(year, month, day, :default | weekday) :: {day_of_week(), 1, 7}
|
||||
@impl true
|
||||
def day_of_week(year, month, day) do
|
||||
date_to_iso_days(year, month, day)
|
||||
|> iso_days_to_day_of_week()
|
||||
def day_of_week(year, month, day, starting_on) do
|
||||
iso_days = date_to_iso_days(year, month, day)
|
||||
{iso_days_to_day_of_week(iso_days, starting_on), 1, 7}
|
||||
end
|
||||
|
||||
defp iso_days_to_day_of_week(iso_days) do
|
||||
Integer.mod(iso_days + 5, 7) + 1
|
||||
@doc false
|
||||
def iso_days_to_day_of_week(iso_days, starting_on) do
|
||||
Integer.mod(iso_days + day_of_week_offset(starting_on), 7) + 1
|
||||
end
|
||||
|
||||
defp day_of_week_offset(:default), do: 5
|
||||
defp day_of_week_offset(:wednesday), do: 3
|
||||
defp day_of_week_offset(:thursday), do: 2
|
||||
defp day_of_week_offset(:friday), do: 1
|
||||
defp day_of_week_offset(:saturday), do: 0
|
||||
defp day_of_week_offset(:sunday), do: 6
|
||||
defp day_of_week_offset(:monday), do: 5
|
||||
defp day_of_week_offset(:tuesday), do: 4
|
||||
|
||||
@doc """
|
||||
Calculates the day of the year from the given `year`, `month`, and `day`.
|
||||
|
||||
@@ -1160,6 +1207,14 @@ defmodule Calendar.ISO do
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def gregorian_seconds_to_iso_days(seconds, microsecond) do
|
||||
{days, rest_seconds} = div_rem(seconds, @seconds_per_day)
|
||||
microseconds_in_day = rest_seconds * @microseconds_per_second + microsecond
|
||||
day_fraction = {microseconds_in_day, @parts_per_day}
|
||||
{days, day_fraction}
|
||||
end
|
||||
|
||||
@doc false
|
||||
def iso_days_to_unit({days, {parts, ppd}}, unit) do
|
||||
day_microseconds = days * @parts_per_day
|
||||
@@ -1312,7 +1367,7 @@ defmodule Calendar.ISO do
|
||||
end
|
||||
|
||||
defp iso_seconds_to_datetime(seconds) do
|
||||
{days, rest_seconds} = div_mod(seconds, @seconds_per_day)
|
||||
{days, rest_seconds} = div_rem(seconds, @seconds_per_day)
|
||||
|
||||
date = date_from_iso_days(days)
|
||||
time = seconds_to_time(rest_seconds)
|
||||
@@ -1320,8 +1375,8 @@ defmodule Calendar.ISO do
|
||||
end
|
||||
|
||||
defp seconds_to_time(seconds) when seconds in 0..@last_second_of_the_day do
|
||||
{hour, rest_seconds} = div_mod(seconds, @seconds_per_hour)
|
||||
{minute, second} = div_mod(rest_seconds, @seconds_per_minute)
|
||||
{hour, rest_seconds} = div_rem(seconds, @seconds_per_hour)
|
||||
{minute, second} = div_rem(rest_seconds, @seconds_per_minute)
|
||||
|
||||
{hour, minute, second}
|
||||
end
|
||||
|
||||
@@ -78,6 +78,8 @@ defmodule NaiveDateTime do
|
||||
microsecond: Calendar.microsecond()
|
||||
}
|
||||
|
||||
@seconds_per_day 24 * 60 * 60
|
||||
|
||||
@doc """
|
||||
Returns the current naive datetime in UTC.
|
||||
|
||||
@@ -242,6 +244,61 @@ defmodule NaiveDateTime do
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Builds a new ISO naive datetime.
|
||||
|
||||
Expects all values to be integers. Returns `naive_datetime`
|
||||
if each entry fits its appropriate range, raises if
|
||||
time or date is invalid.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> NaiveDateTime.new!(2000, 1, 1, 0, 0, 0)
|
||||
~N[2000-01-01 00:00:00]
|
||||
iex> NaiveDateTime.new!(2000, 2, 29, 0, 0, 0)
|
||||
~N[2000-02-29 00:00:00]
|
||||
iex> NaiveDateTime.new!(2000, 1, 1, 23, 59, 59, {0, 1})
|
||||
~N[2000-01-01 23:59:59.0]
|
||||
iex> NaiveDateTime.new!(2000, 1, 1, 23, 59, 59, 999_999)
|
||||
~N[2000-01-01 23:59:59.999999]
|
||||
iex> NaiveDateTime.new!(2000, 1, 1, 23, 59, 59, {0, 1}, Calendar.ISO)
|
||||
~N[2000-01-01 23:59:59.0]
|
||||
iex> NaiveDateTime.new!(2000, 1, 1, 24, 59, 59, 999_999)
|
||||
** (ArgumentError) cannot build naive datetime, reason: :invalid_time
|
||||
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
@spec new!(
|
||||
Calendar.year(),
|
||||
Calendar.month(),
|
||||
Calendar.day(),
|
||||
Calendar.hour(),
|
||||
Calendar.minute(),
|
||||
Calendar.second(),
|
||||
Calendar.microsecond() | non_neg_integer,
|
||||
Calendar.calendar()
|
||||
) :: t
|
||||
def new!(
|
||||
year,
|
||||
month,
|
||||
day,
|
||||
hour,
|
||||
minute,
|
||||
second,
|
||||
microsecond \\ {0, 0},
|
||||
calendar \\ Calendar.ISO
|
||||
)
|
||||
|
||||
def new!(year, month, day, hour, minute, second, microsecond, calendar) do
|
||||
case new(year, month, day, hour, minute, second, microsecond, calendar) do
|
||||
{:ok, naive_datetime} ->
|
||||
naive_datetime
|
||||
|
||||
{:error, reason} ->
|
||||
raise ArgumentError, "cannot build naive datetime, reason: #{inspect(reason)}"
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Builds a naive datetime from date and time structs.
|
||||
|
||||
@@ -272,6 +329,24 @@ defmodule NaiveDateTime do
|
||||
{:ok, naive_datetime}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Builds a naive datetime from date and time structs.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> NaiveDateTime.new!(~D[2010-01-13], ~T[23:00:07.005])
|
||||
~N[2010-01-13 23:00:07.005]
|
||||
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
@spec new!(Date.t(), Time.t()) :: t
|
||||
def new!(date, time)
|
||||
|
||||
def new!(%Date{calendar: calendar} = date, %Time{calendar: calendar} = time) do
|
||||
{:ok, naive_datetime} = new(date, time)
|
||||
naive_datetime
|
||||
end
|
||||
|
||||
@doc """
|
||||
Adds a specified amount of time to a `NaiveDateTime`.
|
||||
|
||||
@@ -769,6 +844,82 @@ defmodule NaiveDateTime do
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts a number of gregorian seconds to a `NaiveDateTime` struct.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> NaiveDateTime.from_gregorian_seconds(1)
|
||||
~N[0000-01-01 00:00:01]
|
||||
iex> NaiveDateTime.from_gregorian_seconds(63_755_511_991, {5000, 3})
|
||||
~N[2020-05-01 00:26:31.005]
|
||||
iex> NaiveDateTime.from_gregorian_seconds(-1)
|
||||
~N[-0001-12-31 23:59:59]
|
||||
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
@spec from_gregorian_seconds(integer(), Calendar.microsecond(), Calendar.calendar()) :: t
|
||||
def from_gregorian_seconds(
|
||||
seconds,
|
||||
{microsecond, precision} \\ {0, 0},
|
||||
calendar \\ Calendar.ISO
|
||||
)
|
||||
when is_integer(seconds) do
|
||||
iso_days = Calendar.ISO.gregorian_seconds_to_iso_days(seconds, microsecond)
|
||||
|
||||
{year, month, day, hour, minute, second, {microsecond, _}} =
|
||||
calendar.naive_datetime_from_iso_days(iso_days)
|
||||
|
||||
%NaiveDateTime{
|
||||
calendar: calendar,
|
||||
year: year,
|
||||
month: month,
|
||||
day: day,
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
second: second,
|
||||
microsecond: {microsecond, precision}
|
||||
}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts a `NaiveDateTime` struct to a number of gregorian seconds and microseconds.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> NaiveDateTime.to_gregorian_seconds(~N[0000-01-01 00:00:01])
|
||||
{1, 0}
|
||||
iex> NaiveDateTime.to_gregorian_seconds(~N[2020-05-01 00:26:31.005])
|
||||
{63_755_511_991, 5000}
|
||||
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
@spec to_gregorian_seconds(Calendar.naive_datetime()) :: {integer(), non_neg_integer()}
|
||||
def to_gregorian_seconds(%{
|
||||
calendar: calendar,
|
||||
year: year,
|
||||
month: month,
|
||||
day: day,
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
second: second,
|
||||
microsecond: {microsecond, precision}
|
||||
}) do
|
||||
{days, day_fraction} =
|
||||
calendar.naive_datetime_to_iso_days(
|
||||
year,
|
||||
month,
|
||||
day,
|
||||
hour,
|
||||
minute,
|
||||
second,
|
||||
{microsecond, precision}
|
||||
)
|
||||
|
||||
seconds_in_day = seconds_from_day_fraction(day_fraction)
|
||||
{days * @seconds_per_day + seconds_in_day, microsecond}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Compares two `NaiveDateTime` structs.
|
||||
|
||||
@@ -913,6 +1064,12 @@ defmodule NaiveDateTime do
|
||||
|
||||
## Helpers
|
||||
|
||||
defp seconds_from_day_fraction({parts_in_day, @seconds_per_day}),
|
||||
do: parts_in_day
|
||||
|
||||
defp seconds_from_day_fraction({parts_in_day, parts_per_day}),
|
||||
do: div(parts_in_day * @seconds_per_day, parts_per_day)
|
||||
|
||||
# Keep it multiline for proper function clause errors.
|
||||
defp to_iso_days(%{
|
||||
calendar: calendar,
|
||||
|
||||
@@ -46,6 +46,7 @@ defmodule Time do
|
||||
}
|
||||
|
||||
@parts_per_day 86_400_000_000
|
||||
@seconds_per_day 24 * 60 * 60
|
||||
|
||||
@doc """
|
||||
Returns the current time in UTC.
|
||||
@@ -139,6 +140,44 @@ defmodule Time do
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Builds a new time.
|
||||
|
||||
Expects all values to be integers. Returns `time` if each
|
||||
entry fits its appropriate range, raises if the time is invalid.
|
||||
|
||||
Microseconds can also be given with a precision, which must be an
|
||||
integer between 0 and 6.
|
||||
|
||||
The built-in calendar does not support leap seconds.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Time.new!(0, 0, 0, 0)
|
||||
~T[00:00:00.000000]
|
||||
iex> Time.new!(23, 59, 59, 999_999)
|
||||
~T[23:59:59.999999]
|
||||
iex> Time.new!(24, 59, 59, 999_999)
|
||||
** (ArgumentError) cannot build time, reason: :invalid_time
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
@spec new!(
|
||||
Calendar.hour(),
|
||||
Calendar.minute(),
|
||||
Calendar.second(),
|
||||
Calendar.microsecond() | non_neg_integer,
|
||||
Calendar.calendar()
|
||||
) :: t
|
||||
def new!(hour, minute, second, microsecond \\ {0, 0}, calendar \\ Calendar.ISO) do
|
||||
case new(hour, minute, second, microsecond, calendar) do
|
||||
{:ok, time} ->
|
||||
time
|
||||
|
||||
{:error, reason} ->
|
||||
raise ArgumentError, "cannot build time, reason: #{inspect(reason)}"
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts the given `time` to a string.
|
||||
|
||||
@@ -358,6 +397,63 @@ defmodule Time do
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts a number of seconds after midnight to a `Time` struct.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Time.from_seconds_after_midnight(10_000)
|
||||
~T[02:46:40]
|
||||
iex> Time.from_seconds_after_midnight(30_000, {5000, 3})
|
||||
~T[08:20:00.005]
|
||||
iex> Time.from_seconds_after_midnight(-1)
|
||||
~T[23:59:59]
|
||||
iex> Time.from_seconds_after_midnight(100_000)
|
||||
~T[03:46:40]
|
||||
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
@spec from_seconds_after_midnight(
|
||||
integer(),
|
||||
Calendar.microsecond(),
|
||||
Calendar.calendar()
|
||||
) :: t
|
||||
def from_seconds_after_midnight(seconds, microsecond \\ {0, 0}, calendar \\ Calendar.ISO)
|
||||
when is_integer(seconds) do
|
||||
seconds_in_day = Integer.mod(seconds, @seconds_per_day)
|
||||
|
||||
{hour, minute, second, {_, _}} =
|
||||
calendar.time_from_day_fraction({seconds_in_day, @seconds_per_day})
|
||||
|
||||
%Time{
|
||||
calendar: calendar,
|
||||
hour: hour,
|
||||
minute: minute,
|
||||
second: second,
|
||||
microsecond: microsecond
|
||||
}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts a `Time` struct to a number of seconds after midnight.
|
||||
|
||||
The returned value is a two-element tuple with the number of seconds and microseconds.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Time.to_seconds_after_midnight(~T[23:30:15])
|
||||
{84615, 0}
|
||||
iex> Time.to_seconds_after_midnight(~N[2010-04-17 23:30:15.999])
|
||||
{84615, 999000}
|
||||
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
@spec to_seconds_after_midnight(Calendar.time()) :: {integer(), non_neg_integer()}
|
||||
def to_seconds_after_midnight(%{microsecond: {microsecond, _precision}} = time) do
|
||||
iso_days = {0, to_day_fraction(time)}
|
||||
{Calendar.ISO.iso_days_to_unit(iso_days, :second), microsecond}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Adds the `number` of `unit`s to the given `time`.
|
||||
|
||||
@@ -556,12 +652,12 @@ defmodule Time do
|
||||
|
||||
As with the `compare/2` function both `Time` structs and other structures
|
||||
containing time can be used. If for instance a `NaiveDateTime` or `DateTime`
|
||||
is passed, only the hour, month, second, and microsecond is considered. Any
|
||||
is passed, only the hour, minute, second, and microsecond is considered. Any
|
||||
additional information about a date or time zone is ignored when calculating
|
||||
the difference.
|
||||
|
||||
The answer can be returned in any `unit` available from
|
||||
`t:System.time_unit/0`. If the first unit is smaller than
|
||||
`t:System.time_unit/0`. If the first time value is earlier than
|
||||
the second, a negative number is returned.
|
||||
|
||||
This function returns the difference in seconds where seconds
|
||||
|
||||
@@ -59,7 +59,7 @@ defmodule Calendar.TimeZoneDatabase do
|
||||
with a limit for when the period ends (wall time). The second nested two-tuple is the period
|
||||
just after the gap and a datetime (wall time) for when the period begins just after the gap.
|
||||
|
||||
If there is only a single possible period for the provided `datetime`, the a tuple with `:single`
|
||||
If there is only a single possible period for the provided `datetime`, then a tuple with `:ok`
|
||||
and the `time_zone_period` is returned.
|
||||
"""
|
||||
@doc since: "1.8.0"
|
||||
|
||||
+94
-30
@@ -43,6 +43,15 @@ defmodule Code do
|
||||
You can configure your list of tracers via `put_compiler_option/2`. The
|
||||
following events are available to tracers:
|
||||
|
||||
* `:start` - (since v1.11.0) invoked whenever the compiler starts to trace
|
||||
a new lexical context, such as a new file. Keep in mind the compiler runs
|
||||
in parallel, so multiple files may invoke `:start` and run at the same
|
||||
time. The value of the `lexical_tracker` of the macro environment, albeit
|
||||
opaque, can be used to uniquely identify the environment.
|
||||
|
||||
* `:stop` - (since v1.11.0) invoked whenever the compiler stops tracing a
|
||||
new lexical context, such as a new file.
|
||||
|
||||
* `{:import, meta, module, opts}` - traced whenever `module` is imported.
|
||||
`meta` is the import AST metadata and `opts` are the import options.
|
||||
|
||||
@@ -75,14 +84,14 @@ defmodule Code do
|
||||
function or macro is referenced. `meta` is the call AST metadata, `module`
|
||||
is the invoked module, followed by the `name` and `arity`.
|
||||
|
||||
* `{:local_function, meta, module, name, arity}` and
|
||||
`{:local_macro, meta, module, name, arity}` - traced whenever a local
|
||||
function or macro is referenced. `meta` is the call AST metadata, `module`
|
||||
is the invoked module, followed by the `name` and `arity`.
|
||||
* `{:local_function, meta, name, arity}` and
|
||||
`{:local_macro, meta, name, arity}` - traced whenever a local
|
||||
function or macro is referenced. `meta` is the call AST metadata, followed by
|
||||
the `name` and `arity`.
|
||||
|
||||
* `{:compile_env, app, path, return}` - traced whenever `Application.compile_env/3`
|
||||
or `Application.compile_env!/2` are called. `app` is an atom, `path` is a list
|
||||
of keys to traverse in the application environemnt and `return` is either
|
||||
of keys to traverse in the application environment and `return` is either
|
||||
`{:ok, value}` or `:error`.
|
||||
|
||||
The `:tracers` compiler option can be combined with the `:parser_options`
|
||||
@@ -162,14 +171,14 @@ defmodule Code do
|
||||
# Now unrequire all files
|
||||
Code.unrequire_files(Code.required_files())
|
||||
|
||||
# Notice modules are still available
|
||||
# Note that modules are still available
|
||||
function_exported?(EExTest.Compiled, :before_compile, 0)
|
||||
#=> true
|
||||
|
||||
"""
|
||||
@doc since: "1.7.0"
|
||||
@spec unrequire_files([binary]) :: :ok
|
||||
def unrequire_files(files) do
|
||||
def unrequire_files(files) when is_list(files) do
|
||||
:elixir_code_server.cast({:unrequire_files, files})
|
||||
end
|
||||
|
||||
@@ -281,7 +290,7 @@ defmodule Code do
|
||||
and the second a list of imported macro names and arity; the list
|
||||
of function names and arity must be sorted
|
||||
|
||||
Notice that setting any of the values above overrides Elixir's default
|
||||
Note that setting any of the values above overrides Elixir's default
|
||||
values. For example, setting `:requires` to `[]` will no longer
|
||||
automatically require the `Kernel` module. In the same way setting
|
||||
`:macros` will no longer auto-import `Kernel` macros like `Kernel.if/2`,
|
||||
@@ -328,7 +337,7 @@ defmodule Code do
|
||||
|
||||
defp eval_string_with_error_handling(string, binding, opts) do
|
||||
%{line: line, file: file} = env = :elixir.env_for_eval(opts)
|
||||
forms = :elixir.string_to_quoted!(to_charlist(string), line, file, [])
|
||||
forms = :elixir.string_to_quoted!(to_charlist(string), line, 1, file, [])
|
||||
{value, binding, _env} = :elixir.eval_forms(forms, binding, env)
|
||||
{value, binding}
|
||||
end
|
||||
@@ -366,7 +375,7 @@ defmodule Code do
|
||||
|
||||
* `:force_do_end_blocks` (since v1.9.0) - when `true`, converts all
|
||||
inline usages of `do: ...`, `else: ...` and friends into `do/end`
|
||||
blocks. Defaults to `false`. Notice this option is convergent:
|
||||
blocks. Defaults to `false`. Note that this option is convergent:
|
||||
once you set it to `true`, all keywords will be converted. If you
|
||||
set it to `false` later on, `do/end` blocks won't be converted
|
||||
back to keywords.
|
||||
@@ -750,6 +759,9 @@ defmodule Code do
|
||||
* `:line` - the starting line of the string being parsed.
|
||||
Defaults to 1.
|
||||
|
||||
* `:column` - (since v1.11.0) the starting column of the string being parsed.
|
||||
Defaults to 1.
|
||||
|
||||
* `:columns` - when `true`, attach a `:column` key to the quoted
|
||||
metadata. Defaults to `false`.
|
||||
|
||||
@@ -817,12 +829,13 @@ defmodule Code do
|
||||
|
||||
"""
|
||||
@spec string_to_quoted(List.Chars.t(), keyword) ::
|
||||
{:ok, Macro.t()} | {:error, {line :: pos_integer, term, term}}
|
||||
{:ok, Macro.t()} | {:error, {location :: keyword, term, term}}
|
||||
def string_to_quoted(string, opts \\ []) when is_list(opts) do
|
||||
file = Keyword.get(opts, :file, "nofile")
|
||||
line = Keyword.get(opts, :line, 1)
|
||||
column = Keyword.get(opts, :column, 1)
|
||||
|
||||
case :elixir.string_to_tokens(to_charlist(string), line, file, opts) do
|
||||
case :elixir.string_to_tokens(to_charlist(string), line, column, file, opts) do
|
||||
{:ok, tokens} ->
|
||||
:elixir.tokens_to_quoted(tokens, file, opts)
|
||||
|
||||
@@ -845,7 +858,8 @@ defmodule Code do
|
||||
def string_to_quoted!(string, opts \\ []) when is_list(opts) do
|
||||
file = Keyword.get(opts, :file, "nofile")
|
||||
line = Keyword.get(opts, :line, 1)
|
||||
:elixir.string_to_quoted!(to_charlist(string), line, file, opts)
|
||||
column = Keyword.get(opts, :column, 1)
|
||||
:elixir.string_to_quoted!(to_charlist(string), line, column, file, opts)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -880,7 +894,7 @@ defmodule Code do
|
||||
If the file was already required, `require_file/2` doesn't do anything and
|
||||
returns `nil`.
|
||||
|
||||
Notice that if `require_file/2` is invoked by different processes concurrently,
|
||||
Note that if `require_file/2` is invoked by different processes concurrently,
|
||||
the first process to invoke `require_file/2` acquires a lock and the remaining
|
||||
ones will block until the file is available. This means that if `require_file/2`
|
||||
is called more than once with a given file, that file will be compiled only once.
|
||||
@@ -923,7 +937,7 @@ defmodule Code do
|
||||
@doc """
|
||||
Gets all compilation options from the code server.
|
||||
|
||||
To get invidual options, see `get_compiler_option/1`.
|
||||
To get individual options, see `get_compiler_option/1`.
|
||||
For a description of all options, see `put_compiler_option/2`.
|
||||
|
||||
## Examples
|
||||
@@ -942,7 +956,7 @@ defmodule Code do
|
||||
@doc """
|
||||
Stores all given compilation options.
|
||||
|
||||
To store invidual options, see `put_compiler_option/2`.
|
||||
To store individual options, see `put_compiler_option/2`.
|
||||
For a description of all options, see `put_compiler_option/2`.
|
||||
|
||||
## Examples
|
||||
@@ -954,8 +968,9 @@ defmodule Code do
|
||||
@spec compiler_options(Enumerable.t()) :: %{optional(atom) => boolean}
|
||||
def compiler_options(opts) do
|
||||
for {key, value} <- opts, into: %{} do
|
||||
previous = get_compiler_option(key)
|
||||
put_compiler_option(key, value)
|
||||
{key, value}
|
||||
{key, previous}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1266,7 +1281,7 @@ defmodule Code do
|
||||
def ensure_compiled(module) when is_atom(module) do
|
||||
case :code.ensure_loaded(module) do
|
||||
{:error, :nofile} = error ->
|
||||
if is_pid(:erlang.get(:elixir_compiler_pid)) do
|
||||
if can_await_module_compilation?() do
|
||||
case Kernel.ErrorHandler.ensure_compiled(module, :module, :soft) do
|
||||
:found -> {:module, module}
|
||||
:deadlock -> {:error, :unavailable}
|
||||
@@ -1281,6 +1296,21 @@ defmodule Code do
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns true if the current process can await for module compilation.
|
||||
|
||||
When compiling Elixir code via `Kernel.ParallelCompiler`, which is
|
||||
used by Mix and `elixirc`, calling a module that has not yet been
|
||||
compiled will block the caller until the module becomes available.
|
||||
Executing Elixir scripts, such as passing a filename to `elixir`,
|
||||
does not await.
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
@spec can_await_module_compilation? :: boolean
|
||||
def can_await_module_compilation? do
|
||||
:erlang.process_info(self(), :error_handler) == {:error_handler, Kernel.ErrorHandler}
|
||||
end
|
||||
|
||||
@doc false
|
||||
@deprecated "Use Code.ensure_compiled/1 instead (see the proper disclaimers in its docs)"
|
||||
def ensure_compiled?(module) when is_atom(module) do
|
||||
@@ -1317,8 +1347,8 @@ defmodule Code do
|
||||
docs :: [doc_element]}
|
||||
| {:error, :module_not_found | :chunk_not_found | {:invalid_chunk, binary}}
|
||||
when annotation: :erl_anno.anno(),
|
||||
beam_language: :elixir | :erlang | :lfe | :alpaca | atom(),
|
||||
doc_content: %{required(binary) => binary} | :none | :hidden,
|
||||
beam_language: :elixir | :erlang | atom(),
|
||||
doc_content: %{optional(binary) => binary} | :none | :hidden,
|
||||
doc_element:
|
||||
{{kind :: atom, function_name :: atom, arity}, annotation, signature, doc_content,
|
||||
metadata},
|
||||
@@ -1329,31 +1359,65 @@ defmodule Code do
|
||||
|
||||
def fetch_docs(module) when is_atom(module) do
|
||||
case :code.get_object_code(module) do
|
||||
{_module, bin, _beam_path} -> do_fetch_docs(bin)
|
||||
:error -> {:error, :module_not_found}
|
||||
{_module, bin, beam_path} ->
|
||||
case fetch_docs_from_beam(bin) do
|
||||
{:error, :chunk_not_found} ->
|
||||
app_root = Path.expand(Path.join(["..", ".."]), beam_path)
|
||||
path = Path.join([app_root, "doc", "chunks", "#{module}.chunk"])
|
||||
fetch_docs_from_chunk(path)
|
||||
|
||||
other ->
|
||||
other
|
||||
end
|
||||
|
||||
:error ->
|
||||
case :code.which(module) do
|
||||
:preloaded ->
|
||||
path = Path.join([:code.lib_dir(:erts), "doc", "chunks", "#{module}.chunk"])
|
||||
fetch_docs_from_chunk(path)
|
||||
|
||||
_ ->
|
||||
{:error, :module_not_found}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def fetch_docs(path) when is_binary(path) do
|
||||
do_fetch_docs(String.to_charlist(path))
|
||||
fetch_docs_from_beam(String.to_charlist(path))
|
||||
end
|
||||
|
||||
@docs_chunk 'Docs'
|
||||
|
||||
defp do_fetch_docs(bin_or_path) do
|
||||
defp fetch_docs_from_beam(bin_or_path) do
|
||||
case :beam_lib.chunks(bin_or_path, [@docs_chunk]) do
|
||||
{:ok, {_module, [{@docs_chunk, bin}]}} ->
|
||||
try do
|
||||
:erlang.binary_to_term(bin)
|
||||
rescue
|
||||
_ -> {:error, {:invalid_chunk, bin}}
|
||||
end
|
||||
load_docs_chunk(bin)
|
||||
|
||||
{:error, :beam_lib, {:missing_chunk, _, @docs_chunk}} ->
|
||||
{:error, :chunk_not_found}
|
||||
|
||||
{:error, :beam_lib, {:file_error, _, :enoent}} ->
|
||||
{:error, :module_not_found}
|
||||
end
|
||||
end
|
||||
|
||||
defp fetch_docs_from_chunk(path) do
|
||||
case File.read(path) do
|
||||
{:ok, bin} ->
|
||||
load_docs_chunk(bin)
|
||||
|
||||
{:error, _} ->
|
||||
{:error, :chunk_not_found}
|
||||
end
|
||||
end
|
||||
|
||||
defp load_docs_chunk(bin) do
|
||||
:erlang.binary_to_term(bin)
|
||||
rescue
|
||||
_ ->
|
||||
{:error, {:invalid_chunk, bin}}
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
Deprecated function to retrieve old documentation format.
|
||||
|
||||
@@ -1392,6 +1456,6 @@ defmodule Code do
|
||||
defp verify_loaded(loaded) do
|
||||
maps_binaries = Enum.map(loaded, fn {_module, map, binary} -> {map, binary} end)
|
||||
Module.ParallelChecker.verify(maps_binaries, [])
|
||||
Enum.map(loaded, fn {module, map, _binary} -> {module, map} end)
|
||||
Enum.map(loaded, fn {module, _map, binary} -> {module, binary} end)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -42,6 +42,8 @@ defmodule Code.Formatter do
|
||||
:<~>,
|
||||
:<|>,
|
||||
:^^^,
|
||||
:+++,
|
||||
:---,
|
||||
:in,
|
||||
:++,
|
||||
:--,
|
||||
@@ -140,8 +142,8 @@ defmodule Code.Formatter do
|
||||
Checks if two strings are equivalent.
|
||||
"""
|
||||
def equivalent(string1, string2) when is_binary(string1) and is_binary(string2) do
|
||||
quoted1 = :elixir.string_to_quoted!(to_charlist(string1), 1, "nofile", [])
|
||||
quoted2 = :elixir.string_to_quoted!(to_charlist(string2), 1, "nofile", [])
|
||||
quoted1 = :elixir.string_to_quoted!(to_charlist(string1), 1, 1, "nofile", [])
|
||||
quoted2 = :elixir.string_to_quoted!(to_charlist(string2), 1, 1, "nofile", [])
|
||||
|
||||
case not_equivalent(quoted1, quoted2) do
|
||||
{left, right} -> {:error, left, right}
|
||||
@@ -210,7 +212,7 @@ defmodule Code.Formatter do
|
||||
token_metadata: true
|
||||
]
|
||||
|
||||
with {:ok, tokens} <- :elixir.string_to_tokens(charlist, line, file, tokenizer_options),
|
||||
with {:ok, tokens} <- :elixir.string_to_tokens(charlist, line, 1, file, tokenizer_options),
|
||||
{:ok, forms} <- :elixir.tokens_to_quoted(tokens, file, parser_options) do
|
||||
state =
|
||||
Process.get(:code_formatter_comments)
|
||||
@@ -237,8 +239,8 @@ defmodule Code.Formatter do
|
||||
{:ok, doc} ->
|
||||
doc
|
||||
|
||||
{:error, {line, error, token}} ->
|
||||
:elixir_errors.parse_error(line, Keyword.get(opts, :file, "nofile"), error, token)
|
||||
{:error, {location, error, token}} ->
|
||||
:elixir_errors.parse_error(location, Keyword.get(opts, :file, "nofile"), error, token)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -483,6 +485,10 @@ defmodule Code.Formatter do
|
||||
quoted_to_algebra(arg, context, state)
|
||||
end
|
||||
|
||||
defp quoted_to_algebra({:__block__, _meta, []}, _context, state) do
|
||||
{"nil", state}
|
||||
end
|
||||
|
||||
defp quoted_to_algebra({:__block__, meta, _} = block, _context, state) do
|
||||
{block, state} = block_to_algebra(block, line(meta), closing_line(meta), state)
|
||||
{surround("(", block, ")"), state}
|
||||
@@ -1914,8 +1920,7 @@ defmodule Code.Formatter do
|
||||
{args_docs, comments?, %{state | comments: comments}}
|
||||
end
|
||||
|
||||
defp each_quoted_to_algebra_with_comments(args, acc, max_line, state, comments?, fun) do
|
||||
[arg | args] = args
|
||||
defp each_quoted_to_algebra_with_comments([arg | args], acc, max_line, state, comments?, fun) do
|
||||
{doc_start, doc_end} = traverse_line(arg, {@max_line, @min_line})
|
||||
|
||||
{acc, comments, comments?} =
|
||||
@@ -1926,7 +1931,7 @@ defmodule Code.Formatter do
|
||||
{acc, comments, comments?} =
|
||||
extract_comments_trailing(doc_start, doc_end, acc, state.comments, comments?)
|
||||
|
||||
acc = [doc_triplet | acc]
|
||||
acc = [adjust_trailing_newlines(doc_triplet, doc_end, comments) | acc]
|
||||
state = %{state | comments: comments}
|
||||
each_quoted_to_algebra_with_comments(args, acc, max_line, state, comments?, fun)
|
||||
end
|
||||
@@ -1957,6 +1962,15 @@ defmodule Code.Formatter do
|
||||
{acc, rest, comments?}
|
||||
end
|
||||
|
||||
# If the document is immediately followed by comment which is followed by newlines,
|
||||
# its newlines wouldn't have considered the comment, so we need to adjust it.
|
||||
defp adjust_trailing_newlines({doc, next_line, newlines}, doc_end, [{line, _, _} | _])
|
||||
when newlines > 1 and line == doc_end + 1 do
|
||||
{doc, next_line, 1}
|
||||
end
|
||||
|
||||
defp adjust_trailing_newlines(doc_triplet, _, _), do: doc_triplet
|
||||
|
||||
defp traverse_line({expr, meta, args}, {min, max}) do
|
||||
acc =
|
||||
case Keyword.fetch(meta, :line) do
|
||||
@@ -2139,7 +2153,7 @@ defmodule Code.Formatter do
|
||||
end
|
||||
|
||||
defp next_break_fits?({:__block__, meta, [list]}, _state) when is_list(list) do
|
||||
meta[:delimeter] != ~s[']
|
||||
meta[:delimiter] != ~s[']
|
||||
end
|
||||
|
||||
defp next_break_fits?({form, _, [_ | _]}, _state) when form in [:fn, :%{}, :%] do
|
||||
@@ -2239,10 +2253,10 @@ defmodule Code.Formatter do
|
||||
defp keyword?(rest), do: rest == []
|
||||
|
||||
defp keyword_key?({:__block__, meta, [atom]}) when is_atom(atom),
|
||||
do: meta[:delimiter] != ":"
|
||||
do: meta[:format] == :keyword
|
||||
|
||||
defp keyword_key?({{:., _, [:erlang, :binary_to_atom]}, meta, [{:<<>>, _, _}, :utf8]}),
|
||||
do: meta[:delimiter] != ":"
|
||||
do: meta[:format] == :keyword
|
||||
|
||||
defp keyword_key?(_),
|
||||
do: false
|
||||
|
||||
@@ -44,7 +44,7 @@ defmodule Code.Identifier do
|
||||
op in [:|>, :<<<, :>>>, :<~, :~>, :<<~, :~>>, :<~>, :<|>] -> {:left, 170}
|
||||
op in [:in] -> {:left, 180}
|
||||
op in [:^^^] -> {:left, 190}
|
||||
op in [:++, :--, :.., :<>] -> {:right, 200}
|
||||
op in [:++, :--, :.., :<>, :+++, :---] -> {:right, 200}
|
||||
op in [:+, :-] -> {:left, 210}
|
||||
op in [:*, :/] -> {:left, 220}
|
||||
op in [:.] -> {:left, 310}
|
||||
|
||||
@@ -217,7 +217,8 @@ defmodule Code.Typespec do
|
||||
end
|
||||
|
||||
defp typespec_to_quoted({:user_type, line, name, args}) do
|
||||
typespec_to_quoted({:type, line, name, args})
|
||||
args = for arg <- args, do: typespec_to_quoted(arg)
|
||||
{name, [line: line], args}
|
||||
end
|
||||
|
||||
defp typespec_to_quoted({:type, line, :tuple, :any}) do
|
||||
@@ -272,13 +273,13 @@ defmodule Code.Typespec do
|
||||
{{:optional, [], [typespec_to_quoted(k)]}, typespec_to_quoted(v)}
|
||||
end)
|
||||
|
||||
{struct, fields} = Keyword.pop(fields, :__struct__)
|
||||
map = {:%{}, [line: line], fields}
|
||||
case List.keytake(fields, :__struct__, 0) do
|
||||
{{:__struct__, struct}, fields_pruned} when is_atom(struct) and struct != nil ->
|
||||
map_pruned = {:%{}, [line: line], fields_pruned}
|
||||
{:%, [line: line], [struct, map_pruned]}
|
||||
|
||||
if struct do
|
||||
{:%, [line: line], [struct, map]}
|
||||
else
|
||||
map
|
||||
_ ->
|
||||
{:%{}, [line: line], fields}
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
+115
-53
@@ -13,7 +13,7 @@ defmodule Config do
|
||||
key1: "value1",
|
||||
key2: "value2"
|
||||
|
||||
import_config "#{Mix.env()}.exs"
|
||||
import_config "#{config_env()}.exs"
|
||||
|
||||
`import Config` will import the functions `config/2`, `config/3`
|
||||
and `import_config/1` to help you manage your configuration.
|
||||
@@ -26,9 +26,9 @@ defmodule Config do
|
||||
|
||||
"value1" = Application.fetch_env!(:some_app, :key1)
|
||||
|
||||
Finally, the line `import_config "#{Mix.env()}.exs"` will import other
|
||||
config files, based on the current Mix environment, such as
|
||||
`config/dev.exs` and `config/test.exs`.
|
||||
Finally, the line `import_config "#{config_env()}.exs"` will import
|
||||
other config files based on the current configuration environment,
|
||||
such as `config/dev.exs` and `config/test.exs`.
|
||||
|
||||
`Config` also provides a low-level API for evaluating and reading
|
||||
configuration, under the `Config.Reader` module.
|
||||
@@ -36,14 +36,14 @@ defmodule Config do
|
||||
**Important:** 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.html).
|
||||
read our [library guidelines](library-guidelines.md).
|
||||
|
||||
## Migrating from `use Mix.Config`
|
||||
|
||||
The `Config` module in Elixir was introduced in v1.9 as a replacement to
|
||||
`Mix.Config`, which was specific to Mix and has been deprecated.
|
||||
|
||||
You can leverage `Config` instead of `Mix.Config` in two steps. The first
|
||||
You can leverage `Config` instead of `Mix.Config` in three steps. The first
|
||||
step is to replace `use Mix.Config` at the top of your config files by
|
||||
`import Config`.
|
||||
|
||||
@@ -59,43 +59,30 @@ defmodule Config do
|
||||
import_config config
|
||||
end
|
||||
|
||||
## config/releases.exs
|
||||
The last step is to replace all `Mix.env()` calls by `config_env()`.
|
||||
|
||||
If you are using releases, see `mix release`, there is another configuration
|
||||
file called `config/releases.exs`. While `config/config.exs` and friends
|
||||
mentioned in the previous section are executed whenever you run a Mix
|
||||
command, including when you assemble a release, `config/releases.exs` is
|
||||
executed every time your production system boots. Since Mix is not available
|
||||
in a production system, `config/releases.exs` must not use any of the
|
||||
functions from Mix.
|
||||
## config/runtime.exs
|
||||
|
||||
For runtime configuration, you can use the `config/runtime.exs` file.
|
||||
It is executed right before applications start in both Mix and releases
|
||||
(assembled with `mix release`).
|
||||
"""
|
||||
|
||||
@opts_key {__MODULE__, :opts}
|
||||
@config_key {__MODULE__, :config}
|
||||
@files_key {__MODULE__, :files}
|
||||
@imports_key {__MODULE__, :imports}
|
||||
|
||||
defp get_config!() do
|
||||
Process.get(@config_key) || raise_improper_use!()
|
||||
end
|
||||
defp get_opts!(), do: Process.get(@opts_key)
|
||||
defp put_opts(value), do: Process.put(@opts_key, value)
|
||||
defp delete_opts(), do: Process.delete(@opts_key)
|
||||
|
||||
defp put_config(value) do
|
||||
Process.put(@config_key, value)
|
||||
end
|
||||
defp get_config!(), do: Process.get(@config_key) || raise_improper_use!()
|
||||
defp put_config(value), do: Process.put(@config_key, value)
|
||||
defp delete_config(), do: Process.delete(@config_key)
|
||||
|
||||
defp delete_config() do
|
||||
Process.delete(@config_key)
|
||||
end
|
||||
|
||||
defp get_files!() do
|
||||
Process.get(@files_key) || raise_improper_use!()
|
||||
end
|
||||
|
||||
defp put_files(value) do
|
||||
Process.put(@files_key, value)
|
||||
end
|
||||
|
||||
defp delete_files() do
|
||||
Process.delete(@files_key)
|
||||
end
|
||||
defp get_imports!(), do: Process.get(@imports_key) || raise_improper_use!()
|
||||
defp put_imports(value), do: Process.put(@imports_key, value)
|
||||
defp delete_imports(), do: Process.delete(@imports_key)
|
||||
|
||||
defp raise_improper_use!() do
|
||||
raise "could not set configuration via Config. " <>
|
||||
@@ -172,6 +159,55 @@ defmodule Config do
|
||||
|> put_config()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the environment this configuration file is executed on.
|
||||
|
||||
In Mix projects this function returns the environment this configuration
|
||||
file is executed on. In releases, the environment when `mix release` ran.
|
||||
|
||||
This is most often used to execute conditional code:
|
||||
|
||||
if config_env() == :prod do
|
||||
config :my_app, :debug, false
|
||||
end
|
||||
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
defmacro config_env() do
|
||||
quote do
|
||||
Config.__env__!()
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec __env__!() :: atom()
|
||||
def __env__!() do
|
||||
elem(get_opts!(), 0) || raise "no :env key was given to this configuration file"
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the target this configuration file is executed on.
|
||||
|
||||
This is most often used to execute conditional code:
|
||||
|
||||
if config_target() == :host do
|
||||
config :my_app, :debug, false
|
||||
end
|
||||
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
defmacro config_target() do
|
||||
quote do
|
||||
Config.__target__!()
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec __target__!() :: atom()
|
||||
def __target__!() do
|
||||
elem(get_opts!(), 1) || raise "no :target key was given to this configuration file"
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
Imports configuration from the given file.
|
||||
|
||||
@@ -184,8 +220,11 @@ defmodule Config do
|
||||
|
||||
This is often used to emulate configuration across environments:
|
||||
|
||||
import_config "#{Mix.env()}.exs"
|
||||
import_config "#{config_env()}.exs"
|
||||
|
||||
Note, however, some configuration files, such as `config/runtime.exs`
|
||||
does not support imports, as they are meant to be copied across
|
||||
systems.
|
||||
"""
|
||||
@doc since: "1.9.0"
|
||||
defmacro import_config(file) do
|
||||
@@ -198,39 +237,62 @@ defmodule Config do
|
||||
@doc false
|
||||
@spec __import__!(Path.t()) :: {term, Code.binding()}
|
||||
def __import__!(file) when is_binary(file) do
|
||||
current_files = get_files!()
|
||||
|
||||
if file in current_files do
|
||||
raise ArgumentError,
|
||||
"attempting to load configuration #{Path.relative_to_cwd(file)} recursively"
|
||||
end
|
||||
|
||||
put_files([file | current_files])
|
||||
Code.eval_file(file)
|
||||
import_config!(file, File.read!(file), true)
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec __eval__!(Path.t(), [Path.t()]) :: {keyword, [Path.t()]}
|
||||
def __eval__!(file, imported_paths \\ []) when is_binary(file) and is_list(imported_paths) do
|
||||
@spec __eval__!(Path.t(), binary(), keyword) :: {keyword, [Path.t()] | :disabled}
|
||||
def __eval__!(file, content, opts \\ []) when is_binary(file) and is_list(opts) do
|
||||
env = Keyword.get(opts, :env)
|
||||
target = Keyword.get(opts, :target)
|
||||
imports = Keyword.get(opts, :imports, [])
|
||||
|
||||
previous_opts = put_opts({env, target})
|
||||
previous_config = put_config([])
|
||||
previous_files = put_files(imported_paths)
|
||||
previous_imports = put_imports(imports)
|
||||
|
||||
try do
|
||||
{eval_config, _} = __import__!(Path.expand(file))
|
||||
{eval_config, _} = import_config!(file, content, false)
|
||||
|
||||
case get_config!() do
|
||||
[] when is_list(eval_config) ->
|
||||
{validate!(eval_config, file), get_files!()}
|
||||
{validate!(eval_config, file), get_imports!()}
|
||||
|
||||
pdict_config ->
|
||||
{pdict_config, get_files!()}
|
||||
{pdict_config, get_imports!()}
|
||||
end
|
||||
after
|
||||
if previous_opts, do: put_opts(previous_opts), else: delete_opts()
|
||||
if previous_config, do: put_config(previous_config), else: delete_config()
|
||||
if previous_files, do: put_files(previous_files), else: delete_files()
|
||||
if previous_imports, do: put_imports(previous_imports), else: delete_imports()
|
||||
end
|
||||
end
|
||||
|
||||
defp import_config!(file, contents, raise_when_disabled?) do
|
||||
current_imports = get_imports!()
|
||||
|
||||
cond do
|
||||
current_imports == :disabled ->
|
||||
if raise_when_disabled? do
|
||||
raise "import_config/1 is not enabled for this configuration file. " <>
|
||||
"Some configuration files do not allow importing other files " <>
|
||||
"as they are often copied to external systems"
|
||||
end
|
||||
|
||||
file in current_imports ->
|
||||
raise ArgumentError,
|
||||
"attempting to load configuration #{Path.relative_to_cwd(file)} recursively"
|
||||
|
||||
true ->
|
||||
put_imports([file | current_imports])
|
||||
:ok
|
||||
end
|
||||
|
||||
# TODO: Emit a warning if Mix.env() is found in said files in Elixir v1.15.
|
||||
# Note this won't be a deprecation warning as it will always be emitted.
|
||||
Code.eval_string(contents, [], file: file)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def __merge__(config1, config2) when is_list(config1) and is_list(config2) do
|
||||
Keyword.merge(config1, config2, fn _, app1, app2 ->
|
||||
|
||||
@@ -109,10 +109,13 @@ defmodule Config.Provider do
|
||||
:providers,
|
||||
:config_path,
|
||||
extra_config: [],
|
||||
prune_after_boot: false,
|
||||
reboot_after_config: true
|
||||
prune_runtime_sys_config_after_boot: false,
|
||||
reboot_system_after_config: false,
|
||||
validate_compile_env: false
|
||||
]
|
||||
|
||||
@reserved_apps [:kernel, :stdlib]
|
||||
|
||||
@doc """
|
||||
Validates a `t:config_path/0`.
|
||||
"""
|
||||
@@ -146,80 +149,99 @@ defmodule Config.Provider do
|
||||
def resolve_config_path!(path) when is_binary(path), do: path
|
||||
def resolve_config_path!({:system, name, path}), do: System.fetch_env!(name) <> path
|
||||
|
||||
# Private keys
|
||||
@init_key :config_provider_init
|
||||
@booted_key :config_provider_booted
|
||||
|
||||
# Public keys
|
||||
@reboot_mode_key :config_provider_reboot_mode
|
||||
|
||||
@doc false
|
||||
def init(providers, config_path, opts \\ []) when is_list(providers) and is_list(opts) do
|
||||
validate_config_path!(config_path)
|
||||
providers = for {provider, init} <- providers, do: {provider, provider.init(init)}
|
||||
struct!(%Config.Provider{config_path: config_path, providers: providers}, opts)
|
||||
init = struct!(%Config.Provider{config_path: config_path, providers: providers}, opts)
|
||||
[elixir: [{@init_key, init}]]
|
||||
end
|
||||
|
||||
@doc false
|
||||
def boot(app, key, restart_fun \\ &restart_and_sleep/0) do
|
||||
# The app with the config provider settings may not
|
||||
# have been loaded at this point, so make sure we load
|
||||
# its environment before querying it.
|
||||
_ = :application.load(app)
|
||||
|
||||
def boot(reboot_fun \\ &restart_and_sleep/0) do
|
||||
# The config provider typically runs very early in the
|
||||
# release process, so we need to make sure Elixir is started
|
||||
# before we go around running Elixir code.
|
||||
{:ok, _} = :application.ensure_all_started(:elixir)
|
||||
|
||||
case :application.get_env(app, key) do
|
||||
{:ok, %Config.Provider{} = provider} ->
|
||||
path = resolve_config_path!(provider.config_path)
|
||||
validate_no_cyclic_boot!(path)
|
||||
loaded_applications = :application.loaded_applications()
|
||||
original_config = read_config!(path)
|
||||
case Application.fetch_env(:elixir, @booted_key) do
|
||||
{:ok, {:booted, path}} ->
|
||||
path && File.rm(path)
|
||||
|
||||
config =
|
||||
original_config
|
||||
|> Config.__merge__(provider.extra_config)
|
||||
|> run_providers(provider)
|
||||
|
||||
if provider.reboot_after_config do
|
||||
config
|
||||
|> Config.__merge__([{app, [{key, booted_key(provider, path)}]}])
|
||||
|> write_config!(path)
|
||||
|
||||
restart_fun.()
|
||||
else
|
||||
for {app, _, _} <- loaded_applications, config[app] != original_config[app] do
|
||||
abort("""
|
||||
Cannot configure #{inspect(app)} because :reboot_after_config has been set \
|
||||
to false and #{inspect(app)} has already been loaded, meaning any further \
|
||||
configuration won't have an effect.
|
||||
|
||||
The configuration for #{inspect(app)} before config providers was:
|
||||
|
||||
#{inspect(original_config[app])}
|
||||
|
||||
The configuration for #{inspect(app)} after config providers was:
|
||||
|
||||
#{inspect(config[app])}
|
||||
""")
|
||||
end
|
||||
|
||||
_ = Application.put_all_env(config, persistent: true)
|
||||
:ok
|
||||
with {:ok, %Config.Provider{} = provider} <- Application.fetch_env(:elixir, @init_key) do
|
||||
maybe_validate_compile_env(provider)
|
||||
end
|
||||
|
||||
{:ok, {:booted, path}} ->
|
||||
File.rm(path)
|
||||
:booted
|
||||
|
||||
{:ok, :booted} ->
|
||||
:booted
|
||||
|
||||
_ ->
|
||||
:skip
|
||||
case Application.fetch_env(:elixir, @init_key) do
|
||||
{:ok, %Config.Provider{} = provider} ->
|
||||
path = resolve_config_path!(provider.config_path)
|
||||
reboot_config = [elixir: [{@booted_key, booted_value(provider, path)}]]
|
||||
boot_providers(path, provider, reboot_config, reboot_fun)
|
||||
|
||||
_ ->
|
||||
:skip
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp boot_providers(path, provider, reboot_config, reboot_fun) do
|
||||
validate_no_cyclic_boot!(path)
|
||||
original_config = read_config!(path)
|
||||
|
||||
config =
|
||||
original_config
|
||||
|> Config.__merge__(provider.extra_config)
|
||||
|> run_providers(provider)
|
||||
|
||||
if provider.reboot_system_after_config do
|
||||
config
|
||||
|> Config.__merge__(reboot_config)
|
||||
|> write_config!(path)
|
||||
|
||||
reboot_fun.()
|
||||
else
|
||||
for app <- @reserved_apps, config[app] != original_config[app] do
|
||||
abort("""
|
||||
Cannot configure #{inspect(app)} because :reboot_system_after_config has been set \
|
||||
to false and #{inspect(app)} has already been loaded, meaning any further \
|
||||
configuration won't have an effect.
|
||||
|
||||
The configuration for #{inspect(app)} before config providers was:
|
||||
|
||||
#{inspect(original_config[app])}
|
||||
|
||||
The configuration for #{inspect(app)} after config providers was:
|
||||
|
||||
#{inspect(config[app])}
|
||||
""")
|
||||
end
|
||||
|
||||
_ = Application.put_all_env(config, persistent: true)
|
||||
maybe_validate_compile_env(provider)
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_validate_compile_env(provider) do
|
||||
with [_ | _] = compile_env <- provider.validate_compile_env do
|
||||
validate_compile_env(compile_env)
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def validate_compile_env(compile_env) do
|
||||
def validate_compile_env(compile_env, ensure_loaded? \\ true) do
|
||||
for {app, [key | path], compile_return} <- compile_env,
|
||||
Application.ensure_loaded(app) == :ok do
|
||||
ensure_app_loaded?(app, ensure_loaded?) do
|
||||
try do
|
||||
traverse_env(Application.fetch_env(app, key), path)
|
||||
rescue
|
||||
@@ -254,6 +276,9 @@ defmodule Config.Provider do
|
||||
:ok
|
||||
end
|
||||
|
||||
defp ensure_app_loaded?(app, true), do: Application.ensure_loaded(app) == :ok
|
||||
defp ensure_app_loaded?(app, false), do: Application.spec(app, :vsn) != nil
|
||||
|
||||
defp path(key, []), do: "for key #{inspect(key)}"
|
||||
defp path(key, path), do: "for path #{inspect(path)} inside key #{inspect(key)}"
|
||||
|
||||
@@ -278,13 +303,22 @@ defmodule Config.Provider do
|
||||
defp traverse_env(:error, _paths), do: :error
|
||||
defp traverse_env({:ok, value}, [key | keys]), do: traverse_env(Access.fetch(value, key), keys)
|
||||
|
||||
defp restart_and_sleep do
|
||||
:init.restart()
|
||||
@compile {:no_warn_undefined, {:init, :restart, 1}}
|
||||
defp restart_and_sleep() do
|
||||
mode = Application.get_env(:elixir, @reboot_mode_key)
|
||||
|
||||
# TODO: Remove otp_release check once we require Erlang/OTP 23+
|
||||
if :erlang.system_info(:otp_release) >= '23' and mode in [:embedded, :interactive] do
|
||||
:init.restart(mode: mode)
|
||||
else
|
||||
:init.restart()
|
||||
end
|
||||
|
||||
Process.sleep(:infinity)
|
||||
end
|
||||
|
||||
defp booted_key(%{prune_after_boot: true}, path), do: {:booted, path}
|
||||
defp booted_key(%{prune_after_boot: false}, _path), do: :booted
|
||||
defp booted_value(%{prune_runtime_sys_config_after_boot: true}, path), do: {:booted, path}
|
||||
defp booted_value(%{prune_runtime_sys_config_after_boot: false}, _path), do: {:booted, nil}
|
||||
|
||||
defp validate_no_cyclic_boot!(path) do
|
||||
if System.get_env("ELIXIR_CONFIG_PROVIDER_BOOTED") do
|
||||
@@ -329,7 +363,7 @@ defmodule Config.Provider do
|
||||
defp write_config!(config, path) do
|
||||
contents = :io_lib.format("%% coding: utf-8~n~tw.~n", [config])
|
||||
|
||||
case File.write(path, contents, [:utf8]) do
|
||||
case File.write(path, IO.chardata_to_string(contents)) do
|
||||
:ok ->
|
||||
:ok
|
||||
|
||||
|
||||
@@ -18,8 +18,18 @@ defmodule Config.Reader do
|
||||
|
||||
config_provider: [{Config.Reader, {:system, "RELEASE_ROOT", "/config.exs"}}]
|
||||
|
||||
You can also pass a keyword list of options to the reader,
|
||||
where the `:path` is a required key:
|
||||
|
||||
config_providers: [
|
||||
{Config.Reader,
|
||||
path: "/etc/config.exs",
|
||||
env: :prod,
|
||||
imports: :disabled}
|
||||
]
|
||||
|
||||
Note by default Mix releases supports runtime configuration via
|
||||
a `config/releases.exs`. If a `config/releases.exs` exists in your
|
||||
a `config/runtime.exs`. If a `config/runtime.exs` exists in your
|
||||
application, it is automatically copied inside the release and
|
||||
automatically set as a config provider.
|
||||
"""
|
||||
@@ -27,49 +37,71 @@ defmodule Config.Reader do
|
||||
@behaviour Config.Provider
|
||||
|
||||
@impl true
|
||||
def init(path) do
|
||||
def init(opts) when is_list(opts) do
|
||||
{path, opts} = Keyword.pop!(opts, :path)
|
||||
Config.Provider.validate_config_path!(path)
|
||||
path
|
||||
{path, opts}
|
||||
end
|
||||
|
||||
def init(path) do
|
||||
init(path: path)
|
||||
end
|
||||
|
||||
@impl true
|
||||
def load(config, path) do
|
||||
merge(config, path |> Config.Provider.resolve_config_path!() |> read!())
|
||||
def load(config, {path, opts}) do
|
||||
merge(config, path |> Config.Provider.resolve_config_path!() |> read!(opts))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Evaluates the configuration `contents` for the given `file`.
|
||||
|
||||
Accepts the same options as `read!/2`.
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
@spec eval!(Path.t(), binary, keyword) :: keyword
|
||||
def eval!(file, contents, opts \\ [])
|
||||
when is_binary(file) and is_binary(contents) and is_list(opts) do
|
||||
Config.__eval__!(Path.expand(file), contents, opts) |> elem(0)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Reads the configuration file.
|
||||
|
||||
The same as `read_imports!/2` but only returns the configuration
|
||||
in the given file, without returning the imported paths.
|
||||
## Options
|
||||
|
||||
It exists for convenience purposes. For example, you could
|
||||
invoke it inside your `mix.exs` to read some external data
|
||||
you decided to move to a configuration file:
|
||||
* `:imports` - a list of already imported paths or `:disabled`
|
||||
to disable imports
|
||||
|
||||
releases: Config.Reader.read!("rel/releases.exs")
|
||||
* `:env` - the environment the configuration file runs on.
|
||||
See `Config.config_env/0` for sample usage
|
||||
|
||||
* `:target` - the target the configuration file runs on.
|
||||
See `Config.config_target/0` for sample usage
|
||||
|
||||
"""
|
||||
@doc since: "1.9.0"
|
||||
@spec read!(Path.t(), [Path.t()]) :: keyword
|
||||
def read!(file, imported_paths \\ [])
|
||||
when is_binary(file) and is_list(imported_paths) do
|
||||
Config.__eval__!(file, imported_paths) |> elem(0)
|
||||
@spec read!(Path.t(), keyword) :: keyword
|
||||
def read!(file, opts \\ []) when is_binary(file) and is_list(opts) do
|
||||
file = Path.expand(file)
|
||||
Config.__eval__!(file, File.read!(file), opts) |> elem(0)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Reads the given configuration file alongside its imports.
|
||||
Reads the given configuration file and returns the configuration
|
||||
with its imports.
|
||||
|
||||
It accepts a list of `imported_paths` that should raise if attempted
|
||||
to be imported again (to avoid recursive imports).
|
||||
|
||||
It returns a tuple with the configuration and the imported paths.
|
||||
Accepts the same options as `read!/2`. Although note the `:imports`
|
||||
option cannot be disabled in `read_imports!/2`.
|
||||
"""
|
||||
@doc since: "1.9.0"
|
||||
@spec read_imports!(Path.t(), [Path.t()]) :: {keyword, [Path.t()]}
|
||||
def read_imports!(file, imported_paths \\ [])
|
||||
when is_binary(file) and is_list(imported_paths) do
|
||||
Config.__eval__!(file, imported_paths)
|
||||
@spec read_imports!(Path.t(), keyword) :: {keyword, [Path.t()]}
|
||||
def read_imports!(file, opts \\ []) when is_binary(file) and is_list(opts) do
|
||||
if opts[:imports] == :disabled do
|
||||
raise ArgumentError, ":imports must be a list of paths"
|
||||
end
|
||||
|
||||
file = Path.expand(file)
|
||||
Config.__eval__!(file, File.read!(file), opts)
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
||||
@@ -18,11 +18,14 @@ defmodule Dict do
|
||||
message =
|
||||
"Use the Map module for working with maps or the Keyword module for working with keyword lists"
|
||||
|
||||
@deprecated message
|
||||
defmacro __using__(_) do
|
||||
# Use this import to guarantee proper code expansion
|
||||
import Kernel, except: [size: 1]
|
||||
|
||||
if __CALLER__.module != HashDict do
|
||||
IO.warn("use Dict is deprecated. " <> unquote(message), Macro.Env.stacktrace(__CALLER__))
|
||||
end
|
||||
|
||||
quote do
|
||||
message = "Use maps and the Map module instead"
|
||||
|
||||
@@ -152,13 +155,13 @@ defmodule Dict do
|
||||
end
|
||||
|
||||
@deprecated message
|
||||
def update(dict, key, initial, fun) do
|
||||
def update(dict, key, default, fun) do
|
||||
case fetch(dict, key) do
|
||||
{:ok, value} ->
|
||||
put(dict, key, fun.(value))
|
||||
|
||||
:error ->
|
||||
put(dict, key, initial)
|
||||
put(dict, key, default)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -375,8 +378,8 @@ defmodule Dict do
|
||||
|
||||
@deprecated message
|
||||
@spec update(t, key, value, (value -> value)) :: t
|
||||
def update(dict, key, initial, fun) do
|
||||
target(dict).update(dict, key, initial, fun)
|
||||
def update(dict, key, default, fun) do
|
||||
target(dict).update(dict, key, default, fun)
|
||||
end
|
||||
|
||||
@deprecated message
|
||||
|
||||
@@ -418,7 +418,8 @@ defmodule DynamicSupervisor do
|
||||
"""
|
||||
@doc since: "1.6.0"
|
||||
@spec which_children(Supervisor.supervisor()) :: [
|
||||
{:undefined, pid | :restarting, :worker | :supervisor, :supervisor.modules()}
|
||||
# module() | :dynamic here because :supervisor.modules() is not exported
|
||||
{:undefined, pid | :restarting, :worker | :supervisor, module() | :dynamic}
|
||||
]
|
||||
def which_children(supervisor) do
|
||||
call(supervisor, :which_children)
|
||||
|
||||
+152
-65
@@ -20,7 +20,7 @@ defprotocol Enumerable do
|
||||
Enumerable.reduce(enumerable, {:cont, []}, reducer) |> elem(1) |> :lists.reverse()
|
||||
end
|
||||
|
||||
Notice the user-supplied function is wrapped into a `t:reducer/0` function.
|
||||
Note that the user-supplied function is wrapped into a `t:reducer/0` function.
|
||||
The `t:reducer/0` function must return a tagged tuple after each step,
|
||||
as described in the `t:acc/0` type. At the end, `Enumerable.reduce/3`
|
||||
returns `t:result/0`.
|
||||
@@ -161,6 +161,9 @@ defprotocol Enumerable do
|
||||
|
||||
Otherwise it should return `{:error, __MODULE__}` and a default algorithm
|
||||
built on top of `reduce/3` that runs in linear time will be used.
|
||||
|
||||
When not called whithin guards, the [`in`](`in/2`) and [`not in`](`in/2`)
|
||||
operators work by using this function.
|
||||
"""
|
||||
@spec member?(t, term) :: {:ok, boolean} | {:error, module}
|
||||
def member?(enumerable, element)
|
||||
@@ -1334,12 +1337,12 @@ defmodule Enum do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Joins the given `enumerable` into a binary using `joiner` as a
|
||||
Joins the given `enumerable` into a string using `joiner` as a
|
||||
separator.
|
||||
|
||||
If `joiner` is not passed at all, it defaults to the empty binary.
|
||||
If `joiner` is not passed at all, it defaults to an empty string.
|
||||
|
||||
All elements in the `enumerable` must be convertible to a binary,
|
||||
All elements in the `enumerable` must be convertible to a string,
|
||||
otherwise an error is raised.
|
||||
|
||||
## Examples
|
||||
@@ -1473,12 +1476,10 @@ defmodule Enum do
|
||||
@doc """
|
||||
Maps and joins the given `enumerable` in one pass.
|
||||
|
||||
`joiner` can be either a binary or a list and the result will be of
|
||||
the same type as `joiner`.
|
||||
If `joiner` is not passed at all, it defaults to an empty binary.
|
||||
If `joiner` is not passed at all, it defaults to an empty string.
|
||||
|
||||
All elements returned from invoking the `mapper` must be convertible to
|
||||
a binary, otherwise an error is raised.
|
||||
a string, otherwise an error is raised.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -1530,6 +1531,12 @@ defmodule Enum do
|
||||
{:lists.reverse(list), acc}
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec max(t, (() -> empty_result)) :: element | empty_result when empty_result: any
|
||||
def max(enumerable, empty_fallback) when is_function(empty_fallback, 0) do
|
||||
max(enumerable, &>=/2, empty_fallback)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the maximal element in the `enumerable` according
|
||||
to Erlang's term ordering.
|
||||
@@ -1557,8 +1564,8 @@ defmodule Enum do
|
||||
In the example above, `max/2` returned March 31st instead of April 1st
|
||||
because the structural comparison compares the day before the year.
|
||||
For this reason, most structs provide a "compare" function, such as
|
||||
`Date.compare/2`, which receives two structs and returns `:lt` (less than),
|
||||
`:eq` (equal), and `:gt` (greater than). If you pass a module as the
|
||||
`Date.compare/2`, which receives two structs and returns `:lt` (less-than),
|
||||
`:eq` (equal to), and `:gt` (greater-than). If you pass a module as the
|
||||
sorting function, Elixir will automatically use the `compare/2` function
|
||||
of said module:
|
||||
|
||||
@@ -1572,26 +1579,30 @@ defmodule Enum do
|
||||
0
|
||||
|
||||
"""
|
||||
@spec max(t, (element, element -> boolean) | module()) ::
|
||||
element | empty_result
|
||||
when empty_result: any
|
||||
@spec max(t, (element, element -> boolean) | module(), (() -> empty_result)) ::
|
||||
element | empty_result
|
||||
when empty_result: any
|
||||
def max(enumerable, sorter \\ &>=/2, empty_fallback \\ fn -> raise Enum.EmptyError end) do
|
||||
{sorter, empty_fallback} = max_sort_fun(sorter, empty_fallback)
|
||||
aggregate(enumerable, sorter, empty_fallback)
|
||||
aggregate(enumerable, max_sort_fun(sorter), empty_fallback)
|
||||
end
|
||||
|
||||
# TODO: Deprecate me on 1.14
|
||||
defp max_sort_fun(empty_fallback, default_empty_fallback)
|
||||
when is_function(empty_fallback, 0) and is_function(default_empty_fallback, 0),
|
||||
do: {&>=/2, empty_fallback}
|
||||
defp max_sort_fun(sorter) when is_function(sorter, 2), do: sorter
|
||||
defp max_sort_fun(module) when is_atom(module), do: &(module.compare(&1, &2) != :lt)
|
||||
|
||||
defp max_sort_fun(sorter, empty_fallback)
|
||||
when is_function(sorter, 2) and is_function(empty_fallback, 0),
|
||||
do: {sorter, empty_fallback}
|
||||
|
||||
defp max_sort_fun(module, empty_fallback)
|
||||
when is_atom(module) and is_function(empty_fallback, 0),
|
||||
do: {&(module.compare(&1, &2) != :lt), empty_fallback}
|
||||
@doc false
|
||||
@spec max_by(
|
||||
t,
|
||||
(element -> any),
|
||||
(() -> empty_result) | (element, element -> boolean) | module()
|
||||
) :: element | empty_result
|
||||
when empty_result: any
|
||||
def max_by(enumerable, fun, empty_fallback)
|
||||
when is_function(fun, 1) and is_function(empty_fallback, 0) do
|
||||
max_by(enumerable, fun, &>=/2, empty_fallback)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the maximal element in the `enumerable` as calculated
|
||||
@@ -1617,8 +1628,8 @@ defmodule Enum do
|
||||
The fact this function uses Erlang's term ordering means that the
|
||||
comparison is structural and not semantic. Therefore, if you want
|
||||
to compare structs, most structs provide a "compare" function, such as
|
||||
`Date.compare/2`, which receives two structs and returns `:lt` (less than),
|
||||
`:eq` (equal), and `:gt` (greater than). If you pass a module as the
|
||||
`Date.compare/2`, which receives two structs and returns `:lt` (less-than),
|
||||
`:eq` (equal to), and `:gt` (greater-than). If you pass a module as the
|
||||
sorting function, Elixir will automatically use the `compare/2` function
|
||||
of said module:
|
||||
|
||||
@@ -1646,8 +1657,7 @@ defmodule Enum do
|
||||
when empty_result: any
|
||||
def max_by(enumerable, fun, sorter \\ &>=/2, empty_fallback \\ fn -> raise Enum.EmptyError end)
|
||||
when is_function(fun, 1) do
|
||||
{sorter, empty_fallback} = max_sort_fun(sorter, empty_fallback)
|
||||
aggregate_by(enumerable, fun, sorter, empty_fallback)
|
||||
aggregate_by(enumerable, fun, max_sort_fun(sorter), empty_fallback)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -1670,6 +1680,9 @@ defmodule Enum do
|
||||
iex> Enum.member?([:a, :b, :c], :d)
|
||||
false
|
||||
|
||||
|
||||
When not called whithin guards, the [`in`](`in/2`) and [`not in`](`in/2`)
|
||||
operators work by using this function.
|
||||
"""
|
||||
@spec member?(t, element) :: boolean
|
||||
def member?(enumerable, element) when is_list(enumerable) do
|
||||
@@ -1690,6 +1703,12 @@ defmodule Enum do
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec min(t, (() -> empty_result)) :: element | empty_result when empty_result: any
|
||||
def min(enumerable, empty_fallback) when is_function(empty_fallback, 0) do
|
||||
min(enumerable, &<=/2, empty_fallback)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the minimal element in the `enumerable` according
|
||||
to Erlang's term ordering.
|
||||
@@ -1717,8 +1736,8 @@ defmodule Enum do
|
||||
In the example above, `min/2` returned April 1st instead of March 31st
|
||||
because the structural comparison compares the day before the year.
|
||||
For this reason, most structs provide a "compare" function, such as
|
||||
`Date.compare/2`, which receives two structs and returns `:lt` (less than),
|
||||
`:eq` (equal), and `:gt` (greater than). If you pass a module as the
|
||||
`Date.compare/2`, which receives two structs and returns `:lt` (less-than),
|
||||
`:eq` (equal to), and `:gt` (greater-than). If you pass a module as the
|
||||
sorting function, Elixir will automatically use the `compare/2` function
|
||||
of said module:
|
||||
|
||||
@@ -1728,30 +1747,34 @@ defmodule Enum do
|
||||
Finally, if you don't want to raise on empty enumerables, you can pass
|
||||
the empty fallback:
|
||||
|
||||
iex> Enum.min([], &<=/2, fn -> 0 end)
|
||||
iex> Enum.min([], fn -> 0 end)
|
||||
0
|
||||
|
||||
"""
|
||||
@spec min(t, (element, element -> boolean) | module()) ::
|
||||
element | empty_result
|
||||
when empty_result: any
|
||||
@spec min(t, (element, element -> boolean) | module(), (() -> empty_result)) ::
|
||||
element | empty_result
|
||||
when empty_result: any
|
||||
def min(enumerable, sorter \\ &<=/2, empty_fallback \\ fn -> raise Enum.EmptyError end) do
|
||||
{sorter, empty_fallback} = min_sort_fun(sorter, empty_fallback)
|
||||
aggregate(enumerable, sorter, empty_fallback)
|
||||
aggregate(enumerable, min_sort_fun(sorter), empty_fallback)
|
||||
end
|
||||
|
||||
# TODO: Deprecate me on 1.14
|
||||
defp min_sort_fun(empty_fallback, default_empty_fallback)
|
||||
when is_function(empty_fallback, 0) and is_function(default_empty_fallback, 0),
|
||||
do: {&<=/2, empty_fallback}
|
||||
defp min_sort_fun(sorter) when is_function(sorter, 2), do: sorter
|
||||
defp min_sort_fun(module) when is_atom(module), do: &(module.compare(&1, &2) != :gt)
|
||||
|
||||
defp min_sort_fun(sorter, empty_fallback)
|
||||
when is_function(sorter, 2) and is_function(empty_fallback, 0),
|
||||
do: {sorter, empty_fallback}
|
||||
|
||||
defp min_sort_fun(module, empty_fallback)
|
||||
when is_atom(module) and is_function(empty_fallback, 0),
|
||||
do: {&(module.compare(&1, &2) != :gt), empty_fallback}
|
||||
@doc false
|
||||
@spec min_by(
|
||||
t,
|
||||
(element -> any),
|
||||
(() -> empty_result) | (element, element -> boolean) | module()
|
||||
) :: element | empty_result
|
||||
when empty_result: any
|
||||
def min_by(enumerable, fun, empty_fallback)
|
||||
when is_function(fun, 1) and is_function(empty_fallback, 0) do
|
||||
min_by(enumerable, fun, &<=/2, empty_fallback)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the minimal element in the `enumerable` as calculated
|
||||
@@ -1777,8 +1800,8 @@ defmodule Enum do
|
||||
The fact this function uses Erlang's term ordering means that the
|
||||
comparison is structural and not semantic. Therefore, if you want
|
||||
to compare structs, most structs provide a "compare" function, such as
|
||||
`Date.compare/2`, which receives two structs and returns `:lt` (less than),
|
||||
`:eq` (equal), and `:gt` (greater than). If you pass a module as the
|
||||
`Date.compare/2`, which receives two structs and returns `:lt` (less-than),
|
||||
`:eq` (equal to), and `:gt` (greater-than). If you pass a module as the
|
||||
sorting function, Elixir will automatically use the `compare/2` function
|
||||
of said module:
|
||||
|
||||
@@ -1797,12 +1820,16 @@ defmodule Enum do
|
||||
nil
|
||||
|
||||
"""
|
||||
@spec min_by(t, (element -> any), (() -> empty_result)) :: element | empty_result
|
||||
@spec min_by(
|
||||
t,
|
||||
(element -> any),
|
||||
(element, element -> boolean) | module(),
|
||||
(() -> empty_result)
|
||||
) :: element | empty_result
|
||||
when empty_result: any
|
||||
def min_by(enumerable, fun, sorter \\ &<=/2, empty_fallback \\ fn -> raise Enum.EmptyError end)
|
||||
when is_function(fun, 1) do
|
||||
{sorter, empty_fallback} = min_sort_fun(sorter, empty_fallback)
|
||||
aggregate_by(enumerable, fun, sorter, empty_fallback)
|
||||
aggregate_by(enumerable, fun, min_sort_fun(sorter), empty_fallback)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -1845,6 +1872,14 @@ defmodule Enum do
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec min_max_by(t, (element -> any), (() -> empty_result)) :: {element, element} | empty_result
|
||||
when empty_result: any
|
||||
def min_max_by(enumerable, fun, empty_fallback)
|
||||
when is_function(fun, 1) and is_function(empty_fallback, 0) do
|
||||
min_max_by(enumerable, fun, &</2, empty_fallback)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a tuple with the minimal and the maximal elements in the
|
||||
enumerable as calculated by the given function.
|
||||
@@ -1852,9 +1887,6 @@ defmodule Enum do
|
||||
If multiple elements are considered maximal or minimal, the first one
|
||||
that was found is returned.
|
||||
|
||||
Calls the provided `empty_fallback` function and returns its value if
|
||||
`enumerable` is empty. The default `empty_fallback` raises `Enum.EmptyError`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Enum.min_max_by(["aaa", "bb", "c"], fn x -> String.length(x) end)
|
||||
@@ -1866,11 +1898,56 @@ defmodule Enum do
|
||||
iex> Enum.min_max_by([], &String.length/1, fn -> {nil, nil} end)
|
||||
{nil, nil}
|
||||
|
||||
The fact this function uses Erlang's term ordering means that the
|
||||
comparison is structural and not semantic. Therefore, if you want
|
||||
to compare structs, most structs provide a "compare" function, such as
|
||||
`Date.compare/2`, which receives two structs and returns `:lt` (less-than),
|
||||
`:eq` (equal to), and `:gt` (greater-than). If you pass a module as the
|
||||
sorting function, Elixir will automatically use the `compare/2` function
|
||||
of said module:
|
||||
|
||||
iex> users = [
|
||||
...> %{name: "Ellis", birthday: ~D[1943-05-11]},
|
||||
...> %{name: "Lovelace", birthday: ~D[1815-12-10]},
|
||||
...> %{name: "Turing", birthday: ~D[1912-06-23]}
|
||||
...> ]
|
||||
iex> Enum.min_max_by(users, &(&1.birthday), Date)
|
||||
{
|
||||
%{name: "Lovelace", birthday: ~D[1815-12-10]},
|
||||
%{name: "Ellis", birthday: ~D[1943-05-11]}
|
||||
}
|
||||
|
||||
Finally, if you don't want to raise on empty enumerables, you can pass
|
||||
the empty fallback:
|
||||
|
||||
iex> Enum.min_max_by([], &String.length/1, fn -> nil end)
|
||||
nil
|
||||
|
||||
"""
|
||||
@spec min_max_by(t, (element -> any), (() -> empty_result)) :: {element, element} | empty_result
|
||||
@spec min_max_by(t, (element -> any), (element, element -> boolean) | module()) ::
|
||||
{element, element} | empty_result
|
||||
when empty_result: any
|
||||
def min_max_by(enumerable, fun, empty_fallback \\ fn -> raise Enum.EmptyError end)
|
||||
when is_function(fun, 1) and is_function(empty_fallback, 0) do
|
||||
@spec min_max_by(
|
||||
t,
|
||||
(element -> any),
|
||||
(element, element -> boolean) | module(),
|
||||
(() -> empty_result)
|
||||
) :: {element, element} | empty_result
|
||||
when empty_result: any
|
||||
def min_max_by(
|
||||
enumerable,
|
||||
fun,
|
||||
sorter_or_empty_fallback \\ &</2,
|
||||
empty_fallback \\ fn -> raise Enum.EmptyError end
|
||||
)
|
||||
|
||||
def min_max_by(enumerable, fun, sorter, empty_fallback)
|
||||
when is_function(fun, 1) and is_atom(sorter) and is_function(empty_fallback, 0) do
|
||||
min_max_by(enumerable, fun, min_max_by_sort_fun(sorter), empty_fallback)
|
||||
end
|
||||
|
||||
def min_max_by(enumerable, fun, sorter, empty_fallback)
|
||||
when is_function(fun, 1) and is_function(sorter, 2) and is_function(empty_fallback, 0) do
|
||||
first_fun = fn entry ->
|
||||
fun_entry = fun.(entry)
|
||||
{entry, entry, fun_entry, fun_entry}
|
||||
@@ -1880,10 +1957,10 @@ defmodule Enum do
|
||||
fun_entry = fun.(entry)
|
||||
|
||||
cond do
|
||||
fun_entry < fun_min ->
|
||||
sorter.(fun_entry, fun_min) ->
|
||||
{entry, prev_max, fun_entry, fun_max}
|
||||
|
||||
fun_entry > fun_max ->
|
||||
sorter.(fun_max, fun_entry) ->
|
||||
{prev_min, entry, fun_min, fun_entry}
|
||||
|
||||
true ->
|
||||
@@ -1897,6 +1974,8 @@ defmodule Enum do
|
||||
end
|
||||
end
|
||||
|
||||
defp min_max_by_sort_fun(module) when is_atom(module), do: &(module.compare(&1, &2) == :lt)
|
||||
|
||||
@doc """
|
||||
Splits the `enumerable` in two lists according to the given function `fun`.
|
||||
|
||||
@@ -2097,7 +2176,7 @@ defmodule Enum do
|
||||
operation cannot be expressed by any of the functions in the `Enum`
|
||||
module, developers will most likely resort to `reduce/3`.
|
||||
"""
|
||||
@spec reduce(t, any, (element, any -> any)) :: any
|
||||
@spec reduce(t, any, (element, acc -> acc)) :: acc
|
||||
def reduce(enumerable, acc, fun) when is_list(enumerable) do
|
||||
:lists.foldl(fun, acc, enumerable)
|
||||
end
|
||||
@@ -2485,15 +2564,15 @@ defmodule Enum do
|
||||
iex> Enum.sort(dates)
|
||||
[~D[2019-01-01], ~D[2020-03-02], ~D[2019-06-06]]
|
||||
|
||||
Notice the returned result is incorrect, because `sort/1` by default uses
|
||||
Note that the returned result is incorrect, because `sort/1` by default uses
|
||||
`<=/2`, which will compare their structure. When comparing structures, the
|
||||
fields are compared in alphabetical order, which means the dates above will
|
||||
be compared by `day`, `month` and then `year`, which is the opposite of what
|
||||
we want.
|
||||
|
||||
For this reason, most structs provide a "compare" function, such as
|
||||
`Date.compare/2`, which receives two structs and returns `:lt` (less than),
|
||||
`:eq` (equal), and `:gt` (greather than). If you pass a module as the
|
||||
`Date.compare/2`, which receives two structs and returns `:lt` (less-than),
|
||||
`:eq` (equal to), and `:gt` (greater-than). If you pass a module as the
|
||||
sorting function, Elixir will automatically use the `compare/2` function
|
||||
of said module:
|
||||
|
||||
@@ -2829,7 +2908,7 @@ defmodule Enum do
|
||||
@doc """
|
||||
Takes `count` random elements from `enumerable`.
|
||||
|
||||
Notice this function will traverse the whole `enumerable` to
|
||||
Note that this function will traverse the whole `enumerable` to
|
||||
get the random sublist.
|
||||
|
||||
See `random/1` for notes on implementation and random seed.
|
||||
@@ -3071,10 +3150,18 @@ defmodule Enum do
|
||||
"""
|
||||
@spec with_index(t, integer) :: [{element, index}]
|
||||
def with_index(enumerable, offset \\ 0) do
|
||||
map_reduce(enumerable, offset, fn x, acc ->
|
||||
{{x, acc}, acc + 1}
|
||||
end)
|
||||
|> elem(0)
|
||||
enumerable
|
||||
|> to_list()
|
||||
|> do_with_index(offset)
|
||||
end
|
||||
|
||||
@spec do_with_index(list, integer) :: [{element, index}]
|
||||
defp do_with_index([], _) do
|
||||
[]
|
||||
end
|
||||
|
||||
defp do_with_index([head | tail], index) do
|
||||
[{head, index} | do_with_index(tail, index + 1)]
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
||||
+74
-47
@@ -21,15 +21,15 @@ defmodule Exception do
|
||||
|
||||
@typedoc "The kind handled by formatting functions"
|
||||
@type kind :: :error | non_error_kind
|
||||
@typep non_error_kind :: :exit | :throw | {:EXIT, pid}
|
||||
@type non_error_kind :: :exit | :throw | {:EXIT, pid}
|
||||
|
||||
@type stacktrace :: [stacktrace_entry]
|
||||
@type stacktrace_entry ::
|
||||
{module, atom, arity_or_args, location}
|
||||
| {(... -> any), arity_or_args, location}
|
||||
|
||||
@typep arity_or_args :: non_neg_integer | list
|
||||
@typep location :: keyword
|
||||
@type arity_or_args :: non_neg_integer | list
|
||||
@type location :: keyword
|
||||
|
||||
@callback exception(term) :: t
|
||||
@callback message(t) :: String.t()
|
||||
@@ -46,6 +46,8 @@ defmodule Exception do
|
||||
@doc """
|
||||
Returns `true` if the given `term` is an exception.
|
||||
"""
|
||||
# TODO: Remove this on Elixir v1.15
|
||||
@doc deprecated: "Use Kernel.is_exception/1 instead"
|
||||
def exception?(term)
|
||||
def exception?(%_{__exception__: true}), do: true
|
||||
def exception?(_), do: false
|
||||
@@ -188,7 +190,7 @@ defmodule Exception do
|
||||
Where `definition` is `:def`, `:defp`, `:defmacro` or `:defmacrop`.
|
||||
"""
|
||||
@doc since: "1.5.0"
|
||||
@spec blame_mfa(module, function, args :: [term]) ::
|
||||
@spec blame_mfa(module, function :: atom, args :: [term]) ::
|
||||
{:ok, :def | :defp | :defmacro | :defmacrop, [{args :: [term], guards :: [term]}]}
|
||||
| :error
|
||||
def blame_mfa(module, function, args)
|
||||
@@ -237,7 +239,11 @@ defmodule Exception do
|
||||
binding = :orddict.store(:VAR, call_arg, binding)
|
||||
|
||||
try do
|
||||
{:value, _, binding} = :erl_eval.expr({:match, 0, erl_arg, {:var, 0, :VAR}}, binding, :none)
|
||||
ann = :erl_anno.new(0)
|
||||
|
||||
{:value, _, binding} =
|
||||
:erl_eval.expr({:match, ann, erl_arg, {:var, ann, :VAR}}, binding, :none)
|
||||
|
||||
{true, binding}
|
||||
rescue
|
||||
_ -> {false, binding}
|
||||
@@ -261,7 +267,13 @@ defmodule Exception do
|
||||
blame_guard(right, scope, binding)
|
||||
]
|
||||
|
||||
{rewrite_guard_call(op), meta, guards}
|
||||
kernel_op =
|
||||
case op do
|
||||
:orelse -> :or
|
||||
:andalso -> :and
|
||||
end
|
||||
|
||||
{kernel_op, meta, guards}
|
||||
end
|
||||
|
||||
defp blame_guard(ex_guard, scope, binding) do
|
||||
@@ -280,32 +292,17 @@ defmodule Exception do
|
||||
|
||||
defp rewrite_guard(guard) do
|
||||
Macro.prewalk(guard, fn
|
||||
{{:., _, [:erlang, :element]}, _, [{{:., _, [:erlang, :+]}, _, [int, 1]}, arg]} ->
|
||||
{:elem, [], [arg, int]}
|
||||
|
||||
{{:., _, [:erlang, :element]}, _, [int, arg]} when is_integer(int) ->
|
||||
{:elem, [], [arg, int - 1]}
|
||||
|
||||
{:., _, [:erlang, call]} ->
|
||||
rewrite_guard_call(call)
|
||||
|
||||
other ->
|
||||
other
|
||||
{{:., _, [mod, fun]}, meta, args} -> erl_to_ex(mod, fun, args, meta)
|
||||
other -> other
|
||||
end)
|
||||
end
|
||||
|
||||
defp rewrite_guard_call(:orelse), do: :or
|
||||
defp rewrite_guard_call(:andalso), do: :and
|
||||
defp rewrite_guard_call(:"=<"), do: :<=
|
||||
defp rewrite_guard_call(:"/="), do: :!=
|
||||
defp rewrite_guard_call(:"=:="), do: :===
|
||||
defp rewrite_guard_call(:"=/="), do: :!==
|
||||
|
||||
defp rewrite_guard_call(op) when op in [:band, :bor, :bnot, :bsl, :bsr, :bxor],
|
||||
do: {:., [], [Bitwise, op]}
|
||||
|
||||
defp rewrite_guard_call(op) when op in [:xor, :element, :size], do: {:., [], [:erlang, op]}
|
||||
defp rewrite_guard_call(op), do: op
|
||||
defp erl_to_ex(mod, fun, args, meta) do
|
||||
case :elixir_rewrite.erl_to_ex(mod, fun, args) do
|
||||
{Kernel, fun, args} -> {fun, meta, args}
|
||||
{mod, fun, args} -> {{:., [], [mod, fun]}, meta, args}
|
||||
end
|
||||
end
|
||||
|
||||
defp blame_wrap(match?, ast), do: %{match?: match?, node: ast}
|
||||
|
||||
@@ -638,6 +635,7 @@ defmodule Exception do
|
||||
|
||||
@doc """
|
||||
Formats the given `file` and `line` as shown in stacktraces.
|
||||
|
||||
If any of the values are `nil`, they are omitted.
|
||||
|
||||
## Examples
|
||||
@@ -653,14 +651,42 @@ defmodule Exception do
|
||||
|
||||
"""
|
||||
def format_file_line(file, line, suffix \\ "") do
|
||||
if file do
|
||||
if line && line != 0 do
|
||||
"#{file}:#{line}:#{suffix}"
|
||||
else
|
||||
"#{file}:#{suffix}"
|
||||
end
|
||||
else
|
||||
cond do
|
||||
is_nil(file) -> ""
|
||||
is_nil(line) or line == 0 -> "#{file}:#{suffix}"
|
||||
true -> "#{file}:#{line}:#{suffix}"
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Formats the given `file`, `line`, and `column` as shown in stacktraces.
|
||||
|
||||
If any of the values are `nil`, they are omitted.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Exception.format_file_line_column("foo", 1, 2)
|
||||
"foo:1:2:"
|
||||
|
||||
iex> Exception.format_file_line_column("foo", 1, nil)
|
||||
"foo:1:"
|
||||
|
||||
iex> Exception.format_file_line_column("foo", nil, nil)
|
||||
"foo:"
|
||||
|
||||
iex> Exception.format_file_line_column("foo", nil, 2)
|
||||
"foo:"
|
||||
|
||||
iex> Exception.format_file_line_column(nil, nil, nil)
|
||||
""
|
||||
|
||||
"""
|
||||
def format_file_line_column(file, line, column, suffix \\ "") do
|
||||
cond do
|
||||
is_nil(file) -> ""
|
||||
is_nil(line) or line == 0 -> "#{file}:#{suffix}"
|
||||
is_nil(column) or column == 0 -> "#{file}:#{line}:#{suffix}"
|
||||
true -> "#{file}:#{line}:#{column}:#{suffix}"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -692,7 +718,7 @@ defmodule ArgumentError do
|
||||
not is_atom(module) and is_atom(function) and args == [] ->
|
||||
"you attempted to apply #{inspect(function)} on #{inspect(module)}. " <>
|
||||
"If you are using apply/3, make sure the module is an atom. " <>
|
||||
"If you are using the dot syntax, such as map.field or module.function, " <>
|
||||
"If you are using the dot syntax, such as map.field or module.function(), " <>
|
||||
"make sure the left side of the dot is an atom or a map"
|
||||
|
||||
not is_atom(module) ->
|
||||
@@ -766,21 +792,22 @@ defmodule SystemLimitError do
|
||||
end
|
||||
|
||||
defmodule SyntaxError do
|
||||
defexception [:file, :line, description: "syntax error"]
|
||||
defexception [:file, :line, :column, description: "syntax error"]
|
||||
|
||||
@impl true
|
||||
def message(exception) do
|
||||
Exception.format_file_line(Path.relative_to_cwd(exception.file), exception.line) <>
|
||||
" " <> exception.description
|
||||
def message(%{file: file, line: line, column: column, description: description}) do
|
||||
Exception.format_file_line_column(Path.relative_to_cwd(file), line, column) <>
|
||||
" " <> description
|
||||
end
|
||||
end
|
||||
|
||||
defmodule TokenMissingError do
|
||||
defexception [:file, :line, description: "expression is incomplete"]
|
||||
defexception [:file, :line, :column, description: "expression is incomplete"]
|
||||
|
||||
@impl true
|
||||
def message(%{file: file, line: line, description: description}) do
|
||||
Exception.format_file_line(file && Path.relative_to_cwd(file), line) <> " " <> description
|
||||
def message(%{file: file, line: line, column: column, description: description}) do
|
||||
Exception.format_file_line_column(file && Path.relative_to_cwd(file), line, column) <>
|
||||
" " <> description
|
||||
end
|
||||
end
|
||||
|
||||
@@ -950,7 +977,7 @@ defmodule UndefinedFunctionError do
|
||||
end
|
||||
|
||||
defp hint(nil, _function, 0, _loaded?) do
|
||||
". If you are using the dot syntax, such as map.field or module.function, " <>
|
||||
". If you are using the dot syntax, such as map.field or module.function(), " <>
|
||||
"make sure the left side of the dot is an atom or a map"
|
||||
end
|
||||
|
||||
@@ -1329,7 +1356,7 @@ defmodule File.CopyError do
|
||||
formatted = IO.iodata_to_binary(:file.format_error(exception.reason))
|
||||
|
||||
location =
|
||||
case exception.on() do
|
||||
case exception.on do
|
||||
"" -> ""
|
||||
on -> ". #{on}"
|
||||
end
|
||||
@@ -1347,7 +1374,7 @@ defmodule File.RenameError do
|
||||
formatted = IO.iodata_to_binary(:file.format_error(exception.reason))
|
||||
|
||||
location =
|
||||
case exception.on() do
|
||||
case exception.on do
|
||||
"" -> ""
|
||||
on -> ". #{on}"
|
||||
end
|
||||
|
||||
@@ -110,6 +110,7 @@ defmodule File do
|
||||
|
||||
@type stream_mode ::
|
||||
encoding_mode()
|
||||
| :append
|
||||
| :trim_bom
|
||||
| {:read_ahead, pos_integer | false}
|
||||
| {:delayed_write, non_neg_integer, non_neg_integer}
|
||||
@@ -735,6 +736,7 @@ defmodule File do
|
||||
File.rename("samples", "tmp")
|
||||
|
||||
"""
|
||||
@doc since: "1.1.0"
|
||||
@spec rename(Path.t(), Path.t()) :: :ok | {:error, posix}
|
||||
def rename(source, destination) do
|
||||
:file.rename(source, destination)
|
||||
@@ -1503,6 +1505,12 @@ defmodule File do
|
||||
@doc """
|
||||
Sets the current working directory.
|
||||
|
||||
The current working directory is set for the BEAM globally. This can lead to
|
||||
race conditions if multiple processes are changing the current working
|
||||
directory concurrently. To run an external command in a given directory
|
||||
without changing the global current working directory, use the `:cd` option
|
||||
of `System.cmd/3` and `Port.open/2`.
|
||||
|
||||
Returns `:ok` if successful, `{:error, reason}` otherwise.
|
||||
"""
|
||||
@spec cd(Path.t()) :: :ok | {:error, posix}
|
||||
@@ -1637,7 +1645,7 @@ defmodule File do
|
||||
|
||||
See `Stream.run/1` for an example of streaming into a file.
|
||||
"""
|
||||
@spec stream!(Path.t(), stream_mode, :line | pos_integer) :: File.Stream.t()
|
||||
@spec stream!(Path.t(), [stream_mode], :line | pos_integer) :: File.Stream.t()
|
||||
def stream!(path, modes \\ [], line_or_bytes \\ :line) do
|
||||
modes = normalize_modes(modes, true)
|
||||
File.Stream.__build__(IO.chardata_to_string(path), modes, line_or_bytes)
|
||||
|
||||
@@ -8,8 +8,21 @@ defmodule Function do
|
||||
iex> add.(1, 2)
|
||||
3
|
||||
|
||||
It is also possible to capture module functions and pass them around
|
||||
as if they were anonymous functions by using the capture operator `&/1`:
|
||||
Anonymous functions can also have multiple clauses. All clauses
|
||||
should expect the same number of arguments:
|
||||
|
||||
iex> negate = fn
|
||||
...> true -> false
|
||||
...> false -> true
|
||||
...> end
|
||||
iex> negate.(false)
|
||||
true
|
||||
|
||||
## The capture operator
|
||||
|
||||
It is also possible to capture public module functions and pass them
|
||||
around as if they were anonymous functions by using the capture
|
||||
operator `Kernel.SpecialForms.&/1`:
|
||||
|
||||
iex> add = &Kernel.+/2
|
||||
iex> add.(1, 2)
|
||||
@@ -19,8 +32,9 @@ defmodule Function do
|
||||
iex> length.("hello")
|
||||
5
|
||||
|
||||
It is also possible to capture a definition in the current module by
|
||||
skipping the module prefix, such as `&my_fun/2`.
|
||||
To capture a definition within the current module, you can skip the
|
||||
module prefix, such as `&my_fun/2`. In those cases, the captured
|
||||
function can be public (`def`) or private (`defp`).
|
||||
|
||||
The capture operator can also be used to create anonymous functions
|
||||
that expect at least one argument:
|
||||
@@ -31,6 +45,8 @@ defmodule Function do
|
||||
|
||||
In such cases, using the capture operator is no different than using `fn`.
|
||||
|
||||
## Internal and external functions
|
||||
|
||||
We say that functions that point to definitions residing in modules, such
|
||||
as `&String.length/1`, are **external** functions. All other functions are
|
||||
**local** and they are always bound to the file or module that defined them.
|
||||
|
||||
@@ -7,7 +7,7 @@ defmodule GenEvent do
|
||||
If you are interested in implementing an event manager, please read the
|
||||
"Alternatives" section below. If you have to implement an event handler to
|
||||
integrate with an existing system, such as Elixir's Logger, please use
|
||||
`:gen_event` instead.
|
||||
[`:gen_event`](https://erlang.org/doc/man/gen_event.html) instead.
|
||||
|
||||
## Alternatives
|
||||
|
||||
|
||||
@@ -280,6 +280,10 @@ defmodule GenServer do
|
||||
GenServer.call(__MODULE__, {:add, a, b})
|
||||
end
|
||||
|
||||
def subtract(a, b) do
|
||||
GenServer.call(__MODULE__, {:subtract, a, b})
|
||||
end
|
||||
|
||||
def handle_call({:add, a, b}, _from, state) do
|
||||
{:reply, a + b, state}
|
||||
end
|
||||
@@ -640,7 +644,7 @@ defmodule GenServer do
|
||||
This callback is optional.
|
||||
"""
|
||||
@callback terminate(reason, state :: term) :: term
|
||||
when reason: :normal | :shutdown | {:shutdown, term}
|
||||
when reason: :normal | :shutdown | {:shutdown, term} | term
|
||||
|
||||
@doc """
|
||||
Invoked to change the state of the `GenServer` when a different version of a
|
||||
@@ -1177,8 +1181,11 @@ defmodule GenServer do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the `pid` or `{name, node}` of a GenServer process, or `nil` if
|
||||
no process is associated with the given `server`.
|
||||
Returns the `pid` or `{name, node}` of a GenServer process, `nil` otherwise.
|
||||
|
||||
To be precise, `nil` is returned whenever a `pid` or `{name, node}` cannot
|
||||
be returned. Note there is no guarantee the returned `pid` or `{name, node}`
|
||||
is alive, as a process could terminate immediately after it is looked up.
|
||||
|
||||
## Examples
|
||||
|
||||
|
||||
@@ -48,8 +48,8 @@ defmodule HashDict do
|
||||
end
|
||||
|
||||
@deprecated message
|
||||
def update(%HashDict{root: root, size: size}, key, initial, fun) when is_function(fun, 1) do
|
||||
{root, counter} = do_update(root, key, fn -> initial end, fun, key_hash(key))
|
||||
def update(%HashDict{root: root, size: size}, key, default, fun) when is_function(fun, 1) do
|
||||
{root, counter} = do_update(root, key, fn -> default end, fun, key_hash(key))
|
||||
%HashDict{root: root, size: size + counter}
|
||||
end
|
||||
|
||||
@@ -135,25 +135,25 @@ defmodule HashDict do
|
||||
end
|
||||
end
|
||||
|
||||
defp do_update(node, key, initial, fun, hash) do
|
||||
defp do_update(node, key, default, fun, hash) do
|
||||
index = key_mask(hash)
|
||||
|
||||
case elem(node, index) do
|
||||
[] ->
|
||||
{put_elem(node, index, [key | initial.()]), 1}
|
||||
{put_elem(node, index, [key | default.()]), 1}
|
||||
|
||||
[^key | value] ->
|
||||
{put_elem(node, index, [key | fun.(value)]), 0}
|
||||
|
||||
[k | v] ->
|
||||
n = put_elem(@node_template, key_mask(key_shift(hash)), [key | initial.()])
|
||||
n = put_elem(@node_template, key_mask(key_shift(hash)), [key | default.()])
|
||||
{put_elem(node, index, {k, v, n}), 1}
|
||||
|
||||
{^key, value, n} ->
|
||||
{put_elem(node, index, {key, fun.(value), n}), 0}
|
||||
|
||||
{k, v, n} ->
|
||||
{n, counter} = do_update(n, key, initial, fun, key_shift(hash))
|
||||
{n, counter} = do_update(n, key, default, fun, key_shift(hash))
|
||||
{put_elem(node, index, {k, v, n}), counter}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -31,15 +31,17 @@ defprotocol Inspect do
|
||||
end
|
||||
end
|
||||
|
||||
The [`concat/1`](`Inspect.Algebra.concat/1`) function comes from `Inspect.Algebra` and it
|
||||
concatenates algebra documents together. In the example above it is
|
||||
concatenating the string `"MapSet<"`, the document returned by
|
||||
`Inspect.Algebra.to_doc/2`, and the final string `">"`. All strings are
|
||||
valid algebra documents that keep their formatting when pretty printed.
|
||||
The [`concat/1`](`Inspect.Algebra.concat/1`) function comes from
|
||||
`Inspect.Algebra` and it concatenates algebra documents together.
|
||||
In the example above it is concatenating the string `"#MapSet<"`,
|
||||
the document returned by `Inspect.Algebra.to_doc/2`, and the final
|
||||
string `">"`. We prefix the module name `#` to denote the inspect
|
||||
presentation is not actually valid Elixir syntax.
|
||||
|
||||
Since regular strings are valid entities in an algebra document,
|
||||
an implementation of the `Inspect` protocol may simply return a
|
||||
string, although that will devoid it of any pretty-printing.
|
||||
Finally, note strings themselves are valid algebra documents that
|
||||
keep their formatting when pretty printed. This means your `Inspect`
|
||||
implementation may simply return a string, although that will devoid
|
||||
it of any pretty-printing.
|
||||
|
||||
## Error handling
|
||||
|
||||
@@ -437,7 +439,7 @@ defimpl Inspect, for: Any do
|
||||
|
||||
def inspect(%module{} = struct, opts) do
|
||||
try do
|
||||
module.__struct__
|
||||
module.__struct__()
|
||||
rescue
|
||||
_ -> Inspect.Map.inspect(struct, opts)
|
||||
else
|
||||
|
||||
@@ -31,22 +31,24 @@ defmodule Inspect.Opts do
|
||||
options. Useful when implementing the `Inspect` protocol for nested structs
|
||||
to pass the custom options through.
|
||||
|
||||
* `:inspect_fun` (since v1.9.0) - a function to build algebra documents,
|
||||
defaults to `Inspect.inspect/2`.
|
||||
* `:inspect_fun` (since v1.9.0) - a function to build algebra documents.
|
||||
Defaults to `Inspect.inspect/2`.
|
||||
|
||||
* `:limit` - limits the number of items that are inspected for tuples,
|
||||
bitstrings, maps, lists and any other collection of items. It does not
|
||||
apply to printable strings nor printable charlists and defaults to 50.
|
||||
bitstrings, maps, lists and any other collection of items, with the exception of
|
||||
printable strings and printable charlists which use the `:printable_limit` option.
|
||||
If you don't want to limit the number of items to a particular number,
|
||||
use `:infinity`.
|
||||
use `:infinity`. It accepts a positive integer or `:infinity`.
|
||||
Defaults to `50`.
|
||||
|
||||
* `:pretty` - if set to `true` enables pretty printing, defaults to `false`.
|
||||
* `:pretty` - if set to `true` enables pretty printing. Defaults to `false`.
|
||||
|
||||
* `:printable_limit` - limits the number of characters that are inspected
|
||||
on printable strings and printable charlists. You can use `String.printable?/1`
|
||||
and `List.ascii_printable?/1` to check if a given string or charlist is
|
||||
printable. Defaults to 4096. If you don't want to limit the number of
|
||||
characters to a particular number, use `:infinity`.
|
||||
printable. If you don't want to limit the number of characters to a particular
|
||||
number, use `:infinity`. It accepts a positive integer or `:infinity`.
|
||||
Defaults to `4096`.
|
||||
|
||||
* `:safe` - when `false`, failures while inspecting structs will be raised
|
||||
as errors instead of being wrapped in the `Inspect.Error` exception. This
|
||||
@@ -54,7 +56,7 @@ defmodule Inspect.Opts do
|
||||
implementations.
|
||||
|
||||
* `:structs` - when `false`, structs are not formatted by the inspect
|
||||
protocol, they are instead printed as maps, defaults to `true`.
|
||||
protocol, they are instead printed as maps. Defaults to `true`.
|
||||
|
||||
* `:syntax_colors` - when set to a keyword list of colors the output is
|
||||
colorized. The keys are types and the values are the colors to use for
|
||||
@@ -63,10 +65,10 @@ defmodule Inspect.Opts do
|
||||
`:string`, and `:tuple`. Custom data types may provide their own options.
|
||||
Colors can be any `t:IO.ANSI.ansidata/0` as accepted by `IO.ANSI.format/1`.
|
||||
|
||||
* `:width` - defaults to 80 characters, used when pretty is `true` or when
|
||||
printing to IO devices. Set to 0 to force each item to be printed on its
|
||||
* `:width` - number of characters per line used when pretty is `true` or when
|
||||
printing to IO devices. Set to `0` to force each item to be printed on its
|
||||
own line. If you don't want to limit the number of items to a particular
|
||||
number, use `:infinity`.
|
||||
number, use `:infinity`. Defaults to `80`.
|
||||
|
||||
"""
|
||||
|
||||
@@ -95,13 +97,13 @@ defmodule Inspect.Opts do
|
||||
charlists: :infer | :as_lists | :as_charlists,
|
||||
custom_options: keyword,
|
||||
inspect_fun: (any, t -> Inspect.Algebra.t()),
|
||||
limit: pos_integer | :infinity,
|
||||
limit: non_neg_integer | :infinity,
|
||||
pretty: boolean,
|
||||
printable_limit: pos_integer | :infinity,
|
||||
printable_limit: non_neg_integer | :infinity,
|
||||
safe: boolean,
|
||||
structs: boolean,
|
||||
syntax_colors: [{color_key, IO.ANSI.ansidata()}],
|
||||
width: pos_integer | :infinity
|
||||
width: non_neg_integer | :infinity
|
||||
}
|
||||
end
|
||||
|
||||
@@ -148,7 +150,7 @@ defmodule Inspect.Algebra do
|
||||
iex> Inspect.Algebra.format(doc, 80)
|
||||
["a", " ", "b"]
|
||||
|
||||
Notice the break was represented as is, because we haven't reached
|
||||
Note that the break was represented as is, because we haven't reached
|
||||
a line limit. Once we do, it is replaced by a newline:
|
||||
|
||||
iex> doc = Inspect.Algebra.glue(String.duplicate("a", 20), " ", "b")
|
||||
@@ -266,6 +268,9 @@ defmodule Inspect.Algebra do
|
||||
when is_binary(doc) or doc in [:doc_nil, :doc_line] or
|
||||
(is_tuple(doc) and elem(doc, 0) in @docs)
|
||||
|
||||
defguardp is_limit(limit) when limit == :infinity or (is_integer(limit) and limit >= 0)
|
||||
defguardp is_width(limit) when limit == :infinity or (is_integer(limit) and limit >= 0)
|
||||
|
||||
# Elixir + Inspect.Opts conveniences
|
||||
|
||||
@doc """
|
||||
@@ -397,13 +402,14 @@ defmodule Inspect.Algebra do
|
||||
{:lists.reverse(["..." | acc]), simple?}
|
||||
end
|
||||
|
||||
defp container_each([term | terms], limit, opts, fun, acc, simple?) when is_list(terms) do
|
||||
defp container_each([term | terms], limit, opts, fun, acc, simple?)
|
||||
when is_list(terms) and is_limit(limit) do
|
||||
limit = decrement(limit)
|
||||
doc = fun.(term, %{opts | limit: limit})
|
||||
container_each(terms, limit, opts, fun, [doc | acc], simple? and simple?(doc))
|
||||
end
|
||||
|
||||
defp container_each([left | right], limit, opts, fun, acc, simple?) do
|
||||
defp container_each([left | right], limit, opts, fun, acc, simple?) when is_limit(limit) do
|
||||
limit = decrement(limit)
|
||||
left = fun.(left, %{opts | limit: limit})
|
||||
right = fun.(right, %{opts | limit: limit})
|
||||
@@ -595,7 +601,7 @@ defmodule Inspect.Algebra do
|
||||
iex> Inspect.Algebra.format(doc, 80)
|
||||
["a", "\t", "b"]
|
||||
|
||||
Notice the break was represented with the given string, because we didn't
|
||||
Note that the break was represented with the given string, because we didn't
|
||||
reach a line limit. Once we do, it is replaced by a newline:
|
||||
|
||||
iex> break = Inspect.Algebra.break("\t")
|
||||
@@ -884,7 +890,7 @@ defmodule Inspect.Algebra do
|
||||
|
||||
"""
|
||||
@spec format(t, non_neg_integer | :infinity) :: iodata
|
||||
def format(doc, width) when is_doc(doc) and (width == :infinity or width >= 0) do
|
||||
def format(doc, width) when is_doc(doc) and is_width(width) do
|
||||
format(width, 0, [{0, :flat, doc}])
|
||||
end
|
||||
|
||||
@@ -900,7 +906,12 @@ defmodule Inspect.Algebra do
|
||||
#
|
||||
@typep mode :: :flat | :flat_no_break | :break | :break_no_flat
|
||||
|
||||
@spec fits?(width :: integer(), column :: integer(), break? :: boolean(), entries) :: boolean()
|
||||
@spec fits?(
|
||||
width :: non_neg_integer(),
|
||||
column :: non_neg_integer(),
|
||||
break? :: boolean(),
|
||||
entries
|
||||
) :: boolean()
|
||||
when entries:
|
||||
maybe_improper_list({integer(), mode(), t()}, {:tail, boolean(), entries} | [])
|
||||
|
||||
@@ -961,7 +972,9 @@ defmodule Inspect.Algebra do
|
||||
defp fits?(w, k, b?, [{i, m, doc_group(x, _)} | t]),
|
||||
do: fits?(w, k, b?, [{i, m, x} | {:tail, b?, t}])
|
||||
|
||||
@spec format(integer | :infinity, integer, [{integer, mode, t}]) :: [binary]
|
||||
@spec format(width :: non_neg_integer() | :infinity, column :: non_neg_integer(), [
|
||||
{integer, mode, t}
|
||||
]) :: [binary]
|
||||
defp format(_, _, []), 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)]
|
||||
|
||||
+22
-5
@@ -317,7 +317,7 @@ defmodule IO do
|
||||
@spec warn(chardata | String.Chars.t(), Exception.stacktrace()) :: :ok
|
||||
def warn(message, []) do
|
||||
message = [to_chardata(message), ?\n]
|
||||
:elixir_errors.io_warn(nil, nil, message, message)
|
||||
:elixir_errors.io_warn(0, nil, message, message)
|
||||
end
|
||||
|
||||
def warn(message, [{_, _, _, opts} | _] = stacktrace) do
|
||||
@@ -327,18 +327,35 @@ defmodule IO do
|
||||
file = opts[:file]
|
||||
|
||||
:elixir_errors.io_warn(
|
||||
line,
|
||||
line || 0,
|
||||
file && List.to_string(file),
|
||||
message,
|
||||
[message, ?\n, " ", formatted_trace, ?\n]
|
||||
)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def warn_once(key, message, stacktrace_drop_levels) do
|
||||
{:current_stacktrace, stacktrace} = Process.info(self(), :current_stacktrace)
|
||||
stacktrace = Enum.drop(stacktrace, stacktrace_drop_levels)
|
||||
|
||||
if :elixir_config.warn(key, stacktrace) do
|
||||
warn(message, stacktrace)
|
||||
else
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Writes a `message` to stderr, along with the current stacktrace.
|
||||
|
||||
It returns `:ok` if it succeeds.
|
||||
|
||||
Do not call this function at the tail of another function. Due to tail
|
||||
call optimization, a stacktrace entry would not be added and the
|
||||
stacktrace would be incorrectly trimmed. Therefore make sure at least
|
||||
one expression (or an atom such as `:ok`) follows the `IO.warn/1` call.
|
||||
|
||||
## Examples
|
||||
|
||||
IO.warn("variable bar is unused")
|
||||
@@ -430,8 +447,8 @@ defmodule IO do
|
||||
See `IO.getn/3` for a description of return values.
|
||||
|
||||
"""
|
||||
@spec getn(chardata | String.Chars.t(), pos_integer) :: chardata | nodata
|
||||
@spec getn(device, chardata | String.Chars.t()) :: chardata | nodata
|
||||
@spec getn(device | chardata | String.Chars.t(), pos_integer | chardata | String.Chars.t()) ::
|
||||
chardata | nodata
|
||||
def getn(prompt, count \\ 1)
|
||||
|
||||
def getn(prompt, count) when is_integer(count) and count > 0 do
|
||||
@@ -582,7 +599,7 @@ defmodule IO do
|
||||
|
||||
The operation is Unicode unsafe.
|
||||
|
||||
Notice that this function treats integers in the given IO data as
|
||||
Note that this function treats integers in the given IO data as
|
||||
raw bytes and does not perform any kind of encoding conversion.
|
||||
If you want to convert from a charlist to a UTF-8-encoded string,
|
||||
use `chardata_to_string/1` instead. For more information about
|
||||
|
||||
@@ -21,6 +21,31 @@ defmodule IO.ANSI do
|
||||
[ANSI escape sequences](https://en.wikipedia.org/wiki/ANSI_escape_code)
|
||||
are characters embedded in text used to control formatting, color, and
|
||||
other output options on video text terminals.
|
||||
|
||||
ANSI escapes are typically enabled on all Unix terminals. They are also
|
||||
available on Windows consoles from Windows 10, although it must be
|
||||
explicitly enabled for the current user in the registry by running the
|
||||
following command:
|
||||
|
||||
reg add HKCU\\Console /v VirtualTerminalLevel /t REG_DWORD /d 1
|
||||
|
||||
After running the command above, you must restart your current console.
|
||||
|
||||
## Examples
|
||||
|
||||
Because the ANSI escape sequences are embedded in text, the normal usage of
|
||||
these functions is to concatenate their output with text.
|
||||
|
||||
formatted_text = IO.ANSI.blue_background() <> "Example" <> IO.ANSI.reset()
|
||||
IO.puts(formatted_text)
|
||||
|
||||
A higher level and more convenient API is also available via `IO.ANSI.format/1`,
|
||||
where you use atoms to represent each ANSI escape sequence and by default
|
||||
checks if ANSI is enabled:
|
||||
|
||||
IO.puts(IO.ANSI.format([:blue_background, "Example"]))
|
||||
|
||||
In case ANSI is disabled, the ANSI escape sequences are simply discarded.
|
||||
"""
|
||||
|
||||
import IO.ANSI.Sequence
|
||||
|
||||
+258
-48
@@ -1,6 +1,7 @@
|
||||
defmodule IO.ANSI.Docs do
|
||||
@moduledoc false
|
||||
|
||||
@bullet_text "• "
|
||||
@bullets [?*, ?-, ?+]
|
||||
@spaces [" ", "\n", "\t"]
|
||||
|
||||
@@ -46,15 +47,20 @@ defmodule IO.ANSI.Docs do
|
||||
|
||||
See `default_options/0` for docs on the supported options.
|
||||
"""
|
||||
@spec print_heading(String.t(), keyword) :: :ok
|
||||
def print_heading(heading, options \\ []) do
|
||||
IO.puts(IO.ANSI.reset())
|
||||
@spec print_headings([String.t()], keyword) :: :ok
|
||||
def print_headings(headings, options \\ []) do
|
||||
options = Keyword.merge(default_options(), options)
|
||||
newline_after_block(options)
|
||||
width = options[:width]
|
||||
padding = div(width + String.length(heading), 2)
|
||||
heading = heading |> String.pad_leading(padding) |> String.pad_trailing(width)
|
||||
write(:doc_title, heading, options)
|
||||
newline_after_block()
|
||||
|
||||
for heading <- headings do
|
||||
padding = div(width + String.length(heading), 2)
|
||||
heading = String.pad_leading(heading, padding)
|
||||
heading = if options[:enabled], do: String.pad_trailing(heading, width), else: heading
|
||||
write(:doc_title, heading, options)
|
||||
end
|
||||
|
||||
newline_after_block(options)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -78,11 +84,11 @@ defmodule IO.ANSI.Docs do
|
||||
write_with_wrap([label | String.split(value, @spaces)], options[:width], indent, true, "")
|
||||
|
||||
{key, value}, _printed when is_boolean(value) and key in @metadata_filter ->
|
||||
IO.puts([metadata_label(key, options), ' ', to_string(value)])
|
||||
IO.puts([metadata_label(key, options), ?\s, to_string(value)])
|
||||
|
||||
{:delegate_to, {m, f, a}}, _printed ->
|
||||
label = metadata_label(:delegate_to, options)
|
||||
IO.puts([label, ' ', Exception.format_mfa(m, f, a)])
|
||||
IO.puts([label, ?\s, Exception.format_mfa(m, f, a)])
|
||||
|
||||
_metadata, printed ->
|
||||
printed
|
||||
@@ -90,21 +96,204 @@ defmodule IO.ANSI.Docs do
|
||||
end
|
||||
|
||||
defp metadata_label(key, options) do
|
||||
if options[:enabled] do
|
||||
"#{color(:doc_metadata, options)}#{key}:#{IO.ANSI.reset()}"
|
||||
else
|
||||
"#{key}:"
|
||||
end
|
||||
"#{color(:doc_metadata, options)}#{key}:#{maybe_reset(options)}"
|
||||
end
|
||||
|
||||
@doc """
|
||||
Prints the documentation body.
|
||||
Prints the documentation body `doc` according to `format`.
|
||||
|
||||
In addition to the printing string, takes a set of `options`
|
||||
defined in `default_options/0`.
|
||||
It takes a set of `options` defined in `default_options/0`.
|
||||
"""
|
||||
@spec print(String.t(), keyword) :: :ok
|
||||
def print(doc, options \\ []) do
|
||||
@spec print(term(), String.t(), keyword) :: :ok
|
||||
def print(doc, format, options \\ [])
|
||||
|
||||
def print(doc, "text/markdown", options) when is_binary(doc) and is_list(options) do
|
||||
print_markdown(doc, options)
|
||||
end
|
||||
|
||||
def print(doc, "application/erlang+html", options) when is_list(options) do
|
||||
print_erlang_html(doc, options)
|
||||
end
|
||||
|
||||
def print(_doc, format, options) when is_binary(format) and is_list(options) do
|
||||
IO.puts("\nUnknown documentation format #{inspect(format)}\n")
|
||||
end
|
||||
|
||||
## Erlang+html
|
||||
|
||||
def print_erlang_html(doc, options) do
|
||||
options = Keyword.merge(default_options(), options)
|
||||
IO.write(traverse_erlang_html(doc, "", options))
|
||||
end
|
||||
|
||||
defp traverse_erlang_html(text, _indent, _options) when is_binary(text) do
|
||||
text
|
||||
end
|
||||
|
||||
defp traverse_erlang_html(nodes, indent, options) when is_list(nodes) do
|
||||
for node <- nodes do
|
||||
traverse_erlang_html(node, indent, options)
|
||||
end
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:div, [class: class] ++ _, entries}, indent, options) do
|
||||
prefix = indent <> quote_prefix(options)
|
||||
|
||||
content =
|
||||
entries
|
||||
|> traverse_erlang_html(indent, options)
|
||||
|> IO.iodata_to_binary()
|
||||
|> String.trim_trailing()
|
||||
|
||||
[
|
||||
prefix,
|
||||
class |> to_string() |> String.upcase(),
|
||||
"\n#{prefix}\n#{prefix}" | String.replace(content, "\n", "\n#{prefix}")
|
||||
]
|
||||
|> newline_cons()
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:p, _, entries}, indent, options) do
|
||||
[indent | handle_erlang_html_text(entries, indent, options)]
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:h1, _, entries}, indent, options) do
|
||||
entries |> traverse_erlang_html(indent, options) |> heading(1, options) |> newline_cons()
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:h2, _, entries}, indent, options) do
|
||||
entries |> traverse_erlang_html(indent, options) |> heading(2, options) |> newline_cons()
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:h3, _, entries}, indent, options) do
|
||||
entries |> traverse_erlang_html(indent, options) |> heading(3, options) |> newline_cons()
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:h4, _, entries}, indent, options) do
|
||||
entries |> traverse_erlang_html(indent, options) |> heading(4, options) |> newline_cons()
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:h5, _, entries}, indent, options) do
|
||||
entries |> traverse_erlang_html(indent, options) |> heading(5, options) |> newline_cons()
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:h6, _, entries}, indent, options) do
|
||||
entries |> traverse_erlang_html(indent, options) |> heading(6, options) |> newline_cons()
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:br, _, []}, _indent, _options) do
|
||||
[]
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:i, _, entries}, indent, options) do
|
||||
inline_text("_", traverse_erlang_html(entries, indent, options), options)
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:em, _, entries}, indent, options) do
|
||||
inline_text("*", traverse_erlang_html(entries, indent, options), options)
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:code, _, entries}, indent, options) do
|
||||
inline_text("`", traverse_erlang_html(entries, indent, options), options)
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:pre, _, [{:code, _, entries}]}, indent, options) do
|
||||
string =
|
||||
entries
|
||||
|> traverse_erlang_html(indent, options)
|
||||
|> IO.iodata_to_binary()
|
||||
|
||||
["#{indent} ", String.replace(string, "\n", "\n#{indent} ")] |> newline_cons()
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:a, attributes, entries}, indent, options) do
|
||||
if href = attributes[:href] do
|
||||
[traverse_erlang_html(entries, indent, options), ?\s, ?(, href, ?)]
|
||||
else
|
||||
traverse_erlang_html(entries, indent, options)
|
||||
end
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:dl, _, entries}, indent, options) do
|
||||
traverse_erlang_html(entries, indent, options)
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:dt, _, entries}, indent, options) do
|
||||
["#{indent} ", @bullet_text | handle_erlang_html_text(entries, indent <> " ", options)]
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:dd, _, entries}, indent, options) do
|
||||
["#{indent} " | handle_erlang_html_text(entries, indent <> " ", options)]
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:ul, attributes, entries}, indent, options) do
|
||||
if attributes[:class] == "types" do
|
||||
types =
|
||||
for {:li, _, lines} <- entries,
|
||||
line <- lines,
|
||||
do: ["#{indent} ", traverse_erlang_html(line, indent <> " ", options), ?\n]
|
||||
|
||||
if types != [] do
|
||||
["#{indent}Typespecs:\n\n", types, ?\n]
|
||||
else
|
||||
[]
|
||||
end
|
||||
else
|
||||
for {:li, _, lines} <- entries do
|
||||
["#{indent} ", @bullet_text | handle_erlang_html_text(lines, indent <> " ", options)]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({:ol, _, entries}, indent, options) do
|
||||
for {{:li, _, lines}, i} <- Enum.with_index(entries, 1) do
|
||||
[
|
||||
"#{indent} ",
|
||||
Integer.to_string(i),
|
||||
". " | handle_erlang_html_text(lines, indent <> " ", options)
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
defp traverse_erlang_html({tag, _, entries}, indent, options) do
|
||||
[
|
||||
indent <> "<#{tag}>\n",
|
||||
traverse_erlang_html(entries, indent <> " ", options)
|
||||
|> IO.iodata_to_binary()
|
||||
|> String.trim_trailing(),
|
||||
"\n" <> indent <> "</#{tag}>"
|
||||
]
|
||||
|> newline_cons()
|
||||
end
|
||||
|
||||
defp newline_cons(text) do
|
||||
[text | "\n\n"]
|
||||
end
|
||||
|
||||
defp handle_erlang_html_text(entries, indent, options) do
|
||||
if Enum.all?(entries, &inline_html?/1) do
|
||||
entries
|
||||
|> traverse_erlang_html(indent, options)
|
||||
|> IO.iodata_to_binary()
|
||||
|> String.split(@spaces)
|
||||
|> wrap_text(options[:width], indent, true, "", [])
|
||||
|> tl()
|
||||
|> newline_cons()
|
||||
else
|
||||
entries
|
||||
|> traverse_erlang_html(indent, options)
|
||||
|> IO.iodata_to_binary()
|
||||
|> String.trim_leading()
|
||||
end
|
||||
end
|
||||
|
||||
defp inline_html?(binary) when is_binary(binary), do: true
|
||||
defp inline_html?({tag, _, _}) when tag in [:a, :code, :em, :i, :br], do: true
|
||||
defp inline_html?(_), do: false
|
||||
|
||||
## Markdown
|
||||
|
||||
def print_markdown(doc, options) do
|
||||
options = Keyword.merge(default_options(), options)
|
||||
|
||||
doc
|
||||
@@ -181,16 +370,16 @@ defmodule IO.ANSI.Docs do
|
||||
end
|
||||
end
|
||||
|
||||
## Headings
|
||||
### Headings
|
||||
|
||||
defp write_heading(heading, rest, text, indent, options) do
|
||||
write_text(text, indent, options)
|
||||
write(:doc_headings, heading, options)
|
||||
newline_after_block()
|
||||
newline_after_block(options)
|
||||
process(rest, [], "", options)
|
||||
end
|
||||
|
||||
## Quotes
|
||||
### Quotes
|
||||
|
||||
defp process_quote([], lines, indent, options) do
|
||||
write_quote(lines, indent, options, false)
|
||||
@@ -223,21 +412,19 @@ defmodule IO.ANSI.Docs do
|
||||
)
|
||||
end
|
||||
|
||||
defp quote_prefix(options), do: "#{color(:doc_quote, options)}> #{IO.ANSI.reset()}"
|
||||
|
||||
defp write_empty_quote_line(options) do
|
||||
options
|
||||
|> quote_prefix()
|
||||
|> IO.puts()
|
||||
end
|
||||
|
||||
## Lists
|
||||
### Lists
|
||||
|
||||
defp process_rest(stripped, rest, count, text, indent, options) do
|
||||
case stripped do
|
||||
<<bullet, ?\s, item::binary>> when bullet in @bullets ->
|
||||
write_text(text, indent, options)
|
||||
process_list("• ", item, rest, count, indent, options)
|
||||
process_list(@bullet_text, item, rest, count, indent, options)
|
||||
|
||||
<<d1, ?., ?\s, item::binary>> when d1 in ?0..?9 ->
|
||||
write_text(text, indent, options)
|
||||
@@ -257,10 +444,12 @@ defmodule IO.ANSI.Docs do
|
||||
entry = if indent == "", do: " " <> entry, else: entry
|
||||
new_indent = indent <> String.duplicate(" ", String.length(entry))
|
||||
|
||||
{contents, rest, done} = process_list_next(rest, count, byte_size(new_indent), [])
|
||||
{contents, rest, done} =
|
||||
process_list_next(rest, count, byte_size(new_indent) - byte_size(indent), [])
|
||||
|
||||
process(contents, [indent <> entry <> line, :no_wrap], new_indent, options)
|
||||
|
||||
if done, do: newline_after_block()
|
||||
if done, do: newline_after_block(options)
|
||||
process(rest, [], indent, options)
|
||||
end
|
||||
|
||||
@@ -301,7 +490,7 @@ defmodule IO.ANSI.Docs do
|
||||
end
|
||||
end
|
||||
|
||||
## Text
|
||||
### Text
|
||||
|
||||
defp write_text(text, indent, options) do
|
||||
case Enum.reverse(text) do
|
||||
@@ -325,7 +514,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()
|
||||
unless no_wrap, do: newline_after_block(options)
|
||||
end
|
||||
|
||||
defp format_text(text, options) do
|
||||
@@ -334,7 +523,7 @@ defmodule IO.ANSI.Docs do
|
||||
|> handle_inline(options)
|
||||
end
|
||||
|
||||
## Code blocks
|
||||
### Code blocks
|
||||
|
||||
defp process_code([], code, indent, options) do
|
||||
write_code(code, indent, options)
|
||||
@@ -373,15 +562,15 @@ defmodule IO.ANSI.Docs do
|
||||
|
||||
defp write_code(code, indent, options) do
|
||||
write(:doc_code, "#{indent} #{Enum.join(Enum.reverse(code), "\n#{indent} ")}", options)
|
||||
newline_after_block()
|
||||
newline_after_block(options)
|
||||
end
|
||||
|
||||
## Tables
|
||||
### Tables
|
||||
|
||||
defp process_table(lines, indent, options) do
|
||||
{table, rest} = Enum.split_while(lines, &table_line?/1)
|
||||
table_lines(table, options)
|
||||
newline_after_block()
|
||||
newline_after_block(options)
|
||||
process(rest, [], indent, options)
|
||||
end
|
||||
|
||||
@@ -407,7 +596,7 @@ defmodule IO.ANSI.Docs do
|
||||
line
|
||||
|> String.trim(" ")
|
||||
|> String.trim("|")
|
||||
|> String.split("|")
|
||||
|> String.split(~r{(?<!\\)\|})
|
||||
|> Enum.map(&render_column(&1, options))
|
||||
end
|
||||
|
||||
@@ -524,7 +713,7 @@ defmodule IO.ANSI.Docs do
|
||||
defp strip_spaces(rest, acc, _max), do: {rest, acc}
|
||||
|
||||
defp write(style, string, options) do
|
||||
IO.puts([color(style, options), string, IO.ANSI.reset()])
|
||||
IO.puts([color(style, options), string, maybe_reset(options)])
|
||||
end
|
||||
|
||||
defp write_with_wrap([], _available, _indent, _first, _prefix) do
|
||||
@@ -534,7 +723,7 @@ defmodule IO.ANSI.Docs do
|
||||
defp write_with_wrap(words, available, indent, first, prefix) do
|
||||
words
|
||||
|> wrap_text(available, indent, first, prefix, [])
|
||||
|> Enum.join("\n")
|
||||
|> tl()
|
||||
|> IO.puts()
|
||||
end
|
||||
|
||||
@@ -547,7 +736,7 @@ defmodule IO.ANSI.Docs do
|
||||
{words, rest} = take_words(words, available - prefix_length, [])
|
||||
line = [if(first, do: "", else: indent), prefix, Enum.join(words, " ")]
|
||||
|
||||
wrap_text(rest, available, indent, false, prefix, [line | wrapped_lines])
|
||||
wrap_text(rest, available, indent, false, prefix, [line, ?\n | wrapped_lines])
|
||||
end
|
||||
|
||||
defp take_words([word | words], available, acc) do
|
||||
@@ -621,7 +810,7 @@ defmodule IO.ANSI.Docs do
|
||||
@delimiters [?\s, ?', ?", ?!, ?@, ?#, ?$, ?%, ?^, ?&] ++
|
||||
[?-, ?+, ?(, ?), ?[, ?], ?{, ?}, ?<, ?>, ?.]
|
||||
|
||||
# Inline start
|
||||
### Inline start
|
||||
|
||||
defp handle_inline(<<?*, ?*, rest::binary>>, options) do
|
||||
handle_inline(rest, ?d, ["**"], [], options)
|
||||
@@ -635,7 +824,7 @@ defmodule IO.ANSI.Docs do
|
||||
handle_inline(rest, nil, [], [], options)
|
||||
end
|
||||
|
||||
# Inline delimiters
|
||||
### Inline delimiters
|
||||
|
||||
defp handle_inline(<<delimiter, ?*, ?*, rest::binary>>, nil, buffer, acc, options)
|
||||
when rest != "" and delimiter in @delimiters do
|
||||
@@ -652,7 +841,7 @@ defmodule IO.ANSI.Docs do
|
||||
handle_inline(rest, ?`, ["`"], [Enum.reverse(buffer) | acc], options)
|
||||
end
|
||||
|
||||
# Clauses for handling escape
|
||||
### Clauses for handling escape
|
||||
|
||||
defp handle_inline(<<?\\, ?\\, ?*, ?*, rest::binary>>, nil, buffer, acc, options)
|
||||
when rest != "" do
|
||||
@@ -673,7 +862,7 @@ defmodule IO.ANSI.Docs do
|
||||
handle_inline(rest, limit, [mark | buffer], acc, options)
|
||||
end
|
||||
|
||||
# Inline end
|
||||
### Inline end
|
||||
|
||||
defp handle_inline(<<?*, ?*, delimiter, rest::binary>>, ?d, buffer, acc, options)
|
||||
when delimiter in @delimiters do
|
||||
@@ -701,7 +890,7 @@ defmodule IO.ANSI.Docs do
|
||||
handle_inline(rest, nil, [], [inline_buffer(buffer, options) | acc], options)
|
||||
end
|
||||
|
||||
# Catch all
|
||||
### Catch all
|
||||
|
||||
defp handle_inline(<<char, rest::binary>>, mark, buffer, acc, options) do
|
||||
handle_inline(rest, mark, [char | buffer], acc, options)
|
||||
@@ -712,8 +901,24 @@ defmodule IO.ANSI.Docs do
|
||||
end
|
||||
|
||||
defp inline_buffer(buffer, options) do
|
||||
[h | t] = Enum.reverse([IO.ANSI.reset() | buffer])
|
||||
[color_for(h, options) | t]
|
||||
[mark | t] = Enum.reverse(buffer)
|
||||
inline_text(mark, t, options)
|
||||
end
|
||||
|
||||
## Helpers
|
||||
|
||||
defp quote_prefix(options), do: "#{color(:doc_quote, options)}> #{maybe_reset(options)}"
|
||||
|
||||
defp heading(text, n, options) do
|
||||
[color(:doc_headings, options), String.duplicate("#", n), " ", text, maybe_reset(options)]
|
||||
end
|
||||
|
||||
defp inline_text(mark, text, options) do
|
||||
if options[:enabled] do
|
||||
[[color_for(mark, options) | text] | IO.ANSI.reset()]
|
||||
else
|
||||
[[mark | text] | mark]
|
||||
end
|
||||
end
|
||||
|
||||
defp color_for(mark, colors) do
|
||||
@@ -726,9 +931,14 @@ defmodule IO.ANSI.Docs do
|
||||
end
|
||||
|
||||
defp color(style, colors) do
|
||||
color = colors[style]
|
||||
IO.ANSI.format_fragment(color, colors[:enabled])
|
||||
IO.ANSI.format_fragment(colors[style], colors[:enabled])
|
||||
end
|
||||
|
||||
defp newline_after_block, do: IO.puts(IO.ANSI.reset())
|
||||
defp newline_after_block(options) do
|
||||
IO.puts(maybe_reset(options))
|
||||
end
|
||||
|
||||
defp maybe_reset(options) do
|
||||
if options[:enabled], do: IO.ANSI.reset(), else: ""
|
||||
end
|
||||
end
|
||||
|
||||
+299
-99
@@ -93,7 +93,7 @@ defmodule Kernel do
|
||||
### Protocols
|
||||
|
||||
Protocols add polymorphic dispatch to Elixir. They are contracts
|
||||
implementable by data types. See `defprotocol/2` for more information on
|
||||
implementable by data types. See `Protocol` for more information on
|
||||
protocols. Elixir provides the following protocols in the standard library:
|
||||
|
||||
* `Collectable` - collects data into a data type
|
||||
@@ -127,19 +127,19 @@ defmodule Kernel do
|
||||
Elixir documentation also includes supporting documents under the
|
||||
"Pages" section. Those are:
|
||||
|
||||
* [Compatibility and Deprecations](compatibility-and-deprecations.html) - lists
|
||||
* [Compatibility and Deprecations](compatibility-and-deprecations.md) - lists
|
||||
compatibility between every Elixir version and Erlang/OTP, release schema;
|
||||
lists all deprecated functions, when they were deprecated and alternatives
|
||||
* [Library Guidelines](library-guidelines.html) - general guidelines, anti-patterns,
|
||||
* [Library Guidelines](library-guidelines.md) - general guidelines, anti-patterns,
|
||||
and rules for those writing libraries
|
||||
* [Naming Conventions](naming-conventions.html) - naming conventions for Elixir code
|
||||
* [Operators](operators.html) - lists all Elixir operators and their precedence
|
||||
* [Patterns and Guards](patterns-and-guards.html) - an introduction to patterns,
|
||||
* [Naming Conventions](naming-conventions.md) - naming conventions for Elixir code
|
||||
* [Operators](operators.md) - lists all Elixir operators and their precedences
|
||||
* [Patterns and Guards](patterns-and-guards.md) - an introduction to patterns,
|
||||
guards, and extensions
|
||||
* [Syntax Reference](syntax-reference.html) - the language syntax reference
|
||||
* [Typespecs](typespecs.html)- types and function specifications, including list of types
|
||||
* [Unicode Syntax](unicode-syntax.html) - outlines Elixir support for Unicode
|
||||
* [Writing Documentation](writing-documentation.html) - guidelines for writing
|
||||
* [Syntax Reference](syntax-reference.md) - the language syntax reference
|
||||
* [Typespecs](typespecs.md)- types and function specifications, including list of types
|
||||
* [Unicode Syntax](unicode-syntax.md) - outlines Elixir support for Unicode
|
||||
* [Writing Documentation](writing-documentation.md) - guidelines for writing
|
||||
documentation in Elixir
|
||||
|
||||
## Guards
|
||||
@@ -156,7 +156,7 @@ defmodule Kernel do
|
||||
or equal to 16. Guards also support joining multiple conditions with
|
||||
`and` and `or`. The whole guard is true if all guard expressions will
|
||||
evaluate to `true`. A more complete introduction to guards is available
|
||||
[in the "Patterns and Guards" page](patterns-and-guards.html).
|
||||
[in the "Patterns and Guards" page](patterns-and-guards.md).
|
||||
|
||||
## Inlining
|
||||
|
||||
@@ -1193,7 +1193,7 @@ defmodule Kernel do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Arithmetic addition.
|
||||
Arithmetic addition operator.
|
||||
|
||||
Allowed in guard tests. Inlined by the compiler.
|
||||
|
||||
@@ -1213,7 +1213,7 @@ defmodule Kernel do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Arithmetic subtraction.
|
||||
Arithmetic subtraction operator.
|
||||
|
||||
Allowed in guard tests. Inlined by the compiler.
|
||||
|
||||
@@ -1233,7 +1233,7 @@ defmodule Kernel do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Arithmetic unary plus.
|
||||
Arithmetic positive unary operator.
|
||||
|
||||
Allowed in guard tests. Inlined by the compiler.
|
||||
|
||||
@@ -1251,7 +1251,7 @@ defmodule Kernel do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Arithmetic unary minus.
|
||||
Arithmetic negative unary operator.
|
||||
|
||||
Allowed in guard tests. Inlined by the compiler.
|
||||
|
||||
@@ -1271,7 +1271,7 @@ defmodule Kernel do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Arithmetic multiplication.
|
||||
Arithmetic multiplication operator.
|
||||
|
||||
Allowed in guard tests. Inlined by the compiler.
|
||||
|
||||
@@ -1291,7 +1291,7 @@ defmodule Kernel do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Arithmetic division.
|
||||
Arithmetic division operator.
|
||||
|
||||
The result is always a float. Use `div/2` and `rem/2` if you want
|
||||
an integer division or the remainder.
|
||||
@@ -1322,7 +1322,7 @@ defmodule Kernel do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Concatenates a proper list and a term, returning a list.
|
||||
List concatenation operator. Concatenates a proper list and a term, returning a list.
|
||||
|
||||
The complexity of `a ++ b` is proportional to `length(a)`, so avoid repeatedly
|
||||
appending to lists of arbitrary length, for example, `list ++ [element]`.
|
||||
@@ -1360,13 +1360,19 @@ defmodule Kernel do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Removes the first occurrence of an element on the left list
|
||||
List subtraction operator. Removes the first occurrence of an element on the left list
|
||||
for each element on the right.
|
||||
|
||||
The complexity of `a -- b` is proportional to `length(a) * length(b)`,
|
||||
meaning that it will be very slow if both `a` and `b` are long lists.
|
||||
In such cases, consider converting each list to a `MapSet` and using
|
||||
`MapSet.difference/2`.
|
||||
Before Erlang/OTP 22, the complexity of `a -- b` was proportional to
|
||||
`length(a) * length(b)`, meaning that it would be very slow if
|
||||
both `a` and `b` were long lists. In such cases, consider
|
||||
converting each list to a `MapSet` and using `MapSet.difference/2`.
|
||||
|
||||
As of Erlang/OTP 22, this operation is significantly faster even if both
|
||||
lists are very long, and using `--/2` is usually faster and uses less
|
||||
memory than using the `MapSet`-based alternative mentioned above.
|
||||
See also the [Erlang efficiency
|
||||
guide](https://erlang.org/doc/efficiency_guide/retired_myths.html).
|
||||
|
||||
Inlined by the compiler.
|
||||
|
||||
@@ -1378,6 +1384,16 @@ defmodule Kernel do
|
||||
iex> [1, 2, 3, 2, 1] -- [1, 2, 2]
|
||||
[3, 1]
|
||||
|
||||
The `--/2` operator is right associative, meaning:
|
||||
|
||||
iex> [1, 2, 3] -- [2] -- [3]
|
||||
[1, 3]
|
||||
|
||||
As it is equivalent to:
|
||||
|
||||
iex> [1, 2, 3] -- ([2] -- [3])
|
||||
[1, 3]
|
||||
|
||||
"""
|
||||
@spec list -- list :: list
|
||||
def left -- right do
|
||||
@@ -1385,9 +1401,9 @@ defmodule Kernel do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Boolean not.
|
||||
Strictly boolean "not" operator.
|
||||
|
||||
`arg` must be a boolean; if it's not, an `ArgumentError` exception is raised.
|
||||
`value` must be a boolean; if it's not, an `ArgumentError` exception is raised.
|
||||
|
||||
Allowed in guard tests. Inlined by the compiler.
|
||||
|
||||
@@ -1405,7 +1421,9 @@ defmodule Kernel do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns `true` if left is less than right.
|
||||
Less-than operator.
|
||||
|
||||
Returns `true` if `left` is less than `right`.
|
||||
|
||||
All terms in Elixir can be compared with each other.
|
||||
|
||||
@@ -1424,7 +1442,9 @@ defmodule Kernel do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns `true` if left is more than right.
|
||||
Greater-than operator.
|
||||
|
||||
Returns `true` if `left` is more than `right`.
|
||||
|
||||
All terms in Elixir can be compared with each other.
|
||||
|
||||
@@ -1443,7 +1463,9 @@ defmodule Kernel do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns `true` if left is less than or equal to right.
|
||||
Less-than or equal to operator.
|
||||
|
||||
Returns `true` if `left` is less than or equal to `right`.
|
||||
|
||||
All terms in Elixir can be compared with each other.
|
||||
|
||||
@@ -1462,7 +1484,9 @@ defmodule Kernel do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns `true` if left is more than or equal to right.
|
||||
Greater-than or equal to operator.
|
||||
|
||||
Returns `true` if `left` is more than or equal to `right`.
|
||||
|
||||
All terms in Elixir can be compared with each other.
|
||||
|
||||
@@ -1481,7 +1505,7 @@ defmodule Kernel do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns `true` if the two terms are equal.
|
||||
Equal to operator. Returns `true` if the two terms are equal.
|
||||
|
||||
This operator considers 1 and 1.0 to be equal. For stricter
|
||||
semantics, use `===/2` instead.
|
||||
@@ -1506,6 +1530,8 @@ defmodule Kernel do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Not equal to operator.
|
||||
|
||||
Returns `true` if the two terms are not equal.
|
||||
|
||||
This operator considers 1 and 1.0 to be equal. For match
|
||||
@@ -1531,6 +1557,8 @@ defmodule Kernel do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Strictly equal to operator.
|
||||
|
||||
Returns `true` if the two terms are exactly equal.
|
||||
|
||||
The terms are only considered to be exactly equal if they
|
||||
@@ -1558,7 +1586,10 @@ defmodule Kernel do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Strictly not equal to operator.
|
||||
|
||||
Returns `true` if the two terms are not exactly equal.
|
||||
See `===/2` for a definition of what is considered "exactly equal".
|
||||
|
||||
All terms in Elixir can be compared with each other.
|
||||
|
||||
@@ -1629,7 +1660,7 @@ defmodule Kernel do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Boolean or.
|
||||
Strictly boolean "or" operator.
|
||||
|
||||
If `left` is `true`, returns `true`; otherwise returns `right`.
|
||||
|
||||
@@ -1643,9 +1674,13 @@ defmodule Kernel do
|
||||
|
||||
iex> true or false
|
||||
true
|
||||
|
||||
iex> false or 42
|
||||
42
|
||||
|
||||
iex> 42 or false
|
||||
** (BadBooleanError) expected a boolean on left-side of "or", got: 42
|
||||
|
||||
"""
|
||||
@doc guard: true
|
||||
defmacro left or right do
|
||||
@@ -1657,7 +1692,7 @@ defmodule Kernel do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Boolean and.
|
||||
Strictly boolean "and" operator.
|
||||
|
||||
If `left` is `false`, returns `false`; otherwise returns `right`.
|
||||
|
||||
@@ -1670,9 +1705,13 @@ defmodule Kernel do
|
||||
|
||||
iex> true and false
|
||||
false
|
||||
|
||||
iex> true and "yay!"
|
||||
"yay!"
|
||||
|
||||
iex> "yay!" and true
|
||||
** (BadBooleanError) expected a boolean on left-side of "and", got: "yay!"
|
||||
|
||||
"""
|
||||
@doc guard: true
|
||||
defmacro left and right do
|
||||
@@ -1696,9 +1735,9 @@ defmodule Kernel do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Boolean not.
|
||||
Boolean "not" operator.
|
||||
|
||||
Receives any argument (not just booleans) and returns `true` if the argument
|
||||
Receives any value (not just booleans) and returns `true` if `value`
|
||||
is `false` or `nil`; returns `false` otherwise.
|
||||
|
||||
Not allowed in guard clauses.
|
||||
@@ -1741,7 +1780,7 @@ defmodule Kernel do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Concatenates two binaries.
|
||||
Binary concatenation operator. Concatenates two binaries.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -1911,7 +1950,7 @@ defmodule Kernel do
|
||||
|
||||
Works like `raise/1` but does not generate a new stacktrace.
|
||||
|
||||
Notice that `__STACKTRACE__` can be used inside catch/rescue
|
||||
Note that `__STACKTRACE__` can be used inside catch/rescue
|
||||
to retrieve the current stacktrace.
|
||||
|
||||
## Examples
|
||||
@@ -1983,11 +2022,12 @@ defmodule Kernel do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Matches the term on the `left` against the regular expression or string on the
|
||||
`right`.
|
||||
Text-based match operator. Matches the term on the `left`
|
||||
against the regular expression or string on the `right`.
|
||||
|
||||
Returns `true` if `left` matches `right` (if it's a regular expression)
|
||||
or contains `right` (if it's a string).
|
||||
If `right` is a regular expression, returns `true` if `left` matches right.
|
||||
|
||||
If `right` is a string, returns `true` if `left` contains `right`.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -1997,12 +2037,18 @@ defmodule Kernel do
|
||||
iex> "abcd" =~ ~r/e/
|
||||
false
|
||||
|
||||
iex> "abcd" =~ ~r//
|
||||
true
|
||||
|
||||
iex> "abcd" =~ "bc"
|
||||
true
|
||||
|
||||
iex> "abcd" =~ "ad"
|
||||
false
|
||||
|
||||
iex> "abcd" =~ "abcd"
|
||||
true
|
||||
|
||||
iex> "abcd" =~ ""
|
||||
true
|
||||
|
||||
@@ -2250,6 +2296,134 @@ defmodule Kernel do
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns true if `term` is a struct of `name`; otherwise returns `false`.
|
||||
|
||||
Allowed in guard tests.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> is_struct(URI.parse("/"), URI)
|
||||
true
|
||||
|
||||
iex> is_struct(URI.parse("/"), Macro.Env)
|
||||
false
|
||||
|
||||
"""
|
||||
@doc since: "1.11.0", guard: true
|
||||
defmacro is_struct(term, name) do
|
||||
case __CALLER__.context do
|
||||
nil ->
|
||||
quote do
|
||||
case unquote(name) do
|
||||
name when is_atom(name) ->
|
||||
case unquote(term) do
|
||||
%{__struct__: ^name} -> true
|
||||
_ -> false
|
||||
end
|
||||
|
||||
_ ->
|
||||
raise ArgumentError
|
||||
end
|
||||
end
|
||||
|
||||
:match ->
|
||||
invalid_match!(:is_struct)
|
||||
|
||||
:guard ->
|
||||
quote do
|
||||
is_map(unquote(term)) and
|
||||
(is_atom(unquote(name)) or :fail) and
|
||||
:erlang.is_map_key(:__struct__, unquote(term)) and
|
||||
:erlang.map_get(:__struct__, unquote(term)) == unquote(name)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns true if `term` is an exception; otherwise returns `false`.
|
||||
|
||||
Allowed in guard tests.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> is_exception(%RuntimeError{})
|
||||
true
|
||||
|
||||
iex> is_exception(%{})
|
||||
false
|
||||
|
||||
"""
|
||||
@doc since: "1.11.0", guard: true
|
||||
defmacro is_exception(term) do
|
||||
case __CALLER__.context do
|
||||
nil ->
|
||||
quote do
|
||||
case unquote(term) do
|
||||
%_{__exception__: true} -> true
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
|
||||
:match ->
|
||||
invalid_match!(:is_exception)
|
||||
|
||||
:guard ->
|
||||
quote do
|
||||
is_map(unquote(term)) and :erlang.is_map_key(:__struct__, unquote(term)) and
|
||||
is_atom(:erlang.map_get(:__struct__, unquote(term))) and
|
||||
:erlang.is_map_key(:__exception__, unquote(term)) and
|
||||
:erlang.map_get(:__exception__, unquote(term)) == true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns true if `term` is an exception of `name`; otherwise returns `false`.
|
||||
|
||||
Allowed in guard tests.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> is_exception(%RuntimeError{}, RuntimeError)
|
||||
true
|
||||
|
||||
iex> is_exception(%RuntimeError{}, Macro.Env)
|
||||
false
|
||||
|
||||
"""
|
||||
@doc since: "1.11.0", guard: true
|
||||
defmacro is_exception(term, name) do
|
||||
case __CALLER__.context do
|
||||
nil ->
|
||||
quote do
|
||||
case unquote(name) do
|
||||
name when is_atom(name) ->
|
||||
case unquote(term) do
|
||||
%{__struct__: ^name, __exception__: true} -> true
|
||||
_ -> false
|
||||
end
|
||||
|
||||
_ ->
|
||||
raise ArgumentError
|
||||
end
|
||||
end
|
||||
|
||||
:match ->
|
||||
invalid_match!(:is_exception)
|
||||
|
||||
:guard ->
|
||||
quote do
|
||||
is_map(unquote(term)) and
|
||||
(is_atom(unquote(name)) or :fail) and
|
||||
:erlang.is_map_key(:__struct__, unquote(term)) and
|
||||
:erlang.map_get(:__struct__, unquote(term)) == unquote(name) and
|
||||
:erlang.is_map_key(:__exception__, unquote(term)) and
|
||||
:erlang.map_get(:__exception__, unquote(term)) == true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a value from a nested structure.
|
||||
|
||||
@@ -2263,8 +2437,7 @@ defmodule Kernel do
|
||||
iex> get_in(users, ["john", :age])
|
||||
27
|
||||
|
||||
In case any of the entries in the middle returns `nil`, `nil` will
|
||||
be returned as per the `Access` module:
|
||||
In case any of the keys returns `nil`, `nil` will be returned:
|
||||
|
||||
iex> users = %{"john" => %{age: 27}, "meg" => %{age: 23}}
|
||||
iex> get_in(users, ["unknown", :age])
|
||||
@@ -2280,9 +2453,6 @@ defmodule Kernel do
|
||||
* a function to be invoked next
|
||||
|
||||
This means `get_in/2` can be extended to provide custom lookups.
|
||||
The downside is that functions cannot be stored as keys in the accessed
|
||||
data structures.
|
||||
|
||||
In the example below, we use a function to get all the maps inside
|
||||
a list:
|
||||
|
||||
@@ -2341,6 +2511,12 @@ defmodule Kernel do
|
||||
function. If the key is a function, it will be invoked
|
||||
as specified in `get_and_update_in/3`.
|
||||
|
||||
`data` is a nested structure (that is, a map, keyword
|
||||
list, or struct that implements the `Access` behaviour).
|
||||
The `fun` argument receives the value of `key` (or `nil`
|
||||
if `key` is not present) and the result replaces the value
|
||||
in the structure.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> users = %{"john" => %{age: 27}, "meg" => %{age: 23}}
|
||||
@@ -2886,7 +3062,7 @@ defmodule Kernel do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Reads and writes attributes of the current module.
|
||||
Module attribute unary operator. Reads and writes attributes in the current module.
|
||||
|
||||
The canonical example for attributes is annotating that a module
|
||||
implements an OTP behaviour, such as `GenServer`:
|
||||
@@ -2910,7 +3086,7 @@ defmodule Kernel do
|
||||
will be available at compile-time. Custom attributes may be configured to
|
||||
behave closer to Erlang by using `Module.register_attribute/3`.
|
||||
|
||||
Finally, notice that attributes can also be read inside functions:
|
||||
Finally, note that attributes can also be read inside functions:
|
||||
|
||||
defmodule MyServer do
|
||||
@my_data 11
|
||||
@@ -3247,7 +3423,7 @@ defmodule Kernel do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a range with the specified `first` and `last` integers.
|
||||
Range creation operator. Returns a range with the specified `first` and `last` integers.
|
||||
|
||||
If last is larger than first, the range will be increasing from
|
||||
first to last. If first is larger than last, the range will be
|
||||
@@ -3302,6 +3478,8 @@ defmodule Kernel do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Boolean "and" operator.
|
||||
|
||||
Provides a short-circuit operator that evaluates and returns
|
||||
the second expression only if the first one evaluates to a truthy value
|
||||
(neither `false` nor `nil`). Returns the first expression
|
||||
@@ -3341,6 +3519,8 @@ defmodule Kernel do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Boolean "or" operator.
|
||||
|
||||
Provides a short-circuit operator that evaluates and returns the second
|
||||
expression only if the first one does not evaluate to a truthy value (that is,
|
||||
it is either `nil` or `false`). Returns the first expression otherwise.
|
||||
@@ -3520,7 +3700,7 @@ defmodule Kernel do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if the element on the left-hand side is a member of the
|
||||
Membership operator. Checks if the element on the left-hand side is a member of the
|
||||
collection on the right-hand side.
|
||||
|
||||
## Examples
|
||||
@@ -3576,7 +3756,7 @@ defmodule Kernel do
|
||||
"""
|
||||
@doc guard: true
|
||||
defmacro left in right do
|
||||
in_module? = __CALLER__.context == nil
|
||||
in_body? = __CALLER__.context == nil
|
||||
|
||||
expand =
|
||||
case bootstrapped?(Macro) do
|
||||
@@ -3585,7 +3765,7 @@ defmodule Kernel do
|
||||
end
|
||||
|
||||
case expand.(right) do
|
||||
[] when not in_module? ->
|
||||
[] when not in_body? ->
|
||||
false
|
||||
|
||||
[] ->
|
||||
@@ -3594,28 +3774,28 @@ defmodule Kernel do
|
||||
false
|
||||
end
|
||||
|
||||
[head | tail] = list when not in_module? ->
|
||||
in_var(in_module?, left, &in_list(&1, head, tail, expand, list, in_module?))
|
||||
[head | tail] = list when not in_body? ->
|
||||
in_list(left, head, tail, expand, list, in_body?)
|
||||
|
||||
[_ | _] = list when in_module? ->
|
||||
[_ | _] = list when in_body? ->
|
||||
case ensure_evaled(list, {0, []}, expand) do
|
||||
{[head | tail], {_, []}} ->
|
||||
in_var(in_module?, left, &in_list(&1, head, tail, expand, list, in_module?))
|
||||
in_var(in_body?, left, &in_list(&1, head, tail, expand, list, in_body?))
|
||||
|
||||
{[head | tail], {_, vars_values}} ->
|
||||
{vars, values} = :lists.unzip(:lists.reverse(vars_values))
|
||||
is_in_list = &in_list(&1, head, tail, expand, list, in_module?)
|
||||
is_in_list = &in_list(&1, head, tail, expand, list, in_body?)
|
||||
|
||||
quote do
|
||||
{unquote_splicing(vars)} = {unquote_splicing(values)}
|
||||
unquote(in_var(in_module?, left, is_in_list))
|
||||
unquote(in_var(in_body?, left, is_in_list))
|
||||
end
|
||||
end
|
||||
|
||||
{:%{}, _meta, [__struct__: Elixir.Range, first: first, last: last]} ->
|
||||
in_var(in_module?, left, &in_range(&1, expand.(first), expand.(last)))
|
||||
in_var(in_body?, left, &in_range(&1, expand.(first), expand.(last)))
|
||||
|
||||
right when in_module? ->
|
||||
right when in_body? ->
|
||||
quote(do: Elixir.Enum.member?(unquote(right), unquote(left)))
|
||||
|
||||
%{__struct__: Elixir.Range, first: _, last: _} ->
|
||||
@@ -3724,18 +3904,12 @@ defmodule Kernel do
|
||||
end
|
||||
end
|
||||
|
||||
defp in_list(left, head, tail, expand, right, in_module?) do
|
||||
[head | tail] =
|
||||
:lists.foldl(
|
||||
&[comp(left, &1, expand, right, in_module?) | &2],
|
||||
[],
|
||||
[head | tail]
|
||||
)
|
||||
|
||||
:lists.foldl("e(do: :erlang.orelse(unquote(&1), unquote(&2))), head, tail)
|
||||
defp in_list(left, head, tail, expand, right, in_body?) do
|
||||
[head | tail] = :lists.map(&comp(left, &1, expand, right, in_body?), [head | tail])
|
||||
:lists.foldl("e(do: :erlang.orelse(unquote(&2), unquote(&1))), head, tail)
|
||||
end
|
||||
|
||||
defp comp(left, {:|, _, [head, tail]}, expand, right, in_module?) do
|
||||
defp comp(left, {:|, _, [head, tail]}, expand, right, in_body?) do
|
||||
case expand.(tail) do
|
||||
[] ->
|
||||
quote(do: :erlang."=:="(unquote(left), unquote(head)))
|
||||
@@ -3744,11 +3918,11 @@ defmodule Kernel do
|
||||
quote do
|
||||
:erlang.orelse(
|
||||
:erlang."=:="(unquote(left), unquote(head)),
|
||||
unquote(in_list(left, tail_head, tail, expand, right, in_module?))
|
||||
unquote(in_list(left, tail_head, tail, expand, right, in_body?))
|
||||
)
|
||||
end
|
||||
|
||||
tail when in_module? ->
|
||||
tail when in_body? ->
|
||||
quote do
|
||||
:erlang.orelse(
|
||||
:erlang."=:="(unquote(left), unquote(head)),
|
||||
@@ -3761,7 +3935,7 @@ defmodule Kernel do
|
||||
end
|
||||
end
|
||||
|
||||
defp comp(left, right, _expand, _right, _in_module?) do
|
||||
defp comp(left, right, _expand, _right, _in_body?) do
|
||||
quote(do: :erlang."=:="(unquote(left), unquote(right)))
|
||||
end
|
||||
|
||||
@@ -3784,13 +3958,20 @@ defmodule Kernel do
|
||||
end
|
||||
|
||||
@doc """
|
||||
When used inside quoting, marks that the given variable should
|
||||
not be hygienized.
|
||||
Marks that the given variable should not be hygienized.
|
||||
|
||||
The argument can be either a variable unquoted or in standard tuple form
|
||||
`{name, meta, context}`.
|
||||
This macro expects a variable and it is typically invoked
|
||||
inside `Kernel.SpecialForms.quote/2` to mark that a variable
|
||||
should not be hygienized. See `Kernel.SpecialForms.quote/2`
|
||||
for more information.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Kernel.var!(example) = 1
|
||||
1
|
||||
iex> Kernel.var!(example)
|
||||
1
|
||||
|
||||
Check `Kernel.SpecialForms.quote/2` for more information.
|
||||
"""
|
||||
defmacro var!(var, context \\ nil)
|
||||
|
||||
@@ -4091,24 +4272,24 @@ defmodule Kernel do
|
||||
## `rescue`/`catch`/`after`/`else`
|
||||
|
||||
Function bodies support `rescue`, `catch`, `after`, and `else` as `Kernel.SpecialForms.try/1`
|
||||
does. For example, the following two functions are equivalent:
|
||||
does (known as "implicit try"). For example, the following two functions are equivalent:
|
||||
|
||||
def format(value) do
|
||||
def convert(number) do
|
||||
try do
|
||||
format!(value)
|
||||
catch
|
||||
:exit, reason -> {:error, reason}
|
||||
String.to_integer(number)
|
||||
rescue
|
||||
e in ArgumentError -> {:error, e.message}
|
||||
end
|
||||
end
|
||||
|
||||
def format(value) do
|
||||
format!(value)
|
||||
catch
|
||||
:exit, reason -> {:error, reason}
|
||||
def convert(number) do
|
||||
String.to_integer(number)
|
||||
rescue
|
||||
e in ArgumentError -> {:error, e.message}
|
||||
end
|
||||
|
||||
"""
|
||||
defmacro def(call, expr \\ []) do
|
||||
defmacro def(call, expr \\ nil) do
|
||||
define(:def, call, expr, __CALLER__)
|
||||
end
|
||||
|
||||
@@ -4138,7 +4319,7 @@ defmodule Kernel do
|
||||
** (UndefinedFunctionError) undefined function Foo.sum/2
|
||||
|
||||
"""
|
||||
defmacro defp(call, expr \\ []) do
|
||||
defmacro defp(call, expr \\ nil) do
|
||||
define(:defp, call, expr, __CALLER__)
|
||||
end
|
||||
|
||||
@@ -4166,7 +4347,7 @@ defmodule Kernel do
|
||||
end
|
||||
|
||||
"""
|
||||
defmacro defmacro(call, expr \\ []) do
|
||||
defmacro defmacro(call, expr \\ nil) do
|
||||
define(:defmacro, call, expr, __CALLER__)
|
||||
end
|
||||
|
||||
@@ -4182,7 +4363,7 @@ defmodule Kernel do
|
||||
naming and default arguments.
|
||||
|
||||
"""
|
||||
defmacro defmacrop(call, expr \\ []) do
|
||||
defmacro defmacrop(call, expr \\ nil) do
|
||||
define(:defmacrop, call, expr, __CALLER__)
|
||||
end
|
||||
|
||||
@@ -4677,7 +4858,7 @@ defmodule Kernel do
|
||||
macro_definition =
|
||||
case impls do
|
||||
[] ->
|
||||
define(kind, call, [], env)
|
||||
define(kind, call, nil, env)
|
||||
|
||||
[guard] ->
|
||||
quoted =
|
||||
@@ -4727,8 +4908,8 @@ defmodule Kernel do
|
||||
|
||||
## Examples
|
||||
|
||||
For example, in order to write test cases using the `ExUnit` framework
|
||||
provided with Elixir, a developer should `use` the `ExUnit.Case` module:
|
||||
For example, to write test cases using the `ExUnit` framework provided
|
||||
with Elixir, a developer should `use` the `ExUnit.Case` module:
|
||||
|
||||
defmodule AssertionTest do
|
||||
use ExUnit.Case, async: true
|
||||
@@ -4738,8 +4919,11 @@ defmodule Kernel do
|
||||
end
|
||||
end
|
||||
|
||||
In this example, `ExUnit.Case.__using__/1` is called with the keyword list
|
||||
`[async: true]` as its argument; `use/2` translates to:
|
||||
In this example, Elixir will call the `__using__/1` macro in the
|
||||
`ExUnit.Case` module with the keyword list `[async: true]` as its
|
||||
argument.
|
||||
|
||||
In other words, `use/2` translates to:
|
||||
|
||||
defmodule AssertionTest do
|
||||
require ExUnit.Case
|
||||
@@ -4750,7 +4934,7 @@ defmodule Kernel do
|
||||
end
|
||||
end
|
||||
|
||||
`ExUnit.Case` will then define the `__using__/1` macro:
|
||||
where `ExUnit.Case` defines the `__using__/1` macro:
|
||||
|
||||
defmodule ExUnit.Case do
|
||||
defmacro __using__(opts) do
|
||||
@@ -4885,6 +5069,18 @@ defmodule Kernel do
|
||||
defmacro defdelegate(funs, opts) do
|
||||
funs = Macro.escape(funs, unquote: true)
|
||||
|
||||
# don't add compile-time dependency on :to
|
||||
opts =
|
||||
with true <- is_list(opts),
|
||||
{:ok, target} <- Keyword.fetch(opts, :to),
|
||||
{:__aliases__, _, _} <- target do
|
||||
target = Macro.expand(target, %{__CALLER__ | function: {:__info__, 1}})
|
||||
Keyword.replace!(opts, :to, target)
|
||||
else
|
||||
_ ->
|
||||
opts
|
||||
end
|
||||
|
||||
quote bind_quoted: [funs: funs, opts: opts] do
|
||||
target =
|
||||
Keyword.get(opts, :to) || raise ArgumentError, "expected to: to be given as argument"
|
||||
@@ -5388,7 +5584,8 @@ defmodule Kernel do
|
||||
true ->
|
||||
parts = String.split(string)
|
||||
|
||||
parts_with_trailing_comma = :lists.filter(&(:binary.last(&1) == ?,), parts)
|
||||
parts_with_trailing_comma =
|
||||
:lists.filter(&(byte_size(&1) > 1 and :binary.last(&1) == ?,), parts)
|
||||
|
||||
if parts_with_trailing_comma != [] do
|
||||
stacktrace = Macro.Env.stacktrace(caller)
|
||||
@@ -5483,9 +5680,12 @@ defmodule Kernel do
|
||||
end
|
||||
|
||||
@doc false
|
||||
# TODO: Remove on v2.0 (also hard-coded in elixir_dispatch)
|
||||
@deprecated "Use Kernel.to_charlist/1 instead"
|
||||
defmacro to_char_list(arg) do
|
||||
IO.warn(
|
||||
"Kernel.to_char_list/1 is deprecated, use Kernel.to_charlist/1 instead",
|
||||
Macro.Env.stacktrace(__CALLER__)
|
||||
)
|
||||
|
||||
quote(do: Kernel.to_charlist(unquote(arg)))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -498,9 +498,12 @@ defmodule Kernel.CLI do
|
||||
|
||||
verbose_opts =
|
||||
if config.verbose_compile do
|
||||
[each_long_compilation: &IO.puts("Compiling #{&1} (it's taking more than 15s)")]
|
||||
[each_file: &IO.puts("Compiling #{Path.relative_to_cwd(&1)}")]
|
||||
else
|
||||
[]
|
||||
[
|
||||
each_long_compilation:
|
||||
&IO.puts("Compiling #{Path.relative_to_cwd(&1)} (it's taking more than 10s)")
|
||||
]
|
||||
end
|
||||
|
||||
profile_opts =
|
||||
@@ -525,6 +528,7 @@ defmodule Kernel.CLI do
|
||||
|
||||
defp filter_patterns(pattern) do
|
||||
pattern
|
||||
|> Path.expand()
|
||||
|> Path.wildcard()
|
||||
|> :lists.usort()
|
||||
|> Enum.filter(&File.regular?/1)
|
||||
|
||||
@@ -29,6 +29,11 @@ defmodule Kernel.LexicalTracker do
|
||||
:gen_server.call(pid, :stop)
|
||||
end
|
||||
|
||||
@doc false
|
||||
def add_require(pid, module) when is_atom(module) do
|
||||
:gen_server.cast(pid, {:add_require, module})
|
||||
end
|
||||
|
||||
@doc false
|
||||
def add_import(pid, module, fas, line, warn) when is_atom(module) do
|
||||
:gen_server.cast(pid, {:add_import, module, fas, line, warn})
|
||||
@@ -45,13 +50,8 @@ defmodule Kernel.LexicalTracker do
|
||||
end
|
||||
|
||||
@doc false
|
||||
def remote_struct(pid, module) when is_atom(module) do
|
||||
:gen_server.cast(pid, {:remote_struct, module})
|
||||
end
|
||||
|
||||
@doc false
|
||||
def import_dispatch(pid, module, fa) when is_atom(module) do
|
||||
:gen_server.cast(pid, {:import_dispatch, module, fa})
|
||||
def import_dispatch(pid, module, fa, mode) when is_atom(module) do
|
||||
:gen_server.cast(pid, {:import_dispatch, module, fa, mode})
|
||||
end
|
||||
|
||||
@doc false
|
||||
@@ -106,7 +106,7 @@ defmodule Kernel.LexicalTracker do
|
||||
state = %{
|
||||
directives: %{},
|
||||
references: %{},
|
||||
structs: %{},
|
||||
exports: %{},
|
||||
cache: %{},
|
||||
compile_env: :ordsets.new(),
|
||||
file: nil
|
||||
@@ -127,7 +127,7 @@ defmodule Kernel.LexicalTracker do
|
||||
|
||||
def handle_call(:references, _from, state) do
|
||||
{compile, runtime} = partition(Map.to_list(state.references), [], [])
|
||||
{:reply, {compile, Map.keys(state.structs), runtime, state.compile_env}, state}
|
||||
{:reply, {compile, Map.keys(state.exports), runtime, state.compile_env}, state}
|
||||
end
|
||||
|
||||
def handle_call({:read_cache, key}, _from, %{cache: cache} = state) do
|
||||
@@ -142,18 +142,13 @@ defmodule Kernel.LexicalTracker do
|
||||
{:noreply, %{state | cache: Map.put(cache, key, value)}}
|
||||
end
|
||||
|
||||
def handle_cast({:remote_struct, module}, state) do
|
||||
structs = Map.put(state.structs, module, true)
|
||||
{:noreply, %{state | structs: structs}}
|
||||
end
|
||||
|
||||
def handle_cast({:remote_dispatch, module, mode}, state) do
|
||||
references = add_reference(state.references, module, mode)
|
||||
{:noreply, %{state | references: references}}
|
||||
end
|
||||
|
||||
def handle_cast({:import_dispatch, module, {function, arity}}, state) do
|
||||
state = add_import_dispatch(state, module, function, arity)
|
||||
def handle_cast({:import_dispatch, module, {function, arity}, mode}, state) do
|
||||
state = add_import_dispatch(state, module, function, arity, mode)
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
@@ -173,6 +168,10 @@ defmodule Kernel.LexicalTracker do
|
||||
{:noreply, update_in(state.compile_env, &:ordsets.add_element({app, path, return}, &1))}
|
||||
end
|
||||
|
||||
def handle_cast({:add_require, module}, state) do
|
||||
{:noreply, put_in(state.exports[module], true)}
|
||||
end
|
||||
|
||||
def handle_cast({:add_import, module, fas, line, warn}, state) do
|
||||
directives =
|
||||
state.directives
|
||||
@@ -227,14 +226,13 @@ defmodule Kernel.LexicalTracker do
|
||||
end
|
||||
end
|
||||
|
||||
defp add_import_dispatch(state, module, function, arity) do
|
||||
defp add_import_dispatch(state, module, function, arity, mode) do
|
||||
directives =
|
||||
add_dispatch(state.directives, module, :import)
|
||||
state.directives
|
||||
|> add_dispatch(module, :import)
|
||||
|> add_dispatch({module, function, arity}, :import)
|
||||
|
||||
# Always compile time because we depend
|
||||
# on the module at compile time
|
||||
references = add_reference(state.references, module, :compile)
|
||||
references = add_reference(state.references, module, mode)
|
||||
%{state | directives: directives, references: references}
|
||||
end
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ defmodule Kernel.ParallelCompiler do
|
||||
always awaited on by calling `Task.await/1`
|
||||
"""
|
||||
@doc since: "1.6.0"
|
||||
def async(fun) when is_function(fun) do
|
||||
def async(fun) when is_function(fun, 0) do
|
||||
if parent = :erlang.get(:elixir_compiler_pid) do
|
||||
file = :erlang.get(:elixir_compiler_file)
|
||||
dest = :erlang.get(:elixir_compiler_dest)
|
||||
@@ -63,12 +63,16 @@ defmodule Kernel.ParallelCompiler do
|
||||
|
||||
* `:each_cycle` - after the given files are compiled, invokes this function
|
||||
that should return the following values:
|
||||
* `{:compile, modules}` - to continue compilation with a list of further modules to compile
|
||||
* `{:runtime, modules}` - to stop compilation and verify the list of modules because
|
||||
dependent modules have changed
|
||||
* `{:compile, modules, warnings}` - to continue compilation with a list of
|
||||
further modules to compile
|
||||
* `{:runtime, modules, warnings}` - to stop compilation and verify the list
|
||||
of modules because dependent modules have changed
|
||||
|
||||
* `:long_compilation_threshold` - the timeout (in seconds) after the
|
||||
`:each_long_compilation` callback is invoked; defaults to `15`
|
||||
* `:long_compilation_threshold` - the timeout (in seconds) to check for modules
|
||||
taking too long to compile. For each file that exceeds the threshold, the
|
||||
`:each_long_compilation` callback is invoked. From Elixir v1.11, only the time
|
||||
spent compiling the actual module is taken into account by the threshold, the
|
||||
time spent waiting is not considered. Defaults to `10` seconds.
|
||||
|
||||
* `:profile` - if set to `:time` measure the compilation time of each compilation cycle
|
||||
and group pass checker
|
||||
@@ -136,41 +140,47 @@ defmodule Kernel.ParallelCompiler do
|
||||
|
||||
defp spawn_workers(files, output, options) do
|
||||
{:module, _} = :code.ensure_loaded(Kernel.ErrorHandler)
|
||||
compiler_pid = self()
|
||||
:elixir_code_server.cast({:reset_warnings, compiler_pid})
|
||||
schedulers = max(:erlang.system_info(:schedulers_online), 2)
|
||||
beam_timestamp = Keyword.get(options, :beam_timestamp)
|
||||
threshold = Keyword.get(options, :long_compilation_threshold, 10) * 1000
|
||||
timer_ref = Process.send_after(self(), :threshold_check, threshold)
|
||||
|
||||
result =
|
||||
{outcome, state} =
|
||||
spawn_workers(files, 0, [], [], %{}, [], %{
|
||||
dest: Keyword.get(options, :dest),
|
||||
each_cycle: Keyword.get(options, :each_cycle, fn -> {:runtime, []} end),
|
||||
each_cycle: Keyword.get(options, :each_cycle, fn -> {:runtime, [], []} end),
|
||||
each_file: Keyword.get(options, :each_file, fn _, _ -> :ok end) |> each_file(),
|
||||
each_long_compilation: Keyword.get(options, :each_long_compilation, fn _file -> :ok end),
|
||||
each_module: Keyword.get(options, :each_module, fn _file, _module, _binary -> :ok end),
|
||||
beam_timestamp: Keyword.get(options, :beam_timestamp),
|
||||
long_compilation_threshold: Keyword.get(options, :long_compilation_threshold, 15),
|
||||
profile: Keyword.get(options, :profile),
|
||||
cycle_start: System.monotonic_time(),
|
||||
module_counter: 0,
|
||||
profile: profile_init(Keyword.get(options, :profile)),
|
||||
output: output,
|
||||
timer_ref: timer_ref,
|
||||
long_compilation_threshold: threshold,
|
||||
schedulers: schedulers
|
||||
})
|
||||
|
||||
# In case --warning-as-errors is enabled and there was a warning,
|
||||
# compilation status will be set to error.
|
||||
compilation_status = :elixir_code_server.call({:compilation_status, compiler_pid})
|
||||
Process.cancel_timer(state.timer_ref)
|
||||
|
||||
case {result, compilation_status} do
|
||||
{{:ok, _, warnings}, :error} ->
|
||||
receive do
|
||||
:threshold_check -> :ok
|
||||
after
|
||||
0 -> :ok
|
||||
end
|
||||
|
||||
case {outcome, Code.get_compiler_option(:warnings_as_errors)} do
|
||||
{{:ok, _, [_ | _] = warnings}, true} ->
|
||||
message = "Compilation failed due to warnings while using the --warnings-as-errors option"
|
||||
IO.puts(:stderr, message)
|
||||
{:error, warnings, []}
|
||||
|
||||
{{:error, errors, warnings}, :error} ->
|
||||
{{:ok, outcome, warnings}, _} ->
|
||||
{:ok, write_module_binaries(outcome, output, beam_timestamp), warnings}
|
||||
|
||||
{{:error, errors, warnings}, true} ->
|
||||
{:error, errors ++ warnings, []}
|
||||
|
||||
_ ->
|
||||
result
|
||||
{{:error, errors, warnings}, _} ->
|
||||
{:error, errors, warnings}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -187,6 +197,77 @@ defmodule Kernel.ParallelCompiler do
|
||||
end
|
||||
end
|
||||
|
||||
defp write_module_binaries(result, {:compile, path}, timestamp) do
|
||||
Enum.flat_map(result, fn
|
||||
{{:module, module}, {binary, _map}} ->
|
||||
full_path = Path.join(path, Atom.to_string(module) <> ".beam")
|
||||
File.write!(full_path, binary)
|
||||
if timestamp, do: File.touch!(full_path, timestamp)
|
||||
[module]
|
||||
|
||||
_ ->
|
||||
[]
|
||||
end)
|
||||
end
|
||||
|
||||
defp write_module_binaries(result, _output, _timestamp) do
|
||||
for {{:module, module}, _} <- result, do: module
|
||||
end
|
||||
|
||||
## Verification
|
||||
|
||||
defp verify_modules(result, warnings, dependent_modules, state) do
|
||||
checker_warnings = maybe_check_modules(result, dependent_modules, state)
|
||||
warnings = Enum.reverse(warnings, checker_warnings)
|
||||
{{:ok, result, warnings}, state}
|
||||
end
|
||||
|
||||
defp maybe_check_modules(result, runtime_modules, state) do
|
||||
%{schedulers: schedulers, profile: profile} = state
|
||||
|
||||
if :elixir_config.get(:bootstrap) do
|
||||
[]
|
||||
else
|
||||
compiled_modules = checker_compiled_modules(result)
|
||||
runtime_modules = checker_runtime_modules(runtime_modules)
|
||||
|
||||
profile_checker(profile, compiled_modules, runtime_modules, fn ->
|
||||
Module.ParallelChecker.verify(compiled_modules, runtime_modules, schedulers)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
defp checker_compiled_modules(result) do
|
||||
for {{:module, _module}, {binary, module_map}} <- result do
|
||||
{module_map, binary}
|
||||
end
|
||||
end
|
||||
|
||||
defp checker_runtime_modules(modules) do
|
||||
for module <- modules,
|
||||
path = :code.which(module),
|
||||
is_list(path) and path != [] do
|
||||
{module, File.read!(path)}
|
||||
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
|
||||
{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")
|
||||
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
|
||||
defp spawn_workers(
|
||||
queue,
|
||||
@@ -203,23 +284,23 @@ defmodule Kernel.ParallelCompiler do
|
||||
|
||||
# Release waiting processes
|
||||
defp spawn_workers([{ref, found} | t], spawned, waiting, files, result, warnings, state) do
|
||||
waiting =
|
||||
{files, waiting} =
|
||||
case List.keytake(waiting, ref, 2) do
|
||||
{{_kind, pid, ^ref, _on, _defining, _deadlock}, waiting} ->
|
||||
send(pid, {ref, found})
|
||||
waiting
|
||||
{update_timing(files, pid, :waiting), waiting}
|
||||
|
||||
nil ->
|
||||
# In case the waiting process died (for example, it was an async process),
|
||||
# it will no longer be on the list. So we need to take it into account here.
|
||||
waiting
|
||||
{files, waiting}
|
||||
end
|
||||
|
||||
spawn_workers(t, spawned, waiting, files, result, warnings, state)
|
||||
end
|
||||
|
||||
defp spawn_workers([file | queue], spawned, waiting, files, result, warnings, state) do
|
||||
%{output: output, long_compilation_threshold: threshold, dest: dest} = state
|
||||
%{output: output, dest: dest} = state
|
||||
parent = self()
|
||||
file = Path.expand(file)
|
||||
|
||||
@@ -242,24 +323,34 @@ defmodule Kernel.ParallelCompiler do
|
||||
exit(:shutdown)
|
||||
end)
|
||||
|
||||
timer_ref = Process.send_after(self(), {:timed_out, pid}, threshold * 1000)
|
||||
files = [{pid, ref, file, timer_ref} | files]
|
||||
file_data = %{
|
||||
pid: pid,
|
||||
ref: ref,
|
||||
file: file,
|
||||
timestamp: System.monotonic_time(),
|
||||
compiling: 0,
|
||||
waiting: 0,
|
||||
warned: false
|
||||
}
|
||||
|
||||
files = [file_data | files]
|
||||
spawn_workers(queue, spawned + 1, waiting, files, result, warnings, state)
|
||||
end
|
||||
|
||||
# No more queue, nothing waiting, this cycle is done
|
||||
defp spawn_workers([], 0, [], [], result, warnings, state) do
|
||||
cycle_return = each_cycle_return(state.each_cycle.())
|
||||
state = cycle_timing(result, state)
|
||||
|
||||
case each_cycle_return(state.each_cycle.()) do
|
||||
{:runtime, dependent_modules} ->
|
||||
write_and_verify_modules(result, warnings, dependent_modules, state)
|
||||
case cycle_return do
|
||||
{:runtime, dependent_modules, extra_warnings} ->
|
||||
verify_modules(result, extra_warnings ++ warnings, dependent_modules, state)
|
||||
|
||||
{:compile, []} ->
|
||||
write_and_verify_modules(result, warnings, [], state)
|
||||
{:compile, [], extra_warnings} ->
|
||||
verify_modules(result, extra_warnings ++ warnings, [], state)
|
||||
|
||||
{:compile, more} ->
|
||||
spawn_workers(more, 0, [], [], result, warnings, state)
|
||||
{:compile, more, extra_warnings} ->
|
||||
spawn_workers(more, 0, [], [], result, extra_warnings ++ warnings, state)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -270,7 +361,7 @@ defmodule Kernel.ParallelCompiler do
|
||||
[],
|
||||
1,
|
||||
[{_, pid, ref, _, _, _}] = waiting,
|
||||
[{pid, _, _, _}] = files,
|
||||
[%{pid: pid}] = files,
|
||||
result,
|
||||
warnings,
|
||||
state
|
||||
@@ -301,7 +392,7 @@ defmodule Kernel.ParallelCompiler do
|
||||
|
||||
true ->
|
||||
errors = handle_deadlock(waiting, files)
|
||||
{:error, errors, warnings}
|
||||
{{:error, errors, warnings}, state}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -327,8 +418,11 @@ defmodule Kernel.ParallelCompiler do
|
||||
end
|
||||
end
|
||||
|
||||
defp cycle_timing(result, %{profile: :time} = state) do
|
||||
%{cycle_start: cycle_start, module_counter: module_counter} = state
|
||||
defp cycle_timing(_result, %{profile: :none} = state) do
|
||||
state
|
||||
end
|
||||
|
||||
defp cycle_timing(result, %{profile: {:time, cycle_start, module_counter}} = state) do
|
||||
num_modules = count_modules(result)
|
||||
diff_modules = num_modules - module_counter
|
||||
now = System.monotonic_time()
|
||||
@@ -339,92 +433,25 @@ defmodule Kernel.ParallelCompiler do
|
||||
"[profile] Finished compilation cycle of #{diff_modules} modules in #{time}ms"
|
||||
)
|
||||
|
||||
%{state | cycle_start: now, module_counter: num_modules}
|
||||
end
|
||||
|
||||
defp cycle_timing(_result, %{profile: nil} = state) do
|
||||
state
|
||||
%{state | profile: {:time, now, num_modules}}
|
||||
end
|
||||
|
||||
defp count_modules(result) do
|
||||
Enum.count(result, &match?({{:module, _}, _}, &1))
|
||||
end
|
||||
|
||||
# TODO: Deprecate on v1.14
|
||||
defp each_cycle_return(modules) when is_list(modules), do: {:compile, modules}
|
||||
defp each_cycle_return(other), do: other
|
||||
|
||||
defp write_and_verify_modules(result, warnings, dependent_modules, state) do
|
||||
modules = write_module_binaries(result, state)
|
||||
checker_warnings = maybe_check_modules(result, dependent_modules, state)
|
||||
warnings = Enum.reverse(warnings, checker_warnings)
|
||||
{:ok, modules, warnings}
|
||||
end
|
||||
|
||||
defp write_module_binaries(result, %{output: {:compile, path}, beam_timestamp: timestamp}) do
|
||||
Enum.flat_map(result, fn
|
||||
{{:module, module}, {binary, _map}} ->
|
||||
full_path = Path.join(path, Atom.to_string(module) <> ".beam")
|
||||
File.write!(full_path, binary)
|
||||
if timestamp, do: File.touch!(full_path, timestamp)
|
||||
[module]
|
||||
|
||||
_ ->
|
||||
[]
|
||||
end)
|
||||
end
|
||||
|
||||
defp write_module_binaries(result, _state) do
|
||||
for {{:module, module}, _} <- result, do: module
|
||||
end
|
||||
|
||||
defp maybe_check_modules(result, runtime_modules, state) do
|
||||
%{schedulers: schedulers, profile: profile} = state
|
||||
|
||||
if :elixir_config.get(:bootstrap) do
|
||||
[]
|
||||
else
|
||||
compiled_modules = checker_compiled_modules(result)
|
||||
runtime_modules = checker_runtime_modules(runtime_modules)
|
||||
|
||||
profile_checker(profile, compiled_modules, runtime_modules, fn ->
|
||||
Module.ParallelChecker.verify(compiled_modules, runtime_modules, schedulers)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
defp checker_compiled_modules(result) do
|
||||
for {{:module, _module}, {binary, module_map}} <- result do
|
||||
{module_map, binary}
|
||||
end
|
||||
end
|
||||
|
||||
defp checker_runtime_modules(modules) do
|
||||
for module <- modules,
|
||||
path = :code.which(module),
|
||||
is_list(path) do
|
||||
{module, File.read!(path)}
|
||||
end
|
||||
end
|
||||
|
||||
defp profile_checker(_profile = :time, compiled_modules, runtime_modules, 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")
|
||||
result
|
||||
end
|
||||
|
||||
defp profile_checker(_profile = nil, _compiled_modules, _runtime_modules, fun) do
|
||||
fun.()
|
||||
end
|
||||
# TODO: Deprecate other returns on v1.14
|
||||
defp each_cycle_return({kind, modules, warnings}), do: {kind, modules, warnings}
|
||||
defp each_cycle_return({kind, modules}), do: {kind, modules, []}
|
||||
defp each_cycle_return(modules) when is_list(modules), do: {:compile, modules, []}
|
||||
|
||||
# The goal of this function is to find leaves in the dependency graph,
|
||||
# i.e. to find code that depends on code that we know is not being defined.
|
||||
# Note that not all files have been compile yet, so they may not be in waiting.
|
||||
defp without_definition(waiting, files) do
|
||||
nillify_empty(
|
||||
for {pid, _, _, _} <- files,
|
||||
{_, ^pid, ref, on, _, _} = List.keyfind(waiting, pid, 1),
|
||||
for %{pid: pid} <- files,
|
||||
{_, ^pid, ref, on, _, _} <- List.wrap(List.keyfind(waiting, pid, 1)),
|
||||
not Enum.any?(waiting, fn {_, _, _, _, defining, _} -> on in defining end),
|
||||
do: {ref, :not_found}
|
||||
)
|
||||
@@ -464,7 +491,6 @@ defmodule Kernel.ParallelCompiler do
|
||||
for {:module, _, ref, ^module, _defining, _deadlock} <- waiting,
|
||||
do: {ref, :found}
|
||||
|
||||
cancel_waiting_timer(files, child)
|
||||
result = Map.put(result, {:module, module}, {binary, module_map})
|
||||
spawn_workers(available ++ queue, spawned, waiting, files, result, warnings, state)
|
||||
|
||||
@@ -475,24 +501,33 @@ defmodule Kernel.ParallelCompiler do
|
||||
|
||||
{:waiting, kind, child, ref, on, defining, deadlock?} ->
|
||||
# If we already got what we were waiting for, do not put it on waiting.
|
||||
# Alternatively, we're waiting on ourselves,
|
||||
# send :found so that we can crash with a better error.
|
||||
waiting =
|
||||
# If we're waiting on ourselves, send :found so that we can crash with
|
||||
# a better error.
|
||||
{files, waiting} =
|
||||
if Map.has_key?(result, {kind, on}) or on in defining do
|
||||
send(child, {ref, :found})
|
||||
waiting
|
||||
{files, waiting}
|
||||
else
|
||||
[{kind, child, ref, on, defining, deadlock?} | waiting]
|
||||
files = update_timing(files, child, :compiling)
|
||||
{files, [{kind, child, ref, on, defining, deadlock?} | waiting]}
|
||||
end
|
||||
|
||||
spawn_workers(queue, spawned, waiting, files, result, warnings, state)
|
||||
|
||||
{:timed_out, child} ->
|
||||
case List.keyfind(files, child, 0) do
|
||||
{^child, _, file, _} -> state.each_long_compilation.(file)
|
||||
_ -> :ok
|
||||
end
|
||||
:threshold_check ->
|
||||
files =
|
||||
for data <- files do
|
||||
if data.warned or List.keymember?(waiting, data.pid, 1) do
|
||||
data
|
||||
else
|
||||
data = update_timing(data, :compiling)
|
||||
data = maybe_warn_long_compilation(data, state)
|
||||
data
|
||||
end
|
||||
end
|
||||
|
||||
timer_ref = Process.send_after(self(), :threshold_check, state.long_compilation_threshold)
|
||||
state = %{state | timer_ref: timer_ref}
|
||||
spawn_workers(queue, spawned, waiting, files, result, warnings, state)
|
||||
|
||||
{:warning, file, line, message} ->
|
||||
@@ -504,10 +539,9 @@ defmodule Kernel.ParallelCompiler do
|
||||
{:file_ok, child_pid, ref, file, lexical} ->
|
||||
state.each_file.(file, lexical)
|
||||
send(child_pid, ref)
|
||||
cancel_waiting_timer(files, child_pid)
|
||||
|
||||
discard_down(child_pid)
|
||||
new_files = List.keydelete(files, child_pid, 0)
|
||||
new_files = discard_and_maybe_log_file(files, child_pid, state)
|
||||
|
||||
# Sometimes we may have spurious entries in the waiting list
|
||||
# because someone invoked try/rescue UndefinedFunctionError
|
||||
@@ -515,28 +549,77 @@ defmodule Kernel.ParallelCompiler do
|
||||
spawn_workers(queue, spawned - 1, new_waiting, new_files, result, warnings, state)
|
||||
|
||||
{:file_cancel, child_pid} ->
|
||||
cancel_waiting_timer(files, child_pid)
|
||||
discard_down(child_pid)
|
||||
new_files = List.keydelete(files, child_pid, 0)
|
||||
new_files = Enum.reject(files, &(&1.pid == child_pid))
|
||||
spawn_workers(queue, spawned - 1, waiting, new_files, result, warnings, state)
|
||||
|
||||
{:file_error, child_pid, file, {kind, reason, stack}} ->
|
||||
print_error(file, kind, reason, stack)
|
||||
cancel_waiting_timer(files, child_pid)
|
||||
discard_down(child_pid)
|
||||
files |> List.keydelete(child_pid, 0) |> terminate()
|
||||
{:error, [to_error(file, kind, reason, stack)], warnings}
|
||||
files |> Enum.reject(&(&1.pid == child_pid)) |> terminate()
|
||||
{{:error, [to_error(file, kind, reason, stack)], warnings}, state}
|
||||
|
||||
{:DOWN, ref, :process, pid, reason} ->
|
||||
waiting = List.keydelete(waiting, pid, 1)
|
||||
|
||||
case handle_down(files, ref, reason) do
|
||||
:ok -> wait_for_messages(queue, spawned - 1, waiting, files, result, warnings, state)
|
||||
{:error, errors} -> {:error, errors, warnings}
|
||||
{:error, errors} -> {{:error, errors, warnings}, state}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp update_timing(files, pid, key) do
|
||||
Enum.map(files, fn data ->
|
||||
if data.pid == pid do
|
||||
time = System.monotonic_time()
|
||||
%{data | key => data[key] + time - data.timestamp, timestamp: time}
|
||||
else
|
||||
data
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp update_timing(data, key) do
|
||||
time = System.monotonic_time()
|
||||
%{data | key => data[key] + time - data.timestamp, timestamp: time}
|
||||
end
|
||||
|
||||
defp maybe_warn_long_compilation(data, state) do
|
||||
compiling = System.convert_time_unit(data.compiling, :native, :millisecond)
|
||||
|
||||
if not data.warned and compiling >= state.long_compilation_threshold do
|
||||
state.each_long_compilation.(data.file)
|
||||
%{data | warned: true}
|
||||
else
|
||||
data
|
||||
end
|
||||
end
|
||||
|
||||
defp discard_and_maybe_log_file(files, pid, state) do
|
||||
Enum.reject(files, fn data ->
|
||||
if data.pid == pid do
|
||||
data = update_timing(data, :compiling)
|
||||
data = maybe_warn_long_compilation(data, state)
|
||||
|
||||
if state.profile != :none do
|
||||
compiling = System.convert_time_unit(data.compiling, :native, :millisecond)
|
||||
waiting = System.convert_time_unit(data.waiting, :native, :millisecond)
|
||||
extra = if waiting > 0, do: " (plus #{waiting}ms waiting)", else: ""
|
||||
|
||||
IO.puts(
|
||||
:stderr,
|
||||
"[profile] #{Path.relative_to_cwd(data.file)} compiled in #{compiling}ms" <> extra
|
||||
)
|
||||
end
|
||||
|
||||
true
|
||||
else
|
||||
false
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp discard_down(pid) do
|
||||
receive do
|
||||
{:DOWN, _, :process, ^pid, _} -> :ok
|
||||
@@ -548,24 +631,20 @@ defmodule Kernel.ParallelCompiler do
|
||||
end
|
||||
|
||||
defp handle_down(files, ref, reason) do
|
||||
case List.keyfind(files, ref, 1) do
|
||||
{child_pid, ^ref, file, _timer_ref} ->
|
||||
case Enum.find(files, &(&1.ref == ref)) do
|
||||
%{pid: pid, file: file} ->
|
||||
print_error(file, :exit, reason, [])
|
||||
|
||||
files
|
||||
|> List.keydelete(child_pid, 0)
|
||||
|> terminate()
|
||||
|
||||
files |> Enum.reject(&(&1.pid == pid)) |> terminate()
|
||||
{:error, [to_error(file, :exit, reason, [])]}
|
||||
|
||||
_ ->
|
||||
nil ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_deadlock(waiting, files) do
|
||||
deadlock =
|
||||
for {pid, _, file, _} <- files do
|
||||
for %{pid: pid, file: file} <- files do
|
||||
{:current_stacktrace, stacktrace} = Process.info(pid, :current_stacktrace)
|
||||
Process.exit(pid, :kill)
|
||||
|
||||
@@ -600,8 +679,8 @@ defmodule Kernel.ParallelCompiler do
|
||||
end
|
||||
|
||||
defp terminate(files) do
|
||||
for {pid, _, _, _} <- files, do: Process.exit(pid, :kill)
|
||||
for {pid, _, _, _} <- files, do: discard_down(pid)
|
||||
for %{pid: pid} <- files, do: Process.exit(pid, :kill)
|
||||
for %{pid: pid} <- files, do: discard_down(pid)
|
||||
:ok
|
||||
end
|
||||
|
||||
@@ -612,22 +691,6 @@ defmodule Kernel.ParallelCompiler do
|
||||
])
|
||||
end
|
||||
|
||||
defp cancel_waiting_timer(files, child_pid) do
|
||||
case List.keyfind(files, child_pid, 0) do
|
||||
{^child_pid, _ref, _file, timer_ref} ->
|
||||
Process.cancel_timer(timer_ref)
|
||||
# Let's flush the message in case it arrived before we canceled the timeout.
|
||||
receive do
|
||||
{:timed_out, ^child_pid} -> :ok
|
||||
after
|
||||
0 -> :ok
|
||||
end
|
||||
|
||||
nil ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp to_error(file, kind, reason, stack) do
|
||||
line = get_line(file, reason, stack)
|
||||
file = Path.absname(file)
|
||||
@@ -645,6 +708,12 @@ defmodule Kernel.ParallelCompiler do
|
||||
end
|
||||
end
|
||||
|
||||
defp get_line(file, _reason, [{_, _, _, [file: 'expanding macro']}, {_, _, _, info} | _]) do
|
||||
if Keyword.get(info, :file) == to_charlist(Path.relative_to_cwd(file)) do
|
||||
Keyword.get(info, :line)
|
||||
end
|
||||
end
|
||||
|
||||
defp get_line(file, _reason, [{_, _, _, info} | _]) do
|
||||
if Keyword.get(info, :file) == to_charlist(Path.relative_to_cwd(file)) do
|
||||
Keyword.get(info, :line)
|
||||
|
||||
@@ -14,7 +14,8 @@ defmodule Kernel.SpecialForms do
|
||||
forms used to define tuple and binary data structures respectively.
|
||||
|
||||
This module also documents macros that return information about Elixir's
|
||||
compilation environment, such as (`__ENV__/0`, `__MODULE__/0`, `__DIR__/0` and `__CALLER__/0`).
|
||||
compilation environment, such as (`__ENV__/0`, `__MODULE__/0`, `__DIR__/0`,
|
||||
`__STACKTRACE__/0`, and `__CALLER__/0`).
|
||||
|
||||
Additionally, it documents two special forms, `__block__/1` and
|
||||
`__aliases__/1`, which are not intended to be called directly by the
|
||||
@@ -186,13 +187,7 @@ defmodule Kernel.SpecialForms do
|
||||
iex> <<0, "foo">>
|
||||
<<0, 102, 111, 111>>
|
||||
|
||||
Variables or any other type need to be explicitly tagged:
|
||||
|
||||
iex> rest = "oo"
|
||||
iex> <<102, rest>>
|
||||
** (ArgumentError) argument error
|
||||
|
||||
We can solve this by explicitly tagging it as `binary`:
|
||||
Binaries need to be explicitly tagged as `binary`:
|
||||
|
||||
iex> rest = "oo"
|
||||
iex> <<102, rest::binary>>
|
||||
@@ -206,6 +201,12 @@ defmodule Kernel.SpecialForms do
|
||||
iex> <<"foo"::utf32>>
|
||||
<<0, 0, 0, 102, 0, 0, 0, 111, 0, 0, 0, 111>>
|
||||
|
||||
Otherwise we get an `ArgumentError` when construcing the binary:
|
||||
|
||||
rest = "oo"
|
||||
<<102, rest>>
|
||||
** (ArgumentError) argument error
|
||||
|
||||
## Options
|
||||
|
||||
Many options can be given by using `-` as separator. Order is
|
||||
@@ -370,7 +371,7 @@ defmodule Kernel.SpecialForms do
|
||||
defmacro unquote(:<<>>)(args), do: error!([args])
|
||||
|
||||
@doc """
|
||||
Defines a remote call, a call to an anonymous function, or an alias.
|
||||
Dot operator. Defines a remote call, a call to an anonymous function, or an alias.
|
||||
|
||||
The dot (`.`) in Elixir can be used for remote calls:
|
||||
|
||||
@@ -437,7 +438,7 @@ defmodule Kernel.SpecialForms do
|
||||
...> end
|
||||
{{:., [], [{:__aliases__, [alias: false], [:String]}, :downcase]}, [], ["FOO"]}
|
||||
|
||||
Notice we have an inner tuple, containing the atom `:.` representing
|
||||
Note that we have an inner tuple, containing the atom `:.` representing
|
||||
the dot as first element:
|
||||
|
||||
{:., [], [{:__aliases__, [alias: false], [:String]}, :downcase]}
|
||||
@@ -512,7 +513,7 @@ defmodule Kernel.SpecialForms do
|
||||
Keyword.values #=> uses MyKeyword.values
|
||||
Elixir.Keyword.values #=> uses Keyword.values
|
||||
|
||||
Notice that calling `alias` without the `:as` option automatically
|
||||
Note that calling `alias` without the `:as` option automatically
|
||||
sets an alias based on the last part of the module. For example:
|
||||
|
||||
alias Foo.Bar.Baz
|
||||
@@ -598,7 +599,7 @@ defmodule Kernel.SpecialForms do
|
||||
## Selector
|
||||
|
||||
By default, Elixir imports functions and macros from the given
|
||||
module, except the ones starting with underscore (which are
|
||||
module, except the ones starting with an underscore (which are
|
||||
usually callbacks):
|
||||
|
||||
import List
|
||||
@@ -616,7 +617,7 @@ defmodule Kernel.SpecialForms do
|
||||
import List, only: [flatten: 1]
|
||||
import String, except: [split: 2]
|
||||
|
||||
Notice that calling `except` is always exclusive on a previously
|
||||
Note that calling `except` is always exclusive on a previously
|
||||
declared `import/2`. If there is no previous import, then it applies
|
||||
to all functions and macros in the module. For example:
|
||||
|
||||
@@ -636,7 +637,7 @@ defmodule Kernel.SpecialForms do
|
||||
|
||||
## Lexical scope
|
||||
|
||||
It is important to notice that `import/2` is lexical. This means you
|
||||
It is important to note that `import/2` is lexical. This means you
|
||||
can import specific macros inside specific functions:
|
||||
|
||||
defmodule Math do
|
||||
@@ -720,10 +721,11 @@ defmodule Kernel.SpecialForms do
|
||||
To retrieve the stacktrace of the current process, use
|
||||
`Process.info(self(), :current_stacktrace)` instead.
|
||||
"""
|
||||
@doc since: "1.7.0"
|
||||
defmacro __STACKTRACE__, do: error!([])
|
||||
|
||||
@doc """
|
||||
Accesses an already bound variable in match clauses. Also known as the pin operator.
|
||||
Pin operator. Accesses an already bound variable in match clauses.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -755,12 +757,12 @@ defmodule Kernel.SpecialForms do
|
||||
defmacro ^var, do: error!([var])
|
||||
|
||||
@doc """
|
||||
Matches the value on the right against the pattern on the left.
|
||||
Match operator. Matches the value on the right against the pattern on the left.
|
||||
"""
|
||||
defmacro left = right, do: error!([left, right])
|
||||
|
||||
@doc """
|
||||
Used by types and bitstrings to specify types.
|
||||
Type operator. Used by types and bitstrings to specify types.
|
||||
|
||||
This operator is used in two distinct occasions in Elixir.
|
||||
It is used in typespecs to specify the type of a variable,
|
||||
@@ -804,7 +806,7 @@ defmodule Kernel.SpecialForms do
|
||||
* The first element of the tuple is always an atom or
|
||||
another tuple in the same representation.
|
||||
|
||||
* The second element of the tuple represents [metadata](t:Macro.metadata/0).
|
||||
* The second element of the tuple represents [metadata](`t:Macro.metadata/0`).
|
||||
|
||||
* The third element of the tuple are the arguments for the
|
||||
function call. The third argument may be an atom, which is
|
||||
@@ -1062,7 +1064,7 @@ defmodule Kernel.SpecialForms do
|
||||
Hygiene.no_interference()
|
||||
#=> %{}
|
||||
|
||||
Notice that, even though the alias `M` is not available
|
||||
Note that, even though the alias `M` is not available
|
||||
in the context the macro is expanded, the code above works
|
||||
because `M` still expands to `Map`.
|
||||
|
||||
@@ -1557,6 +1559,8 @@ defmodule Kernel.SpecialForms do
|
||||
@doc """
|
||||
Defines an anonymous function.
|
||||
|
||||
See `Function` for more information.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> add = fn a, b -> a + b end
|
||||
@@ -1594,7 +1598,7 @@ defmodule Kernel.SpecialForms do
|
||||
defmacro unquote(:__block__)(args), do: error!([args])
|
||||
|
||||
@doc """
|
||||
Captures or creates an anonymous function.
|
||||
Caputure operator. Captures or creates an anonymous function.
|
||||
|
||||
## Capture
|
||||
|
||||
@@ -1744,7 +1748,7 @@ defmodule Kernel.SpecialForms do
|
||||
|
||||
## Variable handling
|
||||
|
||||
Notice that variables bound in a clause do not leak to the outer context:
|
||||
Note that variables bound in a clause do not leak to the outer context:
|
||||
|
||||
case data do
|
||||
{:ok, value} -> value
|
||||
|
||||
@@ -22,9 +22,9 @@ defmodule Kernel.Typespec do
|
||||
{:docs_v1, _, _, _, _, _, docs} ->
|
||||
for {{:type, name, arity}, _, _, doc, _} <- docs do
|
||||
case doc do
|
||||
:none -> {{name, arity}, nil}
|
||||
:hidden -> {{name, arity}, false}
|
||||
%{"en" => doc_string} -> {{name, arity}, doc_string}
|
||||
:hidden -> {{name, arity}, false}
|
||||
_ -> {{name, arity}, nil}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -83,7 +83,7 @@ defmodule Kernel.Typespec do
|
||||
store_typespec(bag, kind, expr, pos)
|
||||
|
||||
case :ets.lookup(set, {:function, name, arity}) do
|
||||
[{{:function, ^name, ^arity}, line, _, doc, doc_meta}] ->
|
||||
[{{:function, ^name, ^arity}, _, line, _, doc, doc_meta}] ->
|
||||
store_doc(set, kind, name, arity, line, :doc, doc, doc_meta)
|
||||
|
||||
_ ->
|
||||
@@ -130,11 +130,18 @@ defmodule Kernel.Typespec do
|
||||
store_typespec(bag, kind, expr, pos)
|
||||
end
|
||||
|
||||
@reserved_signatures [required: 1, optional: 1]
|
||||
def deftypespec(kind, expr, line, file, module, pos)
|
||||
when kind in [:type, :typep, :opaque] do
|
||||
{set, bag} = :elixir_module.data_tables(module)
|
||||
|
||||
case type_to_signature(expr) do
|
||||
{name, arity} = signature when signature in @reserved_signatures ->
|
||||
compile_error(
|
||||
:elixir_locals.get_cached_env(pos),
|
||||
"type #{name}/#{arity} is a reserved type and it cannot be defined"
|
||||
)
|
||||
|
||||
{name, arity} when kind == :typep ->
|
||||
{line, doc} = get_doc_info(set, :typedoc, line)
|
||||
|
||||
@@ -250,7 +257,13 @@ defmodule Kernel.Typespec do
|
||||
end
|
||||
|
||||
if Map.has_key?(type_pairs, type_pair) do
|
||||
compile_error(env, "type #{name}/#{arity} is already defined")
|
||||
{error_full_path, error_line} = type_pairs[type_pair]
|
||||
error_relative_path = Path.relative_to_cwd(error_full_path)
|
||||
|
||||
compile_error(
|
||||
env,
|
||||
"type #{name}/#{arity} is already defined in #{error_relative_path}:#{error_line}"
|
||||
)
|
||||
end
|
||||
|
||||
Map.put(type_pairs, type_pair, {file, line})
|
||||
@@ -464,7 +477,7 @@ defmodule Kernel.Typespec do
|
||||
_,
|
||||
state
|
||||
)
|
||||
when is_atom(ctx1) and is_atom(ctx2) and is_integer(unit) and unit >= 0 do
|
||||
when is_atom(ctx1) and is_atom(ctx2) and unit in 1..256 do
|
||||
line = line(meta)
|
||||
{{:type, line, :binary, [{:integer, line, 0}, {:integer, line(unit_meta), unit}]}, state}
|
||||
end
|
||||
@@ -489,7 +502,7 @@ defmodule Kernel.Typespec do
|
||||
state
|
||||
)
|
||||
when is_atom(ctx1) and is_atom(ctx2) and is_atom(ctx3) and is_integer(size) and
|
||||
is_integer(unit) and size >= 0 and unit >= 0 do
|
||||
size >= 0 and unit in 1..256 do
|
||||
args = [{:integer, line(size_meta), size}, {:integer, line(unit_meta), unit}]
|
||||
{{:type, line(meta), :binary, args}, state}
|
||||
end
|
||||
@@ -497,7 +510,7 @@ defmodule Kernel.Typespec do
|
||||
defp typespec({:<<>>, _meta, _args}, _vars, caller, _state) do
|
||||
message =
|
||||
"invalid binary specification, expected <<_::size>>, <<_::_*unit>>, " <>
|
||||
"or <<_::size, _::_*unit>> with size and unit being non-negative integers"
|
||||
"or <<_::size, _::_*unit>> with size being non-negative integers, and unit being an integer between 1 and 256"
|
||||
|
||||
compile_error(caller, message)
|
||||
end
|
||||
|
||||
+105
-40
@@ -1,8 +1,11 @@
|
||||
defmodule Keyword do
|
||||
@moduledoc """
|
||||
Keyword lists are lists of two-element tuples, where the first
|
||||
element of the tuple is an atom and the second element can be any
|
||||
value, used mostly to work with optional values.
|
||||
A keyword list is a list that consists exclusively of two-element tuples.
|
||||
|
||||
The first element of these tuples is known as the *key*, and it must be an atom.
|
||||
The second element, known as the *value*, can be any term.
|
||||
|
||||
Keywords are mostly used to work with optional values.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -36,18 +39,32 @@ defmodule Keyword do
|
||||
in the `Map` module. For example, `Keyword.get/3` will get the first
|
||||
entry matching the given key, regardless if duplicated entries exist.
|
||||
Similarly, `Keyword.put/3` and `Keyword.delete/2` ensure all duplicated
|
||||
entries for a given key are removed when invoked. Note however that
|
||||
keyword list operations need to traverse the list in order to find
|
||||
entries for a given key are removed when invoked. Note, however, that
|
||||
keyword list operations need to traverse the whole list in order to find
|
||||
keys, so these operations are slower than their map counterparts.
|
||||
|
||||
A handful of functions exist to handle duplicated keys, for example,
|
||||
`get_values/2` returns all values for a given key and `delete_first/2`
|
||||
deletes just one of the existing entries.
|
||||
|
||||
The functions in `Keyword` do not guarantee any property when it comes
|
||||
to ordering. However, since a keyword list is simply a list, all the
|
||||
operations defined in `Enum` and `List` can be applied too, especially
|
||||
when ordering is required.
|
||||
Even though lists preserve the user ordering, the functions in
|
||||
`Keyword` do not guarantee any ordering. For example, if you invoke
|
||||
`Keyword.put(opts, new_key, new_value)`, there is no guarantee to
|
||||
where `new_key` will be added (to the front, to the end, or
|
||||
anywhere else).
|
||||
|
||||
Given ordering is not guaranteed, it is not recommended to pattern
|
||||
match on keyword lists either. For example, a function such as:
|
||||
|
||||
def my_function([some_key: value, another_key: another_value])
|
||||
|
||||
will match
|
||||
|
||||
my_function([some_key: :foo, another_key: :bar])
|
||||
|
||||
but it won't match
|
||||
|
||||
my_function([another_key: :bar, some_key: :foo])
|
||||
|
||||
Most of the functions in this module work in linear time. This means
|
||||
that, the time it takes to perform an operation grows at the same
|
||||
@@ -55,8 +72,8 @@ defmodule Keyword do
|
||||
|
||||
## Call syntax
|
||||
|
||||
When keyword lists are passed as the last argument to a function, then
|
||||
the square brackets around the keyword list can be omitted as well. For
|
||||
When keyword lists are passed as the last argument to a function,
|
||||
the square brackets around the keyword list can be omitted. For
|
||||
example, the keyword list syntax:
|
||||
|
||||
String.split("1-0", "-", [trim: true, parts: 2])
|
||||
@@ -242,13 +259,13 @@ defmodule Keyword do
|
||||
Gets the value from `key` and updates it, all in one pass.
|
||||
|
||||
This `fun` argument receives the value of `key` (or `nil` if `key`
|
||||
is not present) and must return a two-element tuple: the "get" value
|
||||
is not present) and must return a two-element tuple: the current value
|
||||
(the retrieved value, which can be operated on before being returned)
|
||||
and the new value to be stored under `key`. The `fun` may also
|
||||
return `:pop`, implying the current value shall be removed from the
|
||||
keyword list and returned.
|
||||
|
||||
The returned value is a tuple with the "get" value returned by
|
||||
The returned value is a tuple with the current value returned by
|
||||
`fun` and a new keyword list with the updated value under `key`.
|
||||
|
||||
## Examples
|
||||
@@ -270,7 +287,9 @@ defmodule Keyword do
|
||||
{nil, [a: 1]}
|
||||
|
||||
"""
|
||||
@spec get_and_update(t, key, (value -> {get, value} | :pop)) :: {get, t} when get: term
|
||||
@spec get_and_update(t, key, (value -> {current_value, new_value :: value} | :pop)) ::
|
||||
{current_value, value}
|
||||
when current_value: value
|
||||
def get_and_update(keywords, key, fun)
|
||||
when is_list(keywords) and is_atom(key),
|
||||
do: get_and_update(keywords, [], key, fun)
|
||||
@@ -307,11 +326,11 @@ defmodule Keyword do
|
||||
Gets the value from `key` and updates it. Raises if there is no `key`.
|
||||
|
||||
This `fun` argument receives the value of `key` and must return a
|
||||
two-element tuple: the "get" value (the retrieved value, which can be
|
||||
two-element tuple: the current value (the retrieved value, which can be
|
||||
operated on before being returned) and the new value to be stored under
|
||||
`key`.
|
||||
|
||||
The returned value is a tuple with the "get" value returned by `fun` and a new
|
||||
The returned value is a tuple with the current value returned by `fun` and a new
|
||||
keyword list with the updated value under `key`.
|
||||
|
||||
## Examples
|
||||
@@ -332,7 +351,9 @@ defmodule Keyword do
|
||||
{1, []}
|
||||
|
||||
"""
|
||||
@spec get_and_update!(t, key, (value -> {get, value})) :: {get, t} when get: term
|
||||
@spec get_and_update!(t, key, (value -> {current_value, new_value :: value} | :pop)) ::
|
||||
{current_value, t}
|
||||
when current_value: value
|
||||
def get_and_update!(keywords, key, fun) do
|
||||
get_and_update!(keywords, key, fun, [])
|
||||
end
|
||||
@@ -431,13 +452,30 @@ defmodule Keyword do
|
||||
|
||||
iex> Keyword.keys(a: 1, b: 2)
|
||||
[:a, :b]
|
||||
|
||||
iex> Keyword.keys(a: 1, b: 2, a: 3)
|
||||
[:a, :b, :a]
|
||||
|
||||
iex> Keyword.keys([{:a, 1}, {"b", 2}, {:c, 3}])
|
||||
** (ArgumentError) expected a keyword list, but an entry in the list is not a two-element tuple with an atom as its first element, got: {"b", 2}
|
||||
|
||||
"""
|
||||
@spec keys(t) :: [key]
|
||||
def keys(keywords) when is_list(keywords) do
|
||||
:lists.map(fn {k, _} -> k end, keywords)
|
||||
try do
|
||||
:lists.map(
|
||||
fn
|
||||
{key, _} when is_atom(key) -> key
|
||||
element -> throw(element)
|
||||
end,
|
||||
keywords
|
||||
)
|
||||
catch
|
||||
element ->
|
||||
raise ArgumentError,
|
||||
"expected a keyword list, but an entry in the list is not a two-element tuple with an atom as its first element, " <>
|
||||
"got: #{inspect(element)}"
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -612,18 +650,41 @@ defmodule Keyword do
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
@deprecated "Use Keyword.fetch/2 + Keyword.put/3 instead"
|
||||
@doc """
|
||||
Puts a value under `key` only if the `key` already exists in `keywords`.
|
||||
|
||||
In the case a value is stored multiple times in the keyword list,
|
||||
later occurrences are removed.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Keyword.replace([a: 1, b: 2, a: 4], :a, 3)
|
||||
[a: 3, b: 2]
|
||||
|
||||
iex> Keyword.replace([a: 1], :b, 2)
|
||||
[a: 1]
|
||||
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
@spec replace(t, key, value) :: t
|
||||
def replace(keywords, key, value) when is_list(keywords) and is_atom(key) do
|
||||
case :lists.keyfind(key, 1, keywords) do
|
||||
{^key, _} -> [{key, value} | delete(keywords, key)]
|
||||
false -> keywords
|
||||
end
|
||||
do_replace(keywords, key, value)
|
||||
end
|
||||
|
||||
defp do_replace([{key, _} | keywords], key, value) do
|
||||
[{key, value} | delete(keywords, key)]
|
||||
end
|
||||
|
||||
defp do_replace([{_, _} = e | keywords], key, value) do
|
||||
[e | do_replace(keywords, key, value)]
|
||||
end
|
||||
|
||||
defp do_replace([], _key, _value) do
|
||||
[]
|
||||
end
|
||||
|
||||
@doc """
|
||||
Alters the value stored under `key` to `value`, but only
|
||||
if the entry `key` already exists in `keywords`.
|
||||
Puts a value under `key` only if the `key` already exists in `keywords`.
|
||||
|
||||
If `key` is not present in `keywords`, a `KeyError` exception is raised.
|
||||
|
||||
@@ -652,7 +713,7 @@ defmodule Keyword do
|
||||
[e | replace!(keywords, key, value, original)]
|
||||
end
|
||||
|
||||
defp replace!([], key, _value, original) when is_atom(key) do
|
||||
defp replace!([], key, _value, original) do
|
||||
raise(KeyError, key: key, term: original)
|
||||
end
|
||||
|
||||
@@ -822,7 +883,7 @@ defmodule Keyword do
|
||||
** (KeyError) key :b not found in: [a: 1]
|
||||
|
||||
"""
|
||||
@spec update!(t, key, (value -> value)) :: t
|
||||
@spec update!(t, key, (current_value :: value -> new_value :: value)) :: t
|
||||
def update!(keywords, key, fun)
|
||||
when is_list(keywords) and is_atom(key) and is_function(fun, 1) do
|
||||
update!(keywords, key, fun, keywords)
|
||||
@@ -843,34 +904,38 @@ defmodule Keyword do
|
||||
@doc """
|
||||
Updates the `key` in `keywords` with the given function.
|
||||
|
||||
If the `key` does not exist, inserts the given `initial` value.
|
||||
If the `key` does not exist, it inserts the given `default` value.
|
||||
|
||||
If there are duplicated keys, they are all removed and only the first one
|
||||
is updated.
|
||||
|
||||
The default value will not be passed through the update function.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Keyword.update([a: 1], :a, 13, &(&1 * 2))
|
||||
iex> Keyword.update([a: 1], :a, 13, fn existing_value -> existing_value * 2 end)
|
||||
[a: 2]
|
||||
iex> Keyword.update([a: 1, a: 2], :a, 13, &(&1 * 2))
|
||||
|
||||
iex> Keyword.update([a: 1, a: 2], :a, 13, fn existing_value -> existing_value * 2 end)
|
||||
[a: 2]
|
||||
iex> Keyword.update([a: 1], :b, 11, &(&1 * 2))
|
||||
|
||||
iex> Keyword.update([a: 1], :b, 11, fn existing_value -> existing_value * 2 end)
|
||||
[a: 1, b: 11]
|
||||
|
||||
"""
|
||||
@spec update(t, key, value, (value -> value)) :: t
|
||||
def update(keywords, key, initial, fun)
|
||||
@spec update(t, key, default :: value, (existing_value :: value -> updated_value :: value)) :: t
|
||||
def update(keywords, key, default, fun)
|
||||
|
||||
def update([{key, value} | keywords], key, _initial, fun) do
|
||||
def update([{key, value} | keywords], key, _default, fun) do
|
||||
[{key, fun.(value)} | delete(keywords, key)]
|
||||
end
|
||||
|
||||
def update([{_, _} = e | keywords], key, initial, fun) do
|
||||
[e | update(keywords, key, initial, fun)]
|
||||
def update([{_, _} = e | keywords], key, default, fun) do
|
||||
[e | update(keywords, key, default, fun)]
|
||||
end
|
||||
|
||||
def update([], key, initial, _fun) when is_atom(key) do
|
||||
[{key, initial}]
|
||||
def update([], key, default, _fun) when is_atom(key) do
|
||||
[{key, default}]
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -974,7 +1039,7 @@ defmodule Keyword do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the first value for `key` and removes all associated antries in the keyword list,
|
||||
Returns the first value for `key` and removes all associated entries in the keyword list,
|
||||
raising if `key` is not present.
|
||||
|
||||
This function behaves like `pop/3`, but raises in cases the `key` is not present in the
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
defmodule List do
|
||||
@moduledoc """
|
||||
Linked lists hold zero, one, or more elements in the choosen order.
|
||||
Linked lists hold zero, one, or more elements in the chosen order.
|
||||
|
||||
Lists in Elixir are specified between square brackets:
|
||||
|
||||
@@ -897,7 +897,7 @@ defmodule List do
|
||||
* integers representing Unicode code points
|
||||
* a list containing one of these three elements
|
||||
|
||||
Notice that this function expects a list of integers representing
|
||||
Note that this function expects a list of integers representing
|
||||
Unicode code points. If you have a list of bytes, you must instead use
|
||||
the [`:binary` module](http://www.erlang.org/doc/man/binary.html).
|
||||
|
||||
@@ -952,7 +952,7 @@ defmodule List do
|
||||
Converts a list of integers representing Unicode code points, lists or
|
||||
strings into a charlist.
|
||||
|
||||
Notice that this function expects a list of integers representing
|
||||
Note that this function expects a list of integers representing
|
||||
Unicode code points. If you have a list of bytes, you must instead use
|
||||
the [`:binary` module](http://www.erlang.org/doc/man/binary.html).
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ defprotocol List.Chars do
|
||||
The `List.Chars` protocol is responsible for
|
||||
converting a structure to a charlist (only if applicable).
|
||||
|
||||
The only function required to be implemented is
|
||||
The only function that must be implemented is
|
||||
`to_charlist/1` which does the conversion.
|
||||
|
||||
The `to_charlist/1` function automatically imported
|
||||
|
||||
+23
-17
@@ -14,7 +14,7 @@ defmodule Macro do
|
||||
end
|
||||
|
||||
def fun_inspect(value) do
|
||||
IO.inpect(value)
|
||||
IO.inspect(value)
|
||||
value
|
||||
end
|
||||
end
|
||||
@@ -32,7 +32,7 @@ defmodule Macro do
|
||||
#=> 1
|
||||
|
||||
So far they behave the same, as we are passing an integer as argument.
|
||||
But what happens when we pass an expresion:
|
||||
But what happens when we pass an expression:
|
||||
|
||||
macro_inspect(1 + 2)
|
||||
#=> {:+, [line: 3], [1, 2]}
|
||||
@@ -163,8 +163,9 @@ defmodule Macro do
|
||||
with parens. The `:closing` does not delimit the end of expression if
|
||||
there are `:do` and `:end` metadata (when `:token_metadata` is true)
|
||||
* `:column` - the column number of the AST node (when `:columns` is true)
|
||||
* `:delimiter` - contains the opening delimiter for sigils, strings, atoms,
|
||||
and charlists as a string (such as `"{"`, `"/"`, `":"`, and the like)
|
||||
* `:delimiter` - contains the opening delimiter for sigils, strings,
|
||||
and charlists as a string (such as `"{"`, `"/"`, `"'"`, and the like)
|
||||
* `:format` - set to `:keyword` when an atom is defined as a keyword
|
||||
* `:do` - contains metadata about the `do` location in a function call with
|
||||
`do/end` blocks (when `:token_metadata` is true)
|
||||
* `:end` - contains metadata about the `end` location in a function call with
|
||||
@@ -172,6 +173,7 @@ defmodule Macro do
|
||||
* `:end_of_expression` - denotes when the end of expression effectively
|
||||
happens. Available for all expressions except the last one inside a
|
||||
`__block__` (when `:token_metadata` is true)
|
||||
* `:indentation` - indentation of a sigil heredoc
|
||||
|
||||
The following metadata keys are private:
|
||||
|
||||
@@ -531,7 +533,7 @@ defmodule Macro do
|
||||
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 byte code, because the AST contains metadata,
|
||||
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.
|
||||
@@ -574,7 +576,7 @@ defmodule Macro do
|
||||
|
||||
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 even capable of
|
||||
not have been compiled. This function is also capable of
|
||||
expanding structs defined under the module being compiled.
|
||||
|
||||
It will raise `CompileError` if the struct is not available.
|
||||
@@ -894,13 +896,13 @@ defmodule Macro do
|
||||
end
|
||||
|
||||
# All other calls
|
||||
def to_string({target, meta, []} = ast, fun) do
|
||||
target = call_to_string(target, fun)
|
||||
def to_string({{:., _, [left, _]} = target, meta, []} = ast, fun) do
|
||||
to_string = call_to_string(target, fun)
|
||||
|
||||
if meta[:no_parens] do
|
||||
fun.(ast, target)
|
||||
if is_tuple(left) && meta[:no_parens] do
|
||||
fun.(ast, to_string)
|
||||
else
|
||||
fun.(ast, target <> "()")
|
||||
fun.(ast, to_string <> "()")
|
||||
end
|
||||
end
|
||||
|
||||
@@ -961,10 +963,14 @@ defmodule Macro do
|
||||
Kernel.inspect(value, limit: :infinity, printable_limit: :infinity)
|
||||
end
|
||||
|
||||
defp bitpart_to_string({:"::", _, [left, right]} = ast, fun) do
|
||||
defp bitpart_to_string({:"::", meta, [left, right]} = ast, fun) do
|
||||
result =
|
||||
op_to_string(left, fun, :"::", :left) <>
|
||||
"::" <> bitmods_to_string(right, fun, :"::", :right)
|
||||
if meta[:inferred_bitstring_spec] do
|
||||
to_string(left, fun)
|
||||
else
|
||||
op_to_string(left, fun, :"::", :left) <>
|
||||
"::" <> bitmods_to_string(right, fun, :"::", :right)
|
||||
end
|
||||
|
||||
fun.(ast, result)
|
||||
end
|
||||
@@ -1021,11 +1027,11 @@ defmodule Macro do
|
||||
|
||||
binary when is_binary(binary) ->
|
||||
binary = inspect_no_limit(binary)
|
||||
binary_part(binary, 1, byte_size(binary) - 2)
|
||||
binary = binary_part(binary, 1, byte_size(binary) - 2)
|
||||
escape_sigil(binary, left)
|
||||
end)
|
||||
|
||||
escaped = escape_sigil(parts, left)
|
||||
<<left::binary, escaped::binary, right::binary>>
|
||||
<<left::binary, parts::binary, right::binary>>
|
||||
end
|
||||
|
||||
defp escape_sigil(parts, "("), do: String.replace(parts, ")", ~S"\)")
|
||||
|
||||
+68
-47
@@ -37,15 +37,19 @@ defmodule Map do
|
||||
iex> map["non_existing_key"]
|
||||
nil
|
||||
|
||||
For accessing atom keys, one may also `map.key`. Note that while `map[key]` will
|
||||
return `nil` if `map` doesn't contain `key`, `map.key` will raise if `map` doesn't
|
||||
contain the key `:key`.
|
||||
To access atom keys, one may also use the `map.key` notation. Note that `map.key`
|
||||
will raise a `KeyError` if the `map` doesn't contain the key `:key`, compared to
|
||||
`map[:key]`, that would return `nil`.
|
||||
|
||||
iex> map = %{foo: "bar", baz: "bong"}
|
||||
iex> map.foo
|
||||
"bar"
|
||||
iex> map.non_existing_key
|
||||
** (KeyError) key :non_existing_key not found in: %{baz: "bong", foo: "bar"}
|
||||
map = %{foo: "bar", baz: "bong"}
|
||||
map.foo
|
||||
#=> "bar"
|
||||
map.non_existing_key
|
||||
#=> ** (KeyError) key :non_existing_key not found in: %{baz: "bong", foo: "bar"}
|
||||
|
||||
> Note: do not add parens when accessing fields, such as in `data.key()`.
|
||||
> If parenthesis are used, Elixir will consider it to be a function call
|
||||
> on `data`, which would be expected to be an atom.
|
||||
|
||||
The two syntaxes for accessing keys reveal the dual nature of maps. The `map[key]`
|
||||
syntax is used for dynamically created maps that may have any key, of any type.
|
||||
@@ -63,8 +67,10 @@ defmodule Map do
|
||||
iex> %{a: a} = %{:a => 1, "b" => 2, [:c, :e, :e] => 3}
|
||||
iex> a
|
||||
1
|
||||
iex> %{:c => 3} = %{:a => 1, 2 => :b}
|
||||
** (MatchError) no match of right hand side value: %{2 => :b, :a => 1}
|
||||
|
||||
But this will raise a `MatchError` exception:
|
||||
|
||||
%{:c => 3} = %{:a => 1, 2 => :b}
|
||||
|
||||
Variables can be used as map keys both when writing map literals as well as
|
||||
when matching:
|
||||
@@ -82,8 +88,10 @@ defmodule Map do
|
||||
iex> map = %{one: 1, two: 2}
|
||||
iex> %{map | one: "one"}
|
||||
%{one: "one", two: 2}
|
||||
iex> %{map | three: 3}
|
||||
** (KeyError) key :three not found
|
||||
|
||||
When a key that does not exist in the map is updated a `KeyError` exception will be raised:
|
||||
|
||||
%{map | three: 3}
|
||||
|
||||
The functions in this module that need to find a specific key work in logarithmic time.
|
||||
This means that the time it takes to find keys grows as the map grows, but it's not
|
||||
@@ -236,8 +244,8 @@ defmodule Map do
|
||||
@doc """
|
||||
Fetches the value for a specific `key` in the given `map`.
|
||||
|
||||
If `map` contains the given `key` with value `value`, then `{:ok, value}` is
|
||||
returned. If `map` doesn't contain `key`, `:error` is returned.
|
||||
If `map` contains the given `key` then its value is returned in the shape of `{:ok, value}`.
|
||||
If `map` doesn't contain `key`, `:error` is returned.
|
||||
|
||||
Inlined by the compiler.
|
||||
|
||||
@@ -256,7 +264,7 @@ defmodule Map do
|
||||
Fetches the value for a specific `key` in the given `map`, erroring out if
|
||||
`map` doesn't contain `key`.
|
||||
|
||||
If `map` contains the given `key`, the corresponding value is returned. If
|
||||
If `map` contains `key`, the corresponding value is returned. If
|
||||
`map` doesn't contain `key`, a `KeyError` exception is raised.
|
||||
|
||||
Inlined by the compiler.
|
||||
@@ -265,8 +273,6 @@ defmodule Map do
|
||||
|
||||
iex> Map.fetch!(%{a: 1}, :a)
|
||||
1
|
||||
iex> Map.fetch!(%{a: 1}, :b)
|
||||
** (KeyError) key :b not found in: %{a: 1}
|
||||
|
||||
"""
|
||||
@spec fetch!(map, key) :: value
|
||||
@@ -300,8 +306,20 @@ defmodule Map do
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
@deprecated "Use Map.fetch/2 + Map.put/3 instead"
|
||||
@doc """
|
||||
Puts a value under `key` only if the `key` already exists in `map`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Map.replace(%{a: 1, b: 2}, :a, 3)
|
||||
%{a: 3, b: 2}
|
||||
|
||||
iex> Map.replace(%{a: 1}, :b, 2)
|
||||
%{a: 1}
|
||||
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
@spec replace(map, key, value) :: map
|
||||
def replace(map, key, value) do
|
||||
case map do
|
||||
%{^key => _value} ->
|
||||
@@ -316,8 +334,7 @@ defmodule Map do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Alters the value stored under `key` to `value`, but only
|
||||
if the entry `key` already exists in `map`.
|
||||
Puts a value under `key` only if the `key` already exists in `map`.
|
||||
|
||||
If `key` is not present in `map`, a `KeyError` exception is raised.
|
||||
|
||||
@@ -422,7 +439,7 @@ defmodule Map do
|
||||
@doc """
|
||||
Gets the value for a specific `key` in `map`.
|
||||
|
||||
If `key` is present in `map` with value `value`, then `value` is
|
||||
If `key` is present in `map` then its value `value` is
|
||||
returned. Otherwise, `default` is returned.
|
||||
|
||||
If `default` is not provided, `nil` is used.
|
||||
@@ -456,7 +473,7 @@ defmodule Map do
|
||||
@doc """
|
||||
Gets the value for a specific `key` in `map`.
|
||||
|
||||
If `key` is present in `map` with value `value`, then `value` is
|
||||
If `key` is present in `map` then its value `value` is
|
||||
returned. Otherwise, `fun` is evaluated and its result is returned.
|
||||
|
||||
This is useful if the default value is very expensive to calculate or
|
||||
@@ -584,38 +601,39 @@ defmodule Map do
|
||||
@doc """
|
||||
Updates the `key` in `map` with the given function.
|
||||
|
||||
If `key` is present in `map` with value `value`, `fun` is invoked with
|
||||
argument `value` and its result is used as the new value of `key`. If `key` is
|
||||
not present in `map`, `initial` is inserted as the value of `key`. The initial
|
||||
If `key` is present in `map` then the existing value is passed to `fun` and its result is
|
||||
used as the updated value of `key`. If `key` is
|
||||
not present in `map`, `default` is inserted as the value of `key`. The default
|
||||
value will not be passed through the update function.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Map.update(%{a: 1}, :a, 13, &(&1 * 2))
|
||||
iex> Map.update(%{a: 1}, :a, 13, fn existing_value -> existing_value * 2 end)
|
||||
%{a: 2}
|
||||
iex> Map.update(%{a: 1}, :b, 11, &(&1 * 2))
|
||||
iex> Map.update(%{a: 1}, :b, 11, fn existing_value -> existing_value * 2 end)
|
||||
%{a: 1, b: 11}
|
||||
|
||||
"""
|
||||
@spec update(map, key, value, (value -> value)) :: map
|
||||
def update(map, key, initial, fun) when is_function(fun, 1) do
|
||||
@spec update(map, key, default :: value, (existing_value :: value -> updated_value :: value)) ::
|
||||
map
|
||||
def update(map, key, default, fun) when is_function(fun, 1) do
|
||||
case map do
|
||||
%{^key => value} ->
|
||||
put(map, key, fun.(value))
|
||||
|
||||
%{} ->
|
||||
put(map, key, initial)
|
||||
put(map, key, default)
|
||||
|
||||
other ->
|
||||
:erlang.error({:badmap, other}, [map, key, initial, fun])
|
||||
:erlang.error({:badmap, other}, [map, key, default, fun])
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns and removes the value associated with `key` in `map`.
|
||||
Removes the value associated with `key` in `map` and returns the value and the updated map.
|
||||
|
||||
If `key` is present in `map` with value `value`, `{value, new_map}` is
|
||||
returned where `new_map` is the result of removing `key` from `map`. If `key`
|
||||
If `key` is present in `map`, it returns `{value, new_map}` where `value` is the value of
|
||||
the key and `new_map` is the result of removing `key` from `map`. If `key`
|
||||
is not present in `map`, `{default, map}` is returned.
|
||||
|
||||
## Examples
|
||||
@@ -628,7 +646,7 @@ defmodule Map do
|
||||
{3, %{a: 1}}
|
||||
|
||||
"""
|
||||
@spec pop(map, key, value) :: {value, map}
|
||||
@spec pop(map, key, value) :: {value, new_map :: map}
|
||||
def pop(map, key, default \\ nil) do
|
||||
case :maps.take(key, map) do
|
||||
{_, _} = tuple -> tuple
|
||||
@@ -664,8 +682,8 @@ defmodule Map do
|
||||
@doc """
|
||||
Lazily returns and removes the value associated with `key` in `map`.
|
||||
|
||||
If `key` is present in `map` with value `value`, `{value, new_map}` is
|
||||
returned where `new_map` is the result of removing `key` from `map`. If `key`
|
||||
If `key` is present in `map`, it returns `{value, new_map}` where `value` is the value of
|
||||
the key and `new_map` is the result of removing `key` from `map`. If `key`
|
||||
is not present in `map`, `{fun_result, map}` is returned, where `fun_result`
|
||||
is the result of applying `fun`.
|
||||
|
||||
@@ -781,8 +799,8 @@ defmodule Map do
|
||||
@doc """
|
||||
Updates `key` with the given function.
|
||||
|
||||
If `key` is present in `map` with value `value`, `fun` is invoked with
|
||||
argument `value` and its result is used as the new value of `key`. If `key` is
|
||||
If `key` is present in `map` then the existing value is passed to `fun` and its result is
|
||||
used as the updated value of `key`. If `key` is
|
||||
not present in `map`, a `KeyError` exception is raised.
|
||||
|
||||
## Examples
|
||||
@@ -794,7 +812,7 @@ defmodule Map do
|
||||
** (KeyError) key :b not found in: %{a: 1}
|
||||
|
||||
"""
|
||||
@spec update!(map, key, (value -> value)) :: map
|
||||
@spec update!(map, key, (existing_value :: value -> updated_value :: value)) :: map
|
||||
def update!(map, key, fun) when is_function(fun, 1) do
|
||||
value = fetch!(map, key)
|
||||
put(map, key, fun.(value))
|
||||
@@ -804,13 +822,13 @@ defmodule Map do
|
||||
Gets the value from `key` and updates it, all in one pass.
|
||||
|
||||
`fun` is called with the current value under `key` in `map` (or `nil` if `key`
|
||||
is not present in `map`) and must return a two-element tuple: the "get" value
|
||||
is not present in `map`) and must return a two-element tuple: the current value
|
||||
(the retrieved value, which can be operated on before being returned) and the
|
||||
new value to be stored under `key` in the resulting new map. `fun` may also
|
||||
return `:pop`, which means the current value shall be removed from `map` and
|
||||
returned (making this function behave like `Map.pop(map, key)`).
|
||||
|
||||
The returned value is a tuple with the "get" value returned by
|
||||
The returned value is a two-element tuple with the current value returned by
|
||||
`fun` and a new map with the updated value under `key`.
|
||||
|
||||
## Examples
|
||||
@@ -832,7 +850,9 @@ defmodule Map do
|
||||
{nil, %{a: 1}}
|
||||
|
||||
"""
|
||||
@spec get_and_update(map, key, (value -> {get, value} | :pop)) :: {get, map} when get: term
|
||||
@spec get_and_update(map, key, (value -> {current_value, new_value :: value} | :pop)) ::
|
||||
{current_value, map}
|
||||
when current_value: value
|
||||
def get_and_update(map, key, fun) when is_function(fun, 1) do
|
||||
current = get(map, key)
|
||||
|
||||
@@ -849,7 +869,7 @@ defmodule Map do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets the value from `key` and updates it. Raises if there is no `key`.
|
||||
Gets the value from `key` and updates it, all in one pass. Raises if there is no `key`.
|
||||
|
||||
Behaves exactly like `get_and_update/3`, but raises a `KeyError` exception if
|
||||
`key` is not present in `map`.
|
||||
@@ -872,8 +892,9 @@ defmodule Map do
|
||||
{1, %{}}
|
||||
|
||||
"""
|
||||
@spec get_and_update!(map, key, (value -> {get, value} | :pop)) :: {get, map}
|
||||
when get: term
|
||||
@spec get_and_update!(map, key, (value -> {current_value, new_value :: value} | :pop)) ::
|
||||
{current_value, map}
|
||||
when current_value: value
|
||||
def get_and_update!(map, key, fun) when is_function(fun, 1) do
|
||||
value = fetch!(map, key)
|
||||
|
||||
|
||||
@@ -238,7 +238,7 @@ defmodule MapSet do
|
||||
Map.equal?(map1, map2)
|
||||
end
|
||||
|
||||
# Elixir v1.5 change the map representation, so on
|
||||
# Elixir v1.5 changed the map representation, so on
|
||||
# version mismatch we need to compare the keys directly.
|
||||
def equal?(%MapSet{map: map1}, %MapSet{map: map2}) do
|
||||
map_size(map1) == map_size(map2) and all_in?(map1, map2)
|
||||
|
||||
+41
-21
@@ -57,7 +57,7 @@ defmodule Module do
|
||||
one of the callbacks, a warning will be raised.
|
||||
|
||||
For detailed documentation, see the
|
||||
[behaviour typespec documentation](typespecs.html#behaviours).
|
||||
[behaviour typespec documentation](typespecs.md#behaviours).
|
||||
|
||||
### `@impl`
|
||||
|
||||
@@ -248,8 +248,13 @@ defmodule Module do
|
||||
attribute allows the module to annotate which external resources
|
||||
have been used.
|
||||
|
||||
Tools like Mix may use this information to ensure the module is
|
||||
recompiled in case any of the external resources change.
|
||||
Tools may use this information to ensure the module is recompiled
|
||||
in case any of the external resources change, see for example:
|
||||
[`mix compile.elixir`](https://hexdocs.pm/mix/Mix.Tasks.Compile.Elixir.html).
|
||||
|
||||
If the external resource does not exist, the module still has
|
||||
a dependency on it, causing the module be recompiled as soon
|
||||
as the file is added.
|
||||
|
||||
### `@file`
|
||||
|
||||
@@ -345,6 +350,8 @@ defmodule Module do
|
||||
behaviour callbacks are optional
|
||||
* `@impl` - declares an implementation of a callback function or macro
|
||||
|
||||
For detailed documentation, see the [typespec documentation](typespecs.md).
|
||||
|
||||
### Custom attributes
|
||||
|
||||
In addition to the built-in attributes outlined above, custom attributes may
|
||||
@@ -499,8 +506,6 @@ defmodule Module do
|
||||
`@compile {:no_warn_undefined, {Mod, fun, arity}}` - does not warn if
|
||||
the given module or the given `Mod.fun/arity` are not defined
|
||||
|
||||
You can see a handful more options used by the Erlang compiler in
|
||||
the documentation for the [`:compile` module](http://www.erlang.org/doc/man/compile.html).
|
||||
'''
|
||||
|
||||
@typep definition :: {atom, arity}
|
||||
@@ -625,7 +630,7 @@ defmodule Module do
|
||||
|
||||
It returns a tuple of shape `{:module, module, binary, term}`
|
||||
where `module` is the module name, `binary` is the module
|
||||
byte code and `term` is the result of the last expression in
|
||||
bytecode and `term` is the result of the last expression in
|
||||
`quoted`.
|
||||
|
||||
Similar to `Kernel.defmodule/2`, the binary will only be
|
||||
@@ -1049,11 +1054,11 @@ defmodule Module do
|
||||
|
||||
"""
|
||||
@spec definitions_in(module, def_kind) :: [definition]
|
||||
def definitions_in(module, def_kind)
|
||||
when is_atom(module) and def_kind in [:def, :defp, :defmacro, :defmacrop] do
|
||||
def definitions_in(module, kind)
|
||||
when is_atom(module) and kind in [:def, :defp, :defmacro, :defmacrop] do
|
||||
assert_not_compiled!(__ENV__.function, module, @extra_error_msg_definitions_in)
|
||||
{set, _} = data_tables_for(module)
|
||||
:lists.concat(:ets.match(set, {{:def, :"$1"}, def_kind, :_, :_, :_, :_}))
|
||||
:ets.select(set, [{{{:def, :"$1"}, kind, :_, :_, :_, :_}, [], [:"$1"]}])
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -1397,7 +1402,7 @@ defmodule Module do
|
||||
if doc, do: {:error, :private_doc}, else: :ok
|
||||
else
|
||||
{set, _bag} = data_tables_for(module)
|
||||
compile_doc(set, line, kind, name, arity, signature, nil, doc, %{}, __ENV__, false)
|
||||
compile_doc(set, nil, line, kind, name, arity, signature, nil, doc, %{}, __ENV__, false)
|
||||
:ok
|
||||
end
|
||||
end
|
||||
@@ -1410,16 +1415,17 @@ defmodule Module do
|
||||
{set, bag} = data_tables_for(module)
|
||||
{arity, defaults} = args_count(args, 0, 0)
|
||||
|
||||
impl = compile_impl(set, bag, name, env, kind, arity, defaults)
|
||||
context = Keyword.get(:ets.lookup_element(set, {:def, {name, arity}}, 3), :context)
|
||||
impl = compile_impl(set, bag, context, name, env, kind, arity, defaults)
|
||||
doc_meta = compile_doc_meta(set, bag, name, arity, defaults)
|
||||
|
||||
{line, doc} = get_doc_info(set, env)
|
||||
compile_doc(set, line, kind, name, arity, args, body, doc, doc_meta, env, impl)
|
||||
compile_doc(set, context, line, kind, name, arity, args, body, doc, doc_meta, env, impl)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
defp compile_doc(_table, line, kind, name, arity, _args, _body, doc, _doc_meta, env, _impl)
|
||||
defp compile_doc(_table, _ctx, line, kind, name, arity, _args, _body, doc, _meta, env, _impl)
|
||||
when kind in [:defp, :defmacrop] do
|
||||
if doc do
|
||||
message =
|
||||
@@ -1430,21 +1436,37 @@ defmodule Module do
|
||||
end
|
||||
end
|
||||
|
||||
defp compile_doc(table, line, kind, name, arity, args, _body, doc, doc_meta, env, impl) do
|
||||
defp compile_doc(table, ctx, line, kind, name, arity, args, body, doc, doc_meta, env, impl) do
|
||||
key = {doc_key(kind), name, arity}
|
||||
signature = build_signature(args, env)
|
||||
|
||||
case :ets.lookup(table, key) do
|
||||
[] ->
|
||||
doc = if is_nil(doc) && impl, do: false, else: doc
|
||||
:ets.insert(table, {key, line, signature, doc, doc_meta})
|
||||
:ets.insert(table, {key, ctx, line, signature, doc, doc_meta})
|
||||
|
||||
[{_, current_ctx, current_line, current_sign, current_doc, current_doc_meta}] ->
|
||||
if is_binary(current_doc) and is_binary(doc) and body != nil and is_nil(current_ctx) do
|
||||
message = ~s'''
|
||||
redefining @doc attribute previously set at line #{current_line}.
|
||||
|
||||
Please remove the duplicate docs. If instead you want to override a \
|
||||
previously defined @doc, attach the @doc attribute to a function head:
|
||||
|
||||
@doc """
|
||||
new docs
|
||||
"""
|
||||
def #{name}(...)
|
||||
'''
|
||||
|
||||
IO.warn(message, Macro.Env.stacktrace(%{env | line: line}))
|
||||
end
|
||||
|
||||
[{_, current_line, current_sign, current_doc, current_doc_meta}] ->
|
||||
signature = merge_signatures(current_sign, signature, 1)
|
||||
doc = if is_nil(doc), do: current_doc, else: doc
|
||||
doc = if is_nil(doc) && impl, do: false, else: doc
|
||||
doc_meta = Map.merge(current_doc_meta, doc_meta)
|
||||
:ets.insert(table, {key, current_line, signature, doc, doc_meta})
|
||||
:ets.insert(table, {key, ctx, current_line, signature, doc, doc_meta})
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1492,14 +1514,12 @@ defmodule Module do
|
||||
defp deprecated_reason(name, arity, reason),
|
||||
do: {:deprecated, {{name, arity}, reason}}
|
||||
|
||||
defp compile_impl(set, bag, name, env, kind, arity, defaults) do
|
||||
defp compile_impl(set, bag, context, name, env, kind, arity, defaults) do
|
||||
%{line: line, file: file} = env
|
||||
|
||||
case :ets.take(set, :impl) do
|
||||
[{:impl, value, _}] ->
|
||||
pair = {name, arity}
|
||||
meta = :ets.lookup_element(set, {:def, pair}, 3)
|
||||
impl = {pair, Keyword.get(meta, :context), defaults, kind, line, file, value}
|
||||
impl = {{name, arity}, context, defaults, kind, line, file, value}
|
||||
:ets.insert(bag, {:impls, impl})
|
||||
value
|
||||
|
||||
|
||||
@@ -1,315 +0,0 @@
|
||||
defmodule Module.Checker do
|
||||
alias Module.ParallelChecker
|
||||
|
||||
@moduledoc false
|
||||
|
||||
def verify(module, cache) do
|
||||
case prepare_module(module) do
|
||||
{:ok, map} ->
|
||||
undefined_and_deprecation_warnings = undefined_and_deprecation_warnings(map, cache)
|
||||
infer_warnings = infer_definitions(map)
|
||||
warnings = infer_warnings ++ undefined_and_deprecation_warnings
|
||||
emit_warnings(warnings)
|
||||
|
||||
:error ->
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
defp prepare_module({module, module_map}) when is_map(module_map) do
|
||||
{:ok,
|
||||
%{
|
||||
module: module,
|
||||
file: module_map.file,
|
||||
definitions: module_map.definitions,
|
||||
deprecated: module_map.deprecated,
|
||||
no_warn_undefined: no_warn_undefined(module_map.compile_opts)
|
||||
}}
|
||||
end
|
||||
|
||||
defp prepare_module({module, binary}) when is_binary(binary) do
|
||||
with {:ok, debug_info} <- debug_info(module, binary),
|
||||
{:ok, checker_info} <- checker_chunk(binary) do
|
||||
{:ok,
|
||||
%{
|
||||
module: module,
|
||||
file: debug_info.file,
|
||||
definitions: debug_info.definitions,
|
||||
deprecated: checker_info.deprecated,
|
||||
no_warn_undefined: checker_info.no_warn_undefined
|
||||
}}
|
||||
end
|
||||
end
|
||||
|
||||
defp no_warn_undefined(compile_opts) do
|
||||
for(
|
||||
{:no_warn_undefined, values} <- compile_opts,
|
||||
value <- List.wrap(values),
|
||||
do: value
|
||||
)
|
||||
end
|
||||
|
||||
defp debug_info(module, binary) do
|
||||
with {:ok, {_, [debug_info: chunk]}} <- :beam_lib.chunks(binary, [:debug_info]),
|
||||
{:debug_info_v1, backend, data} <- chunk,
|
||||
{:ok, info} <- backend.debug_info(:elixir_v1, module, data, []) do
|
||||
{:ok, %{definitions: info.definitions, file: info.relative_file}}
|
||||
else
|
||||
_ -> :error
|
||||
end
|
||||
end
|
||||
|
||||
defp checker_chunk(binary) do
|
||||
with {:ok, {_, [{'ExCk', chunk}]}} <- :beam_lib.chunks(binary, ['ExCk']),
|
||||
{:elixir_checker_v1, contents} <- :erlang.binary_to_term(chunk) do
|
||||
deprecated = Enum.map(contents.exports, fn {fun, map} -> {fun, map.deprecated_reason} end)
|
||||
{:ok, %{deprecated: deprecated, no_warn_undefined: contents.no_warn_undefined}}
|
||||
else
|
||||
_ -> :error
|
||||
end
|
||||
end
|
||||
|
||||
defp infer_definitions(map) do
|
||||
results = Module.Types.infer_definitions(map.file, map.module, map.definitions)
|
||||
Enum.flat_map(results, fn {_function, reasons} -> reasons end)
|
||||
end
|
||||
|
||||
defp undefined_and_deprecation_warnings(map, cache) do
|
||||
state = %{
|
||||
cache: cache,
|
||||
file: map.file,
|
||||
module: map.module,
|
||||
no_warn_undefined: merge_no_warn_undefined(map),
|
||||
function: nil,
|
||||
warnings: []
|
||||
}
|
||||
|
||||
state = check_definitions(map.definitions, state)
|
||||
|
||||
state.warnings
|
||||
|> merge_warnings()
|
||||
|> sort_warnings()
|
||||
end
|
||||
|
||||
defp merge_no_warn_undefined(map) do
|
||||
case Code.get_compiler_option(:no_warn_undefined) do
|
||||
:all ->
|
||||
:all
|
||||
|
||||
list when is_list(list) ->
|
||||
map.no_warn_undefined ++ list
|
||||
end
|
||||
end
|
||||
|
||||
defp check_definitions(definitions, state) do
|
||||
Enum.reduce(definitions, state, &check_definition/2)
|
||||
end
|
||||
|
||||
defp check_definition({function, _kind, meta, clauses}, state) do
|
||||
with_file_meta(%{state | function: function}, meta, fn state ->
|
||||
Enum.reduce(clauses, state, &check_clause/2)
|
||||
end)
|
||||
end
|
||||
|
||||
defp with_file_meta(%{file: original_file} = state, meta, fun) do
|
||||
case Keyword.fetch(meta, :file) do
|
||||
{:ok, {meta_file, _}} ->
|
||||
state = fun.(%{state | file: meta_file})
|
||||
%{state | file: original_file}
|
||||
|
||||
:error ->
|
||||
fun.(state)
|
||||
end
|
||||
end
|
||||
|
||||
defp check_clause({_meta, args, _guards, body}, state) do
|
||||
state = check_expr(args, state)
|
||||
check_expr(body, state)
|
||||
end
|
||||
|
||||
# &Mod.fun/arity
|
||||
defp check_expr({:&, meta, [{:/, _, [{{:., _, [module, fun]}, _, []}, arity]}]}, state)
|
||||
when is_atom(module) and is_atom(fun) do
|
||||
check_remote(module, fun, arity, meta, state)
|
||||
end
|
||||
|
||||
# Mod.fun(...)
|
||||
defp check_expr({{:., meta, [module, fun]}, _, args}, state)
|
||||
when is_atom(module) and is_atom(fun) do
|
||||
state = check_remote(module, fun, length(args), meta, state)
|
||||
check_expr(args, state)
|
||||
end
|
||||
|
||||
# %Module{...}
|
||||
defp check_expr({:%, meta, [module, {:%{}, _meta, args}]}, state)
|
||||
when is_atom(module) and is_list(args) do
|
||||
state = check_remote(module, :__struct__, 0, meta, state)
|
||||
check_expr(args, state)
|
||||
end
|
||||
|
||||
# Function call
|
||||
defp check_expr({left, _meta, right}, state) when is_list(right) do
|
||||
state = check_expr(right, state)
|
||||
check_expr(left, state)
|
||||
end
|
||||
|
||||
# {x, y}
|
||||
defp check_expr({left, right}, state) do
|
||||
state = check_expr(right, state)
|
||||
check_expr(left, state)
|
||||
end
|
||||
|
||||
# [...]
|
||||
defp check_expr(list, state) when is_list(list) do
|
||||
Enum.reduce(list, state, &check_expr/2)
|
||||
end
|
||||
|
||||
defp check_expr(_other, state) do
|
||||
state
|
||||
end
|
||||
|
||||
defp check_remote(module, fun, arity, meta, state) do
|
||||
# TODO: In the future we may want to warn for modules defined
|
||||
# in the local context
|
||||
if Keyword.get(meta, :context_module, false) and state.module != module do
|
||||
state
|
||||
else
|
||||
ParallelChecker.preload_module(state.cache, module)
|
||||
check_export(module, fun, arity, meta, state)
|
||||
end
|
||||
end
|
||||
|
||||
defp check_export(module, fun, arity, meta, state) do
|
||||
case ParallelChecker.fetch_export(state.cache, module, fun, arity) do
|
||||
{:ok, :def, reason} ->
|
||||
check_deprecated(module, fun, arity, reason, meta, state)
|
||||
|
||||
{:ok, :defmacro, reason} ->
|
||||
state = warn(meta, state, {:unrequired_module, module, fun, arity})
|
||||
check_deprecated(module, fun, arity, reason, meta, state)
|
||||
|
||||
{:error, :module} ->
|
||||
if warn_undefined?(module, fun, arity, state) do
|
||||
warn(meta, state, {:undefined_module, module, fun, arity})
|
||||
else
|
||||
state
|
||||
end
|
||||
|
||||
{:error, :function} ->
|
||||
if warn_undefined?(module, fun, arity, state) do
|
||||
exports = ParallelChecker.all_exports(state.cache, module)
|
||||
warn(meta, state, {:undefined_function, module, fun, arity, exports})
|
||||
else
|
||||
state
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp check_deprecated(module, fun, arity, reason, meta, state) do
|
||||
if reason do
|
||||
warn(meta, state, {:deprecated, module, fun, arity, reason})
|
||||
else
|
||||
state
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: Do not warn inside guards
|
||||
# TODO: Properly handle protocols
|
||||
defp warn_undefined?(_module, :__impl__, 1, _state), do: false
|
||||
defp warn_undefined?(:erlang, :orelse, 2, _state), do: false
|
||||
defp warn_undefined?(:erlang, :andalso, 2, _state), do: false
|
||||
|
||||
defp warn_undefined?(_, _, _, %{no_warn_undefined: :all}) do
|
||||
false
|
||||
end
|
||||
|
||||
defp warn_undefined?(module, fun, arity, state) do
|
||||
not Enum.any?(state.no_warn_undefined, &(&1 == module or &1 == {module, fun, arity}))
|
||||
end
|
||||
|
||||
defp warn(meta, state, warning) do
|
||||
{fun, arity} = state.function
|
||||
location = {state.file, meta[:line], {state.module, fun, arity}}
|
||||
%{state | warnings: [{__MODULE__, warning, location} | state.warnings]}
|
||||
end
|
||||
|
||||
defp merge_warnings(warnings) do
|
||||
Enum.reduce(warnings, %{}, fn {module, warning, location}, acc ->
|
||||
locations = MapSet.new([location])
|
||||
Map.update(acc, {module, warning}, locations, &MapSet.put(&1, location))
|
||||
end)
|
||||
end
|
||||
|
||||
defp sort_warnings(warnings) do
|
||||
warnings
|
||||
|> Enum.map(fn {{module, warning}, locations} -> {module, warning, Enum.sort(locations)} end)
|
||||
|> Enum.sort()
|
||||
end
|
||||
|
||||
defp emit_warnings(warnings) do
|
||||
Enum.flat_map(warnings, fn {module, warning, locations} ->
|
||||
message = module.format_warning(warning)
|
||||
print_warning([message, ?\n, format_locations(locations)])
|
||||
|
||||
Enum.map(locations, fn {file, line, _mfa} ->
|
||||
{file, line, message}
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
def format_warning({:undefined_module, module, fun, arity}) do
|
||||
[
|
||||
Exception.format_mfa(module, fun, arity),
|
||||
" is undefined (module ",
|
||||
inspect(module),
|
||||
" is not available or is yet to be defined)"
|
||||
]
|
||||
end
|
||||
|
||||
def format_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
|
||||
|
||||
defp format_locations([location]) do
|
||||
format_location(location)
|
||||
end
|
||||
|
||||
defp format_locations(locations) do
|
||||
[
|
||||
"Found at #{length(locations)} locations:\n",
|
||||
Enum.map(locations, &format_location/1)
|
||||
]
|
||||
end
|
||||
|
||||
defp format_location({file, line, {module, fun, arity}}) do
|
||||
file = Path.relative_to_cwd(file)
|
||||
line = if line, do: [Integer.to_string(line), ": "], else: []
|
||||
mfa = Exception.format_mfa(module, fun, arity)
|
||||
[" ", file, ?:, line, mfa, ?\n]
|
||||
end
|
||||
|
||||
defp print_warning(message) do
|
||||
IO.puts(:stderr, [:elixir_errors.warning_prefix(), message])
|
||||
end
|
||||
end
|
||||
@@ -137,7 +137,7 @@ defmodule Module.LocalsTracker do
|
||||
end
|
||||
|
||||
defp reachable?(tuple, :defmacrop, reachable, reattached) do
|
||||
# All private micros are unreachable unless they have been
|
||||
# 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
|
||||
|
||||
@@ -10,17 +10,24 @@ defmodule Module.ParallelChecker do
|
||||
the modules and adds the ExCk chunk to the binaries. Returns the updated
|
||||
binaries and a list of warnings from the verification.
|
||||
"""
|
||||
@spec verify([{map(), binary()}], [{module(), binary()}], pos_integer()) :: [warning()]
|
||||
@spec verify([{map(), binary()}], [{module(), binary()}], pos_integer() | nil) :: [warning()]
|
||||
def verify(compiled_modules, runtime_binaries, schedulers \\ nil) do
|
||||
compiled_maps = Enum.map(compiled_modules, fn {map, _binary} -> {map.module, map} end)
|
||||
check_modules = compiled_maps ++ runtime_binaries
|
||||
|
||||
schedulers = schedulers || max(:erlang.system_info(:schedulers_online), 2)
|
||||
{:ok, server} = :gen_server.start_link(__MODULE__, [check_modules, self(), schedulers], [])
|
||||
preload_cache(get_ets(server), check_modules)
|
||||
start(server)
|
||||
case compiled_maps ++ runtime_binaries do
|
||||
[] ->
|
||||
[]
|
||||
|
||||
collect_results(length(check_modules), [])
|
||||
check_modules ->
|
||||
schedulers = schedulers || max(:erlang.system_info(:schedulers_online), 2)
|
||||
|
||||
{:ok, server} =
|
||||
:gen_server.start_link(__MODULE__, [check_modules, self(), schedulers], [])
|
||||
|
||||
preload_cache(get_ets(server), check_modules)
|
||||
start(server)
|
||||
collect_results(length(check_modules), [])
|
||||
end
|
||||
end
|
||||
|
||||
defp collect_results(0, warnings) do
|
||||
@@ -34,6 +41,14 @@ defmodule Module.ParallelChecker do
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Test cache.
|
||||
"""
|
||||
def test_cache do
|
||||
{:ok, pid} = :gen_server.start_link(__MODULE__, [[], self(), 1], [])
|
||||
{pid, get_ets(pid)}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Preloads a module into the cache. Call this function before any other
|
||||
cache lookups for the module.
|
||||
@@ -81,6 +96,116 @@ defmodule Module.ParallelChecker do
|
||||
|> Enum.sort()
|
||||
end
|
||||
|
||||
## Module checking
|
||||
|
||||
defp check_module(module, cache) do
|
||||
case extract_definitions(module) do
|
||||
{:ok, module, file, definitions, no_warn_undefined} ->
|
||||
Module.Types.warnings(module, file, definitions, no_warn_undefined, cache)
|
||||
|> group_warnings()
|
||||
|> emit_warnings()
|
||||
|
||||
:error ->
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
defp extract_definitions({module, module_map}) when is_map(module_map) do
|
||||
no_warn_undefined =
|
||||
module_map.compile_opts
|
||||
|> extract_no_warn_undefined()
|
||||
|> merge_compiler_no_warn_undefined()
|
||||
|
||||
{:ok, module, module_map.file, module_map.definitions, no_warn_undefined}
|
||||
end
|
||||
|
||||
defp extract_definitions({module, binary}) when is_binary(binary) do
|
||||
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
|
||||
extract_definitions({module, module_map})
|
||||
else
|
||||
_ -> :error
|
||||
end
|
||||
end
|
||||
|
||||
defp extract_no_warn_undefined(compile_opts) do
|
||||
for(
|
||||
{:no_warn_undefined, values} <- compile_opts,
|
||||
value <- List.wrap(values),
|
||||
do: value
|
||||
)
|
||||
end
|
||||
|
||||
defp merge_compiler_no_warn_undefined(no_warn_undefined) do
|
||||
case Code.get_compiler_option(:no_warn_undefined) do
|
||||
:all ->
|
||||
:all
|
||||
|
||||
list when is_list(list) ->
|
||||
no_warn_undefined ++ list
|
||||
end
|
||||
end
|
||||
|
||||
## Warning helpers
|
||||
|
||||
def 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()
|
||||
end
|
||||
|
||||
def emit_warnings(warnings) do
|
||||
Enum.flat_map(warnings, fn {module, warning, locations} ->
|
||||
message = module.format_warning(warning)
|
||||
print_warning([message, ?\n, format_locations(locations)])
|
||||
|
||||
Enum.map(locations, fn {file, line, _mfa} ->
|
||||
{file, line, message}
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
defp format_locations([location]) do
|
||||
format_location(location)
|
||||
end
|
||||
|
||||
defp format_locations(locations) do
|
||||
[
|
||||
"Found at #{length(locations)} locations:\n",
|
||||
Enum.map(locations, &format_location/1)
|
||||
]
|
||||
end
|
||||
|
||||
defp format_location({file, line, {module, fun, arity}}) do
|
||||
mfa = Exception.format_mfa(module, fun, arity)
|
||||
[format_file_line(file, line), ": ", mfa, ?\n]
|
||||
end
|
||||
|
||||
defp format_location({file, line, nil}) do
|
||||
[format_file_line(file, line), ?\n]
|
||||
end
|
||||
|
||||
defp format_location({file, line, module}) do
|
||||
[format_file_line(file, line), ": ", inspect(module), ?\n]
|
||||
end
|
||||
|
||||
defp format_file_line(file, line) do
|
||||
file = Path.relative_to_cwd(file)
|
||||
line = if line > 0, do: [?: | Integer.to_string(line)], else: []
|
||||
[" ", file, line]
|
||||
end
|
||||
|
||||
defp print_warning(message) do
|
||||
IO.puts(:stderr, [:elixir_errors.warning_prefix(), message])
|
||||
end
|
||||
|
||||
## Server callbacks
|
||||
|
||||
def init([modules, send_results, schedulers]) do
|
||||
ets = :ets.new(:checker_cache, [:set, :public, {:read_concurrency, true}])
|
||||
|
||||
@@ -119,10 +244,6 @@ defmodule Module.ParallelChecker do
|
||||
{:reply, ets, state}
|
||||
end
|
||||
|
||||
def handle_cast(:start, %{modules: []} = state) do
|
||||
{:stop, :normal, state}
|
||||
end
|
||||
|
||||
def handle_cast(:start, state) do
|
||||
{:noreply, spawn_checkers(state)}
|
||||
end
|
||||
@@ -176,7 +297,7 @@ defmodule Module.ParallelChecker do
|
||||
send_results_pid = state.send_results
|
||||
|
||||
spawn_link(fn ->
|
||||
warnings = Module.Checker.verify(verify, {parent, ets})
|
||||
warnings = check_module(verify, {parent, ets})
|
||||
send(send_results_pid, {__MODULE__, module, warnings})
|
||||
send(parent, {__MODULE__, :done})
|
||||
end)
|
||||
@@ -209,7 +330,11 @@ defmodule Module.ParallelChecker do
|
||||
end
|
||||
|
||||
defp cache_from_module_map(ets, map) do
|
||||
exports = [{{:__info__, 1}, :def} | definitions_to_exports(map.definitions)]
|
||||
exports =
|
||||
[{{:__info__, 1}, :def}] ++
|
||||
behaviour_exports(map) ++
|
||||
definitions_to_exports(map.definitions)
|
||||
|
||||
deprecated = Map.new(map.deprecated)
|
||||
cache_info(ets, map.module, exports, deprecated)
|
||||
end
|
||||
@@ -227,6 +352,7 @@ defmodule Module.ParallelChecker do
|
||||
defp info_exports(module) do
|
||||
Map.new(
|
||||
[{{:__info__, 1}, :def}] ++
|
||||
behaviour_exports(module) ++
|
||||
Enum.map(module.__info__(:macros), &{&1, :defmacro}) ++
|
||||
Enum.map(module.__info__(:functions), &{&1, :def})
|
||||
)
|
||||
@@ -268,6 +394,17 @@ defmodule Module.ParallelChecker do
|
||||
:ets.insert(ets, {{:cached, module}, true})
|
||||
end
|
||||
|
||||
defp behaviour_exports(%{is_behaviour: true}), do: [{{:behaviour_info, 1}, :def}]
|
||||
defp behaviour_exports(%{is_behaviour: false}), do: []
|
||||
|
||||
defp behaviour_exports(module) when is_atom(module) do
|
||||
if {:behaviour_info, 1} in module.module_info(:functions) do
|
||||
[{{:behaviour_info, 1}, :def}]
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
defp definitions_to_exports(definitions) do
|
||||
Enum.flat_map(definitions, fn {function, kind, _meta, _clauses} ->
|
||||
if kind in [:def, :defmacro] do
|
||||
|
||||
+329
-189
@@ -1,69 +1,75 @@
|
||||
defmodule Module.Types do
|
||||
@moduledoc false
|
||||
|
||||
defmodule Error do
|
||||
defexception [:message]
|
||||
end
|
||||
|
||||
import Module.Types.Helpers
|
||||
alias Module.Types.{Expr, Pattern}
|
||||
alias Module.Types.{Expr, Pattern, Unify}
|
||||
|
||||
@doc """
|
||||
Infer function definitions' types.
|
||||
"""
|
||||
def infer_definitions(file, module, defs) do
|
||||
clauses = infer_signatures(file, module, defs)
|
||||
infer_bodies(clauses)
|
||||
end
|
||||
@doc false
|
||||
def warnings(module, file, defs, no_warn_undefined, cache) do
|
||||
stack = stack()
|
||||
|
||||
defp infer_signatures(file, module, defs) do
|
||||
Enum.map(defs, fn {{fun, _arity} = function, kind, meta, clauses} ->
|
||||
stack = head_stack()
|
||||
context = head_context(file, module, function)
|
||||
Enum.flat_map(defs, fn {{fun, arity} = function, kind, meta, clauses} ->
|
||||
context = context(with_file_meta(meta, file), module, function, no_warn_undefined, cache)
|
||||
|
||||
clauses =
|
||||
Enum.map(clauses, fn {_meta, params, guards, body} ->
|
||||
def_expr = {kind, meta, [guards_to_expr(guards, {fun, [], params})]}
|
||||
stack = push_expr_stack(def_expr, stack)
|
||||
Enum.flat_map(clauses, fn {_meta, args, guards, body} ->
|
||||
def_expr = {kind, meta, [guards_to_expr(guards, {fun, [], args})]}
|
||||
|
||||
case of_head(params, guards, stack, context) do
|
||||
{:ok, _signature, context} -> {:ok, {context, body}}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end)
|
||||
try do
|
||||
warnings_from_clause(args, guards, body, def_expr, stack, context)
|
||||
rescue
|
||||
e ->
|
||||
def_expr = {kind, meta, [guards_to_expr(guards, {fun, [], args}), [do: body]]}
|
||||
|
||||
{function, clauses}
|
||||
error =
|
||||
Error.exception("""
|
||||
found error while checking types for #{Exception.format_mfa(module, fun, arity)}
|
||||
|
||||
#{Macro.to_string(def_expr)}
|
||||
|
||||
Please report this bug: https://github.com/elixir-lang/elixir/issues
|
||||
|
||||
#{Exception.format_banner(:error, e, __STACKTRACE__)}\
|
||||
""")
|
||||
|
||||
reraise error, __STACKTRACE__
|
||||
end
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
defp infer_bodies(clauses) do
|
||||
Enum.map(clauses, fn {function, clauses} ->
|
||||
errors =
|
||||
Enum.flat_map(clauses, fn
|
||||
{:ok, {head_context, body}} ->
|
||||
stack = body_stack()
|
||||
context = body_context(head_context)
|
||||
defp with_file_meta(meta, file) do
|
||||
case Keyword.fetch(meta, :file) do
|
||||
{:ok, {meta_file, _}} -> meta_file
|
||||
:error -> file
|
||||
end
|
||||
end
|
||||
|
||||
case Expr.of_expr(body, stack, context) do
|
||||
{:ok, _type, _context} -> []
|
||||
{:error, reason} -> [reason]
|
||||
end
|
||||
defp guards_to_expr([], left) do
|
||||
left
|
||||
end
|
||||
|
||||
{:error, reason} ->
|
||||
[reason]
|
||||
end)
|
||||
defp guards_to_expr([guard | guards], left) do
|
||||
guards_to_expr(guards, {:when, [], [left, guard]})
|
||||
end
|
||||
|
||||
{function, errors}
|
||||
end)
|
||||
defp warnings_from_clause(args, guards, body, def_expr, stack, context) do
|
||||
head_stack = Unify.push_expr_stack(def_expr, stack)
|
||||
|
||||
with {:ok, _types, context} <- Pattern.of_head(args, guards, head_stack, context),
|
||||
{:ok, _type, context} <- Expr.of_expr(body, stack, context) do
|
||||
context.warnings
|
||||
else
|
||||
{:error, {type, error, context}} ->
|
||||
[error_to_warning(type, error, context) | context.warnings]
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def of_head(params, guards, stack, context) do
|
||||
with {:ok, types, context} <-
|
||||
map_reduce_ok(params, context, &Pattern.of_pattern(&1, stack, &2)),
|
||||
# TODO: Check that of_guard/3 returns a boolean
|
||||
{:ok, _, context} <- Pattern.of_guard(guards_to_or(guards), stack, context),
|
||||
do: {:ok, lift_types(types, context), context}
|
||||
end
|
||||
|
||||
@doc false
|
||||
def head_context(file, module, function) do
|
||||
def context(file, module, function, no_warn_undefined, cache) do
|
||||
%{
|
||||
# File of module
|
||||
file: file,
|
||||
@@ -71,6 +77,10 @@ defmodule Module.Types do
|
||||
module: module,
|
||||
# Current function
|
||||
function: function,
|
||||
# List of calls to not warn on as undefined
|
||||
no_warn_undefined: no_warn_undefined,
|
||||
# A list of cached modules received from the parallel compiler
|
||||
cache: cache,
|
||||
# Expression variable to type variable
|
||||
vars: %{},
|
||||
# Type variable to expression variable
|
||||
@@ -85,18 +95,23 @@ defmodule Module.Types do
|
||||
# Track if a variable was infered from a type guard function such is_tuple/1
|
||||
# or a guard function that fails such as elem/2, possible values are:
|
||||
# `:guarded` when `is_tuple(x)`
|
||||
# `:guarded` when `is_tuple and elem(x, 0)`
|
||||
# `:fail` when `elem(x, 0)`
|
||||
# `:guarded_fail` when `is_tuple and elem(x, 0)`
|
||||
guard_sources: %{}
|
||||
guard_sources: %{},
|
||||
# A list with all warnings from the running the code
|
||||
warnings: []
|
||||
}
|
||||
end
|
||||
|
||||
@doc false
|
||||
def head_stack() do
|
||||
def stack() do
|
||||
%{
|
||||
# Stack of expression we have recursed through during inference,
|
||||
# Stack of variables we have refined during unification,
|
||||
# used for creating relevant traces
|
||||
unify_stack: [],
|
||||
# Last expression we have recursed through during inference,
|
||||
# used for tracing
|
||||
expr_stack: [],
|
||||
last_expr: nil,
|
||||
# When false do not add a trace when a type variable is refined,
|
||||
# useful when merging contexts where the variables already have traces
|
||||
trace: true,
|
||||
@@ -105,47 +120,11 @@ defmodule Module.Types do
|
||||
type_guards_enabled?: true,
|
||||
# Context used to determine if unification is bi-directional, :expr
|
||||
# is directional, :pattern is bi-directional
|
||||
context: :pattern
|
||||
context: nil
|
||||
}
|
||||
end
|
||||
|
||||
@doc false
|
||||
def body_context(head_context) do
|
||||
%{
|
||||
# File of module
|
||||
file: head_context.file,
|
||||
# Module of definitions
|
||||
module: head_context.module,
|
||||
# Current function
|
||||
function: head_context.function,
|
||||
# Expression variable to type variable
|
||||
vars: head_context.vars,
|
||||
# Type variable to expression variable
|
||||
types_to_vars: head_context.types_to_vars,
|
||||
# Type variable to type
|
||||
types: head_context.types,
|
||||
# Trace of all variables that have been refined to a type,
|
||||
# including the type they were refined to, why, and where
|
||||
traces: head_context.traces,
|
||||
# Counter to give type variables unique names
|
||||
counter: head_context.counter
|
||||
}
|
||||
end
|
||||
|
||||
@doc false
|
||||
def body_stack() do
|
||||
%{
|
||||
# Stack of expression we have recursed through during inference,
|
||||
# used for tracing
|
||||
expr_stack: [],
|
||||
# When false do not add a trace when a type variable is refined,
|
||||
# useful when merging contexts where the variables already have traces
|
||||
trace: true,
|
||||
# Context used to determine if unification is bi-directional, :expr
|
||||
# is directional, :pattern is bi-directional
|
||||
context: :expr
|
||||
}
|
||||
end
|
||||
## VARIABLE LIFTING
|
||||
|
||||
@doc """
|
||||
Lifts type variables to their infered types from the context.
|
||||
@@ -161,7 +140,9 @@ defmodule Module.Types do
|
||||
types
|
||||
end
|
||||
|
||||
@doc false
|
||||
@doc """
|
||||
Lifts a single type to its infered type from the context.
|
||||
"""
|
||||
def lift_type(type, context) do
|
||||
context = %{
|
||||
types: context.types,
|
||||
@@ -173,28 +154,6 @@ defmodule Module.Types do
|
||||
type
|
||||
end
|
||||
|
||||
## GUARDS
|
||||
|
||||
# TODO: Remove this and let multiple when be treated as multiple clauses,
|
||||
# meaning they will be intersection types
|
||||
defp guards_to_or([]) do
|
||||
[]
|
||||
end
|
||||
|
||||
defp guards_to_or(guards) do
|
||||
Enum.reduce(guards, fn guard, acc -> {{:., [], [:erlang, :orelse]}, [], [guard, acc]} end)
|
||||
end
|
||||
|
||||
defp guards_to_expr([], left) do
|
||||
left
|
||||
end
|
||||
|
||||
defp guards_to_expr([guard | guards], left) do
|
||||
guards_to_expr(guards, {:when, [], [left, guard]})
|
||||
end
|
||||
|
||||
## VARIABLE LIFTING
|
||||
|
||||
# Lift type variable to its infered (hopefully concrete) types from the context
|
||||
defp do_lift_type({:var, var}, context) do
|
||||
case Map.fetch(context.lifted_types, var) do
|
||||
@@ -220,17 +179,17 @@ defmodule Module.Types do
|
||||
end
|
||||
end
|
||||
|
||||
defp do_lift_type({:tuple, types}, context) do
|
||||
defp do_lift_type({:tuple, n, types}, context) do
|
||||
{types, context} = Enum.map_reduce(types, context, &do_lift_type/2)
|
||||
{{:tuple, types}, context}
|
||||
{{:tuple, n, types}, context}
|
||||
end
|
||||
|
||||
defp do_lift_type({:map, pairs}, context) do
|
||||
{pairs, context} =
|
||||
Enum.map_reduce(pairs, context, fn {key, value}, context ->
|
||||
Enum.map_reduce(pairs, context, fn {kind, key, value}, context ->
|
||||
{key, context} = do_lift_type(key, context)
|
||||
{value, context} = do_lift_type(value, context)
|
||||
{{key, value}, context}
|
||||
{{kind, key, value}, context}
|
||||
end)
|
||||
|
||||
{{:map, pairs}, context}
|
||||
@@ -254,54 +213,158 @@ defmodule Module.Types do
|
||||
{type, context}
|
||||
end
|
||||
|
||||
## ERROR FORMATTING
|
||||
## ERROR TO WARNING
|
||||
|
||||
def format_warning({:unable_unify, left, right, expr, traces}) do
|
||||
[
|
||||
"function clause will never match, found incompatibility:\n\n ",
|
||||
format_type(left),
|
||||
" !~ ",
|
||||
format_type(right),
|
||||
"\n\n",
|
||||
format_expr(expr),
|
||||
format_traces(traces),
|
||||
"Conflict found at"
|
||||
]
|
||||
# Collect relevant information from context and traces to report error
|
||||
def error_to_warning(:unable_unify, {left, right, stack}, context) do
|
||||
{fun, arity} = context.function
|
||||
line = get_meta(stack.last_expr)[:line]
|
||||
location = {context.file, line, {context.module, fun, arity}}
|
||||
|
||||
traces = type_traces(stack, context)
|
||||
traces = tag_traces(traces, context)
|
||||
|
||||
error = {:unable_unify, left, right, {location, stack.last_expr, traces}}
|
||||
{Module.Types, error, location}
|
||||
end
|
||||
|
||||
defp format_expr(nil) do
|
||||
[]
|
||||
# Collect relevant traces from context.traces using stack.unify_stack
|
||||
defp type_traces(stack, context) do
|
||||
# TODO: Do we need the unify_stack or is enough to only get the last variable
|
||||
# in the stack since we get related variables anyway?
|
||||
stack =
|
||||
stack.unify_stack
|
||||
|> Enum.uniq()
|
||||
|> Enum.flat_map(&[&1 | related_variables(&1, context.types)])
|
||||
|> Enum.uniq()
|
||||
|
||||
Enum.flat_map(stack, fn var_index ->
|
||||
with %{^var_index => traces} <- context.traces,
|
||||
%{^var_index => expr_var} <- context.types_to_vars do
|
||||
Enum.map(traces, &{expr_var, &1})
|
||||
else
|
||||
_other -> []
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp format_expr(expr) do
|
||||
[
|
||||
"in expression:\n\n ",
|
||||
expr_to_string(expr),
|
||||
"\n\n"
|
||||
]
|
||||
defp related_variables(var, types) do
|
||||
Enum.flat_map(types, fn
|
||||
{related_var, {:var, ^var}} ->
|
||||
[related_var | related_variables(related_var, types)]
|
||||
|
||||
_ ->
|
||||
[]
|
||||
end)
|
||||
end
|
||||
|
||||
defp format_traces([]) do
|
||||
[]
|
||||
# Tag if trace is for a concrete type or type variable
|
||||
defp tag_traces(traces, context) do
|
||||
Enum.flat_map(traces, fn {var, {type, expr, location}} ->
|
||||
with {:var, var_index} <- type,
|
||||
%{^var_index => expr_var} <- context.types_to_vars do
|
||||
[{var, {:var, expr_var, expr, location}}]
|
||||
else
|
||||
_ -> [{var, {:type, type, expr, location}}]
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp format_traces(traces) do
|
||||
Enum.map(traces, fn
|
||||
{var, {:type, type, expr, location}} ->
|
||||
## FORMAT WARNINGS
|
||||
|
||||
def format_warning({:unable_unify, left, right, {location, expr, traces}}) do
|
||||
cond do
|
||||
map_type?(left) and map_type?(right) and match?({:ok, _, _}, missing_field(left, right)) ->
|
||||
{:ok, atom, known_atoms} = missing_field(left, right)
|
||||
|
||||
# Drop the last trace which is the expression map.foo
|
||||
traces = Enum.drop(traces, 1)
|
||||
{traces, hints} = format_traces(traces, true)
|
||||
|
||||
[
|
||||
"undefined field \"#{atom}\" ",
|
||||
format_expr(expr, location),
|
||||
"expected one of the following fields: ",
|
||||
Enum.map_join(Enum.sort(known_atoms), ", ", & &1),
|
||||
"\n\n",
|
||||
traces,
|
||||
format_message_hints(hints),
|
||||
"Conflict found at"
|
||||
]
|
||||
|
||||
true ->
|
||||
simplify_left? = simplify_type?(left, right)
|
||||
simplify_right? = simplify_type?(right, left)
|
||||
|
||||
{traces, hints} = format_traces(traces, simplify_left? or simplify_right?)
|
||||
|
||||
[
|
||||
"incompatible types:\n\n ",
|
||||
Unify.format_type(left, simplify_left?),
|
||||
" !~ ",
|
||||
Unify.format_type(right, simplify_right?),
|
||||
"\n\n",
|
||||
format_expr(expr, location),
|
||||
traces,
|
||||
format_message_hints(hints),
|
||||
"Conflict found at"
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
defp missing_field(
|
||||
{:map, [{:required, {:atom, atom} = type, _}, {:optional, :dynamic, :dynamic}]},
|
||||
{:map, fields}
|
||||
) do
|
||||
matched_missing_field(fields, type, atom)
|
||||
end
|
||||
|
||||
defp missing_field(
|
||||
{:map, fields},
|
||||
{:map, [{:required, {:atom, atom} = type, _}, {:optional, :dynamic, :dynamic}]}
|
||||
) do
|
||||
matched_missing_field(fields, type, atom)
|
||||
end
|
||||
|
||||
defp missing_field(_, _), do: :error
|
||||
|
||||
defp matched_missing_field(fields, type, atom) do
|
||||
if List.keymember?(fields, type, 1) do
|
||||
:error
|
||||
else
|
||||
known_atoms = for {_, {:atom, atom}, _} <- fields, do: atom
|
||||
{:ok, atom, known_atoms}
|
||||
end
|
||||
end
|
||||
|
||||
defp format_traces([], _simplify?) do
|
||||
{[], []}
|
||||
end
|
||||
|
||||
defp format_traces(traces, simplify?) do
|
||||
traces
|
||||
|> Enum.reverse()
|
||||
|> Enum.map_reduce([], fn
|
||||
{var, {:type, type, expr, location}}, hints ->
|
||||
{hint, hints} = format_type_hint(type, expr, hints)
|
||||
|
||||
trace = [
|
||||
"where \"",
|
||||
Macro.to_string(var),
|
||||
"\" was given the type ",
|
||||
Module.Types.format_type(type),
|
||||
Unify.format_type(type, simplify?),
|
||||
hint,
|
||||
" in:\n\n # ",
|
||||
format_location(location),
|
||||
" ",
|
||||
expr_to_string(expr),
|
||||
indent(expr_to_string(expr)),
|
||||
"\n\n"
|
||||
]
|
||||
|
||||
{var1, {:var, var2, expr, location}} ->
|
||||
[
|
||||
{trace, hints}
|
||||
|
||||
{var1, {:var, var2, expr, location}}, hints ->
|
||||
trace = [
|
||||
"where \"",
|
||||
Macro.to_string(var1),
|
||||
"\" was given the same type as \"",
|
||||
@@ -309,57 +372,42 @@ defmodule Module.Types do
|
||||
"\" in:\n\n # ",
|
||||
format_location(location),
|
||||
" ",
|
||||
expr_to_string(expr),
|
||||
indent(expr_to_string(expr)),
|
||||
"\n\n"
|
||||
]
|
||||
|
||||
{trace, hints}
|
||||
end)
|
||||
end
|
||||
|
||||
defp format_location({file, line, _mfa}) do
|
||||
format_location({file, line})
|
||||
end
|
||||
|
||||
defp format_location({file, line}) do
|
||||
file = Path.relative_to_cwd(file)
|
||||
line = if line, do: [Integer.to_string(line)], else: []
|
||||
[file, ?:, line, ?\n]
|
||||
end
|
||||
|
||||
@doc false
|
||||
def format_type({:union, types}) do
|
||||
"#{Enum.map_join(types, " | ", &format_type/1)}"
|
||||
defp simplify_type?(type, other) do
|
||||
map_type?(type) and not map_type?(other)
|
||||
end
|
||||
|
||||
def format_type({:tuple, types}) do
|
||||
"{#{Enum.map_join(types, ", ", &format_type/1)}}"
|
||||
## EXPRESSION FORMATTING
|
||||
|
||||
defp format_expr(nil, _location) do
|
||||
[]
|
||||
end
|
||||
|
||||
def format_type({:list, type}) do
|
||||
"[#{format_type(type)}]"
|
||||
end
|
||||
|
||||
def format_type({:map, pairs}) do
|
||||
case List.keytake(pairs, :__struct__, 0) do
|
||||
{{:__struct__, struct}, pairs} ->
|
||||
"%#{inspect(struct)}{#{format_map_pairs(pairs)}}"
|
||||
|
||||
nil ->
|
||||
"%{#{format_map_pairs(pairs)}}"
|
||||
end
|
||||
end
|
||||
|
||||
def format_type({:atom, literal}) do
|
||||
inspect(literal)
|
||||
end
|
||||
|
||||
def format_type(atom) when is_atom(atom) do
|
||||
"#{atom}()"
|
||||
end
|
||||
|
||||
def format_type({:var, index}) do
|
||||
"var#{index}"
|
||||
end
|
||||
|
||||
defp format_map_pairs(pairs) do
|
||||
Enum.map_join(pairs, ", ", fn {left, right} ->
|
||||
"#{format_type(left)} => #{format_type(right)}"
|
||||
end)
|
||||
defp format_expr(expr, location) do
|
||||
[
|
||||
"in expression:\n\n # ",
|
||||
format_location(location),
|
||||
" ",
|
||||
indent(expr_to_string(expr)),
|
||||
"\n\n"
|
||||
]
|
||||
end
|
||||
|
||||
@doc false
|
||||
@@ -371,17 +419,109 @@ defmodule Module.Types do
|
||||
|
||||
defp reverse_rewrite(guard) do
|
||||
Macro.prewalk(guard, fn
|
||||
{:., _, [:erlang, :orelse]} -> :or
|
||||
{:., _, [:erlang, :andalso]} -> :and
|
||||
{{:., _, [mod, fun]}, _, args} -> erl_to_ex(mod, fun, args)
|
||||
{{:., _, [mod, fun]}, meta, args} -> erl_to_ex(mod, fun, args, meta)
|
||||
other -> other
|
||||
end)
|
||||
end
|
||||
|
||||
defp erl_to_ex(mod, fun, args) do
|
||||
defp erl_to_ex(mod, fun, args, meta) do
|
||||
case :elixir_rewrite.erl_to_ex(mod, fun, args) do
|
||||
{Kernel, fun, args} -> {fun, [], args}
|
||||
{mod, fun, args} -> {{:., [], [mod, fun]}, [], args}
|
||||
{Kernel, fun, args} -> {fun, meta, args}
|
||||
{mod, fun, args} -> {{:., [], [mod, fun]}, meta, args}
|
||||
end
|
||||
end
|
||||
|
||||
## Hints
|
||||
|
||||
defp format_message_hints(hints) do
|
||||
hints |> Enum.uniq() |> Enum.reverse() |> Enum.map(&format_message_hint/1)
|
||||
end
|
||||
|
||||
defp format_message_hint(:inferred_dot) do
|
||||
"""
|
||||
HINT: "var.field" (without parentheses) implies "var" is a map() while \
|
||||
"var.fun()" (with parentheses) implies "var" is an atom()
|
||||
|
||||
"""
|
||||
end
|
||||
|
||||
defp format_message_hint(:inferred_bitstring_spec) do
|
||||
"""
|
||||
HINT: all expressions given to binaries are assumed to be of type \
|
||||
integer() unless said otherwise. For example, <<expr>> assumes "expr" \
|
||||
is an integer. Pass a modifier, such as <<expr::float>> or <<expr::binary>>, \
|
||||
to change the default behaviour.
|
||||
|
||||
"""
|
||||
end
|
||||
|
||||
defp format_type_hint(type, expr, hints) do
|
||||
case format_type_hint(type, expr) do
|
||||
{message, hint} -> {message, [hint | hints]}
|
||||
:error -> {[], hints}
|
||||
end
|
||||
end
|
||||
|
||||
defp format_type_hint(type, expr) do
|
||||
cond do
|
||||
dynamic_map_dot?(type, expr) ->
|
||||
{" (due to calling var.field)", :inferred_dot}
|
||||
|
||||
dynamic_remote_call?(type, expr) ->
|
||||
{" (due to calling var.fun())", :inferred_dot}
|
||||
|
||||
inferred_bitstring_spec?(type, expr) ->
|
||||
{[], :inferred_bitstring_spec}
|
||||
|
||||
true ->
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
||||
defp dynamic_map_dot?(type, expr) do
|
||||
with true <- map_type?(type),
|
||||
{{:., _meta1, [_map, _field]}, meta2, []} <- expr,
|
||||
true <- Keyword.get(meta2, :no_parens, false) do
|
||||
true
|
||||
else
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
|
||||
defp dynamic_remote_call?(type, expr) do
|
||||
with true <- atom_type?(type),
|
||||
{{:., _meta1, [_module, _field]}, meta2, []} <- expr,
|
||||
false <- Keyword.get(meta2, :no_parens, false) do
|
||||
true
|
||||
else
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
|
||||
defp inferred_bitstring_spec?(type, expr) do
|
||||
with true <- integer_type?(type),
|
||||
{:<<>>, _, args} <- expr,
|
||||
true <- Enum.any?(args, &match?({:"::", [{:inferred_bitstring_spec, true} | _], _}, &1)) do
|
||||
true
|
||||
else
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
|
||||
## Formatting helpers
|
||||
|
||||
defp indent(string) do
|
||||
String.replace(string, "\n", " \n")
|
||||
end
|
||||
|
||||
defp map_type?({:map, _}), do: true
|
||||
defp map_type?(_other), do: false
|
||||
|
||||
defp atom_type?(:atom), do: true
|
||||
defp atom_type?({:atom, _}), do: false
|
||||
defp atom_type?({:union, union}), do: Enum.all?(union, &atom_type?/1)
|
||||
defp atom_type?(_other), do: false
|
||||
|
||||
defp integer_type?(:integer), do: true
|
||||
defp integer_type?(_other), do: false
|
||||
end
|
||||
|
||||
@@ -1,7 +1,498 @@
|
||||
defmodule Module.Types.Expr do
|
||||
@moduledoc false
|
||||
|
||||
def of_expr(_expr, _stack, context) do
|
||||
alias Module.Types.{Of, Pattern}
|
||||
import Module.Types.{Helpers, Unify}
|
||||
|
||||
def of_expr(expr, %{context: stack_context} = stack, context) when stack_context != :expr do
|
||||
of_expr(expr, %{stack | context: :expr}, context)
|
||||
end
|
||||
|
||||
# :atom
|
||||
def of_expr(atom, _stack, context) when is_atom(atom) do
|
||||
{:ok, {:atom, atom}, context}
|
||||
end
|
||||
|
||||
# 12
|
||||
def of_expr(literal, _stack, context) when is_integer(literal) do
|
||||
{:ok, :integer, context}
|
||||
end
|
||||
|
||||
# 1.2
|
||||
def of_expr(literal, _stack, context) when is_float(literal) do
|
||||
{:ok, :float, context}
|
||||
end
|
||||
|
||||
# "..."
|
||||
def of_expr(literal, _stack, context) when is_binary(literal) do
|
||||
{:ok, :binary, context}
|
||||
end
|
||||
|
||||
# #PID<...>
|
||||
def of_expr(literal, _stack, context) when is_pid(literal) do
|
||||
{:ok, :dynamic, context}
|
||||
end
|
||||
|
||||
# <<...>>>
|
||||
def of_expr({:<<>>, _meta, args}, stack, context) do
|
||||
result = Of.binary(args, stack, context, &of_expr/3)
|
||||
|
||||
case result do
|
||||
{:ok, context} -> {:ok, :binary, context}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
# left | []
|
||||
def of_expr({:|, _meta, [left_expr, []]} = expr, stack, context) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
of_expr(left_expr, stack, context)
|
||||
end
|
||||
|
||||
# left | right
|
||||
def of_expr({:|, _meta, [left_expr, right_expr]} = expr, stack, context) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
|
||||
case of_expr(left_expr, stack, context) do
|
||||
{:ok, left, context} ->
|
||||
case of_expr(right_expr, stack, context) do
|
||||
{:ok, {:list, right}, context} ->
|
||||
{:ok, to_union([left, right], context), context}
|
||||
|
||||
{:ok, right, context} ->
|
||||
{:ok, to_union([left, right], context), context}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
# []
|
||||
def of_expr([], _stack, context) do
|
||||
{:ok, {:list, :dynamic}, context}
|
||||
end
|
||||
|
||||
# [expr, ...]
|
||||
def of_expr(exprs, stack, context) when is_list(exprs) do
|
||||
stack = push_expr_stack(exprs, stack)
|
||||
|
||||
case map_reduce_ok(exprs, context, &of_expr(&1, stack, &2)) do
|
||||
{:ok, types, context} -> {:ok, {:list, to_union(types, context)}, context}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
# __CALLER__
|
||||
def of_expr({:__CALLER__, _meta, var_context}, _stack, context) when is_atom(var_context) do
|
||||
struct_pair = {:required, {:atom, :__struct__}, {:atom, Macro.Env}}
|
||||
|
||||
pairs =
|
||||
Enum.map(Map.from_struct(Macro.Env.__struct__()), fn {key, _value} ->
|
||||
{:required, {:atom, key}, :dynamic}
|
||||
end)
|
||||
|
||||
{:ok, {:map, [struct_pair | pairs]}, context}
|
||||
end
|
||||
|
||||
# __STACKTRACE__
|
||||
def of_expr({:__STACKTRACE__, _meta, var_context}, _stack, context) when is_atom(var_context) do
|
||||
file = {:tuple, 2, [{:atom, :file}, {:list, :integer}]}
|
||||
line = {:tuple, 2, [{:atom, :line}, :integer]}
|
||||
file_line = {:list, {:union, [file, line]}}
|
||||
type = {:list, {:tuple, 4, [:atom, :atom, :integer, file_line]}}
|
||||
{:ok, type, context}
|
||||
end
|
||||
|
||||
# var
|
||||
def of_expr(var, _stack, context) when is_var(var) do
|
||||
{:ok, get_var!(var, context), context}
|
||||
end
|
||||
|
||||
# {left, right}
|
||||
def of_expr({left, right}, stack, context) do
|
||||
of_expr({:{}, [], [left, right]}, stack, context)
|
||||
end
|
||||
|
||||
# {...}
|
||||
def of_expr({:{}, _meta, exprs} = expr, stack, context) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
|
||||
case map_reduce_ok(exprs, context, &of_expr(&1, stack, &2)) do
|
||||
{:ok, types, context} -> {:ok, {:tuple, length(types), types}, context}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
# left = right
|
||||
def of_expr({:=, _meta, [left_expr, right_expr]} = expr, stack, context) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
|
||||
with {:ok, left_type, context} <-
|
||||
Pattern.of_pattern(left_expr, stack, context),
|
||||
{:ok, right_type, context} <- of_expr(right_expr, stack, context),
|
||||
do: unify(right_type, left_type, %{stack | context: :pattern}, context)
|
||||
end
|
||||
|
||||
# %{map | ...}
|
||||
def of_expr({:%{}, _, [{:|, _, [map, args]}]} = expr, stack, context) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
|
||||
with {:ok, map_type, context} <- of_expr(map, stack, context),
|
||||
{:ok, {:map, arg_pairs}, context} <- Of.closed_map(args, stack, context, &of_expr/3),
|
||||
dynamic_value_pairs =
|
||||
Enum.map(arg_pairs, fn {:required, key, _value} -> {:required, key, :dynamic} end),
|
||||
args_type = {:map, dynamic_value_pairs ++ [{:optional, :dynamic, :dynamic}]},
|
||||
{:ok, type, context} <- unify(args_type, map_type, stack, context) do
|
||||
# Retrieve map type and overwrite with the new value types from the map update
|
||||
{:map, pairs} = resolve_var(type, context)
|
||||
|
||||
updated_pairs =
|
||||
Enum.reduce(arg_pairs, pairs, fn {:required, key, value}, pairs ->
|
||||
List.keyreplace(pairs, key, 1, {:required, key, value})
|
||||
end)
|
||||
|
||||
{:ok, {:map, updated_pairs}, context}
|
||||
end
|
||||
end
|
||||
|
||||
# %Struct{map | ...}
|
||||
def of_expr({:%, meta, [module, {:%{}, _, [{:|, _, [_, _]}]} = update]} = expr, stack, context) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
|
||||
with {:ok, struct, context} <- Of.struct(module, meta, context),
|
||||
{:ok, update, context} <- of_expr(update, stack, context) do
|
||||
unify(update, struct, stack, context)
|
||||
end
|
||||
end
|
||||
|
||||
# %{...}
|
||||
def of_expr({:%{}, _meta, args} = expr, stack, context) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
Of.closed_map(args, stack, context, &of_expr/3)
|
||||
end
|
||||
|
||||
# %Struct{...}
|
||||
def of_expr({:%, meta1, [module, {:%{}, _meta2, args}]} = expr, stack, context) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
|
||||
with {:ok, struct, context} <- Of.struct(module, meta1, context),
|
||||
{:ok, map, context} <- Of.open_map(args, stack, context, &of_expr/3) do
|
||||
unify(map, struct, stack, context)
|
||||
end
|
||||
end
|
||||
|
||||
# ()
|
||||
def of_expr({:__block__, _meta, []}, _stack, context) do
|
||||
{:ok, {:atom, nil}, context}
|
||||
end
|
||||
|
||||
# (expr; expr)
|
||||
def of_expr({:__block__, _meta, exprs}, stack, context) do
|
||||
case map_reduce_ok(exprs, context, &of_expr(&1, stack, &2)) do
|
||||
{:ok, expr_types, context} -> {:ok, Enum.at(expr_types, -1), context}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
# case expr do pat -> expr end
|
||||
def of_expr({:case, _meta, [case_expr, [{:do, clauses}]]} = expr, stack, context) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
|
||||
with {:ok, _expr_type, context} <- of_expr(case_expr, stack, context),
|
||||
{:ok, context} <- of_clauses(clauses, stack, context),
|
||||
do: {:ok, :dynamic, context}
|
||||
end
|
||||
|
||||
# fn pat -> expr end
|
||||
def of_expr({:fn, _meta, clauses} = expr, stack, context) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
|
||||
case of_clauses(clauses, stack, context) do
|
||||
{:ok, context} -> {:ok, :dynamic, context}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
@try_blocks [:do, :after]
|
||||
@try_clause_blocks [:catch, :else, :after]
|
||||
|
||||
# try do expr end
|
||||
def of_expr({:try, _meta, [blocks]} = expr, stack, context) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
|
||||
{result, context} =
|
||||
reduce_ok(blocks, context, fn
|
||||
{:rescue, clauses}, context ->
|
||||
reduce_ok(clauses, context, fn
|
||||
{:->, _, [[{:in, _, [var, _exceptions]}], body]}, context = acc ->
|
||||
{_type, context} = new_pattern_var(var, context)
|
||||
|
||||
with {:ok, context} <- of_expr_context(body, stack, context) do
|
||||
{:ok, keep_warnings(acc, context)}
|
||||
end
|
||||
|
||||
{:->, _, [[var], body]}, context = acc ->
|
||||
{_type, context} = new_pattern_var(var, context)
|
||||
|
||||
with {:ok, context} <- of_expr_context(body, stack, context) do
|
||||
{:ok, keep_warnings(acc, context)}
|
||||
end
|
||||
end)
|
||||
|
||||
{block, body}, context = acc when block in @try_blocks ->
|
||||
with {:ok, context} <- of_expr_context(body, stack, context) do
|
||||
{:ok, keep_warnings(acc, context)}
|
||||
end
|
||||
|
||||
{block, clauses}, context when block in @try_clause_blocks ->
|
||||
of_clauses(clauses, stack, context)
|
||||
end)
|
||||
|
||||
case result do
|
||||
:ok -> {:ok, :dynamic, context}
|
||||
:error -> {:error, context}
|
||||
end
|
||||
end
|
||||
|
||||
# receive do pat -> expr end
|
||||
def of_expr({:receive, _meta, [blocks]} = expr, stack, context) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
|
||||
{result, context} =
|
||||
reduce_ok(blocks, context, fn
|
||||
{:do, {:__block__, _, []}}, context ->
|
||||
{:ok, context}
|
||||
|
||||
{:do, clauses}, context ->
|
||||
of_clauses(clauses, stack, context)
|
||||
|
||||
{:after, [{:->, _meta, [head, body]}]}, context = acc ->
|
||||
with {:ok, _type, context} <- of_expr(head, stack, context),
|
||||
{:ok, _type, context} <- of_expr(body, stack, context),
|
||||
do: {:ok, keep_warnings(acc, context)}
|
||||
end)
|
||||
|
||||
case result do
|
||||
:ok -> {:ok, :dynamic, context}
|
||||
:error -> {:error, context}
|
||||
end
|
||||
end
|
||||
|
||||
# for pat <- expr do expr end
|
||||
def of_expr({:for, _meta, args} = expr, stack, context) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
{clauses, [[{:do, block} | opts]]} = Enum.split(args, -1)
|
||||
|
||||
with {:ok, context} <- reduce_ok(clauses, context, &for_clause(&1, stack, &2)),
|
||||
{:ok, context} <- reduce_ok(opts, context, &for_option(&1, stack, &2)) do
|
||||
if Keyword.has_key?(opts, :reduce) do
|
||||
with {:ok, context} <- of_clauses(block, stack, context) do
|
||||
{:ok, :dynamic, context}
|
||||
end
|
||||
else
|
||||
with {:ok, _type, context} <- of_expr(block, stack, context) do
|
||||
{:ok, :dynamic, context}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# with pat <- expr do expr end
|
||||
def of_expr({:with, _meta, clauses} = expr, stack, context) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
|
||||
case reduce_ok(clauses, context, &with_clause(&1, stack, &2)) do
|
||||
{:ok, _context} -> {:ok, :dynamic, context}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
# fun.(arg)
|
||||
def of_expr({{:., _meta1, [fun]}, _meta2, args} = expr, stack, context) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
|
||||
case of_expr(fun, stack, context) do
|
||||
{:ok, _fun_type, context} ->
|
||||
case map_reduce_ok(args, context, &of_expr(&1, stack, &2)) do
|
||||
{:ok, _arg_types, context} -> {:ok, :dynamic, context}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
# expr.key_or_fun
|
||||
def of_expr({{:., _meta1, [expr1, key_or_fun]}, meta2, []} = expr2, stack, context)
|
||||
when not is_atom(expr1) do
|
||||
stack = push_expr_stack(expr2, stack)
|
||||
|
||||
if Keyword.get(meta2, :no_parens, false) do
|
||||
with {:ok, expr_type, context} <- of_expr(expr1, stack, context),
|
||||
{value_var, context} = add_var(context),
|
||||
pair_type = {:required, {:atom, key_or_fun}, value_var},
|
||||
optional_type = {:optional, :dynamic, :dynamic},
|
||||
map_field_type = {:map, [pair_type, optional_type]},
|
||||
{:ok, _map_type, context} <- unify(map_field_type, expr_type, stack, context),
|
||||
do: {:ok, value_var, context}
|
||||
else
|
||||
with {:ok, expr_type, context} <- of_expr(expr1, stack, context),
|
||||
{:ok, _map_type, context} <- unify(expr_type, :atom, stack, context),
|
||||
do: {:ok, :dynamic, context}
|
||||
end
|
||||
end
|
||||
|
||||
# expr.fun(arg)
|
||||
def of_expr({{:., meta1, [expr1, fun]}, _meta2, args} = expr2, stack, context) do
|
||||
context = Of.remote(expr1, fun, length(args), meta1, context)
|
||||
stack = push_expr_stack(expr2, stack)
|
||||
|
||||
with {:ok, _expr_type, context} <- of_expr(expr1, stack, context),
|
||||
{:ok, _fun_type, context} <- of_expr(fun, stack, context) do
|
||||
case map_reduce_ok(args, context, &of_expr(&1, stack, &2)) do
|
||||
{:ok, _arg_types, context} -> {:ok, :dynamic, context}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# &Foo.bar/1
|
||||
def of_expr({:&, meta, [{:/, _, [{{:., _, [module, fun]}, _, []}, arity]}]}, _stack, context)
|
||||
when is_atom(module) and is_atom(fun) do
|
||||
context = Of.remote(module, fun, arity, meta, context)
|
||||
{:ok, :dynamic, context}
|
||||
end
|
||||
|
||||
# &foo/1
|
||||
# & &1
|
||||
def of_expr({:&, _meta, _arg}, _stack, context) do
|
||||
# TODO: Function type
|
||||
{:ok, :dynamic, context}
|
||||
end
|
||||
|
||||
# fun(arg)
|
||||
def of_expr({fun, _meta, args} = expr, stack, context) when is_atom(fun) and is_list(args) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
|
||||
case map_reduce_ok(args, context, &of_expr(&1, stack, &2)) do
|
||||
{:ok, _arg_types, context} -> {:ok, :dynamic, context}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
defp for_clause({:<-, _, [left, expr]}, stack, context) do
|
||||
{pattern, guards} = extract_head([left])
|
||||
|
||||
with {:ok, _pattern_type, context} <- Pattern.of_pattern(pattern, stack, context),
|
||||
# TODO: Check that of_guard/3 returns a boolean
|
||||
{:ok, _guard_type, context} <- Pattern.of_guard(guards_to_or(guards), stack, context),
|
||||
{:ok, _expr_type, context} <- of_expr(expr, stack, context),
|
||||
do: {:ok, context}
|
||||
end
|
||||
|
||||
defp for_clause({:<<>>, _, [{:<-, _, [pattern, expr]}]}, stack, context) do
|
||||
# TODO: the compiler guarantees pattern is a binary but we need to check expr is a binary
|
||||
with {:ok, _pattern_type, context} <- Pattern.of_pattern(pattern, stack, context),
|
||||
{:ok, _expr_type, context} <- of_expr(expr, stack, context),
|
||||
do: {:ok, context}
|
||||
end
|
||||
|
||||
defp for_clause(list, stack, context) when is_list(list) do
|
||||
reduce_ok(list, context, &for_option(&1, stack, &2))
|
||||
end
|
||||
|
||||
defp for_clause(expr, stack, context) do
|
||||
of_expr_context(expr, stack, context)
|
||||
end
|
||||
|
||||
defp for_option({:into, expr}, stack, context) do
|
||||
of_expr_context(expr, stack, context)
|
||||
end
|
||||
|
||||
defp for_option({:reduce, expr}, stack, context) do
|
||||
of_expr_context(expr, stack, context)
|
||||
end
|
||||
|
||||
defp for_option({:uniq, _}, _stack, context) do
|
||||
{:ok, context}
|
||||
end
|
||||
|
||||
defp with_clause({:<-, _, [left, expr]}, stack, context) do
|
||||
{pattern, guards} = extract_head([left])
|
||||
|
||||
with {:ok, _pattern_type, context} <- Pattern.of_pattern(pattern, stack, context),
|
||||
# TODO: Check that of_guard/3 returns a boolean
|
||||
{:ok, _guard_type, context} <- Pattern.of_guard(guards_to_or(guards), stack, context),
|
||||
{:ok, _expr_type, context} <- of_expr(expr, stack, context),
|
||||
do: {:ok, context}
|
||||
end
|
||||
|
||||
defp with_clause(list, stack, context) when is_list(list) do
|
||||
reduce_ok(list, context, &with_option(&1, stack, &2))
|
||||
end
|
||||
|
||||
defp with_clause(expr, stack, context) do
|
||||
of_expr_context(expr, stack, context)
|
||||
end
|
||||
|
||||
defp with_option({:do, body}, stack, context) do
|
||||
of_expr_context(body, stack, context)
|
||||
end
|
||||
|
||||
defp with_option({:else, clauses}, stack, context) do
|
||||
of_clauses(clauses, stack, context)
|
||||
end
|
||||
|
||||
defp of_clauses(clauses, stack, context) do
|
||||
reduce_ok(clauses, context, fn {:->, _meta, [head, body]}, context = acc ->
|
||||
{patterns, guards} = extract_head(head)
|
||||
|
||||
with {:ok, _, context} <- Pattern.of_head(patterns, guards, stack, context),
|
||||
{:ok, _expr_type, context} <- of_expr(body, stack, context),
|
||||
do: {:ok, keep_warnings(acc, context)}
|
||||
end)
|
||||
end
|
||||
|
||||
defp keep_warnings(context, %{warnings: warnings}) do
|
||||
%{context | warnings: warnings}
|
||||
end
|
||||
|
||||
defp extract_head([{:when, _meta, args}]) do
|
||||
case Enum.split(args, -1) do
|
||||
{patterns, [guards]} -> {patterns, flatten_when(guards)}
|
||||
{patterns, []} -> {patterns, []}
|
||||
end
|
||||
end
|
||||
|
||||
defp extract_head(other) do
|
||||
{other, []}
|
||||
end
|
||||
|
||||
defp flatten_when({:when, _meta, [left, right]}) do
|
||||
[left | flatten_when(right)]
|
||||
end
|
||||
|
||||
defp flatten_when(other) do
|
||||
[other]
|
||||
end
|
||||
|
||||
defp of_expr_context(expr, stack, context) do
|
||||
case of_expr(expr, stack, context) do
|
||||
{:ok, _type, context} -> {:ok, context}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
defp new_pattern_var({:_, _meta, var_context}, context) when is_atom(var_context) do
|
||||
{:dynamic, context}
|
||||
end
|
||||
|
||||
defp new_pattern_var(var, context) do
|
||||
new_var(var, context)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
defmodule Module.Types.Helpers do
|
||||
# AST and enumeration helpers.
|
||||
@moduledoc false
|
||||
|
||||
@doc """
|
||||
@@ -19,14 +20,10 @@ defmodule Module.Types.Helpers do
|
||||
def var_name({_name, meta, _context}), do: Keyword.fetch!(meta, :version)
|
||||
|
||||
@doc """
|
||||
Push expression to stack.
|
||||
|
||||
The expression stack is used to give the context where a type variable
|
||||
was refined when show a type conflict error.
|
||||
Returns the AST metadata.
|
||||
"""
|
||||
def push_expr_stack(expr, stack) do
|
||||
%{stack | expr_stack: [expr | stack.expr_stack]}
|
||||
end
|
||||
def get_meta({_, meta, _}), do: meta
|
||||
def get_meta(_other), do: []
|
||||
|
||||
@doc """
|
||||
Like `Enum.reduce/3` but only continues while `fun` returns `{:ok, acc}`
|
||||
@@ -41,10 +38,6 @@ defmodule Module.Types.Helpers do
|
||||
{:ok, acc} ->
|
||||
do_reduce_ok(tail, acc, fun)
|
||||
|
||||
result when elem(result, 0) == :ok ->
|
||||
result = Tuple.delete_at(result, 0)
|
||||
do_reduce_ok(tail, result, fun)
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
@@ -81,10 +74,6 @@ defmodule Module.Types.Helpers do
|
||||
{:ok, elem} ->
|
||||
do_map_ok(tail, [elem | acc], fun)
|
||||
|
||||
result when elem(result, 0) == :ok ->
|
||||
result = Tuple.delete_at(result, 0)
|
||||
do_map_ok(tail, [result | acc], fun)
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
@@ -112,6 +101,23 @@ defmodule Module.Types.Helpers do
|
||||
|
||||
defp do_map_reduce_ok([], {list, acc}, _fun), do: {:ok, Enum.reverse(list), acc}
|
||||
|
||||
def flat_map_reduce_ok(list, acc, fun) do
|
||||
do_flat_map_reduce_ok(list, {[], acc}, fun)
|
||||
end
|
||||
|
||||
defp do_flat_map_reduce_ok([head | tail], {list, acc}, fun) do
|
||||
case fun.(head, acc) do
|
||||
{:ok, elems, acc} ->
|
||||
do_flat_map_reduce_ok(tail, {[elems | list], acc}, fun)
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
defp do_flat_map_reduce_ok([], {list, acc}, _fun),
|
||||
do: {:ok, Enum.reverse(Enum.concat(list)), acc}
|
||||
|
||||
@doc """
|
||||
Given a list of `[{:ok, term()} | {:error, term()}]` it returns a list of
|
||||
errors `{:error, [term()]}` in case of at least one error or `{:ok, [term()]}`
|
||||
@@ -123,4 +129,14 @@ defmodule Module.Types.Helpers do
|
||||
{_oks, errors} -> {:error, Enum.map(errors, fn {:error, error} -> error end)}
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: Remove this and let multiple when be treated as multiple clauses,
|
||||
# meaning they will be intersection types
|
||||
def guards_to_or([]) do
|
||||
[]
|
||||
end
|
||||
|
||||
def guards_to_or(guards) do
|
||||
Enum.reduce(guards, fn guard, acc -> {{:., [], [:erlang, :orelse]}, [], [guard, acc]} end)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,356 +0,0 @@
|
||||
defmodule Module.Types.Infer do
|
||||
@moduledoc false
|
||||
|
||||
import Module.Types.Helpers
|
||||
|
||||
@doc """
|
||||
Unifies two types and returns the unified type and an updated typing context
|
||||
or an error in case of a typing conflict.
|
||||
"""
|
||||
def unify(source, target, stack, context) do
|
||||
case do_unify(source, target, stack, context) do
|
||||
{:ok, type, context} ->
|
||||
{:ok, type, context}
|
||||
|
||||
{:error, reason} ->
|
||||
if stack.context == :pattern do
|
||||
case do_unify(target, source, stack, context) do
|
||||
{:ok, type, context} ->
|
||||
{:ok, type, context}
|
||||
|
||||
{:error, _} ->
|
||||
{:error, reason}
|
||||
end
|
||||
else
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp do_unify(same, same, _stack, context) do
|
||||
{:ok, same, context}
|
||||
end
|
||||
|
||||
defp do_unify({:var, var}, type, stack, context) do
|
||||
unify_var(var, type, stack, context, _var_source = true)
|
||||
end
|
||||
|
||||
defp do_unify(type, {:var, var}, stack, context) do
|
||||
unify_var(var, type, stack, context, _var_source = false)
|
||||
end
|
||||
|
||||
defp do_unify({:tuple, sources}, {:tuple, targets}, stack, context)
|
||||
when length(sources) == length(targets) do
|
||||
result =
|
||||
map_reduce_ok(Enum.zip(sources, targets), context, fn {source, target}, context ->
|
||||
unify(source, target, stack, context)
|
||||
end)
|
||||
|
||||
case result do
|
||||
{:ok, types, context} -> {:ok, {:tuple, types}, context}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
defp do_unify({:list, source}, {:list, target}, stack, context) do
|
||||
case unify(source, target, stack, context) do
|
||||
{:ok, type, context} -> {:ok, {:list, type}, context}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
defp do_unify({:map, source_pairs}, {:map, target_pairs}, stack, context) do
|
||||
# Since maps in patterns only support literal keys (excluding maps)
|
||||
# we can do exact type match without subtype checking
|
||||
|
||||
unique_right_pairs =
|
||||
Enum.reject(target_pairs, fn {key, _value} ->
|
||||
:lists.keyfind(key, 1, source_pairs)
|
||||
end)
|
||||
|
||||
unique_pairs = source_pairs ++ unique_right_pairs
|
||||
|
||||
# Build union of all unique key-value pairs between the maps
|
||||
result =
|
||||
map_reduce_ok(unique_pairs, context, fn {source_key, source_value}, context ->
|
||||
case :lists.keyfind(source_key, 1, target_pairs) do
|
||||
{^source_key, target_value} ->
|
||||
case unify(source_value, target_value, stack, context) do
|
||||
{:ok, value, context} -> {:ok, {source_key, value}, context}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
|
||||
false ->
|
||||
{:ok, {source_key, source_value}, context}
|
||||
end
|
||||
end)
|
||||
|
||||
case result do
|
||||
{:ok, pairs, context} -> {:ok, {:map, pairs}, context}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
defp do_unify(source, :dynamic, _stack, context) do
|
||||
{:ok, source, context}
|
||||
end
|
||||
|
||||
defp do_unify(source, target, stack, context) do
|
||||
if subtype?(source, target, context) do
|
||||
{:ok, source, context}
|
||||
else
|
||||
error({:unable_unify, source, target}, stack, context)
|
||||
end
|
||||
end
|
||||
|
||||
defp unify_var(var, type, stack, context, var_source?) do
|
||||
case Map.fetch!(context.types, var) do
|
||||
:unbound ->
|
||||
context = refine_var(var, type, stack, context)
|
||||
|
||||
if recursive_type?(type, [], context) do
|
||||
if var_source? do
|
||||
error({:unable_unify, {:var, var}, type}, stack, context)
|
||||
else
|
||||
error({:unable_unify, type, {:var, var}}, stack, context)
|
||||
end
|
||||
else
|
||||
{:ok, {:var, var}, context}
|
||||
end
|
||||
|
||||
var_type ->
|
||||
context = trace_var(var, type, stack, context)
|
||||
|
||||
unify_result =
|
||||
if var_source? do
|
||||
unify(var_type, type, stack, context)
|
||||
else
|
||||
unify(type, var_type, stack, context)
|
||||
end
|
||||
|
||||
case unify_result do
|
||||
{:ok, var_type, context} ->
|
||||
context = refine_var(var, var_type, stack, context)
|
||||
{:ok, {:var, var}, context}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Adds a variable to the typing context and returns its type variables.
|
||||
If the variable has already been added, return the existing type variable.
|
||||
"""
|
||||
def new_var(var, context) do
|
||||
case Map.fetch(context.vars, var_name(var)) do
|
||||
{:ok, type} ->
|
||||
{type, context}
|
||||
|
||||
:error ->
|
||||
type = {:var, context.counter}
|
||||
vars = Map.put(context.vars, var_name(var), type)
|
||||
types_to_vars = Map.put(context.types_to_vars, context.counter, var)
|
||||
types = Map.put(context.types, context.counter, :unbound)
|
||||
traces = Map.put(context.traces, context.counter, [])
|
||||
|
||||
context = %{
|
||||
context
|
||||
| vars: vars,
|
||||
types_to_vars: types_to_vars,
|
||||
types: types,
|
||||
traces: traces,
|
||||
counter: context.counter + 1
|
||||
}
|
||||
|
||||
{type, context}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Set the type for a variable and add trace.
|
||||
"""
|
||||
def refine_var(var, type, stack, context) do
|
||||
types = Map.put(context.types, var, type)
|
||||
context = %{context | types: types}
|
||||
trace_var(var, type, stack, context)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Remove type variable and all its traces.
|
||||
"""
|
||||
def remove_var(var, context) do
|
||||
types = Map.delete(context.types, var)
|
||||
traces = Map.delete(context.traces, var)
|
||||
%{context | types: types, traces: traces}
|
||||
end
|
||||
|
||||
defp trace_var(var, type, %{trace: true, expr_stack: expr_stack} = _stack, context) do
|
||||
line = get_meta(hd(expr_stack))[:line]
|
||||
trace = {type, expr_stack, {context.file, line}}
|
||||
traces = Map.update!(context.traces, var, &[trace | &1])
|
||||
%{context | traces: traces}
|
||||
end
|
||||
|
||||
defp trace_var(_var, _type, %{trace: false} = _stack, context) do
|
||||
context
|
||||
end
|
||||
|
||||
# Check if a variable is recursive and incompatible with itself
|
||||
# Bad: `{var} = var`
|
||||
# Good: `x = y; y = z; z = x`
|
||||
defp recursive_type?({:var, var} = parent, parents, context) do
|
||||
case Map.fetch!(context.types, var) do
|
||||
:unbound ->
|
||||
false
|
||||
|
||||
type ->
|
||||
if type in parents do
|
||||
not Enum.all?(parents, &match?({:var, _}, &1))
|
||||
else
|
||||
recursive_type?(type, [parent | parents], context)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp recursive_type?({:list, type} = parent, parents, context) do
|
||||
recursive_type?(type, [parent | parents], context)
|
||||
end
|
||||
|
||||
defp recursive_type?({:tuple, types} = parent, parents, context) do
|
||||
Enum.any?(types, &recursive_type?(&1, [parent | parents], context))
|
||||
end
|
||||
|
||||
defp recursive_type?({:map, pairs} = parent, parents, context) do
|
||||
Enum.any?(pairs, fn {key, value} ->
|
||||
recursive_type?(key, [parent | parents], context) or
|
||||
recursive_type?(value, [parent | parents], context)
|
||||
end)
|
||||
end
|
||||
|
||||
defp recursive_type?(_other, _parents, _context) do
|
||||
false
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if the first argument is a subtype of the second argument.
|
||||
Only checks for simple and concrete types.
|
||||
"""
|
||||
def subtype?({:atom, boolean}, :boolean, _context) when is_boolean(boolean), do: true
|
||||
def subtype?({:atom, atom}, :atom, _context) when is_atom(atom), do: true
|
||||
def subtype?(:boolean, :atom, _context), do: true
|
||||
def subtype?(:float, :number, _context), do: true
|
||||
def subtype?(:integer, :number, _context), do: true
|
||||
def subtype?({:tuple, _}, :tuple, _context), do: true
|
||||
|
||||
# TODO: Lift unions to unify/3?
|
||||
def subtype?({:union, left_types}, {:union, _} = right_union, context) do
|
||||
Enum.all?(left_types, &subtype?(&1, right_union, context))
|
||||
end
|
||||
|
||||
def subtype?(left, {:union, right_types}, context) do
|
||||
Enum.any?(right_types, &subtype?(left, &1, context))
|
||||
end
|
||||
|
||||
def subtype?(left, right, _context), do: left == right
|
||||
|
||||
@doc """
|
||||
Returns a "simplified" union using `subtype?/3` to remove redundant types.
|
||||
|
||||
Due to limitations in `subtype?/3` some overlapping types may still be
|
||||
included. For example unions with overlapping non-concrete types such as
|
||||
`{boolean()} | {atom()}` will not be merged or types with variables that
|
||||
are distinct but equivalent such as `a | b when a ~ b`.
|
||||
"""
|
||||
# TODO: Translate union of all top types to dynamic()
|
||||
def to_union(types, context) when types != [] do
|
||||
if :dynamic in types do
|
||||
:dynamic
|
||||
else
|
||||
case unique_super_types(flatten_union(types), context) do
|
||||
[type] -> type
|
||||
types -> {:union, types}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp flatten_union(types) do
|
||||
Enum.flat_map(types, fn
|
||||
{:union, types} -> flatten_union(types)
|
||||
type -> [type]
|
||||
end)
|
||||
end
|
||||
|
||||
# Filter subtypes
|
||||
# `boolean() | atom()` => `atom()`
|
||||
# `:foo | atom()` => `atom()`
|
||||
# Does not unify `true | false` => `boolean()`
|
||||
defp unique_super_types([type | types], context) do
|
||||
types = Enum.reject(types, &subtype?(&1, type, context))
|
||||
|
||||
if Enum.any?(types, &subtype?(type, &1, context)) do
|
||||
unique_super_types(types, context)
|
||||
else
|
||||
[type | unique_super_types(types, context)]
|
||||
end
|
||||
end
|
||||
|
||||
defp unique_super_types([], _context) do
|
||||
[]
|
||||
end
|
||||
|
||||
# Collect relevant information from context and traces to report error
|
||||
defp error({:unable_unify, left, right}, stack, context) do
|
||||
{fun, arity} = context.function
|
||||
line = get_meta(hd(stack.expr_stack))[:line]
|
||||
location = {context.file, line, {context.module, fun, arity}}
|
||||
|
||||
traces = type_traces(context)
|
||||
common_expr = common_super_expr(traces) || hd(stack.expr_stack)
|
||||
traces = simplify_traces(traces, context)
|
||||
|
||||
{:error, {Module.Types, {:unable_unify, left, right, common_expr, traces}, [location]}}
|
||||
end
|
||||
|
||||
defp type_traces(context) do
|
||||
Enum.flat_map(context.traces, fn {var_index, traces} ->
|
||||
expr_var = Map.fetch!(context.types_to_vars, var_index)
|
||||
Enum.map(traces, &{expr_var, &1})
|
||||
end)
|
||||
end
|
||||
|
||||
# Only use last expr from trace and tag if trace is for
|
||||
# a concrete type or type variable
|
||||
defp simplify_traces(traces, context) do
|
||||
Enum.flat_map(traces, fn {var, {type, [expr | _], location}} ->
|
||||
case type do
|
||||
{:var, var_index} ->
|
||||
var2 = Map.fetch!(context.types_to_vars, var_index)
|
||||
[{var, {:var, var2, expr, location}}]
|
||||
|
||||
_ ->
|
||||
[{var, {:type, type, expr, location}}]
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
# Find first common super expression among all traces
|
||||
defp common_super_expr([]) do
|
||||
nil
|
||||
end
|
||||
|
||||
defp common_super_expr([{_var, {_type, expr_stack, _location}} | traces]) do
|
||||
Enum.find_value(expr_stack, fn expr ->
|
||||
common? =
|
||||
Enum.all?(traces, fn {_var, {_type, expr_stack, _location}} -> expr in expr_stack end)
|
||||
|
||||
if common? do
|
||||
expr
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp get_meta({_fun, meta, _args}) when is_list(meta), do: meta
|
||||
defp get_meta(_other), do: []
|
||||
end
|
||||
@@ -0,0 +1,319 @@
|
||||
defmodule Module.Types.Of do
|
||||
# Typing functionality shared between Expr and Pattern.
|
||||
# Generic AST and Enum helpers go to Module.Types.Helpers.
|
||||
@moduledoc false
|
||||
|
||||
@prefix quote(do: ...)
|
||||
@suffix quote(do: ...)
|
||||
|
||||
alias Module.ParallelChecker
|
||||
|
||||
import Module.Types.Helpers
|
||||
import Module.Types.Unify
|
||||
|
||||
@doc """
|
||||
Handles open maps (with dynamic => dynamic).
|
||||
"""
|
||||
def open_map(args, stack, context, fun) do
|
||||
with {:ok, pairs, context} <- map_pairs(args, stack, context, fun) do
|
||||
pairs =
|
||||
for {key, value} <- pairs, not has_unbound_var?(key, context) do
|
||||
if singleton?(key, context) do
|
||||
{key, value}
|
||||
else
|
||||
{key, to_union([value, :dynamic], context)}
|
||||
end
|
||||
end
|
||||
|
||||
triplets = pairs_to_unions(pairs, [], context) ++ [{:optional, :dynamic, :dynamic}]
|
||||
{:ok, {:map, triplets}, context}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Handles closed maps (without dynamic => dynamic).
|
||||
"""
|
||||
def closed_map(args, stack, context, fun) do
|
||||
with {:ok, pairs, context} <- map_pairs(args, stack, context, fun) do
|
||||
{:ok, {:map, closed_to_unions(pairs, context)}, context}
|
||||
end
|
||||
end
|
||||
|
||||
defp map_pairs(pairs, stack, context, fun) do
|
||||
map_reduce_ok(pairs, context, fn {key, value}, context ->
|
||||
with {:ok, key_type, context} <- fun.(key, stack, context),
|
||||
{:ok, value_type, context} <- fun.(value, stack, context),
|
||||
do: {:ok, {key_type, value_type}, context}
|
||||
end)
|
||||
end
|
||||
|
||||
defp closed_to_unions([{key, value}], _context), do: [{:required, key, value}]
|
||||
|
||||
defp closed_to_unions(pairs, context) do
|
||||
case Enum.split_with(pairs, fn {key, _value} -> has_unbound_var?(key, context) end) do
|
||||
{[], pairs} -> pairs_to_unions(pairs, [], context)
|
||||
{[_ | _], pairs} -> pairs_to_unions([{:dynamic, :dynamic} | pairs], [], context)
|
||||
end
|
||||
end
|
||||
|
||||
defp pairs_to_unions([{key, value} | ahead], behind, context) do
|
||||
{matched_ahead, values} = find_matching_values(ahead, key, [], [])
|
||||
|
||||
# In case nothing matches, use the original ahead
|
||||
ahead = matched_ahead || ahead
|
||||
|
||||
all_values =
|
||||
[value | values] ++
|
||||
find_subtype_values(ahead, key, context) ++
|
||||
find_subtype_values(behind, key, context)
|
||||
|
||||
pairs_to_unions(ahead, [{key, to_union(all_values, context)} | behind], context)
|
||||
end
|
||||
|
||||
defp pairs_to_unions([], acc, context) do
|
||||
acc
|
||||
|> Enum.sort(&subtype?(elem(&1, 0), elem(&2, 0), context))
|
||||
|> Enum.map(fn {key, value} -> {:required, key, value} end)
|
||||
end
|
||||
|
||||
defp find_subtype_values(pairs, key, context) do
|
||||
for {pair_key, pair_value} <- pairs, subtype?(pair_key, key, context), do: pair_value
|
||||
end
|
||||
|
||||
defp find_matching_values([{key, value} | ahead], key, acc, values) do
|
||||
find_matching_values(ahead, key, acc, [value | values])
|
||||
end
|
||||
|
||||
defp find_matching_values([{_, _} = pair | ahead], key, acc, values) do
|
||||
find_matching_values(ahead, key, [pair | acc], values)
|
||||
end
|
||||
|
||||
defp find_matching_values([], _key, acc, [_ | _] = values), do: {Enum.reverse(acc), values}
|
||||
defp find_matching_values([], _key, _acc, []), do: {nil, []}
|
||||
|
||||
@doc """
|
||||
Handles structs.
|
||||
"""
|
||||
def struct(struct, meta, context) do
|
||||
context = remote(struct, :__struct__, 0, meta, context)
|
||||
|
||||
entries =
|
||||
for key <- Map.keys(struct.__struct__()), key != :__struct__ do
|
||||
{:required, {:atom, key}, :dynamic}
|
||||
end
|
||||
|
||||
{:ok, {:map, [{:required, {:atom, :__struct__}, {:atom, struct}} | entries]}, context}
|
||||
end
|
||||
|
||||
## Binary
|
||||
|
||||
@doc """
|
||||
Handles binaries.
|
||||
|
||||
In the stack, we add nodes such as <<expr>>, <<..., expr>>, etc,
|
||||
based on the position of the expression within the binary.
|
||||
"""
|
||||
def binary([], _stack, context, _fun) do
|
||||
{:ok, context}
|
||||
end
|
||||
|
||||
def binary([head], stack, context, fun) do
|
||||
head_stack = push_expr_stack({:<<>>, get_meta(head), [head]}, stack)
|
||||
binary_segment(head, head_stack, context, fun)
|
||||
end
|
||||
|
||||
def binary([head | tail], stack, context, fun) do
|
||||
head_stack = push_expr_stack({:<<>>, get_meta(head), [head, @suffix]}, stack)
|
||||
|
||||
case binary_segment(head, head_stack, context, fun) do
|
||||
{:ok, context} -> binary_many(tail, stack, context, fun)
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
defp binary_many([last], stack, context, fun) do
|
||||
last_stack = push_expr_stack({:<<>>, get_meta(last), [@prefix, last]}, stack)
|
||||
binary_segment(last, last_stack, context, fun)
|
||||
end
|
||||
|
||||
defp binary_many([head | tail], stack, context, fun) do
|
||||
head_stack = push_expr_stack({:<<>>, get_meta(head), [@prefix, head, @suffix]}, stack)
|
||||
|
||||
case binary_segment(head, head_stack, context, fun) do
|
||||
{:ok, context} -> binary_many(tail, stack, context, fun)
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
defp binary_segment({:"::", _meta, [expr, specifiers]}, stack, context, fun) do
|
||||
expected_type =
|
||||
collect_binary_specifier(specifiers, &binary_type(stack.context, &1)) || :integer
|
||||
|
||||
utf? = collect_binary_specifier(specifiers, &utf_type?/1)
|
||||
float? = collect_binary_specifier(specifiers, &float_type?/1)
|
||||
|
||||
# Special case utf and float specifiers because they can be two types as literals
|
||||
# but only a specific type as a variable in a pattern
|
||||
cond do
|
||||
stack.context == :pattern and utf? and is_binary(expr) ->
|
||||
{:ok, context}
|
||||
|
||||
stack.context == :pattern and float? and is_integer(expr) ->
|
||||
{:ok, context}
|
||||
|
||||
true ->
|
||||
with {:ok, type, context} <- fun.(expr, stack, context),
|
||||
{:ok, _type, context} <- unify(type, expected_type, stack, context),
|
||||
do: {:ok, context}
|
||||
end
|
||||
end
|
||||
|
||||
# Collect binary type specifiers,
|
||||
# from `<<pattern::integer-size(10)>>` collect `integer`
|
||||
defp collect_binary_specifier({:-, _meta, [left, right]}, fun) do
|
||||
collect_binary_specifier(left, fun) || collect_binary_specifier(right, fun)
|
||||
end
|
||||
|
||||
defp collect_binary_specifier(other, fun) do
|
||||
fun.(other)
|
||||
end
|
||||
|
||||
defp binary_type(:expr, {:float, _, _}), do: {:union, [:integer, :float]}
|
||||
defp binary_type(:expr, {:utf8, _, _}), do: {:union, [:integer, :binary]}
|
||||
defp binary_type(:expr, {:utf16, _, _}), do: {:union, [:integer, :binary]}
|
||||
defp binary_type(:expr, {:utf32, _, _}), do: {:union, [:integer, :binary]}
|
||||
defp binary_type(:pattern, {:utf8, _, _}), do: :integer
|
||||
defp binary_type(:pattern, {:utf16, _, _}), do: :integer
|
||||
defp binary_type(:pattern, {:utf32, _, _}), do: :integer
|
||||
defp binary_type(:pattern, {:float, _, _}), do: :float
|
||||
defp binary_type(_context, {:integer, _, _}), do: :integer
|
||||
defp binary_type(_context, {:bits, _, _}), do: :binary
|
||||
defp binary_type(_context, {:bitstring, _, _}), do: :binary
|
||||
defp binary_type(_context, {:bytes, _, _}), do: :binary
|
||||
defp binary_type(_context, {:binary, _, _}), do: :binary
|
||||
defp binary_type(_context, _specifier), do: nil
|
||||
|
||||
defp utf_type?({specifier, _, _}), do: specifier in [:utf8, :utf16, :utf32]
|
||||
defp utf_type?(_), do: false
|
||||
|
||||
defp float_type?({:float, _, _}), do: true
|
||||
defp float_type?(_), do: false
|
||||
|
||||
## Remote
|
||||
|
||||
@doc """
|
||||
Handles remote calls.
|
||||
"""
|
||||
def remote(module, fun, arity, meta, context) when is_atom(module) do
|
||||
# TODO: In the future we may want to warn for modules defined
|
||||
# in the local context
|
||||
if Keyword.get(meta, :context_module, false) and context.module != module do
|
||||
context
|
||||
else
|
||||
ParallelChecker.preload_module(context.cache, module)
|
||||
check_export(module, fun, arity, meta, context)
|
||||
end
|
||||
end
|
||||
|
||||
def remote(_module, _fun, _arity, _meta, context), do: context
|
||||
|
||||
defp check_export(module, fun, arity, meta, context) do
|
||||
case ParallelChecker.fetch_export(context.cache, module, fun, arity) do
|
||||
{:ok, :def, reason} ->
|
||||
check_deprecated(module, fun, arity, reason, meta, context)
|
||||
|
||||
{:ok, :defmacro, reason} ->
|
||||
context = warn(meta, context, {:unrequired_module, module, fun, arity})
|
||||
check_deprecated(module, fun, arity, reason, meta, context)
|
||||
|
||||
{:error, :module} ->
|
||||
if warn_undefined?(module, fun, arity, context) do
|
||||
warn(meta, context, {:undefined_module, module, fun, arity})
|
||||
else
|
||||
context
|
||||
end
|
||||
|
||||
{:error, :function} ->
|
||||
if warn_undefined?(module, fun, arity, context) do
|
||||
exports = ParallelChecker.all_exports(context.cache, module)
|
||||
warn(meta, context, {:undefined_function, module, fun, arity, exports})
|
||||
else
|
||||
context
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp check_deprecated(module, fun, arity, reason, meta, context) do
|
||||
if reason do
|
||||
warn(meta, context, {:deprecated, module, fun, arity, reason})
|
||||
else
|
||||
context
|
||||
end
|
||||
end
|
||||
|
||||
# The protocol code dispatches to unknown modules, so we ignore them here.
|
||||
#
|
||||
# try do
|
||||
# SomeProtocol.Atom.__impl__
|
||||
# rescue
|
||||
# ...
|
||||
# end
|
||||
#
|
||||
# But for protocols we don't want to traverse the protocol code anyway.
|
||||
# TODO: remove this clause once we no longer traverse the protocol code.
|
||||
defp warn_undefined?(_module, :__impl__, 1, _context), do: false
|
||||
defp warn_undefined?(_module, :module_info, 0, _context), do: false
|
||||
defp warn_undefined?(_module, :module_info, 1, _context), do: false
|
||||
defp warn_undefined?(:erlang, :orelse, 2, _context), do: false
|
||||
defp warn_undefined?(:erlang, :andalso, 2, _context), do: false
|
||||
|
||||
defp warn_undefined?(_, _, _, %{no_warn_undefined: :all}) do
|
||||
false
|
||||
end
|
||||
|
||||
defp warn_undefined?(module, fun, arity, context) do
|
||||
not Enum.any?(context.no_warn_undefined, &(&1 == module or &1 == {module, fun, arity}))
|
||||
end
|
||||
|
||||
defp warn(meta, context, warning) do
|
||||
{fun, arity} = context.function
|
||||
location = {context.file, meta[:line] || 0, {context.module, fun, arity}}
|
||||
%{context | warnings: [{__MODULE__, warning, location} | context.warnings]}
|
||||
end
|
||||
|
||||
## Warning formating
|
||||
|
||||
def format_warning({:undefined_module, module, fun, arity}) do
|
||||
[
|
||||
Exception.format_mfa(module, fun, arity),
|
||||
" is undefined (module ",
|
||||
inspect(module),
|
||||
" is not available or is yet to be defined)"
|
||||
]
|
||||
end
|
||||
|
||||
def format_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
|
||||
end
|
||||
@@ -1,12 +1,29 @@
|
||||
defmodule Module.Types.Pattern do
|
||||
@moduledoc false
|
||||
|
||||
import Module.Types.{Helpers, Infer}
|
||||
alias Module.Types.Of
|
||||
import Module.Types.{Helpers, Unify}
|
||||
|
||||
@doc """
|
||||
Handles patterns and guards at once.
|
||||
"""
|
||||
def of_head(patterns, guards, stack, context) do
|
||||
with {:ok, types, context} <-
|
||||
map_reduce_ok(patterns, context, &of_pattern(&1, stack, &2)),
|
||||
# TODO: Check that of_guard/3 returns boolean() | :fail
|
||||
{:ok, _, context} <- of_guard(guards_to_or(guards), stack, context),
|
||||
do: {:ok, types, context}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Return the type and typing context of a pattern expression or an error
|
||||
in case of a typing conflict.
|
||||
"""
|
||||
def of_pattern(pattern, %{context: stack_context} = stack, context)
|
||||
when stack_context != :pattern do
|
||||
of_pattern(pattern, %{stack | context: :pattern}, context)
|
||||
end
|
||||
|
||||
# :atom
|
||||
def of_pattern(atom, _stack, context) when is_atom(atom) do
|
||||
{:ok, {:atom, atom}, context}
|
||||
@@ -28,10 +45,10 @@ defmodule Module.Types.Pattern do
|
||||
end
|
||||
|
||||
# <<...>>>
|
||||
def of_pattern({:<<>>, _meta, args} = expr, stack, context) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
def of_pattern({:<<>>, _meta, args}, stack, context) do
|
||||
result = Of.binary(args, stack, context, &of_pattern/3)
|
||||
|
||||
case reduce_ok(args, context, &of_binary(&1, stack, &2)) do
|
||||
case result do
|
||||
{:ok, context} -> {:ok, :binary, context}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
@@ -111,6 +128,11 @@ defmodule Module.Types.Pattern do
|
||||
{:ok, :dynamic, context}
|
||||
end
|
||||
|
||||
# ^var
|
||||
def of_pattern({:^, _meta, [var]}, _stack, context) do
|
||||
{:ok, get_var!(var, context), context}
|
||||
end
|
||||
|
||||
# var
|
||||
def of_pattern(var, _stack, context) when is_var(var) do
|
||||
{type, context} = new_var(var, context)
|
||||
@@ -127,7 +149,7 @@ defmodule Module.Types.Pattern do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
|
||||
case map_reduce_ok(exprs, context, &of_pattern(&1, stack, &2)) do
|
||||
{:ok, types, context} -> {:ok, {:tuple, types}, context}
|
||||
{:ok, types, context} -> {:ok, {:tuple, length(types), types}, context}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
@@ -144,10 +166,17 @@ defmodule Module.Types.Pattern do
|
||||
# %{...}
|
||||
def of_pattern({:%{}, _meta, args} = expr, stack, context) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
Of.open_map(args, stack, context, &of_pattern/3)
|
||||
end
|
||||
|
||||
case of_pairs(args, stack, context) do
|
||||
{:ok, pairs, context} -> {:ok, {:map, pairs_to_unions(pairs, context)}, context}
|
||||
{:error, reason} -> {:error, reason}
|
||||
# %Struct{...}
|
||||
def of_pattern({:%, meta1, [module, {:%{}, _meta2, args}]} = expr, stack, context)
|
||||
when is_atom(module) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
|
||||
with {:ok, struct, context} <- Of.struct(module, meta1, context),
|
||||
{:ok, map, context} <- Of.open_map(args, stack, context, &of_pattern/3) do
|
||||
unify(map, struct, stack, context)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -160,121 +189,80 @@ defmodule Module.Types.Pattern do
|
||||
when is_atom(var_context) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
|
||||
case of_pairs(args, stack, context) do
|
||||
{:ok, pairs, context} ->
|
||||
pairs = [{{:atom, :__struct__}, :atom} | pairs]
|
||||
{:ok, {:map, pairs}, context}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
with {:ok, {:map, pairs}, context} <- Of.open_map(args, stack, context, &of_pattern/3) do
|
||||
{:ok, {:map, [{:required, {:atom, :__struct__}, :atom} | pairs]}, context}
|
||||
end
|
||||
end
|
||||
|
||||
# %var{...}
|
||||
def of_pattern({:%, _meta1, [var, {:%{}, _meta2, args}]} = expr, stack, context)
|
||||
when is_var(var) do
|
||||
# %var{...} and %^var{...}
|
||||
def of_pattern({:%, _meta1, [var, {:%{}, _meta2, args}]} = expr, stack, context) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
|
||||
with {:ok, pairs, context} <- of_pairs(args, stack, context),
|
||||
{var_type, context} = new_var(var, context),
|
||||
{:ok, _, context} <- unify(var_type, :atom, stack, context) do
|
||||
pairs = [{{:atom, :__struct__}, var_type} | pairs]
|
||||
{:ok, {:map, pairs}, context}
|
||||
with {:ok, var_type, context} = of_pattern(var, stack, context),
|
||||
{:ok, _, context} <- unify(var_type, :atom, stack, context),
|
||||
{:ok, {:map, pairs}, context} <- Of.open_map(args, stack, context, &of_pattern/3) do
|
||||
{:ok, {:map, [{:required, {:atom, :__struct__}, var_type} | pairs]}, context}
|
||||
end
|
||||
end
|
||||
|
||||
# %Struct{...}
|
||||
def of_pattern({:%, _meta1, [module, {:%{}, _meta2, args}]} = expr, stack, context)
|
||||
when is_atom(module) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
|
||||
case of_pairs(args, stack, context) do
|
||||
{:ok, pairs, context} ->
|
||||
pairs = [{{:atom, :__struct__}, {:atom, module}} | pairs]
|
||||
{:ok, {:map, pairs}, context}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
defp of_pairs(pairs, stack, context) do
|
||||
map_reduce_ok(pairs, context, fn {key, value}, context ->
|
||||
with {:ok, key_type, context} <- of_pattern(key, stack, context),
|
||||
{:ok, value_type, context} <- of_pattern(value, stack, context),
|
||||
do: {:ok, {key_type, value_type}, context}
|
||||
end)
|
||||
end
|
||||
|
||||
defp pairs_to_unions(pairs, context) do
|
||||
# Maps only allow simple literal keys in patterns so
|
||||
# we do not have to do subtype checking
|
||||
|
||||
Enum.reduce(pairs, [], fn {key, value}, pairs ->
|
||||
case :lists.keyfind(key, 1, pairs) do
|
||||
{^key, {:union, union}} ->
|
||||
:lists.keystore(key, 1, pairs, {key, to_union([value | union], context)})
|
||||
|
||||
{^key, original_value} ->
|
||||
:lists.keystore(key, 1, pairs, {key, to_union([value, original_value], context)})
|
||||
|
||||
false ->
|
||||
[{key, value} | pairs]
|
||||
end
|
||||
end)
|
||||
end
|
||||
def unify_kinds(:required, _), do: :required
|
||||
def unify_kinds(_, :required), do: :required
|
||||
def unify_kinds(:optional, :optional), do: :optional
|
||||
|
||||
## GUARDS
|
||||
|
||||
# TODO: Some guards can be changed to intersection types or higher order types
|
||||
@boolean {:union, [{:atom, true}, {:atom, false}]}
|
||||
@number {:union, [:integer, :float]}
|
||||
|
||||
@guard_functions %{
|
||||
{:is_atom, 1} => {[:atom], :boolean},
|
||||
{:is_binary, 1} => {[:binary], :boolean},
|
||||
{:is_bitstring, 1} => {[:binary], :boolean},
|
||||
{:is_boolean, 1} => {[:boolean], :boolean},
|
||||
{:is_float, 1} => {[:float], :boolean},
|
||||
{:is_function, 1} => {[:fun], :boolean},
|
||||
{:is_function, 2} => {[:fun, :integer], :boolean},
|
||||
{:is_integer, 1} => {[:integer], :boolean},
|
||||
{:is_list, 1} => {[{:list, :dynamic}], :boolean},
|
||||
{:is_map, 1} => {[{:map, []}], :boolean},
|
||||
{:is_map_key, 2} => {[:dynamic, {:map, []}], :dynamic},
|
||||
{:is_number, 1} => {[:number], :boolean},
|
||||
{:is_pid, 1} => {[:pid], :boolean},
|
||||
{:is_port, 1} => {[:port], :boolean},
|
||||
{:is_reference, 1} => {[:reference], :boolean},
|
||||
{:is_tuple, 1} => {[:tuple], :boolean},
|
||||
{:<, 2} => {[:dynamic, :dynamic], :boolean},
|
||||
{:"=<", 2} => {[:dynamic, :dynamic], :boolean},
|
||||
{:>, 2} => {[:dynamic, :dynamic], :boolean},
|
||||
{:>=, 2} => {[:dynamic, :dynamic], :boolean},
|
||||
{:"/=", 2} => {[:dynamic, :dynamic], :boolean},
|
||||
{:"=/=", 2} => {[:dynamic, :dynamic], :boolean},
|
||||
{:==, 2} => {[:dynamic, :dynamic], :boolean},
|
||||
{:"=:=", 2} => {[:dynamic, :dynamic], :boolean},
|
||||
{:*, 2} => {[:number, :number], :number},
|
||||
{:+, 1} => {[:number], :number},
|
||||
{:+, 2} => {[:number, :number], :number},
|
||||
{:-, 1} => {[:number], :number},
|
||||
{:-, 2} => {[:number, :number], :number},
|
||||
{:/, 2} => {[:number, :number], :number},
|
||||
{:abs, 1} => {[:number], :number},
|
||||
{:ceil, 1} => {[:number], :integer},
|
||||
{:floor, 1} => {[:number], :integer},
|
||||
{:round, 1} => {[:number], :integer},
|
||||
{:trunc, 1} => {[:number], :integer},
|
||||
{:is_atom, 1} => {[:atom], @boolean},
|
||||
{:is_binary, 1} => {[:binary], @boolean},
|
||||
{:is_bitstring, 1} => {[:binary], @boolean},
|
||||
{:is_boolean, 1} => {[@boolean], @boolean},
|
||||
{:is_float, 1} => {[:float], @boolean},
|
||||
{:is_function, 1} => {[:fun], @boolean},
|
||||
{:is_function, 2} => {[:fun, :integer], @boolean},
|
||||
{:is_integer, 1} => {[:integer], @boolean},
|
||||
{:is_list, 1} => {[{:list, :dynamic}], @boolean},
|
||||
{:is_map, 1} => {[{:map, [{:optional, :dynamic, :dynamic}]}], @boolean},
|
||||
{:is_map_key, 2} => {[:dynamic, {:map, [{:optional, :dynamic, :dynamic}]}], :dynamic},
|
||||
{:is_number, 1} => {[@number], @boolean},
|
||||
{:is_pid, 1} => {[:pid], @boolean},
|
||||
{:is_port, 1} => {[:port], @boolean},
|
||||
{:is_reference, 1} => {[:reference], @boolean},
|
||||
{:is_tuple, 1} => {[:tuple], @boolean},
|
||||
{:<, 2} => {[:dynamic, :dynamic], @boolean},
|
||||
{:"=<", 2} => {[:dynamic, :dynamic], @boolean},
|
||||
{:>, 2} => {[:dynamic, :dynamic], @boolean},
|
||||
{:>=, 2} => {[:dynamic, :dynamic], @boolean},
|
||||
{:"/=", 2} => {[:dynamic, :dynamic], @boolean},
|
||||
{:"=/=", 2} => {[:dynamic, :dynamic], @boolean},
|
||||
{:==, 2} => {[:dynamic, :dynamic], @boolean},
|
||||
{:"=:=", 2} => {[:dynamic, :dynamic], @boolean},
|
||||
{:*, 2} => {[@number, @number], @number},
|
||||
{:+, 1} => {[@number], @number},
|
||||
{:+, 2} => {[@number, @number], @number},
|
||||
{:-, 1} => {[@number], @number},
|
||||
{:-, 2} => {[@number, @number], @number},
|
||||
{:/, 2} => {[@number, @number], @number},
|
||||
{:abs, 1} => {[@number], @number},
|
||||
{:ceil, 1} => {[@number], :integer},
|
||||
{:floor, 1} => {[@number], :integer},
|
||||
{:round, 1} => {[@number], :integer},
|
||||
{:trunc, 1} => {[@number], :integer},
|
||||
{:element, 2} => {[:integer, :tuple], :dynamic},
|
||||
{:hd, 1} => {[{:list, :dynamic}], :dynamic},
|
||||
{:length, 1} => {[{:list, :dynamic}], :integer},
|
||||
{:map_get, 2} => {[:dynamic, {:map, []}], :dynamic},
|
||||
{:map_size, 1} => {[{:map, []}], :integer},
|
||||
{:map_get, 2} => {[:dynamic, {:map, [{:optional, :dynamic, :dynamic}]}], :dynamic},
|
||||
{:map_size, 1} => {[{:map, [{:optional, :dynamic, :dynamic}]}], :integer},
|
||||
{:tl, 1} => {[{:list, :dynamic}], :dynamic},
|
||||
{:tuple_size, 1} => {[:tuple], :integer},
|
||||
{:node, 1} => {[{:union, [:pid, :reference, :port]}], :atom},
|
||||
{:binary_part, 3} => {[:binary, :integer, :integer], :binary},
|
||||
{:bit_size, 1} => {[:binary], :integer},
|
||||
{:byte_size, 1} => {[:binary], :integer},
|
||||
{:size, 1} => {[{:union, [:binary, :tuple]}], @boolean},
|
||||
{:div, 2} => {[:integer, :integer], :integer},
|
||||
{:rem, 2} => {[:integer, :integer], :integer},
|
||||
{:node, 0} => {[], :atom},
|
||||
@@ -285,13 +273,15 @@ defmodule Module.Types.Pattern do
|
||||
{:bxor, 2} => {[:integer, :integer], :integer},
|
||||
{:bsl, 2} => {[:integer, :integer], :integer},
|
||||
{:bsr, 2} => {[:integer, :integer], :integer},
|
||||
{:xor, 2} => {[:boolean, :boolean], :boolean},
|
||||
{:not, 1} => {[:boolean], :boolean}
|
||||
{:or, 2} => {[@boolean, @boolean], @boolean},
|
||||
{:and, 2} => {[@boolean, @boolean], @boolean},
|
||||
{:xor, 2} => {[@boolean, @boolean], @boolean},
|
||||
{:not, 1} => {[@boolean], @boolean}
|
||||
|
||||
# Following guards are matched explicitly to handle
|
||||
# type guard functions such as is_atom/1
|
||||
# {:andalso, 2} => {[:boolean, :boolean], :boolean}
|
||||
# {:orelse, 2} => {[:boolean, :boolean], :boolean}
|
||||
# {:andalso, 2} => {[@boolean, @boolean], @boolean}
|
||||
# {:orelse, 2} => {[@boolean, @boolean], @boolean}
|
||||
}
|
||||
|
||||
@type_guards [
|
||||
@@ -316,6 +306,10 @@ defmodule Module.Types.Pattern do
|
||||
Refines the type variables in the typing context using type check guards
|
||||
such as `is_integer/1`.
|
||||
"""
|
||||
def of_guard(expr, %{context: stack_context} = stack, context) when stack_context != :pattern do
|
||||
of_guard(expr, %{stack | context: :pattern}, context)
|
||||
end
|
||||
|
||||
def of_guard({{:., _, [:erlang, :andalso]}, _, [left, right]} = expr, stack, context) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
fresh_context = fresh_context(context)
|
||||
@@ -323,9 +317,9 @@ defmodule Module.Types.Pattern do
|
||||
with {:ok, left_type, left_context} <- of_guard(left, stack, fresh_context),
|
||||
{:ok, right_type, right_context} <- of_guard(right, stack, fresh_context),
|
||||
{:ok, context} <- merge_context_and(context, stack, left_context, right_context),
|
||||
{:ok, _, context} <- unify(left_type, :boolean, stack, context),
|
||||
{:ok, _, context} <- unify(right_type, :boolean, stack, context),
|
||||
do: {:ok, :boolean, context}
|
||||
{:ok, _, context} <- unify(left_type, @boolean, stack, context),
|
||||
{:ok, _, context} <- unify(right_type, @boolean, stack, context),
|
||||
do: {:ok, @boolean, context}
|
||||
end
|
||||
|
||||
def of_guard({{:., _, [:erlang, :orelse]}, _, [left, right]} = expr, stack, context) do
|
||||
@@ -333,13 +327,27 @@ defmodule Module.Types.Pattern do
|
||||
fresh_context = fresh_context(context)
|
||||
|
||||
with {:ok, left_type, left_context} <- of_guard(left, stack, fresh_context),
|
||||
{:ok, right_type, right_context} <- of_guard(right, stack, fresh_context),
|
||||
{:ok, _right_type, right_context} <- of_guard(right, stack, fresh_context),
|
||||
{:ok, context} <- merge_context_or(context, stack, left_context, right_context),
|
||||
{:ok, _, context} <- unify(left_type, :boolean, stack, context),
|
||||
{:ok, _, context} <- unify(right_type, :boolean, stack, context),
|
||||
do: {:ok, :boolean, context}
|
||||
{:ok, _, context} <- unify(left_type, @boolean, stack, context),
|
||||
do: {:ok, @boolean, context}
|
||||
end
|
||||
|
||||
# The unary operators + and - are special cased to avoid common warnings until
|
||||
# we add support for intersection types for the guard functions
|
||||
# -integer / +integer
|
||||
def of_guard({{:., _, [:erlang, guard]}, _, [integer]}, _stack, context)
|
||||
when guard in [:+, :-] and is_integer(integer) do
|
||||
{:ok, :integer, context}
|
||||
end
|
||||
|
||||
# -float / +float
|
||||
def of_guard({{:., _, [:erlang, guard]}, _, [float]}, _stack, context)
|
||||
when guard in [:+, :-] and is_float(float) do
|
||||
{:ok, :float, context}
|
||||
end
|
||||
|
||||
# fun(args)
|
||||
def of_guard({{:., _, [:erlang, guard]}, _, args} = expr, stack, context) do
|
||||
stack = push_expr_stack(expr, stack)
|
||||
{param_types, return_type} = guard_signature(guard, length(args))
|
||||
@@ -353,7 +361,7 @@ defmodule Module.Types.Pattern do
|
||||
|
||||
with {:ok, arg_types, context} <-
|
||||
map_reduce_ok(args, context, &of_guard(&1, arg_stack, &2)),
|
||||
{:ok, context} <- unify_call(param_types, arg_types, stack, context) do
|
||||
{:ok, context} <- unify_call(arg_types, param_types, stack, context) do
|
||||
{arg_types, guard_sources} =
|
||||
case arg_types do
|
||||
[{:var, index} | rest_arg_types] when type_guard? ->
|
||||
@@ -382,11 +390,18 @@ defmodule Module.Types.Pattern do
|
||||
end
|
||||
end
|
||||
|
||||
# map.field
|
||||
def of_guard({{:., meta1, [map, field]}, meta2, []}, stack, context) do
|
||||
of_guard({{:., meta1, [:erlang, :map_get]}, meta2, [field, map]}, stack, context)
|
||||
end
|
||||
|
||||
# var
|
||||
def of_guard(var, _stack, context) when is_var(var) do
|
||||
type = Map.fetch!(context.vars, var_name(var))
|
||||
{:ok, type, context}
|
||||
end
|
||||
|
||||
# other literals
|
||||
def of_guard(expr, stack, context) do
|
||||
# Fall back to of_pattern/3 for literals
|
||||
of_pattern(expr, stack, context)
|
||||
@@ -398,9 +413,9 @@ defmodule Module.Types.Pattern do
|
||||
%{context | types: types, traces: traces}
|
||||
end
|
||||
|
||||
defp unify_call(params, args, stack, context) do
|
||||
reduce_ok(Enum.zip(params, args), context, fn {param, arg}, context ->
|
||||
case unify(param, arg, stack, context) do
|
||||
defp unify_call(args, params, stack, context) do
|
||||
reduce_ok(Enum.zip(args, params), context, fn {arg, param}, context ->
|
||||
case unify(arg, param, stack, context) do
|
||||
{:ok, _, context} -> {:ok, context}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
@@ -445,7 +460,6 @@ defmodule Module.Types.Pattern do
|
||||
|
||||
cond do
|
||||
:fail in sources -> [:fail]
|
||||
:guarded_fail in sources -> [:guarded_fail]
|
||||
:guarded in sources -> [:guarded]
|
||||
true -> []
|
||||
end
|
||||
@@ -456,7 +470,7 @@ defmodule Module.Types.Pattern do
|
||||
# When the failing guard function wont fail due to type check function before it,
|
||||
# for example: is_list(x) and length(x)
|
||||
if :guarded in left and :fail in right do
|
||||
[:guarded_fail]
|
||||
[:guarded]
|
||||
else
|
||||
join_guard_source(left, right)
|
||||
end
|
||||
@@ -529,52 +543,6 @@ defmodule Module.Types.Pattern do
|
||||
{:ok, context}
|
||||
end
|
||||
|
||||
# binary-pattern :: specifier
|
||||
defp of_binary({:"::", _meta, [expr, specifiers]} = full_expr, stack, context) do
|
||||
{expected_type, utf?} = collect_binary_type(specifiers) || {:integer, false}
|
||||
stack = push_expr_stack(full_expr, stack)
|
||||
|
||||
# Special case utf specifiers with binary literals since they allow
|
||||
# both integer and binary literals but variables are always integer
|
||||
if is_binary(expr) and utf? do
|
||||
{:ok, context}
|
||||
else
|
||||
with {:ok, type, context} <- of_pattern(expr, stack, context),
|
||||
{:ok, _type, context} <- unify(type, expected_type, stack, context),
|
||||
do: {:ok, context}
|
||||
end
|
||||
end
|
||||
|
||||
# binary-pattern
|
||||
defp of_binary(expr, stack, context) do
|
||||
case of_pattern(expr, stack, context) do
|
||||
{:ok, type, context} when type in [:integer, :float, :binary] ->
|
||||
{:ok, context}
|
||||
|
||||
{:ok, type, _context} ->
|
||||
{:error, {:invalid_binary_type, type}}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
# Collect binary type specifiers,
|
||||
# from `<<pattern::integer-size(10)>>` collect `integer`
|
||||
defp collect_binary_type({:-, _meta, [left, right]}),
|
||||
do: collect_binary_type(left) || collect_binary_type(right)
|
||||
|
||||
defp collect_binary_type({:integer, _, _}), do: {:integer, false}
|
||||
defp collect_binary_type({:float, _, _}), do: {:float, false}
|
||||
defp collect_binary_type({:bits, _, _}), do: {:binary, false}
|
||||
defp collect_binary_type({:bitstring, _, _}), do: {:binary, false}
|
||||
defp collect_binary_type({:bytes, _, _}), do: {:binary, false}
|
||||
defp collect_binary_type({:binary, _, _}), do: {:binary, false}
|
||||
defp collect_binary_type({:utf8, _, _}), do: {:integer, true}
|
||||
defp collect_binary_type({:utf16, _, _}), do: {:integer, true}
|
||||
defp collect_binary_type({:utf32, _, _}), do: {:integer, true}
|
||||
defp collect_binary_type(_), do: nil
|
||||
|
||||
defp guard_signature(name, arity) do
|
||||
Map.fetch!(@guard_functions, {name, arity})
|
||||
end
|
||||
|
||||
@@ -0,0 +1,700 @@
|
||||
defmodule Module.Types.Unify do
|
||||
@moduledoc false
|
||||
|
||||
import Module.Types.Helpers
|
||||
|
||||
# Those are the simple types known to the system:
|
||||
#
|
||||
# :dynamic
|
||||
# {:var, var}
|
||||
# {:atom, atom} < :atom
|
||||
# :integer
|
||||
# :float
|
||||
# :pid
|
||||
# :port
|
||||
# :reference
|
||||
#
|
||||
# Those are the composite types:
|
||||
#
|
||||
# {:list, type}
|
||||
# {:tuple, size, [type]} < :tuple
|
||||
# {:union, [type]}
|
||||
# {:map, [{:required | :optional, key_type, value_type}]}
|
||||
#
|
||||
# Once new types are added, they should be considered in:
|
||||
#
|
||||
# * unify (all)
|
||||
# * format_type (all)
|
||||
# * subtype? (subtypes only)
|
||||
# * has_unbound_var? (composite only)
|
||||
# * recursive_type? (composite only)
|
||||
#
|
||||
|
||||
@doc """
|
||||
Unifies two types and returns the unified type and an updated typing context
|
||||
or an error in case of a typing conflict.
|
||||
"""
|
||||
def unify(source, target, stack, context) do
|
||||
case do_unify(source, target, stack, context) do
|
||||
{:ok, type, context} ->
|
||||
{:ok, type, context}
|
||||
|
||||
{:error, reason} ->
|
||||
if stack.context == :pattern do
|
||||
case do_unify(target, source, stack, context) do
|
||||
{:ok, type, context} ->
|
||||
{:ok, type, context}
|
||||
|
||||
{:error, _} ->
|
||||
{:error, reason}
|
||||
end
|
||||
else
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp do_unify(same, same, _stack, context) do
|
||||
{:ok, same, context}
|
||||
end
|
||||
|
||||
defp do_unify(type, {:var, var}, stack, context) do
|
||||
case context.types do
|
||||
%{^var => {:var, var_type}} ->
|
||||
do_unify(type, {:var, var_type}, stack, context)
|
||||
|
||||
%{} ->
|
||||
unify_var(var, type, stack, context, _var_source = false)
|
||||
end
|
||||
end
|
||||
|
||||
defp do_unify({:var, var}, type, stack, context) do
|
||||
case context.types do
|
||||
%{^var => {:var, var_type}} ->
|
||||
do_unify({:var, var_type}, type, stack, context)
|
||||
|
||||
%{} ->
|
||||
unify_var(var, type, stack, context, _var_source = true)
|
||||
end
|
||||
end
|
||||
|
||||
defp do_unify({:tuple, n, sources}, {:tuple, n, targets}, stack, context) do
|
||||
result =
|
||||
map_reduce_ok(Enum.zip(sources, targets), context, fn {source, target}, context ->
|
||||
unify(source, target, stack, context)
|
||||
end)
|
||||
|
||||
case result do
|
||||
{:ok, types, context} -> {:ok, {:tuple, n, types}, context}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
defp do_unify({:list, source}, {:list, target}, stack, context) do
|
||||
case unify(source, target, stack, context) do
|
||||
{:ok, type, context} -> {:ok, {:list, type}, context}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
defp do_unify({:map, source_pairs}, {:map, target_pairs}, stack, context) do
|
||||
unify_maps(source_pairs, target_pairs, stack, context)
|
||||
end
|
||||
|
||||
defp do_unify(source, :dynamic, _stack, context) do
|
||||
{:ok, source, context}
|
||||
end
|
||||
|
||||
defp do_unify(:dynamic, target, _stack, context) do
|
||||
{:ok, target, context}
|
||||
end
|
||||
|
||||
defp do_unify(source, target, stack, context) do
|
||||
cond do
|
||||
# This condition exists to handle unions with unbound vars.
|
||||
# TODO: handle unions properly. Note we can easily unify
|
||||
# "union < type" even if union has vars as the vars must be
|
||||
# type
|
||||
(match?({:union, _}, source) and has_unbound_var?(source, context)) or
|
||||
(match?({:union, _}, target) and has_unbound_var?(target, context)) ->
|
||||
{:ok, source, context}
|
||||
|
||||
subtype?(source, target, context) ->
|
||||
{:ok, source, context}
|
||||
|
||||
true ->
|
||||
error(:unable_unify, {source, target, stack}, context)
|
||||
end
|
||||
end
|
||||
|
||||
defp unify_var(var, :dynamic, _stack, context, _var_source?) do
|
||||
{:ok, {:var, var}, context}
|
||||
end
|
||||
|
||||
defp unify_var(var, type, stack, context, var_source?) do
|
||||
case context.types do
|
||||
%{^var => :unbound} ->
|
||||
context = refine_var(var, type, stack, context)
|
||||
stack = push_unify_stack(var, stack)
|
||||
|
||||
if recursive_type?(type, [], context) do
|
||||
if var_source? do
|
||||
error(:unable_unify, {{:var, var}, type, stack}, context)
|
||||
else
|
||||
error(:unable_unify, {type, {:var, var}, stack}, context)
|
||||
end
|
||||
else
|
||||
{:ok, {:var, var}, context}
|
||||
end
|
||||
|
||||
%{^var => var_type} ->
|
||||
# Only add trace if the variable wasn't already "expanded"
|
||||
context =
|
||||
if variable_expanded?(var, stack, context) do
|
||||
context
|
||||
else
|
||||
trace_var(var, type, stack, context)
|
||||
end
|
||||
|
||||
stack = push_unify_stack(var, stack)
|
||||
|
||||
unify_result =
|
||||
if var_source? do
|
||||
unify(var_type, type, stack, context)
|
||||
else
|
||||
unify(type, var_type, stack, context)
|
||||
end
|
||||
|
||||
case unify_result do
|
||||
{:ok, var_type, context} ->
|
||||
context = refine_var(var, var_type, stack, context)
|
||||
{:ok, {:var, var}, context}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# * All required keys on each side need to match to the other side.
|
||||
# * All optional keys on each side that do not match must be discarded.
|
||||
|
||||
defp unify_maps(source_pairs, target_pairs, stack, context) do
|
||||
{source_required, source_optional} = split_pairs(source_pairs)
|
||||
{target_required, target_optional} = split_pairs(target_pairs)
|
||||
|
||||
with {:ok, source_required_pairs, context} <-
|
||||
unify_source_required(source_required, target_pairs, stack, context),
|
||||
{:ok, target_required_pairs, context} <-
|
||||
unify_target_required(target_required, source_pairs, stack, context),
|
||||
{:ok, source_optional_pairs, context} <-
|
||||
unify_source_optional(source_optional, target_optional, stack, context),
|
||||
{:ok, target_optional_pairs, context} <-
|
||||
unify_target_optional(target_optional, source_optional, stack, context) do
|
||||
# Remove duplicate pairs from matching in both left and right directions
|
||||
pairs =
|
||||
Enum.uniq(
|
||||
source_required_pairs ++
|
||||
target_required_pairs ++
|
||||
source_optional_pairs ++
|
||||
target_optional_pairs
|
||||
)
|
||||
|
||||
{:ok, {:map, pairs}, context}
|
||||
else
|
||||
{:error, :unify} ->
|
||||
error(:unable_unify, {{:map, source_pairs}, {:map, target_pairs}, stack}, context)
|
||||
|
||||
{:error, context} ->
|
||||
{:error, context}
|
||||
end
|
||||
end
|
||||
|
||||
defp unify_source_required(source_required, target_pairs, stack, context) do
|
||||
map_reduce_ok(source_required, context, fn {source_key, source_value}, context ->
|
||||
Enum.find_value(target_pairs, fn {target_kind, target_key, target_value} ->
|
||||
with {:ok, key, context} <- unify(source_key, target_key, stack, context) do
|
||||
case unify(source_value, target_value, stack, context) do
|
||||
{:ok, value, context} ->
|
||||
{:ok, {:required, key, value}, context}
|
||||
|
||||
{:error, _reason} ->
|
||||
source_map = {:map, [{:required, source_key, source_value}]}
|
||||
target_map = {:map, [{target_kind, target_key, target_value}]}
|
||||
error(:unable_unify, {source_map, target_map, stack}, context)
|
||||
end
|
||||
else
|
||||
{:error, _reason} -> nil
|
||||
end
|
||||
end) || {:error, :unify}
|
||||
end)
|
||||
end
|
||||
|
||||
defp unify_target_required(target_required, source_pairs, stack, context) do
|
||||
map_reduce_ok(target_required, context, fn {target_key, target_value}, context ->
|
||||
Enum.find_value(source_pairs, fn {source_kind, source_key, source_value} ->
|
||||
with {:ok, key, context} <- unify(source_key, target_key, stack, context) do
|
||||
case unify(source_value, target_value, stack, context) do
|
||||
{:ok, value, context} ->
|
||||
{:ok, {:required, key, value}, context}
|
||||
|
||||
{:error, _reason} ->
|
||||
source_map = {:map, [{source_kind, source_key, source_value}]}
|
||||
target_map = {:map, [{:required, target_key, target_value}]}
|
||||
error(:unable_unify, {source_map, target_map, stack}, context)
|
||||
end
|
||||
else
|
||||
{:error, _reason} -> nil
|
||||
end
|
||||
end) || {:error, :unify}
|
||||
end)
|
||||
end
|
||||
|
||||
defp unify_source_optional(source_optional, target_optional, stack, context) do
|
||||
flat_map_reduce_ok(source_optional, context, fn {source_key, source_value}, context ->
|
||||
Enum.find_value(target_optional, fn {target_key, target_value} ->
|
||||
with {:ok, key, context} <- unify(source_key, target_key, stack, context) do
|
||||
case unify(source_value, target_value, stack, context) do
|
||||
{:ok, value, context} ->
|
||||
{:ok, [{:optional, key, value}], context}
|
||||
|
||||
{:error, _reason} ->
|
||||
source_map = {:map, [{:optional, source_key, source_value}]}
|
||||
target_map = {:map, [{:optional, target_key, target_value}]}
|
||||
error(:unable_unify, {source_map, target_map, stack}, context)
|
||||
end
|
||||
else
|
||||
_ -> nil
|
||||
end
|
||||
end) || {:ok, [], context}
|
||||
end)
|
||||
end
|
||||
|
||||
defp unify_target_optional(target_optional, source_optional, stack, context) do
|
||||
flat_map_reduce_ok(target_optional, context, fn {target_key, target_value}, context ->
|
||||
Enum.find_value(source_optional, fn {source_key, source_value} ->
|
||||
with {:ok, key, context} <- unify(source_key, target_key, stack, context) do
|
||||
case unify(source_value, target_value, stack, context) do
|
||||
{:ok, value, context} ->
|
||||
{:ok, [{:optional, key, value}], context}
|
||||
|
||||
{:error, _reason} ->
|
||||
source_map = {:map, [{:optional, source_key, source_value}]}
|
||||
target_map = {:map, [{:optional, target_key, target_value}]}
|
||||
error(:unable_unify, {source_map, target_map, stack}, context)
|
||||
end
|
||||
else
|
||||
_ -> nil
|
||||
end
|
||||
end) || {:ok, [], context}
|
||||
end)
|
||||
end
|
||||
|
||||
defp split_pairs(pairs) do
|
||||
{required, optional} =
|
||||
Enum.split_with(pairs, fn {kind, _key, _value} -> kind == :required end)
|
||||
|
||||
required = Enum.map(required, fn {_kind, key, value} -> {key, value} end)
|
||||
optional = Enum.map(optional, fn {_kind, key, value} -> {key, value} end)
|
||||
{required, optional}
|
||||
end
|
||||
|
||||
defp error(type, reason, context), do: {:error, {type, reason, context}}
|
||||
|
||||
@doc """
|
||||
Push expression to stack.
|
||||
|
||||
The expression stack is used to give the context where a type variable
|
||||
was refined when show a type conflict error.
|
||||
"""
|
||||
def push_expr_stack(expr, stack) do
|
||||
%{stack | last_expr: expr}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Gets a variable.
|
||||
"""
|
||||
def get_var!(var, context) do
|
||||
Map.fetch!(context.vars, var_name(var))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Adds a variable to the typing context and returns its type variable.
|
||||
If the variable has already been added, return the existing type variable.
|
||||
"""
|
||||
def new_var(var, context) do
|
||||
var_name = var_name(var)
|
||||
|
||||
case context.vars do
|
||||
%{^var_name => type} ->
|
||||
{type, context}
|
||||
|
||||
%{} ->
|
||||
type = {:var, context.counter}
|
||||
vars = Map.put(context.vars, var_name, type)
|
||||
types_to_vars = Map.put(context.types_to_vars, context.counter, var)
|
||||
types = Map.put(context.types, context.counter, :unbound)
|
||||
traces = Map.put(context.traces, context.counter, [])
|
||||
|
||||
context = %{
|
||||
context
|
||||
| vars: vars,
|
||||
types_to_vars: types_to_vars,
|
||||
types: types,
|
||||
traces: traces,
|
||||
counter: context.counter + 1
|
||||
}
|
||||
|
||||
{type, context}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Adds an internal variable to the typing context and returns its type variable.
|
||||
An internal variable is used to help unify complex expressions,
|
||||
it does not belong to a specific AST expression.
|
||||
"""
|
||||
def add_var(context) do
|
||||
type = {:var, context.counter}
|
||||
types = Map.put(context.types, context.counter, :unbound)
|
||||
traces = Map.put(context.traces, context.counter, [])
|
||||
|
||||
context = %{
|
||||
context
|
||||
| types: types,
|
||||
traces: traces,
|
||||
counter: context.counter + 1
|
||||
}
|
||||
|
||||
{type, context}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Resolves a variable raising if it is unbound.
|
||||
"""
|
||||
def resolve_var({:var, var}, context) do
|
||||
case context.types do
|
||||
%{^var => :unbound} -> raise "cannot resolve unbound var"
|
||||
%{^var => type} -> resolve_var(type, context)
|
||||
end
|
||||
end
|
||||
|
||||
def resolve_var(other, _context), do: other
|
||||
|
||||
# Check unify stack to see if variable was already expanded
|
||||
defp variable_expanded?(var, stack, context) do
|
||||
Enum.any?(stack.unify_stack, &variable_same?(var, &1, context))
|
||||
end
|
||||
|
||||
defp variable_same?(left, right, context) do
|
||||
case context.types do
|
||||
%{^left => {:var, new_left}} ->
|
||||
variable_same?(new_left, right, context)
|
||||
|
||||
%{^right => {:var, new_right}} ->
|
||||
variable_same?(left, new_right, context)
|
||||
|
||||
%{} ->
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
defp push_unify_stack(var, stack) do
|
||||
%{stack | unify_stack: [var | stack.unify_stack]}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Set the type for a variable and add trace.
|
||||
"""
|
||||
def refine_var(var, type, stack, context) do
|
||||
types = Map.put(context.types, var, type)
|
||||
context = %{context | types: types}
|
||||
trace_var(var, type, stack, context)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Remove type variable and all its traces.
|
||||
"""
|
||||
def remove_var(var, context) do
|
||||
types = Map.delete(context.types, var)
|
||||
traces = Map.delete(context.traces, var)
|
||||
%{context | types: types, traces: traces}
|
||||
end
|
||||
|
||||
defp trace_var(var, type, %{trace: true, last_expr: last_expr} = _stack, context) do
|
||||
line = get_meta(last_expr)[:line]
|
||||
trace = {type, last_expr, {context.file, line}}
|
||||
traces = Map.update!(context.traces, var, &[trace | &1])
|
||||
%{context | traces: traces}
|
||||
end
|
||||
|
||||
defp trace_var(_var, _type, %{trace: false} = _stack, context) do
|
||||
context
|
||||
end
|
||||
|
||||
# Check if a variable is recursive and incompatible with itself
|
||||
# Bad: `{var} = var`
|
||||
# Good: `x = y; y = z; z = x`
|
||||
defp recursive_type?({:var, var} = parent, parents, context) do
|
||||
case context.types do
|
||||
%{^var => :unbound} ->
|
||||
false
|
||||
|
||||
%{^var => type} ->
|
||||
if type in parents do
|
||||
not Enum.all?(parents, &match?({:var, _}, &1))
|
||||
else
|
||||
recursive_type?(type, [parent | parents], context)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp recursive_type?({:list, type} = parent, parents, context) do
|
||||
recursive_type?(type, [parent | parents], context)
|
||||
end
|
||||
|
||||
defp recursive_type?({:union, types} = parent, parents, context) do
|
||||
Enum.any?(types, &recursive_type?(&1, [parent | parents], context))
|
||||
end
|
||||
|
||||
defp recursive_type?({:tuple, _, types} = parent, parents, context) do
|
||||
Enum.any?(types, &recursive_type?(&1, [parent | parents], context))
|
||||
end
|
||||
|
||||
defp recursive_type?({:map, pairs} = parent, parents, context) do
|
||||
Enum.any?(pairs, fn {_kind, key, value} ->
|
||||
recursive_type?(key, [parent | parents], context) or
|
||||
recursive_type?(value, [parent | parents], context)
|
||||
end)
|
||||
end
|
||||
|
||||
defp recursive_type?(_other, _parents, _context) do
|
||||
false
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if the type has a type var.
|
||||
"""
|
||||
def has_unbound_var?({:var, var}, context) do
|
||||
case context.types do
|
||||
%{^var => :unbound} -> true
|
||||
%{^var => type} -> has_unbound_var?(type, context)
|
||||
end
|
||||
end
|
||||
|
||||
def has_unbound_var?({:tuple, _, args}, context),
|
||||
do: Enum.any?(args, &has_unbound_var?(&1, context))
|
||||
|
||||
def has_unbound_var?({:union, args}, context),
|
||||
do: Enum.any?(args, &has_unbound_var?(&1, context))
|
||||
|
||||
def has_unbound_var?({:list, arg}, context),
|
||||
do: has_unbound_var?(arg, context)
|
||||
|
||||
def has_unbound_var?({:map, pairs}, context) do
|
||||
Enum.any?(pairs, fn {_, key, value} ->
|
||||
has_unbound_var?(key, context) or has_unbound_var?(value, context)
|
||||
end)
|
||||
end
|
||||
|
||||
def has_unbound_var?(_type, _context), do: false
|
||||
|
||||
@doc """
|
||||
Returns true if it is a singleton type.
|
||||
|
||||
Only atoms are singleton types. Unbound vars are not
|
||||
considered singleton types.
|
||||
"""
|
||||
def singleton?({:var, var}, context) do
|
||||
case context.types do
|
||||
%{^var => :unbound} -> false
|
||||
%{^var => type} -> singleton?(type, context)
|
||||
end
|
||||
end
|
||||
|
||||
def singleton?({:atom, _}, _context), do: true
|
||||
def singleton?(_type, _context), do: false
|
||||
|
||||
@doc """
|
||||
Checks if the first argument is a subtype of the second argument.
|
||||
|
||||
This function assumes that:
|
||||
|
||||
* unbound variables are not subtype of anything
|
||||
|
||||
* dynamic is not considered a subtype of all other types but the top type.
|
||||
This allows this function can be used for ordering, in other cases, you
|
||||
may need to check for both sides
|
||||
|
||||
"""
|
||||
def subtype?(type, type, _context), do: true
|
||||
|
||||
def subtype?({:var, var}, other, context) do
|
||||
case context.types do
|
||||
%{^var => :unbound} -> false
|
||||
%{^var => type} -> subtype?(type, other, context)
|
||||
end
|
||||
end
|
||||
|
||||
def subtype?(other, {:var, var}, context) do
|
||||
case context.types do
|
||||
%{^var => :unbound} -> false
|
||||
%{^var => type} -> subtype?(other, type, context)
|
||||
end
|
||||
end
|
||||
|
||||
def subtype?(_, :dynamic, _context), do: true
|
||||
def subtype?({:atom, atom}, :atom, _context) when is_atom(atom), do: true
|
||||
|
||||
# Composite
|
||||
|
||||
def subtype?({:tuple, _, _}, :tuple, _context), do: true
|
||||
|
||||
def subtype?({:tuple, n, left_types}, {:tuple, n, right_types}, context) do
|
||||
left_types
|
||||
|> Enum.zip(right_types)
|
||||
|> Enum.any?(fn {left, right} -> subtype?(left, right, context) end)
|
||||
end
|
||||
|
||||
def subtype?({:map, left_pairs}, {:map, right_pairs}, context) do
|
||||
Enum.all?(left_pairs, fn
|
||||
{:required, left_key, left_value} ->
|
||||
Enum.any?(right_pairs, fn {_, right_key, right_value} ->
|
||||
subtype?(left_key, right_key, context) and subtype?(left_value, right_value, context)
|
||||
end)
|
||||
|
||||
{:optional, _, _} ->
|
||||
true
|
||||
end)
|
||||
end
|
||||
|
||||
def subtype?({:list, left}, {:list, right}, context) do
|
||||
subtype?(left, right, context)
|
||||
end
|
||||
|
||||
def subtype?({:union, left_types}, {:union, _} = right_union, context) do
|
||||
Enum.all?(left_types, &subtype?(&1, right_union, context))
|
||||
end
|
||||
|
||||
def subtype?(left, {:union, right_types}, context) do
|
||||
Enum.any?(right_types, &subtype?(left, &1, context))
|
||||
end
|
||||
|
||||
def subtype?({:union, left_types}, right, context) do
|
||||
Enum.all?(left_types, &subtype?(&1, right, context))
|
||||
end
|
||||
|
||||
def subtype?(_left, _right, _context), do: false
|
||||
|
||||
@doc """
|
||||
Returns a "simplified" union using `subtype?/3` to remove redundant types.
|
||||
|
||||
Due to limitations in `subtype?/3` some overlapping types may still be
|
||||
included. For example unions with overlapping non-concrete types such as
|
||||
`{boolean()} | {atom()}` will not be merged or types with variables that
|
||||
are distinct but equivalent such as `a | b when a ~ b`.
|
||||
"""
|
||||
def to_union([type], _context), do: type
|
||||
|
||||
def to_union(types, context) when types != [] do
|
||||
flat_types = flatten_union(types)
|
||||
|
||||
case unique_super_types(flat_types, context) do
|
||||
[type] -> type
|
||||
types -> {:union, types}
|
||||
end
|
||||
end
|
||||
|
||||
defp flatten_union(types) do
|
||||
Enum.flat_map(types, fn
|
||||
{:union, types} -> flatten_union(types)
|
||||
type -> [type]
|
||||
end)
|
||||
end
|
||||
|
||||
# Filter subtypes
|
||||
#
|
||||
# `boolean() | atom()` => `atom()`
|
||||
# `:foo | atom()` => `atom()`
|
||||
#
|
||||
# Does not merge `true | false` => `boolean()`
|
||||
defp unique_super_types([type | types], context) do
|
||||
types = Enum.reject(types, &subtype?(&1, type, context))
|
||||
|
||||
if Enum.any?(types, &subtype?(type, &1, context)) do
|
||||
unique_super_types(types, context)
|
||||
else
|
||||
[type | unique_super_types(types, context)]
|
||||
end
|
||||
end
|
||||
|
||||
defp unique_super_types([], _context) do
|
||||
[]
|
||||
end
|
||||
|
||||
@doc """
|
||||
Formats types.
|
||||
|
||||
The second argument says when complex types such as maps and
|
||||
structs should be simplified and not shown.
|
||||
"""
|
||||
def format_type({:map, pairs}, true) do
|
||||
case List.keyfind(pairs, {:atom, :__struct__}, 1) do
|
||||
{:required, {:atom, :__struct__}, {:atom, struct}} ->
|
||||
"%#{inspect(struct)}{}"
|
||||
|
||||
_ ->
|
||||
"map()"
|
||||
end
|
||||
end
|
||||
|
||||
def format_type({:union, types}, simplify?) do
|
||||
"#{Enum.map_join(types, " | ", &format_type(&1, simplify?))}"
|
||||
end
|
||||
|
||||
def format_type({:tuple, _, types}, simplify?) do
|
||||
"{#{Enum.map_join(types, ", ", &format_type(&1, simplify?))}}"
|
||||
end
|
||||
|
||||
def format_type({:list, type}, simplify?) do
|
||||
"[#{format_type(type, simplify?)}]"
|
||||
end
|
||||
|
||||
def format_type({:map, pairs}, false) do
|
||||
case List.keytake(pairs, {:atom, :__struct__}, 1) do
|
||||
{{:required, {:atom, :__struct__}, {:atom, struct}}, pairs} ->
|
||||
"%#{inspect(struct)}{#{format_map_pairs(pairs)}}"
|
||||
|
||||
_ ->
|
||||
"%{#{format_map_pairs(pairs)}}"
|
||||
end
|
||||
end
|
||||
|
||||
def format_type({:atom, literal}, _simplify?) do
|
||||
inspect(literal)
|
||||
end
|
||||
|
||||
def format_type({:var, index}, _simplify?) do
|
||||
"var#{index}"
|
||||
end
|
||||
|
||||
def format_type(atom, _simplify?) when is_atom(atom) do
|
||||
"#{atom}()"
|
||||
end
|
||||
|
||||
defp format_map_pairs(pairs) do
|
||||
{atoms, others} = Enum.split_with(pairs, &match?({:required, {:atom, _}, _}, &1))
|
||||
{required, optional} = Enum.split_with(others, &match?({:required, _, _}, &1))
|
||||
|
||||
Enum.map_join(atoms ++ required ++ optional, ", ", fn
|
||||
{:required, {:atom, atom}, right} ->
|
||||
"#{atom}: #{format_type(right, false)}"
|
||||
|
||||
{:required, left, right} ->
|
||||
"#{format_type(left, false)} => #{format_type(right, false)}"
|
||||
|
||||
{:optional, left, right} ->
|
||||
"optional(#{format_type(left, false)}) => #{format_type(right, false)}"
|
||||
end)
|
||||
end
|
||||
end
|
||||
@@ -106,7 +106,7 @@ defmodule Node do
|
||||
|
||||
For more information, see `:erlang.monitor_node/2`.
|
||||
|
||||
For monitoring status changes of all nodes, see `:net_kernel.monitor_nodes/3`.
|
||||
For monitoring status changes of all nodes, see `:net_kernel.monitor_nodes/2`.
|
||||
"""
|
||||
@spec monitor(t, boolean) :: true
|
||||
def monitor(node, flag) do
|
||||
@@ -119,7 +119,7 @@ defmodule Node do
|
||||
|
||||
For more information, see `:erlang.monitor_node/3`.
|
||||
|
||||
For monitoring status changes of all nodes, see `:net_kernel.monitor_nodes/3`.
|
||||
For monitoring status changes of all nodes, see `:net_kernel.monitor_nodes/2`.
|
||||
"""
|
||||
@spec monitor(t, boolean, [:allow_passive_connect]) :: true
|
||||
def monitor(node, flag, options) do
|
||||
@@ -219,7 +219,7 @@ defmodule Node do
|
||||
|
||||
If `node` does not exist, a useless PID is returned.
|
||||
|
||||
For the list of available options, see `:erlang.spawn/5`.
|
||||
For the list of available options, see `:erlang.spawn/4`.
|
||||
|
||||
Inlined by the compiler.
|
||||
"""
|
||||
|
||||
@@ -152,8 +152,8 @@ defmodule Path do
|
||||
|
||||
## Examples
|
||||
|
||||
Path.expand("/foo/bar/../bar")
|
||||
#=> "/foo/bar"
|
||||
Path.expand("/foo/bar/../baz")
|
||||
#=> "/foo/baz"
|
||||
|
||||
"""
|
||||
@spec expand(t) :: binary
|
||||
@@ -565,9 +565,6 @@ defmodule Path do
|
||||
"""
|
||||
@spec split(t) :: [binary]
|
||||
|
||||
# Work around a bug in Erlang on Unix-like operating systems
|
||||
def split(""), do: []
|
||||
|
||||
def split(path) do
|
||||
:filename.split(IO.chardata_to_string(path))
|
||||
end
|
||||
|
||||
@@ -29,7 +29,7 @@ defmodule Port do
|
||||
|
||||
After sending those two messages, we invoked the IEx helper `flush()`,
|
||||
which printed all messages received from the port, in this case we got
|
||||
"hello" and "world" back. Notice the messages are in binary because we
|
||||
"hello" and "world" back. Note that the messages are in binary because we
|
||||
passed the `:binary` option when opening the port in `Port.open/2`. Without
|
||||
such option, it would have yielded a list of bytes.
|
||||
|
||||
@@ -132,9 +132,9 @@ defmodule Port do
|
||||
While we encourage graceful termination by detecting if stdin/stdout has been
|
||||
closed, we do not always have control over how third-party software terminates.
|
||||
In those cases, you can wrap the application in a script that checks for stdin.
|
||||
Here is such script in `sh`:
|
||||
Here is such script that has been verified to work on bash shells:
|
||||
|
||||
#!/bin/sh
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Start the program in the background
|
||||
exec "$@" &
|
||||
@@ -180,7 +180,7 @@ defmodule Port do
|
||||
@type name ::
|
||||
{:spawn, charlist | binary}
|
||||
| {:spawn_driver, charlist | binary}
|
||||
| {:spawn_executable, charlist | atom}
|
||||
| {:spawn_executable, :file.name_all()}
|
||||
| {:fd, non_neg_integer, non_neg_integer}
|
||||
|
||||
@doc """
|
||||
|
||||
@@ -705,7 +705,8 @@ defmodule Process do
|
||||
"""
|
||||
@spec flag(:error_handler, module) :: module
|
||||
@spec flag(:max_heap_size, heap_size) :: heap_size
|
||||
@spec flag(:message_queue_data, :erlang.message_queue_data()) :: :erlang.message_queue_data()
|
||||
# :off_heap | :on_heap twice because :erlang.message_queue_data() is not exported
|
||||
@spec flag(:message_queue_data, :off_heap | :on_heap) :: :off_heap | :on_heap
|
||||
@spec flag(:min_bin_vheap_size, non_neg_integer) :: non_neg_integer
|
||||
@spec flag(:min_heap_size, non_neg_integer) :: non_neg_integer
|
||||
@spec flag(:priority, priority_level) :: priority_level
|
||||
|
||||
@@ -43,10 +43,14 @@ defmodule Protocol do
|
||||
def size(tuple), do: tuple_size(tuple)
|
||||
end
|
||||
|
||||
Notice we didn't implement it for lists as we don't have the
|
||||
Note that we didn't implement it for lists as we don't have the
|
||||
`size` information on lists, rather its value needs to be
|
||||
computed with `length`.
|
||||
|
||||
The data structure you are implementing the protocol for
|
||||
must be the first argument to all functions defined in the
|
||||
protocol.
|
||||
|
||||
It is possible to implement protocols for all Elixir types:
|
||||
|
||||
* Structs (see below)
|
||||
|
||||
+31
-26
@@ -401,6 +401,7 @@ defmodule Record do
|
||||
# Using {} here is safe, since it's not valid AST
|
||||
default = if Macro.Env.in_match?(caller), do: {:_, [], nil}, else: {}
|
||||
{default, keyword} = Keyword.pop(keyword, :_, default)
|
||||
{keyword, exprs} = hoist_expressions(keyword, caller)
|
||||
|
||||
{elements, remaining} =
|
||||
Enum.map_reduce(fields, keyword, fn {key, field_default}, remaining ->
|
||||
@@ -413,6 +414,7 @@ defmodule Record do
|
||||
case remaining do
|
||||
[] ->
|
||||
quote(do: {unquote(tag), unquote_splicing(elements)})
|
||||
|> maybe_prepend_reversed_exprs(exprs)
|
||||
|
||||
[{key, _} | _] ->
|
||||
raise ArgumentError, "record #{inspect(tag)} does not have the key: #{inspect(key)}"
|
||||
@@ -425,27 +427,45 @@ defmodule Record do
|
||||
raise ArgumentError, "cannot invoke update style macro inside match"
|
||||
end
|
||||
|
||||
{keyword, exprs} = hoist_expressions(keyword, caller)
|
||||
|
||||
if Keyword.has_key?(keyword, :_) do
|
||||
message = "updating a record with a default (:_) is equivalent to creating a new record"
|
||||
IO.warn(message, Macro.Env.stacktrace(caller))
|
||||
create(tag, fields, keyword, caller)
|
||||
else
|
||||
case build_updates(keyword, fields, [], [], []) do
|
||||
{updates, [], []} ->
|
||||
build_update(updates, var)
|
||||
|
||||
{updates, vars, exprs} ->
|
||||
quote do
|
||||
{unquote_splicing(:lists.reverse(vars))} = {unquote_splicing(:lists.reverse(exprs))}
|
||||
unquote(build_update(updates, var))
|
||||
updates =
|
||||
Enum.map(keyword, fn {key, value} ->
|
||||
if index = find_index(fields, key, 2) do
|
||||
{index, value}
|
||||
else
|
||||
raise ArgumentError, "record #{inspect(tag)} does not have the key: #{inspect(key)}"
|
||||
end
|
||||
end)
|
||||
|
||||
{:error, key} ->
|
||||
raise ArgumentError, "record #{inspect(tag)} does not have the key: #{inspect(key)}"
|
||||
end
|
||||
build_update(updates, var) |> maybe_prepend_reversed_exprs(exprs)
|
||||
end
|
||||
end
|
||||
|
||||
defp hoist_expressions(keyword, %{context: nil}) do
|
||||
Enum.map_reduce(keyword, [], fn {key, expr}, acc ->
|
||||
if simple_argument?(expr) do
|
||||
{{key, expr}, acc}
|
||||
else
|
||||
var = Macro.var(key, __MODULE__)
|
||||
{{key, var}, [{:=, [], [var, expr]} | acc]}
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp hoist_expressions(keyword, _), do: {keyword, []}
|
||||
|
||||
defp maybe_prepend_reversed_exprs(expr, []),
|
||||
do: expr
|
||||
|
||||
defp maybe_prepend_reversed_exprs(expr, exprs),
|
||||
do: {:__block__, [], :lists.reverse([expr | exprs])}
|
||||
|
||||
defp build_update(updates, initial) do
|
||||
updates
|
||||
|> Enum.sort(fn {left, _}, {right, _} -> right <= left end)
|
||||
@@ -454,21 +474,6 @@ defmodule Record do
|
||||
end)
|
||||
end
|
||||
|
||||
defp build_updates([{key, value} | rest], fields, updates, vars, exprs) do
|
||||
if index = find_index(fields, key, 2) do
|
||||
if simple_argument?(value) do
|
||||
build_updates(rest, fields, [{index, value} | updates], vars, exprs)
|
||||
else
|
||||
var = Macro.var(key, __MODULE__)
|
||||
build_updates(rest, fields, [{index, var} | updates], [var | vars], [value | exprs])
|
||||
end
|
||||
else
|
||||
{:error, key}
|
||||
end
|
||||
end
|
||||
|
||||
defp build_updates([], _fields, updates, vars, exprs), do: {updates, vars, exprs}
|
||||
|
||||
defp simple_argument?({name, _, ctx}) when is_atom(name) and is_atom(ctx), do: true
|
||||
defp simple_argument?(other), do: Macro.quoted_literal?(other)
|
||||
|
||||
|
||||
+18
-20
@@ -16,13 +16,14 @@ defmodule Regex do
|
||||
~r/foo/iu
|
||||
|
||||
Regular expressions created via sigils are pre-compiled and stored
|
||||
in the `.beam` file. Notice this may be a problem if you are precompiling
|
||||
in the `.beam` file. Note that this may be a problem if you are precompiling
|
||||
Elixir, see the "Precompilation" section for more information.
|
||||
|
||||
A Regex is represented internally as the `Regex` struct. Therefore,
|
||||
`%Regex{}` can be used whenever there is a need to match on them.
|
||||
Keep in mind it is not guaranteed two regular expressions from the
|
||||
same source are equal, for example:
|
||||
Keep in mind that all of the structs fields are private. There is
|
||||
also not guarantee two regular expressions from the same source are
|
||||
equal, for example:
|
||||
|
||||
~r/(?<foo>.)(?<bar>.)/ == ~r/(?<foo>.)(?<bar>.)/
|
||||
|
||||
@@ -37,9 +38,10 @@ defmodule Regex do
|
||||
|
||||
The modifiers available when creating a Regex are:
|
||||
|
||||
* `unicode` (u) - enables Unicode specific patterns like `\p` and change
|
||||
modifiers like `\w`, `\W`, `\s` and friends to also match on Unicode.
|
||||
It expects valid Unicode strings to be given on match
|
||||
* `unicode` (u) - enables Unicode specific patterns like `\p` and causes
|
||||
character classes like `\w`, `\W`, `\s`, etc. to also match on Unicode
|
||||
(see examples below in "Character classes"). It expects valid Unicode
|
||||
strings to be given on match
|
||||
|
||||
* `caseless` (i) - adds case insensitivity
|
||||
|
||||
@@ -125,6 +127,10 @@ defmodule Regex do
|
||||
false
|
||||
iex> String.match?("josé", ~r/^[[:lower:]]+$/u)
|
||||
true
|
||||
iex> Regex.replace(~r/\s/, "Unicode\u00A0spaces", "-")
|
||||
"Unicode spaces"
|
||||
iex> Regex.replace(~r/\s/u, "Unicode\u00A0spaces", "-")
|
||||
"Unicode-spaces"
|
||||
|
||||
## Precompilation
|
||||
|
||||
@@ -218,7 +224,7 @@ defmodule Regex do
|
||||
and recompiles the regex in case of version mismatch.
|
||||
"""
|
||||
@doc since: "1.4.0"
|
||||
@spec recompile(t) :: t
|
||||
@spec recompile(t) :: {:ok, t} | {:error, any}
|
||||
def recompile(%Regex{} = regex) do
|
||||
version = version()
|
||||
|
||||
@@ -273,17 +279,9 @@ defmodule Regex do
|
||||
@doc """
|
||||
Returns `true` if the given `term` is a regex.
|
||||
Otherwise returns `false`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Regex.regex?(~r/foo/)
|
||||
true
|
||||
|
||||
iex> Regex.regex?(0)
|
||||
false
|
||||
|
||||
"""
|
||||
@spec regex?(any) :: boolean
|
||||
# TODO: deprecate permanently on Elixir v1.15
|
||||
@doc deprecated: "Use Kernel.is_struct/2 or pattern match on %Regex{} instead"
|
||||
def regex?(term)
|
||||
def regex?(%Regex{}), do: true
|
||||
def regex?(_), do: false
|
||||
@@ -759,12 +757,12 @@ defmodule Regex do
|
||||
end
|
||||
end
|
||||
|
||||
defp get_index(_string, {pos, _len}) when pos < 0 do
|
||||
defp get_index(_string, {pos, _length}) when pos < 0 do
|
||||
""
|
||||
end
|
||||
|
||||
defp get_index(string, {pos, len}) do
|
||||
<<_::size(pos)-binary, res::size(len)-binary, _::binary>> = string
|
||||
defp get_index(string, {pos, length}) do
|
||||
<<_::size(pos)-binary, res::size(length)-binary, _::binary>> = string
|
||||
res
|
||||
end
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ defmodule Registry do
|
||||
A local, decentralized and scalable key-value process storage.
|
||||
|
||||
It allows developers to lookup one or more processes with a given key.
|
||||
If the registry has `:unique` keys, a key points to 0 or 1 processes.
|
||||
If the registry has `:unique` keys, a key points to 0 or 1 process.
|
||||
If the registry allows `:duplicate` keys, a single key may point to any
|
||||
number of processes. In both cases, different keys could identify the
|
||||
same process.
|
||||
@@ -204,7 +204,7 @@ defmodule Registry do
|
||||
@type guards :: [guard]
|
||||
|
||||
@typedoc "A pattern used to representing the output format part of a match spec"
|
||||
@type body :: [atom | tuple]
|
||||
@type body :: [term]
|
||||
|
||||
@typedoc "A full match spec used when selecting objects in the registry"
|
||||
@type spec :: [{match_pattern, guards, body}]
|
||||
@@ -261,9 +261,8 @@ defmodule Registry do
|
||||
end
|
||||
|
||||
@doc false
|
||||
def unregister_name({registry, key}) do
|
||||
unregister(registry, key)
|
||||
end
|
||||
def unregister_name({registry, key}), do: unregister(registry, key)
|
||||
def unregister_name({registry, key, _value}), do: unregister(registry, key)
|
||||
|
||||
## Registry API
|
||||
|
||||
@@ -784,7 +783,8 @@ defmodule Registry do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Unregister entries for a given key matching a pattern.
|
||||
Unregisters entries for keys matching a pattern associated to the current
|
||||
process in `registry`.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -922,7 +922,7 @@ defmodule Registry do
|
||||
key_ets = key_ets || key_ets!(registry, key_partition)
|
||||
{pid_server, pid_ets} = pid_ets || pid_ets!(registry, pid_partition)
|
||||
|
||||
# Notice we write first to the pid_ets table because it will
|
||||
# Note that we write first to the pid_ets table because it will
|
||||
# always be able to do the cleanup. If we register first to the
|
||||
# key one and the process crashes, the key will stay there forever.
|
||||
Process.link(pid_server)
|
||||
@@ -958,7 +958,7 @@ defmodule Registry do
|
||||
if :ets.insert_new(key_ets, entry) do
|
||||
:ok
|
||||
else
|
||||
# Notice we have to call register_key recursively
|
||||
# Note that we have to call register_key recursively
|
||||
# because we are always at odds of a race condition.
|
||||
case :ets.lookup(key_ets, key) do
|
||||
[{^key, {pid, _}} = current] ->
|
||||
@@ -1033,6 +1033,34 @@ defmodule Registry do
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes registry metadata for the given `key` in `registry`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Registry.start_link(keys: :unique, name: Registry.DeleteMetaTest)
|
||||
iex> Registry.put_meta(Registry.DeleteMetaTest, :custom_key, "custom_value")
|
||||
:ok
|
||||
iex> Registry.meta(Registry.DeleteMetaTest, :custom_key)
|
||||
{:ok, "custom_value"}
|
||||
iex> Registry.delete_meta(Registry.DeleteMetaTest, :custom_key)
|
||||
:ok
|
||||
iex> Registry.meta(Registry.DeleteMetaTest, :custom_key)
|
||||
:error
|
||||
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
@spec delete_meta(registry, meta_key) :: :ok
|
||||
def delete_meta(registry, key) when is_atom(registry) and (is_atom(key) or is_tuple(key)) do
|
||||
try do
|
||||
:ets.delete(registry, key)
|
||||
:ok
|
||||
catch
|
||||
:error, :badarg ->
|
||||
raise ArgumentError, "unknown registry: #{inspect(registry)}"
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the number of registered keys in a registry.
|
||||
It runs in constant time.
|
||||
|
||||
@@ -21,7 +21,7 @@ defmodule Stream do
|
||||
iex> Enum.map(stream, &(&1 + 1))
|
||||
[3, 5, 7]
|
||||
|
||||
Notice we started with a range and then we created a stream that is
|
||||
Note that we started with a range and then we created a stream that is
|
||||
meant to multiply each element in the range by 2. At this point, no
|
||||
computation was done. Only when `Enum.map/2` is called we actually
|
||||
enumerate over each element in the range, multiplying it by 2 and adding 1.
|
||||
@@ -46,7 +46,7 @@ defmodule Stream do
|
||||
6
|
||||
#=> [2, 4, 6]
|
||||
|
||||
Notice that we first printed each element in the list, then multiplied each
|
||||
Note that we first printed each element in the list, then multiplied each
|
||||
element by 2 and finally printed each new value. In this example, the list
|
||||
was enumerated three times. Let's see an example with streams:
|
||||
|
||||
@@ -68,7 +68,7 @@ defmodule Stream do
|
||||
its double. In this example, the list was enumerated just once!
|
||||
|
||||
That's what we meant when we said earlier that streams are composable,
|
||||
lazy enumerables. Notice we could call `Stream.map/2` multiple times,
|
||||
lazy enumerables. Note that we could call `Stream.map/2` multiple times,
|
||||
effectively composing the streams and keeping them lazy. The computations
|
||||
are only performed when you call a function from the `Enum` module.
|
||||
|
||||
@@ -107,6 +107,7 @@ defmodule Stream do
|
||||
@type index :: non_neg_integer
|
||||
|
||||
@type default :: any
|
||||
@type timer :: non_neg_integer | :infinity
|
||||
|
||||
# Require Stream.Reducers and its callbacks
|
||||
require Stream.Reducers, as: R
|
||||
@@ -505,8 +506,10 @@ defmodule Stream do
|
||||
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
|
||||
|
||||
"""
|
||||
@spec interval(non_neg_integer) :: Enumerable.t()
|
||||
def interval(n) when is_integer(n) and n >= 0 do
|
||||
@spec interval(timer()) :: Enumerable.t()
|
||||
def interval(n)
|
||||
when is_integer(n) and n >= 0
|
||||
when n == :infinity do
|
||||
unfold(0, fn count ->
|
||||
Process.sleep(n)
|
||||
{count, count + 1}
|
||||
@@ -787,8 +790,10 @@ defmodule Stream do
|
||||
[0]
|
||||
|
||||
"""
|
||||
@spec timer(non_neg_integer) :: Enumerable.t()
|
||||
def timer(n) when is_integer(n) and n >= 0 do
|
||||
@spec timer(timer()) :: Enumerable.t()
|
||||
def timer(n)
|
||||
when is_integer(n) and n >= 0
|
||||
when n == :infinity do
|
||||
take(interval(n), 1)
|
||||
end
|
||||
|
||||
|
||||
+125
-48
@@ -60,7 +60,7 @@ defmodule String do
|
||||
Note it is generally not advised to use `\xNN` in Elixir
|
||||
strings, as introducing an invalid byte sequence would
|
||||
make the string invalid. If you have to introduce a
|
||||
character by its hexdecimal representation, it is best
|
||||
character by its hexadecimal representation, it is best
|
||||
to work with Unicode code points, such as `\uNNNN`. In fact,
|
||||
understanding Unicode code points can be essential when doing
|
||||
low-level manipulations of string, so let's explore them in
|
||||
@@ -193,14 +193,15 @@ defmodule String do
|
||||
|
||||
## Integer code points
|
||||
|
||||
Although code points could be represented as integers, this
|
||||
module represents all code points as strings. For example:
|
||||
Although code points are represented as integers, this module
|
||||
represents code points in their encoded format as strings.
|
||||
For example:
|
||||
|
||||
iex> String.codepoints("olá")
|
||||
["o", "l", "á"]
|
||||
|
||||
There are a couple of ways to retrieve a character integer
|
||||
code point. One may use the `?` construct:
|
||||
There are a couple of ways to retrieve the character code point.
|
||||
One may use the `?` construct:
|
||||
|
||||
iex> ?o
|
||||
111
|
||||
@@ -220,9 +221,9 @@ defmodule String do
|
||||
iex> "ol\u00E1"
|
||||
"olá"
|
||||
|
||||
Finally, to convert a String into a list of integers
|
||||
code points, usually known as "char lists", you can call
|
||||
`Strig.to_charlist`:
|
||||
Finally, to convert a String into a list of integer
|
||||
code points, known as "charlists" in Elixir, you can call
|
||||
`String.to_charlist`:
|
||||
|
||||
iex> String.to_charlist("olá")
|
||||
[111, 108, 225]
|
||||
@@ -247,7 +248,7 @@ defmodule String do
|
||||
## Compile binary patterns
|
||||
|
||||
Many functions in this module work with patterns. For example,
|
||||
`String.split/2` can split a string into multiple strings given
|
||||
`String.split/3` can split a string into multiple strings given
|
||||
a pattern. This pattern can be a string, a list of strings or
|
||||
a compiled pattern:
|
||||
|
||||
@@ -282,7 +283,7 @@ defmodule String do
|
||||
@typedoc "Multiple code points that may be perceived as a single character by readers"
|
||||
@type grapheme :: t
|
||||
|
||||
@typedoc "Pattern used in functions like `replace/3` and `split/2`"
|
||||
@typedoc "Pattern used in functions like `replace/4` and `split/3`"
|
||||
@type pattern :: t | [t] | :binary.cp()
|
||||
|
||||
@conditional_mappings [:greek]
|
||||
@@ -373,9 +374,10 @@ defmodule String do
|
||||
@doc ~S"""
|
||||
Divides a string into parts based on a pattern.
|
||||
|
||||
Returns a list of these parts. The pattern can
|
||||
be a string, a list of strings, a regular expression,
|
||||
or a compiled pattern.
|
||||
Returns a list of these parts.
|
||||
|
||||
The `pattern` may be a string, a list of strings, a regular expression, or a
|
||||
compiled pattern.
|
||||
|
||||
The string is split into as many parts as possible by
|
||||
default, but can be controlled via the `:parts` option.
|
||||
@@ -455,13 +457,13 @@ defmodule String do
|
||||
For example, take the grapheme "é" which is made of the characters
|
||||
"e" and the acute accent. The following will split the string into two parts:
|
||||
|
||||
iex> String.split(:unicode.characters_to_nfd_binary("é"), "e")
|
||||
iex> String.split(String.normalize("é", :nfd), "e")
|
||||
["", "́"]
|
||||
|
||||
However, if "é" is represented by the single character "e with acute"
|
||||
accent, then it will split the string into just one part:
|
||||
|
||||
iex> String.split(:unicode.characters_to_nfc_binary("é"), "e")
|
||||
iex> String.split(String.normalize("é", :nfc), "e")
|
||||
["é"]
|
||||
|
||||
"""
|
||||
@@ -650,9 +652,9 @@ defmodule String do
|
||||
|
||||
String.normalize(string1, :nfd) == String.normalize(string2, :nfd)
|
||||
|
||||
Therefore, if you plan to compare multiple strings, multiple times
|
||||
in a row, you may normalize them upfront and compare them directly
|
||||
to avoid multiple normalization passes.
|
||||
If you plan to compare multiple strings, multiple times in a row, you
|
||||
may normalize them upfront and compare them directly to avoid multiple
|
||||
normalization passes.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -674,21 +676,83 @@ defmodule String do
|
||||
normalize(string1, :nfd) == normalize(string2, :nfd)
|
||||
end
|
||||
|
||||
@doc false
|
||||
@deprecated "Use :unicode.characters_to_nfc_binary/1 or :unicode.characters_to_nfd_binary/1 instead"
|
||||
@doc """
|
||||
Converts all characters in `string` to Unicode normalization
|
||||
form identified by `form`.
|
||||
|
||||
Invalid Unicode codepoints are skipped and the remaining of
|
||||
the string is converted. If you want the algorithm to stop
|
||||
and return on invalid codepoint, use `:unicode.characters_to_nfd_binary/1`,
|
||||
`:unicode.characters_to_nfc_binary/1`, `:unicode.characters_to_nfkd_binary/1`,
|
||||
and `:unicode.characters_to_nfkc_binary/1` instead.
|
||||
|
||||
Normalization forms `:nfkc` and `:nfkd` should not be blindly applied
|
||||
to arbitrary text. Because they erase many formatting distinctions,
|
||||
they will prevent round-trip conversion to and from many legacy
|
||||
character sets.
|
||||
|
||||
## Forms
|
||||
|
||||
The supported forms are:
|
||||
|
||||
* `:nfd` - Normalization Form Canonical Decomposition.
|
||||
Characters are decomposed by canonical equivalence, and
|
||||
multiple combining characters are arranged in a specific
|
||||
order.
|
||||
|
||||
* `:nfc` - Normalization Form Canonical Composition.
|
||||
Characters are decomposed and then recomposed by canonical equivalence.
|
||||
|
||||
* `:nfkd` - Normalization Form Compatibility Decomposition.
|
||||
Characters are decomposed by compatibility equivalence, and
|
||||
multiple combining characters are arranged in a specific
|
||||
order.
|
||||
|
||||
* `:nfkc` - Normalization Form Compatibility Composition.
|
||||
Characters are decomposed and then recomposed by compatibility equivalence.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> String.normalize("yêṩ", :nfd)
|
||||
"yêṩ"
|
||||
|
||||
iex> String.normalize("leña", :nfc)
|
||||
"leña"
|
||||
|
||||
iex> String.normalize("fi", :nfkd)
|
||||
"fi"
|
||||
|
||||
iex> String.normalize("fi", :nfkc)
|
||||
"fi"
|
||||
|
||||
"""
|
||||
def normalize(string, form)
|
||||
|
||||
def normalize(string, :nfd) do
|
||||
case :unicode.characters_to_nfd_binary(string) do
|
||||
string when is_binary(string) -> string
|
||||
{:error, bad, rest} -> bad <> normalize(rest, :nfd)
|
||||
{:error, good, <<head, rest::binary>>} -> good <> <<head>> <> normalize(rest, :nfd)
|
||||
end
|
||||
end
|
||||
|
||||
def normalize(string, :nfc) do
|
||||
case :unicode.characters_to_nfc_binary(string) do
|
||||
string when is_binary(string) -> string
|
||||
{:error, bad, rest} -> bad <> normalize(rest, :nfc)
|
||||
{:error, good, <<head, rest::binary>>} -> good <> <<head>> <> normalize(rest, :nfc)
|
||||
end
|
||||
end
|
||||
|
||||
def normalize(string, :nfkd) do
|
||||
case :unicode.characters_to_nfkd_binary(string) do
|
||||
string when is_binary(string) -> string
|
||||
{:error, good, <<head, rest::binary>>} -> good <> <<head>> <> normalize(rest, :nfkd)
|
||||
end
|
||||
end
|
||||
|
||||
def normalize(string, :nfkc) do
|
||||
case :unicode.characters_to_nfkc_binary(string) do
|
||||
string when is_binary(string) -> string
|
||||
{:error, good, <<head, rest::binary>>} -> good <> <<head>> <> normalize(rest, :nfkc)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1252,12 +1316,12 @@ defmodule String do
|
||||
end
|
||||
|
||||
defp pad(kind, string, count, padding) do
|
||||
string_len = length(string)
|
||||
string_length = length(string)
|
||||
|
||||
if string_len >= count do
|
||||
if string_length >= count do
|
||||
string
|
||||
else
|
||||
filler = build_filler(count - string_len, padding, padding, 0, [])
|
||||
filler = build_filler(count - string_length, padding, padding, 0, [])
|
||||
|
||||
case kind do
|
||||
:leading -> [filler | string]
|
||||
@@ -1293,26 +1357,26 @@ defmodule String do
|
||||
|
||||
@doc false
|
||||
@deprecated "Use String.pad_leading/2 instead"
|
||||
def rjust(subject, len) do
|
||||
rjust(subject, len, ?\s)
|
||||
def rjust(subject, length) do
|
||||
rjust(subject, length, ?\s)
|
||||
end
|
||||
|
||||
@doc false
|
||||
@deprecated "Use String.pad_leading/3 with a binary padding instead"
|
||||
def rjust(subject, len, pad) when is_integer(pad) and is_integer(len) and len >= 0 do
|
||||
pad(:leading, subject, len, [<<pad::utf8>>])
|
||||
def rjust(subject, length, pad) when is_integer(pad) and is_integer(length) and length >= 0 do
|
||||
pad(:leading, subject, length, [<<pad::utf8>>])
|
||||
end
|
||||
|
||||
@doc false
|
||||
@deprecated "Use String.pad_trailing/2 instead"
|
||||
def ljust(subject, len) do
|
||||
ljust(subject, len, ?\s)
|
||||
def ljust(subject, length) do
|
||||
ljust(subject, length, ?\s)
|
||||
end
|
||||
|
||||
@doc false
|
||||
@deprecated "Use String.pad_trailing/3 with a binary padding instead"
|
||||
def ljust(subject, len, pad) when is_integer(pad) and is_integer(len) and len >= 0 do
|
||||
pad(:trailing, subject, len, [<<pad::utf8>>])
|
||||
def ljust(subject, length, pad) when is_integer(pad) and is_integer(length) and length >= 0 do
|
||||
pad(:trailing, subject, length, [<<pad::utf8>>])
|
||||
end
|
||||
|
||||
@doc ~S"""
|
||||
@@ -1321,7 +1385,8 @@ defmodule String do
|
||||
|
||||
The `subject` is always a string.
|
||||
|
||||
The `pattern` may be a string, a regular expression, or a compiled pattern.
|
||||
The `pattern` may be a string, a list of strings, a regular expression, or a
|
||||
compiled pattern.
|
||||
|
||||
The `replacement` may be a string or a function that receives the matched
|
||||
pattern and must return the replacement as a string or iodata.
|
||||
@@ -1356,7 +1421,7 @@ defmodule String do
|
||||
iex> String.replace("a,b,c", ~r/,(.)/, ",\\1\\g{1}")
|
||||
"a,bb,cc"
|
||||
|
||||
Notice we had to escape the backslash escape character (i.e., we used `\\N`
|
||||
Note that we had to escape the backslash escape character (i.e., we used `\\N`
|
||||
instead of just `\N` to escape the backslash; same thing for `\\g{N}`). By
|
||||
giving `\0`, one can inject the whole match in the replacement string.
|
||||
|
||||
@@ -1531,10 +1596,13 @@ defmodule String do
|
||||
:binary.copy(subject, n)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns all code points in the string.
|
||||
@doc ~S"""
|
||||
Returns a list of code points encoded as strings.
|
||||
|
||||
For details about code points and graphemes, see the `String` module documentation.
|
||||
To retrieve code points in their natural integer
|
||||
representation, see `to_charlist/1`. For details about
|
||||
code points and graphemes, see the `String` module
|
||||
documentation.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -1736,6 +1804,9 @@ defmodule String do
|
||||
iex> String.next_grapheme("olá")
|
||||
{"o", "lá"}
|
||||
|
||||
iex> String.next_grapheme("")
|
||||
nil
|
||||
|
||||
"""
|
||||
@spec next_grapheme(t) :: {grapheme, t} | nil
|
||||
def next_grapheme(binary) do
|
||||
@@ -1746,9 +1817,9 @@ defmodule String do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the size of the next grapheme.
|
||||
Returns the size (in bytes) of the next grapheme.
|
||||
|
||||
The result is a tuple with the next grapheme size and
|
||||
The result is a tuple with the next grapheme size in bytes and
|
||||
the remainder of the string or `nil` in case the string
|
||||
reached its end.
|
||||
|
||||
@@ -1757,6 +1828,9 @@ defmodule String do
|
||||
iex> String.next_grapheme_size("olá")
|
||||
{1, "lá"}
|
||||
|
||||
iex> String.next_grapheme_size("")
|
||||
nil
|
||||
|
||||
"""
|
||||
@spec next_grapheme_size(t) :: {pos_integer, t} | nil
|
||||
defdelegate next_grapheme_size(string), to: String.Unicode
|
||||
@@ -1773,6 +1847,9 @@ defmodule String do
|
||||
iex> String.first("եոգլի")
|
||||
"ե"
|
||||
|
||||
iex> String.first("")
|
||||
nil
|
||||
|
||||
"""
|
||||
@spec first(t) :: grapheme | nil
|
||||
def first(string) do
|
||||
@@ -1866,8 +1943,7 @@ defmodule String do
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a substring starting at the offset `start`, and of
|
||||
length `len`.
|
||||
Returns a substring starting at the offset `start`, and of the given `length`.
|
||||
|
||||
If the offset is greater than string length, then it returns `""`.
|
||||
|
||||
@@ -1908,22 +1984,22 @@ defmodule String do
|
||||
""
|
||||
end
|
||||
|
||||
def slice(string, start, len) when start >= 0 and len >= 0 do
|
||||
def slice(string, start, length) when start >= 0 and length >= 0 do
|
||||
case String.Unicode.split_at(string, start) do
|
||||
{_, nil} ->
|
||||
""
|
||||
|
||||
{start_bytes, rest} ->
|
||||
{len_bytes, _} = String.Unicode.split_at(rest, len)
|
||||
{len_bytes, _} = String.Unicode.split_at(rest, length)
|
||||
binary_part(string, start_bytes, len_bytes)
|
||||
end
|
||||
end
|
||||
|
||||
def slice(string, start, len) when start < 0 and len >= 0 do
|
||||
def slice(string, start, length) when start < 0 and length >= 0 do
|
||||
start = length(string) + start
|
||||
|
||||
case start >= 0 do
|
||||
true -> slice(string, start, len)
|
||||
true -> slice(string, start, length)
|
||||
false -> ""
|
||||
end
|
||||
end
|
||||
@@ -2174,13 +2250,13 @@ defmodule String do
|
||||
For example, take the grapheme "é" which is made of the characters
|
||||
"e" and the acute accent. The following returns `true`:
|
||||
|
||||
iex> String.contains?(:unicode.characters_to_nfd_binary("é"), "e")
|
||||
iex> String.contains?(String.normalize("é", :nfd), "e")
|
||||
true
|
||||
|
||||
However, if "é" is represented by the single character "e with acute"
|
||||
accent, then it will return `false`:
|
||||
|
||||
iex> String.contains?(:unicode.characters_to_nfc_binary("é"), "e")
|
||||
iex> String.contains?(String.normalize("é", :nfc), "e")
|
||||
false
|
||||
|
||||
"""
|
||||
@@ -2527,6 +2603,7 @@ defmodule String do
|
||||
[eq: "fox ", del: "ho", ins: "jum", eq: "ps over the ", del: "dog", ins: "lazy cat"]
|
||||
|
||||
"""
|
||||
@doc since: "1.3.0"
|
||||
@spec myers_difference(t, t) :: [{:eq | :ins | :del, t}]
|
||||
def myers_difference(string1, string2) do
|
||||
graphemes(string1)
|
||||
|
||||
@@ -68,7 +68,7 @@ defmodule Supervisor do
|
||||
Supervisor.count_children(pid)
|
||||
#=> %{active: 1, specs: 1, supervisors: 0, workers: 1}
|
||||
|
||||
Notice that when starting the GenServer, we are registering it
|
||||
Note that when starting the GenServer, we are registering it
|
||||
with name `Stack`, which allows us to call it directly and get
|
||||
what is on the stack:
|
||||
|
||||
@@ -108,7 +108,7 @@ defmodule Supervisor do
|
||||
The child specification describes how the supervisor starts, shuts down,
|
||||
and restarts child processes.
|
||||
|
||||
The child specification is a map which contains 6 elements. The first two keys
|
||||
The child specification is a map containing up to 6 elements. The first two keys
|
||||
in the following list are required, and the remaining ones are optional:
|
||||
|
||||
* `:id` - any term used to identify the child specification
|
||||
@@ -194,7 +194,7 @@ defmodule Supervisor do
|
||||
start: {Stack, :start_link, [[:hello]]}
|
||||
}
|
||||
|
||||
The map above defines a supervisor with `:id` of `Stack` that is started
|
||||
The map above defines a child with `:id` of `Stack` that is started
|
||||
by calling `Stack.start_link([:hello])`.
|
||||
|
||||
However, specifying the child specification for each child as a map can be
|
||||
@@ -433,7 +433,7 @@ defmodule Supervisor do
|
||||
restarts in transient mode, and linked processes exit with the same
|
||||
reason unless they're trapping exits
|
||||
|
||||
Notice that the supervisor that reaches maximum restart intensity will exit with
|
||||
Note that the supervisor that reaches maximum restart intensity will exit with
|
||||
`:shutdown` reason. In this case the supervisor will only be restarted if its
|
||||
child specification was defined with the `:restart` option set to `:permanent`
|
||||
(the default).
|
||||
@@ -823,10 +823,11 @@ defmodule Supervisor do
|
||||
end
|
||||
|
||||
def start_child(supervisor, args) when is_list(args) do
|
||||
# TODO: Deprecate in v1.11
|
||||
# IO.warn(
|
||||
# "Supervisor.start_child/2 with a list of args is deprecated, please use DynamicSupervisor instead"
|
||||
# )
|
||||
IO.warn_once(
|
||||
{__MODULE__, :start_child},
|
||||
"Supervisor.start_child/2 with a list of args is deprecated, please use DynamicSupervisor instead",
|
||||
_stacktrace_drop_levels = 2
|
||||
)
|
||||
|
||||
call(supervisor, {:start_child, args})
|
||||
end
|
||||
@@ -853,10 +854,9 @@ defmodule Supervisor do
|
||||
def terminate_child(supervisor, child_id)
|
||||
|
||||
def terminate_child(supervisor, pid) when is_pid(pid) do
|
||||
# TODO: Deprecate in v1.11
|
||||
# IO.warn(
|
||||
# "Supervisor.terminate_child/2 with a PID is deprecated, please use DynamicSupervisor instead"
|
||||
# )
|
||||
IO.warn(
|
||||
"Supervisor.terminate_child/2 with a PID is deprecated, please use DynamicSupervisor instead"
|
||||
)
|
||||
|
||||
call(supervisor, {:terminate_child, pid})
|
||||
end
|
||||
@@ -929,7 +929,8 @@ defmodule Supervisor do
|
||||
|
||||
"""
|
||||
@spec which_children(supervisor) :: [
|
||||
{term() | :undefined, child | :restarting, :worker | :supervisor, :supervisor.modules()}
|
||||
# inlining module() | :dynamic here because :supervisor.modules() is not exported
|
||||
{term() | :undefined, child | :restarting, :worker | :supervisor, module() | :dynamic}
|
||||
]
|
||||
def which_children(supervisor) do
|
||||
call(supervisor, :which_children)
|
||||
|
||||
@@ -41,10 +41,10 @@ defmodule Supervisor.Spec do
|
||||
end
|
||||
end
|
||||
|
||||
Notice in this case we don't have to explicitly import
|
||||
`Supervisor.Spec` as `use Supervisor` automatically does so.
|
||||
Note that in this case we don't have to explicitly import
|
||||
`Supervisor.Spec` since `use Supervisor` automatically does so.
|
||||
Defining a module-based supervisor can be useful, for example,
|
||||
to perform initialization tasks in the `c:init/1` callback.
|
||||
to perform initialization tasks in the `c:Supervisor.init/1` callback.
|
||||
|
||||
## Supervisor and worker options
|
||||
|
||||
@@ -82,9 +82,9 @@ defmodule Supervisor.Spec do
|
||||
terminates abnormally, i.e., with an exit reason other than
|
||||
`:normal`, `:shutdown` or `{:shutdown, term}`
|
||||
|
||||
Notice that supervisor that reached maximum restart intensity will exit with `:shutdown` reason.
|
||||
In this case the supervisor will only be restarted if its child specification was defined with
|
||||
the `:restart` option is set to `:permanent` (the default).
|
||||
Note that supervisor that reached maximum restart intensity will exit with `:shutdown` reason.
|
||||
In this case the supervisor will only restart if its child specification was defined with
|
||||
the `:restart` option set to `:permanent` (the default).
|
||||
|
||||
### Shutdown values (`:shutdown`)
|
||||
|
||||
@@ -136,7 +136,7 @@ defmodule Supervisor.Spec do
|
||||
supervise and a set of `options`.
|
||||
|
||||
Returns a tuple containing the supervisor specification. This tuple can be
|
||||
used as the return value of the `c:init/1` callback when implementing a
|
||||
used as the return value of the `c:Supervisor.init/1` callback when implementing a
|
||||
module-based supervisor.
|
||||
|
||||
## Examples
|
||||
@@ -234,8 +234,7 @@ defmodule Supervisor.Spec do
|
||||
function: atom,
|
||||
modules: modules
|
||||
) :: spec
|
||||
# TODO: Deprecate on v1.11
|
||||
# @deprecated "Use the new child specifications outlined in the Supervisor module instead"
|
||||
@deprecated "Use the new child specifications outlined in the Supervisor module instead"
|
||||
def worker(module, args, options \\ []) do
|
||||
child(:worker, module, args, options)
|
||||
end
|
||||
@@ -269,8 +268,7 @@ defmodule Supervisor.Spec do
|
||||
function: atom,
|
||||
modules: modules
|
||||
) :: spec
|
||||
# TODO: Deprecate on v1.11
|
||||
# @deprecated "Use the new child specifications outlined in the Supervisor module instead"
|
||||
@deprecated "Use the new child specifications outlined in the Supervisor module instead"
|
||||
def supervisor(module, args, options \\ []) do
|
||||
options = Keyword.put_new(options, :shutdown, :infinity)
|
||||
child(:supervisor, module, args, options)
|
||||
|
||||
+41
-19
@@ -312,9 +312,14 @@ defmodule System do
|
||||
"""
|
||||
@spec user_home() :: String.t() | nil
|
||||
def user_home do
|
||||
{:ok, [[home] | _]} = :init.get_argument(:home)
|
||||
encoding = :file.native_name_encoding()
|
||||
:unicode.characters_to_binary(home, encoding, encoding)
|
||||
case :init.get_argument(:home) do
|
||||
{:ok, [[home] | _]} ->
|
||||
encoding = :file.native_name_encoding()
|
||||
:unicode.characters_to_binary(home, encoding, encoding)
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -393,8 +398,9 @@ defmodule System do
|
||||
@doc """
|
||||
Registers a program exit handler function.
|
||||
|
||||
Registers a function that will be invoked at the end of program execution.
|
||||
Useful for invoking a hook in "script" mode.
|
||||
Registers a function that will be invoked at the end of an Elixir script.
|
||||
A script is typically started via the command line via the `elixir` and
|
||||
`mix` executables.
|
||||
|
||||
The handler always executes in a different process from the one it was
|
||||
registered in. As a consequence, any resources managed by the calling process
|
||||
@@ -402,6 +408,9 @@ defmodule System do
|
||||
function is invoked.
|
||||
|
||||
The function must receive the exit status code as an argument.
|
||||
|
||||
If the VM terminates programmatically, via `System.stop/1` or `System.halt/1`,
|
||||
the `at_exit/1` callbacks are not executed.
|
||||
"""
|
||||
@spec at_exit((non_neg_integer -> any)) :: :ok
|
||||
def at_exit(fun) when is_function(fun, 1) do
|
||||
@@ -582,15 +591,20 @@ defmodule System do
|
||||
`__STACKTRACE__/0` inside a rescue/catch. If you want to support
|
||||
earlier Elixir versions, move `System.stacktrace/0` inside a rescue/catch.
|
||||
|
||||
Starting from Erlang/OTP 23, this function will always return an empty list.
|
||||
|
||||
Note that the Erlang VM (and therefore this function) does not
|
||||
return the current stacktrace but rather the stacktrace of the
|
||||
latest exception. To retrieve the stacktrace of the current process,
|
||||
use `Process.info(self(), :current_stacktrace)` instead.
|
||||
"""
|
||||
# TODO: Fully deprecate it on Elixir v1.11 via @deprecated
|
||||
# It is currently partially deprecated in elixir_dispatch.erl
|
||||
def stacktrace do
|
||||
apply(:erlang, :get_stacktrace, [])
|
||||
# TODO: Once Erlang/OTP 23 is required, remove conditional, and update @doc accordingly.
|
||||
# The warning is emitted by the compiler - so a @doc annotation is enough
|
||||
@doc deprecated: "Use __STACKTRACE__ instead"
|
||||
if function_exported?(:erlang, :get_stacktrace, 0) do
|
||||
def stacktrace, do: apply(:erlang, :get_stacktrace, [])
|
||||
else
|
||||
def stacktrace, do: []
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -623,6 +637,7 @@ defmodule System do
|
||||
System.halt(:abort)
|
||||
|
||||
"""
|
||||
@spec halt() :: no_return
|
||||
@spec halt(non_neg_integer | binary | :abort) :: no_return
|
||||
def halt(status \\ 0)
|
||||
|
||||
@@ -738,7 +753,12 @@ defmodule System do
|
||||
|
||||
* `:into` - injects the result into the given collectable, defaults to `""`
|
||||
* `:cd` - the directory to run the command in
|
||||
* `:env` - an enumerable of tuples containing environment key-value as binary
|
||||
* `:env` - an enumerable of tuples containing environment key-value as
|
||||
binary. The child process inherits all environment variables from its
|
||||
parent process, the Elixir application, except those overwritten or
|
||||
cleared using this option. Specify a value of `nil` to clear (unset) an
|
||||
environment variable, which is useful for preventing credentials passed
|
||||
to the application from leaking into child processes.
|
||||
* `:arg0` - sets the command arg0
|
||||
* `:stderr_to_stdout` - redirects stderr to stdout when `true`
|
||||
* `:parallelism` - when `true`, the VM will schedule port tasks to improve
|
||||
@@ -968,6 +988,7 @@ defmodule System do
|
||||
Inlined by the compiler.
|
||||
"""
|
||||
@spec os_time() :: integer
|
||||
@doc since: "1.3.0"
|
||||
def os_time do
|
||||
:os.system_time()
|
||||
end
|
||||
@@ -979,6 +1000,7 @@ defmodule System do
|
||||
with no limitation and is not monotonic.
|
||||
"""
|
||||
@spec os_time(time_unit) :: integer
|
||||
@doc since: "1.3.0"
|
||||
def os_time(unit) do
|
||||
:os.system_time(normalize_time_unit(unit))
|
||||
end
|
||||
@@ -987,6 +1009,7 @@ defmodule System do
|
||||
Returns the Erlang/OTP release number.
|
||||
"""
|
||||
@spec otp_release :: String.t()
|
||||
@doc since: "1.3.0"
|
||||
def otp_release do
|
||||
:erlang.list_to_binary(:erlang.system_info(:otp_release))
|
||||
end
|
||||
@@ -995,6 +1018,7 @@ defmodule System do
|
||||
Returns the number of schedulers in the VM.
|
||||
"""
|
||||
@spec schedulers :: pos_integer
|
||||
@doc since: "1.3.0"
|
||||
def schedulers do
|
||||
:erlang.system_info(:schedulers)
|
||||
end
|
||||
@@ -1003,6 +1027,7 @@ defmodule System do
|
||||
Returns the number of schedulers online in the VM.
|
||||
"""
|
||||
@spec schedulers_online :: pos_integer
|
||||
@doc since: "1.3.0"
|
||||
def schedulers_online do
|
||||
:erlang.system_info(:schedulers_online)
|
||||
end
|
||||
@@ -1071,15 +1096,12 @@ defmodule System do
|
||||
end
|
||||
|
||||
defp warn(unit, replacement_unit) do
|
||||
{:current_stacktrace, stacktrace} = Process.info(self(), :current_stacktrace)
|
||||
stacktrace = Enum.drop(stacktrace, 3)
|
||||
|
||||
:elixir_config.warn({System, unit}, stacktrace) &&
|
||||
IO.warn(
|
||||
"deprecated time unit: #{inspect(unit)}. A time unit should be " <>
|
||||
":second, :millisecond, :microsecond, :nanosecond, or a positive integer",
|
||||
stacktrace
|
||||
)
|
||||
IO.warn_once(
|
||||
{__MODULE__, unit},
|
||||
"deprecated time unit: #{inspect(unit)}. A time unit should be " <>
|
||||
":second, :millisecond, :microsecond, :nanosecond, or a positive integer",
|
||||
_stacktrace_drop_levels = 4
|
||||
)
|
||||
|
||||
replacement_unit
|
||||
end
|
||||
|
||||
+149
-11
@@ -203,6 +203,9 @@ defmodule Task do
|
||||
a list `[pid_n, ..., pid2, pid1]` with at least one entry Where `pid_n` is
|
||||
the PID that called the current process, `pid2` called `pid_n`, and `pid2` was
|
||||
called by `pid1`.
|
||||
|
||||
If a task crashes, the callers field is included as part of the log message
|
||||
metadata under the `:callers` key.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
@@ -458,8 +461,8 @@ defmodule Task do
|
||||
This is also useful when you're using the tasks for side effects.
|
||||
Defaults to `true`.
|
||||
|
||||
* `:timeout` - the maximum amount of time (in milliseconds) each
|
||||
task is allowed to execute for. Defaults to `5000`.
|
||||
* `:timeout` - the maximum amount of time (in milliseconds or `:infinity`)
|
||||
each task is allowed to execute for. Defaults to `5000`.
|
||||
|
||||
* `:on_timeout` - what to do when a task times out. The possible
|
||||
values are:
|
||||
@@ -488,6 +491,38 @@ defmodule Task do
|
||||
stream = Task.async_stream(collection, Mod, :expensive_fun, [], ordered: false)
|
||||
Stream.run(stream)
|
||||
|
||||
## Attention: async + take
|
||||
|
||||
Given items in an async stream are processed concurrently, doing
|
||||
`async_stream` followed by `Enum.take/2` may cause more items than
|
||||
requested to be processed. Let's see an example:
|
||||
|
||||
1..100
|
||||
|> Task.async_stream(fn i ->
|
||||
Process.sleep(100)
|
||||
IO.puts(to_string(i))
|
||||
end)
|
||||
|> Enum.take(10)
|
||||
|
||||
For a machine with 8 cores, the above will process 16 items instead
|
||||
of 10. The reason is that `async_stream/5` always have 8 elements
|
||||
processing at once. So by the time `Enum` says it got all elements
|
||||
it needed, there are still 6 elements left to be processed.
|
||||
|
||||
The solution here is to use `Stream.take/2` instead of `Enum.take/2`
|
||||
to filter elements before-hand:
|
||||
|
||||
1..100
|
||||
|> Stream.take(10)
|
||||
|> Task.async_stream(fn i ->
|
||||
Process.sleep(100)
|
||||
IO.puts(to_string(i))
|
||||
end)
|
||||
|> Enum.to_list()
|
||||
|
||||
If for some reason you cannot take the elements before hand,
|
||||
you can use `:max_concurrency` to limit how many elements
|
||||
may be over processed at the cost of reducing concurrency.
|
||||
"""
|
||||
@doc since: "1.4.0"
|
||||
@spec async_stream(Enumerable.t(), module, atom, [term], keyword) :: Enumerable.t()
|
||||
@@ -558,11 +593,12 @@ defmodule Task do
|
||||
In case the task process dies, the current process will exit with the same
|
||||
reason as the task.
|
||||
|
||||
A timeout in milliseconds or `:infinity`, can be given with a default value of `5000`. If the
|
||||
timeout is exceeded, then the current process will exit. If the task process
|
||||
is linked to the current process which is the case when a task is started with
|
||||
`async`, then the task process will also exit. If the task process is trapping
|
||||
exits or not linked to the current process, then it will continue to run.
|
||||
A timeout, in milliseconds or `:infinity`, can be given with a default value
|
||||
of `5000`. If the timeout is exceeded, then the current process will exit. If
|
||||
the task process is linked to the current process which is the case when a
|
||||
task is started with `async`, then the task process will also exit. If the
|
||||
task process is trapping exits or not linked to the current process, then it
|
||||
will continue to run.
|
||||
|
||||
This function assumes the task's monitor is still active or the monitor's
|
||||
`:DOWN` message is in the message queue. If it has been demonitored, or the
|
||||
@@ -608,6 +644,109 @@ defmodule Task do
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Awaits replies from multiple tasks and returns them.
|
||||
|
||||
This function receives a list of tasks and waits for their replies in the
|
||||
given time interval. It returns a list of the results, in the same order as
|
||||
the tasks supplied in the `tasks` input argument.
|
||||
|
||||
If any of the task processes dies, the current process will exit with the
|
||||
same reason as that task.
|
||||
|
||||
A timeout, in milliseconds or `:infinity`, can be given with a default value
|
||||
of `5000`. If the timeout is exceeded, then the current process will exit.
|
||||
Any task processes that are linked to the current process (which is the case
|
||||
when a task is started with `async`) will also exit. Any task processes that
|
||||
are trapping exits or not linked to the current process will continue to run.
|
||||
|
||||
This function assumes the tasks' monitors are still active or the monitors'
|
||||
`:DOWN` message is in the message queue. If any tasks have been demonitored,
|
||||
or the message already received, this function will wait for the duration of
|
||||
the timeout.
|
||||
|
||||
This function can only be called once for any given task. If you want to be
|
||||
able to check multiple times if a long-running task has finished its
|
||||
computation, use `yield_many/2` instead.
|
||||
|
||||
## Compatibility with OTP behaviours
|
||||
|
||||
It is not recommended to `await` long-running tasks inside an OTP behaviour
|
||||
such as `GenServer`. See `await/2` for more information.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> tasks = [
|
||||
...> Task.async(fn -> 1 + 1 end),
|
||||
...> Task.async(fn -> 2 + 3 end)
|
||||
...> ]
|
||||
iex> Task.await_many(tasks)
|
||||
[2, 5]
|
||||
|
||||
"""
|
||||
@doc since: "1.11.0"
|
||||
@spec await_many([t], timeout) :: [term]
|
||||
def await_many(tasks, timeout \\ 5000) when is_timeout(timeout) do
|
||||
awaiting =
|
||||
for task <- tasks, into: %{} do
|
||||
%Task{ref: ref, owner: owner} = task
|
||||
|
||||
if owner != self() do
|
||||
raise ArgumentError, invalid_owner_error(task)
|
||||
end
|
||||
|
||||
{ref, true}
|
||||
end
|
||||
|
||||
timeout_ref = make_ref()
|
||||
|
||||
timer_ref =
|
||||
if timeout != :infinity do
|
||||
Process.send_after(self(), timeout_ref, timeout)
|
||||
end
|
||||
|
||||
try do
|
||||
await_many(tasks, timeout, awaiting, %{}, timeout_ref)
|
||||
after
|
||||
timer_ref && Process.cancel_timer(timer_ref)
|
||||
receive do: (^timeout_ref -> :ok), after: (0 -> :ok)
|
||||
end
|
||||
end
|
||||
|
||||
defp await_many(tasks, _timeout, awaiting, replies, _timeout_ref)
|
||||
when map_size(awaiting) == 0 do
|
||||
for %{ref: ref} <- tasks, do: Map.fetch!(replies, ref)
|
||||
end
|
||||
|
||||
defp await_many(tasks, timeout, awaiting, replies, timeout_ref) do
|
||||
receive do
|
||||
^timeout_ref ->
|
||||
demonitor_pending_tasks(awaiting)
|
||||
exit({:timeout, {__MODULE__, :await_many, [tasks, timeout]}})
|
||||
|
||||
{:DOWN, ref, _, proc, reason} when is_map_key(awaiting, ref) ->
|
||||
demonitor_pending_tasks(awaiting)
|
||||
exit({reason(reason, proc), {__MODULE__, :await_many, [tasks, timeout]}})
|
||||
|
||||
{ref, reply} when is_map_key(awaiting, ref) ->
|
||||
Process.demonitor(ref, [:flush])
|
||||
|
||||
await_many(
|
||||
tasks,
|
||||
timeout,
|
||||
Map.delete(awaiting, ref),
|
||||
Map.put(replies, ref, reply),
|
||||
timeout_ref
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
defp demonitor_pending_tasks(awaiting) do
|
||||
Enum.each(awaiting, fn {ref, _} ->
|
||||
Process.demonitor(ref, [:flush])
|
||||
end)
|
||||
end
|
||||
|
||||
@doc false
|
||||
@deprecated "Pattern match directly on the message instead"
|
||||
def find(tasks, {ref, reply}) when is_reference(ref) do
|
||||
@@ -647,10 +786,9 @@ defmodule Task do
|
||||
* the caller is trapping exits
|
||||
|
||||
A timeout, in milliseconds or `:infinity`, can be given with a default value
|
||||
of `5000`. If the time runs out before a message from
|
||||
the task is received, this function will return `nil`
|
||||
and the monitor will remain active. Therefore `yield/2` can be
|
||||
called multiple times on the same task.
|
||||
of `5000`. If the time runs out before a message from the task is received,
|
||||
this function will return `nil` and the monitor will remain active. Therefore
|
||||
`yield/2` can be called multiple times on the same task.
|
||||
|
||||
This function assumes the task's monitor is still active or the
|
||||
monitor's `:DOWN` message is in the message queue. If it has been
|
||||
|
||||
@@ -102,8 +102,8 @@ defmodule Task.Supervised do
|
||||
%{
|
||||
label: {Task.Supervisor, :terminating},
|
||||
report: %{
|
||||
name: get_from(owner),
|
||||
starter: self(),
|
||||
name: self(),
|
||||
starter: get_from(owner),
|
||||
function: fun,
|
||||
args: args,
|
||||
reason: {log_value(kind, value), __STACKTRACE__}
|
||||
@@ -112,7 +112,8 @@ defmodule Task.Supervised do
|
||||
%{
|
||||
domain: [:otp, :elixir],
|
||||
error_logger: %{tag: :error_msg},
|
||||
report_cb: &__MODULE__.format_report/1
|
||||
report_cb: &__MODULE__.format_report/1,
|
||||
callers: Process.get(:"$callers")
|
||||
}
|
||||
)
|
||||
|
||||
@@ -268,7 +269,7 @@ defmodule Task.Supervised do
|
||||
receive do
|
||||
# The task at position "position" replied with "value". We put the
|
||||
# response in the "waiting" map and do nothing, since we'll only act on
|
||||
# this response when the replying task dies (we'll notice in the :down
|
||||
# this response when the replying task dies (we'll see this in the :down
|
||||
# message).
|
||||
{{^monitor_ref, position}, reply} ->
|
||||
%{^position => {pid, :running}} = waiting
|
||||
@@ -539,7 +540,7 @@ defmodule Task.Supervised do
|
||||
|
||||
# One of the spawned processes went down. We inform the parent process of
|
||||
# this and keep going.
|
||||
{:DOWN, ref, _, _, reason} ->
|
||||
{:DOWN, ref, _, _, reason} when is_map_key(running_tasks, ref) ->
|
||||
{task, running_tasks} = Map.pop(running_tasks, ref)
|
||||
%{position: position, timer_ref: timer_ref, timed_out?: timed_out?} = task
|
||||
|
||||
|
||||
@@ -27,8 +27,10 @@ defmodule Task.Supervisor do
|
||||
@typedoc "Option values used by `start_link`"
|
||||
@type option ::
|
||||
DynamicSupervisor.option()
|
||||
| {:restart, :supervisor.restart()}
|
||||
| {:shutdown, :supervisor.shutdown()}
|
||||
# :permanent | :transient | :temporary here because :supervisor.restart() is not exported
|
||||
| {:restart, :permanent | :transient | :temporary}
|
||||
# :brutal_kill | timeout() here because :supervisor.shutdown() is not exported
|
||||
| {:shutdown, :brutal_kill | timeout()}
|
||||
|
||||
@doc false
|
||||
def child_spec(opts) when is_list(opts) do
|
||||
|
||||
@@ -60,7 +60,7 @@ defmodule Tuple do
|
||||
|
||||
"""
|
||||
@spec duplicate(term, non_neg_integer) :: tuple
|
||||
def duplicate(data, size) do
|
||||
def duplicate(data, size) when is_integer(size) and size >= 0 do
|
||||
:erlang.make_tuple(size, data)
|
||||
end
|
||||
|
||||
@@ -83,7 +83,7 @@ defmodule Tuple do
|
||||
|
||||
"""
|
||||
@spec insert_at(tuple, non_neg_integer, term) :: tuple
|
||||
def insert_at(tuple, index, value) do
|
||||
def insert_at(tuple, index, value) when is_integer(index) and index >= 0 do
|
||||
:erlang.insert_element(index + 1, tuple, value)
|
||||
end
|
||||
|
||||
@@ -124,7 +124,7 @@ defmodule Tuple do
|
||||
|
||||
"""
|
||||
@spec delete_at(tuple, non_neg_integer) :: tuple
|
||||
def delete_at(tuple, index) do
|
||||
def delete_at(tuple, index) when is_integer(index) and index >= 0 do
|
||||
:erlang.delete_element(index + 1, tuple)
|
||||
end
|
||||
|
||||
|
||||
@@ -204,7 +204,7 @@ defmodule URI do
|
||||
next_pair =
|
||||
case :binary.split(undecoded_next_pair, "=") do
|
||||
[key, value] -> {decode_www_form(key), decode_www_form(value)}
|
||||
[key] -> {decode_www_form(key), nil}
|
||||
[key] -> {decode_www_form(key), ""}
|
||||
end
|
||||
|
||||
{next_pair, rest}
|
||||
|
||||
+116
-201
@@ -95,7 +95,9 @@ defmodule Version do
|
||||
"""
|
||||
|
||||
import Kernel, except: [match?: 2]
|
||||
defstruct [:major, :minor, :patch, :pre, :build]
|
||||
|
||||
@enforce_keys [:major, :minor, :patch]
|
||||
defstruct [:major, :minor, :patch, :build, pre: []]
|
||||
|
||||
@type version :: String.t() | t
|
||||
@type requirement :: String.t() | Version.Requirement.t()
|
||||
@@ -116,36 +118,92 @@ defmodule Version do
|
||||
for more information.
|
||||
"""
|
||||
|
||||
defstruct [:source, :matchspec, :compiled]
|
||||
defstruct [:source, :lexed]
|
||||
|
||||
@opaque t :: %__MODULE__{
|
||||
source: String.t(),
|
||||
matchspec: :ets.match_spec() | :ets.comp_match_spec(),
|
||||
compiled: boolean
|
||||
lexed: [atom | matchable]
|
||||
}
|
||||
|
||||
@typep matchable ::
|
||||
{Version.major(), Version.minor(), Version.patch(), Version.pre(), Version.build()}
|
||||
|
||||
@compile inline: [compare: 2]
|
||||
|
||||
@doc false
|
||||
@spec new(String.t(), :ets.match_spec()) :: t
|
||||
def new(source, spec) do
|
||||
%__MODULE__{source: source, matchspec: spec, compiled: false}
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec compile(t) :: t
|
||||
def compile(%__MODULE__{matchspec: spec} = requirement) do
|
||||
%{requirement | matchspec: :ets.match_spec_compile(spec), compiled: true}
|
||||
def new(source, lexed) do
|
||||
%__MODULE__{source: source, lexed: lexed}
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec match?(t, tuple) :: boolean
|
||||
def match?(%__MODULE__{matchspec: spec, compiled: true}, matchable_pattern) do
|
||||
matches = :ets.match_spec_run([matchable_pattern], spec)
|
||||
matches != []
|
||||
def match?(%__MODULE__{lexed: lexed}, matchable_pattern) do
|
||||
match_lexed?(lexed, matchable_pattern)
|
||||
end
|
||||
|
||||
def match?(%__MODULE__{matchspec: spec, compiled: false}, matchable_pattern) do
|
||||
{:ok, result} = :ets.test_ms(matchable_pattern, spec)
|
||||
result != false
|
||||
defp match_lexed?([operator, req, :&& | rest], version) do
|
||||
match_op?(operator, req, version) and match_lexed?(rest, version)
|
||||
end
|
||||
|
||||
defp match_lexed?([operator, req, :|| | rest], version) do
|
||||
match_op?(operator, req, version) or match_lexed?(rest, version)
|
||||
end
|
||||
|
||||
defp match_lexed?([operator, req], version) do
|
||||
match_op?(operator, req, version)
|
||||
end
|
||||
|
||||
defp match_op?(:==, req, version) do
|
||||
compare(version, req) == :eq
|
||||
end
|
||||
|
||||
defp match_op?(:!=, req, version) do
|
||||
compare(version, req) != :eq
|
||||
end
|
||||
|
||||
defp match_op?(:~>, {major, minor, nil, req_pre, _}, {_, _, _, pre, allow_pre} = version) do
|
||||
compare(version, {major, minor, 0, req_pre, nil}) in [:eq, :gt] and
|
||||
compare(version, {major + 1, 0, 0, [0], nil}) == :lt and
|
||||
(allow_pre or req_pre != [] or pre == [])
|
||||
end
|
||||
|
||||
defp match_op?(:~>, {major, minor, _, req_pre, _} = req, {_, _, _, pre, allow_pre} = version) do
|
||||
compare(version, req) in [:eq, :gt] and
|
||||
compare(version, {major, minor + 1, 0, [0], nil}) == :lt and
|
||||
(allow_pre or req_pre != [] or pre == [])
|
||||
end
|
||||
|
||||
defp match_op?(:>, {_, _, _, req_pre, _} = req, {_, _, _, pre, allow_pre} = version) do
|
||||
compare(version, req) == :gt and (allow_pre or req_pre != [] or pre == [])
|
||||
end
|
||||
|
||||
defp match_op?(:>=, {_, _, _, req_pre, _} = req, {_, _, _, pre, allow_pre} = version) do
|
||||
compare(version, req) in [:eq, :gt] and (allow_pre or req_pre != [] or pre == [])
|
||||
end
|
||||
|
||||
defp match_op?(:<, req, version) do
|
||||
compare(version, req) == :lt
|
||||
end
|
||||
|
||||
defp match_op?(:<=, req, version) do
|
||||
compare(version, req) in [:eq, :lt]
|
||||
end
|
||||
|
||||
defp compare({major1, minor1, patch1, pre1, _}, {major2, minor2, patch2, pre2, _}) do
|
||||
cond do
|
||||
major1 > major2 -> :gt
|
||||
major1 < major2 -> :lt
|
||||
minor1 > minor2 -> :gt
|
||||
minor1 < minor2 -> :lt
|
||||
patch1 > patch2 -> :gt
|
||||
patch1 < patch2 -> :lt
|
||||
pre1 == [] and pre2 != [] -> :gt
|
||||
pre1 != [] and pre2 == [] -> :lt
|
||||
pre1 > pre2 -> :gt
|
||||
pre1 < pre2 -> :lt
|
||||
true -> :eq
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -270,8 +328,12 @@ defmodule Version do
|
||||
|
||||
defp do_compare({major1, minor1, patch1, pre1, _}, {major2, minor2, patch2, pre2, _}) do
|
||||
cond do
|
||||
{major1, minor1, patch1} > {major2, minor2, patch2} -> :gt
|
||||
{major1, minor1, patch1} < {major2, minor2, patch2} -> :lt
|
||||
major1 > major2 -> :gt
|
||||
major1 < major2 -> :lt
|
||||
minor1 > minor2 -> :gt
|
||||
minor1 < minor2 -> :lt
|
||||
patch1 > patch2 -> :gt
|
||||
patch1 < patch2 -> :lt
|
||||
pre1 == [] and pre2 != [] -> :gt
|
||||
pre1 != [] and pre2 == [] -> :lt
|
||||
pre1 > pre2 -> :gt
|
||||
@@ -344,12 +406,8 @@ defmodule Version do
|
||||
@spec parse_requirement(String.t()) :: {:ok, Requirement.t()} | :error
|
||||
def parse_requirement(string) when is_binary(string) do
|
||||
case Version.Parser.parse_requirement(string) do
|
||||
{:ok, spec} ->
|
||||
requirement = Requirement.new(string, spec)
|
||||
{:ok, requirement}
|
||||
|
||||
:error ->
|
||||
:error
|
||||
{:ok, lexed} -> {:ok, Requirement.new(string, lexed)}
|
||||
:error -> :error
|
||||
end
|
||||
end
|
||||
|
||||
@@ -370,27 +428,20 @@ defmodule Version do
|
||||
@doc since: "1.8.0"
|
||||
@spec parse_requirement!(String.t()) :: Requirement.t()
|
||||
def parse_requirement!(string) when is_binary(string) do
|
||||
case Version.Parser.parse_requirement(string) do
|
||||
{:ok, spec} ->
|
||||
Requirement.new(string, spec)
|
||||
|
||||
:error ->
|
||||
raise InvalidRequirementError, string
|
||||
case parse_requirement(string) do
|
||||
{:ok, requirement} -> requirement
|
||||
:error -> raise InvalidRequirementError, string
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Compiles a requirement to its internal representation with
|
||||
`:ets.match_spec_compile/1` for faster matching.
|
||||
Compiles a requirement to an internal representation that may optimize matching.
|
||||
|
||||
The internal representation is opaque and cannot be converted to external
|
||||
term format and then back again without losing its properties (meaning it
|
||||
can not be sent to a process on another node and still remain a valid
|
||||
compiled match_spec, nor can it be stored on disk).
|
||||
The internal representation is opaque.
|
||||
"""
|
||||
@spec compile_requirement(Requirement.t()) :: Requirement.t()
|
||||
def compile_requirement(requirement) do
|
||||
Requirement.compile(requirement)
|
||||
requirement
|
||||
end
|
||||
|
||||
defp to_matchable(%Version{major: major, minor: minor, patch: patch, pre: pre}, allow_pre?) do
|
||||
@@ -454,13 +505,27 @@ defmodule Version do
|
||||
end
|
||||
|
||||
def lexer("", acc) do
|
||||
Enum.reverse(acc)
|
||||
Enum.map(Enum.reverse(acc), fn
|
||||
op when is_atom(op) ->
|
||||
op
|
||||
|
||||
version when is_binary(version) ->
|
||||
case Version.Parser.parse_version(version, true) do
|
||||
{:ok, version} -> version
|
||||
:error -> :error
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
@spec parse_requirement(String.t()) :: {:ok, term} | :error
|
||||
def parse_requirement(source) do
|
||||
lexed = lexer(source, [])
|
||||
to_matchspec(lexed)
|
||||
|
||||
if valid_requirement?(lexed) do
|
||||
{:ok, lexed}
|
||||
else
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
||||
def parse_version(string, approximate? \\ false) when is_binary(string) do
|
||||
@@ -550,7 +615,7 @@ defmodule Version do
|
||||
defp valid_requirement?([a | next]), do: valid_requirement?(a, next)
|
||||
|
||||
# it must finish with a version
|
||||
defp valid_requirement?(a, []) when is_binary(a) do
|
||||
defp valid_requirement?(a, []) when is_tuple(a) do
|
||||
true
|
||||
end
|
||||
|
||||
@@ -560,179 +625,29 @@ defmodule Version do
|
||||
end
|
||||
|
||||
# <version> or | <version> and
|
||||
defp valid_requirement?(a, [b | next]) when is_binary(a) and is_atom(b) and b in [:||, :&&] do
|
||||
defp valid_requirement?(a, [b | next]) when is_tuple(a) and is_atom(b) and b in [:||, :&&] do
|
||||
valid_requirement?(b, next)
|
||||
end
|
||||
|
||||
# or <version> | and <version>
|
||||
defp valid_requirement?(a, [b | next]) when is_atom(a) and is_binary(b) and a in [:||, :&&] do
|
||||
defp valid_requirement?(a, [b | next]) when is_atom(a) and is_tuple(b) and a in [:||, :&&] do
|
||||
valid_requirement?(b, next)
|
||||
end
|
||||
|
||||
# ~> <version>
|
||||
defp valid_requirement?(:~>, [b | next]) when is_tuple(b) do
|
||||
valid_requirement?(b, next)
|
||||
end
|
||||
|
||||
# <op> <version>
|
||||
defp valid_requirement?(a, [b | next]) when is_atom(a) and is_binary(b) do
|
||||
defp valid_requirement?(a, [{_major, _minor, patch, _pre, _build} = b | next])
|
||||
when is_atom(a) and is_integer(patch) do
|
||||
valid_requirement?(b, next)
|
||||
end
|
||||
|
||||
defp valid_requirement?(_, _) do
|
||||
false
|
||||
end
|
||||
|
||||
defp approximate_upper(version) do
|
||||
case version do
|
||||
{major, _minor, nil, _} ->
|
||||
{major + 1, 0, 0, [0]}
|
||||
|
||||
{major, minor, _patch, _} ->
|
||||
{major, minor + 1, 0, [0]}
|
||||
end
|
||||
end
|
||||
|
||||
defp to_matchspec(lexed) do
|
||||
if valid_requirement?(lexed) do
|
||||
first = to_condition(lexed)
|
||||
rest = Enum.drop(lexed, 2)
|
||||
{:ok, [{{:"$1", :"$2", :"$3", :"$4", :"$5"}, [to_condition(first, rest)], [:"$_"]}]}
|
||||
else
|
||||
:error
|
||||
end
|
||||
catch
|
||||
:invalid_matchspec -> :error
|
||||
end
|
||||
|
||||
defp to_condition([:==, version | _]) do
|
||||
matchable = parse_condition(version)
|
||||
main_condition(:==, matchable)
|
||||
end
|
||||
|
||||
defp to_condition([:!=, version | _]) do
|
||||
matchable = parse_condition(version)
|
||||
main_condition(:"/=", matchable)
|
||||
end
|
||||
|
||||
defp to_condition([:~>, version | _]) do
|
||||
from = parse_condition(version, true)
|
||||
to = approximate_upper(from)
|
||||
|
||||
{
|
||||
:andalso,
|
||||
to_condition([:>=, matchable_to_string(from)]),
|
||||
to_condition([:<, matchable_to_string(to)])
|
||||
}
|
||||
end
|
||||
|
||||
defp to_condition([:>, version | _]) do
|
||||
{major, minor, patch, pre} = parse_condition(version)
|
||||
|
||||
{
|
||||
:andalso,
|
||||
{
|
||||
:orelse,
|
||||
main_condition(:>, {major, minor, patch}),
|
||||
{:andalso, main_condition(:==, {major, minor, patch}), pre_condition(:>, pre)}
|
||||
},
|
||||
no_pre_condition(pre)
|
||||
}
|
||||
end
|
||||
|
||||
defp to_condition([:>=, version | _]) do
|
||||
matchable = parse_condition(version)
|
||||
|
||||
{:orelse, main_condition(:==, matchable), to_condition([:>, version])}
|
||||
end
|
||||
|
||||
defp to_condition([:<, version | _]) do
|
||||
{major, minor, patch, pre} = parse_condition(version)
|
||||
|
||||
{
|
||||
:orelse,
|
||||
main_condition(:<, {major, minor, patch}),
|
||||
{:andalso, main_condition(:==, {major, minor, patch}), pre_condition(:<, pre)}
|
||||
}
|
||||
end
|
||||
|
||||
defp to_condition([:<=, version | _]) do
|
||||
matchable = parse_condition(version)
|
||||
|
||||
{:orelse, main_condition(:==, matchable), to_condition([:<, version])}
|
||||
end
|
||||
|
||||
defp to_condition(current, []) do
|
||||
current
|
||||
end
|
||||
|
||||
defp to_condition(current, [:&&, operator, version | rest]) do
|
||||
to_condition({:andalso, current, to_condition([operator, version])}, rest)
|
||||
end
|
||||
|
||||
defp to_condition(current, [:||, operator, version | rest]) do
|
||||
to_condition({:orelse, current, to_condition([operator, version])}, rest)
|
||||
end
|
||||
|
||||
defp parse_condition(version, approximate? \\ false) do
|
||||
case parse_version(version, approximate?) do
|
||||
{:ok, {major, minor, patch, pre, _build}} -> {major, minor, patch, pre}
|
||||
:error -> throw(:invalid_matchspec)
|
||||
end
|
||||
end
|
||||
|
||||
defp main_condition(op, version) when tuple_size(version) == 3 do
|
||||
{op, {{:"$1", :"$2", :"$3"}}, {:const, version}}
|
||||
end
|
||||
|
||||
defp main_condition(op, version) when tuple_size(version) == 4 do
|
||||
{op, {{:"$1", :"$2", :"$3", :"$4"}}, {:const, version}}
|
||||
end
|
||||
|
||||
defp pre_condition(:>, pre) do
|
||||
length_pre = length(pre)
|
||||
|
||||
{
|
||||
:orelse,
|
||||
{:andalso, {:==, {:length, :"$4"}, 0}, {:const, length_pre != 0}},
|
||||
{
|
||||
:andalso,
|
||||
{:const, length_pre != 0},
|
||||
{
|
||||
:orelse,
|
||||
{:>, {:length, :"$4"}, length_pre},
|
||||
{:andalso, {:==, {:length, :"$4"}, length_pre}, {:>, :"$4", {:const, pre}}}
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp pre_condition(:<, pre) do
|
||||
length_pre = length(pre)
|
||||
|
||||
{
|
||||
:orelse,
|
||||
{:andalso, {:"/=", {:length, :"$4"}, 0}, {:const, length_pre == 0}},
|
||||
{
|
||||
:andalso,
|
||||
{:"/=", {:length, :"$4"}, 0},
|
||||
{
|
||||
:orelse,
|
||||
{:<, {:length, :"$4"}, length_pre},
|
||||
{:andalso, {:==, {:length, :"$4"}, length_pre}, {:<, :"$4", {:const, pre}}}
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp no_pre_condition([]) do
|
||||
{:orelse, :"$5", {:==, {:length, :"$4"}, 0}}
|
||||
end
|
||||
|
||||
defp no_pre_condition(_pre) do
|
||||
{:const, true}
|
||||
end
|
||||
|
||||
defp matchable_to_string({major, minor, patch, pre}) do
|
||||
patch = if patch, do: "#{patch}", else: "0"
|
||||
pre = if pre != [], do: "-#{Enum.join(pre, ".")}"
|
||||
"#{major}.#{minor}.#{patch}#{pre}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
+11
-8
@@ -8,12 +8,12 @@ Elixir applies bug fixes only to the latest minor branch. Security patches are a
|
||||
|
||||
Elixir version | Support
|
||||
:------------- | :-----------------------------
|
||||
1.10 | Development
|
||||
1.9 | Bug fixes and security patches
|
||||
1.11 | Development
|
||||
1.10 | Bug fixes and security patches
|
||||
1.9 | Security patches only
|
||||
1.8 | Security patches only
|
||||
1.7 | Security patches only
|
||||
1.6 | Security patches only
|
||||
1.5 | Security patches only
|
||||
|
||||
New releases are announced in the read-only [announcements mailing list](https://groups.google.com/group/elixir-lang-ann). All security releases [will be tagged with `[security]`](https://groups.google.com/forum/#!searchin/elixir-lang-ann/%5Bsecurity%5D%7Csort:date).
|
||||
|
||||
@@ -53,7 +53,8 @@ Elixir version | Supported Erlang/OTP versions
|
||||
1.7 | 19 - 22
|
||||
1.8 | 20 - 22
|
||||
1.9 | 20 - 22
|
||||
1.10 | 21 - 22
|
||||
1.10 | 21 - 22 (and Erlang/OTP 23 from v1.10.3)
|
||||
1.11 | 21 - 23
|
||||
|
||||
While Elixir often adds compatibility to new Erlang/OTP versions on released branches, such as support for Erlang/OTP 20 in v1.4.5, those releases usually contain the minimum changes for Elixir to run without errors. Only the next minor release, in this case v1.5.0, does effectively leverage the new features provided by the latest Erlang/OTP release.
|
||||
|
||||
@@ -75,6 +76,10 @@ The first column is the version the feature was hard deprecated. The second colu
|
||||
|
||||
Version | Deprecated feature | Replaced by (available since)
|
||||
:-------| :-------------------------------------------------- | :---------------------------------------------------------------
|
||||
[v1.11] | `Mix.Project.compile/2` | `Mix.Task.run("compile", args)` (v1.0)
|
||||
[v1.11] | `Supervisor.Spec.worker/3` and `Supervisor.Spec.supervisor/3` | The new child specs outlined in `Supervisor` (v1.5)
|
||||
[v1.11] | `Supervisor.start_child/2` and `Supervisor.terminate_child/2` | `DynamicSupervisor` (v1.6)
|
||||
[v1.11] | `System.stacktrace/1` | `__STACKTRACE__` in `try/catch/rescue` (v1.7)
|
||||
[v1.10] | `Code.ensure_compiled?/1` | `Code.ensure_compiled/1` (v1.0)
|
||||
[v1.10] | `Code.load_file/2` | `Code.require_file/2` (v1.0) or `Code.compile_file/2` (v1.7)
|
||||
[v1.10] | `Code.loaded_files/0` | `Code.required_files/0` (v1.7)
|
||||
@@ -82,7 +87,6 @@ Version | Deprecated feature | Replaced by (ava
|
||||
[v1.10] | Passing non-chardata to `Logger.log/2` | Explicitly convert to string with `to_string/1` (v1.0)
|
||||
[v1.10] | `:compile_time_purge_level` in `Logger` app environment | `:compile_time_purge_matching` in `Logger` app environment (v1.7)
|
||||
[v1.10] | `Supervisor.Spec.supervise/2` | The new child specs outlined in `Supervisor` (v1.5)
|
||||
[v1.10] | `String.normalize/2` | `:unicode.characters_to_nfc_binary/1` or `:unicode.characters_to_nfd_binary/1` (Erlang/OTP 20)
|
||||
[v1.10] | `:simple_one_for_one` strategy in `Supervisor` | `DynamicSupervisor` (v1.6)
|
||||
[v1.10] | `:restart` and `:shutdown` in `Task.Supervisor.start_link/1` | `:restart` and `:shutdown` in `Task.Supervisor.start_child/3` (v1.6)
|
||||
[v1.9] | Enumerable keys in `Map.drop/2`, `Map.split/2`, and `Map.take/2` | Call `Enum.to_list/1` on the second argument before hand (v1.0)
|
||||
@@ -106,9 +110,7 @@ Version | Deprecated feature | Replaced by (ava
|
||||
[v1.7] | `Registry.start_link/3` | `Registry.start_link/1` (v1.5)
|
||||
[v1.7] | `Stream.chunk/2,3,4` | `Stream.chunk_every/2` and [`Stream.chunk_every/3,4`](`Stream.chunk_every/4`) (v1.5)
|
||||
[v1.6] | `Enum.partition/2` | `Enum.split_with/2` (v1.4)
|
||||
[v1.6] | `Keyword.replace/3` | `Keyword.fetch/2` + `Keyword.put/3` (v1.0)
|
||||
[v1.6] | `Macro.unescape_tokens/1,2` | Use `Enum.map/2` to traverse over the arguments (v1.0)
|
||||
[v1.6] | `Map.replace/3` | `Map.fetch/2` + `Map.put/3` (v1.0)
|
||||
[v1.6] | `Module.add_doc/6` | [`@doc`](`Module`) module attribute (v1.0)
|
||||
[v1.6] | `Range.range?/1` | Pattern match on [`_.._`](`Kernel.../2`) (v1.0)
|
||||
[v1.5] | `()` to mean `nil` | `nil` (v1.0)
|
||||
@@ -171,4 +173,5 @@ Version | Deprecated feature | Replaced by (ava
|
||||
[v1.7]: https://github.com/elixir-lang/elixir/blob/v1.7/CHANGELOG.md#4-hard-deprecations
|
||||
[v1.8]: https://github.com/elixir-lang/elixir/blob/v1.8/CHANGELOG.md#4-hard-deprecations
|
||||
[v1.9]: https://github.com/elixir-lang/elixir/blob/v1.9/CHANGELOG.md#4-hard-deprecations
|
||||
[v1.10]: https://github.com/elixir-lang/elixir/blob/master/CHANGELOG.md#4-hard-deprecations
|
||||
[v1.10]: https://github.com/elixir-lang/elixir/blob/v1.10/CHANGELOG.md#4-hard-deprecations
|
||||
[v1.11]: https://github.com/elixir-lang/elixir/blob/v1.11/CHANGELOG.md#4-hard-deprecations
|
||||
@@ -8,7 +8,7 @@ You can create a new Elixir library by running the `mix new` command:
|
||||
|
||||
$ mix new my_library
|
||||
|
||||
The project name is given in the `snake_case` convention where all letters are lowercase and words are separate with underscores. This is the same convention used by variables, function names and atoms in Elixir. See the [Naming Conventions](naming-conventions.html) document for more information.
|
||||
The project name is given in the `snake_case` convention where all letters are lowercase and words are separate with underscores. This is the same convention used by variables, function names and atoms in Elixir. See the [Naming Conventions](naming-conventions.md) document for more information.
|
||||
|
||||
Every project has a `mix.exs` file, with instructions on how to build, compile, run tests, and so on. Libraries commonly have a `lib` directory, which includes Elixir source code, and a `test` directory. A `src` directory may also exist for Erlang sources.
|
||||
|
||||
@@ -30,7 +30,7 @@ Writing code is only the first of many steps to publish a package. We strongly r
|
||||
|
||||
* Write tests. Elixir ships with a test-framework named [ExUnit](https://hexdocs.pm/ex_unit/ExUnit.html). The project generated by `mix new` includes sample tests and doctests.
|
||||
|
||||
* Write documentation. The Elixir community is proud of treating documentation as a first-class citizen and making documentation easily accessible. Libraries contribute to the status quo by providing complete API documentation with examples for their modules, types and functions. See the [Writing Documentation](writing-documentation.html) guide for more information. Projects like [ExDoc](https://github.com/elixir-lang/ex_doc) can be used to generate HTML and EPUB documents from the documentation. ExDoc also supports "extra pages", like this one that you are reading. Such pages augment the documentation with tutorials, guides and references.
|
||||
* Write documentation. The Elixir community is proud of treating documentation as a first-class citizen and making documentation easily accessible. Libraries contribute to the status quo by providing complete API documentation with examples for their modules, types and functions. See the [Writing Documentation](writing-documentation.md) guide for more information. Projects like [ExDoc](https://github.com/elixir-lang/ex_doc) can be used to generate HTML and EPUB documents from the documentation. ExDoc also supports "extra pages", like this one that you are reading. Such pages augment the documentation with tutorials, guides and references.
|
||||
|
||||
Projects are often made available to other developers [by publishing a Hex package](https://hex.pm/docs/publish). Hex also [supports private packages for organizations](https://hex.pm/pricing). If ExDoc is configured for the Mix project, publishing a package on Hex will also automatically publish the generated documentation to [HexDocs](https://hexdocs.pm).
|
||||
|
||||
@@ -144,6 +144,34 @@ The application environment should be reserved only for configurations that are
|
||||
|
||||
For all remaining scenarios, libraries should not force their users to use the application environment for configuration. If the user of a library believes that certain parameter should be configured globally, then they can wrap the library functionality with their own application environment configuration.
|
||||
|
||||
### Avoid compile-time application configuration
|
||||
|
||||
Assuming you need to use the application configuration and you cannot avoid it as explained in the previous section, you should also avoid compile-time application configuration. For example, instead of doing this:
|
||||
|
||||
```elixir
|
||||
@http_client Application.fetch_env!(:my_app, :http_client)
|
||||
|
||||
def request(path) do
|
||||
@http_client.request(path)
|
||||
end
|
||||
```
|
||||
|
||||
you should do this:
|
||||
|
||||
```elixir
|
||||
def request(path) do
|
||||
http_client().request(path)
|
||||
end
|
||||
|
||||
defp http_client() do
|
||||
Application.fetch_env!(:my_app, :http_client)
|
||||
end
|
||||
```
|
||||
|
||||
That's because by reading the application in the module body and storing it in a module attribute, we are effectively reading the configuration at compile-time, which may become an issue when configuring the system later.
|
||||
|
||||
If, for some reason, you must read the application environment at compile time, use `Application.compile_env/2`. Read [the "Compile-time environment" section of the Application docs](Application.html#module-compile-time-environment) for more information.
|
||||
|
||||
### Avoid `use` when an `import` is enough
|
||||
|
||||
A library should not provide `use MyLib` functionality if all `use MyLib` does is to `import`/`alias` the module itself. For example, this is an anti-pattern:
|
||||
@@ -71,7 +71,7 @@ More examples of paired functions: `Base.decode16/2` and `Base.decode16!/2`, `Fi
|
||||
|
||||
There are also some non-paired functions, with no non-bang variant. The bang still signifies that it will raise an exception on failure. Example: `Protocol.assert_protocol!/1`.
|
||||
|
||||
In macro code, the bang on `Kernel.alias!/1` and `Kernel.var!/2` signifies that [macro hygiene](https://elixir-lang.org/getting-started/meta/macros.html#macros-hygiene) is set aside.
|
||||
In macro code, the bang on `Kernel.alias!/1` and `Kernel.var!/2` signifies that [macro hygiene](https://elixir-lang.org/getting-started/meta/macros.html#macro-hygiene) is set aside.
|
||||
|
||||
## Trailing question mark (`foo?`)
|
||||
|
||||
@@ -89,7 +89,7 @@ Examples: `Integer.is_even/1`, `Kernel.is_list/1`
|
||||
|
||||
These functions and macros follow the Erlang convention of an `is_` prefix, instead of a trailing question mark, precisely to indicate that they are allowed in guard clauses.
|
||||
|
||||
Note that type checks that are not valid in guard clauses do not follow this convention. Examples: `Keyword.keyword?/1`, `Regex.regex?/1`
|
||||
Note that type checks that are not valid in guard clauses do not follow this convention. For example: `Keyword.keyword?/1`.
|
||||
|
||||
## Special names
|
||||
|
||||
@@ -13,7 +13,7 @@ Operator
|
||||
`+` `-` `!` `^` `not` `~~~` | Unary
|
||||
`*` `/` | Left to right
|
||||
`+` `-` | Left to right
|
||||
`++` `--` `..` `<>` | Right to left
|
||||
`++` `--` `..` `<>` `+++` `---` | Right to left
|
||||
`^^^` | Left to right
|
||||
`in` `not in` | Left to right
|
||||
`\|>` `<<<` `>>>` `<<~` `~>>` `<~` `~>` `<~>` `<\|>` | Left to right
|
||||
@@ -29,18 +29,55 @@ Operator
|
||||
`when` | Right to left
|
||||
`<-` `\\` | Left to right
|
||||
|
||||
## General operators
|
||||
|
||||
Elixir provides the following built-in operators that are defined as functions that can be overridden:
|
||||
|
||||
* [`+`](`+/1`) and [`-`](`-/1`) - unary positive/negative
|
||||
* [`+`](`+/2`), [`-`](`-/2`), [`*`](`*/2`), and [`/`](`//2`) - basic arithmetic operations
|
||||
* [`++`](`++/2`) and [`--`](`--/2`) - list concatenation and subtraction
|
||||
* [`and`](`and/2`) and [`&&`](`&&/2`) - strict and relaxed boolean "and"
|
||||
* [`or`](`or/2`) and [`||`](`||/2`) - strict and relaxed boolean "or"
|
||||
* [`not`](`not/1`) and [`!`](`!/1`) - strict and relaxed boolean "not"
|
||||
* [`in`](`in/2`) and [`not in`](`in/2`) - membership
|
||||
* [`@`](`@/1`) - module attribute
|
||||
* [`..`](`../2`) - range creation
|
||||
* [`<>`](`<>/2`) - binary concatenation
|
||||
* [`|>`](`|>/2`) - pipeline
|
||||
* [`=~`](`=~/2`) - text-based match
|
||||
|
||||
Many of those can be used in guards; consult the [list of allowed guard functions and operators](patterns-and-guards.md#list-of-allowed-functions-and-operators).
|
||||
|
||||
Additionally, there are a few other operators that Elixir parses but doesn't actually use.
|
||||
See [Custom and overridden operators](#custom-and-overridden-operators) below for a list and for guidelines about their use.
|
||||
|
||||
Some other operators are special forms and cannot be overridden:
|
||||
|
||||
* [`^`](`^/1`) - pin operator
|
||||
* [`.`](`./2`) - dot operator
|
||||
* [`=`](`=/2`) - match operator
|
||||
* [`&`](`&/1`) - capture operator
|
||||
* [`::`](`Kernel.SpecialForms.::/2`) - type operator
|
||||
|
||||
Finally, these operators appear in the precedence table above but are only meaningful within certain constructs:
|
||||
|
||||
* `=>` - see [`%{}`](`%{}/1`)
|
||||
* `when` - see [Guards](patterns-and-guards.md#guards)
|
||||
* `<-` - see [`for`](`for/1`) and [`with`](`with/1`)
|
||||
* `\\` - see [Default arguments](Kernel.html#def/2-default-arguments)
|
||||
|
||||
## Comparison operators
|
||||
|
||||
Elixir provides the following built-in comparison operators:
|
||||
Elixir provides the following built-in comparison operators (all of which can be used in guards):
|
||||
|
||||
* [`==`](`==/2`) - equality
|
||||
* [`===`](`===/2`) - strict equality
|
||||
* [`!=`](`!=/2`) - inequality
|
||||
* [`!==`](`!==/2`) - strict inequality
|
||||
* [`<`](`</2`) - less than
|
||||
* [`>`](`>/2`) - greater than
|
||||
* [`<=`](`<=/2`) - less than or equal
|
||||
* [`>=`](`>=/2`) - greater than or equal
|
||||
* [`==`](`==/2`) - equal to
|
||||
* [`===`](`===/2`) - strictly equal to
|
||||
* [`!=`](`!=/2`) - inequal to
|
||||
* [`!==`](`!==/2`) - strictly inequal to
|
||||
* [`<`](`</2`) - less-than
|
||||
* [`>`](`>/2`) - greater-than
|
||||
* [`<=`](`<=/2`) - less-than or equal to
|
||||
* [`>=`](`>=/2`) - greater-than or equal to
|
||||
|
||||
The only difference between [`==`](`==/2`) and [`===`](`===/2`) is that [`===`](`===/2`) is strict when it comes to comparing integers and floats:
|
||||
|
||||
@@ -76,6 +113,7 @@ The collection types are compared using the following rules:
|
||||
* 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.
|
||||
* 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.
|
||||
|
||||
## Custom and overridden operators
|
||||
|
||||
@@ -107,7 +145,6 @@ iex> 1 <~ 2
|
||||
|
||||
The following is a table of all the operators that Elixir is capable of parsing, but that are not used by default:
|
||||
|
||||
* `|`
|
||||
* `|||`
|
||||
* `&&&`
|
||||
* `<<<`
|
||||
@@ -119,6 +156,8 @@ The following is a table of all the operators that Elixir is capable of parsing,
|
||||
* `<~>`
|
||||
* `<|>`
|
||||
* `^^^`
|
||||
* `+++`
|
||||
* `---`
|
||||
* `~~~`
|
||||
|
||||
The following operators are used by the `Bitwise` module when imported: [`&&&`](`Bitwise.&&&/2`), [`^^^`](`Bitwise.^^^/2`), [`<<<`](`Bitwise.<<</2`), [`>>>`](`Bitwise.>>>/2`), [`|||`](`Bitwise.|||/2`), [`~~~`](`Bitwise.~~~/1`). See the documentation for `Bitwise` for more information.
|
||||
@@ -6,7 +6,7 @@ This page describes the semantics of patterns and guards, where they are all all
|
||||
|
||||
## Patterns
|
||||
|
||||
Patterns in Elixir are made of variables, literals, and data-structure specific syntax. One of the most used constructs to perform pattern matching is the match operator (`=`):
|
||||
Patterns in Elixir are made of variables, literals, and data-structure specific syntax. One of the most used constructs to perform pattern matching is the match operator ([`=`](`=/2`)):
|
||||
|
||||
```iex
|
||||
iex> x = 1
|
||||
@@ -83,7 +83,7 @@ Atoms and numbers (integers and floats) can appear in patterns and they are alwa
|
||||
|
||||
```iex
|
||||
iex> :atom = :atom
|
||||
:atotm
|
||||
:atom
|
||||
iex> :atom = :another_atom
|
||||
** (MatchError) no match of right hand side value: :another_atom
|
||||
```
|
||||
@@ -156,7 +156,7 @@ iex> [head | tail] = []
|
||||
** (MatchError) no match of right hand side value: []
|
||||
```
|
||||
|
||||
Given charlists are represented as a list of integers, one can also perform prefix matches on charlists using the list concatenation operator (`++`):
|
||||
Given charlists are represented as a list of integers, one can also perform prefix matches on charlists using the list concatenation operator ([`++`](`++/2`)):
|
||||
|
||||
```elixir
|
||||
iex> 'hello ' ++ world = 'hello world'
|
||||
@@ -207,7 +207,7 @@ Finally, note map keys in patterns must always be literals or previously bound v
|
||||
|
||||
### Binaries
|
||||
|
||||
Binaries may appear in patterns using the double less-than/greater-than syntax (`<<>>`). A binary in a pattern can match multiple segments at the same, each with different type, size, and unit:
|
||||
Binaries may appear in patterns using the double less-than/greater-than syntax ([`<<>>`](`<<>>/1`)). A binary in a pattern can match multiple segments at the same, each with different type, size, and unit:
|
||||
|
||||
```iex
|
||||
iex> <<val::unit(8)-size(2)-integer>> = <<123, 56>>
|
||||
@@ -216,9 +216,9 @@ iex> val
|
||||
31544
|
||||
```
|
||||
|
||||
See the documentation for `<<>>` for a complete definition of pattern matching for binaries.
|
||||
See the documentation for [`<<>>`](`<<>>/1`) for a complete definition of pattern matching for binaries.
|
||||
|
||||
Finally, remember that strings in Elixir are UTF-8 encoded binaries. This means that, similar to charlists, prefix matches on strings are also possible with the binary concatenation operator (`<>`):
|
||||
Finally, remember that strings in Elixir are UTF-8 encoded binaries. This means that, similar to charlists, prefix matches on strings are also possible with the binary concatenation operator ([`<>`](`<>/2`)):
|
||||
|
||||
```elixir
|
||||
iex> "hello " <> world = "hello world"
|
||||
@@ -231,7 +231,7 @@ Suffix matches (`hello <> " world"`) are not valid patterns.
|
||||
|
||||
## Guards
|
||||
|
||||
Guards are a way to augment pattern matching with more complex checks. They are allowed in a predefined set of constructs where pattern matching is allowed, such as function definitions, case clauses, and ohers.
|
||||
Guards are a way to augment pattern matching with more complex checks. They are allowed in a predefined set of constructs where pattern matching is allowed, such as function definitions, case clauses, and others.
|
||||
|
||||
Not all expressions are allowed in guard clauses, but only a handful of them. This is a deliberate choice. This way, Elixir (and Erlang) can make sure that nothing bad happens while executing guards and no mutations happen anywhere. It also allows the compiler to optimize the code related to guards efficiently.
|
||||
|
||||
@@ -282,7 +282,7 @@ not_nil_head?(["some_value", "another_value"])
|
||||
Even though the head of the list is not `nil`, the first clause for `not_nil_head?/1` fails because the expression does not evaluate to `true`, but to `"some_value"`, therefore triggering the second clause which returns `false`. To make the guard behave correctly, you must ensure that the guard evaluates to `true`, like so:
|
||||
|
||||
```elixir
|
||||
def not_nil_head?(term) when head != nil, do: true
|
||||
def not_nil_head?([head | _]) when head != nil, do: true
|
||||
def not_nil_head?(_), do: false
|
||||
|
||||
not_nil_head?(["some_value", "another_value"])
|
||||
@@ -309,7 +309,7 @@ iex> case "hello" do
|
||||
...> _anything_else ->
|
||||
...> :failed
|
||||
...> end
|
||||
:worked
|
||||
:failed
|
||||
```
|
||||
|
||||
In many cases, we can take advantage of this. In the code above, we used `tuple_size/1` to both check that the given value is a tuple *and* check its size (instead of using `is_tuple(something) and tuple_size(something) == 2`).
|
||||
@@ -380,13 +380,7 @@ Check.empty?({})
|
||||
|
||||
## Where patterns and guards can be used
|
||||
|
||||
In the examples above, we have used the match operator (`=`) and function clauses to showcase patterns and guards respectively. Here is the list of the built-in constructs in Elixir that support patterns and guards.
|
||||
|
||||
* the match operator (`=`) (exceptionally does not support guards):
|
||||
|
||||
```elixir
|
||||
{:ok, binary} = File.read("some/file")
|
||||
```
|
||||
In the examples above, we have used the match operator ([`=`](`=/2`)) and function clauses to showcase patterns and guards respectively. Here is the list of the built-in constructs in Elixir that support patterns and guards.
|
||||
|
||||
* `match?/2`:
|
||||
|
||||
@@ -430,8 +424,16 @@ In the examples above, we have used the match operator (`=`) and function clause
|
||||
|
||||
* [`try`](`try/1`) supports patterns and guards on `catch` and `else`
|
||||
|
||||
* [`receive`](`receive/1`) supports patterns and guards to match on the received messages.
|
||||
|
||||
* custom guards can also be defined with `defguard/1` and `defguardp/1`. A custom guard can only be defined based on existing guards.
|
||||
|
||||
Note that the match operator ([`=`](`=/2`)) does *not* support guards:
|
||||
|
||||
```elixir
|
||||
{:ok, binary} = File.read("some/file")
|
||||
```
|
||||
|
||||
## Custom patterns and guards expressions
|
||||
|
||||
Only the constructs listed in this page are allowed in patterns and guards. However, we can take advantage of macros to write custom patterns guards that can simplify our programs or make them more domain-specific. At the end of the day, what matters is that the *output* of the macros boils down to a combination of the constructs above.
|
||||
@@ -21,7 +21,7 @@ Integers (`1234`) and floats (`123.4`) in Elixir are represented as a sequence o
|
||||
|
||||
### Atoms
|
||||
|
||||
Unquoted atoms start with a colon (`:`) which must be immediately followed by an underscore or a Unicode letter. The atom may continue using a sequence of Unicode letters, numbers, underscores, and `@`. Atoms may end in `!` or `?`. See [Unicode Syntax](unicode-syntax.html) for a formal specification. Valid unquoted atoms are: `:ok`, `:ISO8601`, and `:integer?`.
|
||||
Unquoted atoms start with a colon (`:`) which must be immediately followed by an underscore or a Unicode letter. The atom may continue using a sequence of Unicode letters, numbers, underscores, and `@`. Atoms may end in `!` or `?`. See [Unicode Syntax](unicode-syntax.md) for a formal specification. Valid unquoted atoms are: `:ok`, `:ISO8601`, and `:integer?`.
|
||||
|
||||
If the colon is immediately followed by a pair of double- or single-quotes surrounding the atom name, the atom is considered quoted. In contrast with an unquoted atom, this one can be made of any Unicode character (not only letters), such as `:'🌢 Elixir'`, `:"++olá++"`, and `:"123"`.
|
||||
|
||||
@@ -84,27 +84,27 @@ Structs built on the map syntax by passing the struct name between `%` and `{`.
|
||||
|
||||
### Variables
|
||||
|
||||
Variables in Elixir must start with an underscore or a Unicode letter that is not in uppercase or titlecase. The variable may continue using a sequence of Unicode letters, numbers, and underscores. Variables may end in `?` or `!`. See [Unicode Syntax](unicode-syntax.html) for a formal specification.
|
||||
Variables in Elixir must start with an underscore or a Unicode letter that is not in uppercase or titlecase. The variable may continue using a sequence of Unicode letters, numbers, and underscores. Variables may end in `?` or `!`. See [Unicode Syntax](unicode-syntax.md) for a formal specification.
|
||||
|
||||
[Elixir's naming conventions](naming-conventions.html) recommend variables to be in `snake_case` format.
|
||||
[Elixir's naming conventions](naming-conventions.md) recommend variables to be in `snake_case` format.
|
||||
|
||||
### Non-qualified calls (local calls)
|
||||
|
||||
Non-qualified calls, such as `add(1, 2)`, must start with an underscore or a Unicode letter that is not in uppercase or titlecase. The call may continue using a sequence of Unicode letters, numbers, and underscore. Calls may end in `?` or `!`. See [Unicode Syntax](unicode-syntax.html) for a formal specification.
|
||||
Non-qualified calls, such as `add(1, 2)`, must start with an underscore or a Unicode letter that is not in uppercase or titlecase. The call may continue using a sequence of Unicode letters, numbers, and underscore. Calls may end in `?` or `!`. See [Unicode Syntax](unicode-syntax.md) for a formal specification.
|
||||
|
||||
Parentheses for non-qualified calls are optional, except for zero-arity calls, which would then be ambiguous with variables. If parentheses are used, they must immediately follow the function name *without spaces*. For example, `add (1, 2)` is a syntax error, since `(1, 2)` is treated as an invalid block which is attempted to be given as a single argument to `add`.
|
||||
|
||||
[Elixir's naming conventions](naming-conventions.html) recommend calls to be in `snake_case` format.
|
||||
[Elixir's naming conventions](naming-conventions.md) recommend calls to be in `snake_case` format.
|
||||
|
||||
### Operators
|
||||
|
||||
As many programming languages, Elixir also support operators as non-qualified calls with their precedence and associativity rules. Constructs such as `=`, `when`, `&` and `@` are simply treated as operators. See [the Operators page](operators.html) for a full reference.
|
||||
As many programming languages, Elixir also support operators as non-qualified calls with their precedence and associativity rules. Constructs such as `=`, `when`, `&` and `@` are simply treated as operators. See [the Operators page](operators.md) for a full reference.
|
||||
|
||||
### Qualified calls (remote calls)
|
||||
|
||||
Qualified calls, such as `Math.add(1, 2)`, must start with an underscore or a Unicode letter that is not in uppercase or titlecase. The call may continue using a sequence of Unicode letters, numbers, and underscores. Calls may end in `?` or `!`. See [Unicode Syntax](unicode-syntax.html) for a formal specification.
|
||||
Qualified calls, such as `Math.add(1, 2)`, must start with an underscore or a Unicode letter that is not in uppercase or titlecase. The call may continue using a sequence of Unicode letters, numbers, and underscores. Calls may end in `?` or `!`. See [Unicode Syntax](unicode-syntax.md) for a formal specification.
|
||||
|
||||
[Elixir's naming conventions](naming-conventions.html) recommend calls to be in `snake_case` format.
|
||||
[Elixir's naming conventions](naming-conventions.md) recommend calls to be in `snake_case` format.
|
||||
|
||||
For qualified calls, Elixir also allows the function name to be written between double- or single-quotes, allowing calls such as `Math."++add++"(1, 2)`. Operators can be used as qualified calls without a need for quote, such as `Kernel.+(1, 2)`.
|
||||
|
||||
@@ -114,7 +114,7 @@ Parentheses for qualified calls are optional. If parentheses are used, they must
|
||||
|
||||
Aliases are constructs that expand to atoms at compile-time. The alias `String` expands to the atom `:"Elixir.String"`. Aliases must start with an ASCII uppercase character which may be followed by any ASCII letter, number, or underscore. Non-ASCII characters are not supported in aliases.
|
||||
|
||||
[Elixir's naming conventions](naming-conventions.html) recommend aliases to be in `CamelCase` format.
|
||||
[Elixir's naming conventions](naming-conventions.md) recommend aliases to be in `CamelCase` format.
|
||||
|
||||
### Blocks
|
||||
|
||||
@@ -234,7 +234,7 @@ end
|
||||
#=> {:+, [], [1, 2]}
|
||||
```
|
||||
|
||||
Notice that `.` is also an operator. Remote calls use the dot in the AST with two arguments, where the second argument is always an atom:
|
||||
Note that `.` is also an operator. Remote calls use the dot in the AST with two arguments, where the second argument is always an atom:
|
||||
|
||||
```elixir
|
||||
quote do
|
||||
@@ -104,6 +104,7 @@ The following literals are also supported in typespecs:
|
||||
## Maps
|
||||
| %{} # empty map
|
||||
| %{key: value_type} # map with required key :key of value_type
|
||||
| %{key_type => value_type} # map with required pairs of key_type and value_type
|
||||
| %{required(key_type) => value_type} # map with required pairs of key_type and value_type
|
||||
| %{optional(key_type) => value_type} # map with optional pairs of key_type and value_type
|
||||
| %SomeStruct{} # struct with all fields of any type
|
||||
@@ -162,7 +163,7 @@ A map value does not belong to this type if it contains a key that is not in the
|
||||
If you want to denote that keys that were not previously defined in the map are allowed,
|
||||
it is common to end a map type with `optional(any) => any`.
|
||||
|
||||
Notice that the syntactic representation of `map()` is `%{optional(any) => any}`, not `%{}`. The notation `%{}` specifies the singleton type for the empty map.
|
||||
Note that the syntactic representation of `map()` is `%{optional(any) => any}`, not `%{}`. The notation `%{}` specifies the singleton type for the empty map.
|
||||
|
||||
### User-defined types
|
||||
|
||||
@@ -259,7 +260,7 @@ Elixir's standard library contains a few frequently used behaviours such as `Gen
|
||||
|
||||
### Inspecting behaviours
|
||||
|
||||
The `@callback` and `@optional_callback` attributes are used to create a `behaviour_info/1` function available on the defining module. This function can be used to retrieve the callbacks and optional callbacks defined by that module.
|
||||
The `@callback` and `@optional_callbacks` attributes are used to create a `behaviour_info/1` function available on the defining module. This function can be used to retrieve the callbacks and optional callbacks defined by that module.
|
||||
|
||||
For example, for the `MyBehaviour` module defined in "Optional callbacks" above:
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user