Compare commits
76
Commits
v1.13.0-rc.0
...
v1.10.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c88ecdec16 | ||
|
|
1b4536bf17 | ||
|
|
ab4d6817ab | ||
|
|
717490a1b0 | ||
|
|
8cb1361e1a | ||
|
|
967f4b412c | ||
|
|
8a876d6c78 | ||
|
|
831b12a116 | ||
|
|
d16497b966 | ||
|
|
7ef6dfc4e4 | ||
|
|
e1158a55a3 | ||
|
|
78cc8bea7a | ||
|
|
5914aba7e4 | ||
|
|
40301963e4 | ||
|
|
248d532ae4 | ||
|
|
74bc9506a9 | ||
|
|
41e218b160 | ||
|
|
94221f2894 | ||
|
|
6eaac2dc72 | ||
|
|
92650c84be | ||
|
|
b30cbb600b | ||
|
|
6092fb4618 | ||
|
|
07450832e7 | ||
|
|
34adbcb95a | ||
|
|
286d310ac3 | ||
|
|
2fc916088a | ||
|
|
9a428e9cdd | ||
|
|
090dddcf00 | ||
|
|
089c470e13 | ||
|
|
68481b7377 | ||
|
|
e2dc9ae72e | ||
|
|
51c95b6000 | ||
|
|
12917b00e5 | ||
|
|
ab9ecf2d7f | ||
|
|
eca809285e | ||
|
|
8e71e65aae | ||
|
|
c3a27be073 | ||
|
|
e10f9fb6fe | ||
|
|
f66ffd0733 | ||
|
|
696bad8de1 | ||
|
|
96c8e31410 | ||
|
|
70be440359 | ||
|
|
c6d6683225 | ||
|
|
db2504eca6 | ||
|
|
98c3f5c602 | ||
|
|
db4684b2f5 | ||
|
|
f3f65e4cd1 | ||
|
|
5331174934 | ||
|
|
f4f2be67d7 | ||
|
|
686daadaaa | ||
|
|
6e8563e930 | ||
|
|
65be24a8c2 | ||
|
|
5bd7a90864 | ||
|
|
831046fcb7 | ||
|
|
6c44feae2f | ||
|
|
35eef87590 | ||
|
|
fa091c6bd9 | ||
|
|
a561f81539 | ||
|
|
739c1b6292 | ||
|
|
3e1e6ddf8a | ||
|
|
ccb8c5a2e7 | ||
|
|
07e580cf17 | ||
|
|
1f9f713d9c | ||
|
|
9814a4cd61 | ||
|
|
3dfdd80dff | ||
|
|
0fdc7510b8 | ||
|
|
0e8873238d | ||
|
|
ba02eb90a0 | ||
|
|
fdc7716bf3 | ||
|
|
a050c00fbc | ||
|
|
d395c3e3cf | ||
|
|
c3e903f339 | ||
|
|
452ba40855 | ||
|
|
8854492cc5 | ||
|
|
105770cfa0 | ||
|
|
28b053a122 |
+2
-2
@@ -130,11 +130,11 @@ test_windows_task:
|
||||
test_freebsd_task:
|
||||
<<: *DEFAULT_TEST_SETTINGS
|
||||
|
||||
name: FreeBSD 12.0
|
||||
name: FreeBSD 12.1
|
||||
alias: FreeBSD Stable
|
||||
|
||||
freebsd_instance:
|
||||
image_family: freebsd-12-0
|
||||
image_family: freebsd-12-1
|
||||
cpu: 8
|
||||
memory: 7424Mi
|
||||
|
||||
|
||||
+174
-33
@@ -26,73 +26,204 @@ See the full CHANGELOG for more improvements.
|
||||
|
||||
## Improvements to sort-based APIs in Enum
|
||||
|
||||
`Enum.sort/1` in Elixir always sorts from lowest to highest. If you want to sort from highest to lowest, you need to call `Enum.sort/2` with a custom sorting function, such as `Enum.sort(collection, &>=/2)`, which is not immediately obvious to someone reading the code.
|
||||
`Enum.sort/1` in Elixir by default sorts from lowest to highest:
|
||||
|
||||
To make matters worse, comparison operators, such as `<=` and `>=`, perform structural sorting, instead of a semantic one. For example, using `>=` to sort dates descendingly won't yield the correct result. Therefore, to sort dates from more recent to oldest, one has to write `Enum.sort(dates, &(Date.compare(&1, &2) != :lt))`.
|
||||
```elixir
|
||||
iex> Enum.sort(["banana", "apple", "pineapple"])
|
||||
["apple", "banana", "pineapple"]
|
||||
```
|
||||
|
||||
If you want to sort from highest to lowest, you need to call `Enum.sort/2` with a custom sorting function, such as `Enum.sort(collection, &>=/2)`, which is not immediately obvious to someone reading the code:
|
||||
|
||||
```elixir
|
||||
iex> Enum.sort(["banana", "apple", "pineapple"], &>=/2)
|
||||
["pineapple", "banana", "apple"]
|
||||
```
|
||||
|
||||
Furthermore, comparison operators, such as `<=` and `>=`, perform structural sorting, instead of a semantic one. For example, using `>=` to sort dates descendingly won't yield the correct result:
|
||||
|
||||
```elixir
|
||||
iex> Enum.sort([~D[2019-12-31], ~D[2020-01-01]])
|
||||
[~D[2020-01-01], ~D[2019-12-31]]
|
||||
```
|
||||
|
||||
To perform proper semantic comparison for dates, one would also need to pass a custom sorting function:
|
||||
|
||||
```elixir
|
||||
iex> Enum.sort([~D[2019-12-31], ~D[2020-01-01]], &(Date.compare(&1, &2) != :lt))
|
||||
[~D[2019-12-31], ~D[2020-01-01]]
|
||||
```
|
||||
|
||||
Elixir v1.10 streamlines the sorting functions by introducing both `:asc` and `:desc` shortcuts:
|
||||
|
||||
Enum.sort(collection, :asc) # the default
|
||||
Enum.sort(collection, :desc) # in reverse
|
||||
```elixir
|
||||
iex> Enum.sort(["banana", "apple", "pineapple"], :asc)
|
||||
["apple", "banana", "pineapple"]
|
||||
iex> Enum.sort(["banana", "apple", "pineapple"], :desc)
|
||||
["pineapple", "banana", "apple"]
|
||||
```
|
||||
|
||||
Furthermore, if you want to perform semantic comparison, you can pass a module that provides the relevant comparison function. For example, to sort dates:
|
||||
As well as adding the possibility to pass a module to perform semantic comparisons. For example, to sort dates, one now only needs to pass the `Date` module or even `{:desc, Date}` for descending semantical sort:
|
||||
|
||||
Enum.sort(birth_dates, Date)
|
||||
Enum.sort(birth_dates, {:asc, Date})
|
||||
Enum.sort(birth_dates, {:desc, Date})
|
||||
```elixir
|
||||
iex> Enum.sort([~D[2019-12-31], ~D[2020-01-01]], Date)
|
||||
[~D[2019-12-31], ~D[2020-01-01]]
|
||||
iex> Enum.sort([~D[2019-12-31], ~D[2020-01-01]], {:desc, Date})
|
||||
[~D[2020-01-01], ~D[2019-12-31]]
|
||||
```
|
||||
|
||||
This new API has also been added to `Enum.sort_by`, `Enum.min_by`, `Enum.max_by`, and friends.
|
||||
These API improvements make the code more concise and readable and they have also been added to `Enum.sort_by`, `Enum.min_by`, `Enum.max_by`, and friends.
|
||||
|
||||
### Tracking of compile-time configuration
|
||||
## Tracking of compile-time configuration
|
||||
|
||||
All applications in Elixir come with an application environment. This environment is a key-value store that allows us to configure said application. While reading the application environment at runtime is the preferred approach, in some rare occasions you may want to use the application environment to configure the compilation of a certain project. This is often done by calling `Application.get_env/3` outside of a function:
|
||||
In Elixir, we organize our code in applications. Libraries, your dependencies, and your own project are all separate applications. All applications in Elixir also come with an application environment.
|
||||
|
||||
defmodule MyApp.DBClient do
|
||||
@db_host Application.get_env(:my_app, :db_host, "db.local")
|
||||
The application environment is a key-value store that allows us to configure said application. While reading the application environment at runtime is the preferred approach, in some rare occasions you may want to use the application environment to configure the compilation of a certain project. This is often done by calling `Application.get_env/3` outside of a function:
|
||||
|
||||
def start_link() do
|
||||
SomeLib.DBClient.start_link(host: @db_host)
|
||||
end
|
||||
end
|
||||
```elixir
|
||||
defmodule MyApp.DBClient do
|
||||
@db_host Application.get_env(:my_app, :db_host, "db.local")
|
||||
def start_link() do
|
||||
SomeLib.DBClient.start_link(host: @db_host)
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
This approach has one big limitation: if you change the value of the application environment after the code is compiled, the value used at runtime is not going to change! For example, if you are using `mix release` and your `config/releases.exs` has:
|
||||
|
||||
config :my_app, :db_host, "db.production"
|
||||
|
||||
The new value will have no effect as the code was compiled to connect to "db.local", which is mostly likely unavailable in the production environment.
|
||||
Because `config/releases.exs` is read after the code is compiled, the new value will have no effect as the code was compiled to connect to "db.local".
|
||||
|
||||
For those reasons, reading the application environment at runtime should be the first choice. However, if you really have to read the application environment during compilation, Elixir v1.10 introduces a `Application.compile_env/3` function:
|
||||
Of course, the obvious solution to this mismatch is to not read the application environment at compilation time in the first place, and instead move the code to inside a function:
|
||||
|
||||
@db_host Application.compile_env(:my_app, :db_host, "db.local")
|
||||
```elixir
|
||||
defmodule MyApp.DBClient do
|
||||
def start_link() do
|
||||
SomeLib.DBClient.start_link(host: db_host())
|
||||
end
|
||||
defp db_host() do
|
||||
Application.get_env(:my_app, :db_host, "db.local")
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
By using `compile_env/3`, Elixir will store the values used during compilation and compare the compilation values with the runtime values whenever your system starts, raising an error in case they differ. This helps developers ensure they are running their production systems with the configuration they intend to.
|
||||
While this is the preferred approach, there are still two scenarios we need to address:
|
||||
|
||||
### Compiler tracing
|
||||
1. Not everyone may be aware of this pitfall, so they will mistakenly read the application environemnt at compile-time, until they are bitten by this behaviour
|
||||
|
||||
2. In rare occasions, you trully need to read the application environment at compile-time, and you want to be warned when you try to configure at runtime something that is valid only at compilation time
|
||||
|
||||
Elixir v1.10 aims to solve these two scenarios by introducing a `Application.compile_env/3` function. For example, to read the value at compile time, you can now do:
|
||||
|
||||
```elixir
|
||||
@db_host Application.compile_env(:my_app, :db_host, "db.local")
|
||||
```
|
||||
|
||||
By using `compile_env/3`, Elixir will store the values used during compilation and compare them with the runtime values whenever your system starts, raising an error in case they differ. This helps developers ensure they are running their production systems with the configuration they intend to.
|
||||
|
||||
In future versions, we will deprecate the use `Application.get_env` at compile-time with a clear message pointing users to configuration best practices, effectively addressing the scenario where users read from the application environment at compile time unaware of its pitfalls.
|
||||
|
||||
## Compiler tracing
|
||||
|
||||
This release brings enhancements to the Elixir compiler and adds new capabilities for developers to listen to compilation events.
|
||||
|
||||
In previous Elixir releases, Elixir would compile a database of cross references between modules (such as function calls, references, structs, etc) for each project. Although developers could traverse this database, they often requested more events or more information to be made available.
|
||||
In previous Elixir versions, Elixir would compile a database of cross references between modules (such as function calls, references, structs, etc) for each project in order to perform all kinds of checks, such as deprecations and undefined functions.
|
||||
|
||||
In Elixir v1.10, we have replaced this database by compiler tracing. This means that developers can now directly listen to events emitted by the compiler to store and collect all the information they need (and only the information they need).
|
||||
Although this database was not public, developers would still use it to run their own checks against their projects. With time, developers would request more data to be included in the database, which was problematic as Elixir itself did not have a use for the additional data, and the database was not meant to be used externally in the first place.
|
||||
|
||||
Elixir itself is already using the new compiler tracing to provide new functionality. In particular, the compiler now checks for undefined function warnings more consistently. In previous versions, we would emit undefined function warnings only for files in `lib`, skipping test files and scripts.
|
||||
In Elixir v1.10, we have addressed these problems by introducing compiler tracing. The compiler tracing allows developers to listen to events as they are emitted by the compiler, so they can store all of the information they need - and only the information they need.
|
||||
|
||||
Furthermore, in Elixir v1.10 developers can now disable undefined function warnings directly on the callsite. For example, imagine you have an optional dependency which may not be available in some cases. You can tell the compiler to skip warning on calls to optional modules with:
|
||||
Elixir itself is using the new compiler tracing to provide new functionality. One advantage of this approach is that developers can now disable undefined function warnings directly on the callsite. For example, imagine you have an optional dependency which may not be available in some cases. You can tell the compiler to skip warning on calls to optional modules with:
|
||||
|
||||
@compile {:no_warn_undefined, OptionalDependency}
|
||||
defdelegate my_function_call(arg), to: OptionalDependency
|
||||
|
||||
Finally, as consequence of these improvements, some functionality related to `xref` (our previous database), has been deprecated in favor of the new compiler tracing.
|
||||
Previously, this information had to be added to the overall project configuration, which was far away from where the optional call effectively happened.
|
||||
|
||||
### Other enhancements
|
||||
## Other enhancements
|
||||
|
||||
The calendar data types got many improvements, such as sigil support for third-party calendars, as well as the additions of `DateTime.now!/2`, `DateTime.shift_zone!/3`, and `NaiveDateTime.local_now/0`.
|
||||
Elixir's calendar data types got many improvements, such as sigil support for third-party calendars, as well as the additions of `DateTime.now!/2`, `DateTime.shift_zone!/3`, and `NaiveDateTime.local_now/0`.
|
||||
|
||||
There are many improvements related to the Elixir AST in this release too. First of all, `Code.string_to_quoted/2` has two new options, `:token_metadata` and `:literal_encoder`, that give more control over Elixir's parser. This information has already been available to the Elixir formatter for a couple versions and has now been made public. Furthermore, all public metadata entries in the AST nodes have been extensively documented. These changes alongside the compiler improvements from previous section means tools like Credo and Boundary now have a better foundation to analyze the source code.
|
||||
There are many improvements related to Elixir's AST in this release too. First of all, `Code.string_to_quoted/2` has two new options, `:token_metadata` and `:literal_encoder`, that give more control over Elixir's parser. This information was already available to the Elixir code formatter and has now been made public. We have also extensively documented all of Elixir's AST metadata. These changes alongside compiler tracing means static analyzers and IDE integrations have a better foundation to analyze the source code.
|
||||
|
||||
Finally, ExUnit comes with two small but important improvements: `ExUnit.CaptureIO` can now be used in tests that run asynchronously and we have added "data-structure diffing" when performing assertions with pattern matching. So now, whenever an assertion such `assert %{field: value} = expression()` fails, ExUnit will show both left-hand and right-hand sides, highlighting the parts that did not match in red.
|
||||
ExUnit, our test framework, ships two small but important improvements: `ExUnit.CaptureIO` can now be used by tests that run concurrently and we have added "pattern-matching diffing". To understand the last feature, take this code:
|
||||
|
||||
## v1.10.0
|
||||
```elixir
|
||||
assert %{"status" => 200, "body" => %{"key" => "foo"}} = json_payload
|
||||
```
|
||||
|
||||
Now imagine that `json_payload` is a large JSON blob and the `"key"` inside the `"body"` did not have value of `"foo"`. In previous Elixir versions, if the assertion failed, Elixir would print the right side and let you up to your own devices to figure out what went wrong. In Elixir v1.10, we diff the data structure against the pattern so you can see exactly which parts of the data matched the pattern and which ones did not. Note ExUnit already performed diffing when comparing data types, this new version adds diffing when matching data against a pattern.
|
||||
|
||||
## v1.10.3 (2020-04-25)
|
||||
|
||||
### 1. Bug fixes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Code] Return `[{mod, bin}]` from `Code.compile_file/2`, `Code.require_file/2`, `Code.load_file/2`
|
||||
* [Code] Make sure the formatter respects newlines before and after module attributes
|
||||
* [Kernel.ParallelCompiler] Fix a bug where the parallel compiler would raise in long compilation cycles
|
||||
* [Kernel.ParallelCompiler] Fix a bug where the parallel compiler would raise if some of the modules being compiled referred to a module that has been loaded directly to memory
|
||||
* [Module] Fix accidental breaking change where bodiless clauses had their body value on `@on_definition` callbacks set to an empty list instead of `nil`
|
||||
* [String] Undeprecate `String.normalize/2` normalize and fix infinite loop caused by certain invalid strings
|
||||
|
||||
#### ExUnit
|
||||
|
||||
* [ExUnit.Assertions] Fix pattern matching diff when matching on pinned variables
|
||||
* [ExUnit.Assertions] Fix pattern matching diff when matching variable struct names
|
||||
* [ExUnit.Assertions] Fix pattern matching diff when matching on the binary concat operator (`<>`) and the left side is not a literal string
|
||||
* [ExUnit.Assertions] Fix pattern matching diff when matching on pseudo-vars (`__MODULE__`, `__DIR__`, etc)
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix release] Respect the `:path` option when creating a `:tar` file for releases
|
||||
|
||||
## v1.10.2 (2020-02-26)
|
||||
|
||||
### 1. Bug fixes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Macro] Fix a bug where `Macro.to_string/1` would emit invalid code for sigils
|
||||
* [Task] Do not crash `async_stream` monitor if it receives spurious DOWN messages
|
||||
|
||||
#### Logger
|
||||
|
||||
* [Logger] Fix a bug where the Logger formatter would fail when handling unknown metadata values
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix compile] Do not write files to disk if `--warnings-as-errors` was given and warnings were emitted
|
||||
|
||||
## v1.10.1 (2020-02-10)
|
||||
|
||||
### 1. Bug fixes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Code] Do not emit invalid code when formatting `nil`, `false`, and `true` keys in maps
|
||||
* [Kernel] Ensure `with` clauses properly unpack "implicit guards" (such as matching on the struct name)
|
||||
* [Kernel] Do not warn if commas are used by themselves in `~w`/`~W` sigils
|
||||
* [Kernel] Do not validate the `:line` option in quote (the validation has been moved to v1.11 to give users more time to update their code)
|
||||
* [Module] Ensure the code verifier handles the `:erlang.size/1` guard properly
|
||||
|
||||
#### Logger
|
||||
|
||||
* [Logger] Properly handle the `report_cb/2` option from Erlang
|
||||
* [Logger] Fix truncation for multi-byte characters
|
||||
* [Logger] Do not rebroadcast messages from remote nodes as this is now taken care by Erlang's logger
|
||||
|
||||
#### ExUnit
|
||||
|
||||
* [ExUnit] Ensure `assert_receive` produces valid exception messages in case of errors
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix release] Make sure the install command (Window specific) works on paths with spaces in the name
|
||||
* [mix release] Allow using `remote` and `rpc` commands with `Application.compile_env/3`
|
||||
|
||||
## v1.10.0 (2020-01-27)
|
||||
|
||||
### 1. Enhancements
|
||||
|
||||
@@ -116,6 +247,7 @@ Finally, ExUnit comes with two small but important improvements: `ExUnit.Capture
|
||||
* [Kernel] Warn when function head comes immediately after the implementation instead of before the implementation
|
||||
* [Kernel] Warn if duplicate key is found in struct declaration
|
||||
* [Kernel] Print all undefined functions as warnings and then raise. This allows users to see all undefined calls at once, when it would otherwise require them to compile the code multiple times
|
||||
* [Kernel] Allow file, line and context to be dynamically set on `quote`
|
||||
* [Keyword] Add `Keyword.pop!/2` and `Keyword.pop_values/2`
|
||||
* [Map] Add `Map.pop!/2`
|
||||
* [MapSet] Optimize multiple operations
|
||||
@@ -144,7 +276,7 @@ Finally, ExUnit comes with two small but important improvements: `ExUnit.Capture
|
||||
#### Mix
|
||||
|
||||
* [mix compile] Add `--profile time` flag to profile compilation steps
|
||||
* [mix deps.compile] Add `--skip-umbrella-apps` flag. The new flag does not compile umbrella apps. This is useful for building caches in CD/CI pipelines
|
||||
* [mix deps.compile] Add `--skip-umbrella-children` flag. The new flag does not compile umbrella apps. This is useful for building caches in CD/CI pipelines
|
||||
* [mix deps.unlock] Add `--check-unused` flag. The new flag raises if there are any unused dependencies in the lock file
|
||||
* [mix release] Allow `RELEASE_DISTRIBUTION` to be set to `none`
|
||||
* [mix release] Support overlays in `rel/overlays`
|
||||
@@ -164,12 +296,19 @@ Finally, ExUnit comes with two small but important improvements: `ExUnit.Capture
|
||||
|
||||
* [Enum] Allow positive range slices on infinite streams given to `Enum.slice/2`
|
||||
* [Kernel] Raise error on functions/guards without implementation
|
||||
* [Kernel] Do not expand expressions inside interpolation twice
|
||||
* [Keyword] Ensure keyword replace and update preserve order
|
||||
* [Module] Raise instead of silently failing when performing a write module operation during after-compile
|
||||
* [Module] Fix `@macrocallback` definitions with a `when` clause
|
||||
* [Path] Fix `Path.absname/1` to correctly handle UNC paths on Windows
|
||||
* [Stream] Close with correct accumulator in `Stream.resource/3` when called for a single-element list
|
||||
* [Stream] Allow `Stream.cycle/1` to be double nested inside `Stream.cycle/1`
|
||||
* [URI] Preserve slashes in URIs without authority
|
||||
* [URI] Require a nil or an absolute path on URIs with host or authority
|
||||
|
||||
#### ExUnit
|
||||
|
||||
* [ExUnit.Assertions] Fix `assert_receive` and `assert match?` to behave consistently compared `receive` and `match?` when given an invalid macro
|
||||
|
||||
#### IEx
|
||||
|
||||
@@ -192,6 +331,7 @@ Finally, ExUnit comes with two small but important improvements: `ExUnit.Capture
|
||||
|
||||
#### Mix
|
||||
|
||||
* [Mix.Config] `Mix.Config.persist/1` has been deprecated. Instead of `Mix.Config.persist(config)` use `Application.put_all_env(config, persistent: true)` (`Application.put_all_env/2` was added in v1.9)
|
||||
* [mix xref] `calls/0` is deprecated in favor of compiler tracer
|
||||
* [mix xref] The `xref.exclude` option has been moved to `elixirc_options.no_warn_undefined` as the `xref` pass has been moved into the compiler
|
||||
|
||||
@@ -214,6 +354,7 @@ Finally, ExUnit comes with two small but important improvements: `ExUnit.Capture
|
||||
#### Mix
|
||||
|
||||
* [mix compile.xref] This check has been moved into the compiler and has no effect now
|
||||
* [mix xref] `xref` now only tracks dependencies between modules and files, no longer between functions. See "Compilation tracers" to learn more about how to track this information directly
|
||||
* [mix xref deprecations] This check has been moved into the compiler and has no effect now
|
||||
* [mix xref unreachable] This check has been moved into the compiler and has no effect now
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ PREFIX ?= /usr/local
|
||||
TEST_FILES ?= "*_test.exs"
|
||||
SHARE_PREFIX ?= $(PREFIX)/share
|
||||
MAN_PREFIX ?= $(SHARE_PREFIX)/man
|
||||
CANONICAL := master/ # master/ or vMAJOR.MINOR/
|
||||
CANONICAL := v1.10/ # master/ or vMAJOR.MINOR/
|
||||
ELIXIRC := bin/elixirc --verbose --ignore-module-conflict $(ELIXIRC_OPTS)
|
||||
ERLC := erlc -I lib/elixir/include $(ERLC_OPTS)
|
||||
ERL := erl -I lib/elixir/include -noshell -pa lib/elixir/ebin
|
||||
|
||||
+2
-3
@@ -6,12 +6,11 @@ Elixir applies bug fixes only to the latest minor branch. Security patches are a
|
||||
|
||||
| Elixir version | Support
|
||||
| -------------- | ------------------------------
|
||||
| 1.10 | Development
|
||||
| 1.9 | Bug fixes and security patches
|
||||
| 1.10 | Bug fixes and security patches
|
||||
| 1.9 | Security patches only
|
||||
| 1.8 | Security patches only
|
||||
| 1.7 | Security patches only
|
||||
| 1.6 | Security patches only
|
||||
| 1.5 | Security patches only
|
||||
|
||||
## Announcements
|
||||
|
||||
|
||||
+12
-3
@@ -43,6 +43,15 @@ defmodule Code do
|
||||
You can configure your list of tracers via `put_compiler_option/2`. The
|
||||
following events are available to tracers:
|
||||
|
||||
* `:start` - (since v1.11.0) invoked whenever the compiler starts to trace
|
||||
a new lexical context, such as a new file. Keep in mind the compiler runs
|
||||
in parallel, so multiple files may invoke `:start` and run at the same
|
||||
time. The value of the `lexical_tracker` of the macro environment, albeit
|
||||
opaque, can be used to uniquely identify the environment.
|
||||
|
||||
* `:stop` - (since v1.11.0) invoked whenever the compiler stops tracing a
|
||||
new lexical context, such as a new file.
|
||||
|
||||
* `{:import, meta, module, opts}` - traced whenever `module` is imported.
|
||||
`meta` is the import AST metadata and `opts` are the import options.
|
||||
|
||||
@@ -75,8 +84,8 @@ defmodule Code do
|
||||
function or macro is referenced. `meta` is the call AST metadata, `module`
|
||||
is the invoked module, followed by the `name` and `arity`.
|
||||
|
||||
* `{:local_function, meta, module, name, arity}` and
|
||||
`{:local_macro, meta, module, name, arity}` - traced whenever a local
|
||||
* `{:local_function, meta, name, arity}` and
|
||||
`{:local_macro, meta, name, arity}` - traced whenever a local
|
||||
function or macro is referenced. `meta` is the call AST metadata, `module`
|
||||
is the invoked module, followed by the `name` and `arity`.
|
||||
|
||||
@@ -1392,6 +1401,6 @@ defmodule Code do
|
||||
defp verify_loaded(loaded) do
|
||||
maps_binaries = Enum.map(loaded, fn {_module, map, binary} -> {map, binary} end)
|
||||
Module.ParallelChecker.verify(maps_binaries, [])
|
||||
Enum.map(loaded, fn {module, map, _binary} -> {module, map} end)
|
||||
Enum.map(loaded, fn {module, _map, binary} -> {module, binary} end)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1914,8 +1914,7 @@ defmodule Code.Formatter do
|
||||
{args_docs, comments?, %{state | comments: comments}}
|
||||
end
|
||||
|
||||
defp each_quoted_to_algebra_with_comments(args, acc, max_line, state, comments?, fun) do
|
||||
[arg | args] = args
|
||||
defp each_quoted_to_algebra_with_comments([arg | args], acc, max_line, state, comments?, fun) do
|
||||
{doc_start, doc_end} = traverse_line(arg, {@max_line, @min_line})
|
||||
|
||||
{acc, comments, comments?} =
|
||||
@@ -1926,7 +1925,7 @@ defmodule Code.Formatter do
|
||||
{acc, comments, comments?} =
|
||||
extract_comments_trailing(doc_start, doc_end, acc, state.comments, comments?)
|
||||
|
||||
acc = [doc_triplet | acc]
|
||||
acc = [adjust_trailing_newlines(doc_triplet, doc_end, comments) | acc]
|
||||
state = %{state | comments: comments}
|
||||
each_quoted_to_algebra_with_comments(args, acc, max_line, state, comments?, fun)
|
||||
end
|
||||
@@ -1957,6 +1956,15 @@ defmodule Code.Formatter do
|
||||
{acc, rest, comments?}
|
||||
end
|
||||
|
||||
# If the document is immediately followed by comment which is followed by newlines,
|
||||
# its newlines wouldn't have considerd the comment, so we need to adjust it.
|
||||
defp adjust_trailing_newlines({doc, next_line, newlines}, doc_end, [{line, _, _} | _])
|
||||
when newlines > 1 and line == doc_end + 1 do
|
||||
{doc, next_line, 1}
|
||||
end
|
||||
|
||||
defp adjust_trailing_newlines(doc_triplet, _, _), do: doc_triplet
|
||||
|
||||
defp traverse_line({expr, meta, args}, {min, max}) do
|
||||
acc =
|
||||
case Keyword.fetch(meta, :line) do
|
||||
@@ -2239,10 +2247,10 @@ defmodule Code.Formatter do
|
||||
defp keyword?(rest), do: rest == []
|
||||
|
||||
defp keyword_key?({:__block__, meta, [atom]}) when is_atom(atom),
|
||||
do: meta[:delimiter] != ":"
|
||||
do: meta[:format] == :keyword
|
||||
|
||||
defp keyword_key?({{:., _, [:erlang, :binary_to_atom]}, meta, [{:<<>>, _, _}, :utf8]}),
|
||||
do: meta[:delimiter] != ":"
|
||||
do: meta[:format] == :keyword
|
||||
|
||||
defp keyword_key?(_),
|
||||
do: false
|
||||
|
||||
@@ -110,7 +110,8 @@ defmodule Config.Provider do
|
||||
:config_path,
|
||||
extra_config: [],
|
||||
prune_after_boot: false,
|
||||
reboot_after_config: true
|
||||
reboot_after_config: true,
|
||||
validate_compile_env: false
|
||||
]
|
||||
|
||||
@doc """
|
||||
@@ -165,54 +166,74 @@ defmodule Config.Provider do
|
||||
# before we go around running Elixir code.
|
||||
{:ok, _} = :application.ensure_all_started(:elixir)
|
||||
|
||||
case :application.get_env(app, key) do
|
||||
{:ok, %Config.Provider{} = provider} ->
|
||||
path = resolve_config_path!(provider.config_path)
|
||||
validate_no_cyclic_boot!(path)
|
||||
loaded_applications = :application.loaded_applications()
|
||||
original_config = read_config!(path)
|
||||
# The key we store if the system already booted
|
||||
booted_key = :"#{key}_booted"
|
||||
|
||||
config =
|
||||
original_config
|
||||
|> Config.__merge__(provider.extra_config)
|
||||
|> run_providers(provider)
|
||||
case :application.get_env(app, booted_key) do
|
||||
{:ok, {:booted, path}} ->
|
||||
path && File.rm(path)
|
||||
|
||||
if provider.reboot_after_config do
|
||||
config
|
||||
|> Config.__merge__([{app, [{key, booted_key(provider, path)}]}])
|
||||
|> write_config!(path)
|
||||
|
||||
restart_fun.()
|
||||
else
|
||||
for {app, _, _} <- loaded_applications, config[app] != original_config[app] do
|
||||
abort("""
|
||||
Cannot configure #{inspect(app)} because :reboot_after_config has been set \
|
||||
to false and #{inspect(app)} has already been loaded, meaning any further \
|
||||
configuration won't have an effect.
|
||||
|
||||
The configuration for #{inspect(app)} before config providers was:
|
||||
|
||||
#{inspect(original_config[app])}
|
||||
|
||||
The configuration for #{inspect(app)} after config providers was:
|
||||
|
||||
#{inspect(config[app])}
|
||||
""")
|
||||
end
|
||||
|
||||
_ = Application.put_all_env(config, persistent: true)
|
||||
:ok
|
||||
with {:ok, %Config.Provider{} = provider} <- :application.get_env(app, key) do
|
||||
maybe_validate_compile_env(provider)
|
||||
end
|
||||
|
||||
{:ok, {:booted, path}} ->
|
||||
File.rm(path)
|
||||
:booted
|
||||
|
||||
{:ok, :booted} ->
|
||||
:booted
|
||||
|
||||
_ ->
|
||||
:skip
|
||||
case :application.get_env(app, key) do
|
||||
{:ok, %Config.Provider{} = provider} ->
|
||||
path = resolve_config_path!(provider.config_path)
|
||||
reboot_config = [{app, [{booted_key, booted_value(provider, path)}]}]
|
||||
boot_providers(path, provider, reboot_config, restart_fun)
|
||||
|
||||
_ ->
|
||||
:skip
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp boot_providers(path, provider, reboot_config, restart_fun) do
|
||||
validate_no_cyclic_boot!(path)
|
||||
loaded_applications = :application.loaded_applications()
|
||||
original_config = read_config!(path)
|
||||
|
||||
config =
|
||||
original_config
|
||||
|> Config.__merge__(provider.extra_config)
|
||||
|> run_providers(provider)
|
||||
|
||||
if provider.reboot_after_config do
|
||||
config
|
||||
|> Config.__merge__(reboot_config)
|
||||
|> write_config!(path)
|
||||
|
||||
restart_fun.()
|
||||
else
|
||||
for {app, _, _} <- loaded_applications, config[app] != original_config[app] do
|
||||
abort("""
|
||||
Cannot configure #{inspect(app)} because :reboot_after_config has been set \
|
||||
to false and #{inspect(app)} has already been loaded, meaning any further \
|
||||
configuration won't have an effect.
|
||||
|
||||
The configuration for #{inspect(app)} before config providers was:
|
||||
|
||||
#{inspect(original_config[app])}
|
||||
|
||||
The configuration for #{inspect(app)} after config providers was:
|
||||
|
||||
#{inspect(config[app])}
|
||||
""")
|
||||
end
|
||||
|
||||
_ = Application.put_all_env(config, persistent: true)
|
||||
maybe_validate_compile_env(provider)
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_validate_compile_env(provider) do
|
||||
with [_ | _] = compile_env <- provider.validate_compile_env do
|
||||
validate_compile_env(compile_env)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -283,8 +304,8 @@ defmodule Config.Provider do
|
||||
Process.sleep(:infinity)
|
||||
end
|
||||
|
||||
defp booted_key(%{prune_after_boot: true}, path), do: {:booted, path}
|
||||
defp booted_key(%{prune_after_boot: false}, _path), do: :booted
|
||||
defp booted_value(%{prune_after_boot: true}, path), do: {:booted, path}
|
||||
defp booted_value(%{prune_after_boot: false}, _path), do: {:booted, nil}
|
||||
|
||||
defp validate_no_cyclic_boot!(path) do
|
||||
if System.get_env("ELIXIR_CONFIG_PROVIDER_BOOTED") do
|
||||
|
||||
+40
-31
@@ -1530,6 +1530,11 @@ defmodule Enum do
|
||||
{:lists.reverse(list), acc}
|
||||
end
|
||||
|
||||
@doc false
|
||||
def max(enumerable, empty_fallback) when is_function(empty_fallback, 0) do
|
||||
max(enumerable, &>=/2, empty_fallback)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the maximal element in the `enumerable` according
|
||||
to Erlang's term ordering.
|
||||
@@ -1572,26 +1577,22 @@ defmodule Enum do
|
||||
0
|
||||
|
||||
"""
|
||||
@spec max(t, (() -> empty_result)) :: element | empty_result when empty_result: any
|
||||
@spec max(t, (element, element -> boolean) | module(), (() -> empty_result)) ::
|
||||
element | empty_result
|
||||
when empty_result: any
|
||||
def max(enumerable, sorter \\ &>=/2, empty_fallback \\ fn -> raise Enum.EmptyError end) do
|
||||
{sorter, empty_fallback} = max_sort_fun(sorter, empty_fallback)
|
||||
aggregate(enumerable, sorter, empty_fallback)
|
||||
aggregate(enumerable, max_sort_fun(sorter), empty_fallback)
|
||||
end
|
||||
|
||||
# TODO: Deprecate me on 1.14
|
||||
defp max_sort_fun(empty_fallback, default_empty_fallback)
|
||||
when is_function(empty_fallback, 0) and is_function(default_empty_fallback, 0),
|
||||
do: {&>=/2, empty_fallback}
|
||||
defp max_sort_fun(sorter) when is_function(sorter, 2), do: sorter
|
||||
defp max_sort_fun(module) when is_atom(module), do: &(module.compare(&1, &2) != :lt)
|
||||
|
||||
defp max_sort_fun(sorter, empty_fallback)
|
||||
when is_function(sorter, 2) and is_function(empty_fallback, 0),
|
||||
do: {sorter, empty_fallback}
|
||||
|
||||
defp max_sort_fun(module, empty_fallback)
|
||||
when is_atom(module) and is_function(empty_fallback, 0),
|
||||
do: {&(module.compare(&1, &2) != :lt), empty_fallback}
|
||||
@doc false
|
||||
def max_by(enumerable, fun, empty_fallback)
|
||||
when is_function(fun, 1) and is_function(empty_fallback, 0) do
|
||||
max_by(enumerable, fun, &>=/2, empty_fallback)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the maximal element in the `enumerable` as calculated
|
||||
@@ -1637,6 +1638,8 @@ defmodule Enum do
|
||||
nil
|
||||
|
||||
"""
|
||||
@spec max_by(t, (element -> any), (() -> empty_result)) :: element | empty_result
|
||||
when empty_result: any
|
||||
@spec max_by(
|
||||
t,
|
||||
(element -> any),
|
||||
@@ -1646,8 +1649,7 @@ defmodule Enum do
|
||||
when empty_result: any
|
||||
def max_by(enumerable, fun, sorter \\ &>=/2, empty_fallback \\ fn -> raise Enum.EmptyError end)
|
||||
when is_function(fun, 1) do
|
||||
{sorter, empty_fallback} = max_sort_fun(sorter, empty_fallback)
|
||||
aggregate_by(enumerable, fun, sorter, empty_fallback)
|
||||
aggregate_by(enumerable, fun, max_sort_fun(sorter), empty_fallback)
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -1690,6 +1692,11 @@ defmodule Enum do
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def min(enumerable, empty_fallback) when is_function(empty_fallback, 0) do
|
||||
min(enumerable, &<=/2, empty_fallback)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the minimal element in the `enumerable` according
|
||||
to Erlang's term ordering.
|
||||
@@ -1728,30 +1735,26 @@ defmodule Enum do
|
||||
Finally, if you don't want to raise on empty enumerables, you can pass
|
||||
the empty fallback:
|
||||
|
||||
iex> Enum.min([], &<=/2, fn -> 0 end)
|
||||
iex> Enum.min([], fn -> 0 end)
|
||||
0
|
||||
|
||||
"""
|
||||
@spec min(t, (() -> empty_result)) :: element | empty_result when empty_result: any
|
||||
@spec min(t, (element, element -> boolean) | module(), (() -> empty_result)) ::
|
||||
element | empty_result
|
||||
when empty_result: any
|
||||
def min(enumerable, sorter \\ &<=/2, empty_fallback \\ fn -> raise Enum.EmptyError end) do
|
||||
{sorter, empty_fallback} = min_sort_fun(sorter, empty_fallback)
|
||||
aggregate(enumerable, sorter, empty_fallback)
|
||||
aggregate(enumerable, min_sort_fun(sorter), empty_fallback)
|
||||
end
|
||||
|
||||
# TODO: Deprecate me on 1.14
|
||||
defp min_sort_fun(empty_fallback, default_empty_fallback)
|
||||
when is_function(empty_fallback, 0) and is_function(default_empty_fallback, 0),
|
||||
do: {&<=/2, empty_fallback}
|
||||
defp min_sort_fun(sorter) when is_function(sorter, 2), do: sorter
|
||||
defp min_sort_fun(module) when is_atom(module), do: &(module.compare(&1, &2) != :gt)
|
||||
|
||||
defp min_sort_fun(sorter, empty_fallback)
|
||||
when is_function(sorter, 2) and is_function(empty_fallback, 0),
|
||||
do: {sorter, empty_fallback}
|
||||
|
||||
defp min_sort_fun(module, empty_fallback)
|
||||
when is_atom(module) and is_function(empty_fallback, 0),
|
||||
do: {&(module.compare(&1, &2) != :gt), empty_fallback}
|
||||
@doc false
|
||||
def min_by(enumerable, fun, empty_fallback)
|
||||
when is_function(fun, 1) and is_function(empty_fallback, 0) do
|
||||
min_by(enumerable, fun, &<=/2, empty_fallback)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the minimal element in the `enumerable` as calculated
|
||||
@@ -1799,10 +1802,16 @@ defmodule Enum do
|
||||
"""
|
||||
@spec min_by(t, (element -> any), (() -> empty_result)) :: element | empty_result
|
||||
when empty_result: any
|
||||
@spec min_by(
|
||||
t,
|
||||
(element -> any),
|
||||
(element, element -> boolean) | module(),
|
||||
(() -> empty_result)
|
||||
) :: element | empty_result
|
||||
when empty_result: any
|
||||
def min_by(enumerable, fun, sorter \\ &<=/2, empty_fallback \\ fn -> raise Enum.EmptyError end)
|
||||
when is_function(fun, 1) do
|
||||
{sorter, empty_fallback} = min_sort_fun(sorter, empty_fallback)
|
||||
aggregate_by(enumerable, fun, sorter, empty_fallback)
|
||||
aggregate_by(enumerable, fun, min_sort_fun(sorter), empty_fallback)
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
||||
@@ -640,7 +640,7 @@ defmodule GenServer do
|
||||
This callback is optional.
|
||||
"""
|
||||
@callback terminate(reason, state :: term) :: term
|
||||
when reason: :normal | :shutdown | {:shutdown, term}
|
||||
when reason: :normal | :shutdown | {:shutdown, term} | term
|
||||
|
||||
@doc """
|
||||
Invoked to change the state of the `GenServer` when a different version of a
|
||||
|
||||
@@ -2263,8 +2263,7 @@ defmodule Kernel do
|
||||
iex> get_in(users, ["john", :age])
|
||||
27
|
||||
|
||||
In case any of the entries in the middle returns `nil`, `nil` will
|
||||
be returned as per the `Access` module:
|
||||
In case any of the keys returns `nil`, `nil` will be returned:
|
||||
|
||||
iex> users = %{"john" => %{age: 27}, "meg" => %{age: 23}}
|
||||
iex> get_in(users, ["unknown", :age])
|
||||
@@ -2280,9 +2279,6 @@ defmodule Kernel do
|
||||
* a function to be invoked next
|
||||
|
||||
This means `get_in/2` can be extended to provide custom lookups.
|
||||
The downside is that functions cannot be stored as keys in the accessed
|
||||
data structures.
|
||||
|
||||
In the example below, we use a function to get all the maps inside
|
||||
a list:
|
||||
|
||||
@@ -4108,7 +4104,7 @@ defmodule Kernel do
|
||||
end
|
||||
|
||||
"""
|
||||
defmacro def(call, expr \\ []) do
|
||||
defmacro def(call, expr \\ nil) do
|
||||
define(:def, call, expr, __CALLER__)
|
||||
end
|
||||
|
||||
@@ -4138,7 +4134,7 @@ defmodule Kernel do
|
||||
** (UndefinedFunctionError) undefined function Foo.sum/2
|
||||
|
||||
"""
|
||||
defmacro defp(call, expr \\ []) do
|
||||
defmacro defp(call, expr \\ nil) do
|
||||
define(:defp, call, expr, __CALLER__)
|
||||
end
|
||||
|
||||
@@ -4166,7 +4162,7 @@ defmodule Kernel do
|
||||
end
|
||||
|
||||
"""
|
||||
defmacro defmacro(call, expr \\ []) do
|
||||
defmacro defmacro(call, expr \\ nil) do
|
||||
define(:defmacro, call, expr, __CALLER__)
|
||||
end
|
||||
|
||||
@@ -4182,7 +4178,7 @@ defmodule Kernel do
|
||||
naming and default arguments.
|
||||
|
||||
"""
|
||||
defmacro defmacrop(call, expr \\ []) do
|
||||
defmacro defmacrop(call, expr \\ nil) do
|
||||
define(:defmacrop, call, expr, __CALLER__)
|
||||
end
|
||||
|
||||
@@ -4677,7 +4673,7 @@ defmodule Kernel do
|
||||
macro_definition =
|
||||
case impls do
|
||||
[] ->
|
||||
define(kind, call, [], env)
|
||||
define(kind, call, nil, env)
|
||||
|
||||
[guard] ->
|
||||
quoted =
|
||||
@@ -5388,7 +5384,8 @@ defmodule Kernel do
|
||||
true ->
|
||||
parts = String.split(string)
|
||||
|
||||
parts_with_trailing_comma = :lists.filter(&(:binary.last(&1) == ?,), parts)
|
||||
parts_with_trailing_comma =
|
||||
:lists.filter(&(byte_size(&1) > 1 and :binary.last(&1) == ?,), parts)
|
||||
|
||||
if parts_with_trailing_comma != [] do
|
||||
stacktrace = Macro.Env.stacktrace(caller)
|
||||
|
||||
@@ -139,15 +139,15 @@ defmodule Kernel.ParallelCompiler do
|
||||
compiler_pid = self()
|
||||
:elixir_code_server.cast({:reset_warnings, compiler_pid})
|
||||
schedulers = max(:erlang.system_info(:schedulers_online), 2)
|
||||
beam_timestamp = Keyword.get(options, :beam_timestamp)
|
||||
|
||||
result =
|
||||
outcome =
|
||||
spawn_workers(files, 0, [], [], %{}, [], %{
|
||||
dest: Keyword.get(options, :dest),
|
||||
each_cycle: Keyword.get(options, :each_cycle, fn -> {:runtime, []} end),
|
||||
each_file: Keyword.get(options, :each_file, fn _, _ -> :ok end) |> each_file(),
|
||||
each_long_compilation: Keyword.get(options, :each_long_compilation, fn _file -> :ok end),
|
||||
each_module: Keyword.get(options, :each_module, fn _file, _module, _binary -> :ok end),
|
||||
beam_timestamp: Keyword.get(options, :beam_timestamp),
|
||||
long_compilation_threshold: Keyword.get(options, :long_compilation_threshold, 15),
|
||||
profile: Keyword.get(options, :profile),
|
||||
cycle_start: System.monotonic_time(),
|
||||
@@ -160,7 +160,7 @@ defmodule Kernel.ParallelCompiler do
|
||||
# compilation status will be set to error.
|
||||
compilation_status = :elixir_code_server.call({:compilation_status, compiler_pid})
|
||||
|
||||
case {result, compilation_status} do
|
||||
case {outcome, compilation_status} do
|
||||
{{:ok, _, warnings}, :error} ->
|
||||
message = "Compilation failed due to warnings while using the --warnings-as-errors option"
|
||||
IO.puts(:stderr, message)
|
||||
@@ -169,8 +169,11 @@ defmodule Kernel.ParallelCompiler do
|
||||
{{:error, errors, warnings}, :error} ->
|
||||
{:error, errors ++ warnings, []}
|
||||
|
||||
_ ->
|
||||
result
|
||||
{{:ok, outcome, warnings}, _} ->
|
||||
{:ok, write_module_binaries(outcome, output, beam_timestamp), warnings}
|
||||
|
||||
{{:error, errors, warnings}, _} ->
|
||||
{:error, errors, warnings}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -187,6 +190,74 @@ defmodule Kernel.ParallelCompiler do
|
||||
end
|
||||
end
|
||||
|
||||
defp write_module_binaries(result, {:compile, path}, timestamp) do
|
||||
Enum.flat_map(result, fn
|
||||
{{:module, module}, {binary, _map}} ->
|
||||
full_path = Path.join(path, Atom.to_string(module) <> ".beam")
|
||||
File.write!(full_path, binary)
|
||||
if timestamp, do: File.touch!(full_path, timestamp)
|
||||
[module]
|
||||
|
||||
_ ->
|
||||
[]
|
||||
end)
|
||||
end
|
||||
|
||||
defp write_module_binaries(result, _output, _timestamp) do
|
||||
for {{:module, module}, _} <- result, do: module
|
||||
end
|
||||
|
||||
## Verification
|
||||
|
||||
defp verify_modules(result, warnings, dependent_modules, state) do
|
||||
checker_warnings = maybe_check_modules(result, dependent_modules, state)
|
||||
warnings = Enum.reverse(warnings, checker_warnings)
|
||||
{:ok, result, warnings}
|
||||
end
|
||||
|
||||
defp maybe_check_modules(result, runtime_modules, state) do
|
||||
%{schedulers: schedulers, profile: profile} = state
|
||||
|
||||
if :elixir_config.get(:bootstrap) do
|
||||
[]
|
||||
else
|
||||
compiled_modules = checker_compiled_modules(result)
|
||||
runtime_modules = checker_runtime_modules(runtime_modules)
|
||||
|
||||
profile_checker(profile, compiled_modules, runtime_modules, fn ->
|
||||
Module.ParallelChecker.verify(compiled_modules, runtime_modules, schedulers)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
defp checker_compiled_modules(result) do
|
||||
for {{:module, _module}, {binary, module_map}} <- result do
|
||||
{module_map, binary}
|
||||
end
|
||||
end
|
||||
|
||||
defp checker_runtime_modules(modules) do
|
||||
for module <- modules,
|
||||
path = :code.which(module),
|
||||
is_list(path) and path != [] do
|
||||
{module, File.read!(path)}
|
||||
end
|
||||
end
|
||||
|
||||
defp profile_checker(_profile = :time, compiled_modules, runtime_modules, fun) do
|
||||
{time, result} = :timer.tc(fun)
|
||||
time = div(time, 1000)
|
||||
num_modules = length(compiled_modules) + length(runtime_modules)
|
||||
IO.puts(:stderr, "[profile] Finished group pass check of #{num_modules} modules in #{time}ms")
|
||||
result
|
||||
end
|
||||
|
||||
defp profile_checker(_profile = nil, _compiled_modules, _runtime_modules, fun) do
|
||||
fun.()
|
||||
end
|
||||
|
||||
## Compiler worker spawning
|
||||
|
||||
# We already have n=schedulers currently running, don't spawn new ones
|
||||
defp spawn_workers(
|
||||
queue,
|
||||
@@ -253,10 +324,10 @@ defmodule Kernel.ParallelCompiler do
|
||||
|
||||
case each_cycle_return(state.each_cycle.()) do
|
||||
{:runtime, dependent_modules} ->
|
||||
write_and_verify_modules(result, warnings, dependent_modules, state)
|
||||
verify_modules(result, warnings, dependent_modules, state)
|
||||
|
||||
{:compile, []} ->
|
||||
write_and_verify_modules(result, warnings, [], state)
|
||||
verify_modules(result, warnings, [], state)
|
||||
|
||||
{:compile, more} ->
|
||||
spawn_workers(more, 0, [], [], result, warnings, state)
|
||||
@@ -354,77 +425,13 @@ defmodule Kernel.ParallelCompiler do
|
||||
defp each_cycle_return(modules) when is_list(modules), do: {:compile, modules}
|
||||
defp each_cycle_return(other), do: other
|
||||
|
||||
defp write_and_verify_modules(result, warnings, dependent_modules, state) do
|
||||
modules = write_module_binaries(result, state)
|
||||
checker_warnings = maybe_check_modules(result, dependent_modules, state)
|
||||
warnings = Enum.reverse(warnings, checker_warnings)
|
||||
{:ok, modules, warnings}
|
||||
end
|
||||
|
||||
defp write_module_binaries(result, %{output: {:compile, path}, beam_timestamp: timestamp}) do
|
||||
Enum.flat_map(result, fn
|
||||
{{:module, module}, {binary, _map}} ->
|
||||
full_path = Path.join(path, Atom.to_string(module) <> ".beam")
|
||||
File.write!(full_path, binary)
|
||||
if timestamp, do: File.touch!(full_path, timestamp)
|
||||
[module]
|
||||
|
||||
_ ->
|
||||
[]
|
||||
end)
|
||||
end
|
||||
|
||||
defp write_module_binaries(result, _state) do
|
||||
for {{:module, module}, _} <- result, do: module
|
||||
end
|
||||
|
||||
defp maybe_check_modules(result, runtime_modules, state) do
|
||||
%{schedulers: schedulers, profile: profile} = state
|
||||
|
||||
if :elixir_config.get(:bootstrap) do
|
||||
[]
|
||||
else
|
||||
compiled_modules = checker_compiled_modules(result)
|
||||
runtime_modules = checker_runtime_modules(runtime_modules)
|
||||
|
||||
profile_checker(profile, compiled_modules, runtime_modules, fn ->
|
||||
Module.ParallelChecker.verify(compiled_modules, runtime_modules, schedulers)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
defp checker_compiled_modules(result) do
|
||||
for {{:module, _module}, {binary, module_map}} <- result do
|
||||
{module_map, binary}
|
||||
end
|
||||
end
|
||||
|
||||
defp checker_runtime_modules(modules) do
|
||||
for module <- modules,
|
||||
path = :code.which(module),
|
||||
is_list(path) do
|
||||
{module, File.read!(path)}
|
||||
end
|
||||
end
|
||||
|
||||
defp profile_checker(_profile = :time, compiled_modules, runtime_modules, fun) do
|
||||
{time, result} = :timer.tc(fun)
|
||||
time = div(time, 1000)
|
||||
num_modules = length(compiled_modules) + length(runtime_modules)
|
||||
IO.puts(:stderr, "[profile] Finished group pass check of #{num_modules} modules in #{time}ms")
|
||||
result
|
||||
end
|
||||
|
||||
defp profile_checker(_profile = nil, _compiled_modules, _runtime_modules, fun) do
|
||||
fun.()
|
||||
end
|
||||
|
||||
# The goal of this function is to find leaves in the dependency graph,
|
||||
# i.e. to find code that depends on code that we know is not being defined.
|
||||
# Note that not all files have been compile yet, so they may not be in waiting.
|
||||
defp without_definition(waiting, files) do
|
||||
nillify_empty(
|
||||
for {pid, _, _, _} <- files,
|
||||
{_, ^pid, ref, on, _, _} = List.keyfind(waiting, pid, 1),
|
||||
{_, ^pid, ref, on, _, _} <- List.wrap(List.keyfind(waiting, pid, 1)),
|
||||
not Enum.any?(waiting, fn {_, _, _, _, defining, _} -> on in defining end),
|
||||
do: {ref, :not_found}
|
||||
)
|
||||
|
||||
@@ -14,7 +14,8 @@ defmodule Kernel.SpecialForms do
|
||||
forms used to define tuple and binary data structures respectively.
|
||||
|
||||
This module also documents macros that return information about Elixir's
|
||||
compilation environment, such as (`__ENV__/0`, `__MODULE__/0`, `__DIR__/0` and `__CALLER__/0`).
|
||||
compilation environment, such as (`__ENV__/0`, `__MODULE__/0`, `__DIR__/0`,
|
||||
`__STACKTRACE__/0`, and `__CALLER__/0`).
|
||||
|
||||
Additionally, it documents two special forms, `__block__/1` and
|
||||
`__aliases__/1`, which are not intended to be called directly by the
|
||||
|
||||
@@ -163,8 +163,9 @@ defmodule Macro do
|
||||
with parens. The `:closing` does not delimit the end of expression if
|
||||
there are `:do` and `:end` metadata (when `:token_metadata` is true)
|
||||
* `:column` - the column number of the AST node (when `:columns` is true)
|
||||
* `:delimiter` - contains the opening delimiter for sigils, strings, atoms,
|
||||
and charlists as a string (such as `"{"`, `"/"`, `":"`, and the like)
|
||||
* `:delimiter` - contains the opening delimiter for sigils, strings,
|
||||
and charlists as a string (such as `"{"`, `"/"`, `"'"`, and the like)
|
||||
* `:format` - set to `:keyword` when an atom is defined as a keyword
|
||||
* `:do` - contains metadata about the `do` location in a function call with
|
||||
`do/end` blocks (when `:token_metadata` is true)
|
||||
* `:end` - contains metadata about the `end` location in a function call with
|
||||
@@ -1021,11 +1022,11 @@ defmodule Macro do
|
||||
|
||||
binary when is_binary(binary) ->
|
||||
binary = inspect_no_limit(binary)
|
||||
binary_part(binary, 1, byte_size(binary) - 2)
|
||||
binary = binary_part(binary, 1, byte_size(binary) - 2)
|
||||
escape_sigil(binary, left)
|
||||
end)
|
||||
|
||||
escaped = escape_sigil(parts, left)
|
||||
<<left::binary, escaped::binary, right::binary>>
|
||||
<<left::binary, parts::binary, right::binary>>
|
||||
end
|
||||
|
||||
defp escape_sigil(parts, "("), do: String.replace(parts, ")", ~S"\)")
|
||||
|
||||
@@ -265,8 +265,6 @@ defmodule Map do
|
||||
|
||||
iex> Map.fetch!(%{a: 1}, :a)
|
||||
1
|
||||
iex> Map.fetch!(%{a: 1}, :b)
|
||||
** (KeyError) key :b not found in: %{a: 1}
|
||||
|
||||
"""
|
||||
@spec fetch!(map, key) :: value
|
||||
|
||||
@@ -216,6 +216,8 @@ defmodule Module.Checker do
|
||||
# TODO: Do not warn inside guards
|
||||
# TODO: Properly handle protocols
|
||||
defp warn_undefined?(_module, :__impl__, 1, _state), do: false
|
||||
defp warn_undefined?(_module, :module_info, 0, _state), do: false
|
||||
defp warn_undefined?(_module, :module_info, 1, _state), do: false
|
||||
defp warn_undefined?(:erlang, :orelse, 2, _state), do: false
|
||||
defp warn_undefined?(:erlang, :andalso, 2, _state), do: false
|
||||
|
||||
|
||||
@@ -209,7 +209,11 @@ defmodule Module.ParallelChecker do
|
||||
end
|
||||
|
||||
defp cache_from_module_map(ets, map) do
|
||||
exports = [{{:__info__, 1}, :def} | definitions_to_exports(map.definitions)]
|
||||
exports =
|
||||
[{{:__info__, 1}, :def}] ++
|
||||
behaviour_exports(map) ++
|
||||
definitions_to_exports(map.definitions)
|
||||
|
||||
deprecated = Map.new(map.deprecated)
|
||||
cache_info(ets, map.module, exports, deprecated)
|
||||
end
|
||||
@@ -227,6 +231,7 @@ defmodule Module.ParallelChecker do
|
||||
defp info_exports(module) do
|
||||
Map.new(
|
||||
[{{:__info__, 1}, :def}] ++
|
||||
behaviour_exports(module) ++
|
||||
Enum.map(module.__info__(:macros), &{&1, :defmacro}) ++
|
||||
Enum.map(module.__info__(:functions), &{&1, :def})
|
||||
)
|
||||
@@ -268,6 +273,17 @@ defmodule Module.ParallelChecker do
|
||||
:ets.insert(ets, {{:cached, module}, true})
|
||||
end
|
||||
|
||||
defp behaviour_exports(%{is_behaviour: true}), do: [{{:behaviour_info, 1}, :def}]
|
||||
defp behaviour_exports(%{is_behaviour: false}), do: []
|
||||
|
||||
defp behaviour_exports(module) when is_atom(module) do
|
||||
if {:behaviour_info, 1} in module.module_info(:functions) do
|
||||
[{{:behaviour_info, 1}, :def}]
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
defp definitions_to_exports(definitions) do
|
||||
Enum.flat_map(definitions, fn {function, kind, _meta, _clauses} ->
|
||||
if kind in [:def, :defmacro] do
|
||||
|
||||
@@ -57,7 +57,7 @@ defmodule Module.Types do
|
||||
def of_head(params, guards, stack, context) do
|
||||
with {:ok, types, context} <-
|
||||
map_reduce_ok(params, context, &Pattern.of_pattern(&1, stack, &2)),
|
||||
# TODO: Check that of_guard/3 returns a boolean
|
||||
# TODO: Check that of_guard/3 returns boolean() | :fail
|
||||
{:ok, _, context} <- Pattern.of_guard(guards_to_or(guards), stack, context),
|
||||
do: {:ok, lift_types(types, context), context}
|
||||
end
|
||||
@@ -258,7 +258,7 @@ defmodule Module.Types do
|
||||
|
||||
def format_warning({:unable_unify, left, right, expr, traces}) do
|
||||
[
|
||||
"function clause will never match, found incompatibility:\n\n ",
|
||||
"incompatible types:\n\n ",
|
||||
format_type(left),
|
||||
" !~ ",
|
||||
format_type(right),
|
||||
|
||||
@@ -275,6 +275,7 @@ defmodule Module.Types.Pattern do
|
||||
{:binary_part, 3} => {[:binary, :integer, :integer], :binary},
|
||||
{:bit_size, 1} => {[:binary], :integer},
|
||||
{:byte_size, 1} => {[:binary], :integer},
|
||||
{:size, 1} => {[{:union, [:binary, :tuple]}], :boolean},
|
||||
{:div, 2} => {[:integer, :integer], :integer},
|
||||
{:rem, 2} => {[:integer, :integer], :integer},
|
||||
{:node, 0} => {[], :atom},
|
||||
@@ -333,10 +334,9 @@ defmodule Module.Types.Pattern do
|
||||
fresh_context = fresh_context(context)
|
||||
|
||||
with {:ok, left_type, left_context} <- of_guard(left, stack, fresh_context),
|
||||
{:ok, right_type, right_context} <- of_guard(right, stack, fresh_context),
|
||||
{:ok, _right_type, right_context} <- of_guard(right, stack, fresh_context),
|
||||
{:ok, context} <- merge_context_or(context, stack, left_context, right_context),
|
||||
{:ok, _, context} <- unify(left_type, :boolean, stack, context),
|
||||
{:ok, _, context} <- unify(right_type, :boolean, stack, context),
|
||||
do: {:ok, :boolean, context}
|
||||
end
|
||||
|
||||
|
||||
@@ -132,9 +132,9 @@ defmodule Port do
|
||||
While we encourage graceful termination by detecting if stdin/stdout has been
|
||||
closed, we do not always have control over how third-party software terminates.
|
||||
In those cases, you can wrap the application in a script that checks for stdin.
|
||||
Here is such script in `sh`:
|
||||
Here is such script that has been verified to work on bash shells:
|
||||
|
||||
#!/bin/sh
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Start the program in the background
|
||||
exec "$@" &
|
||||
|
||||
@@ -107,6 +107,7 @@ defmodule Stream do
|
||||
@type index :: non_neg_integer
|
||||
|
||||
@type default :: any
|
||||
@type timer :: non_neg_integer | :infinity
|
||||
|
||||
# Require Stream.Reducers and its callbacks
|
||||
require Stream.Reducers, as: R
|
||||
@@ -505,8 +506,10 @@ defmodule Stream do
|
||||
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
|
||||
|
||||
"""
|
||||
@spec interval(non_neg_integer) :: Enumerable.t()
|
||||
def interval(n) when is_integer(n) and n >= 0 do
|
||||
@spec interval(timer()) :: Enumerable.t()
|
||||
def interval(n)
|
||||
when is_integer(n) and n >= 0
|
||||
when n == :infinity do
|
||||
unfold(0, fn count ->
|
||||
Process.sleep(n)
|
||||
{count, count + 1}
|
||||
@@ -787,8 +790,10 @@ defmodule Stream do
|
||||
[0]
|
||||
|
||||
"""
|
||||
@spec timer(non_neg_integer) :: Enumerable.t()
|
||||
def timer(n) when is_integer(n) and n >= 0 do
|
||||
@spec timer(timer()) :: Enumerable.t()
|
||||
def timer(n)
|
||||
when is_integer(n) and n >= 0
|
||||
when n == :infinity do
|
||||
take(interval(n), 1)
|
||||
end
|
||||
|
||||
|
||||
+39
-11
@@ -455,13 +455,13 @@ defmodule String do
|
||||
For example, take the grapheme "é" which is made of the characters
|
||||
"e" and the acute accent. The following will split the string into two parts:
|
||||
|
||||
iex> String.split(:unicode.characters_to_nfd_binary("é"), "e")
|
||||
iex> String.split(String.normalize("é", :nfd), "e")
|
||||
["", "́"]
|
||||
|
||||
However, if "é" is represented by the single character "e with acute"
|
||||
accent, then it will split the string into just one part:
|
||||
|
||||
iex> String.split(:unicode.characters_to_nfc_binary("é"), "e")
|
||||
iex> String.split(String.normalize("é", :nfc), "e")
|
||||
["é"]
|
||||
|
||||
"""
|
||||
@@ -650,9 +650,9 @@ defmodule String do
|
||||
|
||||
String.normalize(string1, :nfd) == String.normalize(string2, :nfd)
|
||||
|
||||
Therefore, if you plan to compare multiple strings, multiple times
|
||||
in a row, you may normalize them upfront and compare them directly
|
||||
to avoid multiple normalization passes.
|
||||
If you plan to compare multiple strings, multiple times in a row, you
|
||||
may normalize them upfront and compare them directly to avoid multiple
|
||||
normalization passes.
|
||||
|
||||
## Examples
|
||||
|
||||
@@ -674,21 +674,49 @@ defmodule String do
|
||||
normalize(string1, :nfd) == normalize(string2, :nfd)
|
||||
end
|
||||
|
||||
@doc false
|
||||
@deprecated "Use :unicode.characters_to_nfc_binary/1 or :unicode.characters_to_nfd_binary/1 instead"
|
||||
@doc """
|
||||
Converts all characters in `string` to Unicode normalization
|
||||
form identified by `form`.
|
||||
|
||||
Invalid Unicode codepoints are skipped and the remaining of
|
||||
the string is converted. If you want the algorith to stop
|
||||
and return on invalid codepoint, use `:unicode.characters_to_nfd_binary/1`
|
||||
and `:unicode.characters_to_nfc_binary/1` instead.
|
||||
|
||||
## Forms
|
||||
|
||||
The supported forms are:
|
||||
|
||||
* `:nfd` - Normalization Form Canonical Decomposition.
|
||||
Characters are decomposed by canonical equivalence, and
|
||||
multiple combining characters are arranged in a specific
|
||||
order.
|
||||
|
||||
* `:nfc` - Normalization Form Canonical Composition.
|
||||
Characters are decomposed and then recomposed by canonical equivalence.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> String.normalize("yêṩ", :nfd)
|
||||
"yêṩ"
|
||||
|
||||
iex> String.normalize("leña", :nfc)
|
||||
"leña"
|
||||
|
||||
"""
|
||||
def normalize(string, form)
|
||||
|
||||
def normalize(string, :nfd) do
|
||||
case :unicode.characters_to_nfd_binary(string) do
|
||||
string when is_binary(string) -> string
|
||||
{:error, bad, rest} -> bad <> normalize(rest, :nfd)
|
||||
{:error, good, <<head, rest::binary>>} -> good <> <<head>> <> normalize(rest, :nfd)
|
||||
end
|
||||
end
|
||||
|
||||
def normalize(string, :nfc) do
|
||||
case :unicode.characters_to_nfc_binary(string) do
|
||||
string when is_binary(string) -> string
|
||||
{:error, bad, rest} -> bad <> normalize(rest, :nfc)
|
||||
{:error, good, <<head, rest::binary>>} -> good <> <<head>> <> normalize(rest, :nfc)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -2174,13 +2202,13 @@ defmodule String do
|
||||
For example, take the grapheme "é" which is made of the characters
|
||||
"e" and the acute accent. The following returns `true`:
|
||||
|
||||
iex> String.contains?(:unicode.characters_to_nfd_binary("é"), "e")
|
||||
iex> String.contains?(String.normalize("é", :nfd), "e")
|
||||
true
|
||||
|
||||
However, if "é" is represented by the single character "e with acute"
|
||||
accent, then it will return `false`:
|
||||
|
||||
iex> String.contains?(:unicode.characters_to_nfc_binary("é"), "e")
|
||||
iex> String.contains?(String.normalize("é", :nfc), "e")
|
||||
false
|
||||
|
||||
"""
|
||||
|
||||
@@ -539,7 +539,7 @@ defmodule Task.Supervised do
|
||||
|
||||
# One of the spawned processes went down. We inform the parent process of
|
||||
# this and keep going.
|
||||
{:DOWN, ref, _, _, reason} ->
|
||||
{:DOWN, ref, _, _, reason} when is_map_key(running_tasks, ref) ->
|
||||
{task, running_tasks} = Map.pop(running_tasks, ref)
|
||||
%{position: position, timer_ref: timer_ref, timed_out?: timed_out?} = task
|
||||
|
||||
|
||||
@@ -8,12 +8,11 @@ Elixir applies bug fixes only to the latest minor branch. Security patches are a
|
||||
|
||||
Elixir version | Support
|
||||
:------------- | :-----------------------------
|
||||
1.10 | Development
|
||||
1.9 | Bug fixes and security patches
|
||||
1.10 | Bug fixes and security patches
|
||||
1.9 | Security patches only
|
||||
1.8 | Security patches only
|
||||
1.7 | Security patches only
|
||||
1.6 | Security patches only
|
||||
1.5 | Security patches only
|
||||
|
||||
New releases are announced in the read-only [announcements mailing list](https://groups.google.com/group/elixir-lang-ann). All security releases [will be tagged with `[security]`](https://groups.google.com/forum/#!searchin/elixir-lang-ann/%5Bsecurity%5D%7Csort:date).
|
||||
|
||||
@@ -82,7 +81,6 @@ Version | Deprecated feature | Replaced by (ava
|
||||
[v1.10] | Passing non-chardata to `Logger.log/2` | Explicitly convert to string with `to_string/1` (v1.0)
|
||||
[v1.10] | `:compile_time_purge_level` in `Logger` app environment | `:compile_time_purge_matching` in `Logger` app environment (v1.7)
|
||||
[v1.10] | `Supervisor.Spec.supervise/2` | The new child specs outlined in `Supervisor` (v1.5)
|
||||
[v1.10] | `String.normalize/2` | `:unicode.characters_to_nfc_binary/1` or `:unicode.characters_to_nfd_binary/1` (Erlang/OTP 20)
|
||||
[v1.10] | `:simple_one_for_one` strategy in `Supervisor` | `DynamicSupervisor` (v1.6)
|
||||
[v1.10] | `:restart` and `:shutdown` in `Task.Supervisor.start_link/1` | `:restart` and `:shutdown` in `Task.Supervisor.start_child/3` (v1.6)
|
||||
[v1.9] | Enumerable keys in `Map.drop/2`, `Map.split/2`, and `Map.take/2` | Call `Enum.to_list/1` on the second argument before hand (v1.0)
|
||||
@@ -171,4 +169,4 @@ Version | Deprecated feature | Replaced by (ava
|
||||
[v1.7]: https://github.com/elixir-lang/elixir/blob/v1.7/CHANGELOG.md#4-hard-deprecations
|
||||
[v1.8]: https://github.com/elixir-lang/elixir/blob/v1.8/CHANGELOG.md#4-hard-deprecations
|
||||
[v1.9]: https://github.com/elixir-lang/elixir/blob/v1.9/CHANGELOG.md#4-hard-deprecations
|
||||
[v1.10]: https://github.com/elixir-lang/elixir/blob/master/CHANGELOG.md#4-hard-deprecations
|
||||
[v1.10]: https://github.com/elixir-lang/elixir/blob/v1.10/CHANGELOG.md#4-hard-deprecations
|
||||
|
||||
@@ -144,6 +144,34 @@ The application environment should be reserved only for configurations that are
|
||||
|
||||
For all remaining scenarios, libraries should not force their users to use the application environment for configuration. If the user of a library believes that certain parameter should be configured globally, then they can wrap the library functionality with their own application environment configuration.
|
||||
|
||||
### Avoid compile-time application configuration
|
||||
|
||||
Assuming you need to use the application configuration and you cannot avoid it as explained in the previous section, you should also avoid compile-time application configuration. For example, instead of doing this:
|
||||
|
||||
```elixir
|
||||
@http_client Application.fetch_env!(:my_app, :http_client)
|
||||
|
||||
def request(path) do
|
||||
@http_client.request(path)
|
||||
end
|
||||
```
|
||||
|
||||
you should do this:
|
||||
|
||||
```elixir
|
||||
def request(path) do
|
||||
http_client().request(path)
|
||||
end
|
||||
|
||||
defp http_client() do
|
||||
Application.fetch_env!(:my_app, :http_client)
|
||||
end
|
||||
```
|
||||
|
||||
That's because by reading the application in the module body and storing it in a module attribute, we are effectively reading the configuration at compile-time, which may become an issue when configuring the system later.
|
||||
|
||||
If, for some reason, you must read the application environment at compile time, use `Application.compile_env/2`. Read [the "Compile-time environment" section of the Application docs](Application.html#module-compile-time-environment) for more information.
|
||||
|
||||
### Avoid `use` when an `import` is enough
|
||||
|
||||
A library should not provide `use MyLib` functionality if all `use MyLib` does is to `import`/`alias` the module itself. For example, this is an anti-pattern:
|
||||
|
||||
@@ -16,17 +16,6 @@
|
||||
stacktrace=nil %% holds information about the stacktrace variable
|
||||
}).
|
||||
|
||||
-record(elixir_quote, {
|
||||
line=false,
|
||||
file=nil,
|
||||
context=nil,
|
||||
vars_hygiene=true,
|
||||
aliases_hygiene=true,
|
||||
imports_hygiene=true,
|
||||
unquote=true,
|
||||
generated=false
|
||||
}).
|
||||
|
||||
-record(elixir_tokenizer, {
|
||||
file=(<<"nofile">>),
|
||||
terminators=[],
|
||||
|
||||
@@ -132,17 +132,16 @@ compute_alignment(_, _, _) -> unknown.
|
||||
|
||||
%% Expands the expression of a bitstring, that is, the LHS of :: or
|
||||
%% an argument of the bitstring (such as "foo" in "<<foo>>").
|
||||
%% If we are inside a match/guard, we inline interpolations explicitly,
|
||||
%% otherwise they are inlined by elixir_rewrite.erl.
|
||||
|
||||
expand_expr(Meta, {{'.', M1, [Mod, to_string]}, M2, [Arg]}, Fun, E)
|
||||
when Mod == 'Elixir.Kernel'; Mod == 'Elixir.String.Chars' ->
|
||||
expand_expr(_Meta, {{'.', _, [Mod, to_string]}, _, [Arg]} = AST, Fun, {#{context := Context}, _} = E)
|
||||
when Context /= nil, (Mod == 'Elixir.Kernel') orelse (Mod == 'Elixir.String.Chars') ->
|
||||
case Fun(Arg, E) of
|
||||
{EBin, EE} when is_binary(EBin) -> {EBin, EE};
|
||||
_ -> do_expand_expr(Meta, {{'.', M1, ['Elixir.String.Chars', to_string]}, M2, [Arg]}, Fun, E)
|
||||
_ -> Fun(AST, E) % Let it raise
|
||||
end;
|
||||
expand_expr(Meta, Component, Fun, E) ->
|
||||
do_expand_expr(Meta, Component, Fun, E).
|
||||
|
||||
do_expand_expr(Meta, Component, Fun, E) ->
|
||||
case Fun(Component, E) of
|
||||
{EComponent, {ErrorE, _}} when is_list(EComponent); is_atom(EComponent) ->
|
||||
form_error(Meta, ErrorE, ?MODULE, {invalid_literal, EComponent});
|
||||
|
||||
@@ -10,11 +10,11 @@
|
||||
'MACRO-@'(Caller, Tree) ->
|
||||
unless_loaded('MACRO-@', [Caller, Tree], fun() -> nil end).
|
||||
|
||||
'MACRO-def'(Caller, Call) -> 'MACRO-def'(Caller, Call, []).
|
||||
'MACRO-def'(Caller, Call) -> 'MACRO-def'(Caller, Call, nil).
|
||||
'MACRO-def'(Caller, Call, Expr) -> define(Caller, def, Call, Expr).
|
||||
'MACRO-defp'(Caller, Call, Expr) -> define(Caller, defp, Call, Expr).
|
||||
|
||||
'MACRO-defmacro'(Caller, Call) -> 'MACRO-defmacro'(Caller, Call, []).
|
||||
'MACRO-defmacro'(Caller, Call) -> 'MACRO-defmacro'(Caller, Call, nil).
|
||||
'MACRO-defmacro'(Caller, Call, Expr) -> define(Caller, defmacro, Call, Expr).
|
||||
'MACRO-defmacrop'(Caller, Call, Expr) -> define(Caller, defmacrop, Call, Expr).
|
||||
|
||||
|
||||
@@ -212,7 +212,7 @@ run_with_location_change(File, #{file := File} = E, Callback) ->
|
||||
run_with_location_change(File, E, Callback) ->
|
||||
elixir_lexical:with_file(File, E, Callback).
|
||||
|
||||
def_to_clauses(_Kind, Meta, Args, [], [], E) ->
|
||||
def_to_clauses(_Kind, Meta, Args, [], nil, E) ->
|
||||
check_args_for_function_head(Meta, Args, E),
|
||||
[];
|
||||
def_to_clauses(_Kind, Meta, Args, Guards, [{do, Body}], _E) ->
|
||||
|
||||
@@ -97,6 +97,20 @@ dispatch_import(Meta, Name, Args, E, Callback) ->
|
||||
Callback()
|
||||
end.
|
||||
|
||||
dispatch_require(Meta, 'Elixir.System', stacktrace, [], #{contextual_vars := Vars} = E, Callback) ->
|
||||
case lists:member('__STACKTRACE__', Vars) of
|
||||
true ->
|
||||
{{'__STACKTRACE__', [], nil}, E};
|
||||
false ->
|
||||
Message =
|
||||
"System.stacktrace/0 outside of rescue/catch clauses is deprecated. "
|
||||
"If you want to support only Elixir v1.7+, you must access __STACKTRACE__ "
|
||||
"inside a rescue/catch. If you want to support earlier Elixir versions, "
|
||||
"move System.stacktrace/0 inside a rescue/catch",
|
||||
elixir_errors:erl_warn(?line(Meta), ?key(E, file), Message),
|
||||
Callback('Elixir.System', stacktrace, [])
|
||||
end;
|
||||
|
||||
dispatch_require(Meta, Receiver, Name, Args, E, Callback) when is_atom(Receiver) ->
|
||||
Arity = length(Args),
|
||||
|
||||
@@ -353,18 +367,6 @@ check_deprecated(_, erlang, _, _, _) ->
|
||||
ok;
|
||||
check_deprecated(_, _, _, _, #{module := 'Elixir.HashDict'}) ->
|
||||
ok;
|
||||
check_deprecated(Meta, 'Elixir.System', stacktrace, 0, #{contextual_vars := Vars} = E) ->
|
||||
case lists:member('__STACKTRACE__', Vars) of
|
||||
true ->
|
||||
ok;
|
||||
false ->
|
||||
Message =
|
||||
"System.stacktrace/0 outside of rescue/catch clauses is deprecated. "
|
||||
"If you want to support only Elixir v1.7+, you must access __STACKTRACE__ "
|
||||
"inside a rescue/catch. If you want to support earlier Elixir versions, "
|
||||
"move System.stacktrace/0 inside a rescue/catch",
|
||||
elixir_errors:erl_warn(?line(Meta), ?key(E, file), Message)
|
||||
end;
|
||||
check_deprecated(Meta, Receiver, Name, Arity, E) ->
|
||||
case (?key(E, function) == nil) andalso is_ensure_loaded(Receiver) of
|
||||
true ->
|
||||
|
||||
@@ -54,13 +54,8 @@ definition_to_anonymous(Module, Kind, Meta, Clauses) ->
|
||||
{value, Result, _Binding} = erl_eval:expr(Fun, [], {value, LocalHandler}),
|
||||
Result.
|
||||
|
||||
invoke_local(Module, RawName, Args) ->
|
||||
%% If we have a macro, its arity in the table is
|
||||
%% actually one less than in the function call
|
||||
{Name, Arity} = case atom_to_list(RawName) of
|
||||
"MACRO-" ++ Rest -> {list_to_atom(Rest), length(Args) - 1};
|
||||
_ -> {RawName, length(Args)}
|
||||
end,
|
||||
invoke_local(Module, ErlName, Args) ->
|
||||
{Name, Arity} = elixir_utils:erl_fa_to_elixir_fa(ErlName, length(Args)),
|
||||
|
||||
case elixir_def:local_for(Module, Name, Arity, all) of
|
||||
false ->
|
||||
@@ -549,7 +544,7 @@ signature_to_binary(Module, '__struct__', []) ->
|
||||
signature_to_binary(_, Name, Signature) ->
|
||||
'Elixir.Macro':to_string({Name, [], Signature}).
|
||||
|
||||
checker_chunk(#{definitions := Definitions, deprecated := Deprecated}, NoWarnUndefined) ->
|
||||
checker_chunk(#{definitions := Definitions, deprecated := Deprecated, is_behaviour := IsBehaviour}, NoWarnUndefined) ->
|
||||
DeprecatedMap = maps:from_list(Deprecated),
|
||||
|
||||
Exports =
|
||||
@@ -564,12 +559,15 @@ checker_chunk(#{definitions := Definitions, deprecated := Deprecated}, NoWarnUnd
|
||||
end, [], Definitions),
|
||||
|
||||
Contents = #{
|
||||
exports => lists:sort(Exports),
|
||||
exports => lists:sort(behaviour_info_exports(IsBehaviour) ++ Exports),
|
||||
no_warn_undefined => NoWarnUndefined
|
||||
},
|
||||
|
||||
[{<<"ExCk">>, erlang:term_to_binary({elixir_checker_v1, Contents})}].
|
||||
|
||||
behaviour_info_exports(true) -> [{{behaviour_info, 1}, #{kind => def, deprecated_reason => nil}}];
|
||||
behaviour_info_exports(false) -> [].
|
||||
|
||||
split_no_warn_undefined([{no_warn_undefined, NoWarnUndefined} | CompileOpts], AccNWU, AccCO) ->
|
||||
split_no_warn_undefined(CompileOpts, list_wrap(NoWarnUndefined) ++ AccNWU, AccCO);
|
||||
split_no_warn_undefined([Opt | CompileOpts], AccNWU, AccCO) ->
|
||||
|
||||
@@ -48,8 +48,10 @@ compile(Forms, File, Opts) when is_list(Forms), is_list(Opts), is_binary(File) -
|
||||
case erl_to_core(Forms, Opts) of
|
||||
{ok, CoreForms, CoreWarnings} ->
|
||||
format_warnings(Opts, CoreWarnings),
|
||||
CompileOpts = [?NO_SPAWN_COMPILER_PROCESS, from_core, no_core_prepare,
|
||||
no_auto_import, return, {source, Source} | Opts],
|
||||
|
||||
case compile:noenv_forms(CoreForms, [?NO_SPAWN_COMPILER_PROCESS, from_core, no_auto_import, return, {source, Source} | Opts]) of
|
||||
case compile:noenv_forms(CoreForms, CompileOpts) of
|
||||
{ok, Module, Binary, Warnings} when is_binary(Binary) ->
|
||||
format_warnings(Opts, Warnings),
|
||||
{Module, Binary};
|
||||
@@ -134,6 +136,14 @@ custom_format(sys_core_fold, {eval_failure, Error}) ->
|
||||
#{'__struct__' := Struct} = 'Elixir.Exception':normalize(error, Error),
|
||||
["this expression will fail with ", elixir_aliases:inspect(Struct)];
|
||||
|
||||
custom_format(sys_core_fold, {nomatch_shadow,Line,{ErlName,ErlArity}}) ->
|
||||
{Name, Arity} = elixir_utils:erl_fa_to_elixir_fa(ErlName, ErlArity),
|
||||
|
||||
io_lib:format(
|
||||
"this clause for ~ts/~B cannot match because a previous clause at line ~B always matches",
|
||||
[Name, Arity, Line]
|
||||
);
|
||||
|
||||
custom_format([], Desc) ->
|
||||
io_lib:format("~p", [Desc]);
|
||||
|
||||
|
||||
@@ -126,7 +126,7 @@ translate({'cond', CondMeta, [[{do, Clauses}]]}, S) ->
|
||||
true -> Meta
|
||||
end,
|
||||
|
||||
Error = {{'.', Meta, [erlang, error]}, [], [cond_clause]},
|
||||
Error = {{'.', Meta, [erlang, error]}, Meta, [cond_clause]},
|
||||
{Case, SC} = build_cond_clauses([H | T], Error, FirstMeta, S),
|
||||
translate(replace_case_meta(CondMeta, Case), SC);
|
||||
|
||||
@@ -409,8 +409,8 @@ translate_with_do([{'<-', Meta, [Left, Expr]} | Rest], Do, Else, S) ->
|
||||
{Args, Guards} = elixir_utils:extract_guards(Left),
|
||||
{TExpr, SR} = elixir_erl_pass:translate(Expr, S),
|
||||
{TArgs, SA} = elixir_erl_clauses:match(fun elixir_erl_pass:translate/2, Args, SR),
|
||||
TGuards = elixir_erl_clauses:guards(Guards, [], SA),
|
||||
{TBody, SB} = translate_with_do(Rest, Do, Else, SA),
|
||||
TGuards = elixir_erl_clauses:guards(Guards, SA#elixir_erl.extra_guards, SA),
|
||||
{TBody, SB} = translate_with_do(Rest, Do, Else, SA#elixir_erl{extra_guards=[]}),
|
||||
|
||||
Clause = {clause, ?ann(Meta), [TArgs], TGuards, unblock(TBody)},
|
||||
{{'case', ?ann(?generated(Meta)), TExpr, [Clause, Else]}, SB};
|
||||
@@ -528,29 +528,20 @@ extract_bit_type({Other, _, []}, Acc) ->
|
||||
%% Optimizations that are specific to Erlang and change
|
||||
%% the format of the AST.
|
||||
|
||||
translate_remote('Elixir.Access' = Mod, get, Meta, [Container, Value], S) ->
|
||||
Ann = ?ann(Meta),
|
||||
{TArgs, SA} = translate_args([Container, Value, nil], S),
|
||||
{?remote(Ann, Mod, get, TArgs), SA};
|
||||
translate_remote('Elixir.String.Chars', to_string, Meta, [Arg], S) ->
|
||||
case is_always_string(Arg) of
|
||||
true ->
|
||||
translate(Arg, S);
|
||||
false ->
|
||||
{TArg, TS} = translate(Arg, S),
|
||||
{VarName, VS} = elixir_erl_var:build('_', TS),
|
||||
{TArg, TS} = translate(Arg, S),
|
||||
{VarName, VS} = elixir_erl_var:build('_', TS),
|
||||
|
||||
Generated = ?ann(?generated(Meta)),
|
||||
Var = {var, Generated, VarName},
|
||||
Guard = ?remote(Generated, erlang, is_binary, [Var]),
|
||||
Slow = ?remote(Generated, 'Elixir.String.Chars', to_string, [Var]),
|
||||
Fast = Var,
|
||||
Generated = ?ann(?generated(Meta)),
|
||||
Var = {var, Generated, VarName},
|
||||
Guard = ?remote(Generated, erlang, is_binary, [Var]),
|
||||
Slow = ?remote(Generated, 'Elixir.String.Chars', to_string, [Var]),
|
||||
Fast = Var,
|
||||
|
||||
{{'case', Generated, TArg, [
|
||||
{clause, Generated, [Var], [[Guard]], [Fast]},
|
||||
{clause, Generated, [Var], [], [Slow]}
|
||||
]}, VS}
|
||||
end;
|
||||
{{'case', Generated, TArg, [
|
||||
{clause, Generated, [Var], [[Guard]], [Fast]},
|
||||
{clause, Generated, [Var], [], [Slow]}
|
||||
]}, VS};
|
||||
translate_remote(maps, put, Meta, [Key, Value, Map], S) ->
|
||||
Ann = ?ann(Meta),
|
||||
|
||||
@@ -600,20 +591,6 @@ translate_remote(Left, Right, Meta, Args, S) ->
|
||||
{{call, Ann, {remote, Ann, TLeft, TRight}, TArgs}, SA}
|
||||
end.
|
||||
|
||||
is_always_string({{'.', _, [Module, Function]}, _, Args}) ->
|
||||
is_always_string(Module, Function, length(Args));
|
||||
%% Binary literals were already excluded in earlier passes.
|
||||
is_always_string(_Ast) ->
|
||||
false.
|
||||
|
||||
is_always_string('Elixir.Enum', join, _) -> true;
|
||||
is_always_string('Elixir.Enum', map_join, _) -> true;
|
||||
is_always_string('Elixir.Kernel', inspect, _) -> true;
|
||||
is_always_string('Elixir.Macro', to_string, _) -> true;
|
||||
is_always_string('Elixir.String.Chars', to_string, _) -> true;
|
||||
is_always_string('Elixir.Path', join, _) -> true;
|
||||
is_always_string(_Module, _Function, _Args) -> false.
|
||||
|
||||
generate_struct_name_guard([{map_field_exact, Ann, {atom, _, '__struct__'} = Key, Var} | Rest], Acc, S0) ->
|
||||
{ModuleVarName, S1} = elixir_erl_var:build('_', S0),
|
||||
Generated = erl_anno:set_generated(true, Ann),
|
||||
|
||||
@@ -156,31 +156,16 @@ expand({quote, Meta, [Opts, Do]}, E) when is_list(Do) ->
|
||||
ValidOpts = [context, location, line, file, unquote, bind_quoted, generated],
|
||||
{EOpts, ET} = expand_opts(Meta, quote, ValidOpts, Opts, E),
|
||||
|
||||
Context = case lists:keyfind(context, 1, EOpts) of
|
||||
{context, Ctx} when is_atom(Ctx) and (Ctx /= nil) ->
|
||||
Ctx;
|
||||
{context, Ctx} ->
|
||||
form_error(Meta, E, ?MODULE, {invalid_context_for_quote, Ctx});
|
||||
false ->
|
||||
case ?key(E, module) of
|
||||
nil -> 'Elixir';
|
||||
Mod -> Mod
|
||||
end
|
||||
end,
|
||||
Context = proplists:get_value(context, EOpts, case ?key(E, module) of
|
||||
nil -> 'Elixir';
|
||||
Mod -> Mod
|
||||
end),
|
||||
|
||||
{File, Line} = case lists:keyfind(location, 1, EOpts) of
|
||||
{location, keep} ->
|
||||
{elixir_utils:relative_to_cwd(?key(E, file)), false};
|
||||
false ->
|
||||
{ case lists:keyfind(file, 1, EOpts) of
|
||||
{file, F} -> F;
|
||||
false -> nil
|
||||
end,
|
||||
|
||||
case lists:keyfind(line, 1, EOpts) of
|
||||
{line, L} -> L;
|
||||
false -> false
|
||||
end }
|
||||
{proplists:get_value(file, EOpts, nil), proplists:get_value(line, EOpts, false)}
|
||||
end,
|
||||
|
||||
{Binding, DefaultUnquote} = case lists:keyfind(bind_quoted, 1, EOpts) of
|
||||
@@ -191,20 +176,14 @@ expand({quote, Meta, [Opts, Do]}, E) when is_list(Do) ->
|
||||
false -> form_error(Meta, E, ?MODULE, {invalid_bind_quoted_for_quote, BQ})
|
||||
end;
|
||||
false ->
|
||||
{nil, true}
|
||||
{[], true}
|
||||
end,
|
||||
|
||||
Unquote = case lists:keyfind(unquote, 1, EOpts) of
|
||||
{unquote, U} when is_boolean(U) -> U;
|
||||
false -> DefaultUnquote
|
||||
end,
|
||||
Unquote = proplists:get_value(unquote, EOpts, DefaultUnquote),
|
||||
Generated = proplists:get_value(generated, EOpts, false),
|
||||
|
||||
Generated = lists:keyfind(generated, 1, EOpts) == {generated, true},
|
||||
|
||||
Q = #elixir_quote{line=Line, file=File, unquote=Unquote,
|
||||
context=Context, generated=Generated},
|
||||
|
||||
Quoted = elixir_quote:quote(Meta, Exprs, Binding, Q, ET),
|
||||
{Q, Prelude} = elixir_quote:build(Meta, Line, File, Context, Unquote, Generated),
|
||||
Quoted = elixir_quote:quote(Meta, Exprs, Binding, Q, Prelude, ET),
|
||||
expand(Quoted, ET);
|
||||
|
||||
expand({quote, Meta, [_, _]}, E) ->
|
||||
@@ -1133,9 +1112,6 @@ 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({invalid_context_for_quote, Context}) ->
|
||||
io_lib:format("invalid :context for quote, expected non-nil compile time atom or alias, got: ~ts",
|
||||
['Elixir.Macro':to_string(Context)]);
|
||||
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)]);
|
||||
|
||||
@@ -10,6 +10,7 @@ run(#{tracers := Tracers} = E, ExecutionCallback, AfterExecutionCallback) ->
|
||||
false ->
|
||||
{ok, Pid} = ?tracker:start_link(),
|
||||
LexEnv = E#{lexical_tracker := Pid, tracers := [?MODULE | Tracers]},
|
||||
elixir_env:trace(start, LexEnv),
|
||||
|
||||
try ExecutionCallback(LexEnv) of
|
||||
Res ->
|
||||
@@ -18,6 +19,7 @@ run(#{tracers := Tracers} = E, ExecutionCallback, AfterExecutionCallback) ->
|
||||
AfterExecutionCallback(LexEnv),
|
||||
Res
|
||||
after
|
||||
elixir_env:trace(stop, LexEnv),
|
||||
unlink(Pid),
|
||||
?tracker:stop(Pid)
|
||||
end;
|
||||
|
||||
@@ -87,7 +87,13 @@ validate_match_key(_, _, _) ->
|
||||
validate_not_repeated(Meta, Key, Used, E) ->
|
||||
case is_literal(Key) andalso Used of
|
||||
#{Key := true} ->
|
||||
form_error(Meta, ?key(E, file), ?MODULE, {repeated_key, Key});
|
||||
case E of
|
||||
#{context := match} ->
|
||||
form_error(Meta, ?key(E, file), ?MODULE, {repeated_key, Key});
|
||||
_ ->
|
||||
form_warn(Meta, ?key(E, file), ?MODULE, {repeated_key, Key}),
|
||||
Used
|
||||
end;
|
||||
|
||||
#{} ->
|
||||
Used#{Key => true};
|
||||
|
||||
@@ -133,7 +133,8 @@ compile(Line, Module, Block, Vars, E) ->
|
||||
definitions => AllDefinitions,
|
||||
unreachable => Unreachable,
|
||||
compile_opts => CompileOpts,
|
||||
deprecated => get_deprecated(DataBag)
|
||||
deprecated => get_deprecated(DataBag),
|
||||
is_behaviour => is_behaviour(DataBag)
|
||||
},
|
||||
|
||||
Binary = elixir_erl:compile(ModuleMap),
|
||||
@@ -199,6 +200,9 @@ validate_on_load_attribute({on_load, Def}, Defs, File, Line) ->
|
||||
end;
|
||||
validate_on_load_attribute(false, _Defs, _File, _Line) -> ok.
|
||||
|
||||
is_behaviour(DataBag) ->
|
||||
ets:member(DataBag, {accumulate, callback}) orelse ets:member(DataBag, {accumulate, macrocallback}).
|
||||
|
||||
%% An undef error for a function in the module being compiled might result in an
|
||||
%% exception message suggesting the current module is not loaded. This is
|
||||
%% misleading so use a custom reason.
|
||||
|
||||
@@ -259,9 +259,9 @@ access_expr -> bin_heredoc : build_bin_heredoc('$1').
|
||||
access_expr -> list_heredoc : build_list_heredoc('$1').
|
||||
access_expr -> bit_string : '$1'.
|
||||
access_expr -> sigil : build_sigil('$1').
|
||||
access_expr -> atom : handle_literal(?exprs('$1'), '$1', delimiter(<<$:>>)).
|
||||
access_expr -> atom_safe : build_quoted_atom('$1', true, delimiter(<<$:>>)).
|
||||
access_expr -> atom_unsafe : build_quoted_atom('$1', false, delimiter(<<$:>>)).
|
||||
access_expr -> atom : handle_literal(?exprs('$1'), '$1').
|
||||
access_expr -> atom_safe : build_quoted_atom('$1', true, []).
|
||||
access_expr -> atom_unsafe : build_quoted_atom('$1', false, []).
|
||||
access_expr -> dot_alias : '$1'.
|
||||
access_expr -> parens_call : '$1'.
|
||||
|
||||
@@ -526,12 +526,12 @@ call_args_parens -> open_paren call_args_parens_base ',' kw_base ',' close_paren
|
||||
|
||||
% KV
|
||||
|
||||
kw_eol -> kw_identifier : handle_literal(?exprs('$1'), '$1').
|
||||
kw_eol -> kw_identifier eol : handle_literal(?exprs('$1'), '$1').
|
||||
kw_eol -> kw_identifier_safe : build_quoted_atom('$1', true, []).
|
||||
kw_eol -> kw_identifier_safe eol : build_quoted_atom('$1', true, []).
|
||||
kw_eol -> kw_identifier_unsafe : build_quoted_atom('$1', false, []).
|
||||
kw_eol -> kw_identifier_unsafe eol : build_quoted_atom('$1', false, []).
|
||||
kw_eol -> kw_identifier : handle_literal(?exprs('$1'), '$1', [{format, keyword}]).
|
||||
kw_eol -> kw_identifier eol : handle_literal(?exprs('$1'), '$1', [{format, keyword}]).
|
||||
kw_eol -> kw_identifier_safe : build_quoted_atom('$1', true, [{format, keyword}]).
|
||||
kw_eol -> kw_identifier_safe eol : build_quoted_atom('$1', true, [{format, keyword}]).
|
||||
kw_eol -> kw_identifier_unsafe : build_quoted_atom('$1', false, [{format, keyword}]).
|
||||
kw_eol -> kw_identifier_unsafe eol : build_quoted_atom('$1', false, [{format, keyword}]).
|
||||
|
||||
kw_base -> kw_eol container_expr : [{'$1', '$2'}].
|
||||
kw_base -> kw_base ',' kw_eol container_expr : [{'$3', '$4'} | '$1'].
|
||||
|
||||
@@ -1,12 +1,73 @@
|
||||
-module(elixir_quote).
|
||||
-export([escape/3, linify/3, linify_with_context_counter/3, quote/5, has_unquotes/1]).
|
||||
-export([dot/5, tail_list/3, list/2]). %% Quote callbacks
|
||||
-export([escape/3, linify/3, linify_with_context_counter/3, build/6, quote/6, has_unquotes/1]).
|
||||
-export([dot/5, tail_list/3, list/2, validate_runtime/2]). %% Quote callbacks
|
||||
|
||||
-include("elixir.hrl").
|
||||
-define(defs(Kind), Kind == def; Kind == defp; Kind == defmacro; Kind == defmacrop; Kind == '@').
|
||||
-define(lexical(Kind), Kind == import; Kind == alias; Kind == require).
|
||||
-compile({inline, [keyfind/2, keystore/3, keydelete/2, keynew/3, do_tuple_linify/5]}).
|
||||
|
||||
-record(elixir_quote, {
|
||||
line=false,
|
||||
file=nil,
|
||||
context=nil,
|
||||
vars_hygiene=true,
|
||||
aliases_hygiene=true,
|
||||
imports_hygiene=true,
|
||||
unquote=true,
|
||||
generated=false
|
||||
}).
|
||||
|
||||
build(Meta, Line, File, Context, Unquote, Generated) ->
|
||||
Acc0 = [],
|
||||
{ELine, Acc1} = validate_compile(Meta, line, Line, Acc0),
|
||||
{EFile, Acc2} = validate_compile(Meta, file, File, Acc1),
|
||||
{EContext, Acc3} = validate_compile(Meta, context, Context, Acc2),
|
||||
validate_runtime(unquote, Unquote),
|
||||
validate_runtime(generated, Generated),
|
||||
|
||||
Q = #elixir_quote{
|
||||
line=ELine,
|
||||
file=EFile,
|
||||
unquote=Unquote,
|
||||
context=EContext,
|
||||
generated=Generated
|
||||
},
|
||||
|
||||
{Q, Acc3}.
|
||||
|
||||
validate_compile(_Meta, file, nil, Acc) ->
|
||||
{nil, Acc};
|
||||
validate_compile(Meta, Key, Value, Acc) ->
|
||||
case is_valid(Key, Value) of
|
||||
true ->
|
||||
{Value, Acc};
|
||||
false ->
|
||||
Var = {Key, Meta, ?MODULE},
|
||||
Call = {{'.', Meta, [?MODULE, validate_runtime]}, Meta, [Key, Value]},
|
||||
{Var, [{'=', Meta, [Var, Call]} | Acc]}
|
||||
end.
|
||||
|
||||
validate_runtime(Key, Value) ->
|
||||
case is_valid(Key, Value) of
|
||||
true ->
|
||||
Value;
|
||||
|
||||
false ->
|
||||
erlang:error(
|
||||
'Elixir.ArgumentError':exception(
|
||||
<<"invalid value for option :", (erlang:atom_to_binary(Key, utf8))/binary,
|
||||
" in quote, got: ", ('Elixir.Kernel':inspect(Value))/binary>>
|
||||
)
|
||||
)
|
||||
end.
|
||||
|
||||
is_valid(line, _Line) -> true;
|
||||
is_valid(file, File) -> is_binary(File);
|
||||
is_valid(context, Context) -> is_atom(Context) andalso (Context /= nil);
|
||||
is_valid(generated, Generated) -> is_boolean(Generated);
|
||||
is_valid(unquote, Unquote) -> is_boolean(Unquote).
|
||||
|
||||
%% Apply the line from site call on quoted contents.
|
||||
%% Receives a Key to look for the default line as argument.
|
||||
linify(0, _Key, Exprs) ->
|
||||
@@ -164,26 +225,31 @@ escape(Expr, Kind, Unquote) ->
|
||||
|
||||
%% Quotes an expression and return its quoted Elixir AST.
|
||||
|
||||
quote(_Meta, {unquote_splicing, _, [_]}, _Binding, #elixir_quote{unquote=true}, _) ->
|
||||
quote(_Meta, {unquote_splicing, _, [_]}, _Binding, #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">>);
|
||||
|
||||
quote(_Meta, Expr, nil, Q, E) ->
|
||||
do_quote(Expr, Q, E);
|
||||
|
||||
quote(Meta, Expr, Binding, Q, E) ->
|
||||
quote(Meta, Expr, Binding, Q, Prelude, E) ->
|
||||
Context = Q#elixir_quote.context,
|
||||
VarMeta = [Pair || {K, _} = Pair <- Meta, K == counter],
|
||||
|
||||
Vars = [ {'{}', [],
|
||||
[ '=', [], [
|
||||
{'{}', [], [K, VarMeta, Context]},
|
||||
Vars = [{'{}', [],
|
||||
['=', [], [
|
||||
{'{}', [], [K, Meta, Context]},
|
||||
V
|
||||
] ]
|
||||
]]
|
||||
} || {K, V} <- Binding],
|
||||
|
||||
TExprs = do_quote(Expr, Q, E),
|
||||
{'{}', [], ['__block__', [], Vars ++ [TExprs]]}.
|
||||
Quoted = do_quote(Expr, Q, E),
|
||||
|
||||
WithVars = case Vars of
|
||||
[] -> Quoted;
|
||||
_ -> {'{}', [], ['__block__', [], Vars ++ [Quoted]]}
|
||||
end,
|
||||
|
||||
case Prelude of
|
||||
[] -> WithVars;
|
||||
_ -> {'__block__', [], Prelude ++ [WithVars]}
|
||||
end.
|
||||
|
||||
%% Actual quoting and helpers
|
||||
|
||||
@@ -224,11 +290,12 @@ do_quote({'__aliases__', Meta, [H | T]} = Alias, #elixir_quote{aliases_hygiene=t
|
||||
|
||||
%% Vars
|
||||
|
||||
do_quote({Left, Meta, nil}, #elixir_quote{vars_hygiene=true, imports_hygiene=true} = Q, E) when is_atom(Left) ->
|
||||
do_quote_import(Left, Meta, Q#elixir_quote.context, Q, E);
|
||||
do_quote({Name, Meta, nil}, #elixir_quote{vars_hygiene=true, imports_hygiene=true} = Q, E) when is_atom(Name) ->
|
||||
ImportMeta = import_meta(Meta, Name, 0, Q, E),
|
||||
{'{}', [], [Name, meta(ImportMeta, Q), Q#elixir_quote.context]};
|
||||
|
||||
do_quote({Left, Meta, nil}, #elixir_quote{vars_hygiene=true} = Q, E) when is_atom(Left) ->
|
||||
do_quote_tuple(Left, Meta, Q#elixir_quote.context, Q, E);
|
||||
do_quote({Name, Meta, nil}, #elixir_quote{vars_hygiene=true} = Q, _E) when is_atom(Name) ->
|
||||
{'{}', [], [Name, meta(Meta, Q), Q#elixir_quote.context]};
|
||||
|
||||
%% Unquote
|
||||
|
||||
@@ -244,8 +311,11 @@ do_quote({'&', Meta, [{'/', _, [{F, _, C}, A]}] = Args},
|
||||
#elixir_quote{imports_hygiene=true} = Q, E) when is_atom(F), is_integer(A), is_atom(C) ->
|
||||
do_quote_fa('&', Meta, Args, F, A, Q, E);
|
||||
|
||||
do_quote({Name, Meta, ArgsOrAtom}, #elixir_quote{imports_hygiene=true} = Q, E) when is_atom(Name) ->
|
||||
do_quote_import(Name, Meta, ArgsOrAtom, Q, E);
|
||||
do_quote({Name, Meta, Args}, #elixir_quote{imports_hygiene=true} = Q, E) when is_atom(Name), is_list(Args) ->
|
||||
do_quote_import(Meta, Name, length(Args), Args, Q, E);
|
||||
|
||||
do_quote({Name, Meta, Context}, #elixir_quote{imports_hygiene=true} = Q, E) when is_atom(Name), is_atom(Context) ->
|
||||
do_quote_import(Meta, Name, 0, Context, Q, E);
|
||||
|
||||
%% Two-element tuples
|
||||
|
||||
@@ -341,15 +411,8 @@ bad_escape(Arg) ->
|
||||
"The supported values are: lists, tuples, maps, atoms, numbers, bitstrings, ",
|
||||
"PIDs and remote functions in the format &Mod.fun/arity">>).
|
||||
|
||||
%% do_quote_*
|
||||
|
||||
do_quote_import(Name, Meta, ArgsOrAtom, #elixir_quote{imports_hygiene=true} = Q, E) ->
|
||||
Arity = case is_atom(ArgsOrAtom) of
|
||||
true -> 0;
|
||||
false -> length(ArgsOrAtom)
|
||||
end,
|
||||
|
||||
NewMeta = case (keyfind(import, Meta) == false) andalso
|
||||
import_meta(Meta, Name, Arity, Q, E) ->
|
||||
case (keyfind(import, Meta) == false) andalso
|
||||
elixir_dispatch:find_import(Meta, Name, Arity, E) of
|
||||
false ->
|
||||
case (Arity == 1) andalso keyfind(ambiguous_op, Meta) of
|
||||
@@ -358,9 +421,13 @@ do_quote_import(Name, Meta, ArgsOrAtom, #elixir_quote{imports_hygiene=true} = Q,
|
||||
end;
|
||||
Receiver ->
|
||||
keystore(import, keystore(context, Meta, Q#elixir_quote.context), Receiver)
|
||||
end,
|
||||
end.
|
||||
|
||||
Annotated = annotate({Name, NewMeta, ArgsOrAtom}, Q#elixir_quote.context),
|
||||
%% do_quote_*
|
||||
|
||||
do_quote_import(Meta, Name, Arity, ArgsOrAtom, Q, E) ->
|
||||
ImportMeta = import_meta(Meta, Name, Arity, Q, E),
|
||||
Annotated = annotate({Name, ImportMeta, ArgsOrAtom}, Q#elixir_quote.context),
|
||||
do_quote_tuple(Annotated, Q, E).
|
||||
|
||||
do_quote_call(Left, Meta, Expr, Args, Q, E) ->
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
-module(elixir_rewrite).
|
||||
-compile({inline, [inner_inline/4, inner_rewrite/4]}).
|
||||
-compile({inline, [inner_inline/4, inner_rewrite/5]}).
|
||||
-export([erl_to_ex/3, inline/3, rewrite/5, match_rewrite/5, guard_rewrite/5, format_error/1]).
|
||||
-include("elixir.hrl").
|
||||
|
||||
@@ -34,13 +34,13 @@
|
||||
|
||||
-define(
|
||||
rewrite(ExMod, ExFun, ExArgs, ErlMod, ErlFun, ErlArgs),
|
||||
inner_rewrite(ex_to_erl, ExMod, ExFun, ExArgs) -> {ErlMod, ErlFun, ErlArgs};
|
||||
inner_rewrite(erl_to_ex, ErlMod, ErlFun, ErlArgs) -> {ExMod, ExFun, ExArgs}
|
||||
inner_rewrite(ex_to_erl, _Meta, ExMod, ExFun, ExArgs) -> {ErlMod, ErlFun, ErlArgs};
|
||||
inner_rewrite(erl_to_ex, _Meta, ErlMod, ErlFun, ErlArgs) -> {ExMod, ExFun, ExArgs}
|
||||
).
|
||||
|
||||
erl_to_ex(Mod, Fun, Args) ->
|
||||
case inner_inline(erl_to_ex, Mod, Fun, length(Args)) of
|
||||
false -> inner_rewrite(erl_to_ex, Mod, Fun, Args);
|
||||
false -> inner_rewrite(erl_to_ex, [], Mod, Fun, Args);
|
||||
{ExMod, ExFun} -> {ExMod, ExFun, Args}
|
||||
end.
|
||||
|
||||
@@ -223,12 +223,13 @@ inner_inline(_, _, _, _) -> false.
|
||||
%% as they may change the number of arguments. However, they
|
||||
%% don't add new code (such as case statements), at best they
|
||||
%% perform dead code removal.
|
||||
rewrite(?string_chars, _, to_string, _, [String]) when is_binary(String) ->
|
||||
String;
|
||||
rewrite(?string_chars, _, to_string, _, [{{'.', _, [?kernel, inspect]}, _, _} = Call]) ->
|
||||
Call;
|
||||
rewrite(?string_chars, DotMeta, to_string, Meta, [Arg]) ->
|
||||
case is_always_string(Arg) of
|
||||
true -> Arg;
|
||||
false -> {{'.', DotMeta, [?string_chars, to_string]}, Meta, [Arg]}
|
||||
end;
|
||||
rewrite(Receiver, DotMeta, Right, Meta, Args) ->
|
||||
{EReceiver, ERight, EArgs} = inner_rewrite(ex_to_erl, Receiver, Right, Args),
|
||||
{EReceiver, ERight, EArgs} = inner_rewrite(ex_to_erl, DotMeta, Receiver, Right, Args),
|
||||
{{'.', DotMeta, [EReceiver, ERight]}, Meta, EArgs}.
|
||||
|
||||
?rewrite(?atom, to_string, [Arg], erlang, atom_to_binary, [Arg, utf8]);
|
||||
@@ -248,39 +249,39 @@ rewrite(Receiver, DotMeta, Right, Meta, Args) ->
|
||||
?rewrite(?string, to_existing_atom, [Arg], erlang, binary_to_existing_atom, [Arg, utf8]);
|
||||
?rewrite(?tuple, duplicate, [Data, Size], erlang, make_tuple, [Size, Data]);
|
||||
|
||||
inner_rewrite(ex_to_erl, ?tuple, delete_at, [Tuple, Index]) ->
|
||||
{erlang, delete_element, [increment(Index), Tuple]};
|
||||
inner_rewrite(ex_to_erl, ?tuple, insert_at, [Tuple, Index, Term]) ->
|
||||
{erlang, insert_element, [increment(Index), Tuple, Term]};
|
||||
inner_rewrite(ex_to_erl, ?kernel, elem, [Tuple, Index]) ->
|
||||
{erlang, element, [increment(Index), Tuple]};
|
||||
inner_rewrite(ex_to_erl, ?kernel, put_elem, [Tuple, Index, Value]) ->
|
||||
{erlang, setelement, [increment(Index), Tuple, Value]};
|
||||
inner_rewrite(ex_to_erl, Meta, ?tuple, delete_at, [Tuple, Index]) ->
|
||||
{erlang, delete_element, [increment(Meta, Index), Tuple]};
|
||||
inner_rewrite(ex_to_erl, Meta, ?tuple, insert_at, [Tuple, Index, Term]) ->
|
||||
{erlang, insert_element, [increment(Meta, Index), Tuple, Term]};
|
||||
inner_rewrite(ex_to_erl, Meta, ?kernel, elem, [Tuple, Index]) ->
|
||||
{erlang, element, [increment(Meta, Index), Tuple]};
|
||||
inner_rewrite(ex_to_erl, Meta, ?kernel, put_elem, [Tuple, Index, Value]) ->
|
||||
{erlang, setelement, [increment(Meta, Index), Tuple, Value]};
|
||||
|
||||
inner_rewrite(erl_to_ex, erlang, delete_element, [Index, Tuple]) when is_number(Index) ->
|
||||
inner_rewrite(erl_to_ex, _Meta, erlang, delete_element, [Index, Tuple]) when is_number(Index) ->
|
||||
{?tuple, delete_at, [Tuple, Index - 1]};
|
||||
inner_rewrite(erl_to_ex, erlang, insert_element, [Index, Tuple, Term]) when is_number(Index) ->
|
||||
inner_rewrite(erl_to_ex, _Meta, erlang, insert_element, [Index, Tuple, Term]) when is_number(Index) ->
|
||||
{?tuple, insert_at, [Tuple, Index - 1, Term]};
|
||||
inner_rewrite(erl_to_ex, erlang, element, [Index, Tuple]) when is_number(Index) ->
|
||||
inner_rewrite(erl_to_ex, _Meta, erlang, element, [Index, Tuple]) when is_number(Index) ->
|
||||
{?kernel, elem, [Tuple, Index - 1]};
|
||||
inner_rewrite(erl_to_ex, erlang, setelement, [Index, Tuple, Value]) when is_number(Index) ->
|
||||
inner_rewrite(erl_to_ex, _Meta, erlang, setelement, [Index, Tuple, Value]) when is_number(Index) ->
|
||||
{?kernel, put_elem, [Tuple, Index - 1, Value]};
|
||||
|
||||
inner_rewrite(erl_to_ex, erlang, delete_element, [{{'.', _, [erlang, '+']}, _, [Index, 1]}, Tuple]) ->
|
||||
inner_rewrite(erl_to_ex, _Meta, erlang, delete_element, [{{'.', _, [erlang, '+']}, _, [Index, 1]}, Tuple]) ->
|
||||
{?tuple, delete_at, [Tuple, Index]};
|
||||
inner_rewrite(erl_to_ex, erlang, insert_element, [{{'.', _, [erlang, '+']}, _, [Index, 1]}, Tuple, Term]) ->
|
||||
inner_rewrite(erl_to_ex, _Meta, erlang, insert_element, [{{'.', _, [erlang, '+']}, _, [Index, 1]}, Tuple, Term]) ->
|
||||
{?tuple, insert_at, [Tuple, Index, Term]};
|
||||
inner_rewrite(erl_to_ex, erlang, element, [{{'.', _, [erlang, '+']}, _, [Index, 1]}, Tuple]) ->
|
||||
inner_rewrite(erl_to_ex, _Meta, erlang, element, [{{'.', _, [erlang, '+']}, _, [Index, 1]}, Tuple]) ->
|
||||
{?kernel, elem, [Tuple, Index]};
|
||||
inner_rewrite(erl_to_ex, erlang, setelement, [{{'.', _, [erlang, '+']}, _, [Index, 1]}, Tuple, Value]) ->
|
||||
inner_rewrite(erl_to_ex, _Meta, erlang, setelement, [{{'.', _, [erlang, '+']}, _, [Index, 1]}, Tuple, Value]) ->
|
||||
{?kernel, put_elem, [Tuple, Index, Value]};
|
||||
|
||||
inner_rewrite(_To, Mod, Fun, Args) -> {Mod, Fun, Args}.
|
||||
inner_rewrite(_To, _Meta, Mod, Fun, Args) -> {Mod, Fun, Args}.
|
||||
|
||||
increment(Number) when is_number(Number) ->
|
||||
increment(_Meta, Number) when is_number(Number) ->
|
||||
Number + 1;
|
||||
increment(Other) ->
|
||||
{{'.', [], [erlang, '+']}, [], [Other, 1]}.
|
||||
increment(Meta, Other) ->
|
||||
{{'.', Meta, [erlang, '+']}, Meta, [Other, 1]}.
|
||||
|
||||
%% Match rewrite
|
||||
%%
|
||||
@@ -312,7 +313,7 @@ static_append(_, _, _) -> throw(impossible).
|
||||
%% it also verifies the resulting function is supported in
|
||||
%% guard context - only certain BIFs and operators are.
|
||||
guard_rewrite(Receiver, DotMeta, Right, Meta, Args) ->
|
||||
case inner_rewrite(ex_to_erl, Receiver, Right, Args) of
|
||||
case inner_rewrite(ex_to_erl, DotMeta, Receiver, Right, Args) of
|
||||
{erlang, RRight, RArgs} ->
|
||||
case allowed_guard(RRight, length(RArgs)) of
|
||||
true -> {ok, {{'.', DotMeta, [erlang, RRight]}, Meta, RArgs}};
|
||||
@@ -333,3 +334,16 @@ format_error({invalid_match, Receiver, Right, Arity}) ->
|
||||
format_error({invalid_match_append, Arg}) ->
|
||||
io_lib:format("invalid argument for ++ operator inside a match, expected a literal proper list, got: ~ts",
|
||||
['Elixir.Macro':to_string(Arg)]).
|
||||
|
||||
is_always_string({{'.', _, [Module, Function]}, _, Args}) ->
|
||||
is_always_string(Module, Function, length(Args));
|
||||
is_always_string(Ast) ->
|
||||
is_binary(Ast).
|
||||
|
||||
is_always_string('Elixir.Enum', join, _) -> true;
|
||||
is_always_string('Elixir.Enum', map_join, _) -> true;
|
||||
is_always_string('Elixir.Kernel', inspect, _) -> true;
|
||||
is_always_string('Elixir.Macro', to_string, _) -> true;
|
||||
is_always_string('Elixir.String.Chars', to_string, _) -> true;
|
||||
is_always_string('Elixir.Path', join, _) -> true;
|
||||
is_always_string(_Module, _Function, _Args) -> false.
|
||||
|
||||
@@ -7,15 +7,19 @@
|
||||
read_file_type/1, read_file_type/2, read_link_type/1, read_posix_mtime_and_size/1,
|
||||
change_posix_time/2, change_universal_time/2,
|
||||
guard_op/2, extract_splat_guards/1, extract_guards/1,
|
||||
erlang_comparison_op_to_elixir/1]).
|
||||
erlang_comparison_op_to_elixir/1, erl_fa_to_elixir_fa/2]).
|
||||
-include("elixir.hrl").
|
||||
-include_lib("kernel/include/file.hrl").
|
||||
|
||||
% Builds the macro name
|
||||
|
||||
macro_name(Macro) ->
|
||||
list_to_atom("MACRO-" ++ atom_to_list(Macro)).
|
||||
|
||||
erl_fa_to_elixir_fa(Name, Arity) ->
|
||||
case atom_to_list(Name) of
|
||||
"MACRO-" ++ Rest -> {list_to_atom(Rest), Arity - 1};
|
||||
_ -> {Name, Arity}
|
||||
end.
|
||||
|
||||
guard_op('andalso', 2) ->
|
||||
true;
|
||||
guard_op('orelse', 2) ->
|
||||
|
||||
@@ -102,6 +102,108 @@ defmodule Code.Formatter.CommentsTest do
|
||||
end
|
||||
end
|
||||
|
||||
describe "modules attributes" do
|
||||
test "with comments around" do
|
||||
assert_same """
|
||||
defmodule Sample do
|
||||
# Comment 0
|
||||
@moduledoc false
|
||||
# Comment 1
|
||||
|
||||
# Comment 2
|
||||
@attr1 1
|
||||
# Comment 3
|
||||
|
||||
# Comment 4
|
||||
@doc "Doc"
|
||||
# Comment 5
|
||||
@attr2 2
|
||||
# Comment 6
|
||||
def sample, do: :sample
|
||||
end
|
||||
"""
|
||||
end
|
||||
|
||||
test "with comments only after" do
|
||||
assert_same """
|
||||
@moduledoc false
|
||||
# Comment 1
|
||||
|
||||
@attr 1
|
||||
"""
|
||||
end
|
||||
|
||||
test "with too many new lines" do
|
||||
bad = """
|
||||
defmodule Sample do
|
||||
|
||||
# Comment 0
|
||||
|
||||
|
||||
@moduledoc false
|
||||
|
||||
|
||||
# Comment 1
|
||||
|
||||
|
||||
# Comment 2
|
||||
|
||||
|
||||
@attr1 1
|
||||
|
||||
|
||||
# Comment 3
|
||||
|
||||
|
||||
# Comment 4
|
||||
|
||||
|
||||
@doc "Doc"
|
||||
|
||||
|
||||
# Comment 5
|
||||
|
||||
|
||||
@attr2 2
|
||||
|
||||
|
||||
# Comment 6
|
||||
|
||||
|
||||
def sample, do: :sample
|
||||
end
|
||||
"""
|
||||
|
||||
assert_format bad, """
|
||||
defmodule Sample do
|
||||
# Comment 0
|
||||
|
||||
@moduledoc false
|
||||
|
||||
# Comment 1
|
||||
|
||||
# Comment 2
|
||||
|
||||
@attr1 1
|
||||
|
||||
# Comment 3
|
||||
|
||||
# Comment 4
|
||||
|
||||
@doc "Doc"
|
||||
|
||||
# Comment 5
|
||||
|
||||
@attr2 2
|
||||
|
||||
# Comment 6
|
||||
|
||||
def sample, do: :sample
|
||||
end
|
||||
"""
|
||||
end
|
||||
end
|
||||
|
||||
describe "interpolation" do
|
||||
test "with comment outside before, during and after" do
|
||||
assert_same ~S"""
|
||||
|
||||
@@ -412,6 +412,11 @@ defmodule Code.Formatter.ContainersTest do
|
||||
assert_same map, @medium_length
|
||||
end
|
||||
|
||||
test "preserves user choice in regards to =>" do
|
||||
assert_same "%{:hello => 1, :world => 2}"
|
||||
assert_format "%{:true => 1, :false => 2}", "%{true => 1, false => 2}"
|
||||
end
|
||||
|
||||
test "preserves user choice even when it fits" do
|
||||
assert_same """
|
||||
%{
|
||||
|
||||
@@ -103,15 +103,21 @@ defmodule CodeTest do
|
||||
test "compile_file/1" do
|
||||
assert Code.compile_file(fixture_path("code_sample.exs")) == []
|
||||
refute fixture_path("code_sample.exs") in Code.required_files()
|
||||
|
||||
assert [{CompileSample, binary}] = Code.compile_file(fixture_path("compile_sample.ex"))
|
||||
assert is_binary(binary)
|
||||
after
|
||||
:code.purge(CompileSample)
|
||||
:code.delete(CompileSample)
|
||||
end
|
||||
|
||||
test "compile_file/1 also emits checker warnings" do
|
||||
output =
|
||||
ExUnit.CaptureIO.capture_io(:stderr, fn ->
|
||||
Code.compile_file(PathHelpers.fixture_path("checker_warning.exs"))
|
||||
Code.compile_file(fixture_path("checker_warning.exs"))
|
||||
end)
|
||||
|
||||
assert output =~ "function clause will never match"
|
||||
assert output =~ "incompatible types"
|
||||
end
|
||||
|
||||
test "require_file/1" do
|
||||
@@ -122,8 +128,13 @@ defmodule CodeTest do
|
||||
Code.unrequire_files([fixture_path("code_sample.exs")])
|
||||
refute fixture_path("code_sample.exs") in Code.required_files()
|
||||
assert Code.require_file(fixture_path("code_sample.exs")) != nil
|
||||
|
||||
assert [{CompileSample, binary}] = Code.require_file(fixture_path("compile_sample.ex"))
|
||||
assert is_binary(binary)
|
||||
after
|
||||
Code.unrequire_files([fixture_path("code_sample.exs")])
|
||||
Code.unrequire_files([fixture_path("code_sample.exs"), fixture_path("compile_sample.ex")])
|
||||
:code.purge(CompileSample)
|
||||
:code.delete(CompileSample)
|
||||
end
|
||||
|
||||
describe "string_to_quoted/2" do
|
||||
@@ -338,20 +349,22 @@ defmodule CodeTest do
|
||||
assert string_to_quoted.("0xEF") == {:__block__, [token: "0xEF", line: 1], [239]}
|
||||
assert string_to_quoted.("12.3") == {:__block__, [token: "12.3", line: 1], [12.3]}
|
||||
assert string_to_quoted.("nil") == {:__block__, [line: 1], [nil]}
|
||||
assert string_to_quoted.(":one") == {:__block__, [delimiter: ":", line: 1], [:one]}
|
||||
assert string_to_quoted.(":one") == {:__block__, [line: 1], [:one]}
|
||||
|
||||
args = [[{:__block__, [token: "1", line: 1], [1]}]]
|
||||
assert string_to_quoted.("[one: :two]") == {
|
||||
:__block__,
|
||||
[{:closing, [line: 1]}, {:line, 1}],
|
||||
[
|
||||
[
|
||||
{{:__block__, [format: :keyword, line: 1], [:one]},
|
||||
{:__block__, [line: 1], [:two]}}
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
assert string_to_quoted.("[1]") ==
|
||||
{:__block__, [closing: [line: 1], line: 1], args}
|
||||
|
||||
args = [
|
||||
{{:__block__, [delimiter: ":", line: 1], [:ok]},
|
||||
{:__block__, [delimiter: ":", line: 1], [:test]}}
|
||||
]
|
||||
|
||||
assert string_to_quoted.("{:ok, :test}") ==
|
||||
{:__block__, [closing: [line: 1], line: 1], args}
|
||||
{:__block__, [closing: [line: 1], line: 1],
|
||||
[[{:__block__, [token: "1", line: 1], [1]}]]}
|
||||
|
||||
assert string_to_quoted.(~s("""\nhello\n""")) ==
|
||||
{:__block__, [delimiter: ~s["""], line: 1], ["hello\n"]}
|
||||
@@ -359,16 +372,15 @@ defmodule CodeTest do
|
||||
assert string_to_quoted.("'''\nhello\n'''") ==
|
||||
{:__block__, [delimiter: ~s['''], line: 1], ['hello\n']}
|
||||
|
||||
args = [
|
||||
{:->, [line: 1],
|
||||
[
|
||||
[{:__block__, [token: "1", line: 1, closing: [line: 1], line: 1], [1]}],
|
||||
{:__block__, [delimiter: "\"", line: 1], ["hello"]}
|
||||
]}
|
||||
]
|
||||
|
||||
assert string_to_quoted.(~s[fn (1) -> "hello" end]) ==
|
||||
{:fn, [closing: [line: 1], line: 1], args}
|
||||
{:fn, [closing: [line: 1], line: 1],
|
||||
[
|
||||
{:->, [line: 1],
|
||||
[
|
||||
[{:__block__, [token: "1", line: 1, closing: [line: 1], line: 1], [1]}],
|
||||
{:__block__, [delimiter: "\"", line: 1], ["hello"]}
|
||||
]}
|
||||
]}
|
||||
end
|
||||
|
||||
test "raises on bad literal" do
|
||||
|
||||
@@ -20,6 +20,7 @@ defmodule Config.ProviderTest do
|
||||
|
||||
on_exit(fn ->
|
||||
Application.delete_env(@config_app, :config_providers)
|
||||
Application.delete_env(@config_app, :config_providers_booted)
|
||||
System.delete_env(@env_var)
|
||||
end)
|
||||
end
|
||||
@@ -79,7 +80,7 @@ defmodule Config.ProviderTest do
|
||||
init_and_assert_boot()
|
||||
config = consult(@sys_config)
|
||||
assert config[:my_app] == [key: :value]
|
||||
assert config[@config_app] == [config_providers: :booted]
|
||||
assert config[@config_app] == [config_providers_booted: {:booted, nil}]
|
||||
end
|
||||
|
||||
@tag sys_config: [my_app: [encoding: {:"£", "£", '£'}]]
|
||||
@@ -113,6 +114,18 @@ defmodule Config.ProviderTest do
|
||||
refute File.exists?(@sys_config)
|
||||
end
|
||||
|
||||
test "returns :booted if already booted and runs validate_compile_env" do
|
||||
init_and_assert_boot(
|
||||
prune_after_boot: true,
|
||||
validate_compile_env: [{:elixir, [:unknown], {:ok, :value}}]
|
||||
)
|
||||
|
||||
Application.put_all_env(Keyword.take(consult(@sys_config), [@config_app]))
|
||||
|
||||
assert capture_abort(fn -> boot() end) =~
|
||||
"the application :elixir has a different value set for key :unknown"
|
||||
end
|
||||
|
||||
test "raises if booting twice in a row" do
|
||||
init_and_assert_boot()
|
||||
|
||||
|
||||
@@ -8,11 +8,9 @@ defmodule IntegerTest do
|
||||
require Integer
|
||||
|
||||
def test_is_odd_in_guards(number) when Integer.is_odd(number), do: number
|
||||
def test_is_odd_in_guards(atom) when is_atom(atom) and not Integer.is_odd(atom), do: :atom
|
||||
def test_is_odd_in_guards(_number), do: false
|
||||
|
||||
def test_is_even_in_guards(number) when Integer.is_even(number), do: number
|
||||
def test_is_even_in_guards(atom) when is_atom(atom) and not Integer.is_even(atom), do: :atom
|
||||
def test_is_even_in_guards(_number), do: false
|
||||
|
||||
test "is_odd/1" do
|
||||
@@ -25,7 +23,7 @@ defmodule IntegerTest do
|
||||
assert Integer.is_odd(-3) == true
|
||||
assert test_is_odd_in_guards(10) == false
|
||||
assert test_is_odd_in_guards(11) == 11
|
||||
assert test_is_odd_in_guards(:not_integer) == :atom
|
||||
assert test_is_odd_in_guards(:not_integer) == false
|
||||
end
|
||||
|
||||
test "is_even/1" do
|
||||
@@ -38,7 +36,7 @@ defmodule IntegerTest do
|
||||
assert Integer.is_even(-3) == false
|
||||
assert test_is_even_in_guards(10) == 10
|
||||
assert test_is_even_in_guards(11) == false
|
||||
assert test_is_even_in_guards(:not_integer) == :atom
|
||||
assert test_is_even_in_guards(:not_integer) == false
|
||||
end
|
||||
|
||||
test "mod/2" do
|
||||
|
||||
@@ -1226,23 +1226,13 @@ defmodule Kernel.ErrorsTest do
|
||||
end
|
||||
|
||||
test "duplicate map keys" do
|
||||
assert_eval_raise CompileError, "nofile:1: key :a will be overridden in map", """
|
||||
%{a: :b, a: :c}
|
||||
"""
|
||||
|
||||
assert_eval_raise CompileError, "nofile:1: key :a will be overridden in map", """
|
||||
%{a: :b, a: :c} = %{a: :c}
|
||||
"""
|
||||
|
||||
assert_eval_raise CompileError, "nofile:1: key :m will be overridden in map", """
|
||||
%{m: :n, m: :o, m: :p}
|
||||
assert_eval_raise CompileError, "nofile:1: key :a will be overridden in map", """
|
||||
%{a: :b, a: :c, a: :d} = %{a: :c}
|
||||
"""
|
||||
|
||||
assert_eval_raise CompileError, "nofile:1: key 1 will be overridden in map", """
|
||||
%{1 => 2, 1 => 3}
|
||||
"""
|
||||
|
||||
assert map_size(%{System.unique_integer() => 1, System.unique_integer() => 2}) == 2
|
||||
end
|
||||
|
||||
defp bad_remote_call(x), do: x.foo
|
||||
|
||||
@@ -3,6 +3,11 @@ Code.require_file("../test_helper.exs", __DIR__)
|
||||
defmodule Kernel.ExpansionTarget do
|
||||
defmacro seventeen, do: 17
|
||||
defmacro bar, do: "bar"
|
||||
|
||||
defmacro message_hello(arg) do
|
||||
send(self(), :hello)
|
||||
arg
|
||||
end
|
||||
end
|
||||
|
||||
defmodule Kernel.ExpansionTest do
|
||||
@@ -543,16 +548,6 @@ defmodule Kernel.ExpansionTest do
|
||||
assert expand(quote(do: quote(do: hello))) == {:{}, [], [:hello, [], __MODULE__]}
|
||||
end
|
||||
|
||||
test "raises if the :context option is invalid" do
|
||||
assert_raise CompileError, ~r"invalid :context for quote, .*, got: :erlang\.self\(\)", fn ->
|
||||
expand(quote(do: quote(context: self(), do: :ok)))
|
||||
end
|
||||
|
||||
assert_raise CompileError, ~r"invalid :context for quote, .*, got: nil", fn ->
|
||||
expand(quote(do: quote(context: nil, do: :ok)))
|
||||
end
|
||||
end
|
||||
|
||||
test "raises if the :bind_quoted option is invalid" do
|
||||
assert_raise CompileError, ~r"invalid :bind_quoted for quote", fn ->
|
||||
expand(quote(do: quote(bind_quoted: self(), do: :ok)))
|
||||
@@ -2149,6 +2144,14 @@ defmodule Kernel.ExpansionTest do
|
||||
test "inlines binaries inside interpolation" do
|
||||
import Kernel.ExpansionTarget
|
||||
|
||||
# Check expansion happens only once
|
||||
assert expand(quote(do: "foo#{message_hello("bar")}")) |> clean_meta([:alignment]) ==
|
||||
quote(do: <<"foo"::binary(), "bar"::binary()>>)
|
||||
|
||||
assert_received :hello
|
||||
refute_received :hello
|
||||
|
||||
# And it also works in match
|
||||
assert expand(quote(do: "foo#{bar()}" = "foobar")) |> clean_meta([:alignment]) ==
|
||||
quote(do: <<"foo"::binary(), "bar"::binary()>> = "foobar")
|
||||
end
|
||||
|
||||
@@ -278,14 +278,17 @@ defmodule Kernel.ParallelCompilerTest do
|
||||
"""
|
||||
)
|
||||
|
||||
output = tmp_path("not_to_be_used")
|
||||
|
||||
try do
|
||||
Code.compiler_options(warnings_as_errors: true)
|
||||
|
||||
msg =
|
||||
capture_io(:stderr, fn ->
|
||||
assert {:error, [error], []} = Kernel.ParallelCompiler.compile([fixture])
|
||||
msg = "this clause cannot match because a previous clause at line 2 always matches"
|
||||
assert error == {fixture, 3, msg}
|
||||
assert {:error, [error], []} =
|
||||
Kernel.ParallelCompiler.compile_to_path([fixture], output)
|
||||
|
||||
assert {^fixture, 3, "this clause " <> _} = error
|
||||
end)
|
||||
|
||||
assert msg =~
|
||||
@@ -294,6 +297,8 @@ defmodule Kernel.ParallelCompilerTest do
|
||||
Code.compiler_options(warnings_as_errors: warnings_as_errors)
|
||||
purge([WarningsSample])
|
||||
end
|
||||
|
||||
refute File.exists?(output)
|
||||
end
|
||||
|
||||
test "does not use incorrect line number when error originates in another file" do
|
||||
@@ -428,10 +433,7 @@ defmodule Kernel.ParallelCompilerTest do
|
||||
capture_io(:stderr, fn ->
|
||||
assert {:error, [error], []} = Kernel.ParallelCompiler.require([fixture])
|
||||
|
||||
message =
|
||||
"this clause cannot match because a previous clause at line 2 always matches"
|
||||
|
||||
assert error == {fixture, 3, message}
|
||||
assert {^fixture, 3, "this clause " <> _} = error
|
||||
end)
|
||||
|
||||
assert msg =~
|
||||
|
||||
@@ -12,9 +12,10 @@ defmodule Kernel.QuoteTest do
|
||||
end
|
||||
|
||||
test "keep line" do
|
||||
# DO NOT MOVE THIS LINE
|
||||
line = __ENV__.line + 2
|
||||
|
||||
assert quote(location: :keep, do: bar(1, 2, 3)) ==
|
||||
{:bar, [keep: {Path.relative_to_cwd(__ENV__.file), 16}], [1, 2, 3]}
|
||||
{:bar, [keep: {Path.relative_to_cwd(__ENV__.file), line}], [1, 2, 3]}
|
||||
end
|
||||
|
||||
test "fixed line" do
|
||||
@@ -22,9 +23,23 @@ defmodule Kernel.QuoteTest do
|
||||
end
|
||||
|
||||
test "quote line var" do
|
||||
# DO NOT MOVE THIS LINE
|
||||
line = __ENV__.line
|
||||
assert quote(line: line, do: bar(1, 2, 3)) == {:bar, [line: 26], [1, 2, 3]}
|
||||
assert quote(line: line, do: bar(1, 2, 3)) == {:bar, [line: line], [1, 2, 3]}
|
||||
end
|
||||
|
||||
test "quote context var" do
|
||||
context = :dynamic
|
||||
assert quote(context: context, do: bar) == {:bar, [], :dynamic}
|
||||
|
||||
assert_raise ArgumentError, fn ->
|
||||
context = "oops"
|
||||
quote(context: context, do: bar)
|
||||
end
|
||||
|
||||
assert_raise ArgumentError, fn ->
|
||||
context = nil
|
||||
quote(context: context, do: bar)
|
||||
end
|
||||
end
|
||||
|
||||
test "operator precedence" do
|
||||
@@ -185,7 +200,7 @@ defmodule Kernel.QuoteTest do
|
||||
|
||||
test "bind quoted" do
|
||||
args = [
|
||||
{:=, [], [{:foo, [], Kernel.QuoteTest}, 3]},
|
||||
{:=, [], [{:foo, [line: __ENV__.line + 4], Kernel.QuoteTest}, 3]},
|
||||
{:foo, [], Kernel.QuoteTest}
|
||||
]
|
||||
|
||||
@@ -276,6 +291,8 @@ end
|
||||
|
||||
# DO NOT MOVE THIS LINE
|
||||
defmodule Kernel.QuoteTest.Errors do
|
||||
def line, do: __ENV__.line + 4
|
||||
|
||||
defmacro defraise do
|
||||
quote location: :keep do
|
||||
def will_raise(_a, _b), do: raise("oops")
|
||||
@@ -294,6 +311,7 @@ defmodule Kernel.QuoteTest.ErrorsTest do
|
||||
# Defines the add function
|
||||
defraise()
|
||||
|
||||
@line line()
|
||||
test "inside function error" do
|
||||
try do
|
||||
will_raise(:a, :b)
|
||||
@@ -301,12 +319,13 @@ defmodule Kernel.QuoteTest.ErrorsTest do
|
||||
RuntimeError ->
|
||||
mod = Kernel.QuoteTest.ErrorsTest
|
||||
file = __ENV__.file |> Path.relative_to_cwd() |> String.to_charlist()
|
||||
assert [{^mod, :will_raise, 2, [file: ^file, line: 281]} | _] = __STACKTRACE__
|
||||
assert [{^mod, :will_raise, 2, [file: ^file, line: @line]} | _] = __STACKTRACE__
|
||||
else
|
||||
_ -> flunk("expected failure")
|
||||
end
|
||||
end
|
||||
|
||||
@line __ENV__.line + 3
|
||||
test "outside function error" do
|
||||
try do
|
||||
will_raise()
|
||||
@@ -314,7 +333,7 @@ defmodule Kernel.QuoteTest.ErrorsTest do
|
||||
RuntimeError ->
|
||||
mod = Kernel.QuoteTest.ErrorsTest
|
||||
file = __ENV__.file |> Path.relative_to_cwd() |> String.to_charlist()
|
||||
assert [{^mod, _, _, [file: ^file, line: 312]} | _] = __STACKTRACE__
|
||||
assert [{^mod, _, _, [file: ^file, line: @line]} | _] = __STACKTRACE__
|
||||
else
|
||||
_ -> flunk("expected failure")
|
||||
end
|
||||
|
||||
@@ -66,6 +66,7 @@ bar) in ["foo\\\nbar", "foo\\\r\nbar"]
|
||||
|
||||
test "sigil w" do
|
||||
assert ~w() == []
|
||||
assert ~w([ , ]) == ["[", ",", "]"]
|
||||
assert ~w(foo bar baz) == ["foo", "bar", "baz"]
|
||||
assert ~w(foo #{:bar} baz) == ["foo", "bar", "baz"]
|
||||
|
||||
@@ -100,6 +101,7 @@ bar) in ["foo\\\nbar", "foo\\\r\nbar"]
|
||||
|
||||
test "sigil W" do
|
||||
assert ~W() == []
|
||||
assert ~W([ , ]) == ["[", ",", "]"]
|
||||
assert ~W(foo #{bar} baz) == ["foo", "\#{bar}", "baz"]
|
||||
|
||||
assert ~W(foo\ bar) == ["foo\\", "bar"]
|
||||
|
||||
@@ -52,5 +52,20 @@ defmodule Kernel.SpecialFormsTest do
|
||||
:otherwise -> :also_good
|
||||
end
|
||||
end
|
||||
|
||||
def false_fun(), do: false
|
||||
|
||||
test "cond_clause error keeps line number in stacktrace" do
|
||||
try do
|
||||
cond do
|
||||
false_fun() -> :ok
|
||||
end
|
||||
rescue
|
||||
_ ->
|
||||
assert [{Kernel.SpecialFormsTest, _, _, meta} | _] = __STACKTRACE__
|
||||
assert meta[:file]
|
||||
assert meta[:line]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -20,6 +20,15 @@ defmodule Kernel.TracersTest do
|
||||
end)
|
||||
end
|
||||
|
||||
test "traces start and stop" do
|
||||
compile_string("""
|
||||
Foo
|
||||
""")
|
||||
|
||||
assert_receive {:start, %{lexical_tracker: pid}} when is_pid(pid)
|
||||
assert_receive {:stop, %{lexical_tracker: pid}} when is_pid(pid)
|
||||
end
|
||||
|
||||
test "traces alias references" do
|
||||
compile_string("""
|
||||
Foo
|
||||
|
||||
@@ -578,6 +578,23 @@ defmodule Kernel.WarningTest do
|
||||
purge(Sample)
|
||||
end
|
||||
|
||||
test "duplicate map keys" do
|
||||
output =
|
||||
capture_err(fn ->
|
||||
defmodule DuplicateMapKeys do
|
||||
assert %{a: :b, a: :c} == %{a: :c}
|
||||
assert %{m: :n, m: :o, m: :p} == %{m: :p}
|
||||
assert %{1 => 2, 1 => 3} == %{1 => 3}
|
||||
end
|
||||
end)
|
||||
|
||||
assert output =~ "key :a will be overridden in map"
|
||||
assert output =~ "key :m will be overridden in map"
|
||||
assert output =~ "key 1 will be overridden in map"
|
||||
|
||||
assert map_size(%{System.unique_integer() => 1, System.unique_integer() => 2}) == 2
|
||||
end
|
||||
|
||||
test "unused guard" do
|
||||
assert capture_err(fn ->
|
||||
Code.eval_string("""
|
||||
@@ -644,24 +661,6 @@ defmodule Kernel.WarningTest do
|
||||
purge(Sample)
|
||||
end
|
||||
|
||||
test "previous clause always matches" do
|
||||
assert capture_err(fn ->
|
||||
Code.eval_string("""
|
||||
defmodule Sample do
|
||||
def binary_cond do
|
||||
v = "bc"
|
||||
cond do
|
||||
is_binary(v) -> :bin
|
||||
true -> :ok
|
||||
end
|
||||
end
|
||||
end
|
||||
""")
|
||||
end) =~ "this clause cannot match because a previous clause at line 5 always matches"
|
||||
after
|
||||
purge(Sample)
|
||||
end
|
||||
|
||||
test "late function heads" do
|
||||
assert capture_err(fn ->
|
||||
Code.eval_string("""
|
||||
@@ -723,7 +722,8 @@ defmodule Kernel.WarningTest do
|
||||
def hello, do: nil
|
||||
end
|
||||
""")
|
||||
end) =~ "this clause cannot match because a previous clause at line 2 always matches"
|
||||
end) =~
|
||||
~r"this clause( for hello/0)? cannot match because a previous clause at line 2 always matches"
|
||||
after
|
||||
purge(Sample)
|
||||
end
|
||||
@@ -743,7 +743,8 @@ defmodule Kernel.WarningTest do
|
||||
use Sample
|
||||
end
|
||||
""")
|
||||
end) =~ "this clause cannot match because a previous clause at line 10 always matches"
|
||||
end) =~
|
||||
~r"this clause( for hello/0)? cannot match because a previous clause at line 10 always matches"
|
||||
after
|
||||
purge(Sample)
|
||||
purge(UseSample)
|
||||
@@ -903,7 +904,7 @@ defmodule Kernel.WarningTest do
|
||||
purge(Sample)
|
||||
end
|
||||
|
||||
test "badarg warning" do
|
||||
test "eval failure warning" do
|
||||
assert capture_err(fn ->
|
||||
assert_raise ArgumentError, fn ->
|
||||
Code.eval_string("""
|
||||
@@ -912,7 +913,15 @@ defmodule Kernel.WarningTest do
|
||||
end
|
||||
""")
|
||||
end
|
||||
end) =~ "this expression will fail with ArgumentError"
|
||||
end) =~ ~r"this expression will fail with ArgumentError\n.*nofile:2"
|
||||
|
||||
assert capture_err(fn ->
|
||||
Code.eval_string("""
|
||||
defmodule Sample do
|
||||
def foo, do: 1 + nil
|
||||
end
|
||||
""")
|
||||
end) =~ ~r"this expression will fail with ArithmeticError\n.*nofile:2"
|
||||
after
|
||||
purge([Sample])
|
||||
end
|
||||
@@ -1603,7 +1612,8 @@ defmodule Kernel.WarningTest do
|
||||
defguard foo(baz) when baz == :baz
|
||||
end
|
||||
""")
|
||||
end) =~ "this clause cannot match because a previous clause at line 2 always matches"
|
||||
end) =~
|
||||
~r"this clause( for foo/1)? cannot match because a previous clause at line 2 always matches"
|
||||
after
|
||||
purge(Sample)
|
||||
end
|
||||
@@ -1616,7 +1626,8 @@ defmodule Kernel.WarningTest do
|
||||
defmacro foo(bar), do: bar == :bar
|
||||
end
|
||||
""")
|
||||
end) =~ "this clause cannot match because a previous clause at line 2 always matches"
|
||||
end) =~
|
||||
~r"this clause( for foo/1)? cannot match because a previous clause at line 2 always matches"
|
||||
after
|
||||
purge(Sample)
|
||||
end
|
||||
|
||||
@@ -73,6 +73,16 @@ defmodule Kernel.WithTest do
|
||||
assert state == 1
|
||||
end
|
||||
|
||||
test "with extra guards" do
|
||||
var =
|
||||
with %_{} = a <- struct(URI),
|
||||
%_{} <- a do
|
||||
:ok
|
||||
end
|
||||
|
||||
assert var == :ok
|
||||
end
|
||||
|
||||
test "errors in with" do
|
||||
assert_raise RuntimeError, fn ->
|
||||
with({:ok, res} <- oops(), do: res)
|
||||
|
||||
@@ -344,6 +344,7 @@ defmodule MacroTest do
|
||||
assert Macro.to_string(quote(do: ~R"\n123")) == ~S/~R"\n123"/
|
||||
|
||||
assert Macro.to_string(quote(do: ~S["'(123)'"])) == ~S/~S["'(123)'"]/
|
||||
assert Macro.to_string(quote(do: ~s"#{"foo"}")) == ~S/~s"#{"foo"}"/
|
||||
|
||||
assert Macro.to_string(
|
||||
quote do
|
||||
@@ -704,6 +705,12 @@ defmodule MacroTest do
|
||||
assert Macro.to_string(quote(do: 'abc')) == "'abc'"
|
||||
end
|
||||
|
||||
test "string" do
|
||||
assert Macro.to_string(quote(do: "")) == ~S/""/
|
||||
assert Macro.to_string(quote(do: "abc")) == ~S/"abc"/
|
||||
assert Macro.to_string(quote(do: "#{"abc"}")) == ~S/"#{"abc"}"/
|
||||
end
|
||||
|
||||
test "last arg keyword list" do
|
||||
assert Macro.to_string(quote(do: foo([]))) == "foo([])"
|
||||
assert Macro.to_string(quote(do: foo(x: y))) == "foo(x: y)"
|
||||
|
||||
@@ -23,17 +23,34 @@ defmodule Module.CheckerTest do
|
||||
@deprecated "oops"
|
||||
def e, do: :ok
|
||||
end
|
||||
""",
|
||||
"b.ex" => """
|
||||
defmodule B do
|
||||
@callback f() :: :ok
|
||||
end
|
||||
""",
|
||||
"c.ex" => """
|
||||
defmodule C do
|
||||
@macrocallback g() :: :ok
|
||||
end
|
||||
"""
|
||||
}
|
||||
|
||||
modules = compile(files)
|
||||
contents = read_chunk(modules[A])
|
||||
|
||||
assert contents.exports == [
|
||||
assert read_chunk(modules[A]).exports == [
|
||||
{{:c, 0}, %{deprecated_reason: nil, kind: :def}},
|
||||
{{:d, 0}, %{deprecated_reason: nil, kind: :defmacro}},
|
||||
{{:e, 0}, %{deprecated_reason: "oops", kind: :def}}
|
||||
]
|
||||
|
||||
assert read_chunk(modules[B]).exports == [
|
||||
{{:behaviour_info, 1}, %{deprecated_reason: nil, kind: :def}}
|
||||
]
|
||||
|
||||
assert read_chunk(modules[C]).exports == [
|
||||
{{:behaviour_info, 1}, %{deprecated_reason: nil, kind: :def}}
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -60,6 +77,28 @@ defmodule Module.CheckerTest do
|
||||
assert_warnings(files, warning)
|
||||
end
|
||||
|
||||
test "handles built in functions" do
|
||||
files = %{
|
||||
"a.ex" => """
|
||||
defmodule A do
|
||||
def a, do: Kernel.module_info()
|
||||
def b, do: Kernel.module_info(:functions)
|
||||
def c, do: Kernel.__info__(:functions)
|
||||
def d, do: GenServer.behaviour_info(:callbacks)
|
||||
def e, do: Kernel.behaviour_info(:callbacks)
|
||||
end
|
||||
"""
|
||||
}
|
||||
|
||||
warning = """
|
||||
warning: Kernel.behaviour_info/1 is undefined or private
|
||||
a.ex:6: A.e/0
|
||||
|
||||
"""
|
||||
|
||||
assert_warnings(files, warning)
|
||||
end
|
||||
|
||||
test "handles module body conditionals" do
|
||||
files = %{
|
||||
"a.ex" => """
|
||||
@@ -674,7 +713,7 @@ defmodule Module.CheckerTest do
|
||||
}
|
||||
|
||||
warning = """
|
||||
warning: function clause will never match, found incompatibility:
|
||||
warning: incompatible types:
|
||||
|
||||
integer() !~ binary()
|
||||
|
||||
@@ -710,7 +749,7 @@ defmodule Module.CheckerTest do
|
||||
}
|
||||
|
||||
warning = """
|
||||
warning: function clause will never match, found incompatibility:
|
||||
warning: incompatible types:
|
||||
|
||||
integer() !~ binary()
|
||||
|
||||
@@ -746,7 +785,7 @@ defmodule Module.CheckerTest do
|
||||
}
|
||||
|
||||
warning = """
|
||||
warning: function clause will never match, found incompatibility:
|
||||
warning: incompatible types:
|
||||
|
||||
{var0} !~ var0
|
||||
|
||||
@@ -777,7 +816,7 @@ defmodule Module.CheckerTest do
|
||||
}
|
||||
|
||||
warning = """
|
||||
warning: function clause will never match, found incompatibility:
|
||||
warning: incompatible types:
|
||||
|
||||
integer() !~ binary()
|
||||
|
||||
@@ -813,7 +852,7 @@ defmodule Module.CheckerTest do
|
||||
}
|
||||
|
||||
warning = """
|
||||
warning: function clause will never match, found incompatibility:
|
||||
warning: incompatible types:
|
||||
|
||||
integer() !~ binary()
|
||||
|
||||
|
||||
@@ -225,6 +225,11 @@ defmodule Module.Types.PatternTest do
|
||||
assert {:ok, :dynamic, context} = quoted_guard([x], elem(x, 0))
|
||||
assert Types.lift_type({:var, 0}, context) == :tuple
|
||||
|
||||
assert {:ok, {:atom, true}, context} = quoted_guard([], true)
|
||||
assert {:ok, {:atom, false}, context} = quoted_guard([], false)
|
||||
assert {:ok, {:atom, :fail}, context} = quoted_guard([], :fail)
|
||||
assert {:ok, :boolean, context} = quoted_guard([], is_atom(true or :fail))
|
||||
|
||||
assert {:error, {_, {:unable_unify, :tuple, :boolean, _, _}, _}} =
|
||||
quoted_guard([x], is_tuple(x) and is_boolean(x))
|
||||
end
|
||||
|
||||
@@ -161,6 +161,11 @@ defmodule Module.TypesTest do
|
||||
quoted_head([x], [is_tuple(is_atom(x))])
|
||||
end
|
||||
|
||||
test "erlang-only guards" do
|
||||
assert quoted_head([x], [:erlang.size(x)]) ==
|
||||
{:ok, [{:union, [:binary, :tuple]}]}
|
||||
end
|
||||
|
||||
test "failing guard functions" do
|
||||
assert quoted_head([x], [length([])]) == {:ok, [{:var, 0}]}
|
||||
|
||||
|
||||
@@ -91,8 +91,10 @@ defmodule RegexTest do
|
||||
assert <<0xA0::utf8>> =~ ~r/[[:space:]]/u
|
||||
assert <<0xA0::utf8>> =~ ~r/\s/u
|
||||
|
||||
# Erlang/OTP 23 raises badarg on invalid UTF-8.
|
||||
# Earlier versions simply would not match.
|
||||
assert catch_error(if <<?<, 255, ?>>> =~ ~r/<.>/u, do: flunk("failed"), else: raise("failed"))
|
||||
assert <<?<, 255, ?>>> =~ ~r/<.>/
|
||||
refute <<?<, 255, ?>>> =~ ~r/<.>/u
|
||||
end
|
||||
|
||||
test "ungreedy" do
|
||||
@@ -349,6 +351,6 @@ defmodule RegexTest do
|
||||
end
|
||||
|
||||
defp matches_escaped?(string, match) do
|
||||
Regex.match?(~r/#{Regex.escape(string)}/simxu, match)
|
||||
Regex.match?(~r/#{Regex.escape(string)}/simx, match)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -499,13 +499,17 @@ defmodule StreamTest do
|
||||
|
||||
test "interval/1" do
|
||||
stream = Stream.interval(10)
|
||||
|
||||
{time_us, value} = :timer.tc(fn -> Enum.take(stream, 5) end)
|
||||
|
||||
assert value == [0, 1, 2, 3, 4]
|
||||
assert time_us >= 50000
|
||||
end
|
||||
|
||||
test "interval/1 with infinity" do
|
||||
stream = Stream.interval(:infinity)
|
||||
spawn(Stream, :run, [stream])
|
||||
end
|
||||
|
||||
test "into/2 and run/1" do
|
||||
Process.put(:stream_cont, [])
|
||||
Process.put(:stream_done, false)
|
||||
@@ -1119,6 +1123,11 @@ defmodule StreamTest do
|
||||
assert time_us >= 5000
|
||||
end
|
||||
|
||||
test "timer/1 with infinity" do
|
||||
stream = Stream.timer(:infinity)
|
||||
spawn(Stream, :run, [stream])
|
||||
end
|
||||
|
||||
test "unfold/2" do
|
||||
stream = Stream.unfold(10, fn x -> if x > 0, do: {x, x - 1} end)
|
||||
assert Enum.take(stream, 5) == [10, 9, 8, 7, 6]
|
||||
|
||||
@@ -749,4 +749,79 @@ defmodule StringTest do
|
||||
assert String.myers_difference("abc", "aйbc") == [eq: "a", ins: "й", eq: "bc"]
|
||||
assert String.myers_difference("aйbc", "abc") == [eq: "a", del: "й", eq: "bc"]
|
||||
end
|
||||
|
||||
test "normalize/2" do
|
||||
assert String.normalize("ŝ", :nfd) == "ŝ"
|
||||
assert String.normalize("ḇravô", :nfd) == "ḇravô"
|
||||
assert String.normalize("ṩierra", :nfd) == "ṩierra"
|
||||
assert String.normalize("뢴", :nfd) == "뢴"
|
||||
assert String.normalize("êchǭ", :nfc) == "êchǭ"
|
||||
assert String.normalize("거̄", :nfc) == "거̄"
|
||||
assert String.normalize("뢴", :nfc) == "뢴"
|
||||
|
||||
## Error cases
|
||||
assert String.normalize(<<15, 216>>, :nfc) == <<15, 216>>
|
||||
assert String.normalize(<<15, 216>>, :nfd) == <<15, 216>>
|
||||
assert String.normalize(<<216, 15>>, :nfc) == <<216, 15>>
|
||||
assert String.normalize(<<216, 15>>, :nfd) == <<216, 15>>
|
||||
|
||||
## Cases from NormalizationTest.txt
|
||||
|
||||
# 05B8 05B9 05B1 0591 05C3 05B0 05AC 059F
|
||||
# 05B1 05B8 05B9 0591 05C3 05B0 05AC 059F
|
||||
# HEBREW POINT QAMATS, HEBREW POINT HOLAM, HEBREW POINT HATAF SEGOL,
|
||||
# HEBREW ACCENT ETNAHTA, HEBREW PUNCTUATION SOF PASUQ, HEBREW POINT SHEVA,
|
||||
# HEBREW ACCENT ILUY, HEBREW ACCENT QARNEY PARA
|
||||
assert String.normalize("ֱָֹ֑׃ְ֬֟", :nfc) == "ֱָֹ֑׃ְ֬֟"
|
||||
|
||||
# 095D (exclusion list)
|
||||
# 0922 093C
|
||||
# DEVANAGARI LETTER RHA
|
||||
assert String.normalize("ढ़", :nfc) == "ढ़"
|
||||
|
||||
# 0061 0315 0300 05AE 0340 0062
|
||||
# 00E0 05AE 0300 0315 0062
|
||||
# LATIN SMALL LETTER A, COMBINING COMMA ABOVE RIGHT, COMBINING GRAVE ACCENT,
|
||||
# HEBREW ACCENT ZINOR, COMBINING GRAVE TONE MARK, LATIN SMALL LETTER B
|
||||
assert String.normalize("à֮̀̕b", :nfc) == "à֮̀̕b"
|
||||
|
||||
# 0344
|
||||
# 0308 0301
|
||||
# COMBINING GREEK DIALYTIKA TONOS
|
||||
assert String.normalize("\u0344", :nfc) == "\u0308\u0301"
|
||||
|
||||
# 115B9 0334 115AF
|
||||
# 115B9 0334 115AF
|
||||
# SIDDHAM VOWEL SIGN AI, COMBINING TILDE OVERLAY, SIDDHAM VOWEL SIGN AA
|
||||
assert String.normalize("𑖹̴𑖯", :nfc) == "𑖹̴𑖯"
|
||||
# HEBREW ACCENT ETNAHTA, HEBREW PUNCTUATION SOF PASUQ, HEBREW POINT SHEVA,
|
||||
# HEBREW ACCENT ILUY, HEBREW ACCENT QARNEY PARA
|
||||
assert String.normalize("ֱָֹ֑׃ְ֬֟", :nfc) == "ֱָֹ֑׃ְ֬֟"
|
||||
|
||||
# 095D (exclusion list)
|
||||
# HEBREW ACCENT ETNAHTA, HEBREW PUNCTUATION SOF PASUQ, HEBREW POINT SHEVA,
|
||||
# HEBREW ACCENT ILUY, HEBREW ACCENT QARNEY PARA
|
||||
assert String.normalize("ֱָֹ֑׃ְ֬֟", :nfc) == "ֱָֹ֑׃ְ֬֟"
|
||||
|
||||
# 095D (exclusion list)
|
||||
# 0922 093C
|
||||
# DEVANAGARI LETTER RHA
|
||||
assert String.normalize("ढ़", :nfc) == "ढ़"
|
||||
|
||||
# 0061 0315 0300 05AE 0340 0062
|
||||
# 00E0 05AE 0300 0315 0062
|
||||
# LATIN SMALL LETTER A, COMBINING COMMA ABOVE RIGHT, COMBINING GRAVE ACCENT,
|
||||
# HEBREW ACCENT ZINOR, COMBINING GRAVE TONE MARK, LATIN SMALL LETTER B
|
||||
assert String.normalize("à֮̀̕b", :nfc) == "à֮̀̕b"
|
||||
|
||||
# 0344
|
||||
# 0308 0301
|
||||
# COMBINING GREEK DIALYTIKA TONOS
|
||||
assert String.normalize("\u0344", :nfc) == "\u0308\u0301"
|
||||
|
||||
# 115B9 0334 115AF
|
||||
# 115B9 0334 115AF
|
||||
# SIDDHAM VOWEL SIGN AI, COMBINING TILDE OVERLAY, SIDDHAM VOWEL SIGN AA
|
||||
assert String.normalize("𑖹̴𑖯", :nfc) == "𑖹̴𑖯"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -584,6 +584,21 @@ defmodule TaskTest do
|
||||
[ok: :ok] = Task.async_stream([1], fn _ -> :ok end, timeout: :infinity) |> Enum.to_list()
|
||||
end
|
||||
|
||||
test "streams with fake down messages on the inbox" do
|
||||
parent = self()
|
||||
|
||||
assert Task.async_stream([:ok], fn :ok ->
|
||||
{:links, links} = Process.info(self(), :links)
|
||||
|
||||
for link <- links do
|
||||
send(link, {:DOWN, make_ref(), :process, parent, :oops})
|
||||
end
|
||||
|
||||
:ok
|
||||
end)
|
||||
|> Enum.to_list() == [ok: :ok]
|
||||
end
|
||||
|
||||
test "with $callers" do
|
||||
grandparent = self()
|
||||
|
||||
|
||||
@@ -57,7 +57,8 @@ defmodule ExUnit.Diff do
|
||||
end
|
||||
|
||||
defp diff_quoted({name, _, context} = left, right, env)
|
||||
when is_atom(name) and is_atom(context) do
|
||||
when is_atom(name) and is_atom(context) and
|
||||
name not in [:__MODULE__, :__DIR__, :__STACKTRACE__, :__ENV__, :__CALLER__] do
|
||||
diff_var(left, right, env)
|
||||
end
|
||||
|
||||
@@ -81,20 +82,16 @@ defmodule ExUnit.Diff do
|
||||
diff_tuple(Tuple.to_list(left), Tuple.to_list(right), env)
|
||||
end
|
||||
|
||||
defp diff_quoted({:%, _, [struct, {:%{}, _, kw}]}, %{} = right, env)
|
||||
when is_atom(struct) and is_list(kw) do
|
||||
diff_quoted_struct([__struct__: struct] ++ kw, struct, right, env)
|
||||
defp diff_quoted({:%, _, [struct, {:%{}, _, kw}]}, %{} = right, env) when is_list(kw) do
|
||||
diff_quoted_struct([__struct__: struct] ++ kw, right, env)
|
||||
end
|
||||
|
||||
defp diff_quoted({:%{}, _, items}, %{} = right, env) when is_list(items) do
|
||||
if struct = items[:__struct__] do
|
||||
diff_quoted_struct(items, struct, right, env)
|
||||
else
|
||||
diff_map(items, right, nil, maybe_struct(right), env)
|
||||
end
|
||||
defp diff_quoted({:%{}, _, kw}, %{} = right, env) when is_list(kw) do
|
||||
diff_quoted_struct(kw, right, env)
|
||||
end
|
||||
|
||||
defp diff_quoted({:<>, _, _} = left, right, env) when is_binary(right) do
|
||||
defp diff_quoted({:<>, _, [literal, _]} = left, right, env)
|
||||
when is_binary(literal) and is_binary(right) do
|
||||
diff_string_concat(left, right, env)
|
||||
end
|
||||
|
||||
@@ -225,7 +222,7 @@ defmodule ExUnit.Diff do
|
||||
defp diff_pin({:^, _, [var]} = pin, right, %{pins: pins} = env) do
|
||||
identifier = var_context(var)
|
||||
%{^identifier => pin_value} = pins
|
||||
{diff, post_env} = diff(pin_value, right, env)
|
||||
{diff, post_env} = diff_value(pin_value, right, env)
|
||||
|
||||
diff_left = update_diff_meta(pin, not diff.equivalent?)
|
||||
{%{diff | left: diff_left}, post_env}
|
||||
@@ -626,8 +623,9 @@ defmodule ExUnit.Diff do
|
||||
|
||||
# Structs
|
||||
|
||||
defp diff_quoted_struct(kw, struct1, right, env) do
|
||||
left = load_struct(struct1)
|
||||
defp diff_quoted_struct(kw, right, env) do
|
||||
struct1 = kw[:__struct__]
|
||||
left = load_struct(kw[:__struct__])
|
||||
|
||||
if left && Enum.all?(kw, fn {k, _} -> Map.has_key?(left, k) end) do
|
||||
if Macro.quoted_literal?(kw) do
|
||||
@@ -674,7 +672,8 @@ defmodule ExUnit.Diff do
|
||||
end
|
||||
|
||||
defp load_struct(struct) do
|
||||
if Code.ensure_loaded?(struct) and function_exported?(struct, :__struct__, 0) do
|
||||
if is_atom(struct) and struct != nil and
|
||||
Code.ensure_loaded?(struct) and function_exported?(struct, :__struct__, 0) do
|
||||
struct.__struct__
|
||||
end
|
||||
end
|
||||
@@ -761,11 +760,10 @@ defmodule ExUnit.Diff do
|
||||
String.bag_distance(left, right) > 0.4
|
||||
end
|
||||
|
||||
defp parse_string({:<>, _, [literal, rest]}) do
|
||||
defp parse_string({:<>, _, [literal, rest]}) when is_binary(literal) do
|
||||
{parsed, quoted, indexes, parsed_length} = parse_string(rest)
|
||||
literal_length = String.length(literal)
|
||||
length = literal_length + parsed_length
|
||||
|
||||
{literal <> parsed, quoted, [literal_length | indexes], length}
|
||||
end
|
||||
|
||||
|
||||
@@ -338,7 +338,7 @@ defmodule ExUnit.Formatter do
|
||||
{:match, pins},
|
||||
formatter,
|
||||
padding_size + 5,
|
||||
width - 5
|
||||
width
|
||||
)
|
||||
|
||||
[
|
||||
|
||||
@@ -375,6 +375,7 @@ defmodule ExUnit.AssertionsTest do
|
||||
|
||||
"assert_received({:status, ^status, ^status})" = Macro.to_string(error.expr)
|
||||
"{:status, ^status, ^status}" = Macro.to_string(error.left)
|
||||
"\n\nAssertion failed" <> _ = Exception.message(error)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -141,6 +141,11 @@ defmodule ExUnit.DiffTest do
|
||||
refute_diff(^b = :a, "-^b-", "+:a+", pins)
|
||||
end
|
||||
|
||||
test "pseudo vars" do
|
||||
assert_diff(__MODULE__ = ExUnit.DiffTest, [])
|
||||
refute_diff(__MODULE__ = SomethingElse, "-__MODULE__-", "+SomethingElse+")
|
||||
end
|
||||
|
||||
test "integers" do
|
||||
assert_diff(123 = 123, [])
|
||||
assert_diff(-123 = -123, [])
|
||||
@@ -471,6 +476,34 @@ defmodule ExUnit.DiffTest do
|
||||
refute_diff(%{a: 1} = :a, "-%{a: 1}-", "+:a+")
|
||||
end
|
||||
|
||||
test "maps as pinned map value" do
|
||||
user = %{"id" => 13, "name" => "john"}
|
||||
|
||||
notification = %{
|
||||
"user" => user,
|
||||
"subtitle" => "foo"
|
||||
}
|
||||
|
||||
assert_diff(
|
||||
%{
|
||||
"user" => ^user,
|
||||
"subtitle" => "foo"
|
||||
} = notification,
|
||||
[],
|
||||
%{{:user, nil} => user}
|
||||
)
|
||||
|
||||
refute_diff(
|
||||
%{
|
||||
"user" => ^user,
|
||||
"subtitle" => "bar"
|
||||
} = notification,
|
||||
~s|%{"subtitle" => "-bar-", "user" => ^user}|,
|
||||
~s|%{"subtitle" => "+foo+", "user" => %{"id" => 13, "name" => "john"}}|,
|
||||
%{{:user, nil} => user}
|
||||
)
|
||||
end
|
||||
|
||||
test "maps outside match context" do
|
||||
assert_diff(%{a: 1} == %{a: 1}, [])
|
||||
assert_diff(%{a: 1, b: 2} == %{a: 1, b: 2}, [])
|
||||
@@ -594,7 +627,7 @@ defmodule ExUnit.DiffTest do
|
||||
)
|
||||
end
|
||||
|
||||
test "structs with inspect difference" do
|
||||
test "structs with same inspect but different" do
|
||||
refute_diff(
|
||||
%Opaque{data: 1} = %Opaque{data: 2},
|
||||
"%ExUnit.DiffTest.Opaque{data: -1-}",
|
||||
@@ -608,7 +641,7 @@ defmodule ExUnit.DiffTest do
|
||||
)
|
||||
end
|
||||
|
||||
test "structs without inspect difference outside match" do
|
||||
test "structs with same inspect but different outside match" do
|
||||
refute_diff(
|
||||
%Opaque{data: 1} == %Opaque{data: 2},
|
||||
"%ExUnit.DiffTest.Opaque{data: -1-}",
|
||||
@@ -630,6 +663,66 @@ defmodule ExUnit.DiffTest do
|
||||
)
|
||||
end
|
||||
|
||||
test "structs with matched type" do
|
||||
pins = %{{:type, nil} => User, {:age, nil} => 33}
|
||||
|
||||
# pin on __struct__
|
||||
assert_diff(
|
||||
%{__struct__: ^type, age: ^age, name: "john"} = %User{name: "john", age: 33},
|
||||
[],
|
||||
pins
|
||||
)
|
||||
|
||||
refute_diff(
|
||||
%{__struct__: ^type, age: ^age, name: "john"} = %User{name: "jane", age: 33},
|
||||
"%{__struct__: ^type, age: ^age, name: \"j-oh-n\"}",
|
||||
"%ExUnit.DiffTest.User{age: 33, name: \"j+a+n+e+\"}",
|
||||
pins
|
||||
)
|
||||
|
||||
refute_diff(
|
||||
%{__struct__: ^type, age: ^age, name: "john"} = %User{name: "john", age: 35},
|
||||
"%{__struct__: ^type, age: -^age-, name: \"john\"}",
|
||||
"%ExUnit.DiffTest.User{age: 3+5+, name: \"john\"}",
|
||||
pins
|
||||
)
|
||||
|
||||
refute_diff(
|
||||
%{__struct__: ^type, age: ^age, name: "john"} = ~D[2020-01-01],
|
||||
"%{__struct__: -^type-, -age: ^age-, -name: \"john\"-}",
|
||||
"%+Date+{calendar: Calendar.ISO, day: 1, month: 1, year: 2020}",
|
||||
pins
|
||||
)
|
||||
|
||||
# pin on %
|
||||
assert_diff(
|
||||
%^type{age: ^age, name: "john"} = %User{name: "john", age: 33},
|
||||
[],
|
||||
pins
|
||||
)
|
||||
|
||||
refute_diff(
|
||||
%^type{age: ^age, name: "john"} = %User{name: "jane", age: 33},
|
||||
"%{__struct__: ^type, age: ^age, name: \"j-oh-n\"}",
|
||||
"%ExUnit.DiffTest.User{age: 33, name: \"j+a+n+e+\"}",
|
||||
pins
|
||||
)
|
||||
|
||||
refute_diff(
|
||||
%^type{age: ^age, name: "john"} = %User{name: "john", age: 35},
|
||||
"%{__struct__: ^type, age: -^age-, name: \"john\"}",
|
||||
"%ExUnit.DiffTest.User{age: 3+5+, name: \"john\"}",
|
||||
pins
|
||||
)
|
||||
|
||||
refute_diff(
|
||||
%^type{age: ^age, name: "john"} = ~D[2020-01-01],
|
||||
"%{__struct__: -^type-, -age: ^age-, -name: \"john\"-}",
|
||||
"%+Date+{calendar: Calendar.ISO, day: 1, month: 1, year: 2020}",
|
||||
pins
|
||||
)
|
||||
end
|
||||
|
||||
test "invalid structs" do
|
||||
refute_diff(
|
||||
%{__struct__: Unknown} = %{},
|
||||
@@ -721,7 +814,7 @@ defmodule ExUnit.DiffTest do
|
||||
)
|
||||
end
|
||||
|
||||
test "concat operator" do
|
||||
test "concat binaries" do
|
||||
assert_diff("fox hops" <> " over the dog" = "fox hops over the dog", [])
|
||||
assert_diff("fox hops " <> "over " <> "the dog" = "fox hops over the dog", [])
|
||||
|
||||
@@ -755,6 +848,10 @@ defmodule ExUnit.DiffTest do
|
||||
~s/"fox hops over the dog"/
|
||||
)
|
||||
|
||||
refute_diff("fox" <> " hops" = :a, ~s/-"fox" <> " hops"-/, "+:a+")
|
||||
end
|
||||
|
||||
test "concat binaries with pin" do
|
||||
pins = %{{:x, nil} => " over the dog"}
|
||||
|
||||
assert_diff("fox hops" <> x = "fox hops over the dog", x: " over the dog")
|
||||
@@ -773,8 +870,16 @@ defmodule ExUnit.DiffTest do
|
||||
~s/"fox hops over +t+he dog"/,
|
||||
pins
|
||||
)
|
||||
end
|
||||
|
||||
refute_diff("fox" <> " hops" = :a, ~s/-"fox" <> " hops"-/, "+:a+")
|
||||
test "concat binaries with specifiers" do
|
||||
input = "foobar"
|
||||
|
||||
refute_diff(
|
||||
<<trap::binary-size(3)>> <> "baz" = input,
|
||||
"-<<trap::binary-size(3)>> <> \"baz\"-",
|
||||
"+\"foobar\"+"
|
||||
)
|
||||
end
|
||||
|
||||
test "underscore" do
|
||||
|
||||
@@ -382,7 +382,7 @@ defmodule IEx.HelpersTest do
|
||||
"* def left == right\n\n @spec term() == term() :: boolean()\n\nguard: true\n\nReturns `true` if the two terms are equal.\n\n"
|
||||
|
||||
def_h =
|
||||
"* defmacro def(call, expr \\\\ [])\n\nDefines a public function with the given name and body."
|
||||
"* defmacro def(call, expr \\\\ nil)\n\nDefines a public function with the given name and body."
|
||||
|
||||
assert capture_io(fn -> h(IEx.Helpers.pwd() / 0) end) =~ pwd_h
|
||||
assert capture_io(fn -> h(IEx.Helpers.c() / 2) end) =~ c_h
|
||||
|
||||
@@ -32,14 +32,6 @@ defmodule Logger.Config do
|
||||
Application.fetch_env!(:logger, :discard_threshold_periodic_check)}
|
||||
end
|
||||
|
||||
def handle_event({_type, gl, _msg} = event, state) when node(gl) != node() do
|
||||
# Cross node messages are always async which also
|
||||
# means this handler won't crash in case Logger
|
||||
# is not installed in the other node.
|
||||
:gen_event.notify({Logger, node(gl)}, event)
|
||||
{:ok, update_counter(state, false)}
|
||||
end
|
||||
|
||||
def handle_event(_event, state) do
|
||||
{:ok, update_counter(state, false)}
|
||||
end
|
||||
|
||||
@@ -168,22 +168,42 @@ defmodule Logger.Formatter do
|
||||
defp levelpad(:warn), do: " "
|
||||
defp levelpad(:error), do: ""
|
||||
|
||||
defp metadata([{:report_cb, _} | metadata]), do: metadata(metadata)
|
||||
defp metadata([{:time, _} | metadata]), do: metadata(metadata)
|
||||
defp metadata([{:gl, _} | metadata]), do: metadata(metadata)
|
||||
defp metadata([{:crash_reason, _} | metadata]), do: metadata(metadata)
|
||||
defp metadata([{:ancestors, _} | metadata]), do: metadata(metadata)
|
||||
defp metadata([{:callers, _} | metadata]), do: metadata(metadata)
|
||||
defp metadata([{_, nil} | metadata]), do: metadata(metadata)
|
||||
|
||||
defp metadata([{key, value} | metadata]) do
|
||||
[to_string(key), ?=, metadata(key, value), ?\s | metadata(metadata)]
|
||||
if formatted = metadata(key, value) do
|
||||
[to_string(key), ?=, formatted, ?\s | metadata(metadata)]
|
||||
else
|
||||
metadata(metadata)
|
||||
end
|
||||
end
|
||||
|
||||
defp metadata([]) do
|
||||
[]
|
||||
end
|
||||
|
||||
defp metadata(:time, _), do: nil
|
||||
defp metadata(:gl, _), do: nil
|
||||
|
||||
defp metadata(_, nil), do: nil
|
||||
defp metadata(_, string) when is_binary(string), do: string
|
||||
defp metadata(_, integer) when is_integer(integer), do: Integer.to_string(integer)
|
||||
defp metadata(_, float) when is_float(float), do: Float.to_string(float)
|
||||
defp metadata(_, pid) when is_pid(pid), do: :erlang.pid_to_list(pid)
|
||||
|
||||
defp metadata(_, atom) when is_atom(atom) do
|
||||
case Atom.to_string(atom) do
|
||||
"Elixir." <> rest -> rest
|
||||
"nil" -> ""
|
||||
binary -> binary
|
||||
end
|
||||
end
|
||||
|
||||
defp metadata(_, ref) when is_reference(ref) do
|
||||
'#Ref' ++ rest = :erlang.ref_to_list(ref)
|
||||
rest
|
||||
end
|
||||
|
||||
defp metadata(:file, file) when is_list(file), do: file
|
||||
|
||||
defp metadata(:domain, [head | tail]) when is_atom(head) do
|
||||
Enum.map_intersperse([head | tail], ?., &Atom.to_string/1)
|
||||
end
|
||||
@@ -198,22 +218,5 @@ defmodule Logger.Formatter do
|
||||
Exception.format_mfa(mod, fun, arity)
|
||||
end
|
||||
|
||||
defp metadata(_, pid) when is_pid(pid) do
|
||||
:erlang.pid_to_list(pid)
|
||||
end
|
||||
|
||||
defp metadata(_, ref) when is_reference(ref) do
|
||||
'#Ref' ++ rest = :erlang.ref_to_list(ref)
|
||||
rest
|
||||
end
|
||||
|
||||
defp metadata(_, atom) when is_atom(atom) do
|
||||
case Atom.to_string(atom) do
|
||||
"Elixir." <> rest -> rest
|
||||
"nil" -> ""
|
||||
binary -> binary
|
||||
end
|
||||
end
|
||||
|
||||
defp metadata(_, other), do: to_string(other)
|
||||
defp metadata(_, _), do: nil
|
||||
end
|
||||
|
||||
@@ -230,7 +230,7 @@ defmodule Logger.Handler do
|
||||
translate_fallback(:format, callback.(data), meta, truncate)
|
||||
end
|
||||
|
||||
defp translate_fallback(:report, {:logger, data}, %{report_cb: callback} = meta, truncate)
|
||||
defp translate_fallback(:report, {:logger, data}, %{report_cb: callback}, _truncate)
|
||||
when is_function(callback, 2) do
|
||||
translator_opts =
|
||||
struct(Inspect.Opts, Application.fetch_env!(:logger, :translator_inspect_opts))
|
||||
@@ -241,7 +241,7 @@ defmodule Logger.Handler do
|
||||
single_line: false
|
||||
}
|
||||
|
||||
translate_fallback(:format, callback.(data, opts), meta, truncate)
|
||||
callback.(data, opts)
|
||||
end
|
||||
|
||||
defp translate_fallback(:format, {format, args}, _meta, truncate) do
|
||||
|
||||
@@ -81,7 +81,7 @@ defmodule Logger.Utils do
|
||||
end
|
||||
|
||||
defp fix_binary(<<h, t::binary>>, acc) do
|
||||
fix_binary(t, <<h, acc::binary>>)
|
||||
fix_binary(t, <<acc::binary, h>>)
|
||||
end
|
||||
|
||||
defp fix_binary(<<>>, _acc) do
|
||||
|
||||
@@ -77,9 +77,9 @@ defmodule Logger.FormatterTest do
|
||||
"2014-12-30 12:06:30.100"
|
||||
end
|
||||
|
||||
test "format discards callers/ancestors/crash_reason" do
|
||||
test "format discards unknown formats" do
|
||||
compiled = compile("$metadata $message")
|
||||
metadata = [callers: :discard, ancestors: :discard, crash_reason: :discard, foo: :bar]
|
||||
metadata = [ancestors: [self()], crash_reason: {:some, :tuple}, foo: :bar]
|
||||
|
||||
assert format(compiled, :error, "hello", nil, metadata) ==
|
||||
[["foo", 61, "bar", 32], " ", "hello"]
|
||||
|
||||
@@ -147,6 +147,6 @@ defmodule Logger.HandlerTest do
|
||||
defp format_report(report, opts) do
|
||||
send(self(), {:format, report, opts})
|
||||
|
||||
{'~p', [report]}
|
||||
inspect(report)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -23,6 +23,7 @@ defmodule Logger.UtilsTest do
|
||||
assert truncate("olá", 4) == "olá"
|
||||
assert truncate("ááááá:", 10) == ["ááááá", " (truncated)"]
|
||||
assert truncate("áááááá:", 10) == ["ááááá", " (truncated)"]
|
||||
assert truncate("𠜎𠜱𠝹𠱓", 15) == ["𠜎𠜱𠝹", " (truncated)"]
|
||||
|
||||
# Charlists
|
||||
assert truncate('olá', 2) == ['olá', " (truncated)"]
|
||||
|
||||
@@ -37,10 +37,10 @@ defmodule Mix.CLI do
|
||||
file = System.get_env("MIX_EXS") || "mix.exs"
|
||||
|
||||
if File.regular?(file) do
|
||||
old_value = Code.get_compiler_option(:no_warn_undefined)
|
||||
old_undefined = Code.get_compiler_option(:no_warn_undefined)
|
||||
Code.put_compiler_option(:no_warn_undefined, :all)
|
||||
Code.compile_file(file)
|
||||
Code.put_compiler_option(:no_warn_undefined, old_value)
|
||||
Code.put_compiler_option(:no_warn_undefined, old_undefined)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ defmodule Mix.Dep do
|
||||
scm: Mix.SCM.t(),
|
||||
app: atom,
|
||||
requirement: String.t() | Regex.t() | nil,
|
||||
status: atom,
|
||||
status: {:ok, String.t() | nil} | atom | tuple,
|
||||
opts: keyword,
|
||||
top_level: boolean,
|
||||
manager: :rebar | :rebar3 | :mix | :make | nil,
|
||||
|
||||
@@ -65,6 +65,7 @@ defmodule Mix.Dep.Fetcher do
|
||||
end
|
||||
|
||||
if new do
|
||||
File.touch!(Path.join(opts[:dest], ".fetch"))
|
||||
dep = put_in(dep.opts[:lock], new)
|
||||
{dep, [app | acc], Map.put(lock, app, new)}
|
||||
else
|
||||
@@ -92,22 +93,18 @@ defmodule Mix.Dep.Fetcher do
|
||||
# dependency is missing, it could directly affect one of the
|
||||
# dependencies we are trying to compile, causing the whole thing
|
||||
# to fail.
|
||||
#
|
||||
# If there is any other dependency that is not ok, we include
|
||||
# it for compilation too, this is our best to try to solve the
|
||||
# maximum we can at each deps.get and deps.update.
|
||||
deps =
|
||||
parent_deps =
|
||||
if Enum.all?(all_deps, &available?/1) do
|
||||
Enum.uniq_by(with_depending(deps, all_deps), & &1.app)
|
||||
else
|
||||
deps
|
||||
[]
|
||||
end
|
||||
|
||||
# Merge the new lock on top of the old to guarantee we don't
|
||||
# leave out things that could not be fetched and save it.
|
||||
lock = Map.merge(old_lock, new_lock)
|
||||
Mix.Dep.Lock.write(lock)
|
||||
mark_as_fetched(deps)
|
||||
mark_as_fetched(parent_deps)
|
||||
|
||||
# See if any of the deps diverged and abort.
|
||||
show_diverged!(Enum.filter(all_deps, &Mix.Dep.diverged?/1))
|
||||
|
||||
@@ -18,7 +18,7 @@ defmodule Mix.Generator do
|
||||
|
||||
iex> Mix.Generator.create_file(".gitignore", "_build\ndeps\n")
|
||||
* creating .gitignore
|
||||
:ok
|
||||
true
|
||||
|
||||
"""
|
||||
@spec create_file(Path.t(), iodata, keyword) :: boolean()
|
||||
@@ -48,7 +48,7 @@ defmodule Mix.Generator do
|
||||
|
||||
iex> Mix.Generator.create_directory("path/to/dir")
|
||||
* creating path/to/dir
|
||||
:ok
|
||||
true
|
||||
|
||||
"""
|
||||
@spec create_directory(Path.t(), keyword) :: true
|
||||
@@ -73,7 +73,7 @@ defmodule Mix.Generator do
|
||||
|
||||
iex> Mix.Generator.copy_file("source/gitignore", ".gitignore", "_build\ndeps\n")
|
||||
* creating .gitignore
|
||||
:ok
|
||||
true
|
||||
|
||||
"""
|
||||
@doc since: "1.9.0"
|
||||
@@ -99,7 +99,7 @@ defmodule Mix.Generator do
|
||||
|
||||
iex> Mix.Generator.copy_template("source/gitignore", ".gitignore", [project_path: path])
|
||||
* creating .gitignore
|
||||
:ok
|
||||
true
|
||||
|
||||
"""
|
||||
@doc since: "1.9.0"
|
||||
|
||||
@@ -721,15 +721,17 @@ defmodule Mix.Project do
|
||||
|
||||
{new_proj, file} =
|
||||
if File.regular?(file) do
|
||||
old_undefined = Code.get_compiler_option(:no_warn_undefined)
|
||||
|
||||
try do
|
||||
Code.compiler_options(relative_paths: false)
|
||||
Code.compiler_options(relative_paths: false, no_warn_undefined: :all)
|
||||
_ = Code.compile_file(file)
|
||||
get()
|
||||
else
|
||||
^old_proj -> Mix.raise("Could not find a Mix project at #{file}")
|
||||
new_proj -> {new_proj, file}
|
||||
after
|
||||
Code.compiler_options(relative_paths: true)
|
||||
Code.compiler_options(relative_paths: true, no_warn_undefined: old_undefined)
|
||||
end
|
||||
else
|
||||
push(nil, file, app)
|
||||
|
||||
+20
-19
@@ -425,13 +425,29 @@ defmodule Mix.Release do
|
||||
opts = [
|
||||
extra_config: initial_config,
|
||||
prune_after_boot: prune_after_boot,
|
||||
reboot_after_config: reboot?
|
||||
reboot_after_config: reboot?,
|
||||
validate_compile_env: validate_compile_env(release)
|
||||
]
|
||||
|
||||
init = Config.Provider.init(release.config_providers, config_path, opts)
|
||||
{Config.Reader.merge(sys_config, [elixir: [config_providers: init]] ++ extra_config), reboot?}
|
||||
end
|
||||
|
||||
defp validate_compile_env(release) do
|
||||
with true <- Keyword.get(release.options, :validate_compile_env, true),
|
||||
[_ | _] = compile_env <- compile_env(release) do
|
||||
compile_env
|
||||
else
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
|
||||
defp compile_env(release) do
|
||||
for {_, properties} <- release.applications,
|
||||
triplet <- Keyword.get(properties, :compile_env, []),
|
||||
do: triplet
|
||||
end
|
||||
|
||||
defp start_distribution(%{options: opts}) do
|
||||
reboot? = Keyword.get(opts, :reboot_system_after_config, true)
|
||||
early_distribution? = Keyword.get(opts, :start_distribution_during_config, false)
|
||||
@@ -613,8 +629,7 @@ defmodule Mix.Release do
|
||||
&(not match?({:apply, {:application, :start_boot, [:stdlib, _]}}, &1))
|
||||
)
|
||||
|
||||
pre ++
|
||||
[stdlib] ++ config_provider_apply(release) ++ validate_compile_env_apply(release) ++ post
|
||||
pre ++ [stdlib] ++ config_provider_apply(release) ++ post
|
||||
end
|
||||
|
||||
defp config_provider_apply(%{config_providers: []}),
|
||||
@@ -623,21 +638,6 @@ defmodule Mix.Release do
|
||||
defp config_provider_apply(_),
|
||||
do: [{:apply, {Config.Provider, :boot, [:elixir, :config_providers]}}]
|
||||
|
||||
defp validate_compile_env_apply(release) do
|
||||
with true <- Keyword.get(release.options, :validate_compile_env, true),
|
||||
[_ | _] = compile_env <- compile_env(release) do
|
||||
[{:apply, {Config.Provider, :validate_compile_env, [compile_env]}}]
|
||||
else
|
||||
_ -> []
|
||||
end
|
||||
end
|
||||
|
||||
defp compile_env(release) do
|
||||
for {_, properties} <- release.applications,
|
||||
triplet <- Keyword.get(properties, :compile_env, []),
|
||||
do: triplet
|
||||
end
|
||||
|
||||
defp prepend_paths_to_script(instructions, []), do: instructions
|
||||
|
||||
defp prepend_paths_to_script(instructions, prepend_paths) do
|
||||
@@ -672,9 +672,10 @@ defmodule Mix.Release do
|
||||
|
||||
def copy_erts(release) do
|
||||
destination = Path.join(release.path, "erts-#{release.erts_version}")
|
||||
erts_source_contents = File.ls!(release.erts_source)
|
||||
File.mkdir_p!(destination)
|
||||
|
||||
for dir <- ~w(bin include lib src) do
|
||||
for dir <- ~w(bin include lib src), dir in erts_source_contents do
|
||||
source = Path.join(release.erts_source, dir)
|
||||
target = Path.join(destination, dir)
|
||||
File.cp_r!(source, target, fn _, _ -> false end)
|
||||
|
||||
@@ -481,7 +481,9 @@ defmodule Mix.Tasks.Release do
|
||||
will receive a `Mix.Release` struct and must return the same or
|
||||
an updated `Mix.Release` struct. It is also possible to build a tarball
|
||||
of the release by passing the `:tar` step anywhere after `:assemble`.
|
||||
The tarball is created in `_build/MIX_ENV/RELEASE_NAME-RELEASE_VSN.tar.gz`
|
||||
If the release `:path` is not configured, the tarball is created in
|
||||
`_build/MIX_ENV/RELEASE_NAME-RELEASE_VSN.tar.gz` Otherwise it is
|
||||
created inside the configured `:path`.
|
||||
|
||||
See `Mix.Release` for more documentation on the struct and which
|
||||
fields can be modified. Note that `:steps` field itself can be
|
||||
@@ -1065,8 +1067,16 @@ defmodule Mix.Tasks.Release do
|
||||
end
|
||||
|
||||
defp make_tar(release) do
|
||||
tar_filename = "#{release.name}-#{release.version}.tar.gz"
|
||||
out_path = Path.join([release.path, "..", "..", tar_filename]) |> Path.expand()
|
||||
build_path = Mix.Project.build_path()
|
||||
|
||||
dir_path =
|
||||
if release.path == Path.join([build_path, "rel", Atom.to_string(release.name)]) do
|
||||
build_path
|
||||
else
|
||||
release.path
|
||||
end
|
||||
|
||||
out_path = Path.join(dir_path, "#{release.name}-#{release.version}.tar.gz")
|
||||
info(release, [:green, "* building ", :reset, out_path])
|
||||
|
||||
lib_dirs =
|
||||
@@ -1075,14 +1085,20 @@ defmodule Mix.Tasks.Release do
|
||||
[Path.join("lib", "#{name}-#{vsn}") | acc]
|
||||
end)
|
||||
|
||||
erts_dir =
|
||||
case release.erts_source do
|
||||
nil -> []
|
||||
_ -> ["erts-#{release.erts_version}"]
|
||||
end
|
||||
|
||||
release_files =
|
||||
for basename <- File.ls!(Path.join(release.path, "releases")),
|
||||
not File.dir?(Path.join([release.path, "releases", basename])),
|
||||
do: Path.join("releases", basename)
|
||||
|
||||
dirs =
|
||||
["bin", Path.join("releases", release.version), "erts-#{release.erts_version}"] ++
|
||||
lib_dirs ++ release_files
|
||||
["bin", Path.join("releases", release.version)] ++
|
||||
erts_dir ++ lib_dirs ++ release_files
|
||||
|
||||
files =
|
||||
dirs
|
||||
|
||||
@@ -416,9 +416,9 @@ defmodule Mix.Tasks.Release.Init do
|
||||
goto end
|
||||
)
|
||||
|
||||
!ERLSRV! add !RELEASE_NAME!_!RELEASE_NAME! ^
|
||||
"!ERLSRV!" add "!RELEASE_NAME!_!RELEASE_NAME!" ^
|
||||
-!RELEASE_DISTRIBUTION! "!RELEASE_NODE!" ^
|
||||
-env RELEASE_ROOT=!RELEASE_ROOT! -env RELEASE_NAME=!RELEASE_NAME! -env RELEASE_VSN=!RELEASE_VSN! -env RELEASE_COOKIE=!RELEASE_COOKIE! -env RELEASE_NODE=!RELEASE_NODE! -env RELEASE_VM_ARGS=!RELEASE_VM_ARGS! -env RELEASE_TMP=!RELEASE_TMP! -env RELEASE_SYS_CONFIG=!RELEASE_SYS_CONFIG! ^
|
||||
-env RELEASE_ROOT="!RELEASE_ROOT!" -env RELEASE_NAME="!RELEASE_NAME!" -env RELEASE_VSN="!RELEASE_VSN!" -env RELEASE_COOKIE="!RELEASE_COOKIE!" -env RELEASE_NODE="!RELEASE_NODE!" -env RELEASE_VM_ARGS="!RELEASE_VM_ARGS!" -env RELEASE_TMP="!RELEASE_TMP!" -env RELEASE_SYS_CONFIG="!RELEASE_SYS_CONFIG!" ^
|
||||
-args "-setcookie !RELEASE_COOKIE! -config !RELEASE_SYS_CONFIG! -mode !RELEASE_MODE! -boot !REL_VSN_DIR!\start -boot_var RELEASE_LIB !RELEASE_ROOT!\lib -args_file !REL_VSN_DIR!\vm.args"
|
||||
|
||||
if %ERRORLEVEL% EQU 0 (
|
||||
|
||||
@@ -229,15 +229,17 @@ defmodule Mix.Tasks.Xref do
|
||||
end
|
||||
|
||||
# Mod.fun(...)
|
||||
defp walk_expr({{:., meta, [module, fun]}, _, args}, state)
|
||||
defp walk_expr({{:., _, [module, fun]}, meta, args}, state)
|
||||
when is_atom(module) and is_atom(fun) do
|
||||
add_call(module, fun, length(args), meta, state)
|
||||
state = add_call(module, fun, length(args), meta, state)
|
||||
walk_expr(args, state)
|
||||
end
|
||||
|
||||
# %Module{...}
|
||||
defp walk_expr({:%, meta, [module, {:%{}, _meta, args}]}, state)
|
||||
when is_atom(module) and is_list(args) do
|
||||
add_call(module, :__struct__, 0, meta, state)
|
||||
state = add_call(module, :__struct__, 0, meta, state)
|
||||
walk_expr(args, state)
|
||||
end
|
||||
|
||||
# Function call
|
||||
|
||||
@@ -201,6 +201,32 @@ defmodule Mix.Tasks.Compile.ElixirTest do
|
||||
end)
|
||||
end
|
||||
|
||||
test "compiles dependent changed modules without beam files" do
|
||||
in_fixture("no_mixfile", fn ->
|
||||
File.write!("lib/b.ex", """
|
||||
defmodule B do
|
||||
def a, do: A.__info__(:module)
|
||||
end
|
||||
""")
|
||||
|
||||
Mix.Tasks.Compile.Elixir.run(["--verbose"])
|
||||
assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]}
|
||||
assert_received {:mix_shell, :info, ["Compiled lib/b.ex"]}
|
||||
|
||||
assert File.regular?("_build/dev/lib/sample/ebin/Elixir.A.beam")
|
||||
assert File.regular?("_build/dev/lib/sample/ebin/Elixir.B.beam")
|
||||
|
||||
Code.put_compiler_option(:ignore_module_conflict, true)
|
||||
Code.compile_file("lib/b.ex")
|
||||
File.touch!("lib/a.ex", {{2038, 1, 1}, {0, 0, 0}})
|
||||
|
||||
Mix.Tasks.Compile.Elixir.run(["--verbose"])
|
||||
assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]}
|
||||
end)
|
||||
after
|
||||
Code.put_compiler_option(:ignore_module_conflict, false)
|
||||
end
|
||||
|
||||
test "compiles dependent changed modules even on removal" do
|
||||
in_fixture("no_mixfile", fn ->
|
||||
File.write!("lib/a.ex", "defmodule A, do: B.module_info()")
|
||||
|
||||
@@ -135,7 +135,7 @@ defmodule Mix.Tasks.ReleaseTest do
|
||||
end
|
||||
|
||||
describe "tar" do
|
||||
test "with ERTS" do
|
||||
test "with default options" do
|
||||
in_fixture("release_test", fn ->
|
||||
config = [releases: [demo: [steps: [:assemble, :tar]]]]
|
||||
|
||||
@@ -186,13 +186,41 @@ defmodule Mix.Tasks.ReleaseTest do
|
||||
end)
|
||||
end
|
||||
|
||||
test "without ERTS" do
|
||||
test "without ERTS and custom path" do
|
||||
in_fixture("release_test", fn ->
|
||||
config = [
|
||||
releases: [demo: [include_erts: false, path: "tmp/rel", steps: [:assemble, :tar]]]
|
||||
]
|
||||
|
||||
Mix.Project.in_project(:release_test, ".", config, fn _ ->
|
||||
Mix.Task.run("release")
|
||||
tar_path = Path.expand(Path.join(["tmp", "rel", "demo-0.1.0.tar.gz"]))
|
||||
message = "* building #{tar_path}"
|
||||
assert_received {:mix_shell, :info, [^message]}
|
||||
assert File.exists?(tar_path)
|
||||
|
||||
{:ok, files} = String.to_charlist(tar_path) |> :erl_tar.table([:compressed])
|
||||
files = Enum.map(files, &to_string/1)
|
||||
|
||||
assert "bin/demo" in files
|
||||
refute Enum.any?(files, &(&1 =~ "erts"))
|
||||
refute Enum.any?(files, &(&1 =~ "stdlib"))
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
test "without ERTS when a previous build included ERTS" do
|
||||
in_fixture("release_test", fn ->
|
||||
config = [releases: [demo: [include_erts: false, steps: [:assemble, :tar]]]]
|
||||
|
||||
Mix.Project.in_project(:release_test, ".", config, fn _ ->
|
||||
root = Path.absname("_build/#{Mix.env()}/rel/demo")
|
||||
|
||||
erts_dir_from_previous_build =
|
||||
Path.absname("_build/#{Mix.env()}/rel/demo/erts-#{@erts_version}")
|
||||
|
||||
File.mkdir_p!(erts_dir_from_previous_build)
|
||||
|
||||
Mix.Task.run("release")
|
||||
tar_path = Path.expand(Path.join([root, "..", "..", "demo-0.1.0.tar.gz"]))
|
||||
message = "* building #{tar_path}"
|
||||
@@ -486,7 +514,18 @@ defmodule Mix.Tasks.ReleaseTest do
|
||||
in_fixture("release_test", fn ->
|
||||
config = [releases: [permanent1: [include_erts: false]]]
|
||||
|
||||
# We write the compile env to guarantee rpc still works
|
||||
File.write!("lib/compile_env.ex", """
|
||||
_ = Application.compile_env(:release_test, :static)
|
||||
""")
|
||||
|
||||
Mix.Project.in_project(:release_test, ".", config, fn _ ->
|
||||
Mix.Task.run("loadconfig", [])
|
||||
|
||||
File.write!("config/releases.exs", """
|
||||
import Config
|
||||
""")
|
||||
|
||||
root = Path.absname("_build/dev/rel/permanent1")
|
||||
Mix.Task.run("release")
|
||||
script = Path.join(root, "bin/permanent1")
|
||||
|
||||
Reference in New Issue
Block a user