Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
42fb7d2be3 | ||
|
|
332f2ca791 | ||
|
|
082e08664e | ||
|
|
68b660d8e8 | ||
|
|
a897d59df7 | ||
|
|
bd36b69cec | ||
|
|
eecafef2c0 | ||
|
|
1ad6e735cb | ||
|
|
5fd13212d9 | ||
|
|
46c461e617 | ||
|
|
9a2029245d | ||
|
|
b4bd740eb7 | ||
|
|
940d01c1c4 | ||
|
|
f4bf3fd20e | ||
|
|
1d3a5b85f4 | ||
|
|
34c7f2106c | ||
|
|
42cf846de0 | ||
|
|
94dc133058 | ||
|
|
9347ccd564 | ||
|
|
4215bbe986 | ||
|
|
f70e504ebb | ||
|
|
73cf46d785 | ||
|
|
ec50a88ee4 | ||
|
|
311f6fb57d | ||
|
|
8e27ce49a3 | ||
|
|
afedada0d1 | ||
|
|
1e12c7d6be | ||
|
|
264200a4a6 | ||
|
|
39acd239af | ||
|
|
1ac9007bae | ||
|
|
72e5d84b4e | ||
|
|
0cdaea1a8c | ||
|
|
a9976cc5d9 | ||
|
|
fce9b1c0d4 | ||
|
|
83e9b66e65 | ||
|
|
8efdf4eb10 | ||
|
|
56bc730a2a | ||
|
|
fd1109e859 | ||
|
|
1d599978f7 | ||
|
|
b64b214412 | ||
|
|
c540d1fa94 | ||
|
|
75536d80f1 | ||
|
|
0395301060 | ||
|
|
59434b1590 | ||
|
|
97770a660a | ||
|
|
23d9bfd3e1 | ||
|
|
99cd654460 | ||
|
|
45ef8fd94a | ||
|
|
784db65375 | ||
|
|
2421be6732 | ||
|
|
d5e131623d | ||
|
|
9046a2fe72 | ||
|
|
1796733076 | ||
|
|
c958e540e9 | ||
|
|
36f272cdf0 | ||
|
|
efcdfb8eca | ||
|
|
df9d1d6e52 | ||
|
|
1094302c24 | ||
|
|
03c42d723c | ||
|
|
09e1448710 | ||
|
|
502421c94e | ||
|
|
47b762b4f1 | ||
|
|
b48221d221 | ||
|
|
6f18bf70c0 | ||
|
|
82fa62d0a5 | ||
|
|
83ac4fe29c | ||
|
|
6a14c55681 | ||
|
|
3ba669da3e | ||
|
|
7ad36f1bcc | ||
|
|
8032f1dd19 | ||
|
|
9f24a452f8 | ||
|
|
fcf47f34ab | ||
|
|
600b7c63d3 | ||
|
|
f8ffaf01b5 | ||
|
|
f594d98a15 | ||
|
|
bb8480fc3c | ||
|
|
c64417d72f | ||
|
|
eacf0a9baf | ||
|
|
0cdfc80737 | ||
|
|
b2aad3d9f8 | ||
|
|
eab32c2797 | ||
|
|
1cb62b731c | ||
|
|
d714fb203b | ||
|
|
65e6b08576 | ||
|
|
9ee4bcf564 | ||
|
|
8a8fbcf2b1 | ||
|
|
45fc548139 | ||
|
|
9b65902446 | ||
|
|
b897da7b4c | ||
|
|
d44ad8142a | ||
|
|
7f8a8c57a4 | ||
|
|
7ff272706a | ||
|
|
fa1ff52c39 | ||
|
|
55da97b9a3 | ||
|
|
b5f1864d4e | ||
|
|
8ac03ec3b7 | ||
|
|
8f43d2bd2b | ||
|
|
1f50ad0aa5 | ||
|
|
f781a02a24 | ||
|
|
91e622889d | ||
|
|
db6e80277d | ||
|
|
23a431223b | ||
|
|
85ff5da403 | ||
|
|
571c108503 | ||
|
|
92de556716 | ||
|
|
209b0b31ff | ||
|
|
274dbb6e10 | ||
|
|
e17cb856a0 | ||
|
|
32d5bf3f18 | ||
|
|
c494891344 | ||
|
|
5337ab8716 | ||
|
|
8336f33263 | ||
|
|
b08f6fa3e2 | ||
|
|
97abb54442 | ||
|
|
c4a63d3c11 | ||
|
|
07e34342da | ||
|
|
bb0362f3f6 |
@@ -42,7 +42,7 @@ jobs:
|
||||
env:
|
||||
ERLC_OPTS: "warnings_as_errors"
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -123,7 +123,7 @@ jobs:
|
||||
- name: Configure Git
|
||||
run: git config --global core.autocrlf input
|
||||
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
@@ -28,16 +28,16 @@ jobs:
|
||||
build-mode: none
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
|
||||
uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
build-mode: ${{ matrix.build-mode }}
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@e46ed2cbd01164d986452f91f178727624ae40d7 # v4.35.3
|
||||
uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
|
||||
@@ -48,8 +48,8 @@ jobs:
|
||||
security-events: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Run zizmor
|
||||
uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3
|
||||
uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
|
||||
- name: Checkout project
|
||||
id: checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
fail-fast: false
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
fail-fast: false
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
--draft \
|
||||
"$GITHUB_REF_NAME"
|
||||
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
# zizmor: ignore[artipacked]
|
||||
if: github.ref_type == 'branch'
|
||||
|
||||
@@ -82,7 +82,7 @@ jobs:
|
||||
otp_version: "29.0"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
@@ -152,7 +152,7 @@ jobs:
|
||||
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
|
||||
- name: "Sign files with Trusted Signing"
|
||||
uses: azure/trusted-signing-action@b443cf8ea4124818d2ea9f043cba29fc3ec47b16 # v1.2.0
|
||||
uses: azure/trusted-signing-action@c7ab2a863ab5f9a846ddb8265964877ef296ee82 # v2.0.0
|
||||
if: ${{ matrix.flavor == 'windows' && vars.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
|
||||
with:
|
||||
endpoint: https://eus.codesigning.azure.net/
|
||||
@@ -205,7 +205,7 @@ jobs:
|
||||
|
||||
- name: Checkout project
|
||||
id: checkout
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
name: Notify
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
|
||||
+29
-398
@@ -4,433 +4,64 @@
|
||||
SPDX-FileCopyrightText: 2012 Plataformatec
|
||||
-->
|
||||
|
||||
# Changelog for Elixir v1.20
|
||||
# Changelog for Elixir v1.21
|
||||
|
||||
This release requires Erlang/OTP 27+ and is compatible with Erlang/OTP 29.
|
||||
|
||||
## Type system improvements
|
||||
|
||||
Elixir's type system now understands all language constructs and can infer types for your function definitions, using typing information from Elixir's standard library and your dependencies, to find verified bugs and dead code.
|
||||
|
||||
This has been achieved through a series of improvements, such as type refinement across clauses, occurrence typing, typing of map keys and domains, and more.
|
||||
|
||||
### Type inference of guards
|
||||
|
||||
This release also performs inference of guards! Let's see some examples:
|
||||
|
||||
```elixir
|
||||
def example(x, y) when is_list(x) and is_integer(y)
|
||||
```
|
||||
|
||||
The code above correctly infers `x` is a list and `y` is an integer.
|
||||
|
||||
```elixir
|
||||
def example({:ok, x} = y) when is_binary(x) or is_integer(x)
|
||||
```
|
||||
|
||||
The one above infers x is a binary or an integer, and `y` is a two element tuple with `:ok` as first element and a binary or integer as second.
|
||||
|
||||
```elixir
|
||||
def example(x) when is_map_key(x, :foo)
|
||||
```
|
||||
|
||||
The code above infers `x` is a map which has the `:foo` key, represented as `%{..., foo: dynamic()}`. Remember the leading `...` indicates the map may have other keys.
|
||||
|
||||
```elixir
|
||||
def example(x) when not is_map_key(x, :foo)
|
||||
```
|
||||
|
||||
And the code above infers `x` does not have the `:foo` key (hence `x.foo` will raise a typing violation), which has the type: `%{..., foo: not_set()}`.
|
||||
|
||||
You can also have expressions that assert on the size of data structures:
|
||||
|
||||
```elixir
|
||||
def example(x) when tuple_size(x) < 3
|
||||
```
|
||||
|
||||
Elixir will correctly track the tuple has at most two elements, and therefore accessing `elem(x, 3)` will emit a typing violation. In other words, Elixir can look at complex guards, infer types, and use this information to find bugs in our code, without a need to introduce type signatures (yet).
|
||||
|
||||
### Whole-body type inference
|
||||
|
||||
Elixir also performs inference based on the function body itself. Take the following code:
|
||||
|
||||
```elixir
|
||||
def add_foo_and_bar(data) do
|
||||
data.foo + data.bar
|
||||
end
|
||||
```
|
||||
|
||||
Elixir now infers that the function expects a `map` as first argument, and the map must have the keys `.foo` and `.bar` whose values are either `integer()` or `float()`. The return type will be either `integer()` or `float()`.
|
||||
|
||||
Here is another example:
|
||||
|
||||
```elixir
|
||||
def sum_to_string(a, b) do
|
||||
Integer.to_string(a + b)
|
||||
end
|
||||
```
|
||||
|
||||
Even though the `+` operator works with both integers and floats, Elixir infers that `a` and `b` must be both integers, as the result of `+` is given to a function that expects an integer. The inferred type information is then used during type checking to find possible typing errors. The typing inferred from your dependencies are also used to help infer more precise types for your own applications.
|
||||
|
||||
### Typing across clauses
|
||||
|
||||
Elixir now infers the type of a given clause based on previous clauses. Let's see an example:
|
||||
|
||||
```elixir
|
||||
case System.get_env("SOME_VAR") do
|
||||
nil -> :not_found
|
||||
value -> {:ok, String.upcase(value)}
|
||||
end
|
||||
```
|
||||
|
||||
`System.get_env("SOME_VAR")` returns either `nil` or a `binary()`. Because the first clause matches on `nil`, the type system knows `value` can no longer be `nil`, and therefore it must only be a `binary()`, which allows the second clause to also type check without violations.
|
||||
|
||||
This type inference across clauses also helps the type system find redundant clauses and dead code in existing codebases. Elixir v1.20 also implements occurrence typing for `cond`, `case`, and `with`, providing more precise types within each clause.
|
||||
|
||||
### Typing of atom and domain keys in maps
|
||||
|
||||
Maps were one of the first data-structures we implemented within the Elixir type system however, up to this point, they only supported atom keys. If they had additional keys, those keys were simply marked as `dynamic()`.
|
||||
|
||||
As of Elixir v1.20, we can track all possible domains as map keys. For example, the map:
|
||||
|
||||
```elixir
|
||||
%{123 => "hello", 456.0 => :ok}
|
||||
```
|
||||
|
||||
will have the type:
|
||||
|
||||
```elixir
|
||||
%{integer() => binary(), float() => :ok}
|
||||
```
|
||||
|
||||
It is also possible to mix domain keys, as above, with atom keys, yielding the following:
|
||||
|
||||
```elixir
|
||||
%{integer() => integer(), root: integer()}
|
||||
```
|
||||
|
||||
This system is an implementation of [Typing Records, Maps, and Structs, by Giuseppe Castagna (2023)](https://www.irif.fr/~gc/papers/icfp23.pdf).
|
||||
|
||||
### Typing of map operations
|
||||
|
||||
We have typed the majority of the functions in the `Map` module, allowing the type system to track how keys are added, updated, and removed across all possible key types.
|
||||
|
||||
For example, imagine we are calling the following `Map` functions with a variable `map`, which we don't know the exact shape of, and an atom key:
|
||||
|
||||
```elixir
|
||||
Map.put(map, :key, 123)
|
||||
#=> returns type %{..., key: integer()}
|
||||
|
||||
Map.delete(map, :key)
|
||||
#=> returns type %{..., key: not_set()}
|
||||
```
|
||||
|
||||
As you can see, we track when keys are set and also when they are removed.
|
||||
|
||||
Some operations, like `Map.replace/3`, only replace the key if it exists, and that is also propagated by the type system:
|
||||
|
||||
```elixir
|
||||
Map.replace(map, :key, 123)
|
||||
#=> returns type %{..., key: if_set(integer())}
|
||||
```
|
||||
|
||||
In other words, if the key exists, it would have been replaced by an integer value. Furthermore, whenever calling a function in the `Map` module and the given key is statically proven to never exist in the map, an error is emitted.
|
||||
|
||||
By combining full type inference with bang operations like `Map.fetch!/2`, `Map.pop!/2`, `Map.replace!/3`, and `Map.update!/3`, Elixir is able to propagate information about the desired keys. Take this module:
|
||||
|
||||
```elixir
|
||||
defmodule User do
|
||||
def name(map), do: Map.fetch!(map, :name)
|
||||
end
|
||||
|
||||
defmodule CallsUser do
|
||||
def calls_name do
|
||||
User.name(%{})
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
The code above has a type violation, which is now caught by the type system:
|
||||
|
||||
```text
|
||||
warning: incompatible types given to User.name/1:
|
||||
|
||||
User.name(%{})
|
||||
|
||||
given types:
|
||||
|
||||
%{name: not_set()}
|
||||
|
||||
but expected one of:
|
||||
|
||||
dynamic(%{..., name: term()})
|
||||
|
||||
type warning found at:
|
||||
│
|
||||
16 │ User.name(%{})
|
||||
│ ~
|
||||
│
|
||||
└─ lib/calls_user.ex:7:5: CallsUser.calls_name/0
|
||||
```
|
||||
|
||||
### Acknowledgements
|
||||
|
||||
The type system was made possible thanks to a partnership between [CNRS](https://www.cnrs.fr/) and [Remote](https://remote.com/). The development work is currently sponsored by [Fresha](https://www.fresha.com/) and [Tidewave](https://tidewave.ai/).
|
||||
|
||||
## Compile-time improvements
|
||||
|
||||
Elixir's v1.20 improves compilation times once more, especially on applications with many cores.
|
||||
|
||||
It also introduces a new compiler option called `:module_definition`, which if the module definition should be `:compiled` (the default) or `:interpreted`. Note this does not affect the `.beam` file written to disk, only how the contents inside `defmodule` are executed. Using the `:interpreted` mode may offer better compilation times for large projects, especially on machines with high core count, however, it comes with some downsides:
|
||||
|
||||
* Errors during compilation may have less precise stacktraces
|
||||
|
||||
* Anonymous functions within `defmodule` can have only up to 20 arguments.
|
||||
If this is an issue, you can use maps or tuples to group the data.
|
||||
Note the functions themselves inside `defmodule`, such as the ones defined
|
||||
inside `def` and friends, can still have up to 255 arguments
|
||||
|
||||
You can enable it by setting `elixirc_options: [module_definition: :interpreted]` in your `mix.exs`.
|
||||
|
||||
## v1.20.0-rc.6
|
||||
|
||||
This release requires Erlang/OTP 27+ and is compatible with Erlang/OTP 29.
|
||||
|
||||
### 1. Enhancements
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Kernel] Perform type inference across applications
|
||||
|
||||
### 2. Bug fixes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Kernel] Fix type checker bug when validating a `case` inside a `cond` condition (regression)
|
||||
* [Kernel] Preserve evaluation order when rewriting function calls from Elixir modules into Erlang ones
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix test] Respect --raise when mix test --warnings-as-errors passes with warnings
|
||||
|
||||
### 3. Hard deprecations
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix compile.elixir] `xref: [exclude: ...]` in your `mix.exs` is deprecated in favor of `elixirc_options: [no_warn_undefined: ...]`
|
||||
|
||||
## v1.20.0-rc.5 (2026-05-13)
|
||||
## v1.21.0-dev
|
||||
|
||||
### 1. Enhancements
|
||||
|
||||
#### EEx
|
||||
|
||||
* [EEx] Optimize compiler by flattening expr list only once
|
||||
* [EEx] Support splitting middle expressions across EEx clauses
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Base] Optimize Base validation functions by using SWAR techniques
|
||||
* [Float] Optimize `Float.round/2` by avoiding big integers
|
||||
* [Inspect] Increase inspect limit to help print deeply nested data structures
|
||||
* [Inspect] Support printing Erlang records (using Erlang notation)
|
||||
* [Kernel] Add occurrence typing on `case`, `cond`, and `with`
|
||||
* [Registry] Switch `{:duplicate, :key}` key_ets to ordered_set with composite keys
|
||||
* [String] SWAR-optimize ASCII fast paths in `String.length/1` and `String.slice/3`
|
||||
* [Access] Add support for keyword lists in `Access.key/2` and `Access.key!/1`
|
||||
* [Code] Add support for the `:erlc_options` compiler option
|
||||
* [Kernel] Warn on binary patterns with segments that are not byte-aligned
|
||||
* [Keyword] Optimize `Keyword.pop/3`, `Keyword.pop!/2`, and `Keyword.pop_lazy/3`
|
||||
* [List] Add `List.to_existing_atom/2` and `List.to_unsafe_atom/1`
|
||||
* [MapSet] Optimize `MapSet.symmetric_difference/2` when set sizes differ
|
||||
* [Registry] Optimize exact key matching in lookups
|
||||
* [String] Optimize `String.bag_distance/2`
|
||||
* [String] Add `String.to_existing_atom/2` and `String.to_unsafe_atom/1`
|
||||
* [URI] Optimize percent-decoding and `URI.to_string/1`
|
||||
|
||||
#### ExUnit
|
||||
|
||||
* [ExUnit] Show remaining runs when using `--repeat-until-failure`
|
||||
|
||||
#### IEx
|
||||
|
||||
* [IEx.Helpers] Add `source/1`
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix app.tree] Support `--output` option
|
||||
* [mix deps.tree] Support `--output` option
|
||||
* [mix help] Support printing docs for types and callbacks
|
||||
* [mix format] Support `--no-compile` option
|
||||
* [mix source] Add `mix source MODULE` to print or open a given module/function location
|
||||
|
||||
### 2. Potential breaking changes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Kernel] Disallow raw CR line ending in strings, comments and after `?` for security reasons
|
||||
|
||||
### 3. Bug fixes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Kernel] Fix a compiler crash when importing a module with `only: :sigils` option when the imported module exports non-sigil symbols with `sigil_` prefix
|
||||
* [Kernel] Reject negative Duration in `to_timeout/1`
|
||||
* [Macro] Fix generation of heredocs in `Macro.to_string/1` with escaped trailing newline
|
||||
* [Path] Consistently return path as binary in `Path.relative_to_cwd/2`
|
||||
* [Stream] Raise in `Stream.cycle/1` when enumerable reduce call yields no elements
|
||||
* [String] Support empty pattern list in `String.count/2`
|
||||
|
||||
#### Logger
|
||||
|
||||
* [Logger] Persist log level to app env in `Logger.configure/1`
|
||||
|
||||
#### Mix
|
||||
|
||||
* [Mix] Use `non_executable_binary_to_term` on loopback pubsub
|
||||
* [mix compile.elixir] Fix scenario where Elixir would tag mtimes in the future
|
||||
|
||||
## v1.20.0-rc.4 (2026-03-31)
|
||||
|
||||
This release requires Erlang/OTP 27+ and is compatible with Erlang/OTP 29.
|
||||
|
||||
### 1. Enhancements
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Code] Add `:dbg_callback` option to eval functions
|
||||
* [Code.Fragment] Allow preserving sigil metadata in `container_cursor_to_quoted`
|
||||
* [File] Add support for `[:raw]` opts in `File.read/2`
|
||||
* [Kernel] Show undefined function errors even when missing variables (this helps debug errors caused when the developer forgets to require a macro)
|
||||
* [Module] Purge and delete modules if `after_compile/2` callback fails
|
||||
* [PartitionSupervisor] Support via tuples in `count_children/1` and `stop/3`
|
||||
* [Process] Add `Process.get_label/1`
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix deps] Allow overriding specific dependencies in `:override`
|
||||
* [ExUnit.Assertions] Add `trace/3` helper
|
||||
|
||||
### 2. Bug fixes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Integer] Fix `Integer.extended_gcd/2` returning negative GCD for zero base cases
|
||||
* [Integer] Raise when negative out-of-range digits are given to `Integer.undigits/2`
|
||||
* [Kernel] Protocols should not add compile-time dependencies on `Any` implementation
|
||||
* [Kernel] Ensure structs trigger recompilation for type checking purposes (regression)
|
||||
* [Kernel] Ensure type information propagate across `hd/tl` in guards (regression)
|
||||
* [Keyword] Raise `ArgumentError` in `Keyword.from_keys/2` for non-atom keys
|
||||
* [URI] Fix `URI.merge` leaking `:+` marker when base path is empty string
|
||||
* [Kernel] Fix expansion of rebound variables in bitstring size expressions
|
||||
* [Kernel] Fix variables defined in one default argument leaking into subsequent default arguments
|
||||
* [Kernel.Typespec] Preserve metadata when proxying to Elixir typespecs
|
||||
|
||||
#### ExUnit
|
||||
|
||||
* [ExUnit.Diff] Avoid false positives when diffing bitstrings
|
||||
* [ExUnit.CaptureLog] Fix the typespec for capture log options
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix deps] Use config files to pass project state to avoid argv limits on Windows when using `MIX_OS_DEPS_COMPILE_PARTITION_COUNT`
|
||||
* [mix compile] Fix compile env change triggering full recompilation of path dependencies
|
||||
* [mix compile] Add a build lock around protocol consolidation in umbrellas
|
||||
* [mix compile] Ensure compilation of sibling deps do not mark path deps as changed
|
||||
* [mix test] Fix `--warnings-as-errors` not catching misnamed test file warnings
|
||||
|
||||
## v1.20.0-rc.3 (2026-03-09)
|
||||
|
||||
### 1. Enhancements
|
||||
|
||||
#### IEx
|
||||
|
||||
* [IEx] Optimize autocompleting modules
|
||||
|
||||
### 2. Bug fixes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Enum] Fix `Enum.slice/2` for ranges with step > 1 sliced by step > 1
|
||||
* [File] Allowing preserving directory permissions in `File.cp_r/3`
|
||||
* [File] Fix `File.cp_r/3` infinite loop with symlink cycles
|
||||
* [File] Fix `File.cp_r/3` infinite loop when copying into subdirectory of source
|
||||
* [File] Warn when defining `@type record()`, fixes CI on Erlang/OTP 29
|
||||
* [File] Fix `File.Stream` `Enumerable.count` for files without trailing newline
|
||||
* [Float] Fix `Float.parse/1` inconsistent error handling for non-scientific notation overflow
|
||||
* [Kernel] Process fields even when structs are unknown (regression)
|
||||
* [Kernel] Improve performance on several corner cases in the type system (regression)
|
||||
* [Kernel] Fix regression when using `Kernel.in/2` in defguard (regression)
|
||||
|
||||
## v1.20.0-rc.2 (2026-03-04)
|
||||
|
||||
### 1. Enhancements
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Code] Add `module_definition: :interpreted` option to `Code` which allows module definitions to be evaluated instead of compiled. In some applications/architectures, this can lead to drastic improvements to compilation times. Note this does not affect the generated `.beam` file, which will have the same performance/behaviour as before
|
||||
* [Code] Make module purging opt-in and move temporary module deletion to the background to speed up compilation times
|
||||
* [Integer] Add `Integer.popcount/1`
|
||||
* [Kernel] Add type inference across clauses. For example, if one clause says `x when is_integer(x)`, then the next clause may no longer be an integer
|
||||
* [Kernel] Detect and warn on redundant clauses
|
||||
* [List] Add `List.first!/1` and `List.last!/1`
|
||||
* Add Software Bill of Materials guide to the Documentation
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix compile] Add `module_definition: :interpreted` option to `Code` which allows module definitions to be evaluated instead of compiled. In some applications/architectures, this can lead to drastic improvements to compilation times. Note this does not affect the generated `.beam` file, which will have the same performance/behaviour as before
|
||||
* [mix deps] Parallelize dep lock status checks during `deps.loadpaths`, improving boot times in projects with many git dependencies
|
||||
|
||||
### 2. Bug fixes
|
||||
|
||||
#### IEx
|
||||
|
||||
* [IEx] Ensure warnings emitted during IEx parsing are properly displayed/printed
|
||||
* [IEx] Ensure pry works across remote nodes
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix compile.erlang] Topsort Erlang modules before compilation for proper dependency resolution
|
||||
|
||||
## v1.20.0-rc.1 (2026-01-13)
|
||||
|
||||
### 1. Bug fixes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Kernel] Do not crash on map types with struct keys when performing type operations (regression)
|
||||
* [Kernel] Mark the outcome of bitstring types as dynamic (regression)
|
||||
* [Kernel] `<<expr::bitstring>>` will have type `binary` instead of `bitstring` if `expr` is a binary (regression)
|
||||
* [Kernel] Do not crash on conditional variables when calling a function on a module which is represented by a variable (regression)
|
||||
|
||||
## v1.20.0-rc.0 (2026-01-09)
|
||||
|
||||
### 1. Enhancements
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Calendar] Optimize `date_from_iso_days` by using the Neri-Schneider algorithm
|
||||
* [Enum] Add `Enum.min_max` sorter
|
||||
* [Integer] Add `Integer.ceil_div/2`
|
||||
* [IO] Add `IO.iodata_empty?/1`
|
||||
* [File] Skip device, named pipes, etc in `File.cp_r/3` instead of erroring with reason `:eio`
|
||||
* [Kernel] Print intermediate results of `dbg` for pipes
|
||||
* [Kernel] Warn on unused requires
|
||||
* [Regex] Add `Regex.import/1` to import regexes defined with `/E`
|
||||
|
||||
#### ExUnit
|
||||
|
||||
* [ExUnit.CaptureLog] Add `:formatter` option for custom log formatting
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix deps] Support filtering `mix deps` output
|
||||
* [mix compile] Enforce `:elixirc_paths` to be a list of strings to avoid paths from being discarded (the only documented type was lists of strings)
|
||||
* [mix test] Add `mix test --dry-run`
|
||||
|
||||
### 2. Potential breaking changes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* `require SomeModule` no longer expands to the given module at compile-time, but it still returns the module at runtime. Note Elixir does not guarantee macros will expand to certain constructs, but since this can break code relying on the previous behaviour, such as `require(SomeMod).some_macro()`, we are adding this note to the CHANGELOG
|
||||
* [Mix.SCM.Git] Raise if Git refspecs start with `-`
|
||||
* [mix deps.compile] Preserve code paths and compiler options across OS partitions
|
||||
|
||||
### 3. Hard deprecations
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [File] `File.stream!(path, modes, lines_or_bytes)` is deprecated in favor of `File.stream!(path, lines_or_bytes, modes)`
|
||||
* [Kernel] Matching on the size inside a bit pattern now requires the pin operator for consistency, such as `<<x::size(^existing_var)>>`
|
||||
* [Kernel.ParallelCompiler] `Kernel.ParallelCompiler.async/1` is deprecated in favor of `Kernel.ParallelCompiler.pmap/2`, which is more performant and addresses known limitations
|
||||
* [Macro.Env] `Macro.Env.fetch_alias/2` and `Macro.Env.fetch_macro_alias/2` are deprecated, use `Macro.Env.expand_alias/4` instead
|
||||
|
||||
#### Logger
|
||||
### 4. Soft deprecations
|
||||
|
||||
* [Logger] `Logger.*_backend` functions are deprecated in favor of handlers. If you really want to keep on using backends, see the `:logger_backends` package
|
||||
* [Logger] `Logger.enable/1` and `Logger.disable/1` have been deprecated in favor of `Logger.put_process_level/2` and `Logger.delete_process_level/1`
|
||||
#### Elixir
|
||||
|
||||
## v1.19
|
||||
* [Kernel] Atom interpolation (`:"foo_#{bar}"`) is deprecated in favor of explicit `String.to_unsafe_atom/1`
|
||||
* [List] `List.to_atom/1` is deprecated in favor of `List.to_unsafe_atom/1`
|
||||
* [String] `String.to_atom/1` is deprecated in favor of `String.to_unsafe_atom/1`
|
||||
|
||||
The CHANGELOG for v1.19 releases can be found [in the v1.19 branch](https://github.com/elixir-lang/elixir/blob/v1.19/CHANGELOG.md).
|
||||
## v1.20
|
||||
|
||||
The CHANGELOG for v1.20 releases can be found [in the v1.20 branch](https://github.com/elixir-lang/elixir/blob/v1.20/CHANGELOG.md).
|
||||
|
||||
+1
-1
@@ -135,7 +135,7 @@ make docs
|
||||
|
||||
This will produce documentation sets for `elixir`, `eex`, `ex_unit`, `iex`, `logger`,
|
||||
and `mix` under the `doc` directory. If you are planning to contribute documentation,
|
||||
[please check our best practices for writing documentation](https://hexdocs.pm/elixir/writing-documentation.html).
|
||||
[please check our best practices for writing documentation](https://elixir.hexdocs.pm/writing-documentation.html).
|
||||
|
||||
## Licensing and Compliance Requirements
|
||||
|
||||
|
||||
@@ -189,7 +189,7 @@ clean_elixir:
|
||||
#==> Documentation tasks
|
||||
|
||||
SOURCE_REF = $(shell tag="$(call GIT_TAG)" revision="$(call GIT_REVISION)"; echo "$${tag:-$$revision}")
|
||||
DOCS_COMPILE = CANONICAL=$(CANONICAL) bin/elixir ../ex_doc/bin/ex_doc "$(1)" "$(VERSION)" "lib/$(2)/ebin" --main "$(3)" --source-url "https://github.com/elixir-lang/elixir" --source-ref "$(call SOURCE_REF)" --logo lib/elixir/pages/images/logo.png --output doc/$(2) --canonical "https://hexdocs.pm/$(2)/$(CANONICAL)" --homepage-url "https://elixir-lang.org/docs.html" $(DOCS_OPTIONS) $(4)
|
||||
DOCS_COMPILE = CANONICAL=$(CANONICAL) bin/elixir ../ex_doc/bin/ex_doc "$(1)" "$(VERSION)" "lib/$(2)/ebin" --main "$(3)" --source-url "https://github.com/elixir-lang/elixir" --source-ref "$(call SOURCE_REF)" --logo lib/elixir/pages/images/logo.png --output doc/$(2) --canonical "https://$(2).hexdocs.pm/$(CANONICAL)" --homepage-url "https://elixir-lang.org/docs.html" $(DOCS_OPTIONS) $(4)
|
||||
DOCS_CONFIG = bin/elixir lib/elixir/scripts/docs_config.exs "$(1)"
|
||||
|
||||
docs: compile ../ex_doc/bin/ex_doc docs_elixir docs_eex docs_mix docs_iex docs_ex_unit docs_logger
|
||||
|
||||
+3
-3
@@ -12,12 +12,12 @@ Elixir applies bug fixes only to the latest minor branch. Security patches are a
|
||||
|
||||
Elixir version | Support
|
||||
:------------- | :-----------------------------
|
||||
1.20 | Development
|
||||
1.19 | Bug fixes and security patches
|
||||
1.21 | Development
|
||||
1.20 | Bug fixes and security patches
|
||||
1.19 | Security patches only
|
||||
1.18 | Security patches only
|
||||
1.17 | Security patches only
|
||||
1.16 | Security patches only
|
||||
1.15 | Security patches only
|
||||
|
||||
## Announcements
|
||||
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
|
||||
set -e
|
||||
|
||||
ELIXIR_VERSION=1.20.0-rc.5
|
||||
ELIXIR_VERSION=1.21.0-dev
|
||||
|
||||
if [ $# -eq 0 ] || { [ $# -eq 1 ] && { [ "$1" = "--help" ] || [ "$1" = "-h" ]; }; }; then
|
||||
cat <<USAGE >&2
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
:: SPDX-FileCopyrightText: 2021 The Elixir Team
|
||||
:: SPDX-FileCopyrightText: 2012 Plataformatec
|
||||
|
||||
set ELIXIR_VERSION=1.20.0-rc.5
|
||||
set ELIXIR_VERSION=1.21.0-dev
|
||||
|
||||
if ""%1""=="""" if ""%2""=="""" goto documentation
|
||||
if /I ""%1""==""--help"" if ""%2""=="""" goto documentation
|
||||
|
||||
+40
-13
@@ -79,7 +79,7 @@ defmodule EEx.Compiler do
|
||||
{:error, message <> code_snippet(state.source, state.indentation, meta), meta}
|
||||
|
||||
{:ok, expr, new_line, new_column, rest} ->
|
||||
{key, expr} =
|
||||
{key, expr, extra_meta} =
|
||||
case :elixir_tokenizer.tokenize(expr, 1, file: "eex", check_terminators: false) do
|
||||
{:ok, _line, _column, _warnings, rev_tokens, []} ->
|
||||
# We ignore warnings because the code will be tokenized
|
||||
@@ -87,7 +87,7 @@ defmodule EEx.Compiler do
|
||||
token_key(rev_tokens, expr)
|
||||
|
||||
{:error, _, _, _, _} ->
|
||||
{:expr, expr}
|
||||
{:expr, expr, %{}}
|
||||
end
|
||||
|
||||
marker =
|
||||
@@ -102,8 +102,8 @@ defmodule EEx.Compiler do
|
||||
marker
|
||||
end
|
||||
|
||||
token = {key, marker, expr, %{line: line, column: column}}
|
||||
trim_and_tokenize(rest, new_line, new_column, state, buffer, acc, &[token | &1])
|
||||
token = {key, marker, expr, Map.merge(%{line: line, column: column}, extra_meta)}
|
||||
trim_and_tokenize(rest, new_line, new_column, state, buffer, acc, &merge_token(token, &1))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -127,6 +127,27 @@ defmodule EEx.Compiler do
|
||||
tokenize(rest, line, column, state, [{line, column}], fun.(acc))
|
||||
end
|
||||
|
||||
# Merge middle expressions separated only by whitespace so the whitespace is
|
||||
# part of the Elixir expression, not a separate EEx body.
|
||||
defp merge_token(
|
||||
{:middle_expr, ~c"", chars, meta},
|
||||
[{:text, text, text_meta}, {:middle_expr, ~c"", prev_chars, prev_meta} | acc]
|
||||
) do
|
||||
if only_spaces?(text) and clause_block_identifier?(prev_meta) do
|
||||
[{:middle_expr, ~c"", prev_chars ++ text ++ chars, prev_meta} | acc]
|
||||
else
|
||||
[
|
||||
{:middle_expr, ~c"", chars, meta},
|
||||
{:text, text, text_meta},
|
||||
{:middle_expr, ~c"", prev_chars, prev_meta} | acc
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
defp merge_token(token, acc) do
|
||||
[token | acc]
|
||||
end
|
||||
|
||||
# Retrieve marker for <%
|
||||
|
||||
defp retrieve_marker([marker | t]) when marker in [?=, ?/, ?|] do
|
||||
@@ -177,31 +198,31 @@ defmodule EEx.Compiler do
|
||||
defp token_key(rev_tokens, expr) do
|
||||
case {Enum.reverse(rev_tokens), drop_eol(rev_tokens)} do
|
||||
{[{:end, _} | _], [{:do, _} | _]} ->
|
||||
{:middle_expr, expr}
|
||||
{:middle_expr, expr, %{}}
|
||||
|
||||
{_, [{:do, _} | _]} ->
|
||||
{:start_expr, maybe_append_space(expr)}
|
||||
{:start_expr, maybe_append_space(expr), %{}}
|
||||
|
||||
{_, [{:block_identifier, _, _} | _]} ->
|
||||
{:middle_expr, maybe_append_space(expr)}
|
||||
{_, [{:block_identifier, _, identifier} | _]} ->
|
||||
{:middle_expr, maybe_append_space(expr), %{block_identifier: identifier}}
|
||||
|
||||
{[{:end, _} | _], [{:stab_op, _, _} | _]} ->
|
||||
{:middle_expr, expr}
|
||||
{:middle_expr, expr, %{}}
|
||||
|
||||
{_, [{: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 end_index > fn_index do
|
||||
{:start_expr, expr}
|
||||
{:start_expr, expr, %{}}
|
||||
else
|
||||
{:middle_expr, expr}
|
||||
{:middle_expr, expr, %{}}
|
||||
end
|
||||
|
||||
{tokens, _} ->
|
||||
case Enum.drop_while(tokens, &closing_bracket?/1) do
|
||||
[{:end, _} | _] -> {:end_expr, expr}
|
||||
_ -> {:expr, expr}
|
||||
[{:end, _} | _] -> {:end_expr, expr, %{}}
|
||||
_ -> {:expr, expr, %{}}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -476,6 +497,12 @@ defmodule EEx.Compiler do
|
||||
Enum.all?(chars, &(&1 in @all_spaces))
|
||||
end
|
||||
|
||||
defp clause_block_identifier?(%{block_identifier: identifier}) do
|
||||
identifier in [:else, :rescue, :catch]
|
||||
end
|
||||
|
||||
defp clause_block_identifier?(_meta), do: false
|
||||
|
||||
# Changes placeholder to real expression
|
||||
|
||||
defp insert_quoted({:__EEX__, _, [key]}, quoted) do
|
||||
|
||||
@@ -270,7 +270,7 @@ defmodule EEx.TokenizerTest do
|
||||
{:text, ~c"foo ", %{column: 1, line: 1}},
|
||||
{:start_expr, ~c"", ~c" if true do ", %{column: 5, line: 1}},
|
||||
{:text, ~c"bar", %{column: 21, line: 1}},
|
||||
{:middle_expr, ~c"", ~c" else ", %{column: 24, line: 1}},
|
||||
{:middle_expr, ~c"", ~c" else ", %{block_identifier: :else, column: 24, line: 1}},
|
||||
{:text, ~c"baz", %{column: 34, line: 1}},
|
||||
{:end_expr, ~c"", ~c" end ", %{column: 37, line: 1}},
|
||||
{:eof, %{column: 46, line: 1}}
|
||||
@@ -286,7 +286,7 @@ defmodule EEx.TokenizerTest do
|
||||
exprs = [
|
||||
{:start_expr, ~c"=", ~c" if true do ", %{column: 2, line: 1}},
|
||||
{:text, ~c"\n TRUE \n", %{column: 20, line: 1}},
|
||||
{:middle_expr, ~c"", ~c" else ", %{column: 3, line: 3}},
|
||||
{:middle_expr, ~c"", ~c" else ", %{block_identifier: :else, column: 3, line: 3}},
|
||||
{:text, ~c"\n FALSE \n", %{column: 13, line: 3}},
|
||||
{:end_expr, ~c"", ~c" end ", %{column: 3, line: 5}},
|
||||
{:eof, %{column: 3, line: 7}}
|
||||
|
||||
@@ -262,6 +262,37 @@ defmodule EExTest do
|
||||
assert_eval("foo 1,2,3", "foo <% require Enum, as: E %><%= E.join [1, 2, 3], \",\" %>")
|
||||
end
|
||||
|
||||
test "with expression with else clause split across tags" do
|
||||
template = """
|
||||
<%= with {:ok, x} <- @res do %>
|
||||
<p><%= x %></p>
|
||||
<% else %>
|
||||
<% _ -> %>
|
||||
<p>bad</p>
|
||||
<% end %>
|
||||
"""
|
||||
|
||||
assert_eval("\n <p>ok</p>\n\n", template, [assigns: [res: {:ok, "ok"}]],
|
||||
engine: EEx.SmartEngine
|
||||
)
|
||||
|
||||
assert_eval("\n <p>bad</p>\n\n", template, [assigns: [res: :error]],
|
||||
engine: EEx.SmartEngine
|
||||
)
|
||||
end
|
||||
|
||||
test "empty clauses separated by whitespace" do
|
||||
template = """
|
||||
<%= case x do %>
|
||||
<% :foo -> %>
|
||||
<% :bar -> %>
|
||||
<% end %>
|
||||
"""
|
||||
|
||||
assert_eval("\n \n", template, x: :foo)
|
||||
assert_eval("\n\n", template, x: :bar)
|
||||
end
|
||||
|
||||
test "with end of token" do
|
||||
assert_eval("foo bar %>", "foo bar %>")
|
||||
end
|
||||
@@ -522,7 +553,7 @@ defmodule EExTest do
|
||||
|
||||
assert message |> Exception.message() |> strip_ansi() =~ """
|
||||
│
|
||||
514 │ true && @some[\s
|
||||
#{line + 2} │ true && @some[\s
|
||||
│ │ └ missing closing delimiter (expected "]")
|
||||
│ └ unclosed delimiter
|
||||
"""
|
||||
|
||||
+68
-14
@@ -226,6 +226,8 @@ defmodule Access do
|
||||
end
|
||||
end
|
||||
|
||||
defguardp is_probably_keyword(list) when list == [] or is_atom(elem(hd(list), 0))
|
||||
|
||||
@doc """
|
||||
Fetches the value for the given key in a container (a map, keyword
|
||||
list, or struct that implements the `Access` behaviour).
|
||||
@@ -485,7 +487,7 @@ defmodule Access do
|
||||
## Accessors
|
||||
|
||||
@doc """
|
||||
Returns a function that accesses the given key in a map/struct.
|
||||
Returns a function that accesses the given key in a map/struct/keyword list.
|
||||
|
||||
The returned function is typically passed as an accessor to `Kernel.get_in/2`,
|
||||
`Kernel.get_and_update_in/3`, and friends.
|
||||
@@ -514,30 +516,56 @@ defmodule Access do
|
||||
iex> pop_in(map, [Access.key(:user), Access.key(:name)])
|
||||
{"john", %{user: %{}}}
|
||||
|
||||
An error is raised if the accessed structure is not a map or a struct:
|
||||
iex> keyword = [user: [name: "john"]]
|
||||
iex> get_in(keyword, [Access.key(:unknown, []), Access.key(:name, "john")])
|
||||
"john"
|
||||
iex> get_and_update_in(keyword, [Access.key(:user), Access.key(:name)], fn prev ->
|
||||
...> {prev, String.upcase(prev)}
|
||||
...> end)
|
||||
{"john", [user: [name: "JOHN"]]}
|
||||
iex> pop_in(keyword, [Access.key(:user), Access.key(:name)])
|
||||
{"john", [user: []]}
|
||||
|
||||
iex> get_in([], [Access.key(:foo)])
|
||||
** (BadMapError) expected a map, got:
|
||||
...
|
||||
An error is raised if the accessed structure is not a map, struct, or keyword list:
|
||||
|
||||
iex> get_in(123, [Access.key(:foo)])
|
||||
** (RuntimeError) Access.key/2 expected a map/struct/keyword list, got: ...
|
||||
|
||||
iex> put_in([1, 2, 3], [Access.key(:foo)], :bar)
|
||||
** (RuntimeError) Access.key/2 expected a map/struct/keyword list, got: ...
|
||||
"""
|
||||
@spec key(key, term) :: access_fun(data :: struct | map, current_value :: term)
|
||||
@spec key(key, term) :: access_fun(data :: struct | map | keyword, current_value :: term)
|
||||
def key(key, default \\ nil) do
|
||||
fn
|
||||
:get, data, next ->
|
||||
:get, %{} = data, next ->
|
||||
next.(Map.get(data, key, default))
|
||||
|
||||
:get_and_update, data, next ->
|
||||
:get_and_update, %{} = data, next ->
|
||||
value = Map.get(data, key, default)
|
||||
|
||||
case next.(value) do
|
||||
{get, update} -> {get, Map.put(data, key, update)}
|
||||
:pop -> {value, Map.delete(data, key)}
|
||||
end
|
||||
|
||||
:get, data, next when is_probably_keyword(data) ->
|
||||
next.(Keyword.get(data, key, default))
|
||||
|
||||
:get_and_update, data, next when is_probably_keyword(data) ->
|
||||
value = Keyword.get(data, key, default)
|
||||
|
||||
case next.(value) do
|
||||
{get, update} -> {get, Keyword.put(data, key, update)}
|
||||
:pop -> {value, Keyword.delete(data, key)}
|
||||
end
|
||||
|
||||
_op, data, _next ->
|
||||
raise "Access.key/2 expected a map/struct/keyword list, got: #{inspect(data)}"
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns a function that accesses the given key in a map/struct.
|
||||
Returns a function that accesses the given key in a map/struct/keyword list.
|
||||
|
||||
The returned function is typically passed as an accessor to `Kernel.get_in/2`,
|
||||
`Kernel.get_and_update_in/3`, and friends.
|
||||
@@ -546,6 +574,19 @@ defmodule Access do
|
||||
|
||||
## Examples
|
||||
|
||||
iex> keyword = [user: [name: "john"]]
|
||||
iex> get_in(keyword, [Access.key!(:user), Access.key!(:name)])
|
||||
"john"
|
||||
iex> get_and_update_in(keyword, [Access.key!(:user), Access.key!(:name)], fn prev ->
|
||||
...> {prev, String.upcase(prev)}
|
||||
...> end)
|
||||
{"john", [user: [name: "JOHN"]]}
|
||||
iex> pop_in(keyword, [Access.key!(:user), Access.key!(:name)])
|
||||
{"john", [user: []]}
|
||||
iex> get_in(keyword, [Access.key!(:user), Access.key!(:unknown)])
|
||||
** (KeyError) key :unknown not found in:
|
||||
...
|
||||
|
||||
iex> map = %{user: %{name: "john"}}
|
||||
iex> get_in(map, [Access.key!(:user), Access.key!(:name)])
|
||||
"john"
|
||||
@@ -574,13 +615,15 @@ defmodule Access do
|
||||
`Access.key!/1` is useful when the key is not known in advance
|
||||
and must be accessed dynamically.
|
||||
|
||||
An error is raised if the accessed structure is not a map/struct:
|
||||
An error is raised if the accessed structure is not a map/struct/keyword list:
|
||||
|
||||
iex> get_in([], [Access.key!(:foo)])
|
||||
** (RuntimeError) Access.key!/1 expected a map/struct, got: []
|
||||
iex> get_in(123, [Access.key!(:foo)])
|
||||
** (RuntimeError) Access.key!/1 expected a map/struct/keyword list, got: 123
|
||||
|
||||
iex> put_in([1, 2, 3], [Access.key!(:foo)], :bar)
|
||||
** (RuntimeError) Access.key!/1 expected a map/struct/keyword list, got: ...
|
||||
"""
|
||||
@spec key!(key) :: access_fun(data :: struct | map, current_value :: term)
|
||||
@spec key!(key) :: access_fun(data :: struct | map | keyword, current_value :: term)
|
||||
def key!(key) do
|
||||
fn
|
||||
:get, %{} = data, next ->
|
||||
@@ -594,8 +637,19 @@ defmodule Access do
|
||||
:pop -> {value, Map.delete(data, key)}
|
||||
end
|
||||
|
||||
:get, data, next when is_probably_keyword(data) ->
|
||||
next.(Keyword.fetch!(data, key))
|
||||
|
||||
:get_and_update, data, next when is_probably_keyword(data) ->
|
||||
value = Keyword.fetch!(data, key)
|
||||
|
||||
case next.(value) do
|
||||
{get, update} -> {get, Keyword.put(data, key, update)}
|
||||
:pop -> {value, Keyword.delete(data, key)}
|
||||
end
|
||||
|
||||
_op, data, _next ->
|
||||
raise "Access.key!/1 expected a map/struct, got: #{inspect(data)}"
|
||||
raise "Access.key!/1 expected a map/struct/keyword list, got: #{inspect(data)}"
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
# SPDX-FileCopyrightText: 2012 Plataformatec
|
||||
|
||||
defmodule Calendar do
|
||||
@strftime_max_width 1024
|
||||
|
||||
@moduledoc """
|
||||
This module defines the responsibilities for working with
|
||||
calendars, dates, times and datetimes in Elixir.
|
||||
@@ -529,6 +531,7 @@ defmodule Calendar do
|
||||
* `%`: indicates the start of a formatted section
|
||||
* `<padding>`: set the padding (see below)
|
||||
* `<width>`: a number indicating the minimum size of the formatted section
|
||||
(maximum #{@strftime_max_width})
|
||||
* `<format>`: the format itself (see below)
|
||||
|
||||
### Accepted padding options
|
||||
@@ -667,9 +670,13 @@ defmodule Calendar do
|
||||
end
|
||||
|
||||
defp parse_modifiers(<<digit, rest::binary>>, width, pad, parser_data) when digit in ?0..?9 do
|
||||
new_width = (width || 0) * 10 + (digit - ?0)
|
||||
width = (width || 0) * 10 + (digit - ?0)
|
||||
|
||||
parse_modifiers(rest, new_width, pad, parser_data)
|
||||
if width > @strftime_max_width do
|
||||
raise ArgumentError, "invalid strftime format: width must be at most #{@strftime_max_width}"
|
||||
end
|
||||
|
||||
parse_modifiers(rest, width, pad, parser_data)
|
||||
end
|
||||
|
||||
# set default padding if none was specified
|
||||
|
||||
@@ -243,19 +243,19 @@ defmodule Duration do
|
||||
|
||||
"""
|
||||
@spec add(t, t) :: t
|
||||
def add(%Duration{} = d1, %Duration{} = d2) do
|
||||
{m1, p1} = d1.microsecond
|
||||
{m2, p2} = d2.microsecond
|
||||
def add(%Duration{microsecond: {ms1, p1}} = d1, %Duration{microsecond: {ms2, p2}} = d2) do
|
||||
%{year: y1, month: mo1, week: w1, day: day1, hour: h1, minute: mi1, second: s1} = d1
|
||||
%{year: y2, month: mo2, week: w2, day: day2, hour: h2, minute: mi2, second: s2} = d2
|
||||
|
||||
%Duration{
|
||||
year: d1.year + d2.year,
|
||||
month: d1.month + d2.month,
|
||||
week: d1.week + d2.week,
|
||||
day: d1.day + d2.day,
|
||||
hour: d1.hour + d2.hour,
|
||||
minute: d1.minute + d2.minute,
|
||||
second: d1.second + d2.second,
|
||||
microsecond: {m1 + m2, max(p1, p2)}
|
||||
year: y1 + y2,
|
||||
month: mo1 + mo2,
|
||||
week: w1 + w2,
|
||||
day: day1 + day2,
|
||||
hour: h1 + h2,
|
||||
minute: mi1 + mi2,
|
||||
second: s1 + s2,
|
||||
microsecond: {ms1 + ms2, max(p1, p2)}
|
||||
}
|
||||
end
|
||||
|
||||
@@ -273,19 +273,19 @@ defmodule Duration do
|
||||
|
||||
"""
|
||||
@spec subtract(t, t) :: t
|
||||
def subtract(%Duration{} = d1, %Duration{} = d2) do
|
||||
{m1, p1} = d1.microsecond
|
||||
{m2, p2} = d2.microsecond
|
||||
def subtract(%Duration{microsecond: {ms1, p1}} = d1, %Duration{microsecond: {ms2, p2}} = d2) do
|
||||
%{year: y1, month: mo1, week: w1, day: day1, hour: h1, minute: mi1, second: s1} = d1
|
||||
%{year: y2, month: mo2, week: w2, day: day2, hour: h2, minute: mi2, second: s2} = d2
|
||||
|
||||
%Duration{
|
||||
year: d1.year - d2.year,
|
||||
month: d1.month - d2.month,
|
||||
week: d1.week - d2.week,
|
||||
day: d1.day - d2.day,
|
||||
hour: d1.hour - d2.hour,
|
||||
minute: d1.minute - d2.minute,
|
||||
second: d1.second - d2.second,
|
||||
microsecond: {m1 - m2, max(p1, p2)}
|
||||
year: y1 - y2,
|
||||
month: mo1 - mo2,
|
||||
week: w1 - w2,
|
||||
day: day1 - day2,
|
||||
hour: h1 - h2,
|
||||
minute: mi1 - mi2,
|
||||
second: s1 - s2,
|
||||
microsecond: {ms1 - ms2, max(p1, p2)}
|
||||
}
|
||||
end
|
||||
|
||||
@@ -302,14 +302,16 @@ defmodule Duration do
|
||||
"""
|
||||
@spec multiply(t, integer) :: t
|
||||
def multiply(%Duration{microsecond: {ms, p}} = duration, integer) when is_integer(integer) do
|
||||
%{year: y, month: mo, week: w, day: d, hour: h, minute: mi, second: s} = duration
|
||||
|
||||
%Duration{
|
||||
year: duration.year * integer,
|
||||
month: duration.month * integer,
|
||||
week: duration.week * integer,
|
||||
day: duration.day * integer,
|
||||
hour: duration.hour * integer,
|
||||
minute: duration.minute * integer,
|
||||
second: duration.second * integer,
|
||||
year: y * integer,
|
||||
month: mo * integer,
|
||||
week: w * integer,
|
||||
day: d * integer,
|
||||
hour: h * integer,
|
||||
minute: mi * integer,
|
||||
second: s * integer,
|
||||
microsecond: {ms * integer, p}
|
||||
}
|
||||
end
|
||||
@@ -327,14 +329,16 @@ defmodule Duration do
|
||||
"""
|
||||
@spec negate(t) :: t
|
||||
def negate(%Duration{microsecond: {ms, p}} = duration) do
|
||||
%{year: y, month: mo, week: w, day: d, hour: h, minute: mi, second: s} = duration
|
||||
|
||||
%Duration{
|
||||
year: -duration.year,
|
||||
month: -duration.month,
|
||||
week: -duration.week,
|
||||
day: -duration.day,
|
||||
hour: -duration.hour,
|
||||
minute: -duration.minute,
|
||||
second: -duration.second,
|
||||
year: -y,
|
||||
month: -mo,
|
||||
week: -w,
|
||||
day: -d,
|
||||
hour: -h,
|
||||
minute: -mi,
|
||||
second: -s,
|
||||
microsecond: {-ms, p}
|
||||
}
|
||||
end
|
||||
|
||||
+17
-7
@@ -314,7 +314,7 @@ defmodule Code do
|
||||
:relative_paths
|
||||
]
|
||||
|
||||
@list_compiler_options [:tracers, :parser_options]
|
||||
@list_compiler_options [:tracers, :parser_options, :erlc_options]
|
||||
|
||||
@available_compiler_options @boolean_compiler_options ++
|
||||
@list_compiler_options ++
|
||||
@@ -1634,13 +1634,19 @@ defmodule Code do
|
||||
nil
|
||||
|
||||
:proceed ->
|
||||
loaded =
|
||||
Module.ParallelChecker.verify(fn ->
|
||||
:elixir_compiler.string(charlist, file, fn _, _ -> :ok end)
|
||||
end)
|
||||
try do
|
||||
loaded =
|
||||
Module.ParallelChecker.verify(fn ->
|
||||
:elixir_compiler.string(charlist, file, fn _, _ -> :ok end)
|
||||
end)
|
||||
|
||||
:elixir_code_server.cast({:required, file})
|
||||
loaded
|
||||
:elixir_code_server.cast({:required, file})
|
||||
loaded
|
||||
catch
|
||||
kind, reason ->
|
||||
:elixir_code_server.call({:release, file})
|
||||
:erlang.raise(kind, reason, __STACKTRACE__)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1753,6 +1759,10 @@ defmodule Code do
|
||||
* `:docs` - when `true`, retains documentation in the compiled module.
|
||||
Defaults to `true`.
|
||||
|
||||
* `:erlc_options` (since v1.21.0) - a list of Erlang compiler options. For example,
|
||||
`erlc_options: [:beam_debug_info, :beam_debug_stack]` emits Erlang/OTP
|
||||
debug metadata for BEAM debuggers. Defaults to `[]`.
|
||||
|
||||
* `:ignore_already_consolidated` (since v1.10.0) - when `true`, does not warn
|
||||
when a protocol has already been consolidated and a new implementation is added.
|
||||
Defaults to `false`.
|
||||
|
||||
@@ -1457,7 +1457,7 @@ defmodule Code.Formatter do
|
||||
metadata = [
|
||||
file: state.file,
|
||||
line: meta[:line],
|
||||
sigil: String.to_atom(name),
|
||||
sigil: String.to_unsafe_atom(name),
|
||||
modifiers: modifiers,
|
||||
opening_delimiter: opening_delimiter
|
||||
]
|
||||
|
||||
@@ -204,6 +204,7 @@ defmodule Code.Fragment do
|
||||
| {:local_arity, charlist}
|
||||
| {:local_call, charlist}
|
||||
| {:anonymous_call, inside_caller}
|
||||
| {:capture_arg, charlist}
|
||||
| {:module_attribute, charlist}
|
||||
| {:operator, charlist}
|
||||
| {:operator_arity, charlist}
|
||||
|
||||
@@ -65,7 +65,7 @@ defmodule Code.Identifier do
|
||||
with "-" <> rest <- Atom.to_string(atom),
|
||||
[trailing | reversed] = rest |> String.split("/") |> Enum.reverse(),
|
||||
[arity, _inner, _count, ""] <- String.split(trailing, "-") do
|
||||
{reversed |> Enum.reverse() |> Enum.join("/") |> String.to_atom(), arity}
|
||||
{reversed |> Enum.reverse() |> Enum.join("/") |> String.to_unsafe_atom(), arity}
|
||||
else
|
||||
_ -> :error
|
||||
end
|
||||
|
||||
@@ -264,7 +264,7 @@ defmodule Code.Normalizer do
|
||||
"Elixir." <> segments ->
|
||||
segments
|
||||
|> String.split(".")
|
||||
|> Enum.map(&String.to_atom/1)
|
||||
|> Enum.map(&String.to_unsafe_atom/1)
|
||||
end
|
||||
|
||||
{:__aliases__, meta, segments}
|
||||
@@ -559,7 +559,7 @@ defmodule Code.Normalizer do
|
||||
atom
|
||||
|> Atom.to_string()
|
||||
|> maybe_escape_literal(state)
|
||||
|> String.to_atom()
|
||||
|> String.to_unsafe_atom()
|
||||
end
|
||||
|
||||
defp maybe_escape_literal(term, _) do
|
||||
|
||||
@@ -399,10 +399,10 @@ defmodule Code.Typespec do
|
||||
defp erl_to_ex_var(var) do
|
||||
case Atom.to_string(var) do
|
||||
<<"_", c::utf8, rest::binary>> ->
|
||||
String.to_atom("_#{String.downcase(<<c::utf8>>)}#{rest}")
|
||||
String.to_unsafe_atom("_#{String.downcase(<<c::utf8>>)}#{rest}")
|
||||
|
||||
<<c::utf8, rest::binary>> ->
|
||||
String.to_atom("#{String.downcase(<<c::utf8>>)}#{rest}")
|
||||
String.to_unsafe_atom("#{String.downcase(<<c::utf8>>)}#{rest}")
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -1442,7 +1442,7 @@ defmodule Enum do
|
||||
)
|
||||
|
||||
# Avoid warnings about Dict
|
||||
dict_module = String.to_atom("Dict")
|
||||
dict_module = String.to_unsafe_atom("Dict")
|
||||
|
||||
reduce(reverse(enumerable), dict, fn entry, categories ->
|
||||
dict_module.update(categories, fun.(entry), [entry], &[entry | &1])
|
||||
|
||||
@@ -1046,7 +1046,7 @@ defmodule RuntimeError do
|
||||
iex> raise "oops!"
|
||||
** (RuntimeError) oops!
|
||||
|
||||
You should use this exceptions sparingly, since most of the time it might be
|
||||
You should use this exception sparingly, since most of the time it might be
|
||||
better to define your own exceptions specific to your application or library.
|
||||
Sometimes, however, there are situations in which you don't expect a condition to
|
||||
happen, but you want to give a meaningful error message if it does. In those cases,
|
||||
@@ -1137,7 +1137,7 @@ defmodule SystemLimitError do
|
||||
|
||||
For example, this can happen if you try to create an atom that is too large:
|
||||
|
||||
iex> String.to_atom(String.duplicate("a", 100_000))
|
||||
iex> String.to_unsafe_atom(String.duplicate("a", 100_000))
|
||||
** (SystemLimitError) a system limit has been reached
|
||||
"""
|
||||
|
||||
@@ -1807,7 +1807,7 @@ defmodule UndefinedFunctionError do
|
||||
|
||||
defp load_module({name, _path, _loaded?}) do
|
||||
name
|
||||
|> List.to_atom()
|
||||
|> List.to_unsafe_atom()
|
||||
|> Code.ensure_loaded()
|
||||
end
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ defmodule GenEvent do
|
||||
alternative. GenStage is an external Elixir library maintained by the Elixir
|
||||
team; it provides a tool to implement systems that exchange events in a
|
||||
demand-driven way with built-in support for back-pressure. See the [GenStage
|
||||
documentation](https://hexdocs.pm/gen_stage) for more information.
|
||||
documentation](https://gen-stage.hexdocs.pm) for more information.
|
||||
|
||||
### `:gen_event`
|
||||
|
||||
|
||||
@@ -265,13 +265,13 @@ defimpl Enumerable, for: HashDict do
|
||||
|
||||
def reduce(dict, acc, fun) do
|
||||
# Avoid warnings about HashDict being deprecated.
|
||||
module = String.to_atom("HashDict")
|
||||
module = String.to_unsafe_atom("HashDict")
|
||||
module.reduce(dict, acc, fun)
|
||||
end
|
||||
|
||||
def member?(dict, {key, value}) do
|
||||
# Avoid warnings about HashDict being deprecated.
|
||||
module = String.to_atom("HashDict")
|
||||
module = String.to_unsafe_atom("HashDict")
|
||||
{:ok, match?({:ok, ^value}, module.fetch(dict, key))}
|
||||
end
|
||||
|
||||
@@ -281,7 +281,7 @@ defimpl Enumerable, for: HashDict do
|
||||
|
||||
def count(dict) do
|
||||
# Avoid warnings about HashDict being deprecated.
|
||||
module = String.to_atom("HashDict")
|
||||
module = String.to_unsafe_atom("HashDict")
|
||||
{:ok, module.size(dict)}
|
||||
end
|
||||
|
||||
@@ -296,7 +296,7 @@ defimpl Collectable, for: HashDict do
|
||||
|
||||
def into(original) do
|
||||
# Avoid warnings about HashDict being deprecated.
|
||||
module = String.to_atom("HashDict")
|
||||
module = String.to_unsafe_atom("HashDict")
|
||||
|
||||
collector_fun = fn
|
||||
dict, {:cont, {key, value}} -> module.put(dict, key, value)
|
||||
@@ -315,7 +315,7 @@ defimpl Inspect, for: HashDict do
|
||||
|
||||
def inspect(dict, opts) do
|
||||
# Avoid warnings about HashDict being deprecated.
|
||||
module = String.to_atom("HashDict")
|
||||
module = String.to_unsafe_atom("HashDict")
|
||||
concat(["#HashDict<", Inspect.List.inspect(module.to_list(dict), opts), ">"])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -279,19 +279,19 @@ defimpl Enumerable, for: HashSet do
|
||||
|
||||
def reduce(set, acc, fun) do
|
||||
# Avoid warnings about HashSet being deprecated.
|
||||
module = String.to_atom("HashSet")
|
||||
module = String.to_unsafe_atom("HashSet")
|
||||
module.reduce(set, acc, fun)
|
||||
end
|
||||
|
||||
def member?(set, term) do
|
||||
# Avoid warnings about HashSet being deprecated.
|
||||
module = String.to_atom("HashSet")
|
||||
module = String.to_unsafe_atom("HashSet")
|
||||
{:ok, module.member?(set, term)}
|
||||
end
|
||||
|
||||
def count(set) do
|
||||
# Avoid warnings about HashSet being deprecated.
|
||||
module = String.to_atom("HashSet")
|
||||
module = String.to_unsafe_atom("HashSet")
|
||||
{:ok, module.size(set)}
|
||||
end
|
||||
|
||||
@@ -306,7 +306,7 @@ defimpl Collectable, for: HashSet do
|
||||
|
||||
def into(original) do
|
||||
# Avoid warnings about HashSet being deprecated.
|
||||
module = String.to_atom("HashSet")
|
||||
module = String.to_unsafe_atom("HashSet")
|
||||
|
||||
collector_fun = fn
|
||||
set, {:cont, term} -> module.put(set, term)
|
||||
@@ -325,7 +325,7 @@ defimpl Inspect, for: HashSet do
|
||||
|
||||
def inspect(set, opts) do
|
||||
# Avoid warnings about HashSet being deprecated.
|
||||
module = String.to_atom("HashSet")
|
||||
module = String.to_unsafe_atom("HashSet")
|
||||
concat(["#HashSet<", Inspect.List.inspect(module.to_list(set), opts), ">"])
|
||||
end
|
||||
end
|
||||
|
||||
@@ -332,7 +332,7 @@ defmodule IO.ANSI do
|
||||
end
|
||||
|
||||
defp do_format([], [], acc, true, true) do
|
||||
[acc | IO.ANSI.reset()]
|
||||
[acc | reset()]
|
||||
end
|
||||
|
||||
defp do_format([], [], acc, _emit?, _append_reset) do
|
||||
|
||||
@@ -24,7 +24,7 @@ defprotocol JSON.Encoder do
|
||||
> #### Leaking Private Information {: .error}
|
||||
>
|
||||
> Prefer using `:only` to avoid accidentally leaking private information when
|
||||
> new fields are added. Other approaches should be used with auction.
|
||||
> new fields are added. Other approaches should be used with caution.
|
||||
|
||||
You can also use `Protocol.derive/3` if you don't own the struct that you want
|
||||
to encode to JSON:
|
||||
|
||||
+10
-12
@@ -2109,7 +2109,7 @@ defmodule Kernel do
|
||||
assert_no_match_or_guard_scope(__CALLER__.context, "!")
|
||||
|
||||
annotate_case(
|
||||
[optimize_boolean: true, type_check: {:case, :!}],
|
||||
[optimize_boolean: true, type_check: {:case, :"!!"}],
|
||||
quote do
|
||||
case unquote(value) do
|
||||
x when unquote(x_is_false_or_nil()) -> false
|
||||
@@ -4703,10 +4703,8 @@ defmodule Kernel do
|
||||
false
|
||||
|
||||
[] ->
|
||||
quote do
|
||||
_ = unquote(left)
|
||||
false
|
||||
end
|
||||
# inlined as false in erlang pass
|
||||
quote(do: :lists.member(unquote(left), []))
|
||||
|
||||
[head | tail] = list ->
|
||||
case in_body? do
|
||||
@@ -5250,12 +5248,12 @@ defmodule Kernel do
|
||||
# defmodule Alias nested
|
||||
defp alias_defmodule({:__aliases__, _, [h | t]}, _module, env) when is_atom(h) do
|
||||
module = :elixir_aliases.concat([env.module, h])
|
||||
alias = String.to_atom("Elixir." <> Atom.to_string(h))
|
||||
alias = String.to_unsafe_atom("Elixir." <> Atom.to_string(h))
|
||||
opts = [as: alias, warn: false]
|
||||
|
||||
case t do
|
||||
[] -> {module, module, opts}
|
||||
_ -> {String.to_atom(Enum.join([module | t], ".")), module, opts}
|
||||
_ -> {String.to_unsafe_atom(Enum.join([module | t], ".")), module, opts}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6280,7 +6278,7 @@ defmodule Kernel do
|
||||
step through the code it sees). For general stepping, you can set breakpoints
|
||||
using `IEx.break!/4`.
|
||||
|
||||
For more information, [see IEx documentation](https://hexdocs.pm/iex/IEx.html#module-dbg-and-breakpoints).
|
||||
For more information, [see IEx documentation](https://iex.hexdocs.pm/IEx.html#module-dbg-and-breakpoints).
|
||||
|
||||
## Configuring the debug function
|
||||
|
||||
@@ -6899,7 +6897,7 @@ defmodule Kernel do
|
||||
defp maybe_atomize_calendar(<<alias, _::binary>> = last_part, string)
|
||||
when alias >= ?A and alias <= ?Z do
|
||||
string = binary_part(string, 0, byte_size(string) - byte_size(last_part) - 1)
|
||||
{String.to_atom("Elixir." <> last_part), string}
|
||||
{String.to_unsafe_atom("Elixir." <> last_part), string}
|
||||
end
|
||||
|
||||
defp maybe_atomize_calendar(_last_part, string) do
|
||||
@@ -7010,7 +7008,7 @@ defmodule Kernel do
|
||||
|
||||
case mod do
|
||||
?s -> parts
|
||||
?a -> :lists.map(&String.to_atom/1, parts)
|
||||
?a -> :lists.map(&String.to_unsafe_atom/1, parts)
|
||||
?c -> :lists.map(&String.to_charlist/1, parts)
|
||||
end
|
||||
|
||||
@@ -7019,7 +7017,7 @@ defmodule Kernel do
|
||||
|
||||
case mod do
|
||||
?s -> parts
|
||||
?a -> quote(do: :lists.map(&String.to_atom/1, unquote(parts)))
|
||||
?a -> quote(do: :lists.map(&String.to_unsafe_atom/1, unquote(parts)))
|
||||
?c -> quote(do: :lists.map(&String.to_charlist/1, unquote(parts)))
|
||||
end
|
||||
end
|
||||
@@ -7053,7 +7051,7 @@ defmodule Kernel do
|
||||
:guard ->
|
||||
raise ArgumentError,
|
||||
"invalid expression in guard, #{exp} is not allowed in guards. " <>
|
||||
"To learn more about guards, visit: https://hexdocs.pm/elixir/patterns-and-guards.html"
|
||||
"To learn more about guards, visit: https://elixir.hexdocs.pm/patterns-and-guards.html"
|
||||
|
||||
_ ->
|
||||
:ok
|
||||
|
||||
@@ -99,9 +99,14 @@ defmodule Kernel.CLI do
|
||||
Shared helper for error formatting on CLI tools.
|
||||
"""
|
||||
def format_error(kind, reason, stacktrace) do
|
||||
{banner, rest} = format_error_parts(kind, reason, stacktrace)
|
||||
[banner, rest]
|
||||
end
|
||||
|
||||
defp format_error_parts(kind, reason, stacktrace) do
|
||||
{blamed, stacktrace} = Exception.blame(kind, reason, stacktrace)
|
||||
|
||||
iodata =
|
||||
banner =
|
||||
case blamed do
|
||||
%FunctionClauseError{} ->
|
||||
formatted = Exception.format_banner(kind, reason, stacktrace)
|
||||
@@ -112,7 +117,7 @@ defmodule Kernel.CLI do
|
||||
Exception.format_banner(kind, blamed, stacktrace)
|
||||
end
|
||||
|
||||
[iodata, ?\n, Exception.format_stacktrace(prune_stacktrace(stacktrace))]
|
||||
{banner, [?\n, Exception.format_stacktrace(prune_stacktrace(stacktrace))]}
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -179,7 +184,8 @@ defmodule Kernel.CLI do
|
||||
## Error handling
|
||||
|
||||
defp print_error(kind, reason, stacktrace) do
|
||||
IO.write(:stderr, format_error(kind, reason, stacktrace))
|
||||
{banner, rest} = format_error_parts(kind, reason, stacktrace)
|
||||
IO.write(:stderr, [IO.ANSI.format([:red, banner]), rest])
|
||||
end
|
||||
|
||||
defp blame_match(%{match?: true, node: node}), do: blame_ansi(:normal, "+", node)
|
||||
@@ -427,7 +433,7 @@ defmodule Kernel.CLI do
|
||||
end
|
||||
|
||||
defp process_command({:rpc_eval, node, expr}, _config) when is_list(expr) do
|
||||
node = List.to_atom(node)
|
||||
node = List.to_unsafe_atom(node)
|
||||
|
||||
# Explicitly connect the node in case the rpc node was started with --sname/--name undefined.
|
||||
_ = :net_kernel.connect_node(node)
|
||||
|
||||
@@ -197,8 +197,8 @@ defmodule Kernel.ParallelCompiler do
|
||||
deprecation warnings, and type checking should run. Defaults to `true`.
|
||||
We recommend disabling it only for debugging purposes.
|
||||
|
||||
* `:profile` - if set to `:time` measure the compilation time of each compilation cycle
|
||||
and group pass checker
|
||||
* `:profile` - if set to `:time`, measure the compilation time of each compilation cycle,
|
||||
each module type check, and group pass checker
|
||||
|
||||
* `:purge_compiler_modules` - if set to `true`, automatically purge compilation modules
|
||||
after compilation (see `Code.purge_compiler_modules/0`)
|
||||
|
||||
@@ -860,29 +860,29 @@ defmodule Kernel.Typespec do
|
||||
{{:type, location(meta), :nonempty_string, args}, state}
|
||||
end
|
||||
|
||||
defp typespec({type, _meta, []}, vars, caller, state) when type in [:charlist, :char_list] do
|
||||
defp typespec({type, meta, []}, vars, caller, state) when type in [:charlist, :char_list] do
|
||||
if type == :char_list do
|
||||
warning = "the char_list() type is deprecated, use charlist()"
|
||||
IO.warn(warning, caller)
|
||||
end
|
||||
|
||||
typespec(quote(do: :elixir.charlist()), vars, caller, state)
|
||||
remote_typespec(:charlist, meta, [], vars, caller, state)
|
||||
end
|
||||
|
||||
defp typespec({:nonempty_charlist, _meta, []}, vars, caller, state) do
|
||||
typespec(quote(do: :elixir.nonempty_charlist()), vars, caller, state)
|
||||
defp typespec({:nonempty_charlist, meta, []}, vars, caller, state) do
|
||||
remote_typespec(:nonempty_charlist, meta, [], vars, caller, state)
|
||||
end
|
||||
|
||||
defp typespec({:struct, _meta, []}, vars, caller, state) do
|
||||
typespec(quote(do: :elixir.struct()), vars, caller, state)
|
||||
defp typespec({:struct, meta, []}, vars, caller, state) do
|
||||
remote_typespec(:struct, meta, [], vars, caller, state)
|
||||
end
|
||||
|
||||
defp typespec({:as_boolean, _meta, [arg]}, vars, caller, state) do
|
||||
typespec(quote(do: :elixir.as_boolean(unquote(arg))), vars, caller, state)
|
||||
defp typespec({:as_boolean, meta, [arg]}, vars, caller, state) do
|
||||
remote_typespec(:as_boolean, meta, [arg], vars, caller, state)
|
||||
end
|
||||
|
||||
defp typespec({:keyword, _meta, args}, vars, caller, state) when length(args) <= 1 do
|
||||
typespec(quote(do: :elixir.keyword(unquote_splicing(args))), vars, caller, state)
|
||||
defp typespec({:keyword, meta, args}, vars, caller, state) when length(args) <= 1 do
|
||||
remote_typespec(:keyword, meta, args, vars, caller, state)
|
||||
end
|
||||
|
||||
defp typespec({:fun, meta, args}, vars, caller, state) do
|
||||
@@ -1007,6 +1007,10 @@ defmodule Kernel.Typespec do
|
||||
{{:remote_type, location(meta), [remote, name, args]}, state}
|
||||
end
|
||||
|
||||
defp remote_typespec(name, meta, args, vars, caller, state) do
|
||||
typespec({{:., meta, [:elixir, name]}, meta, args}, vars, caller, state)
|
||||
end
|
||||
|
||||
defp collect_union({:|, _, [a, b]}), do: [a | collect_union(b)]
|
||||
defp collect_union(v), do: [v]
|
||||
|
||||
|
||||
@@ -329,21 +329,27 @@ defmodule Kernel.Utils do
|
||||
|
||||
@spec defguard([Macro.t()], Macro.t(), Macro.Env.t()) :: Macro.t()
|
||||
def defguard(args, expr, env) do
|
||||
{^args, vars} = extract_refs_from_args(args)
|
||||
env = :elixir_env.with_vars(%{env | context: :guard}, vars)
|
||||
{expr, _, _} = :elixir_expand.expand(expr, :elixir_env.env_to_ex(env), env)
|
||||
{_, vars} = extract_refs_from_args(args)
|
||||
guard_expr = expand_defguard(expr, %{env | context: :guard}, vars)
|
||||
body_expr = expand_defguard(expr, %{env | context: nil}, vars)
|
||||
|
||||
quote do
|
||||
case Macro.Env.in_guard?(__CALLER__) do
|
||||
true ->
|
||||
unquote(literal_quote(unquote_every_ref(expr, vars), []))
|
||||
unquote(literal_quote(unquote_every_ref(guard_expr, vars), []))
|
||||
|
||||
false ->
|
||||
unquote(literal_quote(unquote_refs_once(expr, vars, env), generated: true))
|
||||
unquote(literal_quote(unquote_refs_once(body_expr, vars, env), generated: true))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp expand_defguard(expr, env, vars) do
|
||||
env = :elixir_env.with_vars(env, vars)
|
||||
{expr, _, _} = :elixir_expand.expand(expr, :elixir_env.env_to_ex(env), env)
|
||||
expr
|
||||
end
|
||||
|
||||
defp extract_refs_from_args(args) do
|
||||
Macro.postwalk(args, [], fn
|
||||
{ref, meta, context} = var, acc when is_atom(ref) and is_atom(context) ->
|
||||
@@ -369,9 +375,7 @@ defmodule Kernel.Utils do
|
||||
end
|
||||
|
||||
# Prefaces `guard` with unquoted versions of `refs`.
|
||||
defp unquote_refs_once(guard, refs, %{module: module} = env) do
|
||||
env = %{env | context: nil}
|
||||
|
||||
defp unquote_refs_once(guard, refs, %{module: module}) do
|
||||
{guard, used_refs} =
|
||||
Macro.postwalk(guard, %{}, fn
|
||||
{ref, meta, context} = var, acc when is_atom(ref) and is_atom(context) ->
|
||||
@@ -384,7 +388,7 @@ defmodule Kernel.Utils do
|
||||
{new_var, acc}
|
||||
|
||||
%{} ->
|
||||
generated = String.to_atom("arg" <> Integer.to_string(map_size(acc) + 1))
|
||||
generated = String.to_unsafe_atom("arg" <> Integer.to_string(map_size(acc) + 1))
|
||||
new_var = Macro.unique_var(generated, module)
|
||||
{new_var, Map.put(acc, pair, {new_var, var})}
|
||||
end
|
||||
@@ -393,12 +397,6 @@ defmodule Kernel.Utils do
|
||||
{var, acc}
|
||||
end
|
||||
|
||||
{{:., dot_meta, [:erlang, :orelse]}, meta, [left, right]}, acc ->
|
||||
{Macro.expand({{:., dot_meta, [Kernel, :or]}, meta, [left, right]}, env), acc}
|
||||
|
||||
{{:., dot_meta, [:erlang, :andalso]}, meta, [left, right]}, acc ->
|
||||
{Macro.expand({{:., dot_meta, [Kernel, :and]}, meta, [left, right]}, env), acc}
|
||||
|
||||
node, acc ->
|
||||
{node, acc}
|
||||
end)
|
||||
|
||||
+52
-30
@@ -269,38 +269,60 @@ defmodule Keyword do
|
||||
@doc since: "1.13.0"
|
||||
@spec validate(keyword(), values :: [atom() | {atom(), term()}]) ::
|
||||
{:ok, keyword()} | {:error, [atom]}
|
||||
def validate([], values) when is_list(values), do: {:ok, move_pairs!(values, [])}
|
||||
|
||||
def validate(keyword, values) when is_list(keyword) and is_list(values) do
|
||||
validate(keyword, values, [], keyword, [])
|
||||
validate_merge(keyword, values, [], keyword)
|
||||
end
|
||||
|
||||
defp validate([{key, _} | keyword], values1, values2, original, bad_keys) when is_atom(key) do
|
||||
case find_key!(key, values1, values2) do
|
||||
{values1, values2} ->
|
||||
validate(keyword, values1, values2, original, bad_keys)
|
||||
defp validate_merge([], values, values_pre, original),
|
||||
do: {:ok, move_pairs!(values, move_pairs!(values_pre, original))}
|
||||
|
||||
:error ->
|
||||
case find_key!(key, values2, values1) do
|
||||
{values1, values2} ->
|
||||
validate(keyword, values1, values2, original, bad_keys)
|
||||
|
||||
:error ->
|
||||
validate(keyword, values1, values2, original, [key | bad_keys])
|
||||
end
|
||||
defp validate_merge([{key, _} = pair | keyword], [head | tail], values_pre, original)
|
||||
when is_atom(key) do
|
||||
case head do
|
||||
^key -> validate_merge(keyword, tail, values_pre, original)
|
||||
{^key, _} -> validate_merge(keyword, tail, values_pre, original)
|
||||
_ -> validate_merge([pair | keyword], tail, [head | values_pre], original)
|
||||
end
|
||||
end
|
||||
|
||||
defp validate([], values1, values2, original, []) do
|
||||
{:ok, move_pairs!(values1, move_pairs!(values2, original))}
|
||||
defp validate_merge([{key, _} | keyword], [], values_pre, original) when is_atom(key) do
|
||||
case find_key!(key, values_pre, []) do
|
||||
{new_values, new_values_pre} ->
|
||||
validate_merge(keyword, new_values, new_values_pre, original)
|
||||
|
||||
:error ->
|
||||
validate_fallback(keyword, values_pre, [key])
|
||||
end
|
||||
end
|
||||
|
||||
defp validate([], _values1, _values2, _original, bad_keys) do
|
||||
{:error, bad_keys}
|
||||
defp validate_merge([pair | _], _, _, _),
|
||||
do:
|
||||
raise(
|
||||
ArgumentError,
|
||||
"expected a keyword list as first argument, got invalid entry: #{inspect(pair)}"
|
||||
)
|
||||
|
||||
defp validate_fallback([{key, _} | keyword], values, bad_keys)
|
||||
when is_atom(key) do
|
||||
case find_key!(key, values, []) do
|
||||
{rest, acc} ->
|
||||
validate_fallback(keyword, rest ++ acc, bad_keys)
|
||||
|
||||
:error ->
|
||||
validate_fallback(keyword, values, [key | bad_keys])
|
||||
end
|
||||
end
|
||||
|
||||
defp validate([pair | _], _values1, _values2, _original, []) do
|
||||
raise ArgumentError,
|
||||
"expected a keyword list as first argument, got invalid entry: #{inspect(pair)}"
|
||||
end
|
||||
defp validate_fallback([], _, bad), do: {:error, bad}
|
||||
|
||||
defp validate_fallback([p | _], _, _),
|
||||
do:
|
||||
raise(
|
||||
ArgumentError,
|
||||
"expected a keyword list as first argument, got invalid entry: #{inspect(p)}"
|
||||
)
|
||||
|
||||
defp find_key!(key, [key | rest], acc), do: {rest, acc}
|
||||
defp find_key!(key, [{key, _} | rest], acc), do: {rest, acc}
|
||||
@@ -1342,9 +1364,9 @@ defmodule Keyword do
|
||||
"""
|
||||
@spec pop(t, key, default) :: {value | default, t}
|
||||
def pop(keywords, key, default \\ nil) when is_list(keywords) and is_atom(key) do
|
||||
case fetch(keywords, key) do
|
||||
{:ok, value} -> {value, delete(keywords, key)}
|
||||
:error -> {default, keywords}
|
||||
case :lists.keyfind(key, 1, keywords) do
|
||||
{^key, value} -> {value, delete_key(keywords, key)}
|
||||
false -> {default, keywords}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1369,9 +1391,9 @@ defmodule Keyword do
|
||||
@doc since: "1.10.0"
|
||||
@spec pop!(t, key) :: {value, t}
|
||||
def pop!(keywords, key) when is_list(keywords) and is_atom(key) do
|
||||
case fetch(keywords, key) do
|
||||
{:ok, value} -> {value, delete(keywords, key)}
|
||||
:error -> raise KeyError, key: key, term: keywords
|
||||
case :lists.keyfind(key, 1, keywords) do
|
||||
{^key, value} -> {value, delete_key(keywords, key)}
|
||||
false -> raise KeyError, key: key, term: keywords
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1434,9 +1456,9 @@ defmodule Keyword do
|
||||
@spec pop_lazy(t, key, (-> value)) :: {value, t}
|
||||
def pop_lazy(keywords, key, fun)
|
||||
when is_list(keywords) and is_atom(key) and is_function(fun, 0) do
|
||||
case fetch(keywords, key) do
|
||||
{:ok, value} -> {value, delete(keywords, key)}
|
||||
:error -> {fun.(), keywords}
|
||||
case :lists.keyfind(key, 1, keywords) do
|
||||
{^key, value} -> {value, delete_key(keywords, key)}
|
||||
false -> {fun.(), keywords}
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
+54
-5
@@ -102,7 +102,7 @@ defmodule List do
|
||||
Even though the representation changed, the raw data does remain a list of
|
||||
integers, which can be handled as such:
|
||||
|
||||
iex> inspect(~c"abc", charlists: :as_list)
|
||||
iex> inspect(~c"abc", charlists: :as_lists)
|
||||
"[97, 98, 99]"
|
||||
iex> Enum.map(~c"abc", fn num -> 1000 + num end)
|
||||
[1097, 1098, 1099]
|
||||
@@ -1024,6 +1024,11 @@ defmodule List do
|
||||
:lists.suffix(suffix, list)
|
||||
end
|
||||
|
||||
@doc deprecated: "Use to_existing_atom/1 or to_unsafe_atom/1 instead"
|
||||
def to_atom(charlist) do
|
||||
:erlang.list_to_atom(charlist)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts a charlist to an atom.
|
||||
|
||||
@@ -1034,21 +1039,24 @@ defmodule List do
|
||||
|
||||
## Examples
|
||||
|
||||
iex> List.to_atom(~c"Elixir")
|
||||
iex> List.to_unsafe_atom(~c"Elixir")
|
||||
:Elixir
|
||||
|
||||
iex> List.to_atom(~c"🌢 Elixir")
|
||||
iex> List.to_unsafe_atom(~c"🌢 Elixir")
|
||||
:"🌢 Elixir"
|
||||
|
||||
"""
|
||||
@spec to_atom(charlist) :: atom
|
||||
def to_atom(charlist) do
|
||||
@doc since: "1.21.0"
|
||||
@spec to_unsafe_atom(charlist) :: atom
|
||||
def to_unsafe_atom(charlist) do
|
||||
:erlang.list_to_atom(charlist)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts a charlist to an existing atom.
|
||||
|
||||
If the list of expected atoms is known upfront, prefer `to_existing_atom/2`.
|
||||
|
||||
Elixir supports conversions from charlists which contain any Unicode
|
||||
code point. Raises an `ArgumentError` if the atom does not exist.
|
||||
|
||||
@@ -1079,6 +1087,47 @@ defmodule List do
|
||||
:erlang.list_to_existing_atom(charlist)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts a charlist to one of the `allowed_atoms` or raises.
|
||||
|
||||
Raises an `ArgumentError` if the atom either does not exist or is not within
|
||||
the existing list.
|
||||
|
||||
This should be preferred to `to_existing_atom/1` if the list is known upfront,
|
||||
since there is no risk that the atom has not been loaded.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> List.to_existing_atom(~c"foo", [:foo, :bar])
|
||||
:foo
|
||||
|
||||
iex> List.to_existing_atom(~c"unknown", [:foo, :bar])
|
||||
** (ArgumentError) unexpected value: ~c\"unknown\", the allowed atoms are: [:foo, :bar]
|
||||
|
||||
"""
|
||||
@doc since: "1.21.0"
|
||||
@spec to_existing_atom(charlist, nonempty_list(a)) :: a when a: atom()
|
||||
def to_existing_atom(charlist, [_ | _] = allowed_atoms) when is_list(charlist) do
|
||||
atom = :erlang.list_to_existing_atom(charlist)
|
||||
|
||||
if atom not in allowed_atoms do
|
||||
to_existing_atom_unexpected(charlist, allowed_atoms)
|
||||
end
|
||||
|
||||
atom
|
||||
end
|
||||
|
||||
# used just to have a less cryptic stacktrace and consistent error
|
||||
@doc false
|
||||
def __to_existing_atom__(charlist, allowed_atoms) do
|
||||
to_existing_atom_unexpected(charlist, allowed_atoms)
|
||||
end
|
||||
|
||||
defp to_existing_atom_unexpected(charlist, allowed_atoms) do
|
||||
raise ArgumentError,
|
||||
"unexpected value: #{inspect(charlist)}, the allowed atoms are: #{inspect(allowed_atoms)}"
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the float whose text representation is `charlist`.
|
||||
|
||||
|
||||
@@ -527,7 +527,7 @@ defmodule Macro do
|
||||
|
||||
defp generate_arguments(amount, context, fun)
|
||||
when is_integer(amount) and amount > 0 and is_atom(context) do
|
||||
for id <- 1..amount, do: fun.(String.to_atom("arg" <> Integer.to_string(id)), context)
|
||||
for id <- 1..amount, do: fun.(String.to_unsafe_atom("arg" <> Integer.to_string(id)), context)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -2699,7 +2699,7 @@ defmodule Macro do
|
||||
:guard ->
|
||||
raise ArgumentError,
|
||||
"invalid expression in guard, dbg is not allowed in guards. " <>
|
||||
"To learn more about guards, visit: https://hexdocs.pm/elixir/patterns-and-guards.html"
|
||||
"To learn more about guards, visit: https://elixir.hexdocs.pm/patterns-and-guards.html"
|
||||
|
||||
_ ->
|
||||
:ok
|
||||
|
||||
@@ -210,13 +210,13 @@ defmodule Macro.Env do
|
||||
[file: file, line: line]
|
||||
end
|
||||
|
||||
# TODO: Deprecate on Elixir 1.21 in favor of expand_alias/4
|
||||
@doc false
|
||||
@deprecated "Use Macro.Env.expand_alias/4 instead"
|
||||
def fetch_alias(%{__struct__: Macro.Env, aliases: aliases}, atom) when is_atom(atom),
|
||||
do: Keyword.fetch(aliases, :"Elixir.#{atom}")
|
||||
|
||||
# TODO: Deprecate on Elixir 1.21 in favor of expand_alias/4
|
||||
@doc false
|
||||
@deprecated "Use Macro.Env.expand_alias/4 instead"
|
||||
def fetch_macro_alias(%{__struct__: Macro.Env, macro_aliases: aliases}, atom)
|
||||
when is_atom(atom),
|
||||
do: Keyword.fetch(aliases, :"Elixir.#{atom}")
|
||||
|
||||
@@ -171,18 +171,14 @@ defmodule MapSet do
|
||||
@doc since: "1.14.0"
|
||||
@spec symmetric_difference(t(val1), t(val2)) :: t(val1 | val2) when val1: value, val2: value
|
||||
def symmetric_difference(%MapSet{map: set1} = map_set1, %MapSet{map: set2} = _map_set2) do
|
||||
{small, large} = if :sets.size(set1) <= :sets.size(set2), do: {set1, set2}, else: {set2, set1}
|
||||
|
||||
disjointer_fun = fn elem, {small, acc} ->
|
||||
if :sets.is_element(elem, small) do
|
||||
{:sets.del_element(elem, small), acc}
|
||||
map =
|
||||
if :sets.is_disjoint(set1, set2) do
|
||||
:sets.union(set1, set2)
|
||||
else
|
||||
{small, [elem | acc]}
|
||||
:sets.union(:sets.subtract(set1, set2), :sets.subtract(set2, set1))
|
||||
end
|
||||
end
|
||||
|
||||
{new_small, list} = :sets.fold(disjointer_fun, {small, []}, large)
|
||||
%{map_set1 | map: :sets.union(new_small, :sets.from_list(list, version: 2))}
|
||||
%{map_set1 | map: map}
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
||||
@@ -190,7 +190,7 @@ defmodule Module do
|
||||
|
||||
* a string (often a heredoc)
|
||||
* `false`, which will make the entity invisible to documentation-extraction
|
||||
tools like [`ExDoc`](https://hexdocs.pm/ex_doc/)
|
||||
tools like [`ExDoc`](https://ex-doc.hexdocs.pm/)
|
||||
* a keyword list, since Elixir 1.7.0
|
||||
|
||||
For example:
|
||||
@@ -216,7 +216,7 @@ defmodule Module do
|
||||
|
||||
As can be seen in the example above, since Elixir 1.7.0 `@doc` and `@typedoc`
|
||||
also accept a keyword list that serves as a way to provide arbitrary metadata
|
||||
about the entity. Tools like [`ExDoc`](https://hexdocs.pm/ex_doc/) and
|
||||
about the entity. Tools like [`ExDoc`](https://ex-doc.hexdocs.pm/) and
|
||||
`IEx` may use this information to display annotations. A common use
|
||||
case is the `:since` key, which may be used to annotate in which version the
|
||||
function was introduced.
|
||||
@@ -263,7 +263,7 @@ defmodule Module do
|
||||
|
||||
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).
|
||||
[`mix compile.elixir`](https://mix.hexdocs.pm/Mix.Tasks.Compile.Elixir.html).
|
||||
|
||||
The specified file path provided is interpreted as relative to
|
||||
the folder containing the project's `mix.exs`, which is the
|
||||
@@ -321,7 +321,7 @@ defmodule Module do
|
||||
|
||||
Accepts a string (often a heredoc) or `false` where `@moduledoc false`
|
||||
will make the module invisible to documentation extraction tools like
|
||||
[`ExDoc`](https://hexdocs.pm/ex_doc/).
|
||||
[`ExDoc`](https://ex-doc.hexdocs.pm/).
|
||||
|
||||
Similarly to `@doc` also accepts a keyword list to provide metadata
|
||||
about the module. For more details, see the documentation of `@doc`
|
||||
@@ -366,7 +366,8 @@ defmodule Module do
|
||||
Unlike other hooks, `@on_definition` will only invoke functions and
|
||||
never macros. This is to avoid `@on_definition` callbacks from
|
||||
redefining functions that have just been defined in favor of more
|
||||
explicit approaches.
|
||||
explicit approaches. They are also invoked in the reverse order of
|
||||
registration.
|
||||
|
||||
When just a module is provided, the function is assumed to be
|
||||
`__on_definition__/6`.
|
||||
@@ -1147,7 +1148,7 @@ defmodule Module do
|
||||
defp simplify_var(var, guess_priority) do
|
||||
case Atom.to_string(var) do
|
||||
"_" -> {:_, [], guess_priority}
|
||||
"_" <> rest -> {String.to_atom(rest), [], guess_priority}
|
||||
"_" <> rest -> {String.to_unsafe_atom(rest), [], guess_priority}
|
||||
_ -> {var, [], nil}
|
||||
end
|
||||
end
|
||||
@@ -1158,7 +1159,7 @@ defmodule Module do
|
||||
rescue
|
||||
ArgumentError -> module
|
||||
else
|
||||
module_name -> String.to_atom(Macro.underscore(List.last(module_name)))
|
||||
module_name -> String.to_unsafe_atom(Macro.underscore(List.last(module_name)))
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -228,11 +228,7 @@ defmodule Module.Behaviour do
|
||||
defp behaviour_defined?(callbacks, behaviour) do
|
||||
callbacks
|
||||
|> Map.values()
|
||||
|> List.flatten()
|
||||
|> Enum.any?(fn
|
||||
{_kind, ^behaviour, _optional?} -> true
|
||||
{_kind, _behaviour, _optional?} -> false
|
||||
end)
|
||||
|> Enum.any?(fn list -> :lists.keymember(behaviour, 2, list) end)
|
||||
end
|
||||
|
||||
defp warn_missing_impls(%{callbacks: callbacks} = context, _impl_contexts, _defs)
|
||||
|
||||
@@ -20,6 +20,7 @@ defmodule Module.ParallelChecker do
|
||||
{:max_concurrency, pos_integer()}
|
||||
| {:long_verification_threshold, pos_integer()}
|
||||
| {:each_long_verification, (module() -> term()) | (module(), pid() -> term())}
|
||||
| {:profile, :time}
|
||||
| {atom(), term()}
|
||||
]
|
||||
|
||||
@@ -117,13 +118,13 @@ defmodule Module.ParallelChecker do
|
||||
send(checker, {ref, :cached})
|
||||
|
||||
receive do
|
||||
{^ref, :check} ->
|
||||
{^ref, :check, profile} ->
|
||||
# Set the compiler info so we can collect warnings
|
||||
:erlang.put(:elixir_compiler_info, {pid, self()})
|
||||
|
||||
{warnings, errors} =
|
||||
if module_tuple do
|
||||
check_module(module_tuple, {checker, table}, log?)
|
||||
check_module(module_tuple, {checker, table}, log?, profile)
|
||||
else
|
||||
{[], []}
|
||||
end
|
||||
@@ -262,7 +263,7 @@ defmodule Module.ParallelChecker do
|
||||
|
||||
## Module checking
|
||||
|
||||
defp check_module(module_tuple, cache, log?) do
|
||||
defp check_module(module_tuple, cache, log?, profile) do
|
||||
{module, file, line, definitions, no_warn_undefined, behaviours, impls, attrs, after_verify} =
|
||||
module_tuple
|
||||
|
||||
@@ -276,9 +277,13 @@ defmodule Module.ParallelChecker do
|
||||
definitions
|
||||
)
|
||||
|
||||
type_warnings =
|
||||
profile(module, profile, fn ->
|
||||
Module.Types.warnings(module, file, attrs, definitions, no_warn_undefined, cache)
|
||||
end)
|
||||
|
||||
{warnings, errors} =
|
||||
module
|
||||
|> Module.Types.warnings(file, attrs, definitions, no_warn_undefined, cache)
|
||||
type_warnings
|
||||
|> Kernel.++(behaviour_warnings)
|
||||
|> group_diagnostics()
|
||||
|> emit_diagnostics(file, log?)
|
||||
@@ -577,6 +582,7 @@ defmodule Module.ParallelChecker do
|
||||
schedulers: schedulers,
|
||||
threshold: threshold,
|
||||
callback: callback,
|
||||
profile: Keyword.get(options, :profile),
|
||||
protocols: [],
|
||||
table: table
|
||||
}
|
||||
@@ -653,9 +659,24 @@ defmodule Module.ParallelChecker do
|
||||
end
|
||||
|
||||
defp run_checkers(%{modules: [{module, pid, ref} | modules]} = state) do
|
||||
send(pid, {ref, :check})
|
||||
send(pid, {ref, :check, state.profile})
|
||||
timer = :erlang.send_after(state.threshold, self(), {__MODULE__, :timeout, module, pid})
|
||||
spawned = Map.put(state.spawned, module, timer)
|
||||
run_checkers(%{state | modules: modules, spawned: spawned})
|
||||
end
|
||||
|
||||
defp profile(module, :time, fun) do
|
||||
{time, result} = :timer.tc(fun)
|
||||
|
||||
IO.puts(
|
||||
:stderr,
|
||||
"[profile] Type checked #{inspect(module)} in #{div(time, 1000)}ms"
|
||||
)
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
defp profile(_module, _profile, fun) do
|
||||
fun.()
|
||||
end
|
||||
end
|
||||
|
||||
@@ -136,8 +136,8 @@ defmodule Module.Types do
|
||||
defp impl_for(attrs) do
|
||||
case List.keyfind(attrs, :__impl__, 0) do
|
||||
{:__impl__, [protocol: protocol, for: for]} ->
|
||||
if Code.ensure_loaded?(protocol) and function_exported?(protocol, :behaviour_info, 1) do
|
||||
{for, protocol.behaviour_info(:callbacks)}
|
||||
if Code.ensure_loaded?(protocol) and function_exported?(protocol, :__protocol__, 1) do
|
||||
{for, protocol.__protocol__(:functions)}
|
||||
else
|
||||
nil
|
||||
end
|
||||
@@ -266,7 +266,7 @@ defmodule Module.Types do
|
||||
|
||||
Enum.reduce(unused_indexes, context, fn clause_index, context ->
|
||||
{meta, _args, _guards, _body} = Enum.fetch!(clauses, clause_index)
|
||||
stack = %{stack | function: fun_arity}
|
||||
stack = %{stack | function: fun_arity} |> with_file_meta(meta)
|
||||
Helpers.warn(__MODULE__, {:unused_clause, kind, fun_arity}, meta, stack, context)
|
||||
end)
|
||||
else
|
||||
@@ -299,11 +299,11 @@ defmodule Module.Types do
|
||||
|
||||
local_sigs ->
|
||||
case finder.(fun_arity) do
|
||||
{mode, {fun_arity, kind, meta, clauses}, expected} ->
|
||||
{mode, {fun_arity, kind, _meta, clauses}, expected} ->
|
||||
context = put_in(context.local_sigs, Map.put(local_sigs, fun_arity, kind))
|
||||
|
||||
{inferred, mapping, context} =
|
||||
local_handler(mode, fun_arity, kind, meta, clauses, expected, stack, context)
|
||||
local_handler(mode, fun_arity, kind, clauses, expected, stack, context)
|
||||
|
||||
context =
|
||||
update_in(context.local_sigs, &Map.put(&1, fun_arity, {kind, inferred, mapping}))
|
||||
@@ -316,9 +316,10 @@ defmodule Module.Types do
|
||||
end
|
||||
end
|
||||
|
||||
defp local_handler(:traverse, {_, arity}, _kind, _meta, clauses, _expected, stack, context) do
|
||||
defp local_handler(:traverse, {_, arity}, _kind, clauses, _expected, stack, context) do
|
||||
context =
|
||||
Enum.reduce(clauses, context, fn {_meta, _args, _guards, body}, context ->
|
||||
Enum.reduce(clauses, context, fn {meta, _args, _guards, body}, context ->
|
||||
stack = with_file_meta(stack, meta)
|
||||
Module.Types.Traverse.of_expr(body, stack, context)
|
||||
end)
|
||||
|
||||
@@ -326,9 +327,9 @@ defmodule Module.Types do
|
||||
{inferred, [{0, 0}], context}
|
||||
end
|
||||
|
||||
defp local_handler(mode, fun_arity, kind, meta, clauses, expected, stack, context) do
|
||||
defp local_handler(mode, fun_arity, kind, clauses, expected, stack, context) do
|
||||
{fun, _arity} = fun_arity
|
||||
stack = stack |> fresh_stack(mode, fun_arity) |> with_file_meta(meta)
|
||||
stack = fresh_stack(stack, mode, fun_arity)
|
||||
base_info = {:def, kind, fun, expected}
|
||||
|
||||
case clauses do
|
||||
@@ -341,6 +342,7 @@ defmodule Module.Types do
|
||||
end
|
||||
|
||||
defp default_local_handler(meta, args, body, base_info, kind, fun, expected, stack, context) do
|
||||
stack = with_file_meta(stack, meta)
|
||||
guards = []
|
||||
previous = Pattern.init_previous()
|
||||
fresh_context = fresh_context(context)
|
||||
@@ -385,7 +387,7 @@ defmodule Module.Types do
|
||||
_ ->
|
||||
inferred
|
||||
|> Enum.map(fn {args, _} -> args end)
|
||||
|> Enum.zip_with(fn types -> Enum.reduce(types, &Descr.union/2) end)
|
||||
|> Enum.zip_with(fn types -> Enum.reduce(types, &Descr.opt_union/2) end)
|
||||
end
|
||||
|
||||
{{:infer, domain, Enum.reverse(inferred)}, mapping, restore_context(body_context, context)}
|
||||
@@ -400,6 +402,7 @@ defmodule Module.Types do
|
||||
Enum.reduce(clauses, {0, 0, Pattern.init_previous(), [], [], [], context}, fn
|
||||
{meta, args, guards, body},
|
||||
{index, total, previous, domain, mapping, inferred, acc_context} ->
|
||||
stack = with_file_meta(stack, meta)
|
||||
fresh_context = fresh_context(acc_context)
|
||||
info = {base_info, args, guards}
|
||||
|
||||
@@ -462,7 +465,7 @@ defmodule Module.Types do
|
||||
# the domain is computed by unioning their inferred types. However, their
|
||||
# inferred types often have the different of the previous clauses:
|
||||
#
|
||||
# union(r3 ^ (c3 - c2 - c1), r2 ^ (c2 - c1), r1 ^ c1)
|
||||
# opt_union(r3 ^ (c3 - c2 - c1), r2 ^ (c2 - c1), r1 ^ c1)
|
||||
#
|
||||
# Where `rN` represents the refinement in every function body.
|
||||
#
|
||||
@@ -493,9 +496,9 @@ defmodule Module.Types do
|
||||
# Furthermore, the signature used in type checking is not refined in any way,
|
||||
# so type checking is still sound.
|
||||
if arg == head_arg do
|
||||
Descr.union(Descr.upper_bound(no_prev_arg), d)
|
||||
Descr.opt_union(Descr.upper_bound(no_prev_arg), d)
|
||||
else
|
||||
Descr.union(arg, d)
|
||||
Descr.opt_union(arg, d)
|
||||
end
|
||||
| compute_domain(args_types, head_args_types, no_prev_args_types, domain)
|
||||
]
|
||||
@@ -506,7 +509,7 @@ defmodule Module.Types do
|
||||
# We check for term equality of types as an optimization
|
||||
# to reduce the amount of check we do at runtime.
|
||||
defp add_inferred([{args, existing_return} | tail], args, return, index, acc),
|
||||
do: {index, Enum.reverse(acc, [{args, Descr.union(existing_return, return)} | tail])}
|
||||
do: {index, Enum.reverse(acc, [{args, Descr.opt_union(existing_return, return)} | tail])}
|
||||
|
||||
defp add_inferred([head | tail], args, return, index, acc),
|
||||
do: add_inferred(tail, args, return, index - 1, [head | acc])
|
||||
@@ -558,7 +561,7 @@ defmodule Module.Types do
|
||||
# Allow exactly one differing argument. That one position is widened
|
||||
# with union/2. A second difference means the clauses must stay separate.
|
||||
defp union_args([existing_arg | existing], [arg | args], acc, false) do
|
||||
union_args(existing, args, [Descr.union(existing_arg, arg) | acc], true)
|
||||
union_args(existing, args, [Descr.opt_union(existing_arg, arg) | acc], true)
|
||||
end
|
||||
|
||||
defp union_args([_ | _], [_ | _], _acc, true), do: nil
|
||||
@@ -628,7 +631,7 @@ defmodule Module.Types do
|
||||
mode: mode,
|
||||
# The function for handling local calls
|
||||
local_handler: handler,
|
||||
# Reverse arrow handling (nil | :cache | :use)
|
||||
# Reverse arrow handling (nil | :cache | :except_none | :include_none)
|
||||
reverse_arrow: nil
|
||||
}
|
||||
end
|
||||
|
||||
@@ -33,12 +33,14 @@ defmodule Module.Types.Apply do
|
||||
|> Enum.map(fn {key, type} when is_atom(key) ->
|
||||
tuple([atom([key]), type])
|
||||
end)
|
||||
|> Enum.reduce(&union/2)
|
||||
|> Enum.reduce(&opt_union/2)
|
||||
|> list()
|
||||
end
|
||||
|
||||
fas = list(tuple([atom(), integer()]))
|
||||
struct_info = list(closed_map(default: if_set(term()), field: atom()))
|
||||
|
||||
struct_info =
|
||||
list(closed_map(default: if_set(term()), field: atom(), required: if_set(boolean())))
|
||||
|
||||
shared_info = [
|
||||
attributes: list(tuple([atom(), list(term())])),
|
||||
@@ -56,7 +58,7 @@ defmodule Module.Types.Apply do
|
||||
exports_md5: binary(),
|
||||
functions: fas,
|
||||
macros: fas,
|
||||
struct: struct_info |> union(atom([nil]))
|
||||
struct: struct_info |> opt_union(atom([nil]))
|
||||
] ++ shared_info
|
||||
|
||||
infos =
|
||||
@@ -72,7 +74,7 @@ defmodule Module.Types.Apply do
|
||||
module: atom(),
|
||||
functions: fas,
|
||||
consolidated?: boolean(),
|
||||
impls: union(atom([:not_consolidated]), tuple([atom([:consolidated]), list(atom())]))}
|
||||
impls: opt_union(atom([:not_consolidated]), tuple([atom([:consolidated]), list(atom())]))}
|
||||
]
|
||||
|
||||
for {name, clauses} <- infos do
|
||||
@@ -96,10 +98,10 @@ defmodule Module.Types.Apply do
|
||||
|
||||
send_destination =
|
||||
pid()
|
||||
|> union(reference())
|
||||
|> union(port())
|
||||
|> union(atom())
|
||||
|> union(tuple([atom(), atom()]))
|
||||
|> opt_union(reference())
|
||||
|> opt_union(port())
|
||||
|> opt_union(atom())
|
||||
|> opt_union(tuple([atom(), atom()]))
|
||||
|
||||
basic_arith_2_args_clauses = [
|
||||
{[integer(), integer()], integer()},
|
||||
@@ -108,16 +110,16 @@ defmodule Module.Types.Apply do
|
||||
{[float(), float()], float()}
|
||||
]
|
||||
|
||||
args_or_arity = union(list(term()), integer())
|
||||
args_or_none = union(list(term()), atom([:none]))
|
||||
args_or_arity = opt_union(list(term()), integer())
|
||||
args_or_none = opt_union(list(term()), atom([:none]))
|
||||
extra_info = kw.(file: list(integer()), line: integer(), error_info: open_map())
|
||||
|
||||
raise_stacktrace =
|
||||
list(
|
||||
tuple([atom(), atom(), args_or_arity, extra_info])
|
||||
|> union(tuple([atom(), atom(), args_or_arity]))
|
||||
|> union(tuple([fun(), args_or_arity, extra_info]))
|
||||
|> union(tuple([fun(), args_or_arity]))
|
||||
|> opt_union(tuple([atom(), atom(), args_or_arity]))
|
||||
|> opt_union(tuple([fun(), args_or_arity, extra_info]))
|
||||
|> opt_union(tuple([fun(), args_or_arity]))
|
||||
)
|
||||
|
||||
not_signature =
|
||||
@@ -145,7 +147,8 @@ defmodule Module.Types.Apply do
|
||||
{:erlang, :-, [{[integer()], integer()}, {[float()], float()}]},
|
||||
{:erlang, :-, basic_arith_2_args_clauses},
|
||||
{:erlang, :*, basic_arith_2_args_clauses},
|
||||
{:erlang, :/, [{[union(integer(), float()), union(integer(), float())], float()}]},
|
||||
{:erlang, :/,
|
||||
[{[opt_union(integer(), float()), opt_union(integer(), float())], float()}]},
|
||||
{:erlang, :"/=", [{[term(), term()], boolean()}]},
|
||||
{:erlang, :"=/=", [{[term(), term()], boolean()}]},
|
||||
{:erlang, :<, [{[term(), term()], boolean()}]},
|
||||
@@ -175,12 +178,12 @@ defmodule Module.Types.Apply do
|
||||
{:erlang, :bsr, [{[integer(), integer()], integer()}]},
|
||||
{:erlang, :bxor, [{[integer(), integer()], integer()}]},
|
||||
{:erlang, :byte_size, [{[bitstring()], integer()}]},
|
||||
{:erlang, :ceil, [{[union(integer(), float())], integer()}]},
|
||||
{:erlang, :ceil, [{[opt_union(integer(), float())], integer()}]},
|
||||
{:erlang, :div, [{[integer(), integer()], integer()}]},
|
||||
{:erlang, :error, [{[term()], none()}]},
|
||||
{:erlang, :error, [{[term(), args_or_none], none()}]},
|
||||
{:erlang, :error, [{[term(), args_or_none, kw.(error_info: open_map())], none()}]},
|
||||
{:erlang, :floor, [{[union(integer(), float())], integer()}]},
|
||||
{:erlang, :floor, [{[opt_union(integer(), float())], integer()}]},
|
||||
{:erlang, :function_exported, [{[atom(), atom(), integer()], boolean()}]},
|
||||
{:erlang, :integer_to_binary, [{[integer()], binary()}]},
|
||||
{:erlang, :integer_to_binary, [{[integer(), integer()], binary()}]},
|
||||
@@ -198,12 +201,12 @@ defmodule Module.Types.Apply do
|
||||
{:erlang, :make_tuple, [{[integer(), term()], tuple()}]},
|
||||
{:erlang, :map_size, [{[open_map()], integer()}]},
|
||||
{:erlang, :node, [{[], atom()}]},
|
||||
{:erlang, :node, [{[pid() |> union(reference()) |> union(port())], atom()}]},
|
||||
{:erlang, :node, [{[pid() |> opt_union(reference()) |> opt_union(port())], atom()}]},
|
||||
{:erlang, :not, not_signature},
|
||||
{:erlang, :or, or_signature},
|
||||
{:erlang, :raise, [{[atom([:error, :exit, :throw]), term(), raise_stacktrace], none()}]},
|
||||
{:erlang, :rem, [{[integer(), integer()], integer()}]},
|
||||
{:erlang, :round, [{[union(integer(), float())], integer()}]},
|
||||
{:erlang, :round, [{[opt_union(integer(), float())], integer()}]},
|
||||
{:erlang, :self, [{[], pid()}]},
|
||||
{:erlang, :spawn, [{[fun(0)], pid()}]},
|
||||
{:erlang, :spawn, [{mfargs, pid()}]},
|
||||
@@ -213,7 +216,7 @@ defmodule Module.Types.Apply do
|
||||
{:erlang, :spawn_monitor, [{mfargs, tuple([pid(), reference()])}]},
|
||||
{:erlang, :split_binary, [{[binary(), integer()], tuple([binary(), binary()])}]},
|
||||
{:erlang, :tuple_size, [{[open_tuple([])], integer()}]},
|
||||
{:erlang, :trunc, [{[union(integer(), float())], integer()}]},
|
||||
{:erlang, :trunc, [{[opt_union(integer(), float())], integer()}]},
|
||||
|
||||
# TODO: Replace term()/dynamic() by parametric types
|
||||
{:erlang, :++,
|
||||
@@ -242,12 +245,16 @@ defmodule Module.Types.Apply do
|
||||
{Kernel, :put_elem, [{[open_tuple([]), integer(), term()], dynamic(open_tuple([]))}]},
|
||||
|
||||
## Lists
|
||||
{:lists, :member, [{[term(), list(term())], boolean()}]},
|
||||
{:lists, :member,
|
||||
[
|
||||
{[term(), empty_list()], atom([false])},
|
||||
{[term(), non_empty_list(term())], boolean()}
|
||||
]},
|
||||
|
||||
## Map
|
||||
{Map, :delete, [{[open_map(), term()], open_map()}]},
|
||||
{Map, :fetch,
|
||||
[{[open_map(), term()], tuple([atom([:ok]), term()]) |> union(atom([:error]))}]},
|
||||
[{[open_map(), term()], tuple([atom([:ok]), term()]) |> opt_union(atom([:error]))}]},
|
||||
{Map, :fetch!, [{[open_map(), term()], term()}]},
|
||||
{Map, :from_struct, [{[open_map()], open_map(__struct__: not_set())}]},
|
||||
{Map, :get, [{[open_map(), term()], term()}]},
|
||||
@@ -271,17 +278,19 @@ defmodule Module.Types.Apply do
|
||||
{Tuple, :insert_at, [{[open_tuple([]), integer(), term()], dynamic(open_tuple([]))}]},
|
||||
{:maps, :from_keys, [{[list(term()), term()], open_map()}]},
|
||||
{:maps, :find,
|
||||
[{[term(), open_map()], tuple([atom([:ok]), term()]) |> union(atom([:error]))}]},
|
||||
[{[term(), open_map()], tuple([atom([:ok]), term()]) |> opt_union(atom([:error]))}]},
|
||||
{:maps, :get, [{[term(), open_map()], term()}]},
|
||||
{:maps, :is_key, [{[term(), open_map()], boolean()}]},
|
||||
{:maps, :keys, [{[open_map()], list(term())}]},
|
||||
{:maps, :put, [{[term(), term(), open_map()], open_map()}]},
|
||||
{:maps, :remove, [{[term(), open_map()], open_map()}]},
|
||||
{:maps, :take,
|
||||
[{[term(), open_map()], tuple([term(), open_map()]) |> union(atom([:error]))}]},
|
||||
[{[term(), open_map()], tuple([term(), open_map()]) |> opt_union(atom([:error]))}]},
|
||||
{:maps, :to_list, [{[open_map()], list(tuple([term(), term()]))}]},
|
||||
{:maps, :update, [{[term(), term(), open_map()], open_map()}]},
|
||||
{:maps, :values, [{[open_map()], list(term())}]}
|
||||
{:maps, :values, [{[open_map()], list(term())}]},
|
||||
{String, :to_existing_atom, [{[binary(), non_empty_list(atom())], atom()}]},
|
||||
{List, :to_existing_atom, [{[list(integer()), non_empty_list(atom())], atom()}]}
|
||||
] do
|
||||
[arity] = Enum.map(clauses, fn {args, _return} -> length(args) end) |> Enum.uniq()
|
||||
|
||||
@@ -294,7 +303,7 @@ defmodule Module.Types.Apply do
|
||||
domain =
|
||||
clauses
|
||||
|> Enum.map(fn {args, _} -> args end)
|
||||
|> Enum.zip_with(fn types -> Enum.reduce(types, &union/2) end)
|
||||
|> Enum.zip_with(fn types -> Enum.reduce(types, &opt_union/2) end)
|
||||
|
||||
{:strong, domain, clauses}
|
||||
end
|
||||
@@ -311,9 +320,9 @@ defmodule Module.Types.Apply do
|
||||
is_float: float(),
|
||||
is_function: fun(),
|
||||
is_integer: integer(),
|
||||
is_list: union(empty_list(), non_empty_list(term(), term())),
|
||||
is_list: opt_union(empty_list(), non_empty_list(term(), term())),
|
||||
is_map: open_map(),
|
||||
is_number: union(float(), integer()),
|
||||
is_number: opt_union(float(), integer()),
|
||||
is_pid: pid(),
|
||||
is_port: port(),
|
||||
is_reference: reference(),
|
||||
@@ -325,7 +334,7 @@ defmodule Module.Types.Apply do
|
||||
{:strong, [term()],
|
||||
[
|
||||
{[type], atom([true])},
|
||||
{[negation(type)], atom([false])}
|
||||
{[Module.Types.Descr.opt_negation(type)], atom([false])}
|
||||
]}
|
||||
|
||||
def signature(:erlang, unquote(guard), 1),
|
||||
@@ -339,7 +348,7 @@ defmodule Module.Types.Apply do
|
||||
{:strong, [term(), integer(), integer()],
|
||||
[
|
||||
{[integer(), integer(), integer()], boolean()},
|
||||
{[negation(integer()), integer(), integer()], atom([false])}
|
||||
{[Module.Types.Descr.opt_negation(integer()), integer(), integer()], atom([false])}
|
||||
]}
|
||||
)
|
||||
)
|
||||
@@ -439,7 +448,7 @@ defmodule Module.Types.Apply do
|
||||
# However, during inference, we type it as `a, b -> a and b` only.
|
||||
{left_type, context} = of_fun.(left, expected, expr, stack, context)
|
||||
{right_type, context} = of_fun.(right, expected, expr, stack, context)
|
||||
result = union(left_type, right_type)
|
||||
result = opt_union(left_type, right_type)
|
||||
|
||||
if error = mismatched_ordered_comparison(left_type, right_type, stack) do
|
||||
remote_error(error, :erlang, name, 2, expr, stack, context)
|
||||
@@ -664,10 +673,14 @@ defmodule Module.Types.Apply do
|
||||
{type, context} = of_fun.(literal, term(), expr, stack, context)
|
||||
|
||||
if singleton?(type) do
|
||||
acc = if polarity, do: union(acc, type), else: intersection(acc, negation(type))
|
||||
acc =
|
||||
if polarity,
|
||||
do: opt_union(acc, type),
|
||||
else: opt_intersection(acc, Module.Types.Descr.opt_negation(type))
|
||||
|
||||
{acc, all_singleton?, context}
|
||||
else
|
||||
acc = if polarity, do: union(acc, type), else: acc
|
||||
acc = if polarity, do: opt_union(acc, type), else: acc
|
||||
{acc, false, context}
|
||||
end
|
||||
end)
|
||||
@@ -705,9 +718,9 @@ defmodule Module.Types.Apply do
|
||||
|
||||
@empty_list empty_list()
|
||||
@non_empty_list non_empty_list(term())
|
||||
@list union(empty_list(), non_empty_list(term()))
|
||||
@list opt_union(empty_list(), non_empty_list(term()))
|
||||
@empty_map empty_map()
|
||||
@non_empty_map difference(open_map(), empty_map())
|
||||
@non_empty_map opt_difference(open_map(), empty_map())
|
||||
|
||||
# Limit the size of tuples to 16 entries
|
||||
# as otherwise we may create large nodes
|
||||
@@ -767,7 +780,7 @@ defmodule Module.Types.Apply do
|
||||
{tuple(List.duplicate(term(), literal)), true}
|
||||
|
||||
:tuple_size ->
|
||||
{difference(open_tuple([]), tuple(List.duplicate(term(), literal))), true}
|
||||
{opt_difference(open_tuple([]), tuple(List.duplicate(term(), literal))), true}
|
||||
end
|
||||
|
||||
{actual, context} = of_fun.(arg, expected, expr, stack, context)
|
||||
@@ -798,7 +811,7 @@ defmodule Module.Types.Apply do
|
||||
# This logic mirrors the code in `Pattern.of_pattern_tree`
|
||||
# If it is a singleton, we can always be precise
|
||||
if singleton?(type) do
|
||||
expected = if polarity, do: type, else: negation(type)
|
||||
expected = if polarity, do: type, else: Module.Types.Descr.opt_negation(type)
|
||||
{arg_type, context} = of_fun.(arg, expected, expr, stack, context)
|
||||
result = if subtype?(arg_type, upper_bound(expected)), do: return, else: boolean()
|
||||
|
||||
@@ -906,13 +919,13 @@ defmodule Module.Types.Apply do
|
||||
defp expected_order(_, :<, 0), do: :none
|
||||
|
||||
defp expected_order(:tuple_size, :<, size),
|
||||
do: {difference(open_tuple([]), open_tuple(List.duplicate(term(), size))), true}
|
||||
do: {opt_difference(open_tuple([]), open_tuple(List.duplicate(term(), size))), true}
|
||||
|
||||
defp expected_order(:tuple_size, :"=<", 0),
|
||||
do: {tuple([]), true}
|
||||
|
||||
defp expected_order(:tuple_size, :"=<", size),
|
||||
do: {difference(open_tuple([]), open_tuple(List.duplicate(term(), size + 1))), true}
|
||||
do: {opt_difference(open_tuple([]), open_tuple(List.duplicate(term(), size + 1))), true}
|
||||
|
||||
defp expected_order(:tuple_size, :>, size),
|
||||
do: {open_tuple(List.duplicate(term(), size + 1)), true}
|
||||
@@ -968,7 +981,7 @@ defmodule Module.Types.Apply do
|
||||
{:strong, [term(), integer()],
|
||||
[
|
||||
{[type, integer()], atom([true])},
|
||||
{[negation(type), integer()], atom([false])}
|
||||
{[Module.Types.Descr.opt_negation(type), integer()], atom([false])}
|
||||
]}
|
||||
|
||||
{info, filter_domain(info, expected, 2), context}
|
||||
@@ -1110,7 +1123,7 @@ defmodule Module.Types.Apply do
|
||||
defp remote_apply(Map, :fetch, _info, [map, key] = args_types, stack) do
|
||||
case map_get(map, key) do
|
||||
{_, value} ->
|
||||
result = tuple([atom([:ok]), value]) |> union(atom([:error]))
|
||||
result = tuple([atom([:ok]), value]) |> opt_union(atom([:error]))
|
||||
{:ok, return(result, args_types, stack)}
|
||||
|
||||
:badmap ->
|
||||
@@ -1131,7 +1144,7 @@ defmodule Module.Types.Apply do
|
||||
|
||||
defp remote_apply(Map, :get, _info, [map, key] = args_types, stack) do
|
||||
case map_get(map, key) do
|
||||
{:ok, value} -> {:ok, return(union(value, @nil_atom), args_types, stack)}
|
||||
{:ok, value} -> {:ok, return(opt_union(value, @nil_atom), args_types, stack)}
|
||||
:badmap -> {:error, badremote(Map, :get, args_types)}
|
||||
:error -> {:error, {:badkeydomain, map, key, @nil_atom}}
|
||||
end
|
||||
@@ -1139,7 +1152,7 @@ defmodule Module.Types.Apply do
|
||||
|
||||
defp remote_apply(Map, :get, _info, [map, key, default] = args_types, stack) do
|
||||
case map_get(map, key) do
|
||||
{:ok, value} -> {:ok, return(union(value, default), args_types, stack)}
|
||||
{:ok, value} -> {:ok, return(opt_union(value, default), args_types, stack)}
|
||||
:badmap -> {:error, badremote(Map, :get, args_types)}
|
||||
:error -> {:error, {:badkeydomain, map, key, default}}
|
||||
end
|
||||
@@ -1149,7 +1162,7 @@ defmodule Module.Types.Apply do
|
||||
case fun_apply(fun, []) do
|
||||
{:ok, default} ->
|
||||
case map_get(map, key) do
|
||||
{:ok, value} -> {:ok, return(union(value, default), args_types, stack)}
|
||||
{:ok, value} -> {:ok, return(opt_union(value, default), args_types, stack)}
|
||||
:badmap -> {:error, badremote(Map, :get_lazy, args_types)}
|
||||
:error -> {:error, {:badkeydomain, map, key, default}}
|
||||
end
|
||||
@@ -1179,7 +1192,7 @@ defmodule Module.Types.Apply do
|
||||
|
||||
case map_update(map, key, not_set(), true, false) do
|
||||
{value, descr, _errors} ->
|
||||
value = union(value, default)
|
||||
value = opt_union(value, default)
|
||||
{:ok, return(tuple([value, descr]), args_types, stack)}
|
||||
|
||||
:badmap ->
|
||||
@@ -1195,7 +1208,7 @@ defmodule Module.Types.Apply do
|
||||
{:ok, default} ->
|
||||
case map_update(map, key, not_set(), true, false) do
|
||||
{value, descr, _errors} ->
|
||||
value = union(value, default)
|
||||
value = opt_union(value, default)
|
||||
{:ok, return(tuple([value, descr]), args_types, stack)}
|
||||
|
||||
:badmap ->
|
||||
@@ -1269,7 +1282,7 @@ defmodule Module.Types.Apply do
|
||||
default
|
||||
else
|
||||
case fun_apply(fun, [arg_type]) do
|
||||
{:ok, res} -> if optional?, do: union(res, default), else: res
|
||||
{:ok, res} -> if optional?, do: opt_union(res, default), else: res
|
||||
reason -> throw({:badapply, reason, [arg_type]})
|
||||
end
|
||||
end
|
||||
@@ -1293,7 +1306,7 @@ defmodule Module.Types.Apply do
|
||||
defp remote_apply(:maps, :find, _info, [key, map] = args_types, stack) do
|
||||
case map_get(map, key) do
|
||||
{_, value} ->
|
||||
result = tuple([atom([:ok]), value]) |> union(atom([:error]))
|
||||
result = tuple([atom([:ok]), value]) |> opt_union(atom([:error]))
|
||||
{:ok, return(result, args_types, stack)}
|
||||
|
||||
:badmap ->
|
||||
@@ -1327,7 +1340,7 @@ defmodule Module.Types.Apply do
|
||||
map_and_maybe_empty_map =
|
||||
case empty_list? do
|
||||
true -> map
|
||||
false -> difference(map, empty_map())
|
||||
false -> opt_difference(map, empty_map())
|
||||
end
|
||||
|
||||
{:ok, return(map_and_maybe_empty_map, args_types, stack)}
|
||||
@@ -1372,7 +1385,7 @@ defmodule Module.Types.Apply do
|
||||
defp remote_apply(:maps, :take, _info, [key, map] = args_types, stack) do
|
||||
case map_update(map, key, not_set(), true, false) do
|
||||
{value, descr, _errors} ->
|
||||
result = union(tuple([value, descr]), atom([:error]))
|
||||
result = opt_union(tuple([value, descr]), atom([:error]))
|
||||
{:ok, return(result, args_types, stack)}
|
||||
|
||||
:badmap ->
|
||||
@@ -1403,7 +1416,22 @@ defmodule Module.Types.Apply do
|
||||
defp remote_apply(:maps, :values, _info, [map], stack) do
|
||||
case map_to_list(map, fn _key, value -> value end) do
|
||||
{:ok, list_type} -> {:ok, return(list_type, [map], stack)}
|
||||
:badmap -> {:error, badremote(:maps, :keys, [map])}
|
||||
:badmap -> {:error, badremote(:maps, :values, [map])}
|
||||
end
|
||||
end
|
||||
|
||||
defp remote_apply(mod, :to_existing_atom, info, [_string, list] = args_types, stack)
|
||||
when mod in [String, List] do
|
||||
# TODO remove once we add parametric types, this will just be:
|
||||
# binary(), non_empty_list(a) -> a when a: atom()
|
||||
|
||||
case remote_apply(info, args_types, stack) do
|
||||
{:ok, _} ->
|
||||
{false, refined_atom} = list_of(list)
|
||||
{:ok, refined_atom}
|
||||
|
||||
other ->
|
||||
other
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1446,10 +1474,10 @@ defmodule Module.Types.Apply do
|
||||
Enum.reduce(modules, {none(), false, context}, fn module, {type, fallback?, context} ->
|
||||
case signature(module, fun, arity, meta, stack, context) do
|
||||
{{:strong, _, clauses}, context} ->
|
||||
{union(type, fun_from_non_overlapping_clauses(clauses)), fallback?, context}
|
||||
{opt_union(type, fun_from_non_overlapping_clauses(clauses)), fallback?, context}
|
||||
|
||||
{{:infer, _, clauses}, context} when length(clauses) <= @max_clauses ->
|
||||
{union(type, fun_from_inferred_clauses(clauses)), fallback?, context}
|
||||
{opt_union(type, fun_from_inferred_clauses(clauses)), fallback?, context}
|
||||
|
||||
{_, context} ->
|
||||
{type, true, context}
|
||||
@@ -1738,7 +1766,7 @@ defmodule Module.Types.Apply do
|
||||
|
||||
defp map_put_new(map, key, value, name, args_types, stack) do
|
||||
fun = fn
|
||||
true, type -> union(type, value)
|
||||
true, type -> opt_union(type, value)
|
||||
false, type -> if empty?(type), do: value, else: type
|
||||
end
|
||||
|
||||
@@ -1794,7 +1822,7 @@ defmodule Module.Types.Apply do
|
||||
case filter_domain(clauses, expected, [], true) do
|
||||
:none -> domain(domain, clauses)
|
||||
:all -> domain(domain, clauses)
|
||||
args -> Enum.zip_with(args, fn types -> Enum.reduce(types, &union/2) end)
|
||||
args -> Enum.zip_with(args, fn types -> Enum.reduce(types, &opt_union/2) end)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1818,7 +1846,7 @@ defmodule Module.Types.Apply do
|
||||
{used, dynamic()}
|
||||
|
||||
{_count, used, returns} ->
|
||||
{used, returns |> Enum.reduce(&union/2) |> dynamic()}
|
||||
{used, returns |> Enum.reduce(&opt_union/2) |> dynamic()}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1835,7 +1863,7 @@ defmodule Module.Types.Apply do
|
||||
# as a non disjoint check. So we skip checking compatibility twice.
|
||||
with true <- zip_compatible_or_only_gradual?(args_types, domain),
|
||||
{count, used, returns} when count > 0 <- apply_clauses(clauses, args_types, 0, 0, [], []) do
|
||||
{used, returns |> Enum.reduce(&union/2) |> return(args_types, stack)}
|
||||
{used, returns |> Enum.reduce(&opt_union/2) |> return(args_types, stack)}
|
||||
else
|
||||
_ -> :error
|
||||
end
|
||||
@@ -2180,7 +2208,7 @@ defmodule Module.Types.Apply do
|
||||
clauses =
|
||||
case mod.__protocol__(:impls) do
|
||||
{:consolidated, mods} ->
|
||||
domain = mods |> Enum.map(&Module.Types.Of.impl/1) |> Enum.reduce(&union/2)
|
||||
domain = mods |> Enum.map(&Module.Types.Of.impl/1) |> Enum.reduce(&opt_union/2)
|
||||
[{[domain], dynamic()}]
|
||||
|
||||
_ ->
|
||||
@@ -2390,10 +2418,10 @@ defmodule Module.Types.Apply do
|
||||
term_type?(actual) do
|
||||
actual |> to_quoted() |> Code.Formatter.to_algebra()
|
||||
else
|
||||
common = intersection(actual, expected)
|
||||
common = opt_intersection(actual, expected)
|
||||
|
||||
uncommon_doc =
|
||||
difference(actual, expected)
|
||||
opt_difference(actual, expected)
|
||||
|> to_quoted()
|
||||
|> Code.Formatter.to_algebra()
|
||||
|> ansi_red()
|
||||
@@ -2411,17 +2439,17 @@ defmodule Module.Types.Apply do
|
||||
end
|
||||
|
||||
@composite_types non_empty_list(term(), term())
|
||||
|> union(tuple())
|
||||
|> union(open_map())
|
||||
|> union(fun())
|
||||
|> opt_union(tuple())
|
||||
|> opt_union(open_map())
|
||||
|> opt_union(fun())
|
||||
|
||||
# If actual/expected have a composite type, computing the
|
||||
# `intersection(actual, expected) or difference(actual, expected)`
|
||||
# `opt_intersection(actual, expected) or opt_difference(actual, expected)`
|
||||
# can lead to an explosion of terms that actually make debugging
|
||||
# harder. So we check that at least one of the two operations
|
||||
# return none() (i.e. actual is a subtype or they are disjoint).
|
||||
defp has_simple_difference?(actual, expected) do
|
||||
composite_types = intersection(actual, @composite_types)
|
||||
composite_types = opt_intersection(actual, @composite_types)
|
||||
subtype?(composite_types, expected) or disjoint?(composite_types, expected)
|
||||
end
|
||||
|
||||
|
||||
+1312
-1044
File diff suppressed because it is too large
Load Diff
@@ -22,9 +22,9 @@ defmodule Module.Types.Expr do
|
||||
context: atom([:match, :guard, nil]),
|
||||
context_modules: list_of_modules,
|
||||
file: binary(),
|
||||
function: union(tuple([atom(), integer()]), atom([nil])),
|
||||
function: opt_union(tuple([atom(), integer()]), atom([nil])),
|
||||
functions: functions_and_macros,
|
||||
lexical_tracker: union(pid(), atom([nil])),
|
||||
lexical_tracker: opt_union(pid(), atom([nil])),
|
||||
line: integer(),
|
||||
macro_aliases: aliases,
|
||||
macros: functions_and_macros,
|
||||
@@ -38,24 +38,24 @@ defmodule Module.Types.Expr do
|
||||
# then we will assume you need to statically handle all possible exceptions.
|
||||
@exception open_map(__struct__: atom(), __exception__: term())
|
||||
|
||||
args_or_arity = union(list(term()), integer())
|
||||
args_or_arity = opt_union(list(term()), integer())
|
||||
|
||||
extra_info =
|
||||
list(
|
||||
tuple([atom([:file]), list(integer())])
|
||||
|> union(tuple([atom([:line]), integer()]))
|
||||
|> union(tuple([atom([:error_info]), open_map()]))
|
||||
|> opt_union(tuple([atom([:line]), integer()]))
|
||||
|> opt_union(tuple([atom([:error_info]), open_map()]))
|
||||
)
|
||||
|
||||
@stacktrace list(
|
||||
union(
|
||||
opt_union(
|
||||
tuple([atom(), atom(), args_or_arity, extra_info]),
|
||||
tuple([fun(), args_or_arity, extra_info])
|
||||
)
|
||||
)
|
||||
|
||||
@falsy atom([false, nil])
|
||||
@truthy negation(atom([false, nil]))
|
||||
@truthy Module.Types.Descr.opt_negation(atom([false, nil]))
|
||||
|
||||
# :atom
|
||||
def of_expr(atom, _expected, _expr, _stack, context) when is_atom(atom),
|
||||
@@ -106,7 +106,7 @@ defmodule Module.Types.Expr do
|
||||
of_expr(suffix, tl_type, expr, stack, context)
|
||||
end
|
||||
|
||||
{non_empty_list(Enum.reduce(prefix, &union/2), suffix), context}
|
||||
{non_empty_list(Enum.reduce(prefix, &opt_union/2), suffix), context}
|
||||
end
|
||||
|
||||
# {left, right}
|
||||
@@ -150,13 +150,13 @@ defmodule Module.Types.Expr do
|
||||
fn pattern_type, context ->
|
||||
# See if we can use the expected type to further refine the pattern type,
|
||||
# if we cannot, use the pattern type as that will fail later on.
|
||||
type = intersection(pattern_type, expected)
|
||||
type = opt_intersection(pattern_type, expected)
|
||||
type = if empty?(type), do: pattern_type, else: type
|
||||
{result, context} = of_expr(right_expr, type, expr, stack, context)
|
||||
|
||||
# The function may still return a too broad type,
|
||||
# so we refine once again to assign the most appropriate to the pattern.
|
||||
{intersection(result, expected), context}
|
||||
{opt_intersection(result, expected), context}
|
||||
end
|
||||
|
||||
Pattern.of_match(left_expr, type_fun, match, meta, stack, context)
|
||||
@@ -187,7 +187,7 @@ defmodule Module.Types.Expr do
|
||||
end
|
||||
end)
|
||||
|
||||
expected = intersection(expected, open_map(expected_pairs))
|
||||
expected = opt_intersection(expected, open_map(expected_pairs))
|
||||
{map_type, context} = of_expr(map, expected, expr, stack, context)
|
||||
|
||||
try do
|
||||
@@ -311,7 +311,7 @@ defmodule Module.Types.Expr do
|
||||
maybe_always_or_never_match_cond(pos_head_type, pos_head, pos_meta, stack, context, false)
|
||||
|
||||
{_, truthy_context} =
|
||||
of_expr(pos_head, @truthy, pos_head, %{stack | reverse_arrow: :use}, context)
|
||||
of_expr(pos_head, @truthy, pos_head, %{stack | reverse_arrow: :except_none}, context)
|
||||
|
||||
# Keep the context except the warnings, and compute the body
|
||||
truthy_context = reset_warnings(truthy_context, context)
|
||||
@@ -323,14 +323,14 @@ defmodule Module.Types.Expr do
|
||||
context = Of.reset_vars(pos_body_context, context)
|
||||
|
||||
{_, falsy_context} =
|
||||
of_expr(pos_head, @falsy, pos_head, %{stack | reverse_arrow: :use}, context)
|
||||
of_expr(pos_head, @falsy, pos_head, %{stack | reverse_arrow: :except_none}, context)
|
||||
|
||||
falsy_context = reset_warnings(falsy_context, context)
|
||||
|
||||
{neg_body_type, neg_body_context} =
|
||||
of_expr(neg_body, expected, expr, stack, falsy_context)
|
||||
|
||||
body_type = union(pos_body_type, neg_body_type)
|
||||
body_type = opt_union(pos_body_type, neg_body_type)
|
||||
|
||||
context =
|
||||
cond do
|
||||
@@ -355,7 +355,7 @@ defmodule Module.Types.Expr do
|
||||
maybe_always_or_never_match_cond(head_type, head, meta, stack, context, last?)
|
||||
|
||||
{_, truthy_context} =
|
||||
of_expr(head, @truthy, head, %{stack | reverse_arrow: :use}, context)
|
||||
of_expr(head, @truthy, head, %{stack | reverse_arrow: :except_none}, context)
|
||||
|
||||
# Keep the context except the warnings, and compute the body
|
||||
truthy_context = reset_warnings(truthy_context, context)
|
||||
@@ -369,12 +369,12 @@ defmodule Module.Types.Expr do
|
||||
context
|
||||
else
|
||||
{_, falsy_context} =
|
||||
of_expr(head, @falsy, head, %{stack | reverse_arrow: :use}, context)
|
||||
of_expr(head, @falsy, head, %{stack | reverse_arrow: :except_none}, context)
|
||||
|
||||
reset_warnings(falsy_context, context)
|
||||
end
|
||||
|
||||
{union(body_type, acc), context}
|
||||
{opt_union(body_type, acc), context}
|
||||
end)
|
||||
|
||||
dynamic_unless_static({body_type, Of.reset_vars(acc_context, context)}, stack)
|
||||
@@ -382,7 +382,7 @@ defmodule Module.Types.Expr do
|
||||
end
|
||||
|
||||
def of_expr({:case, meta, [case_expr, [do: _clauses]]}, expected, _expr, stack, context)
|
||||
when stack.reverse_arrow == :use do
|
||||
when stack.reverse_arrow in [:except_none, :include_none] do
|
||||
version = Keyword.fetch!(meta, :version)
|
||||
clauses = Map.fetch!(context.reverse_arrows, version)
|
||||
original = context
|
||||
@@ -402,7 +402,7 @@ defmodule Module.Types.Expr do
|
||||
context
|
||||
|
||||
# If there is a single clause, we assume it is always evaluated
|
||||
# by doing reverse arrows and incorporting all variables into the context.
|
||||
# by doing reverse arrows and incorporating all variables into the context.
|
||||
# We have to evaluate the head again but it might have been preferred
|
||||
# if we could somehow merge a previously computed context.
|
||||
{_, [{arg_type, _body_type, {:->, meta, [head, body]}}]} ->
|
||||
@@ -419,7 +419,7 @@ defmodule Module.Types.Expr do
|
||||
reset_warnings(context, original)
|
||||
|
||||
{_, filtered} ->
|
||||
case_expected = Enum.reduce(filtered, none(), &union(elem(&1, 0), &2))
|
||||
case_expected = Enum.reduce(filtered, none(), &opt_union(elem(&1, 0), &2))
|
||||
{_, context} = of_expr(case_expr, case_expected, case_expr, stack, context)
|
||||
reset_warnings(context, original)
|
||||
end
|
||||
@@ -461,7 +461,13 @@ defmodule Module.Types.Expr do
|
||||
|
||||
# Now we refine the case_expr context and use it to compute the body
|
||||
{_, refined_context} =
|
||||
of_expr(case_expr, arg_type, case_expr, %{stack | reverse_arrow: :use}, context)
|
||||
of_expr(
|
||||
case_expr,
|
||||
arg_type,
|
||||
case_expr,
|
||||
%{stack | reverse_arrow: :except_none},
|
||||
context
|
||||
)
|
||||
|
||||
{body_type, context} =
|
||||
of_expr(body, expected, body, stack, reset_warnings(refined_context, context))
|
||||
@@ -474,16 +480,22 @@ defmodule Module.Types.Expr do
|
||||
else
|
||||
[arg_type] = Pattern.of_domain(trees, stack, context)
|
||||
clauses_acc = [{arg_type, body_type, clause} | clauses_acc]
|
||||
{{none?, union(body_type, body_acc), clauses_acc}, context}
|
||||
{{none?, opt_union(body_type, body_acc), clauses_acc}, context}
|
||||
end
|
||||
end)
|
||||
|
||||
context =
|
||||
if none? or stack.mode != :static do
|
||||
head_type = Enum.reduce(clauses_acc, none(), &union(elem(&1, 0), &2))
|
||||
head_type = Enum.reduce(clauses_acc, none(), &opt_union(elem(&1, 0), &2))
|
||||
|
||||
{_, refined_context} =
|
||||
of_expr(case_expr, head_type, case_expr, %{stack | reverse_arrow: :use}, context)
|
||||
of_expr(
|
||||
case_expr,
|
||||
head_type,
|
||||
case_expr,
|
||||
%{stack | reverse_arrow: :except_none},
|
||||
context
|
||||
)
|
||||
|
||||
reset_warnings(refined_context, context)
|
||||
else
|
||||
@@ -546,7 +558,7 @@ defmodule Module.Types.Expr do
|
||||
end
|
||||
|
||||
{type, context} = of_expr(body, expected, body, stack, context)
|
||||
{union(type, acc), context |> set_failed(failed?) |> Of.reset_vars(original)}
|
||||
{opt_union(type, acc), context |> set_failed(failed?) |> Of.reset_vars(original)}
|
||||
end)
|
||||
|
||||
{:catch, clauses}, {acc, context} ->
|
||||
@@ -564,7 +576,7 @@ defmodule Module.Types.Expr do
|
||||
end)
|
||||
end
|
||||
|
||||
@timeout_type union(integer(), atom([:infinity]))
|
||||
@timeout_type opt_union(integer(), atom([:infinity]))
|
||||
|
||||
def of_expr({:receive, meta, [blocks]}, expected, expr, stack, original) do
|
||||
cache_result(meta, stack, original, fn ->
|
||||
@@ -581,10 +593,11 @@ defmodule Module.Types.Expr do
|
||||
{body_type, context} = of_expr(body, expected, expr, stack, context)
|
||||
|
||||
if compatible?(timeout_type, @timeout_type) do
|
||||
{union(body_type, acc), Of.reset_vars(context, original)}
|
||||
{opt_union(body_type, acc), Of.reset_vars(context, original)}
|
||||
else
|
||||
error = {:badtimeout, timeout_type, timeout, context}
|
||||
{union(body_type, acc), error(__MODULE__, error, meta, stack, context)}
|
||||
context = error(__MODULE__, error, meta, stack, context)
|
||||
{opt_union(body_type, acc), Of.reset_vars(context, original)}
|
||||
end
|
||||
end)
|
||||
|> dynamic_unless_static(stack)
|
||||
@@ -613,13 +626,13 @@ defmodule Module.Types.Expr do
|
||||
case into_kind do
|
||||
:bitstring ->
|
||||
{block_type, context} = of_expr(block, bitstring(), block, stack, context)
|
||||
intersection = intersection(block_type, bitstring())
|
||||
intersection = opt_intersection(block_type, bitstring())
|
||||
|
||||
if empty?(intersection) do
|
||||
error = {:badbitbody, block_type, block, context}
|
||||
{error_type(), error(__MODULE__, error, meta, stack, context)}
|
||||
else
|
||||
{union(into_type, intersection), context}
|
||||
{opt_union(into_type, intersection), context}
|
||||
end
|
||||
|
||||
:non_empty_list ->
|
||||
@@ -630,7 +643,7 @@ defmodule Module.Types.Expr do
|
||||
end
|
||||
|
||||
{block_type, context} = of_expr(block, expected, block, stack, context)
|
||||
{union(into_type, non_empty_list(block_type)), context}
|
||||
{opt_union(into_type, non_empty_list(block_type)), context}
|
||||
|
||||
:none ->
|
||||
{_, context} = of_expr(block, term(), block, stack, context)
|
||||
@@ -641,7 +654,6 @@ defmodule Module.Types.Expr do
|
||||
end)
|
||||
end
|
||||
|
||||
# TODO: with pat <- expr do expr end
|
||||
def of_expr({:with, meta, [_ | _] = clauses}, expected, _expr, stack, original) do
|
||||
cache_result(meta, stack, original, fn ->
|
||||
{clauses, [[do: do_block] ++ options]} = Enum.split(clauses, -1)
|
||||
@@ -662,7 +674,7 @@ defmodule Module.Types.Expr do
|
||||
{else_types, context}
|
||||
end
|
||||
|
||||
dynamic_unless_static({union(do_result, else_result), context}, stack)
|
||||
dynamic_unless_static({opt_union(do_result, else_result), context}, stack)
|
||||
end)
|
||||
end
|
||||
|
||||
@@ -775,7 +787,7 @@ defmodule Module.Types.Expr do
|
||||
context
|
||||
|
||||
_ ->
|
||||
expected = if structs == [], do: @exception, else: Enum.reduce(structs, &union/2)
|
||||
expected = if structs == [], do: @exception, else: Enum.reduce(structs, &opt_union/2)
|
||||
expr = {:__block__, [type_check: info], [expr]}
|
||||
context = Of.declare_var(var, context)
|
||||
{_ok?, _type, context} = Of.refine_head_var(var, expected, expr, stack, context)
|
||||
@@ -819,7 +831,7 @@ defmodule Module.Types.Expr do
|
||||
context
|
||||
end
|
||||
|
||||
@into_compile union(bitstring(), empty_list())
|
||||
@into_compile opt_union(bitstring(), empty_list())
|
||||
|
||||
defp for_into([], _meta, _stack, context),
|
||||
do: {empty_list(), :non_empty_list, context}
|
||||
@@ -848,7 +860,7 @@ defmodule Module.Types.Expr do
|
||||
# If they can be both be true, then we don't know
|
||||
# what the contents of the block are for
|
||||
{true, true} ->
|
||||
type = union(bitstring(), list(term()))
|
||||
type = opt_union(bitstring(), list(term()))
|
||||
{if(gradual?(type), do: dynamic(type), else: type), :none, context}
|
||||
|
||||
{false, true} ->
|
||||
@@ -879,10 +891,10 @@ defmodule Module.Types.Expr do
|
||||
[pattern_type] = Pattern.of_domain(trees, stack, context)
|
||||
|
||||
{_, refined_context} =
|
||||
of_expr(right, pattern_type, right, %{stack | reverse_arrow: :use}, context)
|
||||
of_expr(right, pattern_type, right, %{stack | reverse_arrow: :except_none}, context)
|
||||
|
||||
else_type = if precise?, do: difference(type, pattern_type), else: type
|
||||
{union(else_type, else_types), reset_warnings(refined_context, context)}
|
||||
else_type = if precise?, do: opt_difference(type, pattern_type), else: type
|
||||
{opt_union(else_type, else_types), reset_warnings(refined_context, context)}
|
||||
end
|
||||
|
||||
defp with_clause(expr, stack, {else_types, context}) do
|
||||
@@ -906,7 +918,7 @@ defmodule Module.Types.Expr do
|
||||
Of.with_conditional_vars(mods, none(), call, stack, context, fn mod, acc, context ->
|
||||
expr = {remote, [type_check: {:invoked_as, mod, fun, length(args)}] ++ meta, args}
|
||||
{type, context} = Apply.remote(mod, fun, args, expected, expr, stack, context, &of_expr/5)
|
||||
{union(acc, type), context}
|
||||
{opt_union(acc, type), context}
|
||||
end)
|
||||
end
|
||||
|
||||
@@ -949,7 +961,7 @@ defmodule Module.Types.Expr do
|
||||
context = put_in(context.reverse_arrows[version], result)
|
||||
{result, context}
|
||||
|
||||
:use ->
|
||||
_ ->
|
||||
version = Keyword.fetch!(meta, :version)
|
||||
{Map.fetch!(context.reverse_arrows, version), context}
|
||||
end
|
||||
@@ -970,7 +982,7 @@ defmodule Module.Types.Expr do
|
||||
defp of_clauses(clauses, domain, expected, base_info, stack, context, acc) do
|
||||
of_acc = fn _args_types, _precise?, {:->, _, [_, body]}, context, acc ->
|
||||
{body_type, context} = of_expr(body, expected, body, stack, context)
|
||||
{union(acc, body_type), context}
|
||||
{opt_union(acc, body_type), context}
|
||||
end
|
||||
|
||||
of_clauses_fun(clauses, domain, base_info, stack, context, acc, of_acc)
|
||||
@@ -1025,7 +1037,7 @@ defmodule Module.Types.Expr do
|
||||
do: {left_expr, right_expr}
|
||||
|
||||
defp add_inferred([{args, existing_return} | tail], args, return),
|
||||
do: [{args, union(existing_return, return)} | tail]
|
||||
do: [{args, opt_union(existing_return, return)} | tail]
|
||||
|
||||
defp add_inferred([head | tail], args, return),
|
||||
do: [head | add_inferred(tail, args, return)]
|
||||
|
||||
@@ -57,7 +57,7 @@ defmodule Module.Types.Helpers do
|
||||
## Warnings
|
||||
|
||||
@doc """
|
||||
Converts an itneger into ordinal.
|
||||
Converts an integer into ordinal.
|
||||
"""
|
||||
def integer_to_ordinal(i) do
|
||||
case rem(i, 10) do
|
||||
@@ -329,7 +329,7 @@ defmodule Module.Types.Helpers do
|
||||
end
|
||||
end
|
||||
|
||||
{{:., _, [:lists, :member]}, meta, [expr, [_ | _] = args]} = call ->
|
||||
{{:., _, [:lists, :member]}, meta, [expr, args]} = call when is_list(args) ->
|
||||
if Enum.any?(args, &match?({:|, _, [_, _]}, &1)) do
|
||||
call
|
||||
else
|
||||
|
||||
@@ -11,7 +11,7 @@ defmodule Module.Types.Of do
|
||||
@prefix quote(do: ...)
|
||||
@suffix quote(do: ...)
|
||||
|
||||
@integer_or_float union(integer(), float())
|
||||
@integer_or_float opt_union(integer(), float())
|
||||
@integer integer()
|
||||
@float float()
|
||||
@binary binary()
|
||||
@@ -31,7 +31,7 @@ defmodule Module.Types.Of do
|
||||
@doc """
|
||||
Marks a variable with error.
|
||||
|
||||
This purposedly deletes all traces of the variable,
|
||||
This purposely deletes all traces of the variable,
|
||||
as it is often invoked when the cause for error is elsewhere.
|
||||
"""
|
||||
def error_var({_, meta, _}, context) do
|
||||
@@ -106,15 +106,15 @@ defmodule Module.Types.Of do
|
||||
context
|
||||
end
|
||||
|
||||
case context do
|
||||
case stack do
|
||||
_ when type in @skip_refinement_for or is_map_key(data, :errored) ->
|
||||
{old_type, context}
|
||||
|
||||
%{pattern_info: %{guard_context: guard_context}} ->
|
||||
new_type = intersection(old_type, type)
|
||||
%{reverse_arrow: reverse_arrow} when reverse_arrow in [:except_none, :include_none] ->
|
||||
new_type = opt_intersection(old_type, type)
|
||||
|
||||
case empty?(new_type) do
|
||||
true when guard_context == :orelse ->
|
||||
true when reverse_arrow == :include_none ->
|
||||
data = %{
|
||||
data
|
||||
| type: none(),
|
||||
@@ -171,7 +171,7 @@ defmodule Module.Types.Of do
|
||||
{:ok, error_type(), context}
|
||||
|
||||
%{^version => %{type: old_type, off_traces: off_traces} = data} = vars ->
|
||||
new_type = intersection(type, old_type)
|
||||
new_type = opt_intersection(type, old_type)
|
||||
|
||||
data = %{
|
||||
data
|
||||
@@ -245,7 +245,7 @@ defmodule Module.Types.Of do
|
||||
type =
|
||||
Enum.reduce(vars_conds, type, fn {vars, _cond}, acc ->
|
||||
%{^version => %{type: type}} = vars
|
||||
union(acc, type)
|
||||
opt_union(acc, type)
|
||||
end)
|
||||
|
||||
{_, context} = refine_body_var(version, type, expr, stack, context)
|
||||
@@ -264,8 +264,8 @@ defmodule Module.Types.Of do
|
||||
{Float, float()},
|
||||
{Function, fun()},
|
||||
{Integer, integer()},
|
||||
{List, union(empty_list(), non_empty_list(term(), term()))},
|
||||
{Map, open_map(__struct__: if_set(negation(atom())))},
|
||||
{List, opt_union(empty_list(), non_empty_list(term(), term()))},
|
||||
{Map, open_map(__struct__: if_set(Module.Types.Descr.opt_negation(atom())))},
|
||||
{Port, port()},
|
||||
{PID, pid()},
|
||||
{Reference, reference()},
|
||||
@@ -384,15 +384,18 @@ defmodule Module.Types.Of do
|
||||
for key <- keys, t <- cartesian_map(tail) do
|
||||
closed_map(non_multiple ++ [{key, type} | t])
|
||||
end
|
||||
|> Enum.reduce(&union/2)
|
||||
|> Enum.reduce(&opt_union/2)
|
||||
end
|
||||
|
||||
{if(dynamic?, do: dynamic(map), else: map), context}
|
||||
end
|
||||
|
||||
defp union_negated([], new_type, single, multiple) do
|
||||
single = Enum.map(single, fn {key, old_type} -> {key, union(old_type, new_type)} end)
|
||||
multiple = Enum.map(multiple, fn {keys, old_type} -> {keys, union(old_type, new_type)} end)
|
||||
single = Enum.map(single, fn {key, old_type} -> {key, opt_union(old_type, new_type)} end)
|
||||
|
||||
multiple =
|
||||
Enum.map(multiple, fn {keys, old_type} -> {keys, opt_union(old_type, new_type)} end)
|
||||
|
||||
{single, multiple}
|
||||
end
|
||||
|
||||
@@ -402,13 +405,13 @@ defmodule Module.Types.Of do
|
||||
if key in negated do
|
||||
{{key, old_type}, [key | matched]}
|
||||
else
|
||||
{{key, union(old_type, new_type)}, matched}
|
||||
{{key, opt_union(old_type, new_type)}, matched}
|
||||
end
|
||||
end)
|
||||
|
||||
multiple =
|
||||
Enum.map(multiple, fn {keys, old_type} ->
|
||||
{keys, union(old_type, new_type)}
|
||||
{keys, opt_union(old_type, new_type)}
|
||||
end)
|
||||
|
||||
{Enum.map(negated -- matched, fn key -> {key, not_set()} end) ++ single, multiple}
|
||||
@@ -467,32 +470,32 @@ defmodule Module.Types.Of do
|
||||
# TODO: Type check the fields match the struct
|
||||
def struct_instance(struct, args, expected, meta, stack, context, of_fun)
|
||||
when is_atom(struct) do
|
||||
{info, context} = struct_info(struct, :expr, meta, stack, context)
|
||||
{info, context} = struct_info(struct, :expr, meta, stack, context, true)
|
||||
|
||||
if is_nil(info) do
|
||||
raise "expected #{inspect(struct)} to return struct metadata, but got none"
|
||||
{dynamic(), context}
|
||||
else
|
||||
# The compiler has already checked the keys are atoms and which ones are required.
|
||||
{args_types, context} =
|
||||
Enum.map_reduce(args, context, fn {key, value}, context when is_atom(key) ->
|
||||
value_type =
|
||||
case map_fetch_key(expected, key) do
|
||||
{_, expected_value_type} -> expected_value_type
|
||||
_ -> term()
|
||||
end
|
||||
|
||||
{type, context} = of_fun.(value, value_type, stack, context)
|
||||
{{key, type}, context}
|
||||
end)
|
||||
|
||||
{closed_map([{:__struct__, atom([struct])} | args_types]), context}
|
||||
end
|
||||
|
||||
# The compiler has already checked the keys are atoms and which ones are required.
|
||||
{args_types, context} =
|
||||
Enum.map_reduce(args, context, fn {key, value}, context when is_atom(key) ->
|
||||
value_type =
|
||||
case map_fetch_key(expected, key) do
|
||||
{_, expected_value_type} -> expected_value_type
|
||||
_ -> term()
|
||||
end
|
||||
|
||||
{type, context} = of_fun.(value, value_type, stack, context)
|
||||
{{key, type}, context}
|
||||
end)
|
||||
|
||||
{closed_map([{:__struct__, atom([struct])} | args_types]), context}
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns `__info__(:struct)` information about a struct.
|
||||
"""
|
||||
def struct_info(struct, kind, meta, stack, context) do
|
||||
def struct_info(struct, kind, meta, stack, context, must_exist? \\ false) do
|
||||
case stack.no_warn_undefined do
|
||||
%Macro.Env{} = env ->
|
||||
case :elixir_map.maybe_load_struct_info(meta, struct, :soft, env) do
|
||||
@@ -511,7 +514,7 @@ defmodule Module.Types.Of do
|
||||
|
||||
{info, context}
|
||||
else
|
||||
error = {:unknown_struct, kind, struct}
|
||||
error = {:unknown_struct, kind, struct, must_exist?}
|
||||
{nil, error(error, meta, stack, context)}
|
||||
end
|
||||
end
|
||||
@@ -848,19 +851,26 @@ defmodule Module.Types.Of do
|
||||
}
|
||||
end
|
||||
|
||||
def format_diagnostic({:unknown_struct, kind, module}) do
|
||||
message =
|
||||
if Code.ensure_loaded?(module) do
|
||||
"struct #{inspect(module)} is undefined (there is such module but it does not define a struct)"
|
||||
else
|
||||
"struct #{inspect(module)} is undefined " <>
|
||||
"(module #{inspect(module)} is not available or is yet to be defined)"
|
||||
def format_diagnostic({:unknown_struct, kind, module, must_exist?}) do
|
||||
detail =
|
||||
case {Code.ensure_loaded?(module), must_exist?} do
|
||||
{true, false} ->
|
||||
"there is such module but it does not define a struct"
|
||||
|
||||
{false, false} ->
|
||||
"module #{inspect(module)} is not available or is yet to be defined"
|
||||
|
||||
{true, true} ->
|
||||
"the module may have been redefined as it no longer defines a struct"
|
||||
|
||||
{false, true} ->
|
||||
"the module was also only available but may have been removed during compilation"
|
||||
end
|
||||
|
||||
%{
|
||||
message: message,
|
||||
message: "struct #{inspect(module)} is undefined (#{detail})",
|
||||
group: true,
|
||||
severity: if(kind == :pattern, do: :error, else: :warning)
|
||||
severity: if(kind == :pattern or must_exist?, do: :error, else: :warning)
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ defmodule Module.Types.Pattern do
|
||||
do: previous
|
||||
|
||||
defp concat_previous(types, {list, descr}),
|
||||
do: {[types | list], union(descr, args_to_previous(types))}
|
||||
do: {[types | list], opt_union(descr, args_to_previous(types))}
|
||||
|
||||
defp args_to_previous([type]), do: upper_bound(type)
|
||||
defp args_to_previous(types), do: args_to_static_domain(types)
|
||||
@@ -39,8 +39,8 @@ defmodule Module.Types.Pattern do
|
||||
defp of_pattern_previous(types, {_, descr}, stack) do
|
||||
refined_types =
|
||||
case types do
|
||||
[type] -> [difference(type, descr)]
|
||||
[_ | _] -> args_to_domain(types) |> difference(descr) |> domain_to_flat_args(types)
|
||||
[type] -> [opt_difference(type, descr)]
|
||||
[_ | _] -> args_to_domain(types) |> opt_difference(descr) |> domain_to_flat_args(types)
|
||||
end
|
||||
|
||||
# check_previous? is an optimization. If types have not changed,
|
||||
@@ -349,7 +349,7 @@ defmodule Module.Types.Pattern do
|
||||
defp of_pattern_intersect([head | tail], index, acc, pattern_info, tag, stack, context) do
|
||||
{tree, expected, pattern} = head
|
||||
actual = of_pattern_tree(tree, stack, context)
|
||||
type = intersection(actual, expected)
|
||||
type = opt_intersection(actual, expected)
|
||||
|
||||
if empty?(type) do
|
||||
context = badpattern_error(pattern, index, tag, stack, context)
|
||||
@@ -498,7 +498,7 @@ defmodule Module.Types.Pattern do
|
||||
|
||||
defp of_pattern_var([{:subpattern, key} | rest], type, context) do
|
||||
%{^key => subpattern} = context.subpatterns
|
||||
of_pattern_var(rest, intersection(type, subpattern), context)
|
||||
of_pattern_var(rest, opt_intersection(type, subpattern), context)
|
||||
end
|
||||
|
||||
defp of_pattern_var([:tail | rest], type, context) do
|
||||
@@ -518,7 +518,7 @@ defmodule Module.Types.Pattern do
|
||||
# This logic mirrors the code in `Apply.compare`
|
||||
cond do
|
||||
# If it is a singleton, we can always be precise
|
||||
singleton?(type) -> if polarity, do: type, else: negation(type)
|
||||
singleton?(type) -> if polarity, do: type, else: Module.Types.Descr.opt_negation(type)
|
||||
# We are checking for `not x == 1` or similar, we can't say anything about x
|
||||
polarity == false -> term()
|
||||
# We are checking for `x == 1`, make sure x is integer or float
|
||||
@@ -550,7 +550,7 @@ defmodule Module.Types.Pattern do
|
||||
tail
|
||||
|> Enum.reduce(
|
||||
of_pattern_tree(head, stack, context),
|
||||
&union(of_pattern_tree(&1, stack, context), &2)
|
||||
&opt_union(of_pattern_tree(&1, stack, context), &2)
|
||||
)
|
||||
|> non_empty_list(of_pattern_tree(suffix, stack, context))
|
||||
end
|
||||
@@ -558,7 +558,7 @@ defmodule Module.Types.Pattern do
|
||||
defp of_pattern_tree({:intersection, entries}, stack, context) do
|
||||
entries
|
||||
|> Enum.map(&of_pattern_tree(&1, stack, context))
|
||||
|> Enum.reduce(&intersection/2)
|
||||
|> Enum.reduce(&opt_intersection/2)
|
||||
end
|
||||
|
||||
defp of_pattern_tree({:var, version}, _stack, context) do
|
||||
@@ -690,7 +690,7 @@ defmodule Module.Types.Pattern do
|
||||
if static == [] do
|
||||
{:intersection, dynamic}
|
||||
else
|
||||
{:intersection, [Enum.reduce(static, &intersection/2) | dynamic]}
|
||||
{:intersection, [Enum.reduce(static, &opt_intersection/2) | dynamic]}
|
||||
end
|
||||
|
||||
context = of_var(var, version, [%{root: return, expr: match}], context)
|
||||
@@ -786,8 +786,9 @@ defmodule Module.Types.Pattern do
|
||||
|
||||
# <<...>>>
|
||||
defp of_pattern({:<<>>, _meta, args} = node, _path, stack, context) do
|
||||
precise? = of_precise_bitstring?(node, context.vars)
|
||||
{type, context} = Of.bitstring(args, :match, stack, context)
|
||||
{type, of_precise_bitstring?(node), context}
|
||||
{type, precise?, context}
|
||||
end
|
||||
|
||||
# left ++ right
|
||||
@@ -950,13 +951,13 @@ defmodule Module.Types.Pattern do
|
||||
type =
|
||||
case {static, dynamic} do
|
||||
{static, []} when is_descr(suffix_type) ->
|
||||
non_empty_list(Enum.reduce(static, &union/2), suffix_type)
|
||||
non_empty_list(Enum.reduce(static, &opt_union/2), suffix_type)
|
||||
|
||||
{[], dynamic} ->
|
||||
{:non_empty_list, dynamic, suffix_type}
|
||||
|
||||
{static, dynamic} ->
|
||||
{:non_empty_list, [Enum.reduce(static, &union/2) | dynamic], suffix_type}
|
||||
{:non_empty_list, [Enum.reduce(static, &opt_union/2) | dynamic], suffix_type}
|
||||
end
|
||||
|
||||
{type, false, context}
|
||||
@@ -987,12 +988,19 @@ defmodule Module.Types.Pattern do
|
||||
{type, precise?, put_in(context.subpatterns[key], of_pattern_tree(type, stack, context))}
|
||||
end
|
||||
|
||||
defp of_precise_bitstring?({:<<>>, _meta, [{:"::", _, [expr, {type, _, _}]}]})
|
||||
defp of_precise_bitstring?({:<<>>, _meta, [{:"::", _, [expr, {type, _, _}]}]}, vars)
|
||||
when type in [:binary, :bitstring, :bytes, :bits] do
|
||||
is_var(expr) or of_precise_bitstring?(expr)
|
||||
new_var?(expr, vars) or of_precise_bitstring?(expr, vars)
|
||||
end
|
||||
|
||||
defp of_precise_bitstring?(_), do: false
|
||||
defp of_precise_bitstring?(_expr, _vars), do: false
|
||||
|
||||
defp new_var?({_, meta, _} = var, vars) when is_var(var) do
|
||||
version = Keyword.fetch!(meta, :version)
|
||||
not is_map_key(vars, version)
|
||||
end
|
||||
|
||||
defp new_var?(_expr, _vars), do: false
|
||||
|
||||
## Guards
|
||||
#
|
||||
@@ -1018,9 +1026,10 @@ defmodule Module.Types.Pattern do
|
||||
end
|
||||
|
||||
defp of_guards(guards, vars, stack, context) do
|
||||
stack = %{stack | reverse_arrow: :except_none}
|
||||
|
||||
context =
|
||||
init_pattern_info(context, %{
|
||||
guard_context: :andalso,
|
||||
parent_version: nil,
|
||||
vars: vars,
|
||||
subpatterns_vars: %{},
|
||||
@@ -1065,10 +1074,6 @@ defmodule Module.Types.Pattern do
|
||||
%{context | pattern_info: %{pattern_info | parent_version: parent_version}}}
|
||||
end
|
||||
|
||||
defp enable_conditional_mode(%{pattern_info: pattern_info} = context) do
|
||||
%{context | pattern_info: %{pattern_info | guard_context: :orelse}, conditional_vars: %{}}
|
||||
end
|
||||
|
||||
defp maybe_badguard(type, guard, stack, context) do
|
||||
case booleaness(type) do
|
||||
:maybe_both ->
|
||||
@@ -1120,7 +1125,7 @@ defmodule Module.Types.Pattern do
|
||||
Enum.map_reduce(prefix, context, &of_guard(&1, term(), expr, stack, &2))
|
||||
|
||||
{suffix, context} = of_guard(suffix, term(), expr, stack, context)
|
||||
{non_empty_list(Enum.reduce(prefix, &union/2), suffix), context}
|
||||
{non_empty_list(Enum.reduce(prefix, &opt_union/2), suffix), context}
|
||||
end
|
||||
|
||||
# {left, right}
|
||||
@@ -1261,7 +1266,7 @@ defmodule Module.Types.Pattern do
|
||||
# `:fail` as dynamic as its whole purpose is to cause failures.
|
||||
defp of_remote(:orelse, [left, :fail], _call, expected, stack, context) do
|
||||
{type, context} = of_guard(left, expected, left, stack, context)
|
||||
{union(type, @dynamic_fail), context}
|
||||
{opt_union(type, @dynamic_fail), context}
|
||||
end
|
||||
|
||||
defp of_remote(fun, _args, call, expected, stack, context)
|
||||
@@ -1294,7 +1299,8 @@ defmodule Module.Types.Pattern do
|
||||
of_guard(left, expected, left, stack, context)
|
||||
|
||||
true ->
|
||||
cond_context = enable_conditional_mode(context)
|
||||
cond_context = %{context | conditional_vars: %{}}
|
||||
stack = %{stack | reverse_arrow: :include_none}
|
||||
|
||||
# Compute the sure types, which are stored directly in the context
|
||||
{_type, context} = of_guard(left, boolean(), left, stack, context)
|
||||
@@ -1432,7 +1438,7 @@ defmodule Module.Types.Pattern do
|
||||
|
||||
case disjoint? do
|
||||
true -> {type, context}
|
||||
false -> {union(to_abort, type), context}
|
||||
false -> {opt_union(to_abort, type), context}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1447,7 +1453,7 @@ defmodule Module.Types.Pattern do
|
||||
of_guard(head, expected, head, stack, context)
|
||||
|
||||
acc_vars = [{vars, cond_vars} | acc_vars]
|
||||
of_logical_cond(tail, union(acc_type, type), acc_vars, expected, stack, context)
|
||||
of_logical_cond(tail, opt_union(acc_type, type), acc_vars, expected, stack, context)
|
||||
end
|
||||
|
||||
defp of_logical_cond([], acc_type, acc_vars, _expected, _stack, _context) do
|
||||
@@ -1564,7 +1570,18 @@ defmodule Module.Types.Pattern do
|
||||
|
||||
#{expr_to_string({:!, [], [expr]}) |> indent(4)}
|
||||
|
||||
will always evaluate to true because the expression has type:
|
||||
will always evaluate to true because its inner expression has type:
|
||||
|
||||
#{to_quoted_string(type) |> indent(4)}
|
||||
"""
|
||||
|
||||
{:case, :"!!"} ->
|
||||
"""
|
||||
the following conditional expression:
|
||||
|
||||
#{expr_to_string({:!, [], [{:!, [], [expr]}]}) |> indent(4)}
|
||||
|
||||
will always evaluate to false because its inner expression has type:
|
||||
|
||||
#{to_quoted_string(type) |> indent(4)}
|
||||
"""
|
||||
@@ -1721,7 +1738,18 @@ defmodule Module.Types.Pattern do
|
||||
|
||||
#{expr_to_string({:!, [], [expr]}) |> indent(4)}
|
||||
|
||||
will always evaluate to false because the expression has type:
|
||||
will always evaluate to false because its inner expression has type:
|
||||
|
||||
#{to_quoted_string(type) |> indent(4)}
|
||||
"""
|
||||
|
||||
op == :"!!" ->
|
||||
"""
|
||||
the following conditional expression:
|
||||
|
||||
#{expr_to_string({:!, [], [{:!, [], [expr]}]}) |> indent(4)}
|
||||
|
||||
will always evaluate to true because its inner expression has type:
|
||||
|
||||
#{to_quoted_string(type) |> indent(4)}
|
||||
"""
|
||||
|
||||
@@ -852,7 +852,7 @@ defmodule OptionParser do
|
||||
|> to_existing_key(allow_nonexistent_atoms?)
|
||||
end
|
||||
|
||||
defp to_existing_key(option, true), do: String.to_atom(option)
|
||||
defp to_existing_key(option, true), do: String.to_unsafe_atom(option)
|
||||
|
||||
defp to_existing_key(option, false) do
|
||||
try do
|
||||
|
||||
@@ -282,7 +282,9 @@ defmodule Protocol do
|
||||
type_args = :lists.map(fn _ -> quote(do: term) end, :lists.seq(2, arity))
|
||||
type_args = [quote(do: t) | type_args]
|
||||
|
||||
to_var = fn pos -> Macro.var(String.to_atom("arg" <> Integer.to_string(pos)), __MODULE__) end
|
||||
to_var = fn pos ->
|
||||
Macro.var(String.to_unsafe_atom("arg" <> Integer.to_string(pos)), __MODULE__)
|
||||
end
|
||||
|
||||
call_args = :lists.map(to_var, :lists.seq(2, arity))
|
||||
call_args = [quote(do: term) | call_args]
|
||||
@@ -694,10 +696,10 @@ defmodule Protocol do
|
||||
|
||||
domain =
|
||||
Enum.reduce(types_minus_any -- structs, structs_domain, fn impl, acc ->
|
||||
Descr.union(Module.Types.Of.impl(impl, :open), acc)
|
||||
Descr.opt_union(Module.Types.Of.impl(impl, :open), acc)
|
||||
end)
|
||||
|
||||
not_domain = Descr.negation(domain)
|
||||
not_domain = Descr.opt_negation(domain)
|
||||
|
||||
if Any in types do
|
||||
clauses =
|
||||
@@ -1228,7 +1230,7 @@ defmodule Protocol do
|
||||
end
|
||||
|
||||
def __concat__(left, right) when is_binary(right) do
|
||||
String.to_atom(ensure_prefix(Atom.to_string(left)) <> "." <> right)
|
||||
String.to_unsafe_atom(ensure_prefix(Atom.to_string(left)) <> "." <> right)
|
||||
end
|
||||
|
||||
defp ensure_prefix("Elixir." <> _ = left), do: left
|
||||
|
||||
@@ -40,7 +40,7 @@ defmodule Record.Extractor do
|
||||
defp from_lib_file(file) do
|
||||
[app | path] = :filename.split(String.to_charlist(file))
|
||||
|
||||
case :code.lib_dir(List.to_atom(app)) do
|
||||
case :code.lib_dir(List.to_unsafe_atom(app)) do
|
||||
{:error, _} ->
|
||||
raise ArgumentError, "lib file #{file} could not be found"
|
||||
|
||||
|
||||
+67
-33
@@ -21,8 +21,8 @@ defmodule Registry do
|
||||
the `:via` option), storing properties, custom dispatching rules, or a pubsub
|
||||
implementation. We explore some of those use cases below.
|
||||
|
||||
The registry may also be transparently partitioned, which provides
|
||||
more scalable behavior for running registries on highly concurrent
|
||||
The registry may also be transparently partitioned with the `:partitions` option,
|
||||
which provides more scalable behavior for running registries on highly concurrent
|
||||
environments with thousands or millions of entries.
|
||||
|
||||
## Using in `:via`
|
||||
@@ -1127,22 +1127,26 @@ defmodule Registry do
|
||||
end
|
||||
|
||||
defp unregister_match_specs({:duplicate, :key}, key, self, pattern, guards) do
|
||||
if is_atom(key) and reserved_atom?(Atom.to_string(key)) do
|
||||
if exact_match_key?(key) do
|
||||
{[{{{key, self, :_}, :_}, [], [true]}], [{{{key, self, :_}, pattern}, guards, [true]}]}
|
||||
else
|
||||
guard = {:"=:=", {:element, 1, {:element, 1, :"$_"}}, {:const, key}}
|
||||
pid_guard = {:"=:=", {:element, 2, {:element, 1, :"$_"}}, {:const, self}}
|
||||
|
||||
{[{{{:_, :_, :_}, :_}, [guard, pid_guard], [true]}],
|
||||
[{{{:_, :_, :_}, pattern}, [guard, pid_guard | guards], [true]}]}
|
||||
else
|
||||
{[{{{key, self, :_}, :_}, [], [true]}], [{{{key, self, :_}, pattern}, guards, [true]}]}
|
||||
end
|
||||
end
|
||||
|
||||
defp unregister_match_specs(_kind, key, self, pattern, guards) do
|
||||
underscore_guard = {:"=:=", {:element, 1, :"$_"}, {:const, key}}
|
||||
if exact_match_key?(key) do
|
||||
{[{{key, {self, :_}}, [], [true]}], [{{key, {self, pattern}}, guards, [true]}]}
|
||||
else
|
||||
underscore_guard = {:"=:=", {:element, 1, :"$_"}, {:const, key}}
|
||||
|
||||
{[{{:_, {self, :_}}, [underscore_guard], [true]}],
|
||||
[{{:_, {self, pattern}}, [underscore_guard | guards], [true]}]}
|
||||
{[{{:_, {self, :_}}, [underscore_guard], [true]}],
|
||||
[{{:_, {self, pattern}}, [underscore_guard | guards], [true]}]}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -1484,8 +1488,9 @@ defmodule Registry do
|
||||
that reference the underlying entry structure via `:"$_"` will return different results.
|
||||
Use named variables like `:"$1"`, `:"$2"`, `:"$3"` instead.
|
||||
|
||||
Note that for large registries with many partitions this will be costly as it builds the result by
|
||||
concatenating all the partitions.
|
||||
Note that for registries with many partitions, the result of each partition is concatenated.
|
||||
Therefore selecting thousands of entries across partitions may become an expensive operation
|
||||
and we recommend profiling/benchmarking whenever selecting too many entries.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -1618,11 +1623,11 @@ defmodule Registry do
|
||||
|
||||
defp lookup_second({:duplicate, :key}, ets, key) do
|
||||
spec =
|
||||
if is_atom(key) and reserved_atom?(Atom.to_string(key)) do
|
||||
if exact_match_key?(key) do
|
||||
[{{{key, :"$1", :_}, :"$2"}, [], [{{:"$1", :"$2"}}]}]
|
||||
else
|
||||
guard = {:"=:=", {:element, 1, {:element, 1, :"$_"}}, {:const, key}}
|
||||
[{{{:_, :"$1", :_}, :"$2"}, [guard], [{{:"$1", :"$2"}}]}]
|
||||
else
|
||||
[{{{key, :"$1", :_}, :"$2"}, [], [{{:"$1", :"$2"}}]}]
|
||||
end
|
||||
|
||||
try do
|
||||
@@ -1664,25 +1669,24 @@ defmodule Registry do
|
||||
def __unregister__(table, match, pos, ordered \\ false)
|
||||
|
||||
def __unregister__(table, {key, {pid, :_}}, 1, true = _ordered) do
|
||||
if is_atom(key) and reserved_atom?(Atom.to_string(key)) do
|
||||
if exact_match_key?(key) do
|
||||
:ets.match_delete(table, {{key, pid, :_}, :_})
|
||||
else
|
||||
guard = {:"=:=", {:element, 1, {:element, 1, :"$_"}}, {:const, key}}
|
||||
pid_guard = {:"=:=", {:element, 2, {:element, 1, :"$_"}}, {:const, pid}}
|
||||
:ets.select_delete(table, [{{:_, :_}, [guard, pid_guard], [true]}]) >= 0
|
||||
else
|
||||
:ets.match_delete(table, {{key, pid, :_}, :_})
|
||||
end
|
||||
end
|
||||
|
||||
def __unregister__(table, match, pos, false = _ordered) do
|
||||
key = :erlang.element(pos, match)
|
||||
|
||||
# We need to perform an element comparison if we have a special atom key.
|
||||
if is_atom(key) and reserved_atom?(Atom.to_string(key)) do
|
||||
if exact_match_key?(key) do
|
||||
:ets.match_delete(table, match)
|
||||
else
|
||||
match = :erlang.setelement(pos, match, :_)
|
||||
guard = {:"=:=", {:element, pos, :"$_"}, {:const, key}}
|
||||
:ets.select_delete(table, [{match, [guard], [true]}]) >= 0
|
||||
else
|
||||
:ets.match_delete(table, match)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1693,42 +1697,72 @@ defmodule Registry do
|
||||
defp match_spec({:duplicate, :key}, key, pattern, guards) do
|
||||
body = [{{{:element, 2, {:element, 1, :"$_"}}, {:element, 2, :"$_"}}}]
|
||||
|
||||
if is_atom(key) and reserved_atom?(Atom.to_string(key)) do
|
||||
if exact_match_key?(key) do
|
||||
[{{{key, :_, :_}, pattern}, guards, body}]
|
||||
else
|
||||
guards = [
|
||||
{:"=:=", {:element, 1, {:element, 1, :"$_"}}, {:const, key}} | guards
|
||||
]
|
||||
|
||||
[{{{:_, :_, :_}, pattern}, guards, body}]
|
||||
else
|
||||
[{{{key, :_, :_}, pattern}, guards, body}]
|
||||
end
|
||||
end
|
||||
|
||||
defp match_spec(_kind, key, pattern, guards) do
|
||||
guards = [{:"=:=", {:element, 1, :"$_"}, {:const, key}} | guards]
|
||||
[{{:_, {:_, pattern}}, guards, [{:element, 2, :"$_"}]}]
|
||||
if exact_match_key?(key) do
|
||||
[{{key, {:_, pattern}}, guards, [{:element, 2, :"$_"}]}]
|
||||
else
|
||||
guards = [{:"=:=", {:element, 1, :"$_"}, {:const, key}} | guards]
|
||||
[{{:_, {:_, pattern}}, guards, [{:element, 2, :"$_"}]}]
|
||||
end
|
||||
end
|
||||
|
||||
defp count_match_spec({:duplicate, :key}, key, pattern, guards) do
|
||||
if is_atom(key) and reserved_atom?(Atom.to_string(key)) do
|
||||
if exact_match_key?(key) do
|
||||
[{{{key, :_, :_}, pattern}, guards, [true]}]
|
||||
else
|
||||
guards = [
|
||||
{:"=:=", {:element, 1, {:element, 1, :"$_"}}, {:const, key}} | guards
|
||||
]
|
||||
|
||||
[{{{:_, :_, :_}, pattern}, guards, [true]}]
|
||||
else
|
||||
[{{{key, :_, :_}, pattern}, guards, [true]}]
|
||||
end
|
||||
end
|
||||
|
||||
defp count_match_spec(_kind, key, pattern, guards) do
|
||||
guards = [{:"=:=", {:element, 1, :"$_"}, {:const, key}} | guards]
|
||||
[{{:_, {:_, pattern}}, guards, [true]}]
|
||||
if exact_match_key?(key) do
|
||||
[{{key, {:_, pattern}}, guards, [true]}]
|
||||
else
|
||||
guards = [{:"=:=", {:element, 1, :"$_"}, {:const, key}} | guards]
|
||||
[{{:_, {:_, pattern}}, guards, [true]}]
|
||||
end
|
||||
end
|
||||
|
||||
defp reserved_atom?("_"), do: true
|
||||
defp reserved_atom?("$" <> _), do: true
|
||||
defp reserved_atom?(_), do: false
|
||||
# Only keys that remain exact when embedded in an ETS match head can use
|
||||
# the optimized direct-key match path.
|
||||
defp exact_match_key?(term) when is_map(term), do: false
|
||||
defp exact_match_key?([]), do: true
|
||||
defp exact_match_key?([head | tail]), do: exact_match_key?(head) and exact_match_key?(tail)
|
||||
|
||||
defp exact_match_key?(term) when is_tuple(term) do
|
||||
exact_match_tuple?(term, tuple_size(term))
|
||||
end
|
||||
|
||||
defp exact_match_key?(term) when is_atom(term) do
|
||||
case Atom.to_string(term) do
|
||||
"_" -> false
|
||||
"$" <> _ -> false
|
||||
_ -> true
|
||||
end
|
||||
end
|
||||
|
||||
defp exact_match_key?(_term), do: true
|
||||
|
||||
defp exact_match_tuple?(_term, 0), do: true
|
||||
|
||||
defp exact_match_tuple?(term, index) do
|
||||
exact_match_key?(:erlang.element(index, term)) and exact_match_tuple?(term, index - 1)
|
||||
end
|
||||
|
||||
defp ordered?({:duplicate, :key}), do: true
|
||||
defp ordered?(_), do: false
|
||||
|
||||
@@ -2962,6 +2962,12 @@ defmodule String do
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: Deprecate String.to_atom, List.to_atom, and atom interpolation in v1.25
|
||||
@doc deprecated: "Use to_existing_atom/1 or to_unsafe_atom/1 instead"
|
||||
def to_atom(string) when is_binary(string) do
|
||||
:erlang.binary_to_atom(string, :utf8)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts a string to an existing atom or creates a new one.
|
||||
|
||||
@@ -2979,12 +2985,13 @@ defmodule String do
|
||||
|
||||
## Examples
|
||||
|
||||
iex> String.to_atom("my_atom")
|
||||
iex> String.to_unsafe_atom("my_atom")
|
||||
:my_atom
|
||||
|
||||
"""
|
||||
@spec to_atom(String.t()) :: atom
|
||||
def to_atom(string) when is_binary(string) do
|
||||
@doc since: "1.21.0"
|
||||
@spec to_unsafe_atom(String.t()) :: atom
|
||||
def to_unsafe_atom(string) when is_binary(string) do
|
||||
:erlang.binary_to_atom(string, :utf8)
|
||||
end
|
||||
|
||||
@@ -2992,6 +2999,8 @@ defmodule String do
|
||||
Converts a string to an existing atom or raises if
|
||||
the atom does not exist.
|
||||
|
||||
If the list of expected atoms is known upfront, prefer `to_existing_atom/2`.
|
||||
|
||||
The maximum atom size is of 255 Unicode code points.
|
||||
Raises an `ArgumentError` if the atom does not exist.
|
||||
|
||||
@@ -3021,6 +3030,47 @@ defmodule String do
|
||||
:erlang.binary_to_existing_atom(string, :utf8)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Converts a string to one of the `allowed_atoms` or raises.
|
||||
|
||||
Raises an `ArgumentError` if the atom either does not exist or is not within
|
||||
the existing list.
|
||||
|
||||
This should be preferred to `to_existing_atom/1` if the list is known upfront,
|
||||
since there is no risk that the atom has not been loaded.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> String.to_existing_atom("foo", [:foo, :bar])
|
||||
:foo
|
||||
|
||||
iex> String.to_existing_atom("unknown", [:foo, :bar])
|
||||
** (ArgumentError) unexpected value: \"unknown\", the allowed atoms are: [:foo, :bar]
|
||||
|
||||
"""
|
||||
@doc since: "1.21.0"
|
||||
@spec to_existing_atom(String.t(), nonempty_list(a)) :: a when a: atom()
|
||||
def to_existing_atom(string, [_ | _] = allowed_atoms) when is_binary(string) do
|
||||
atom = :erlang.binary_to_existing_atom(string, :utf8)
|
||||
|
||||
if atom not in allowed_atoms do
|
||||
to_existing_atom_unexpected(string, allowed_atoms)
|
||||
end
|
||||
|
||||
atom
|
||||
end
|
||||
|
||||
# used just to have a less cryptic stacktrace and consistent error
|
||||
@doc false
|
||||
def __to_existing_atom__(string, allowed_atoms) do
|
||||
to_existing_atom_unexpected(string, allowed_atoms)
|
||||
end
|
||||
|
||||
defp to_existing_atom_unexpected(string, allowed_atoms) do
|
||||
raise ArgumentError,
|
||||
"unexpected value: #{inspect(string)}, the allowed atoms are: #{inspect(allowed_atoms)}"
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns an integer whose text representation is `string`.
|
||||
|
||||
@@ -3130,9 +3180,9 @@ defmodule String do
|
||||
{bag2, length2} = string_to_bag(string2, %{}, 0)
|
||||
|
||||
diff1 = bag_difference(bag1, bag2)
|
||||
diff2 = bag_difference(bag2, bag1)
|
||||
max_diff = if length1 >= length2, do: diff1, else: diff1 - length1 + length2
|
||||
|
||||
1 - max(diff1, diff2) / max(length1, length2)
|
||||
1 - max_diff / max(length1, length2)
|
||||
end
|
||||
|
||||
defp string_to_bag(string, bag, length) do
|
||||
|
||||
@@ -541,7 +541,7 @@ defmodule System do
|
||||
By default, the Erlang VM register traps to the three
|
||||
signals:
|
||||
|
||||
* `:sigstop` - gracefully shuts down the VM with `stop/0`
|
||||
* `:sigterm` - gracefully shuts down the VM with `stop/0`
|
||||
* `:sigquit` - halts the VM via `halt/0`
|
||||
* `:sigusr1` - halts the VM via status code of 1
|
||||
|
||||
@@ -1378,6 +1378,17 @@ defmodule System do
|
||||
|
||||
@doc """
|
||||
Returns the Erlang/OTP release number.
|
||||
|
||||
The version is a single number, the major version of OTP.
|
||||
Erlang/OTP does not provide an API to fetch the exact
|
||||
patch version, see the [*Versions* page](https://www.erlang.org/doc/system/versions.html)
|
||||
in the Erlang documentation for more information.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> System.otp_release()
|
||||
"#{:erlang.system_info(:otp_release)}"
|
||||
|
||||
"""
|
||||
@spec otp_release :: String.t()
|
||||
@doc since: "1.3.0"
|
||||
|
||||
+36
-20
@@ -24,6 +24,12 @@ defmodule URI do
|
||||
|
||||
[scheme]://[userinfo]@[host]:[port][path]?[query]#[fragment]
|
||||
|
||||
The fields contain the encoded URI components as they appear in the URI
|
||||
itself. For example, a slash inside the userinfo must be stored as `%2F`,
|
||||
not as `/`. Functions such as `parse/1` and `new/1` preserve existing
|
||||
percent-encoded sequences in those fields, and functions such as `to_string/1`
|
||||
expects those fields to already be encoded as needed. Whenever setting or
|
||||
modifying the fields directly, you must encode them accordingly.
|
||||
|
||||
Note the `authority` field is deprecated. `parse/1` will still
|
||||
populate it for backwards compatibility but you should generally
|
||||
@@ -246,7 +252,7 @@ defmodule URI do
|
||||
|
||||
{{key, value}, rest} ->
|
||||
# Avoid warnings about Dict being deprecated
|
||||
dict_module = String.to_atom("Dict")
|
||||
dict_module = String.to_unsafe_atom("Dict")
|
||||
decode_query_into_dict(rest, dict_module.put(dict, key, value), encoding)
|
||||
end
|
||||
end
|
||||
@@ -478,18 +484,19 @@ defmodule URI do
|
||||
unpercent(string, "", true)
|
||||
end
|
||||
|
||||
defguardp is_hex(char) when char in ?0..?9 or char in ?A..?F or char in ?a..?f
|
||||
|
||||
defp unpercent(<<?+, tail::binary>>, acc, spaces = true) do
|
||||
unpercent(tail, <<acc::binary, ?\s>>, spaces)
|
||||
end
|
||||
|
||||
defp unpercent(<<?%, h1, h2, tail::binary>>, acc, spaces)
|
||||
when is_hex(h1) and is_hex(h2) do
|
||||
unpercent(tail, <<acc::binary, bsl(hex_to_dec(h1), 4) + hex_to_dec(h2)>>, spaces)
|
||||
end
|
||||
|
||||
defp unpercent(<<?%, tail::binary>>, acc, spaces) do
|
||||
with <<hex1, hex2, tail::binary>> <- tail,
|
||||
dec1 when is_integer(dec1) <- hex_to_dec(hex1),
|
||||
dec2 when is_integer(dec2) <- hex_to_dec(hex2) do
|
||||
unpercent(tail, <<acc::binary, bsl(dec1, 4) + dec2>>, spaces)
|
||||
else
|
||||
_ -> unpercent(tail, <<acc::binary, ?%>>, spaces)
|
||||
end
|
||||
unpercent(tail, <<acc::binary, ?%>>, spaces)
|
||||
end
|
||||
|
||||
defp unpercent(<<head, tail::binary>>, acc, spaces) do
|
||||
@@ -502,7 +509,6 @@ defmodule URI do
|
||||
defp hex_to_dec(n) when n in ?A..?F, do: n - ?A + 10
|
||||
defp hex_to_dec(n) when n in ?a..?f, do: n - ?a + 10
|
||||
defp hex_to_dec(n) when n in ?0..?9, do: n - ?0
|
||||
defp hex_to_dec(_n), do: nil
|
||||
|
||||
@doc """
|
||||
Creates a new URI struct by parsing and validating a string or from an existing URI.
|
||||
@@ -897,6 +903,9 @@ defmodule URI do
|
||||
@doc """
|
||||
Returns the string representation of the given [URI struct](`t:t/0`).
|
||||
|
||||
This function assembles the URI components into a string, assuming each
|
||||
field is valid and escaped as done by `parse/1` and `new/1`.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> uri = URI.parse("http://google.com")
|
||||
@@ -1081,15 +1090,18 @@ defimpl String.Chars, for: URI do
|
||||
":path in URI must be empty or an absolute path if URL has a :host, got: #{inspect(uri)}"
|
||||
end
|
||||
|
||||
def to_string(%{scheme: scheme, port: port, path: path, query: query, fragment: fragment} = uri) do
|
||||
uri =
|
||||
case scheme && URI.default_port(scheme) do
|
||||
^port -> %{uri | port: nil}
|
||||
_ -> uri
|
||||
end
|
||||
|
||||
def to_string(%{
|
||||
host: host,
|
||||
authority: authority,
|
||||
userinfo: userinfo,
|
||||
scheme: scheme,
|
||||
port: port,
|
||||
path: path,
|
||||
query: query,
|
||||
fragment: fragment
|
||||
}) do
|
||||
# Based on https://tools.ietf.org/html/rfc3986#section-5.3
|
||||
authority = extract_authority(uri)
|
||||
authority = extract_authority(host, authority, scheme, userinfo, port)
|
||||
|
||||
IO.iodata_to_binary([
|
||||
if(scheme, do: [scheme, ?:], else: []),
|
||||
@@ -1100,19 +1112,23 @@ defimpl String.Chars, for: URI do
|
||||
])
|
||||
end
|
||||
|
||||
defp extract_authority(%{host: nil, authority: authority}) do
|
||||
defp extract_authority(_host = nil, authority, _scheme, _userinfo, _port) do
|
||||
authority
|
||||
end
|
||||
|
||||
defp extract_authority(%{host: host, userinfo: userinfo, port: port}) do
|
||||
defp extract_authority(host, _authority, scheme, userinfo, port) do
|
||||
# According to the grammar at
|
||||
# https://tools.ietf.org/html/rfc3986#appendix-A, a "host" can have a colon
|
||||
# in it only if it's an IPv6 or "IPvFuture" address, so if there's a colon
|
||||
# in the host we can safely surround it with [].
|
||||
|
||||
[
|
||||
if(userinfo, do: [userinfo | "@"], else: []),
|
||||
if(String.contains?(host, ":"), do: ["[", host | "]"], else: host),
|
||||
if(port, do: [":" | Integer.to_string(port)], else: [])
|
||||
if(port != nil and (is_nil(scheme) or URI.default_port(scheme) != port),
|
||||
do: [":" | Integer.to_string(port)],
|
||||
else: []
|
||||
)
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -18,12 +18,16 @@ defmodule Version do
|
||||
|
||||
MAJOR.MINOR.PATCH
|
||||
|
||||
Each numeric component is limited to at most 14 digits.
|
||||
|
||||
Pre-releases are supported by optionally appending a hyphen and a series of
|
||||
period-separated identifiers immediately following the patch version.
|
||||
Identifiers consist of only ASCII alphanumeric characters and hyphens (`[0-9A-Za-z-]`):
|
||||
|
||||
"1.0.0-alpha.3"
|
||||
|
||||
Numeric pre-release identifiers are also limited to at most 14 digits.
|
||||
|
||||
Build information can be added by appending a plus sign and a series of
|
||||
dot-separated identifiers immediately following the patch or pre-release version.
|
||||
Identifiers consist of only ASCII alphanumeric characters and hyphens (`[0-9A-Za-z-]`):
|
||||
@@ -520,6 +524,8 @@ defmodule Version do
|
||||
defmodule Parser do
|
||||
@moduledoc false
|
||||
|
||||
@max_numeric_component_digits 14
|
||||
|
||||
operators = [
|
||||
{">=", :>=},
|
||||
{"<=", :<=},
|
||||
@@ -621,7 +627,9 @@ defmodule Version do
|
||||
defp require_digits(nil), do: :error
|
||||
|
||||
defp require_digits(string) do
|
||||
if leading_zero?(string), do: :error, else: parse_digits(string, "")
|
||||
if leading_zero?(string) or byte_size(string) > @max_numeric_component_digits,
|
||||
do: :error,
|
||||
else: parse_digits(string, "")
|
||||
end
|
||||
|
||||
defp leading_zero?(<<?0, _, _::binary>>), do: true
|
||||
@@ -649,6 +657,11 @@ defmodule Version do
|
||||
end
|
||||
end
|
||||
|
||||
defp convert_parts_to_integer([part | rest], acc)
|
||||
when byte_size(part) > @max_numeric_component_digits do
|
||||
if all_digits?(part), do: :error, else: convert_parts_to_integer(rest, [part | acc])
|
||||
end
|
||||
|
||||
defp convert_parts_to_integer([part | rest], acc) do
|
||||
case parse_digits(part, "") do
|
||||
{:ok, integer} ->
|
||||
@@ -667,6 +680,10 @@ defmodule Version do
|
||||
{:ok, Enum.reverse(acc)}
|
||||
end
|
||||
|
||||
defp all_digits?(<<char, rest::binary>>) when char in ?0..?9, do: all_digits?(rest)
|
||||
defp all_digits?(<<>>), do: true
|
||||
defp all_digits?(_other), do: false
|
||||
|
||||
defp valid_identifier?(<<char, rest::binary>>)
|
||||
when char in ?0..?9
|
||||
when char in ?a..?z
|
||||
|
||||
@@ -155,7 +155,7 @@ Picture yourself implementing code that converts string values into atoms. These
|
||||
```elixir
|
||||
defmodule MyRequestHandler do
|
||||
def parse(%{"status" => status, "message" => message} = _payload) do
|
||||
%{status: String.to_atom(status), message: message}
|
||||
%{status: String.to_unsafe_atom(status), message: message}
|
||||
end
|
||||
end
|
||||
```
|
||||
@@ -165,75 +165,53 @@ iex> MyRequestHandler.parse(%{"status" => "ok", "message" => "all good"})
|
||||
%{status: :ok, message: "all good"}
|
||||
```
|
||||
|
||||
When we use the `String.to_atom/1` function to dynamically create an atom, it essentially gains potential access to create arbitrary atoms in our system, causing us to lose control over adhering to the limits established by the BEAM. This issue could be exploited by someone to create enough atoms to shut down a system.
|
||||
When we use the `String.to_unsafe_atom/1` function to dynamically create an atom, it essentially gains potential access to create arbitrary atoms in our system, causing us to lose control over adhering to the limits established by the BEAM. This issue could be exploited by someone to create enough atoms to shut down a system.
|
||||
|
||||
#### Refactoring
|
||||
|
||||
To eliminate this anti-pattern, developers must either perform explicit conversions by mapping strings to atoms or replace the use of `String.to_atom/1` with `String.to_existing_atom/1`. An explicit conversion could be done as follows:
|
||||
To eliminate this anti-pattern, developers must either:
|
||||
|
||||
* replace the use of `String.to_unsafe_atom/1` with a list of expected atoms in `String.to_existing_atom/2`
|
||||
|
||||
* use pattern-matching to map strings to their equivalent atoms
|
||||
|
||||
* use `String.to_existing_atom/1` when the former options are not possible
|
||||
|
||||
An explicit conversion could be done as follows:
|
||||
|
||||
```elixir
|
||||
defmodule MyRequestHandler do
|
||||
def parse(%{"status" => status, "message" => message} = _payload) do
|
||||
%{status: convert_status(status), message: message}
|
||||
end
|
||||
|
||||
defp convert_status("ok"), do: :ok
|
||||
defp convert_status("error"), do: :error
|
||||
defp convert_status("redirect"), do: :redirect
|
||||
end
|
||||
```
|
||||
|
||||
```elixir
|
||||
iex> MyRequestHandler.parse(%{"status" => "status_not_seen_anywhere", "message" => "all good"})
|
||||
** (FunctionClauseError) no function clause matching in MyRequestHandler.convert_status/1
|
||||
```
|
||||
|
||||
By explicitly listing all supported statuses, you guarantee only a limited number of conversions may happen. Passing an invalid status will lead to a function clause error.
|
||||
|
||||
An alternative is to use `String.to_existing_atom/1`, which will only convert a string to atom if the atom already exists in the system:
|
||||
|
||||
```elixir
|
||||
defmodule MyRequestHandler do
|
||||
def parse(%{"status" => status, "message" => message} = _payload) do
|
||||
%{status: String.to_existing_atom(status), message: message}
|
||||
status = String.to_existing_atom(status, [:ok, :error, :redirect])
|
||||
%{status: status, message: message}
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
This will only accept valid values and convert `"ok"` as `:ok`, `"error"` as `:error`, and `"redirect"` as `:redirect`. Any other value would result in an `ArgumentError`, either because the atom doesn't exist or because it isn't part of the list:
|
||||
|
||||
```elixir
|
||||
iex> MyRequestHandler.parse(%{"status" => "status_not_seen_anywhere", "message" => "all good"})
|
||||
** (ArgumentError) errors were found at the given arguments:
|
||||
|
||||
* 1st argument: not an already existing atom
|
||||
** (ArgumentError) ...
|
||||
```
|
||||
|
||||
In such cases, passing an unknown status will raise as long as the status was not defined anywhere as an atom in the system. However, assuming `status` can be either `:ok`, `:error`, or `:redirect`, how can you guarantee those atoms exist? You must ensure those atoms exist somewhere **in the same module** where `String.to_existing_atom/1` is called. For example, if you had this code:
|
||||
By explicitly listing all supported statuses, you guarantee that only a limited number of conversions may happen, and that all expected atoms already exist within the system.
|
||||
|
||||
An alternative is to use pattern matching and explicitly convert each string to its respective atom. This is useful when you need additional logic on differnt branches:
|
||||
|
||||
```elixir
|
||||
defmodule MyRequestHandler do
|
||||
def parse(%{"status" => status, "message" => message} = _payload) do
|
||||
%{status: String.to_existing_atom(status), message: message}
|
||||
end
|
||||
|
||||
def handle(%{status: status}) do
|
||||
case status do
|
||||
:ok -> ...
|
||||
:error -> ...
|
||||
:redirect -> ...
|
||||
"ok" -> %{status: :ok, message: message}
|
||||
"error" -> %{status: :error, message: message}
|
||||
"redirect" -> %{status: :redirect, message: message, code: 302}
|
||||
end
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
All valid statuses are defined as atoms within the same module, and that's enough. If you want to be explicit, you could also have a function that lists them:
|
||||
|
||||
```elixir
|
||||
def valid_statuses do
|
||||
[:ok, :error, :redirect]
|
||||
end
|
||||
```
|
||||
|
||||
However, keep in mind using a module attribute or defining the atoms in the module body, outside of a function, are not sufficient, as the module body is only executed during compilation and it is not necessarily part of the compiled module loaded at runtime.
|
||||
You may alternatively use `String.to_existing_atom/1`, but keep in mind it does have pitfalls related to code loading. Read the documentation for more information.
|
||||
|
||||
## Long parameter list
|
||||
|
||||
|
||||
@@ -226,7 +226,7 @@ end
|
||||
|
||||
While you may receive the `address` as a string from a database, web request, or a third-party, if you find yourself frequently manipulating or extracting information from the string, it is a good indicator you should convert the address into structured data:
|
||||
|
||||
Another example of this anti-pattern is using floating numbers to model money and currency, when [richer data structures should be preferred](https://hexdocs.pm/ex_money/).
|
||||
Another example of this anti-pattern is using floating numbers to model money and currency, when [richer data structures should be preferred](https://ex-money.hexdocs.pm/).
|
||||
|
||||
#### Refactoring
|
||||
|
||||
@@ -350,7 +350,7 @@ The difference here is that the `struct/2` function behaves precisely the same f
|
||||
|
||||
#### Problem
|
||||
|
||||
The [*application environment*](https://hexdocs.pm/elixir/Application.html#module-the-application-environment) can be used to parameterize global values that can be used in an Elixir system. This mechanism can be very useful and therefore is not considered an anti-pattern by itself. However, library authors should avoid using the application environment to configure their library. The reason is exactly that the application environment is a **global** state, so there can only be a single value for each key in the environment for an application. This makes it impossible for multiple applications depending on the same library to configure the same aspect of the library in different ways.
|
||||
The [*application environment*](https://elixir.hexdocs.pm/Application.html#module-the-application-environment) can be used to parameterize global values that can be used in an Elixir system. This mechanism can be very useful and therefore is not considered an anti-pattern by itself. However, library authors should avoid using the application environment to configure their library. The reason is exactly that the application environment is a **global** state, so there can only be a single value for each key in the environment for an application. This makes it impossible for multiple applications depending on the same library to configure the same aspect of the library in different ways.
|
||||
|
||||
#### Example
|
||||
|
||||
|
||||
@@ -269,7 +269,7 @@ iex> heartbeats_per_minute = [99, 97, 116]
|
||||
You can always force charlists to be printed in their list representation by calling the `inspect/2` function:
|
||||
|
||||
```elixir
|
||||
iex> inspect(heartbeats_per_minute, charlists: :as_list)
|
||||
iex> inspect(heartbeats_per_minute, charlists: :as_lists)
|
||||
"[99, 97, 116]"
|
||||
```
|
||||
|
||||
|
||||
@@ -96,7 +96,7 @@ defmodule TestCase do
|
||||
|
||||
"""
|
||||
defmacro test(description, do: block) do
|
||||
function_name = String.to_atom("test " <> description)
|
||||
function_name = String.to_unsafe_atom("test " <> description)
|
||||
quote do
|
||||
def unquote(function_name)(), do: unquote(block)
|
||||
end
|
||||
@@ -159,7 +159,7 @@ defmodule TestCase do
|
||||
|
||||
"""
|
||||
defmacro test(description, do: block) do
|
||||
function_name = String.to_atom("test " <> description)
|
||||
function_name = String.to_unsafe_atom("test " <> description)
|
||||
quote do
|
||||
# Prepend the newly defined test to the list of tests
|
||||
@tests [unquote(function_name) | @tests]
|
||||
|
||||
@@ -261,7 +261,7 @@ Open up `config/runtime.exs` and add this to the bottom:
|
||||
nodes =
|
||||
System.get_env("NODES", "")
|
||||
|> String.split(",", trim: true)
|
||||
|> Enum.map(&String.to_atom/1)
|
||||
|> Enum.map(&String.to_unsafe_atom/1)
|
||||
|
||||
config :kv, :nodes, nodes
|
||||
```
|
||||
|
||||
@@ -14,12 +14,12 @@ Elixir applies bug fixes only to the latest minor branch. Security patches are a
|
||||
|
||||
Elixir version | Support
|
||||
:------------- | :-----------------------------
|
||||
1.20 | Development
|
||||
1.19 | Bug fixes and security patches
|
||||
1.21 | Development
|
||||
1.20 | Bug fixes and security patches
|
||||
1.19 | Security patches only
|
||||
1.18 | Security patches only
|
||||
1.17 | Security patches only
|
||||
1.16 | Security patches only
|
||||
1.15 | 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).
|
||||
|
||||
@@ -49,6 +49,7 @@ Erlang/OTP versioning is independent from the versioning of Elixir. Erlang relea
|
||||
|
||||
Elixir version | Supported Erlang/OTP versions
|
||||
:------------- | :-------------------------------
|
||||
1.21 | 27 - 29
|
||||
1.20 | 27 - 29
|
||||
1.19 | 26 - 28
|
||||
1.18 | 25 - 27
|
||||
@@ -91,6 +92,7 @@ The first column is the version the feature was hard deprecated. The second colu
|
||||
|
||||
Version | Deprecated feature | Replaced by (available since)
|
||||
:-------| :-------------------------------------------------- | :---------------------------------------------------------------
|
||||
[v1.21] | `Macro.Env.fetch_alias/2` (and `fetch_macro_alias`) | `Macro.Env.expand_alias/4` (v1.17)
|
||||
[v1.20] | `<<x::size(y)>>` in patterns without `^` | `<<x::size(^y)>>` (v1.15)
|
||||
[v1.20] | `File.stream!(path, modes, lines_or_bytes)` | `File.stream!(path, lines_or_bytes, modes)` (v1.16)
|
||||
[v1.20] | `Kernel.ParallelCompiler.async/1` | `Kernel.ParallelCompiler.pmap/2` (v1.16)
|
||||
@@ -243,4 +245,5 @@ Version | Deprecated feature | Replaced by (ava
|
||||
[v1.17]: https://github.com/elixir-lang/elixir/blob/v1.17/CHANGELOG.md#4-hard-deprecations
|
||||
[v1.18]: https://github.com/elixir-lang/elixir/blob/v1.18/CHANGELOG.md#4-hard-deprecations
|
||||
[v1.19]: https://github.com/elixir-lang/elixir/blob/v1.19/CHANGELOG.md#4-hard-deprecations
|
||||
[v1.20]: https://github.com/elixir-lang/elixir/blob/main/CHANGELOG.md#4-hard-deprecations
|
||||
[v1.20]: https://github.com/elixir-lang/elixir/blob/v1.20/CHANGELOG.md#4-hard-deprecations
|
||||
[v1.21]: https://github.com/elixir-lang/elixir/blob/main/CHANGELOG.md#4-hard-deprecations
|
||||
|
||||
@@ -231,11 +231,47 @@ Inferring type signatures comes with a series of trade-offs:
|
||||
|
||||
* Cascading errors - when a user accidentally makes type errors or the code has conflicting assumptions, type inference may lead to less clear error messages as the type system tries to reconcile diverging type assumptions across code paths.
|
||||
|
||||
On the other hand, type inference offers the benefit of enabling type checking for functions and codebases without requiring the user to add type annotations. To balance these trade-offs, Elixir aims to provide "module type inference": our goal is to infer the types of functions considering the current module, Elixir's standard library and your dependencies, while calls to modules within the same project are assumed to be `dynamic()`. Once types are inferred, then the whole project is type checked considering all modules and all types (inferred or otherwise).
|
||||
On the other hand, type inference offers the benefit of enabling type checking for functions and codebases without requiring the user to add type annotations. To balance these trade-offs, Elixir aims to provide type inference across dependencies: our goal is to infer the types of functions considering the current module, Elixir's standard library and your dependencies, while calls to modules within the same project are assumed to be `dynamic()`. Once types are inferred, then the whole project is type checked considering all modules and all types (inferred or otherwise).
|
||||
|
||||
Type inference in Elixir is best-effort: it doesn't guarantee it will find all possible type incompatibilities, only that it may find bugs where all combinations of a type _will_ fail, even in the absence of explicit type annotations. It is meant to be an efficient routine that brings developers some benefits of static typing, without requiring any effort from them and keeping the expressiveness of the language.
|
||||
|
||||
In the long term, Elixir developers who want static typing guarantees must explicitly add type signatures to their functions (see "Roadmap"). Any function with an explicit type signature will be typed checked against the user-provided annotations, as in other statically typed languages.
|
||||
In the long term, Elixir developers who want static typing guarantees may explicitly add type signatures to their functions (see "Roadmap"). Any function with an explicit type signature will be typed checked against the user-provided annotations, as in other statically typed languages.
|
||||
|
||||
### False positives
|
||||
|
||||
Elixir's type inference generally avoids emitting false positive type violations: which are warnings emitted by the type checker when there are no runtime errors. However, in some situations, those may happen and are documented below.
|
||||
|
||||
#### `for`-comprehensions assume they are executed at least once
|
||||
|
||||
For comprehensions in Elixir assume they are executed at least once. Take this code:
|
||||
|
||||
```elixir
|
||||
def example(x, list) do
|
||||
for _i <- list do
|
||||
Atom.to_string(x)
|
||||
end
|
||||
|
||||
x + 1
|
||||
end
|
||||
```
|
||||
|
||||
`x + 1` will fail because it assumes `x` is an atom from the `Atom.to_string(x)` call, even though the function may raise no runtime error if `list` is an empty list. This is intentional, as it helps find discrepancies inside and outside comprehensions. You can address this by explicitly wrapping the comprehension in a `if list != [] do` block (or similar condition).
|
||||
|
||||
#### Struct update syntax must be statically proven
|
||||
|
||||
Elixir will warn if you use the struct update syntax and it is not statically proven that the given value does not have said struct type. For example:
|
||||
|
||||
```elixir
|
||||
user = find_user_by_id(42)
|
||||
%User{user | name: "John Doe"}
|
||||
```
|
||||
|
||||
Even though it is guaranteed at runtime that user is always a `User` struct. If the type system cannot prove it, it will emit a typing violation. This is how stuct updates work by design. In such cases, you can address it by matching on the struct when the user variable is defined:
|
||||
|
||||
```elixir
|
||||
%User{} = user = find_user_by_id(42)
|
||||
%User{user | name: "John Doe"}
|
||||
```
|
||||
|
||||
## Roadmap
|
||||
|
||||
|
||||
@@ -72,6 +72,15 @@ iex> {x, x} = {1, 2}
|
||||
** (MatchError) no match of right hand side value: {1, 2}
|
||||
```
|
||||
|
||||
However, a variable cannot be defined through itself in the same pattern. This includes cases where the variable appears on both sides of a nested match operator:
|
||||
|
||||
```elixir
|
||||
{:ok, x} = {x, :ok}
|
||||
%{a: value} = %{b: value}
|
||||
```
|
||||
|
||||
Such patterns are rejected as cyclic variable definitions. You can rewrite them by matching once and then using guards or separate comparisons for the additional constraints.
|
||||
|
||||
The underscore variable (`_`) has a special meaning as it can never be bound to any value. It is especially useful when you don't care about certain value in a pattern:
|
||||
|
||||
```iex
|
||||
|
||||
@@ -146,4 +146,4 @@ See the [ORT Mix plugin documentation](https://oss-review-toolkit.org/ort/docs/p
|
||||
|
||||
* [CycloneDX Specification](https://cyclonedx.org/specification/overview/): learn more about the SBoM format
|
||||
* [OWASP Dependency-Track](https://dependencytrack.org/): continuous SBoM analysis platform
|
||||
* [mix_sbom documentation](https://hexdocs.pm/sbom): full documentation and advanced options
|
||||
* [mix_sbom documentation](https://sbom.hexdocs.pm): full documentation and advanced options
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
Elixir is a dynamically typed language, and as such, type specifications are never used by the compiler to optimize or modify code. Still, using type specifications is useful because:
|
||||
|
||||
* they provide documentation (for example, tools such as [`ExDoc`](https://hexdocs.pm/ex_doc/) show type specifications in the documentation)
|
||||
* they provide documentation (for example, tools such as [`ExDoc`](https://ex-doc.hexdocs.pm/) show type specifications in the documentation)
|
||||
* they're used by tools such as [Dialyzer](`:dialyzer`), that can analyze code with typespecs to find type inconsistencies and possible bugs
|
||||
|
||||
Type specifications (most often referred to as *typespecs*) are defined in different contexts using the following attributes:
|
||||
|
||||
@@ -37,7 +37,7 @@ Mixed-script identifiers are not supported for security reasons. 'аdmin' is mad
|
||||
\u006E n {Latin}
|
||||
|
||||
Make sure all characters in the identifier resolve to a single script or a highly
|
||||
restrictive script. See https://hexdocs.pm/elixir/unicode-syntax.html for more information.
|
||||
restrictive script. See https://elixir.hexdocs.pm/unicode-syntax.html for more information.
|
||||
```
|
||||
|
||||
Finally, Elixir will also warn of confusable identifiers in the same file. For example, Elixir will emit a warning if you use both variables `а` (Cyrillic) and `а` (Latin) in your code.
|
||||
|
||||
@@ -85,7 +85,7 @@ defmodule CoverageRecorder do
|
||||
]
|
||||
|
||||
defp skip_from_coverage?(file) do
|
||||
mod = file |> Path.basename(".beam") |> String.to_atom()
|
||||
mod = file |> Path.basename(".beam") |> String.to_unsafe_atom()
|
||||
mod in @to_skip or match?({:docs_v1, _, _, _, _, %{deprecated: _}, _}, Code.fetch_docs(mod))
|
||||
end
|
||||
end
|
||||
|
||||
@@ -42,7 +42,11 @@ latest =
|
||||
version_nodes =
|
||||
for version <- versions do
|
||||
version_string = Version.to_string(version)
|
||||
map = %{version: "v#{version_string}", url: "https://hexdocs.pm/#{app}/#{version_string}"}
|
||||
|
||||
map = %{
|
||||
version: "v#{version_string}",
|
||||
url: "https://#{String.replace(app, "_", "-")}.hexdocs.pm/#{version_string}"
|
||||
}
|
||||
|
||||
if version_string == latest do
|
||||
Map.put(map, :latest, true)
|
||||
|
||||
@@ -3,7 +3,11 @@
|
||||
# SPDX-FileCopyrightText: 2012 Plataformatec
|
||||
|
||||
# Returns config for Elixir docs (exclusively)
|
||||
canonical = System.fetch_env!("CANONICAL")
|
||||
canonical =
|
||||
case System.fetch_env!("CANONICAL") do
|
||||
"" -> System.version() <> "/"
|
||||
canonical -> canonical
|
||||
end
|
||||
|
||||
[
|
||||
search: [
|
||||
@@ -79,11 +83,11 @@ canonical = System.fetch_env!("CANONICAL")
|
||||
"CHANGELOG.md"
|
||||
],
|
||||
deps: [
|
||||
eex: "https://hexdocs.pm/eex/#{canonical}",
|
||||
ex_unit: "https://hexdocs.pm/ex_unit/#{canonical}",
|
||||
iex: "https://hexdocs.pm/iex/#{canonical}",
|
||||
logger: "https://hexdocs.pm/logger/#{canonical}",
|
||||
mix: "https://hexdocs.pm/mix/#{canonical}"
|
||||
eex: "https://eex.hexdocs.pm/#{canonical}",
|
||||
ex_unit: "https://ex-unit.hexdocs.pm/#{canonical}",
|
||||
iex: "https://iex.hexdocs.pm/#{canonical}",
|
||||
logger: "https://logger.hexdocs.pm/#{canonical}",
|
||||
mix: "https://mix.hexdocs.pm/#{canonical}"
|
||||
],
|
||||
groups_for_extras: [
|
||||
"Getting started": ~r"pages/getting-started/.*\.md$",
|
||||
@@ -101,7 +105,9 @@ canonical = System.fetch_env!("CANONICAL")
|
||||
],
|
||||
skip_code_autolink_to: [
|
||||
"Enumerable.List",
|
||||
"Inspect.MapSet"
|
||||
"Inspect.MapSet",
|
||||
"Macro.Env.fetch_alias/2",
|
||||
"Macro.Env.fetch_macro_alias/2"
|
||||
],
|
||||
formatters: ["html", "markdown", "epub"],
|
||||
groups_for_modules: [
|
||||
|
||||
@@ -2,7 +2,11 @@
|
||||
# SPDX-FileCopyrightText: 2021 The Elixir Team
|
||||
|
||||
# Returns config for other apps except Elixir
|
||||
canonical = System.fetch_env!("CANONICAL")
|
||||
canonical =
|
||||
case System.fetch_env!("CANONICAL") do
|
||||
"" -> System.version() <> "/"
|
||||
canonical -> canonical
|
||||
end
|
||||
|
||||
[
|
||||
search: [
|
||||
@@ -21,12 +25,12 @@ canonical = System.fetch_env!("CANONICAL")
|
||||
}
|
||||
],
|
||||
deps: [
|
||||
eex: "https://hexdocs.pm/eex/#{canonical}",
|
||||
elixir: "https://hexdocs.pm/elixir/#{canonical}",
|
||||
ex_unit: "https://hexdocs.pm/ex_unit/#{canonical}",
|
||||
iex: "https://hexdocs.pm/iex/#{canonical}",
|
||||
logger: "https://hexdocs.pm/logger/#{canonical}",
|
||||
mix: "https://hexdocs.pm/mix/#{canonical}"
|
||||
eex: "https://eex.hexdocs.pm/#{canonical}",
|
||||
elixir: "https://elixir.hexdocs.pm/#{canonical}",
|
||||
ex_unit: "https://ex-unit.hexdocs.pm/#{canonical}",
|
||||
iex: "https://iex.hexdocs.pm/#{canonical}",
|
||||
logger: "https://logger.hexdocs.pm/#{canonical}",
|
||||
mix: "https://mix.hexdocs.pm/#{canonical}"
|
||||
],
|
||||
formatters: ["html", "markdown", "epub"],
|
||||
before_closing_body_tag: fn
|
||||
|
||||
@@ -88,6 +88,7 @@ start(_Type, _Args) ->
|
||||
%% Compiler options
|
||||
{debug_info, true},
|
||||
{docs, true},
|
||||
{erlc_options, []},
|
||||
{ignore_already_consolidated, false},
|
||||
{ignore_module_conflict, false},
|
||||
{infer_signatures, [elixir]},
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
}).
|
||||
|
||||
-record(elixir_erl, {
|
||||
%% Can be match, guards or nil
|
||||
%% Can be match, guard or nil
|
||||
context=nil,
|
||||
%% Extra information about the context, like pin_guard and map_key
|
||||
extra=nil,
|
||||
|
||||
@@ -132,7 +132,15 @@ compute_alignment(default, Size, Unit) -> compute_alignment(integer, Size, Unit)
|
||||
compute_alignment(integer, default, Unit) -> compute_alignment(integer, 8, Unit);
|
||||
compute_alignment(integer, Size, default) -> compute_alignment(integer, Size, 1);
|
||||
compute_alignment(bitstring, Size, default) -> compute_alignment(bitstring, Size, 1);
|
||||
compute_alignment(binary, _, _) -> 0;
|
||||
compute_alignment(binary, default, default) -> 0;
|
||||
compute_alignment(binary, default, Unit) when is_integer(Unit) ->
|
||||
case Unit rem 8 of
|
||||
0 -> 0;
|
||||
_ -> unknown
|
||||
end;
|
||||
compute_alignment(binary, Size, default) -> compute_alignment(binary, Size, 8);
|
||||
compute_alignment(binary, _Size, Unit) when is_integer(Unit), Unit rem 8 == 0 -> 0;
|
||||
compute_alignment(binary, _, _) -> unknown;
|
||||
compute_alignment(float, _, _) -> 0;
|
||||
compute_alignment(utf32, _, _) -> 0;
|
||||
compute_alignment(utf16, _, _) -> 0;
|
||||
@@ -180,6 +188,7 @@ expand_specs(ExprType, Meta, Info, S, OriginalS, E, ExpectSize) ->
|
||||
validate_size_required(Meta, ExpectSize, ExprType, MergedType, Size, ES),
|
||||
SizeAndUnit = size_and_unit(Meta, ExprType, Size, Unit, ES),
|
||||
Alignment = compute_alignment(MergedType, Size, Unit),
|
||||
maybe_warn_binary_alignment(Meta, MergedType, Alignment, ES),
|
||||
|
||||
MaybeInferredSize = case {ExpectSize, MergedType, SizeAndUnit} of
|
||||
{{infer, PinnedVar}, binary, []} -> [{size, Meta, [{{'.', Meta, [erlang, byte_size]}, Meta, [PinnedVar]}]}];
|
||||
@@ -307,6 +316,15 @@ validate_size_required(Meta, required, default, Type, default, E) when Type == b
|
||||
validate_size_required(_, _, _, _, _, _) ->
|
||||
ok.
|
||||
|
||||
maybe_warn_binary_alignment(_Meta, binary, Alignment, _E)
|
||||
when is_integer(Alignment), Alignment rem 8 == 0 ->
|
||||
ok;
|
||||
maybe_warn_binary_alignment(Meta, binary, Alignment, E) ->
|
||||
%% TODO: Raise on Elixir v1.25
|
||||
elixir_errors:file_warn(Meta, E, ?MODULE, {unaligned_binary_segment, Alignment});
|
||||
maybe_warn_binary_alignment(_Meta, _Type, _Alignment, _E) ->
|
||||
ok.
|
||||
|
||||
size_and_unit(Meta, bitstring, Size, Unit, E) when Size /= default; Unit /= default ->
|
||||
function_error(Meta, E, ?MODULE, bittype_literal_bitstring),
|
||||
[];
|
||||
@@ -375,6 +393,14 @@ add_spec(Key, Spec) -> [{Key, [], nil} | Spec].
|
||||
format_error({unaligned_binary, Expr}) ->
|
||||
Message = "expected ~ts to be a binary but its number of bits is not divisible by 8",
|
||||
io_lib:format(Message, ['Elixir.Macro':to_string(Expr)]);
|
||||
format_error({unaligned_binary_segment, unknown}) ->
|
||||
"a binary segment must have a size-unit pair that is always divisible by 8. "
|
||||
"Use bitstring instead";
|
||||
format_error({unaligned_binary_segment, Alignment}) ->
|
||||
Message =
|
||||
"a binary segment must have a size-unit pair divisible by 8, got a segment size of ~B bits. "
|
||||
"Use bitstring instead",
|
||||
io_lib:format(Message, [Alignment]);
|
||||
format_error(unsized_binary) ->
|
||||
"a binary field without size is only allowed at the end of a binary pattern, "
|
||||
"at the right side of binary concatenation and never allowed in binary generators. "
|
||||
|
||||
@@ -562,16 +562,16 @@ format_error({recursive, Vars, TypeExpr}) ->
|
||||
Message =
|
||||
case lists:map(fun({Name, Context}) -> elixir_utils:var_info(Name, Context) end, lists:sort(Vars)) of
|
||||
[Var] ->
|
||||
io_lib:format("the variable ~ts is defined in function of itself", [Var]);
|
||||
io_lib:format("the variable ~ts depends on itself through the pattern", [Var]);
|
||||
[Var1, Var2] ->
|
||||
io_lib:format("the variable ~ts is defined recursively in function of ~ts", [Var1, Var2]);
|
||||
io_lib:format("the variable ~ts depends on ~ts through the pattern", [Var1, Var2]);
|
||||
[Head | Tail] ->
|
||||
List = lists:foldl(fun(X, Acc) -> [Acc, $,, $\s, X] end, Head, Tail),
|
||||
io_lib:format("the following variables form a cycle: ~ts", [List])
|
||||
end,
|
||||
|
||||
io_lib:format(
|
||||
"recursive variable definition in patterns:~n~n~ts~n~n~ts",
|
||||
"cyclic variable definition in patterns:~n~n~ts~n~n~ts",
|
||||
[Code, Message]
|
||||
);
|
||||
|
||||
|
||||
@@ -59,6 +59,9 @@ handle_call({acquire, Path}, From, Config) ->
|
||||
handle_call(required, _From, Config) ->
|
||||
{reply, [F || {F, true} <- maps:to_list(Config#elixir_code_server.required)], Config};
|
||||
|
||||
handle_call({release, Path}, _From, Config) ->
|
||||
{reply, ok, release(Path, Config)};
|
||||
|
||||
handle_call(retrieve_compiler_module, _From, Config) ->
|
||||
case Config#elixir_code_server.mod_pool of
|
||||
{Used, [Mod | Unused], Counter} ->
|
||||
@@ -140,6 +143,20 @@ terminate(_Reason, _Config) ->
|
||||
code_change(_Old, Config, _Extra) ->
|
||||
{ok, Config}.
|
||||
|
||||
release(Path, Config) ->
|
||||
Current = Config#elixir_code_server.required,
|
||||
case maps:find(Path, Current) of
|
||||
{ok, []} ->
|
||||
Released = maps:remove(Path, Current),
|
||||
Config#elixir_code_server{required=Released};
|
||||
{ok, [Next | Waiting]} ->
|
||||
_ = gen_server:reply(Next, proceed),
|
||||
Released = maps:put(Path, Waiting, Current),
|
||||
Config#elixir_code_server{required=Released};
|
||||
error ->
|
||||
Config
|
||||
end.
|
||||
|
||||
compiler_module(I) ->
|
||||
list_to_atom("elixir_compiler_" ++ integer_to_list(I)).
|
||||
|
||||
|
||||
@@ -310,7 +310,8 @@ unpack_expanded(_Kind, _Meta, _Name, [], _Counter, Acc, Clauses) ->
|
||||
|
||||
expand_defaults([{'\\\\', Meta, [Expr, Default]} | Args], S, E, Acc) ->
|
||||
{ExpandedDefault, SE, _} = elixir_expand:expand(Default, S, E),
|
||||
expand_defaults(Args, SE, E, [{'\\\\', Meta, [Expr, ExpandedDefault]} | Acc]);
|
||||
expand_defaults(Args, elixir_env:reset_read(SE, S), E,
|
||||
[{'\\\\', Meta, [Expr, ExpandedDefault]} | Acc]);
|
||||
expand_defaults([Arg | Args], S, E, Acc) ->
|
||||
expand_defaults(Args, S, E, [Arg | Acc]);
|
||||
expand_defaults([], S, _E, Acc) ->
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
-define(typespecs, 'Elixir.Kernel.Typespec').
|
||||
|
||||
checker_version() ->
|
||||
elixir_checker_v8.
|
||||
elixir_checker_v9.
|
||||
|
||||
%% debug_info callback
|
||||
|
||||
|
||||
@@ -60,16 +60,30 @@ env_compiler_options() ->
|
||||
|
||||
erl_to_core(Forms, Opts) ->
|
||||
%% TODO: Remove parse transform handling on Elixir v2.0
|
||||
case [M || {parse_transform, M} <- Opts] of
|
||||
[] ->
|
||||
case requires_abstract_format(Opts, false) of
|
||||
false ->
|
||||
v3_core:module(Forms, Opts);
|
||||
_ ->
|
||||
|
||||
beam_debug_info ->
|
||||
{ok, DbgForms} = sys_coverage:beam_debug_info(Forms),
|
||||
v3_core:module(DbgForms, Opts);
|
||||
|
||||
parse_transform ->
|
||||
case compile:noenv_forms(Forms, [no_spawn_compiler_process, to_core0, return, no_auto_import | Opts]) of
|
||||
{ok, _Module, Core, Warnings} -> {ok, Core, Warnings};
|
||||
{error, Errors, Warnings} -> {error, Errors, Warnings}
|
||||
end
|
||||
end.
|
||||
|
||||
requires_abstract_format([{parse_transform, _} | _], _Default) ->
|
||||
parse_transform;
|
||||
requires_abstract_format([beam_debug_info | Tail], _Default) ->
|
||||
requires_abstract_format(Tail, beam_debug_info);
|
||||
requires_abstract_format([_ | Tail], Default) ->
|
||||
requires_abstract_format(Tail, Default);
|
||||
requires_abstract_format([], Default) ->
|
||||
Default.
|
||||
|
||||
noenv_forms(Forms, File, Opts) when is_list(Forms), is_list(Opts), is_binary(File) ->
|
||||
Source = elixir_utils:characters_to_list(File),
|
||||
|
||||
|
||||
@@ -14,12 +14,15 @@
|
||||
translate(Meta, Args, S) ->
|
||||
{Cases, [{do, Expr} | Opts]} = elixir_utils:split_last(Args),
|
||||
|
||||
% needs to be the original variables, excluding variables from generators
|
||||
InitVars = S#elixir_erl.var_names,
|
||||
|
||||
case lists:keyfind(reduce, 1, Opts) of
|
||||
{reduce, Reduce} -> translate_reduce(Meta, Cases, Expr, Reduce, S);
|
||||
false -> translate_into(Meta, Cases, Expr, Opts, S)
|
||||
{reduce, Reduce} -> translate_reduce(Meta, Cases, Expr, Reduce, InitVars, S);
|
||||
false -> translate_into(Meta, Cases, Expr, Opts, InitVars, S)
|
||||
end.
|
||||
|
||||
translate_reduce(Meta, Cases, Expr, Reduce, S) ->
|
||||
translate_reduce(Meta, Cases, Expr, Reduce, InitVars, S) ->
|
||||
Ann = ?ann(Meta),
|
||||
{TReduce, SR} = elixir_erl_pass:translate(Reduce, Ann, S),
|
||||
{TCases, SC} = translate_gen(Meta, Cases, [], SR),
|
||||
@@ -30,9 +33,9 @@ translate_reduce(Meta, Cases, Expr, Reduce, S) ->
|
||||
({'case', CaseAnn, _, CaseBlock}, InnerAcc) -> {'case', CaseAnn, InnerAcc, CaseBlock}
|
||||
end,
|
||||
|
||||
build_reduce(Ann, TCases, InnerFun, TExpr, TReduce, false, SE).
|
||||
build_reduce(Ann, TCases, InnerFun, TExpr, TReduce, false, InitVars, SE).
|
||||
|
||||
translate_into(Meta, Cases, Expr, Opts, S) ->
|
||||
translate_into(Meta, Cases, Expr, Opts, InitVars, S) ->
|
||||
Ann = ?ann(Meta),
|
||||
|
||||
{TInto, SI} =
|
||||
@@ -47,8 +50,8 @@ translate_into(Meta, Cases, Expr, Opts, S) ->
|
||||
{TExpr, SE} = elixir_erl_pass:translate(wrap_expr_if_unused(Expr, TInto), Ann, SC),
|
||||
|
||||
case inline_or_into(TInto) of
|
||||
inline -> build_inline(Ann, TCases, TExpr, TInto, TUniq, SE);
|
||||
into -> build_into(Ann, TCases, TExpr, TInto, TUniq, SE)
|
||||
inline -> build_inline(Ann, TCases, TExpr, TInto, TUniq, InitVars, SE);
|
||||
into -> build_into(Ann, TCases, TExpr, TInto, TUniq, InitVars, SE)
|
||||
end.
|
||||
|
||||
%% In case we have no return, we wrap the expression
|
||||
@@ -115,29 +118,29 @@ collect_filters([H | T], Acc) ->
|
||||
collect_filters([], Acc) ->
|
||||
{Acc, []}.
|
||||
|
||||
build_inline(Ann, Clauses, Expr, Into, Uniq, S) ->
|
||||
build_inline(Ann, Clauses, Expr, Into, Uniq, InitVars, S) ->
|
||||
case not Uniq and lists:all(fun(Clause) -> element(1, Clause) == bin end, Clauses) of
|
||||
true -> {build_comprehension(Ann, Clauses, Expr, Into), S};
|
||||
false -> build_inline_each(Ann, Clauses, Expr, Into, Uniq, S)
|
||||
false -> build_inline_each(Ann, Clauses, Expr, Into, Uniq, InitVars, S)
|
||||
end.
|
||||
|
||||
build_inline_each(Ann, Clauses, Expr, false, Uniq, S) ->
|
||||
build_inline_each(Ann, Clauses, Expr, false, Uniq, InitVars, S) ->
|
||||
InnerFun = fun(InnerExpr, _InnerAcc) -> InnerExpr end,
|
||||
build_reduce(Ann, Clauses, InnerFun, Expr, {nil, Ann}, Uniq, S);
|
||||
build_inline_each(Ann, [{enum, _, Left = {var, _, _}, Right, [] = _Filters}], Expr, {nil, _} = _Into, false, S) ->
|
||||
build_reduce(Ann, Clauses, InnerFun, Expr, {nil, Ann}, Uniq, InitVars, S);
|
||||
build_inline_each(Ann, [{enum, _, Left = {var, _, _}, Right, [] = _Filters}], Expr, {nil, _} = _Into, false, _InitVars, S) ->
|
||||
Clauses = [{clause, Ann, [Left], [], [Expr]}],
|
||||
Args = [Right, {'fun', Ann, {clauses, Clauses}}],
|
||||
{?remote(Ann, 'Elixir.Enum', map, Args), S};
|
||||
build_inline_each(Ann, [{enum, _, Left = {var, _, _}, Right, [] = _Filters}], Expr, {map, _, []} = _Into, false, S) ->
|
||||
build_inline_each(Ann, [{enum, _, Left = {var, _, _}, Right, [] = _Filters}], Expr, {map, _, []} = _Into, false, _InitVars, S) ->
|
||||
Clauses = [{clause, Ann, [Left], [], [Expr]}],
|
||||
Args = [Right, {'fun', Ann, {clauses, Clauses}}],
|
||||
List = ?remote(Ann, 'Elixir.Enum', map, Args),
|
||||
{?remote(Ann, maps, from_list, [List]), S};
|
||||
build_inline_each(Ann, Clauses, Expr, {nil, _} = Into, Uniq, S) ->
|
||||
build_inline_each(Ann, Clauses, Expr, {nil, _} = Into, Uniq, InitVars, S) ->
|
||||
InnerFun = fun(InnerExpr, InnerAcc) -> {cons, Ann, InnerExpr, InnerAcc} end,
|
||||
{ReduceExpr, SR} = build_reduce(Ann, Clauses, InnerFun, Expr, Into, Uniq, S),
|
||||
{ReduceExpr, SR} = build_reduce(Ann, Clauses, InnerFun, Expr, Into, Uniq, InitVars, S),
|
||||
{?remote(Ann, lists, reverse, [ReduceExpr]), SR};
|
||||
build_inline_each(Ann, Clauses, Expr, {bin, _, []}, Uniq, S) ->
|
||||
build_inline_each(Ann, Clauses, Expr, {bin, _, []}, Uniq, InitVars, S) ->
|
||||
{InnerValue, SV} = build_var(Ann, S),
|
||||
Generated = erl_anno:set_generated(true, Ann),
|
||||
|
||||
@@ -155,17 +158,17 @@ build_inline_each(Ann, Clauses, Expr, {bin, _, []}, Uniq, S) ->
|
||||
]}
|
||||
end,
|
||||
|
||||
{ReduceExpr, SR} = build_reduce(Ann, Clauses, InnerFun, Expr, {nil, Ann}, Uniq, SV),
|
||||
{ReduceExpr, SR} = build_reduce(Ann, Clauses, InnerFun, Expr, {nil, Ann}, Uniq, InitVars, SV),
|
||||
{?remote(Ann, erlang, list_to_bitstring, [ReduceExpr]), SR}.
|
||||
|
||||
build_into(Ann, Clauses, Expr, {map, _, []}, Uniq, S) ->
|
||||
{ReduceExpr, SR} = build_inline_each(Ann, Clauses, Expr, {nil, Ann}, Uniq, S),
|
||||
build_into(Ann, Clauses, Expr, {map, _, []}, Uniq, InitVars, S) ->
|
||||
{ReduceExpr, SR} = build_inline_each(Ann, Clauses, Expr, {nil, Ann}, Uniq, InitVars, S),
|
||||
{?remote(Ann, maps, from_list, [ReduceExpr]), SR};
|
||||
build_into(Ann, Clauses, Expr, ?empty_map_set_pattern = _Into, Uniq, S) ->
|
||||
build_into(Ann, Clauses, Expr, ?empty_map_set_pattern = _Into, Uniq, InitVars, S) ->
|
||||
InnerFun = fun(InnerExpr, InnerAcc) -> {cons, Ann, InnerExpr, InnerAcc} end,
|
||||
{ReduceExpr, SR} = build_reduce(Ann, Clauses, InnerFun, Expr, {nil, Ann}, Uniq, S),
|
||||
{ReduceExpr, SR} = build_reduce(Ann, Clauses, InnerFun, Expr, {nil, Ann}, Uniq, InitVars, S),
|
||||
{?remote(Ann, 'Elixir.MapSet', new, [ReduceExpr]), SR};
|
||||
build_into(Ann, Clauses, Expr, Into, Uniq, S) ->
|
||||
build_into(Ann, Clauses, Expr, Into, Uniq, InitVars, S) ->
|
||||
{Fun, SF} = build_var(Ann, S),
|
||||
{Acc, SA} = build_var(Ann, SF),
|
||||
{Kind, SK} = build_var(Ann, SA),
|
||||
@@ -182,7 +185,7 @@ build_into(Ann, Clauses, Expr, Into, Uniq, S) ->
|
||||
?remote(Ann, 'Elixir.Collectable', into, [Into])
|
||||
},
|
||||
|
||||
{IntoReduceExpr, SN} = build_reduce(Ann, Clauses, InnerFun, Expr, Acc, Uniq, SD),
|
||||
{IntoReduceExpr, SN} = build_reduce(Ann, Clauses, InnerFun, Expr, Acc, Uniq, InitVars, SD),
|
||||
|
||||
TryExpr =
|
||||
{'try', Ann,
|
||||
@@ -205,10 +208,10 @@ stacktrace_clause(Ann, Fun, Acc, Kind, Reason, Stack) ->
|
||||
|
||||
%% Helpers
|
||||
|
||||
build_reduce(Ann, Clauses, InnerFun, Expr, Into, false, S) ->
|
||||
build_reduce(Ann, Clauses, InnerFun, Expr, Into, false, InitVars, S) ->
|
||||
{Acc, SA} = build_var(Ann, S),
|
||||
{build_reduce_each(Clauses, InnerFun(Expr, Acc), Into, Acc, SA), SA};
|
||||
build_reduce(Ann, Clauses, InnerFun, Expr, Into, true, S) ->
|
||||
{build_reduce_each(Clauses, InnerFun(Expr, Acc), Into, Acc, InitVars, SA), SA};
|
||||
build_reduce(Ann, Clauses, InnerFun, Expr, Into, true, InitVars, S) ->
|
||||
%% Those variables are used only inside the anonymous function
|
||||
%% so we don't need to worry about returning the scope.
|
||||
{Acc, SA} = build_var(Ann, S),
|
||||
@@ -229,12 +232,12 @@ build_reduce(Ann, Clauses, InnerFun, Expr, Into, true, S) ->
|
||||
]}
|
||||
]},
|
||||
|
||||
EnumReduceCall = build_reduce_each(Clauses, InnerExpr, NewInto, Acc, SU),
|
||||
EnumReduceCall = build_reduce_each(Clauses, InnerExpr, NewInto, Acc, InitVars, SU),
|
||||
{?remote(Ann, erlang, element, [{integer, Ann, 1}, EnumReduceCall]), SU}.
|
||||
|
||||
build_reduce_each([{enum, Meta, Left, Right, Filters} | T], Expr, Arg, Acc, S) ->
|
||||
build_reduce_each([{enum, Meta, Left, Right, Filters} | T], Expr, Arg, Acc, InitVars, S) ->
|
||||
Ann = ?ann(Meta),
|
||||
True = build_reduce_each(T, Expr, Acc, Acc, S),
|
||||
True = build_reduce_each(T, Expr, Acc, Acc, InitVars, S),
|
||||
False = Acc,
|
||||
Generated = erl_anno:set_generated(true, Ann),
|
||||
|
||||
@@ -255,13 +258,13 @@ build_reduce_each([{enum, Meta, Left, Right, Filters} | T], Expr, Arg, Acc, S) -
|
||||
Args = [Right, Arg, {'fun', Ann, {clauses, Clauses1}}],
|
||||
?remote(Ann, 'Elixir.Enum', reduce, Args);
|
||||
|
||||
build_reduce_each([{bin, Meta, Left, Right, Filters} | T], Expr, Arg, Acc, S) ->
|
||||
build_reduce_each([{bin, Meta, Left, Right, Filters} | T], Expr, Arg, Acc, InitVars, S) ->
|
||||
Ann = ?ann(Meta),
|
||||
Generated = erl_anno:set_generated(true, Ann),
|
||||
{Tail, ST} = build_var(Ann, S),
|
||||
{Fun, SF} = build_var(Ann, ST),
|
||||
|
||||
True = build_reduce_each(T, Expr, Acc, Acc, SF),
|
||||
True = build_reduce_each(T, Expr, Acc, Acc, InitVars, SF),
|
||||
False = Acc,
|
||||
{bin, _, Elements} = Left,
|
||||
TailElement = {bin_element, Ann, Tail, default, [bitstring]},
|
||||
@@ -275,7 +278,7 @@ build_reduce_each([{bin, Meta, Left, Right, Filters} | T], Expr, Arg, Acc, S) ->
|
||||
[?remote(Ann, erlang, error, [pair(Ann, badarg, Tail)])]}],
|
||||
|
||||
NoVarClauses =
|
||||
case no_var(Generated, Elements) of
|
||||
case no_unbound_var(Generated, Elements, InitVars) of
|
||||
error ->
|
||||
Clauses;
|
||||
|
||||
@@ -294,7 +297,7 @@ build_reduce_each([{bin, Meta, Left, Right, Filters} | T], Expr, Arg, Acc, S) ->
|
||||
{named_fun, Ann, element(3, Fun), VarClauses},
|
||||
[Right, Arg]};
|
||||
|
||||
build_reduce_each([], Expr, _Arg, _Acc, _S) ->
|
||||
build_reduce_each([], Expr, _Arg, _Acc, _InitVars, _S) ->
|
||||
Expr.
|
||||
|
||||
is_var({var, _, _}) -> true;
|
||||
@@ -307,9 +310,10 @@ build_var(Ann, S) ->
|
||||
{Name, ST} = elixir_erl_var:build('_', S),
|
||||
{{var, Ann, Name}, ST}.
|
||||
|
||||
no_var(ParentAnn, Elements) ->
|
||||
no_unbound_var(ParentAnn, Elements, InitVars) ->
|
||||
Vars = #{V => K || K := V <- InitVars},
|
||||
try
|
||||
[{bin_element, Ann, NoVarExpr, no_var_size(Size), Types} ||
|
||||
[{bin_element, Ann, NoVarExpr, no_unbound_var_size(Size, Vars), Types} ||
|
||||
{bin_element, Ann, Expr, Size, Types} <- Elements,
|
||||
NoVarExpr <- no_var_expr(ParentAnn, Expr)]
|
||||
catch
|
||||
@@ -319,9 +323,17 @@ no_var(ParentAnn, Elements) ->
|
||||
no_var_expr(Ann, {string, _, String}) -> [{var, Ann, '_'} || _ <- String];
|
||||
no_var_expr(Ann, _) -> [{var, Ann, '_'}].
|
||||
|
||||
no_var_size(default) -> default;
|
||||
no_var_size(Size) when is_integer(Size) -> Size;
|
||||
no_var_size(_) -> throw(unbound_size).
|
||||
no_unbound_var_size(Size, Vars) ->
|
||||
valid_var_size(Size, Vars) orelse throw(unbound_size),
|
||||
Size.
|
||||
|
||||
valid_var_size({var, _, Var}, Vars) when is_map_key(Var, Vars) -> true;
|
||||
valid_var_size(default, _Vars) -> true;
|
||||
valid_var_size({integer, _, _}, _Vars) -> true;
|
||||
valid_var_size(Size, _Vars) when is_integer(Size) -> true;
|
||||
valid_var_size({op, _Ann, _Op, Left, Right}, Vars) ->
|
||||
valid_var_size(Left, Vars) andalso valid_var_size(Right, Vars);
|
||||
valid_var_size(_Size, _vars) -> false.
|
||||
|
||||
build_comprehension(Ann, Clauses, Expr, Into) ->
|
||||
{comprehension_kind(Into), Ann, Expr, comprehension_clause(Clauses)}.
|
||||
|
||||
@@ -576,6 +576,10 @@ translate_remote('Elixir.String.Chars', to_string, Meta, [Arg], S) ->
|
||||
{clause, Generated, [Var], [[Guard]], [Fast]},
|
||||
{clause, Generated, [Var], [], [Slow]}
|
||||
]}, VS};
|
||||
translate_remote(lists, member, Meta, [Expr, []], S) ->
|
||||
Ann = ?ann(Meta),
|
||||
{TExpr, S1} = translate(Expr, Ann, S),
|
||||
{{block, Ann, [{match, Ann, {var, Ann, '_'}, TExpr}, {atom, Ann, false}]}, S1};
|
||||
translate_remote(lists, member, Meta, [Expr, [Head | Tail] = List], S) ->
|
||||
Ann = ?ann(Meta),
|
||||
|
||||
@@ -630,10 +634,35 @@ translate_remote(maps, merge, Meta, [Map1, Map2], S) ->
|
||||
{[TMap1, TMap2], TS} ->
|
||||
{{call, Ann, {remote, Ann, {atom, Ann, maps}, {atom, Ann, merge}}, [TMap1, TMap2]}, TS}
|
||||
end;
|
||||
translate_remote(Mod, to_existing_atom, Meta, [String, List], S) when Mod == 'Elixir.String'; Mod == 'Elixir.List' ->
|
||||
Ann = ?ann(Meta),
|
||||
{[TString, TList], TS} = translate_args([String, List], Ann, S),
|
||||
|
||||
case is_list(List) andalso lists:all(fun is_atom/1, List) of
|
||||
true ->
|
||||
Generated = erl_anno:set_generated(true, Ann),
|
||||
LastClause = {clause, Generated,
|
||||
[{var, Generated, '_'}],
|
||||
[],
|
||||
[{call, Ann, {remote, Ann, {atom, Ann, Mod}, {atom, Ann, '__to_existing_atom__'}}, [TString, TList]}]},
|
||||
CastAtom = case Mod of
|
||||
'Elixir.String' -> fun atom_to_binary/1;
|
||||
'Elixir.List' -> fun atom_to_list/1
|
||||
end,
|
||||
Clauses = [
|
||||
{clause, Generated,
|
||||
[elixir_erl:elixir_to_erl(CastAtom(Atom), Ann)],
|
||||
[],
|
||||
[{atom, Ann, Atom}]}
|
||||
|| Atom <- List] ++ [LastClause],
|
||||
{{'case', Generated, TString, Clauses}, TS};
|
||||
false ->
|
||||
{{call, Ann, {remote, Ann, {atom, Ann, Mod}, {atom, Ann, to_existing_atom}}, [TString, TList]}, TS}
|
||||
end;
|
||||
translate_remote(Left, Right, Meta, Args, S) ->
|
||||
Ann = ?ann(Meta),
|
||||
|
||||
case rewrite_strategy(Left, Right, Args) of
|
||||
case rewrite_strategy(Left, Right, Args, S) of
|
||||
guard_op ->
|
||||
{TArgs, SA} = translate_args(Args, Ann, S),
|
||||
%% Rewrite Erlang function calls as operators so they
|
||||
@@ -671,13 +700,9 @@ optimize_list_membership([], _Count) ->
|
||||
optimize_list_membership(_, _Count) ->
|
||||
false.
|
||||
|
||||
rewrite_strategy(erlang, Right, Args) ->
|
||||
Arity = length(Args),
|
||||
case elixir_utils:guard_op(Right, Arity) of
|
||||
true -> guard_op;
|
||||
false -> none
|
||||
end;
|
||||
rewrite_strategy(Left, shift, [Struct, Opts | RestArgs]) when
|
||||
rewrite_strategy(erlang, Right, Args, S) ->
|
||||
guard_op_strategy(Right, length(Args), S);
|
||||
rewrite_strategy(Left, shift, [Struct, Opts | RestArgs], _S) when
|
||||
Left == 'Elixir.Date';
|
||||
Left == 'Elixir.DateTime';
|
||||
Left == 'Elixir.NaiveDateTime';
|
||||
@@ -694,7 +719,7 @@ rewrite_strategy(Left, shift, [Struct, Opts | RestArgs]) when
|
||||
false ->
|
||||
none
|
||||
end;
|
||||
rewrite_strategy(Left, Right, Args) ->
|
||||
rewrite_strategy(Left, Right, Args, _S) ->
|
||||
case inline_pure_function(Left, Right) andalso basic_type_arg(Args) of
|
||||
true ->
|
||||
try
|
||||
@@ -707,13 +732,21 @@ rewrite_strategy(Left, Right, Args) ->
|
||||
reorder_strategy(Left, Right, length(Args))
|
||||
end.
|
||||
|
||||
guard_op_strategy('andalso', 2, #elixir_erl{context=guard}) -> guard_op;
|
||||
guard_op_strategy('orelse', 2, #elixir_erl{context=guard}) -> guard_op;
|
||||
guard_op_strategy(Op, Arity, _S) ->
|
||||
case elixir_utils:guard_op(Op, Arity) of
|
||||
true -> guard_op;
|
||||
false -> none
|
||||
end.
|
||||
|
||||
-define(
|
||||
reorder(ExMod, ExFun, ExArity, ExArgs, ErlMod, ErlFun, ErlArgs),
|
||||
reorder_strategy(ExMod, ExFun, ExArity) -> {reorder, ErlMod, ErlFun, fun(Ann, ExArgs) -> _ = Ann, ErlArgs end}
|
||||
).
|
||||
|
||||
?reorder('Elixir.Kernel', elem, 2, [Tuple, Index], erlang, element, [increment(Ann, Index), Tuple]);
|
||||
?reorder('Elixir.Kernel', put_elem, 2, [Tuple, Index, Term], erlang, element, [increment(Ann, Index), Tuple, Term]);
|
||||
?reorder('Elixir.Kernel', put_elem, 3, [Tuple, Index, Term], erlang, setelement, [increment(Ann, Index), Tuple, Term]);
|
||||
?reorder('Elixir.Kernel', is_map_key, 2, [Map, Key], erlang, is_map_key, [Key, Map]);
|
||||
?reorder('Elixir.Map', delete, 2, [Map, Key], maps, remove, [Key, Map]);
|
||||
?reorder('Elixir.Map', fetch, 2, [Map, Key], maps, find, [Key, Map]);
|
||||
|
||||
@@ -266,6 +266,9 @@ expand({quote, Meta, [Opts, Do]}, S, E) when is_list(Do) ->
|
||||
Unquote = proplists:get_value(unquote, EOpts, DefaultUnquote),
|
||||
Generated = proplists:get_value(generated, EOpts, false),
|
||||
|
||||
(map_get(context, E) /= nil) andalso Unquote andalso elixir_quote:has_unquotes(Exprs) andalso
|
||||
file_error(Meta, E, ?MODULE, quote_in_pattern_with_unquote),
|
||||
|
||||
{Q, QContext, QPrelude} = elixir_quote:build(Meta, Line, File, Context, Unquote, Generated, ET),
|
||||
{EPrelude, SP, EP} = expand(QPrelude, ST, ET),
|
||||
{EContext, SC, EC} = expand(QContext, SP, EP),
|
||||
@@ -287,9 +290,15 @@ expand({quote, Meta, [Opts, Do]}, S, E) when is_list(Do) ->
|
||||
_ -> {'{}', [], ['__block__', [], EBinding ++ [EQuoted]]}
|
||||
end,
|
||||
|
||||
case EPrelude of
|
||||
[] -> {EBindingQuoted, ES, EQ};
|
||||
_ -> {{'__block__', [], EPrelude ++ [EBindingQuoted]}, ES, EQ}
|
||||
EBlock =
|
||||
case EPrelude of
|
||||
[] -> EBindingQuoted;
|
||||
_ -> {'__block__', [], EPrelude ++ [EBindingQuoted]}
|
||||
end,
|
||||
|
||||
case ?key(E, context) of
|
||||
nil -> {{{'.', Meta, [elixir_quote, validate_quote]}, Meta, [EBlock]}, ES, EQ};
|
||||
_ -> {EBlock, ES, EQ}
|
||||
end;
|
||||
|
||||
expand({quote, Meta, [_, _]}, _S, E) ->
|
||||
@@ -445,32 +454,49 @@ expand({Name, Meta, Kind}, S, E) when is_atom(Name), is_atom(Kind) ->
|
||||
#{Pair := CurrentVersion} ->
|
||||
case Prematch of
|
||||
{Pre, _Cycle, {bitsize, Original}} ->
|
||||
if
|
||||
map_get(Pair, Pre) /= CurrentVersion ->
|
||||
case {Original, Pre} of
|
||||
{#{Pair := OriginalVersion}, _} when OriginalVersion /= CurrentVersion ->
|
||||
{ok, CurrentVersion};
|
||||
|
||||
is_map_key(Pair, Pre) ->
|
||||
{#{Pair := CurrentVersion}, #{Pair := CurrentVersion}} ->
|
||||
%% TODO: Remove me on Elixir 2.0
|
||||
elixir_errors:file_warn(Meta, E, ?MODULE, {unpinned_bitsize_var, Name, Kind}),
|
||||
{ok, CurrentVersion};
|
||||
|
||||
not is_map_key(Pair, Original) ->
|
||||
{ok, CurrentVersion};
|
||||
{#{Pair := CurrentVersion}, #{Pair := _}} ->
|
||||
{raise, {unpinned_bitsize_var, Name, Kind}};
|
||||
|
||||
true ->
|
||||
raise
|
||||
{#{Pair := CurrentVersion}, _} ->
|
||||
{raise, {undefined_var, Name, Kind}};
|
||||
|
||||
{_, _} ->
|
||||
{ok, CurrentVersion}
|
||||
end;
|
||||
|
||||
_ ->
|
||||
{ok, CurrentVersion}
|
||||
end;
|
||||
|
||||
_ when S#elixir_ex.prematch =:= pin ->
|
||||
{raise, {undefined_var_pin, Name, Kind}};
|
||||
|
||||
_ ->
|
||||
case E of
|
||||
#{context := guard} -> raise;
|
||||
#{} when S#elixir_ex.prematch =:= pin -> pin;
|
||||
%% TODO: Remove fallback on on_undefined_variable
|
||||
_ -> elixir_config:get(on_undefined_variable)
|
||||
case lists:keyfind(if_undefined, 1, Meta) of
|
||||
{if_undefined, apply} ->
|
||||
apply;
|
||||
|
||||
{if_undefined, raise} ->
|
||||
{raise, {undefined_var, Name, Kind}};
|
||||
|
||||
_ when ?key(E, context) =:= guard ->
|
||||
{raise, {undefined_var, Name, Kind}};
|
||||
|
||||
_ ->
|
||||
%% TODO: Remove fallback as it should always raise
|
||||
case elixir_config:get(on_undefined_variable) of
|
||||
raise -> {raise, {undefined_var, Name, Kind}};
|
||||
Other -> Other
|
||||
end
|
||||
end
|
||||
end,
|
||||
|
||||
@@ -480,30 +506,18 @@ expand({Name, Meta, Kind}, S, E) when is_atom(Name), is_atom(Kind) ->
|
||||
Var = {Name, [{version, PairVersion} | Meta], Kind},
|
||||
{Var, S#elixir_ex{unused=var_used(Pair, Meta, PairVersion, Unused)}, E};
|
||||
|
||||
Error ->
|
||||
case lists:keyfind(if_undefined, 1, Meta) of
|
||||
{if_undefined, apply} ->
|
||||
expand({Name, Meta, []}, S, E);
|
||||
{raise, Reason} ->
|
||||
SpanMeta = elixir_env:calculate_span(Meta, Name),
|
||||
function_error(SpanMeta, E, ?MODULE, Reason),
|
||||
{{Name, SpanMeta, Kind}, S#elixir_ex{tainted_function=true}, E};
|
||||
|
||||
%% TODO: Remove this clause on v2.0 as we will raise by default
|
||||
{if_undefined, raise} ->
|
||||
function_error(Meta, E, ?MODULE, {undefined_var, Name, Kind}),
|
||||
{{Name, Meta, Kind}, S#elixir_ex{tainted_function=true}, E};
|
||||
apply ->
|
||||
expand({Name, Meta, []}, S, E);
|
||||
|
||||
%% TODO: Remove this clause on v2.0 as we will no longer support warn
|
||||
_ when Error == warn ->
|
||||
elixir_errors:file_warn(Meta, E, ?MODULE, {undefined_var_to_call, Name}),
|
||||
expand({Name, [{if_undefined, warn} | Meta], []}, S, E);
|
||||
|
||||
_ when Error == pin ->
|
||||
function_error(Meta, E, ?MODULE, {undefined_var_pin, Name, Kind}),
|
||||
{{Name, Meta, Kind}, S#elixir_ex{tainted_function=true}, E};
|
||||
|
||||
_ when Error == raise ->
|
||||
SpanMeta = elixir_env:calculate_span(Meta, Name),
|
||||
function_error(SpanMeta, E, ?MODULE, {undefined_var, Name, Kind}),
|
||||
{{Name, SpanMeta, Kind}, S#elixir_ex{tainted_function=true}, E}
|
||||
end
|
||||
%% TODO: Remove this clause on v2.0 as we will no longer support warn
|
||||
warn ->
|
||||
elixir_errors:file_warn(Meta, E, ?MODULE, {undefined_var_to_call, Name}),
|
||||
expand({Name, [{if_undefined, warn} | Meta], []}, S, E)
|
||||
end;
|
||||
|
||||
%% Local calls
|
||||
@@ -964,9 +978,13 @@ expand_remote(Receiver, DotMeta, Right, Meta, Args, S, SL, #{context := Context}
|
||||
{EArgs, {SA, _}, EA} = mapfold(fun expand_arg/3, {SL, S}, E, Args),
|
||||
|
||||
SA#elixir_ex.tainted_function andalso is_atom(Receiver) andalso
|
||||
(not is_loaded_and_exported(Receiver, Right, Args)) andalso
|
||||
is_loaded_and_not_exported(Receiver, Right, Args) andalso
|
||||
elixir_errors:file_warn(Meta, E, ?MODULE, {undefined_function, Receiver, Right, length(Args)}),
|
||||
|
||||
%% TODO: Raise on Elixir v2.0
|
||||
(Receiver =:= erlang) andalso ((Right =:= 'orelse') orelse (Right =:= 'andalso')) andalso
|
||||
elixir_errors:file_warn(Meta, E, ?MODULE, {invalid_guard_operator, Right}),
|
||||
|
||||
Rewritten = elixir_rewrite:rewrite(Receiver, DotMeta, Right, AttachedMeta, EArgs),
|
||||
{Rewritten, elixir_env:close_write(SA, S), EA};
|
||||
|
||||
@@ -987,9 +1005,9 @@ expand_remote(Receiver, DotMeta, Right, Meta, Args, _, _, E) ->
|
||||
Call = {{'.', DotMeta, [Receiver, Right]}, Meta, Args},
|
||||
file_error(Meta, E, ?MODULE, {invalid_call, Call}).
|
||||
|
||||
is_loaded_and_exported(Receiver, Fun, Args) ->
|
||||
is_loaded_and_not_exported(Receiver, Fun, Args) ->
|
||||
(code:ensure_loaded(Receiver) =:= {module, Receiver}) andalso
|
||||
erlang:function_exported(Receiver, Fun, length(Args)).
|
||||
not erlang:function_exported(Receiver, Fun, length(Args)).
|
||||
|
||||
attach_runtime_module(Receiver, Meta, S, _E) ->
|
||||
case lists:member(Receiver, S#elixir_ex.runtime_modules) of
|
||||
@@ -1210,6 +1228,8 @@ format_error({expected_compile_time_module, Kind, GivenTerm}) ->
|
||||
format_error({unquote_outside_quote, Unquote}) ->
|
||||
%% Unquote can be "unquote" or "unquote_splicing".
|
||||
io_lib:format("~p called outside quote", [Unquote]);
|
||||
format_error(quote_in_pattern_with_unquote) ->
|
||||
"unquote is not allowed when quote is used inside a pattern or guard";
|
||||
format_error({invalid_bind_quoted_for_quote, BQ}) ->
|
||||
io_lib:format("invalid :bind_quoted for quote, expected a keyword list of variable names, got: ~ts",
|
||||
['Elixir.Macro':to_string(BQ)]);
|
||||
@@ -1242,12 +1262,12 @@ format_error({invalid_expr_in_scope, Scope, Kind}) ->
|
||||
format_error({invalid_expr_in_guard, Kind}) ->
|
||||
Message =
|
||||
"invalid expression in guards, ~ts is not allowed in guards. To learn more about "
|
||||
"guards, visit: https://hexdocs.pm/elixir/patterns-and-guards.html#guards",
|
||||
"guards, visit: https://elixir.hexdocs.pm/patterns-and-guards.html#guards",
|
||||
io_lib:format(Message, [Kind]);
|
||||
format_error({invalid_expr_in_bitsize, Kind}) ->
|
||||
Message =
|
||||
"~ts is not allowed inside a bitstring size specifier. The size specifier in matches works like guards. "
|
||||
"To learn more about guards, visit: https://hexdocs.pm/elixir/patterns-and-guards.html#guards",
|
||||
"To learn more about guards, visit: https://elixir.hexdocs.pm/patterns-and-guards.html#guards",
|
||||
io_lib:format(Message, [Kind]);
|
||||
format_error({invalid_alias, Expr}) ->
|
||||
Message =
|
||||
@@ -1299,6 +1319,8 @@ format_error({options_are_not_keyword, Kind, Opts}) ->
|
||||
[Kind, 'Elixir.Macro':to_string(Opts)]);
|
||||
format_error({undefined_function, Name, Args}) ->
|
||||
io_lib:format("undefined function ~ts/~B (there is no such import)", [Name, length(Args)]);
|
||||
format_error({invalid_guard_operator, Name}) ->
|
||||
io_lib:format(":erlang.~ts/2 must only be used inside guards", [Name]);
|
||||
format_error({unpinned_bitsize_var, Name, Kind}) ->
|
||||
io_lib:format("the variable ~ts is accessed inside size(...) of a bitstring "
|
||||
"but it was defined outside of the match. You must precede it with the "
|
||||
@@ -1340,4 +1362,4 @@ format_error({parens_map_lookup, Map, Field, Context}) ->
|
||||
format_error({super_in_genserver, {Name, Arity}}) ->
|
||||
io_lib:format("calling super for GenServer callback ~ts/~B is deprecated", [Name, Arity]);
|
||||
format_error('__cursor__') ->
|
||||
"reserved special form __cursor__ cannot be expanded, it is used exclusively to annotate ASTs".
|
||||
"reserved special form __cursor__ cannot be expanded, it is used exclusively to annotate ASTs".
|
||||
|
||||
@@ -205,7 +205,7 @@ compile(Meta, Module, ModuleAsCharlist, Block, Vars, Prune, E) ->
|
||||
'Elixir.Module.Types':infer(Module, File, Attributes, AllDefinitions, UsedPrivate, E, CheckerInfo)
|
||||
end,
|
||||
|
||||
RawCompileOpts = bag_lookup_element(DataBag, {accumulate, compile}, 2),
|
||||
RawCompileOpts = bag_lookup_element(DataBag, {accumulate, compile}, 2) ++ elixir_config:get(erlc_options),
|
||||
CompileOpts = validate_compile_opts(RawCompileOpts, AllDefinitions, Unreachable, Line, E),
|
||||
Impls = bag_lookup_element(DataBag, impls, 2),
|
||||
|
||||
|
||||
@@ -355,6 +355,12 @@ stab_expr -> empty_paren stab_op_eol_and_expr :
|
||||
build_op_with_meta([], '$2', parens_meta('$1')).
|
||||
stab_expr -> empty_paren when_op expr stab_op_eol_and_expr :
|
||||
build_op_with_meta([{'when', meta_from_token('$2'), ['$3']}], '$4', parens_meta('$1')).
|
||||
stab_expr -> at_op_eol no_parens_one_expr stab_op_eol_and_expr :
|
||||
build_at_stab('$1', '$2', '$3').
|
||||
stab_expr -> at_op_eol no_parens_one_ambig_expr stab_op_eol_and_expr :
|
||||
build_at_stab('$1', '$2', '$3').
|
||||
stab_expr -> at_op_eol no_parens_many_expr stab_op_eol_and_expr :
|
||||
build_at_stab('$1', '$2', '$3').
|
||||
stab_expr -> call_args_no_parens_all stab_op_eol_and_expr :
|
||||
build_op(unwrap_when(unwrap_splice('$1')), '$2').
|
||||
stab_expr -> stab_parens_many stab_op_eol_and_expr :
|
||||
@@ -772,7 +778,15 @@ build_unary_op({_Kind, {Line, Column, _}, '//'}, Expr) ->
|
||||
{'/', [{line, Line} | Outer], [{'/', [{line, Line} | Inner], nil}, Expr]};
|
||||
|
||||
build_unary_op({_Kind, Location, Op}, Expr) ->
|
||||
{Op, meta_from_location(Location), [Expr]}.
|
||||
{Op, meta_from_location(Location), [normalize_unary_op(Op, Expr)]}.
|
||||
|
||||
build_at_stab(At, {Call, Meta, Args}, {Op, Right}) ->
|
||||
build_unary_op(At, {Call, Meta, [build_op(Args, Op, Right)]}).
|
||||
|
||||
normalize_unary_op('@', {Call, Meta, [[{'->', _, [_, _]} = Stab]]}) when is_atom(Call), is_list(Meta) ->
|
||||
{Call, Meta, [Stab]};
|
||||
normalize_unary_op(_, Expr) ->
|
||||
Expr.
|
||||
|
||||
build_nullary_op({_Kind, Location, Op}) ->
|
||||
{Op, meta_from_location(Location), []}.
|
||||
@@ -1110,6 +1124,9 @@ check_stab([{'->', _, [_, _]}], _) -> stab;
|
||||
check_stab([], none) -> block;
|
||||
check_stab([_], none) -> block;
|
||||
check_stab([_], Meta) -> error_invalid_stab(Meta);
|
||||
check_stab([{'->', Meta, [[{'@', _, [{Call, CallMeta, nil}]}], _]} | T], _)
|
||||
when is_atom(Call) andalso is_list(CallMeta) ->
|
||||
check_stab(T, {ambiguous_at_stab, Meta});
|
||||
check_stab([{'->', Meta, [_, _]} | T], _) -> check_stab(T, Meta);
|
||||
check_stab([_ | T], MaybeMeta) -> check_stab(T, MaybeMeta).
|
||||
|
||||
@@ -1188,6 +1205,11 @@ return_error({Line, Column, _}, ErrorMessage, ErrorToken) ->
|
||||
return_error_with_meta(Meta, ErrorMessage, ErrorToken) ->
|
||||
return_error(Meta, [ErrorMessage, ErrorToken]).
|
||||
|
||||
error_invalid_stab({ambiguous_at_stab, MetaStab}) ->
|
||||
return_error_with_meta(MetaStab,
|
||||
"ambiguous use of @ with ->. If you want a nullary signature, write @sig () -> bar or @sig (-> bar). "
|
||||
"Syntax error before: ", "'->'");
|
||||
|
||||
error_invalid_stab(MetaStab) ->
|
||||
return_error_with_meta(MetaStab,
|
||||
"unexpected operator ->. If you want to define multiple clauses, the first expression must use ->. "
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
-feature(maybe_expr, enable).
|
||||
|
||||
-export([escape/3, linify/3, linify_with_context_counter/3, build/7, quote/2, has_unquotes/1, fun_to_quoted/1]).
|
||||
-export([dot/5, tail_list/3, list/2, validate_runtime/2, shallow_validate_ast/1]). %% Quote callbacks
|
||||
|
||||
%% Quote callbacks (appear in code, must be handled by the type system)
|
||||
-export([dot/5, tail_list/3, list/2, unquote/1, validate_quote/1, validate_runtime/2, shallow_validate_ast/1]).
|
||||
|
||||
-include("elixir.hrl").
|
||||
-define(defs(Kind), Kind == def; Kind == defp; Kind == defmacro; Kind == defmacrop; Kind == '@').
|
||||
@@ -23,7 +25,7 @@
|
||||
imports_hygiene=nil,
|
||||
unquote=true,
|
||||
generated=false,
|
||||
shallow_validate=false
|
||||
validate=false
|
||||
}).
|
||||
|
||||
%% fun_to_quoted
|
||||
@@ -273,11 +275,14 @@ build(Meta, Line, File, Context, Unquote, Generated, E) ->
|
||||
unquote=Unquote,
|
||||
context=VContext,
|
||||
generated=Generated,
|
||||
shallow_validate=true
|
||||
validate=true
|
||||
},
|
||||
|
||||
{Q, VContext, Acc3}.
|
||||
|
||||
validate_quote(Expr) ->
|
||||
Expr.
|
||||
|
||||
validate_compile(_Meta, line, Value, Acc) when is_boolean(Value) ->
|
||||
{Value, Acc};
|
||||
validate_compile(_Meta, file, nil, Acc) ->
|
||||
@@ -312,27 +317,6 @@ is_valid(context, Context) -> is_atom(Context) andalso (Context /= nil);
|
||||
is_valid(generated, Generated) -> is_boolean(Generated);
|
||||
is_valid(unquote, Unquote) -> is_boolean(Unquote).
|
||||
|
||||
shallow_validate_ast(Expr) ->
|
||||
case shallow_valid_ast(Expr) of
|
||||
true -> Expr;
|
||||
false -> argument_error(
|
||||
<<"tried to unquote invalid AST: ", ('Elixir.Kernel':inspect(Expr))/binary,
|
||||
"\nDid you forget to escape term using Macro.escape/1?">>)
|
||||
end.
|
||||
|
||||
shallow_valid_ast(Expr) when is_list(Expr) -> valid_ast_list(Expr);
|
||||
shallow_valid_ast(Expr) -> valid_ast_elem(Expr).
|
||||
|
||||
valid_ast_list([]) -> true;
|
||||
valid_ast_list([Head | Tail]) -> valid_ast_elem(Head) andalso valid_ast_list(Tail);
|
||||
valid_ast_list(_Improper) -> false.
|
||||
|
||||
valid_ast_elem(Expr) when is_list(Expr); is_atom(Expr); is_binary(Expr); is_number(Expr); is_pid(Expr); is_function(Expr) -> true;
|
||||
valid_ast_elem({Left, Right}) -> valid_ast_elem(Left) andalso valid_ast_elem(Right);
|
||||
valid_ast_elem({Atom, Meta, Args}) when is_atom(Atom), is_list(Meta), is_atom(Args) orelse is_list(Args) -> true;
|
||||
valid_ast_elem({Call, Meta, Args}) when is_list(Meta), is_list(Args) -> shallow_valid_ast(Call);
|
||||
valid_ast_elem(_Term) -> false.
|
||||
|
||||
quote({unquote_splicing, _, [_]}, #elixir_quote{unquote=true}) ->
|
||||
argument_error(<<"unquote_splicing only works inside arguments and block contexts, "
|
||||
"wrap it in parens if you want it to work with one-liners">>);
|
||||
@@ -362,9 +346,9 @@ do_quote({quote, Meta, [Opts, Arg]}, Q) when is_list(Meta) ->
|
||||
|
||||
{'{}', [], [quote, meta(NewMeta, Q), [TOpts, TArg]]};
|
||||
|
||||
do_quote({unquote, Meta, [Expr]}, #elixir_quote{unquote=true, shallow_validate=Validate}) when is_list(Meta) ->
|
||||
do_quote({unquote, Meta, [Expr]}, #elixir_quote{unquote=true, validate=Validate}) when is_list(Meta) ->
|
||||
case Validate of
|
||||
true -> {{'.', Meta, [?MODULE, shallow_validate_ast]}, Meta, [Expr]};
|
||||
true -> {{'.', Meta, [?MODULE, unquote]}, Meta, [Expr]};
|
||||
false -> Expr
|
||||
end;
|
||||
|
||||
@@ -490,7 +474,7 @@ collect_trace_import_quoted([], _Mod, Acc, Arities) ->
|
||||
do_quote_call(Left, Meta, Expr, Args, Q) ->
|
||||
All = [Left, {unquote, Meta, [Expr]}, Args, Q#elixir_quote.context],
|
||||
TAll = [do_quote(X, Q) || X <- All],
|
||||
{{'.', Meta, [elixir_quote, dot]}, Meta, [meta(Meta, Q) | TAll]}.
|
||||
{{'.', Meta, [?MODULE, dot]}, Meta, [meta(Meta, Q) | TAll]}.
|
||||
|
||||
do_quote_tuple({Left, Meta, Right}, Q) ->
|
||||
do_quote_tuple(Left, Meta, Right, Q).
|
||||
@@ -533,12 +517,14 @@ do_list_concat([], Right) -> Right;
|
||||
do_list_concat(Left, Right) -> {{'.', [], [erlang, '++']}, [], [Left, Right]}.
|
||||
|
||||
do_runtime_list(Meta, Fun, Args) ->
|
||||
{{'.', Meta, [elixir_quote, Fun]}, Meta, Args}.
|
||||
{{'.', Meta, [?MODULE, Fun]}, Meta, Args}.
|
||||
|
||||
%% Callbacks
|
||||
%% Unquote validation callbacks
|
||||
%%
|
||||
%% They perform shallow runtime validation for performance
|
||||
%% reasons but will be type checked in the future for full
|
||||
%% validation.
|
||||
|
||||
%% Some expressions cannot be unquoted at compilation time.
|
||||
%% This function is responsible for doing runtime unquoting.
|
||||
dot(Meta, Left, Right, Args, Context) ->
|
||||
annotate(dot(Meta, Left, Right, Args), Context).
|
||||
|
||||
@@ -589,12 +575,37 @@ tail_list(Left, Right, Tail) when is_list(Left) ->
|
||||
end.
|
||||
|
||||
validate_list(List) ->
|
||||
case valid_ast_list(List) of
|
||||
case shallow_valid_list(List) of
|
||||
true -> ok;
|
||||
false -> argument_error(<<"expected a list with quoted expressions in unquote_splicing/1, got: ",
|
||||
('Elixir.Kernel':inspect(List))/binary>>)
|
||||
end.
|
||||
|
||||
unquote(Expr) ->
|
||||
case shallow_valid_ast(Expr) of
|
||||
true -> Expr;
|
||||
false -> argument_error(
|
||||
<<"tried to unquote invalid AST: ", ('Elixir.Kernel':inspect(Expr))/binary,
|
||||
"\nDid you forget to escape term using Macro.escape/1?">>)
|
||||
end.
|
||||
|
||||
shallow_valid_ast(Expr) when is_list(Expr) -> shallow_valid_list(Expr);
|
||||
shallow_valid_ast(Expr) -> shallow_valid_elem(Expr).
|
||||
|
||||
shallow_valid_list([]) -> true;
|
||||
shallow_valid_list([Head | Tail]) -> shallow_valid_elem(Head) andalso shallow_valid_list(Tail);
|
||||
shallow_valid_list(_Improper) -> false.
|
||||
|
||||
shallow_valid_elem(Expr) when is_list(Expr); is_atom(Expr); is_binary(Expr); is_number(Expr); is_pid(Expr); is_function(Expr) -> true;
|
||||
shallow_valid_elem({Left, Right}) -> shallow_valid_elem(Left) andalso shallow_valid_elem(Right);
|
||||
shallow_valid_elem({Atom, Meta, Args}) when is_atom(Atom), is_list(Meta), is_atom(Args) orelse is_list(Args) -> true;
|
||||
shallow_valid_elem({Call, Meta, Args}) when is_list(Meta), is_list(Args) -> shallow_valid_ast(Call);
|
||||
shallow_valid_elem(_Term) -> false.
|
||||
|
||||
%% TODO: We keep this with backwards compatibility in previous compiled Elixirw code.
|
||||
shallow_validate_ast(Expr) ->
|
||||
unquote(Expr).
|
||||
|
||||
argument_error(Message) ->
|
||||
error('Elixir.ArgumentError':exception([{message, Message}])).
|
||||
|
||||
|
||||
@@ -147,7 +147,10 @@ inline(Mod, Fun, Arity) -> inner_inline(ex_to_erl, Mod, Fun, Arity).
|
||||
?inline(?kernel, trunc, 1, erlang, trunc);
|
||||
?inline(?kernel, tuple_size, 1, erlang, tuple_size);
|
||||
|
||||
?inline(?list, to_atom, 1, erlang, list_to_atom);
|
||||
% we just want ex_to_erl, not erl_to_ex
|
||||
inner_inline(ex_to_erl, ?list, to_atom, 1) -> {erlang, list_to_atom};
|
||||
|
||||
?inline(?list, to_unsafe_atom, 1, erlang, list_to_atom);
|
||||
?inline(?list, to_existing_atom, 1, erlang, list_to_existing_atom);
|
||||
?inline(?list, to_float, 1, erlang, list_to_float);
|
||||
?inline(?list, to_integer, 1, erlang, list_to_integer);
|
||||
@@ -205,8 +208,11 @@ inline(Mod, Fun, Arity) -> inner_inline(ex_to_erl, Mod, Fun, Arity).
|
||||
?inline(?process, unlink, 1, erlang, unlink);
|
||||
?inline(?process, unregister, 1, erlang, unregister);
|
||||
|
||||
% we just want ex_to_erl, not erl_to_ex
|
||||
inner_inline(ex_to_erl, ?string, to_atom, 1) -> {erlang, binary_to_atom};
|
||||
|
||||
?inline(?string, duplicate, 2, binary, copy);
|
||||
?inline(?string, to_atom, 1, erlang, binary_to_atom);
|
||||
?inline(?string, to_unsafe_atom, 1, erlang, binary_to_atom);
|
||||
?inline(?string, to_existing_atom, 1, erlang, binary_to_existing_atom);
|
||||
?inline(?string, to_float, 1, erlang, binary_to_float);
|
||||
?inline(?string, to_integer, 1, erlang, binary_to_integer);
|
||||
@@ -238,7 +244,7 @@ inner_inline(_, _, _, _) -> false.
|
||||
%%
|
||||
%% Rewrite rules are more complex than regular inlining code
|
||||
%% as they may change the number of arguments. However, they
|
||||
%% cannot change tha argument order, as that can change the
|
||||
%% cannot change the argument order, as that can change the
|
||||
%% code semantics.
|
||||
rewrite(?string_chars, DotMeta, to_string, Meta, [Arg]) ->
|
||||
case is_always_string(Arg) of
|
||||
@@ -328,6 +334,8 @@ guard(Receiver, DotMeta, Right, Meta, Args, S) ->
|
||||
%% need to explicitly forbid as they are allowed in erl_internal.
|
||||
allowed_guard(is_record, 2) -> false;
|
||||
allowed_guard(is_record, 3) -> false;
|
||||
allowed_guard('orelse', 2) -> true;
|
||||
allowed_guard('andalso', 2) -> true;
|
||||
allowed_guard(Right, Arity) ->
|
||||
erl_internal:guard_bif(Right, Arity) orelse elixir_utils:guard_op(Right, Arity).
|
||||
|
||||
|
||||
@@ -1406,7 +1406,7 @@ tokenize_identifier(String, Line, Column, Scope, MaybeKeyword) ->
|
||||
case suggest_simpler_unexpected_token_in_error(Wrong, Line, WrongColumn, Scope) of
|
||||
no_suggestion ->
|
||||
%% we append a pointer to more info if we aren't appending a suggestion
|
||||
MoreInfo = "\nSee https://hexdocs.pm/elixir/unicode-syntax.html for more information.",
|
||||
MoreInfo = "\nSee https://elixir.hexdocs.pm/unicode-syntax.html for more information.",
|
||||
{error, {?LOC(Line, Column), {Prefix, Suffix ++ MoreInfo}, Wrong}};
|
||||
|
||||
{_, {Location, _, SuggestionMessage}} = _SuggestionError ->
|
||||
@@ -1438,12 +1438,12 @@ suggest_simpler_unexpected_token_in_error(Wrong, Line, WrongColumn, Scope) ->
|
||||
{error, _Reason} ->
|
||||
ConfusableSkeleton = 'Elixir.String.Tokenizer.Security':confusable_skeleton(Wrong),
|
||||
case (Scope#elixir_tokenizer.identifier_tokenizer):tokenize(ConfusableSkeleton) of
|
||||
{_, Simpler, _, _, _, _} ->
|
||||
{_, Simpler, _, _, _, _} when Simpler =/= Wrong ->
|
||||
Message = suggest_change("Codepoint failed identifier tokenization, but a simpler form was found.",
|
||||
Wrong,
|
||||
"You could write the above in a similar way that is accepted by Elixir:",
|
||||
Simpler,
|
||||
"See https://hexdocs.pm/elixir/unicode-syntax.html for more information."),
|
||||
"See https://elixir.hexdocs.pm/unicode-syntax.html for more information."),
|
||||
{error, {?LOC(Line, WrongColumn), "unexpected token: ", Message}};
|
||||
_other ->
|
||||
no_suggestion
|
||||
@@ -1453,7 +1453,7 @@ suggest_simpler_unexpected_token_in_error(Wrong, Line, WrongColumn, Scope) ->
|
||||
Wrong,
|
||||
"You could write the above in a compatible format that is accepted by Elixir:",
|
||||
NFKC,
|
||||
"See https://hexdocs.pm/elixir/unicode-syntax.html for more information."),
|
||||
"See https://elixir.hexdocs.pm/unicode-syntax.html for more information."),
|
||||
{error, {?LOC(Line, WrongColumn), "unexpected token: ", Message}}
|
||||
end.
|
||||
|
||||
@@ -1918,13 +1918,13 @@ add_cursor(_Line, Column, noprune, Tokens) ->
|
||||
{Column, Tokens};
|
||||
|
||||
add_cursor(Line, Column, {prune_and_cursor, true},
|
||||
[{sigil, {_, _, {Line, Column}} = Location, Name, Parts, Modifier, Identation, Delimiter} | Rest]) ->
|
||||
[{sigil, {_, _, {Line, Column}} = Location, Name, Parts, Modifier, Indentation, Delimiter} | Rest]) ->
|
||||
Cursor = {'__cursor__', [{line, Line}, {column, Column}], []},
|
||||
CursorModifier = case Modifier of
|
||||
false -> Cursor;
|
||||
List -> List ++ [Cursor]
|
||||
end,
|
||||
{Column + 12, [{sigil, Location, Name, Parts, CursorModifier, Identation, Delimiter} | Rest]};
|
||||
{Column + 12, [{sigil, Location, Name, Parts, CursorModifier, Indentation, Delimiter} | Rest]};
|
||||
|
||||
add_cursor(Line, Column, {prune_and_cursor, _}, Tokens) ->
|
||||
PrePrunedTokens = prune_identifier(Tokens),
|
||||
|
||||
@@ -33,10 +33,6 @@ erl_fa_to_elixir_fa(Name, Arity) ->
|
||||
_ -> {Name, Arity}
|
||||
end.
|
||||
|
||||
guard_op('andalso', 2) ->
|
||||
true;
|
||||
guard_op('orelse', 2) ->
|
||||
true;
|
||||
guard_op(Op, Arity) ->
|
||||
try erl_internal:op_type(Op, Arity) of
|
||||
arith -> true;
|
||||
@@ -233,6 +229,9 @@ returns_boolean({{'.', _, [erlang, Fun]}, _, [_, _]}) when
|
||||
returns_boolean({{'.', _, [erlang, Fun]}, _, [_, _, _]}) when
|
||||
Fun == function_exported; Fun == is_record -> true;
|
||||
|
||||
returns_boolean({{'.', _, [lists, member]}, _, [_, _]}) ->
|
||||
true;
|
||||
|
||||
returns_boolean({'case', _, [_, [{do, Clauses}]]}) ->
|
||||
lists:all(fun
|
||||
({'->', _, [_, Expr]}) -> returns_boolean(Expr)
|
||||
|
||||
@@ -219,6 +219,61 @@ defmodule AccessTest do
|
||||
end
|
||||
end
|
||||
|
||||
describe "key/2 and key!/1" do
|
||||
@test_map %{foo: :bar, baz: :qux}
|
||||
|
||||
test "finds key in map" do
|
||||
assert get_in(@test_map, [Access.key(:foo)]) == :bar
|
||||
assert get_in(@test_map, [Access.key(:missing)]) == nil
|
||||
|
||||
assert get_in(@test_map, [Access.key!(:foo)]) == :bar
|
||||
|
||||
assert_raise KeyError, fn ->
|
||||
get_in(@test_map, [Access.key!(:missing)])
|
||||
end
|
||||
end
|
||||
|
||||
test "finds key in struct" do
|
||||
defmodule KeySample do
|
||||
defstruct foo: :bar, baz: :qux
|
||||
end
|
||||
|
||||
assert get_in(struct(KeySample), [Access.key(:foo)]) == :bar
|
||||
assert get_in(struct(KeySample), [Access.key(:missing)]) == nil
|
||||
|
||||
assert get_in(struct(KeySample), [Access.key!(:foo)]) == :bar
|
||||
|
||||
assert_raise KeyError, fn ->
|
||||
get_in(struct(KeySample), [Access.key!(:missing)])
|
||||
end
|
||||
end
|
||||
|
||||
@test_keyword [foo: :bar, baz: :qux]
|
||||
|
||||
test "finds key in keyword list" do
|
||||
assert get_in(@test_keyword, [Access.key(:foo)]) == :bar
|
||||
assert get_in(@test_keyword, [Access.key(:missing)]) == nil
|
||||
|
||||
assert get_in(@test_keyword, [Access.key!(:foo)]) == :bar
|
||||
|
||||
assert_raise KeyError, fn ->
|
||||
get_in(@test_keyword, [Access.key!(:missing)])
|
||||
end
|
||||
end
|
||||
|
||||
test "raises when key/2 access is attempted on [1,2,3]" do
|
||||
assert_raise RuntimeError, ~r"Access.key/2 expected a map/struct/keyword list", fn ->
|
||||
put_in([1, 2, 3], [Access.key(:foo)], :bar)
|
||||
end
|
||||
end
|
||||
|
||||
test "raises when key!/1 access is attempted on [1,2,3]" do
|
||||
assert_raise RuntimeError, ~r"Access.key!/1 expected a map/struct/keyword list", fn ->
|
||||
put_in([1, 2, 3], [Access.key!(:foo)], :bar)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "at/1" do
|
||||
@test_list [1, 2, 3, 4, 5, 6]
|
||||
|
||||
|
||||
@@ -10,10 +10,10 @@ defmodule AtomTest do
|
||||
doctest Atom, except: [:moduledoc]
|
||||
|
||||
test "to_string/1" do
|
||||
assert "héllo" |> String.to_atom() |> Atom.to_string() == "héllo"
|
||||
assert "héllo" |> String.to_unsafe_atom() |> Atom.to_string() == "héllo"
|
||||
end
|
||||
|
||||
test "to_charlist/1" do
|
||||
assert "héllo" |> String.to_atom() |> Atom.to_charlist() == ~c"héllo"
|
||||
assert "héllo" |> String.to_unsafe_atom() |> Atom.to_charlist() == ~c"héllo"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -340,6 +340,32 @@ defmodule CalendarTest do
|
||||
assert Calendar.strftime(~N[2019-08-15 17:07:57], "%010A") == "00Thursday"
|
||||
end
|
||||
|
||||
test "limits width to at most 1024 characters" do
|
||||
assert Calendar.strftime(~D[2019-08-15], "%1024d") |> byte_size() == 1024
|
||||
|
||||
assert_raise ArgumentError, "invalid strftime format: width must be at most 1024", fn ->
|
||||
Calendar.strftime(~D[2019-08-15], "%1025d")
|
||||
end
|
||||
|
||||
assert_raise ArgumentError, "invalid strftime format: width must be at most 1024", fn ->
|
||||
Calendar.strftime(~D[2019-08-15], "%10000d")
|
||||
end
|
||||
end
|
||||
|
||||
test "limits width in preferred formats" do
|
||||
assert_raise ArgumentError, "invalid strftime format: width must be at most 1024", fn ->
|
||||
Calendar.strftime(~N[2019-08-15 17:07:57], "%c", preferred_datetime: "%1025d")
|
||||
end
|
||||
|
||||
assert_raise ArgumentError, "invalid strftime format: width must be at most 1024", fn ->
|
||||
Calendar.strftime(~N[2019-08-15 17:07:57], "%x", preferred_date: "%1025d")
|
||||
end
|
||||
|
||||
assert_raise ArgumentError, "invalid strftime format: width must be at most 1024", fn ->
|
||||
Calendar.strftime(~N[2019-08-15 17:07:57], "%X", preferred_time: "%1025H")
|
||||
end
|
||||
end
|
||||
|
||||
test "formats Epoch time with %s" do
|
||||
assert Calendar.strftime(~N[2019-08-15 17:07:57], "%s") == "1565888877"
|
||||
|
||||
|
||||
@@ -514,6 +514,25 @@ defmodule CodeTest do
|
||||
Code.unrequire_files([fixture_path("code_sample.exs")])
|
||||
end
|
||||
|
||||
test "require_file/1 releases the file when compilation fails" do
|
||||
path = tmp_path("bad_require_#{System.unique_integer([:positive])}.ex")
|
||||
|
||||
try do
|
||||
File.write!(path, ~s|raise "boom"|)
|
||||
|
||||
assert_raise RuntimeError, "boom", fn ->
|
||||
Code.require_file(path)
|
||||
end
|
||||
|
||||
assert_raise RuntimeError, "boom", fn ->
|
||||
Code.require_file(path)
|
||||
end
|
||||
after
|
||||
File.rm(path)
|
||||
Code.unrequire_files([path])
|
||||
end
|
||||
end
|
||||
|
||||
test "string_to_quoted!/2 errors take lines/columns/indentation into account" do
|
||||
assert_exception(
|
||||
SyntaxError,
|
||||
@@ -898,6 +917,34 @@ defmodule Code.SyncTest do
|
||||
Code.put_compiler_option(:module_definition, :compiled)
|
||||
end
|
||||
|
||||
if System.otp_release() >= "29" do
|
||||
test "uses erlc_options compiler option" do
|
||||
module = CodeTest.ConfiguredBeamDebugInfo
|
||||
previous = Code.compiler_options(erlc_options: [:beam_debug_info])
|
||||
|
||||
try do
|
||||
assert [{^module, binary}] =
|
||||
Code.compile_string("""
|
||||
defmodule CodeTest.ConfiguredBeamDebugInfo do
|
||||
def sample(value) do
|
||||
doubled = value * 2
|
||||
doubled + 1
|
||||
end
|
||||
end
|
||||
""")
|
||||
|
||||
assert {:ok, {_, [{~c"DbgB", <<_version::32, entries::32, _::binary>>}]}} =
|
||||
:beam_lib.chunks(binary, [~c"DbgB"])
|
||||
|
||||
assert entries > 0
|
||||
after
|
||||
Code.compiler_options(previous)
|
||||
:code.purge(module)
|
||||
:code.delete(module)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
test "prepend_path" do
|
||||
path = Path.join(__DIR__, "fixtures")
|
||||
true = Code.prepend_path(path)
|
||||
|
||||
@@ -98,12 +98,12 @@ defmodule EnumTest do
|
||||
end
|
||||
|
||||
test "chunk/3" do
|
||||
enum = String.to_atom("Elixir.Enum")
|
||||
enum = String.to_unsafe_atom("Elixir.Enum")
|
||||
assert enum.chunk(1..5, 2, 1) == Enum.chunk_every(1..5, 2, 1, :discard)
|
||||
end
|
||||
|
||||
test "chunk/4" do
|
||||
enum = String.to_atom("Elixir.Enum")
|
||||
enum = String.to_unsafe_atom("Elixir.Enum")
|
||||
assert enum.chunk(1..5, 2, 1, nil) == Enum.chunk_every(1..5, 2, 1, :discard)
|
||||
end
|
||||
|
||||
|
||||
@@ -210,7 +210,7 @@ defmodule ExceptionTest do
|
||||
assert Exception.format_mfa(Foo, :こんにちは世界, [1, 2]) == "Foo.こんにちは世界(1, 2)"
|
||||
|
||||
nfd = :unicode.characters_to_nfd_binary("olá")
|
||||
assert Exception.format_mfa(Foo, String.to_atom(nfd), [1, 2]) == "Foo.\"#{nfd}\"(1, 2)"
|
||||
assert Exception.format_mfa(Foo, String.to_unsafe_atom(nfd), [1, 2]) == "Foo.\"#{nfd}\"(1, 2)"
|
||||
end
|
||||
|
||||
test "format_fa/2" do
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user