Compare commits

...
96 Commits
Author SHA1 Message Date
José Valim 1145dc0168 Release v1.10.4 2020-07-04 13:10:47 +02:00
José Valim 12f93df04f Revert "Add --repo to archive.install and escript.install (#10121)"
This reverts commit e9ea9dea7f.
2020-07-01 15:40:55 +02:00
José Valim 47a63a73b6 Update CHANGELOG 2020-07-01 15:40:51 +02:00
Cleidiano Oliveira d0ebaa7cdd Allow user to define a type named union (#10055) 2020-07-01 15:26:12 +02:00
Wojtek Mach e9ea9dea7f Add --repo to archive.install and escript.install (#10121) 2020-06-22 17:12:45 +02:00
Mitchell Henke 1feef9cb34 include callers in translated Logger metadata for Task (#10111) 2020-06-19 20:51:21 +02:00
Eksperimental 5c5fa57234 Update .cirrus.yml to latest Erlang/OTP versions and changes from master (#10101) 2020-06-15 19:36:55 +02:00
Wojtek Mach e891b1a831 Don't add compile-time dependency on defdelegate (#10093)
The dependency doesn't seem needed in this case.

We go through extra hoops to maintain dynamic invocations where
`opts` is not a compile-time list and :to is not a literal.

An easy way to see this optimization is:

    defmodule A do
      defdelegate f(), to: B
    end

    defmodule B do
      def f() do
        :ok
      end
    end

    $ touch lib/b.ex && mix compile --verbose
    Compiling 1 file (.ex)
    Compiled lib/b.ex

vs:

    defmodule A do
      opts = [to: B]
      defdelegate f(), opts
    end

    defmodule B do
      def f() do
        :ok
      end
    end

    $ touch lib/b.ex && mix compile --verbose
    Compiling 2 files (.ex)
    Compiled lib/b.ex
    Compiled lib/a.ex
2020-06-11 16:02:19 +02:00
José Valim 001a5eb2e8 Add line numbers to deprecated imports 2020-06-11 16:01:46 +02:00
José Valim 6085ed0288 Do not diff guards if pattern does not match 2020-06-04 20:15:35 +02:00
José Valim 57be943f91 Do not raise on foreign types, closes #10077 2020-06-02 15:06:29 +02:00
Waldemar Rachwał 99cad598a8 Fix read of app file located in archive .ez file (#10062) 2020-05-27 17:59:14 +02:00
José Valim ef4c926299 Provide exception blaming to linked and trapped exits in ExUnit 2020-05-22 11:00:46 +02:00
José Valim d1acad0155 Add extra suggestions when Hex can't be installed, closes #10039 2020-05-18 10:05:09 +02:00
Dallin Osmun 4a69a00c71 Report the correct line number when raising inside a macro (#10040) 2020-05-16 09:33:15 +02:00
José Valim a9ad1f1b2a v1.10.3 is compatible with Erlang/OTP 23 2020-05-13 12:54:37 +02:00
Wojtek Mach 53e3d8f55e Add :cd option to Mix.Shell.cmd/2 and --cd to mix cmd (#10029) 2020-05-13 09:48:29 +02:00
Ryan Zidago f53b8c822d Fix Task PID and caller in Task Supervisor reports (#10025)
Closes #10024
2020-05-09 01:14:29 +02:00
José Valim cdd312b0ca Support proper encoding in releases config (#10012) 2020-05-01 20:14:18 +02:00
José Valim 0cc53735a1 Force modules to be loaded when they are required 2020-04-26 11:02:46 +02:00
José Valim c88ecdec16 Release v1.10.3 2020-04-25 10:30:20 +02:00
José Valim 1b4536bf17 Fix improper regex use, closes #9914 2020-04-25 10:25:43 +02:00
José Valim ab4d6817ab Run the formatter 2020-04-24 18:56:40 +02:00
José Valim 717490a1b0 Respect :path when tar-ing releases, closes #9949 2020-04-24 18:11:22 +02:00
José Valim 8cb1361e1a Fix diffing for variable structs, closes #9958 2020-04-23 21:40:14 +02:00
Dave Cottlehuber 967f4b412c Update build to a supported FreeBSD version (#9944) 2020-04-23 21:00:45 +02:00
José Valim 8a876d6c78 Do not assume all files have started to compile, closes #9943 2020-04-23 20:09:02 +02:00
José Valim 831b12a116 Do not attempt to load in memory beam files, closes #9788 2020-04-23 15:59:08 +02:00
José Valim d16497b966 Bring back String.normalize and fix infinite loop 2020-04-23 13:35:59 +02:00
José Valim 7ef6dfc4e4 Fixes to ExUnit.Diff, closes #9926 2020-04-21 20:24:14 +02:00
José Valim e1158a55a3 Revert "Use an empty keyword list instead of nil for empty definitions"
This reverts commit 4477115675.
2020-03-24 20:22:00 +01:00
Eric Meadows-Jönsson 78cc8bea7a Fix divergedreq error when update changes requirement and version (#9811)
The fix is to mark the dependency for compilation in the Fetcher (by
touching .fetch) so that the Loader wont read the version from the .app
file.
2020-03-23 11:59:22 +01:00
José Valim 5914aba7e4 Add missing CHANGELOG entry 2020-03-22 20:59:21 +01:00
José Valim 40301963e4 Skip sys_core_prepare pass on Erlang/OTP 23+ 2020-03-18 14:22:54 +01:00
José Valim 248d532ae4 Compare pins as values, closes #9872 2020-03-13 11:52:04 +01:00
José Valim 74bc9506a9 Reduce the newlines count when they are immediately followed by a comment, closes #9873 2020-03-13 11:52:02 +01:00
Fernando Tapia Rico 41e218b160 Document version of :start/:stop events (tracers) (#9871) 2020-03-13 09:58:21 +01:00
José Valim 94221f2894 Shutdowns can be any term, closes #9878 2020-03-13 09:58:00 +01:00
José Valim 6eaac2dc72 Notify the script has been tested for bash, closes #9890 2020-03-13 09:57:25 +01:00
José Valim 92650c84be Update CHANGELOG, closes #9887 2020-03-09 22:05:26 +01:00
José Valim b30cbb600b Add start and stop events to compilation tracers, closes #9868 2020-03-02 14:36:32 +01:00
Wojtek Mach 6092fb4618 Return [{mod, bin}] from Code.compile_file/2, require_file/2, load_file/2 (#9864)
This was the documented behaviour on 1.9 that was broken on v1.10: (version `git` is this branch)

    $ cat a.ex
    defmodule A, do: nil

    $ for v in 1.9.4-otp-22 1.10.2-otp-22 git; do asdf local elixir $v && echo $v; elixir -e 'IO.inspect Code.compile_file("a.ex")'; done
    1.9.4-otp-22
    [{A, <<70, 79, ...>>}
    1.10.2-otp-22
    [{A, %{attributes: [], ...}]
    git
    [{A, <<70, 79, ...>>}

    $ for v in 1.9.4-otp-22 1.10.2-otp-22 git; do asdf local elixir $v && echo $v; elixir -e 'IO.inspect Code.require_file("a.ex")'; done
    1.9.4-otp-22
    [{A, <<70, 79, ...>>}]
    1.10.2-otp-22
    [{A, %{attributes: [], ...}]
    git
    [{A, <<70, 79, ...>>}]

    $ for v in 1.9.4-otp-22 1.10.2-otp-22 git; do asdf local elixir $v && echo $v; elixir -e 'IO.inspect Code.load_file("a.ex")'; done
    1.9.4-otp-22
    [{A, <<70, 79, ...>>}]
    1.10.2-otp-22
    warning: Code.load_file/1 is deprecated. Use Code.require_file/2 or Code.compile_file/2 instead
      nofile:1

    [{A, %{attributes: [], ...}]
    git
    warning: Code.load_file/1 is deprecated. Use Code.require_file/2 or Code.compile_file/2 instead
      nofile:1

    [{A, <<70, 79, ...>>}]
2020-03-01 00:50:31 +01:00
José Valim 07450832e7 Release v1.10.2 2020-02-26 12:53:01 +01:00
José Valim 34adbcb95a Fix unused variable warning 2020-02-26 12:17:23 +01:00
José Valim 286d310ac3 Fixes for Erlang/OTP 23 2020-02-26 12:05:47 +01:00
José Valim 2fc916088a Rewrite stacktraces for compatibility with upcoming OTP version 2020-02-24 21:36:51 +01:00
José Valim 9a428e9cdd Fix bootstrap 2020-02-13 12:16:15 +01:00
José Valim 090dddcf00 Only escape contents outside of interpolation, closes #9812 2020-02-12 20:51:58 +01:00
José Valim 089c470e13 Only print metadata of known types, closes #9814 2020-02-12 19:03:59 +01:00
José Valim 68481b7377 Do not crash on fake down messages on stream monitor, closes #9803 2020-02-12 18:31:25 +01:00
José Valim e2dc9ae72e Do not write files to disk if warnings as errors was triggered, closes #9808 2020-02-12 18:00:50 +01:00
José Valim 51c95b6000 Release v1.10.1 2020-02-10 12:42:26 +01:00
José Valim 12917b00e5 Unpack extra guards from with matches, closes #9801 2020-02-09 18:15:43 +01:00
Łukasz Jan Niemier ab9ecf2d7f fix(logger): use correct return value from report_cb/2 (#9798)
Earlier it assumed that the return type is the same as in `report_cb/1`
which is not true. `report_cb/1` returns format string and data while
`report_cb/2` returns already formatted string.
2020-02-09 09:35:02 +01:00
José Valim eca809285e Ensure assert_received still produces valid exceptions, closes #9799 2020-02-09 09:32:40 +01:00
José Valim 8e71e65aae Fix typo 2020-02-08 08:31:09 +01:00
José Valim c3a27be073 Do not imply nil is caused by Access 2020-02-08 01:58:44 +01:00
José Valim e10f9fb6fe Revert "Emit warnings for unused match vars in assertions, closes #9787"
Revert it as we may have false positives in v1.10.
We are still keeping it on master.

This reverts commit 696bad8de1.
2020-02-06 20:26:29 +01:00
José Valim f66ffd0733 Add missing :erlang.size/1 guard, closes #9789 2020-02-06 18:45:01 +01:00
José Valim 696bad8de1 Emit warnings for unused match vars in assertions, closes #9787 2020-02-06 18:35:21 +01:00
Joel C 96c8e31410 Quote variables in cli_bat_text install section (#9780)
This allows the release batch script to be run in a path that
includes spaces.
2020-02-04 14:45:46 +01:00
José Valim 70be440359 Only warn for commas at the end of words 2020-02-04 13:24:50 +01:00
José Valim c6d6683225 Add tests for timer and interval with infinity 2020-02-03 12:41:54 +01:00
José Valim db2504eca6 Do not emit invalid code when formatting assocs in maps, closes #9771 2020-02-01 11:18:42 +01:00
José Valim 98c3f5c602 Do not validate line in quote (moved to v1.11) 2020-01-31 11:23:25 +01:00
Akash Hiremath db4684b2f5 Fix logger truncate for multi-byte characters (#9767) 2020-01-31 10:44:45 +01:00
José Valim f3f65e4cd1 No longer rebroadcast messages as this is taken care by Erlang's logger
Closes #9765.
2020-01-30 17:02:43 +01:00
Piotr Szmielew 5331174934 Allow using remote or rpc with compile_env
Signed-off-by: José Valim <jose.valim@dashbit.co>
2020-01-30 12:05:47 +01:00
Devon Estes f4f2be67d7 Fix docs for compiler tracing events (#9760)
The `local_function` event is a tuple with four elements, not five.
2020-01-30 11:44:12 +01:00
Theo Dowling 686daadaaa Fix typo in CHANGELOG (#9757) 2020-01-28 07:37:53 +01:00
José Valim 6e8563e930 Add space to quote error message 2020-01-27 12:50:23 +01:00
José Valim 65be24a8c2 CHANGELOG fixes 2020-01-27 10:24:23 +01:00
José Valim 5bd7a90864 Release v1.10 2020-01-27 10:09:33 +01:00
Deniz Adrian 831046fcb7 Ensure ERTS is not copied into release (#9752)
The default behaviour when starting a new mix project is to
`include_erts: true`.

If at some point you choose to `include_erts: false`, but have the ERTS
artifact hanging around from previous builds, it gets included in future
builds, therefore rendering `include_erts: false` broken when running on
systems that have built with `include_erts: true` for the same
environment.

`make_tar/1` currently always tries to include the ERTS artifact into
the archive, regardless of our `include_erts` configuration. this fails
in 1.9.4 if no previous artifact is present, and seems to be worked
around in the current implementation by simply ignoring source
directories that don't exist.

This adds a regression test for the behaviour by "simulating" an ERTS
leftover from a previous build.

This also adds logic to not try to include ERTS into archives in the
first place when our release is configured with `include_erts: false`.
2020-01-25 15:44:01 +01:00
José Valim 6c44feae2f Update CHANGELOG 2020-01-24 19:23:51 +01:00
José Valim 35eef87590 Allow file, line and context to be dynamically set on quote, closes #9721 2020-01-23 22:49:31 +01:00
Eric Meadows-Jönsson fa091c6bd9 Only error for duplicate map keys in matches (#9748)
In matches we will error:

    %{a: :b, a: :c} = %{a: :b}

In normal expressions we will only warn:

    %{a: :b, a: :c}
2020-01-23 20:29:24 +01:00
José Valim a561f81539 Update return types in docs for Mix.Generator functions, closes #9749 2020-01-23 14:40:07 +01:00
José Valim 739c1b6292 Keep existing APIs for min/max/min_by/max_by 2020-01-17 12:54:11 +01:00
Fernando Tapia Rico 3e1e6ddf8a Update Library Guidelines.md (#9739) 2020-01-15 19:00:43 +01:00
José Valim ccb8c5a2e7 Add a new anti-pattern section to library guidelines 2020-01-15 17:55:56 +01:00
Eric Meadows-Jönsson 07e580cf17 Keep line number in cond_clause error (#9730) 2020-01-14 17:54:40 +01:00
Jason Axelson 1f9f713d9c Document Mix.Config.persist soft deprecation (#9722) 2020-01-13 15:51:25 +01:00
José Valim 9814a4cd61 Do not expand interpolation twice, closes #9709 2020-01-12 17:22:45 +01:00
José Valim 3dfdd80dff Make sure to traverse args in xref 2020-01-12 16:25:13 +01:00
José Valim 0fdc7510b8 Remove guard checks on is_odd/is_even as those are tested in defguard, closes #9700 2020-01-12 16:00:12 +01:00
Eric Meadows-Jönsson 0e8873238d Do not warn for functions generated by Erlang compiler (#9713) 2020-01-12 15:42:54 +01:00
Eric Meadows-Jönsson ba02eb90a0 Allow any type on the RHS of or/2 (#9706)
Closes #9704.
2020-01-11 12:19:29 +01:00
Eric Meadows-Jönsson fdc7716bf3 Improve type check error message (#9705)
A type error may not always cause a clause to fail.

Related to #9700.
2020-01-11 12:19:29 +01:00
José Valim a050c00fbc Use meta on rewrites, closes #9714 2020-01-11 12:17:43 +01:00
Bram Verburg d395c3e3cf Update CHANGELOG.md (#9708) 2020-01-09 12:28:17 +01:00
Tobiasz Małecki c3e903f339 Fix mix.release raising error when src is missing in erts_source (#9703) 2020-01-09 09:57:03 +01:00
José Valim 452ba40855 Update CHANGELOG 2020-01-08 16:56:36 +01:00
José Valim 8854492cc5 Also remove undefined warnings when loading projects outside the CLI 2020-01-08 11:52:26 +01:00
José Valim 105770cfa0 Release v1.10.0-rc.0 2020-01-07 15:10:04 +01:00
José Valim 28b053a122 Prepare v1.10 release 2020-01-07 15:03:35 +01:00
104 changed files with 2018 additions and 759 deletions
+6 -9
View File
@@ -27,15 +27,13 @@ test_linux_task:
- env:
CHECK_POSIX_COMPLIANT: true
CHECK_REPRODUCIBLE: true
OTP_RELEASE: OTP-22.1
OTP_RELEASE: OTP-23.0
- env:
OTP_RELEASE: OTP-22.3
- env:
OTP_RELEASE: OTP-22.0
- env:
OTP_RELEASE: OTP-21.3.8
- env:
OTP_RELEASE: OTP-21.2
- env:
OTP_RELEASE: OTP-21.1
- env:
OTP_RELEASE: OTP-21.0
@@ -110,7 +108,7 @@ test_windows_task:
image: fertapric/elixir-ci:otp-win64-${OTP_RELEASE}
os_version: ${OS_VERSION}
cpu: 4
memory: 3840Mi
memory: 6GB
install_script:
- rmdir /s /q .git
@@ -130,11 +128,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
@@ -143,7 +141,6 @@ test_freebsd_task:
LC_ALL: en_US.UTF-8
install_script:
- sudo pkg update
- pkg install -y erlang git gmake
- rm -rf .git
- gmake compile
+202 -33
View File
@@ -26,73 +26,232 @@ 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.4 (2020-07-04)
### 1. Bug fixes
#### Elixir
* [Kernel] Fix a bug where custom types were printed as built-in types
* [Kernel] Don't add compile-time dependency on `defdelegate`
* [Kernel] Add line numbers to warnings on deprecated imports
* [Kernel] Report the correct line number when raising inside a macro
* [Task] Include callers in translated Logger metadata for Task
* [Task] Fix Task PID and caller in Task Supervisor reports
#### ExUnit
* [ExUnit.Formatter] Avoid crashes when diffing guards when the pattern does not match
* [ExUnit.Formatter] Also blame exceptions that come from linked and trapped exits
#### IEx
* [IEx.Helpers] Do not crash when printing a type that cannot be code formatted
#### Mix
* [mix app.start] Fix reading `.app` file located in archives (`.ez` files)
* [mix local.hex] Provide more guidance when Hex can't be installed
* [mix release] Properly encode config in releases
## 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 +275,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 +304,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 +324,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 +359,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 +382,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
+1 -1
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -1 +1 @@
1.10.0-dev
1.10.4
+12 -3
View File
@@ -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
+13 -5
View File
@@ -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
+2 -1
View File
@@ -217,7 +217,8 @@ defmodule Code.Typespec do
end
defp typespec_to_quoted({:user_type, line, name, args}) do
typespec_to_quoted({:type, line, name, args})
args = for arg <- args, do: typespec_to_quoted(arg)
{name, [line: line], args}
end
defp typespec_to_quoted({:type, line, :tuple, :any}) do
+66 -45
View File
@@ -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
@@ -329,7 +350,7 @@ defmodule Config.Provider do
defp write_config!(config, path) do
contents = :io_lib.format("%% coding: utf-8~n~tw.~n", [config])
case File.write(path, contents, [:utf8]) do
case File.write(path, IO.chardata_to_string(contents)) do
:ok ->
:ok
+40 -31
View File
@@ -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 """
+1 -1
View File
@@ -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
+2 -2
View File
@@ -317,7 +317,7 @@ defmodule IO do
@spec warn(chardata | String.Chars.t(), Exception.stacktrace()) :: :ok
def warn(message, []) do
message = [to_chardata(message), ?\n]
:elixir_errors.io_warn(nil, nil, message, message)
:elixir_errors.io_warn(0, nil, message, message)
end
def warn(message, [{_, _, _, opts} | _] = stacktrace) do
@@ -327,7 +327,7 @@ defmodule IO do
file = opts[:file]
:elixir_errors.io_warn(
line,
line || 0,
file && List.to_string(file),
message,
[message, ?\n, " ", formatted_trace, ?\n]
+20 -11
View File
@@ -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 =
@@ -4885,6 +4881,18 @@ defmodule Kernel do
defmacro defdelegate(funs, opts) do
funs = Macro.escape(funs, unquote: true)
# don't add compile-time dependency on :to
opts =
with true <- is_list(opts),
{:ok, target} <- Keyword.fetch(opts, :to),
{:__aliases__, _, _} <- target do
target = Macro.expand(target, %{__CALLER__ | function: {:__info__, 1}})
Keyword.replace!(opts, :to, target)
else
_ ->
opts
end
quote bind_quoted: [funs: funs, opts: opts] do
target =
Keyword.get(opts, :to) || raise ArgumentError, "expected to: to be given as argument"
@@ -5388,7 +5396,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)
+86 -73
View File
@@ -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}
)
@@ -645,6 +652,12 @@ defmodule Kernel.ParallelCompiler do
end
end
defp get_line(file, _reason, [{_, _, _, [file: 'expanding macro']}, {_, _, _, info} | _]) do
if Keyword.get(info, :file) == to_charlist(Path.relative_to_cwd(file)) do
Keyword.get(info, :line)
end
end
defp get_line(file, _reason, [{_, _, _, info} | _]) do
if Keyword.get(info, :file) == to_charlist(Path.relative_to_cwd(file)) do
Keyword.get(info, :line)
+2 -1
View File
@@ -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
+6 -5
View File
@@ -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"\)")
-2
View File
@@ -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
+4 -2
View File
@@ -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
@@ -229,7 +231,7 @@ defmodule Module.Checker do
defp warn(meta, state, warning) do
{fun, arity} = state.function
location = {state.file, meta[:line], {state.module, fun, arity}}
location = {state.file, meta[:line] || 0, {state.module, fun, arity}}
%{state | warnings: [{__MODULE__, warning, location} | state.warnings]}
end
@@ -304,7 +306,7 @@ defmodule Module.Checker do
defp format_location({file, line, {module, fun, arity}}) do
file = Path.relative_to_cwd(file)
line = if line, do: [Integer.to_string(line), ": "], else: []
line = if line > 0, do: [Integer.to_string(line), ": "], else: []
mfa = Exception.format_mfa(module, fun, arity)
[" ", file, ?:, line, mfa, ?\n]
end
+17 -1
View File
@@ -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
+2 -2
View File
@@ -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),
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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 "$@" &
+9 -4
View File
@@ -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
View File
@@ -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
"""
+5 -4
View File
@@ -102,8 +102,8 @@ defmodule Task.Supervised do
%{
label: {Task.Supervisor, :terminating},
report: %{
name: get_from(owner),
starter: self(),
name: self(),
starter: get_from(owner),
function: fun,
args: args,
reason: {log_value(kind, value), __STACKTRACE__}
@@ -112,7 +112,8 @@ defmodule Task.Supervised do
%{
domain: [:otp, :elixir],
error_logger: %{tag: :error_msg},
report_cb: &__MODULE__.format_report/1
report_cb: &__MODULE__.format_report/1,
callers: Process.get(:"$callers")
}
)
@@ -539,7 +540,7 @@ defmodule Task.Supervised do
# One of the spawned processes went down. We inform the parent process of
# this and keep going.
{:DOWN, ref, _, _, reason} ->
{:DOWN, ref, _, _, reason} when is_map_key(running_tasks, ref) ->
{task, running_tasks} = Map.pop(running_tasks, ref)
%{position: position, timer_ref: timer_ref, timed_out?: timed_out?} = task
@@ -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).
@@ -53,7 +52,7 @@ Elixir version | Supported Erlang/OTP versions
1.7 | 19 - 22
1.8 | 20 - 22
1.9 | 20 - 22
1.10 | 21 - 22
1.10 | 21 - 22 (and Erlang/OTP 23 from v1.10.3)
While Elixir often adds compatibility to new Erlang/OTP versions on released branches, such as support for Erlang/OTP 20 in v1.4.5, those releases usually contain the minimum changes for Elixir to run without errors. Only the next minor release, in this case v1.5.0, does effectively leverage the new features provided by the latest Erlang/OTP release.
@@ -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
+28
View File
@@ -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:
-11
View File
@@ -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=[],
+5 -6
View File
@@ -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});
+2 -2
View File
@@ -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).
+1 -1
View File
@@ -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) ->
+19 -16
View File
@@ -92,11 +92,25 @@ dispatch_import(Meta, Name, Args, E, Callback) ->
{ok, Receiver, Quoted} ->
expand_quoted(Meta, Receiver, Name, Arity, Quoted, E);
{ok, Receiver, NewName, NewArgs} ->
elixir_expand:expand({{'.', [], [Receiver, NewName]}, Meta, NewArgs}, E);
elixir_expand:expand({{'.', Meta, [Receiver, NewName]}, Meta, NewArgs}, E);
error ->
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),
@@ -314,9 +328,10 @@ get_macros(Receiver, false) ->
end;
get_macros(Receiver, true) ->
case is_ensure_loaded(Receiver) of
true -> get_info(Receiver, macros);
false -> []
try
Receiver:'__info__'(macros)
catch
error:_ -> []
end.
%% Kernel deprecations are inlined.
@@ -353,18 +368,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 ->
+7 -9
View File
@@ -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) ->
+11 -1
View File
@@ -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]);
+14 -37
View File
@@ -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),
+10 -34
View File
@@ -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)]);
+2
View File
@@ -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;
+7 -1
View File
@@ -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};
+5 -1
View File
@@ -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.
+9 -9
View File
@@ -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'].
+98 -31
View File
@@ -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) ->
+44 -30
View File
@@ -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 -3
View File
@@ -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 """
%{
+35 -23
View File
@@ -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,14 +80,14 @@ 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: {:"£", "£", '£'}]]
@tag sys_config: [my_app: [encoding: {:time_μs, :"£", "£", '£'}]]
test "writes sys_config with encoding" do
init_and_assert_boot()
config = consult(@sys_config)
assert config[:my_app][:encoding] == {:"£", "£", '£'}
assert config[:my_app][:encoding] == {:time_μs, :"£", "£", '£'}
end
@tag sys_config: [my_app: [key: :old_value, sys_key: :sys_value, extra_config: :old_value]]
@@ -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()
@@ -172,6 +185,6 @@ defmodule Config.ProviderTest do
end
defp write_sys_config!(data) do
File.write!(@sys_config, :io_lib.format("~tw.~n", [data]), [:utf8])
File.write!(@sys_config, IO.chardata_to_string(:io_lib.format("~tw.~n", [data])))
end
end
+2 -4
View File
@@ -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
+2 -12
View File
@@ -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)))
@@ -659,8 +654,8 @@ defmodule Kernel.ExpansionTest do
expand(quote(do: [1] ++ 2 ++ [3] = [1, 2, 3]))
end
assert {:=, _, [-1, {{:., [], [:erlang, :-]}, _, [1]}]} = expand(quote(do: -1 = -1))
assert {:=, _, [1, {{:., [], [:erlang, :+]}, _, [1]}]} = expand(quote(do: +1 = +1))
assert {:=, _, [-1, {{:., _, [:erlang, :-]}, _, [1]}]} = expand(quote(do: -1 = -1))
assert {:=, _, [1, {{:., _, [:erlang, :+]}, _, [1]}]} = expand(quote(do: +1 = +1))
assert {:=, _, [[{:|, _, [1, [{:|, _, [2, 3]}]]}], [1, 2, 3]]} =
expand(quote(do: [1] ++ [2] ++ 3 = [1, 2, 3]))
@@ -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
@@ -126,4 +126,21 @@ defmodule Kernel.LexicalTrackerTest do
refute Foo.Bar in runtime
refute Foo.Bar in compile
end
test "defdelegate with literal does not add compile dependency" do
{{compile, _structs, _runtime, _}, _binding} =
Code.eval_string("""
defmodule Kernel.LexicalTrackerTest.Defdelegate do
defdelegate a, to: A
opts = [to: B]
defdelegate b, opts
Kernel.LexicalTracker.references(__ENV__.lexical_tracker)
end |> elem(3)
""")
refute A in compile
assert B in compile
end
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 =~
+26 -7
View File
@@ -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
+35 -24
View File
@@ -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)
+7
View File
@@ -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)"
+71 -7
View File
@@ -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" => """
@@ -600,6 +639,31 @@ defmodule Module.CheckerTest do
assert_warnings(files, warning)
end
test "reports imported functions" do
files = %{
"a.ex" => """
defmodule A do
@deprecated "oops"
def a, do: :ok
end
""",
"b.ex" => """
defmodule B do
import A
def b, do: a()
end
"""
}
warning = """
warning: A.a/0 is deprecated. oops
b.ex:3: B.b/0
"""
assert_warnings(files, warning)
end
test "reports structs" do
files = %{
"a.ex" => """
@@ -674,7 +738,7 @@ defmodule Module.CheckerTest do
}
warning = """
warning: function clause will never match, found incompatibility:
warning: incompatible types:
integer() !~ binary()
@@ -710,7 +774,7 @@ defmodule Module.CheckerTest do
}
warning = """
warning: function clause will never match, found incompatibility:
warning: incompatible types:
integer() !~ binary()
@@ -746,7 +810,7 @@ defmodule Module.CheckerTest do
}
warning = """
warning: function clause will never match, found incompatibility:
warning: incompatible types:
{var0} !~ var0
@@ -777,7 +841,7 @@ defmodule Module.CheckerTest do
}
warning = """
warning: function clause will never match, found incompatibility:
warning: incompatible types:
integer() !~ binary()
@@ -813,7 +877,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}]}
+4 -2
View File
@@ -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
+10 -1
View File
@@ -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]
+75
View File
@@ -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
+15
View File
@@ -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()
+10
View File
@@ -1183,6 +1183,12 @@ defmodule TypespecTest do
quoted =
Enum.sort([
quote(do: @spec(foo() :: integer())),
quote(do: @spec(foo() :: union())),
quote(do: @spec(foo() :: union(integer()))),
quote(do: @spec(foo() :: truly_union())),
quote(do: @spec(foo(union()) :: union())),
quote(do: @spec(foo(union(integer())) :: union(integer()))),
quote(do: @spec(foo(truly_union()) :: truly_union())),
quote(do: @spec(foo(atom()) :: integer() | [{}])),
quote(do: @spec(foo(arg) :: integer() when [arg: integer()])),
quote(do: @spec(foo(arg) :: arg when [arg: var])),
@@ -1191,6 +1197,10 @@ defmodule TypespecTest do
bytecode =
test_module do
@type union :: any()
@type union(t) :: t
@type truly_union :: list | map | union
def foo(), do: 1
def foo(arg), do: arg
Module.eval_quoted(__MODULE__, quote(do: (unquote_splicing(quoted))))
+48 -23
View File
@@ -1,4 +1,4 @@
ExUnit.start [seed: 0]
ExUnit.start(seed: 0)
defmodule TestOneOfEach do
@moduledoc """
@@ -10,8 +10,8 @@ defmodule TestOneOfEach do
@one 1
@two 2
@long_data_1 [field1: "one", field2: {:two1, :two2}, field3: 'three', field4: [1, 2, 3, 4]]
@long_data_2 [field1: "one", field2: {:two1, :two3}, field3: 'three', field4: [1, 2, 3, 4]]
@long_data_1 [field1: "one", field2: {:two1, :two2}, field3: 'three', field4: [1, 2, 3, 4]]
@long_data_2 [field1: "one", field2: {:two1, :two3}, field3: 'three', field4: [1, 2, 3, 4]]
setup do
{:ok, user_id: 1, post_id: 2, many_ids: Enum.to_list(1..50)}
@@ -62,10 +62,10 @@ defmodule TestOneOfEach do
end
test "12. assert that a message is received within a timeout" do
send self(), {:ok, 1}
send self(), :message_in_my_inbox
send self(), {:ok, 2}
send self(), :another_message
send(self(), {:ok, 1})
send(self(), :message_in_my_inbox)
send(self(), {:ok, 2})
send(self(), :another_message)
assert_receive :no_message_after_timeout
end
@@ -75,14 +75,14 @@ defmodule TestOneOfEach do
test "14. assert an exception with a given message is raised" do
assert_raise(SomeException, "some message", fn ->
raise "other exception"
end)
raise "other exception"
end)
end
test "15. assert an exception with a given message is raised, but the message is wrong" do
assert_raise(RuntimeError, "some message", fn ->
raise "other error"
end)
raise "other error"
end)
end
test "16. assert an exception is raised" do
@@ -90,7 +90,7 @@ defmodule TestOneOfEach do
end
test "17. assert two values are within some delta" do
assert_in_delta 3.1415926, 22.0/7, 0.001
assert_in_delta 3.1415926, 22.0 / 7, 0.001
end
test "18. refute a value with a message" do
@@ -98,12 +98,12 @@ defmodule TestOneOfEach do
end
test "19. refute a message is received within a timeout" do
send self(), {:hello, "Dave"}
send(self(), {:hello, "Dave"})
refute_receive {:hello, _}, 1000
end
test "20. refute a message is ready to be received" do
send self(), :hello_again
send(self(), :hello_again)
refute_received :hello_again
end
@@ -116,7 +116,7 @@ defmodule TestOneOfEach do
end
test "23. flunk" do
flunk "we failed. totally"
flunk("we failed. totally")
end
test "24. exception raised while running test" do
@@ -124,7 +124,8 @@ defmodule TestOneOfEach do
end
test "25. error due to exit" do
spawn_link fn -> raise "oops" end
spawn_link(fn -> raise "oops" end)
receive do
end
end
@@ -133,15 +134,17 @@ defmodule TestOneOfEach do
error1 =
try do
assert [@one] = [@two]
rescue e in ExUnit.AssertionError ->
{:error, e, System.stacktrace}
rescue
e in ExUnit.AssertionError ->
{:error, e, __STACKTRACE__}
end
error2 =
try do
assert @one * 4 > @two * 3
rescue e in ExUnit.AssertionError ->
{:error, e, System.stacktrace}
rescue
e in ExUnit.AssertionError ->
{:error, e, __STACKTRACE__}
end
raise ExUnit.MultiError, errors: [error1, error2]
@@ -150,8 +153,8 @@ defmodule TestOneOfEach do
@tag capture_log: true
test "27. log capturing" do
require Logger
Logger.debug "this will be logged"
flunk "oops"
Logger.debug("this will be logged")
flunk("oops")
end
test "28. function clause error" do
@@ -162,6 +165,28 @@ defmodule TestOneOfEach do
assert some_vars(1 + 2, 3 + 4)
end
@tag :capture_log
test "30. linked assertion error" do
Task.async(fn -> assert 1 == 2 end) |> Task.await()
end
@tag :capture_log
test "31. linked function clause error" do
Task.async(fn -> Access.fetch(:foo, :bar) end) |> Task.await()
end
@tag :capture_log
test "32. trapped assertion error" do
Process.flag(:trap_exit, true)
Task.async(fn -> assert 1 == 2 end) |> Task.await()
end
@tag :capture_log
test "33. trapped function clause error" do
Process.flag(:trap_exit, true)
Task.async(fn -> Access.fetch(:foo, :bar) end) |> Task.await()
end
defp some_vars(_a, _b) do
false
end
@@ -171,6 +196,6 @@ defmodule TestOneOfEach do
end
defp ignite(val) do
1/val
1 / val
end
end
+24 -21
View File
@@ -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
@@ -183,13 +180,18 @@ defmodule ExUnit.Diff do
defp diff_guard({:when, _, [expression, clause]}, right, env) do
{diff_expression, post_env} = diff_quoted(expression, right, env)
bindings = Map.merge(post_env.pins, post_env.current_vars)
{diff_clause, clause_equivalent?} = diff_guard_clause(clause, Map.to_list(bindings))
{guard_clause, guard_equivalent?} =
if diff_expression.equivalent? do
bindings = Map.merge(post_env.pins, post_env.current_vars)
diff_guard_clause(clause, Map.to_list(bindings))
else
{clause, false}
end
diff = %__MODULE__{
diff_expression
| left: {:when, [], [diff_expression.left, diff_clause]},
equivalent?: diff_expression.equivalent? and clause_equivalent?
| left: {:when, [], [diff_expression.left, guard_clause]},
equivalent?: guard_equivalent?
}
{diff, post_env}
@@ -225,7 +227,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 +628,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 +677,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 +765,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
+61 -31
View File
@@ -54,6 +54,7 @@ defmodule ExUnit.Formatter do
@counter_padding " "
@mailbox_label_padding @counter_padding <> " "
@formatter_exceptions [ExUnit.AssertionError, FunctionClauseError]
@no_value ExUnit.AssertionError.no_value()
@doc """
@@ -136,10 +137,10 @@ defmodule ExUnit.Formatter do
@doc false
def format_assertion_error(%ExUnit.AssertionError{} = struct) do
format_assertion_error(%{}, struct, [], :infinity, fn _, msg -> msg end, "")
format_exception(%{}, struct, [], :infinity, fn _, msg -> msg end, "") |> elem(0)
end
defp format_assertion_error(test, struct, stack, width, formatter, counter_padding) do
defp format_exception(test, %ExUnit.AssertionError{} = struct, stack, width, formatter, pad) do
label_padding_size = if has_value?(struct.right), do: 7, else: 6
padding_size = label_padding_size + byte_size(@counter_padding)
@@ -148,16 +149,27 @@ defmodule ExUnit.Formatter do
do: &pad_multiline(&1, padding_size),
else: &code_multiline(&1, padding_size)
[
note: if_value(struct.message, &format_message(&1, formatter)),
doctest: if_value(struct.doctest, &pad_multiline(&1, 2 + byte_size(@counter_padding))),
code: if_value(struct.expr, code_multiline),
code: unless_value(struct.expr, fn -> get_code(test, stack) || @no_value end),
arguments: if_value(struct.args, &format_args(&1, width))
]
|> Kernel.++(format_context(struct, formatter, padding_size, width))
|> format_meta(formatter, counter_padding, label_padding_size)
|> IO.iodata_to_binary()
formatted =
[
note: if_value(struct.message, &format_message(&1, formatter)),
doctest: if_value(struct.doctest, &pad_multiline(&1, 2 + byte_size(@counter_padding))),
code: if_value(struct.expr, code_multiline),
code: unless_value(struct.expr, fn -> get_code(test, stack) || @no_value end),
arguments: if_value(struct.args, &format_args(&1, width))
]
|> Kernel.++(format_context(struct, formatter, padding_size, width))
|> format_meta(formatter, pad, label_padding_size)
|> IO.iodata_to_binary()
{formatted, stack}
end
defp format_exception(test, %FunctionClauseError{} = struct, stack, _width, formatter, _pad) do
{blamed, stack} = Exception.blame(:error, struct, stack)
banner = Exception.format_banner(:error, struct)
blamed = FunctionClauseError.blame(blamed, &inspect/1, &blame_match(&1, &2, formatter))
message = error_info(banner, formatter) <> "\n" <> pad(String.trim_leading(blamed, "\n"))
{message <> format_code(test, stack, formatter), stack}
end
@doc false
@@ -179,30 +191,48 @@ defmodule ExUnit.Formatter do
end)
end
defp format_kind_reason(
test,
:error,
%ExUnit.AssertionError{} = struct,
stack,
width,
formatter
) do
{format_assertion_error(test, struct, stack, width, formatter, @counter_padding), stack}
defp format_kind_reason(test, :error, %mod{} = struct, stack, width, formatter)
when mod in @formatter_exceptions do
format_exception(test, struct, stack, width, formatter, @counter_padding)
end
defp format_kind_reason(test, :error, %FunctionClauseError{} = struct, stack, _width, formatter) do
{blamed, stack} = Exception.blame(:error, struct, stack)
banner = Exception.format_banner(:error, struct)
blamed = FunctionClauseError.blame(blamed, &inspect/1, &blame_match(&1, &2, formatter))
message = error_info(banner, formatter) <> "\n" <> pad(String.trim_leading(blamed, "\n"))
{message <> format_code(test, stack, formatter), stack}
defp format_kind_reason(test, kind, reason, stack, width, formatter) do
case linked_or_trapped_exit(kind, reason) do
{header, wrapped_reason, wrapped_stack} ->
struct = Exception.normalize(:error, wrapped_reason, wrapped_stack)
{formatted_reason, _} =
format_exception(test, struct, wrapped_stack, width, formatter, @counter_padding)
formatted_stack = format_stacktrace(wrapped_stack, test.module, test.name, formatter)
{error_info(header, formatter) <> pad(formatted_reason <> formatted_stack), stack}
:error ->
{reason, stack} = Exception.blame(kind, reason, stack)
message = error_info(Exception.format_banner(kind, reason), formatter)
{message <> format_code(test, stack, formatter), stack}
end
end
defp format_kind_reason(test, kind, reason, stack, _width, formatter) do
message = error_info(Exception.format_banner(kind, reason), formatter)
{message <> format_code(test, stack, formatter), stack}
defp linked_or_trapped_exit({:EXIT, pid}, {reason, [_ | _] = stack})
when :erlang.map_get(:__struct__, reason) in @formatter_exceptions
when reason == :function_clause do
{"** (EXIT from #{inspect(pid)}) an exception was raised:\n", reason, stack}
end
defp linked_or_trapped_exit(:exit, {{reason, [_ | _] = stack}, {mod, fun, args}})
when is_atom(mod) and is_atom(fun) and is_list(args) and
:erlang.map_get(:__struct__, reason) in @formatter_exceptions
when is_atom(mod) and is_atom(fun) and is_list(args) and reason == :function_clause do
{
"** (exit) exited in: #{Exception.format_mfa(mod, fun, args)}\n ** (EXIT) an exception was raised:",
reason,
stack
}
end
defp linked_or_trapped_exit(_kind, _reason), do: :error
defp format_code(test, stack, formatter) do
if snippet = get_code(test, stack) do
" " <> formatter.(:extra_info, "code: ") <> snippet <> "\n"
@@ -338,7 +368,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
+110 -4
View File
@@ -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
@@ -809,6 +914,7 @@ defmodule ExUnit.DiffTest do
refute_diff((x when x == 1 or x == 2) = 0, "x when -x == 1- or -x == 2-", "0")
refute_diff((x when x == 1 when x == 2) = 0, "x when -x == 1- when -x == 2-", "0")
refute_diff((x when x in [1, 2]) = 0, "x when -x in [1, 2]-", "0")
refute_diff(({:ok, x} when x == 1) = :error, "-{:ok, x}- when x == 1", "+:error+")
end
test "charlists" do
+110
View File
@@ -69,6 +69,61 @@ defmodule ExUnit.FormatterTest do
"""
end
test "formats test exits with function clause mfa" do
{error, stack} =
try do
Access.fetch(:foo, :bar)
catch
:error, error -> {error, __STACKTRACE__}
end
failure = [{:exit, {{error, stack}, {:mod, :fun, []}}, []}]
assert trim_multiline_whitespace(format_test_failure(test(), failure, 1, 80, &formatter/2)) =~
"""
1) world (Hello)
test/ex_unit/formatter_test.exs:1
** (exit) exited in: :mod.fun()
** (EXIT) an exception was raised:
** (FunctionClauseError) no function clause matching in Access.fetch/2
The following arguments were given to Access.fetch/2:
# 1
:foo
# 2
:bar
Attempted function clauses (showing 5 out of 5):
def fetch(%module{} = container, key)
"""
end
test "formats test exits with assertion mfa" do
{error, stack} =
try do
assert 1 == 2
rescue
error -> {error, __STACKTRACE__}
end
failure = [{:exit, {{error, stack}, {:mod, :fun, []}}, []}]
assert trim_multiline_whitespace(format_test_failure(test(), failure, 1, 80, &formatter/2)) =~
"""
1) world (Hello)
test/ex_unit/formatter_test.exs:1
** (exit) exited in: :mod.fun()
** (EXIT) an exception was raised:
Assertion with == failed
code: assert 1 == 2
left: 1
right: 2
"""
end
test "formats test throws" do
failure = [{:throw, 1, []}]
@@ -89,6 +144,61 @@ defmodule ExUnit.FormatterTest do
"""
end
test "formats test EXITs with function clause errors" do
{error, stack} =
try do
Access.fetch(:foo, :bar)
catch
:error, error -> {error, __STACKTRACE__}
end
failure = [{{:EXIT, self()}, {error, stack}, []}]
assert trim_multiline_whitespace(format_test_failure(test(), failure, 1, 80, &formatter/2)) =~
"""
1) world (Hello)
test/ex_unit/formatter_test.exs:1
** (EXIT from #{inspect(self())}) an exception was raised:
** (FunctionClauseError) no function clause matching in Access.fetch/2
The following arguments were given to Access.fetch/2:
# 1
:foo
# 2
:bar
Attempted function clauses (showing 5 out of 5):
def fetch(%module{} = container, key)
"""
end
test "formats test EXITs with assertion errors" do
{error, stack} =
try do
assert 1 == 2
rescue
error -> {error, __STACKTRACE__}
end
failure = [{{:EXIT, self()}, {error, stack}, []}]
assert trim_multiline_whitespace(format_test_failure(test(), failure, 1, 80, &formatter/2)) =~
"""
1) world (Hello)
test/ex_unit/formatter_test.exs:1
** (EXIT from #{inspect(self())}) an exception was raised:
Assertion with == failed
code: assert 1 == 2
left: 1
right: 2
"""
end
test "formats test errors with test_location_relative_path" do
Application.put_env(:ex_unit, :test_location_relative_path, "apps/sample")
failure = [{:error, catch_error(raise "oops"), []}]
+9 -3
View File
@@ -735,9 +735,15 @@ defmodule IEx.Introspection do
## Helpers
defp format_typespec(definition, kind, nesting) do
"@#{kind} #{Macro.to_string(definition)}"
|> Code.format_string!(line_length: IEx.width() - 2 * nesting)
|> IO.iodata_to_binary()
string = "@#{kind} #{Macro.to_string(definition)}"
try do
string
|> Code.format_string!(line_length: IEx.width() - 2 * nesting)
|> IO.iodata_to_binary()
rescue
_ -> string
end
|> color_prefix_with_line()
|> indent(nesting)
end
+7 -1
View File
@@ -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
@@ -853,6 +853,7 @@ defmodule IEx.HelpersTest do
defmodule TypeSample do
@typedoc "An ID with description."
@type id_with_desc :: {number, String.t}
@type unquote(:"?")() :: :question_mark
end
"""
@@ -872,6 +873,11 @@ defmodule IEx.HelpersTest do
An ID with description.
"""
assert capture_io(fn -> t(TypeSample."?"()) end) == """
@type ?() :: :question_mark
"""
end)
after
cleanup_modules([TypeSample])
-8
View File
@@ -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
+30 -27
View File
@@ -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
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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"]
+1 -1
View File
@@ -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
+28 -3
View File
@@ -306,13 +306,14 @@ defmodule Logger.TranslatorTest do
test "translates Task crashes" do
{:ok, pid} = Task.start_link(__MODULE__, :task, [self()])
parent = self()
assert capture_log(fn ->
ref = Process.monitor(pid)
send(pid, :go)
receive do: ({:DOWN, ^ref, _, _, _} -> :ok)
end) =~ ~r"""
\[error\] Task #PID<\d+\.\d+\.\d+> started from #PID<\d+\.\d+\.\d+> terminating
\[error\] Task #{inspect(pid)} started from #{inspect(self())} terminating
\*\* \(RuntimeError\) oops
.*
Function: &Logger.TranslatorTest.task\/1
@@ -324,6 +325,7 @@ defmodule Logger.TranslatorTest do
assert {%RuntimeError{message: "oops"}, [_ | _]} = task_metadata[:crash_reason]
assert {%RuntimeError{message: "oops"}, [_ | _]} = process_metadata[:crash_reason]
assert [parent] == task_metadata[:callers]
refute Keyword.has_key?(task_metadata, :initial_call)
assert process_metadata[:initial_call] == {Logger.TranslatorTest, :task, 1}
@@ -332,6 +334,7 @@ defmodule Logger.TranslatorTest do
test "translates Task async_stream crashes with neighbour" do
fun = fn -> Task.async_stream([:oops], :erlang, :error, []) |> Enum.to_list() end
{:ok, pid} = Task.start(__MODULE__, :task, [self(), fun])
parent = self()
assert capture_log(:debug, fn ->
ref = Process.monitor(pid)
@@ -345,12 +348,15 @@ defmodule Logger.TranslatorTest do
assert_receive {:error, _pid, {Logger, ["Task " <> _ | _], _ts, task_metadata}}
assert_receive {:error, _pid, {Logger, ["Process " | _], _ts, process_metadata}}
assert [pid, parent] == task_metadata[:callers]
assert {:oops, [_ | _]} = task_metadata[:crash_reason]
assert {%ErlangError{original: :oops}, [_ | _]} = process_metadata[:crash_reason]
end
test "translates Task undef module crash" do
parent = self()
assert capture_log(fn ->
{:ok, pid} = Task.start(:module_does_not_exist, :undef, [])
ref = Process.monitor(pid)
@@ -365,12 +371,15 @@ defmodule Logger.TranslatorTest do
assert_receive {:error, _pid, {Logger, ["Task " <> _ | _], _ts, task_metadata}}
assert_receive {:error, _pid, {Logger, ["Process " | _], _ts, process_metadata}}
assert [parent] == task_metadata[:callers]
assert {%UndefinedFunctionError{function: :undef}, [_ | _]} = task_metadata[:crash_reason]
assert {%UndefinedFunctionError{function: :undef}, [_ | _]} = process_metadata[:crash_reason]
end
test "translates Task undef function crash" do
parent = self()
assert capture_log(fn ->
{:ok, pid} = Task.start(__MODULE__, :undef, [])
ref = Process.monitor(pid)
@@ -385,12 +394,15 @@ defmodule Logger.TranslatorTest do
assert_receive {:error, _pid, {Logger, ["Task " <> _ | _], _ts, task_metadata}}
assert_receive {:error, _pid, {Logger, ["Process " | _], _ts, process_metadata}}
assert [parent] == task_metadata[:callers]
assert {%UndefinedFunctionError{function: :undef}, [_ | _]} = task_metadata[:crash_reason]
assert {%UndefinedFunctionError{function: :undef}, [_ | _]} = process_metadata[:crash_reason]
end
test "translates Task raising ErlangError" do
parent = self()
assert capture_log(fn ->
exception =
try do
@@ -413,12 +425,15 @@ defmodule Logger.TranslatorTest do
assert_receive {:error, _pid, {Logger, ["Task " <> _ | _], _ts, task_metadata}}
assert_receive {:error, _pid, {Logger, ["Process " | _], _ts, process_metadata}}
assert [parent] == task_metadata[:callers]
assert {%ErlangError{original: :foo}, [_ | _]} = task_metadata[:crash_reason]
assert {%ErlangError{original: :foo}, [_ | _]} = process_metadata[:crash_reason]
end
test "translates Task raising Erlang badarg error" do
parent = self()
assert capture_log(fn ->
{:ok, pid} = Task.start(:erlang, :error, [:badarg])
ref = Process.monitor(pid)
@@ -433,12 +448,15 @@ defmodule Logger.TranslatorTest do
assert_receive {:error, _pid, {Logger, ["Task " <> _ | _], _ts, task_metadata}}
assert_receive {:error, _pid, {Logger, ["Process " | _], _ts, process_metadata}}
assert [parent] == task_metadata[:callers]
assert {%ArgumentError{message: "argument error"}, [_ | _]} = task_metadata[:crash_reason]
assert {%ArgumentError{message: "argument error"}, [_ | _]} = process_metadata[:crash_reason]
end
test "translates Task exiting abnormally" do
parent = self()
assert capture_log(fn ->
{:ok, pid} = Task.start(:erlang, :exit, [:abnormal])
ref = Process.monitor(pid)
@@ -453,6 +471,7 @@ defmodule Logger.TranslatorTest do
assert_receive {:error, _pid, {Logger, ["Task " <> _ | _], _ts, task_metadata}}
assert_receive {:error, _pid, {Logger, ["Process " | _], _ts, process_metadata}}
assert [parent] == task_metadata[:callers]
assert {:abnormal, [_ | _]} = task_metadata[:crash_reason]
assert {:abnormal, [_ | _]} = process_metadata[:crash_reason]
@@ -625,8 +644,9 @@ defmodule Logger.TranslatorTest do
"""
end
test "translates :proc_lib crashes on debug" do
test "translates :proc_lib+Task crashes on debug" do
{:ok, pid} = Task.start_link(__MODULE__, :task, [self()])
parent = self()
assert capture_log(:debug, fn ->
ref = Process.monitor(pid)
@@ -652,13 +672,15 @@ defmodule Logger.TranslatorTest do
assert process_metadata[:pid] == task_metadata[:pid]
assert is_list(process_metadata[:callers])
assert is_list(process_metadata[:ancestors])
assert [parent] == task_metadata[:callers]
assert {%RuntimeError{message: "oops"}, [_ | _]} = task_metadata[:crash_reason]
assert {%RuntimeError{message: "oops"}, [_ | _]} = process_metadata[:crash_reason]
end
test "translates :proc_lib crashes with neighbour on debug" do
test "translates :proc_lib+Task crashes with neighbour on debug" do
{:ok, pid} = Task.start_link(__MODULE__, :sub_task, [self()])
parent = self()
assert capture_log(:debug, fn ->
ref = Process.monitor(pid)
@@ -677,6 +699,9 @@ defmodule Logger.TranslatorTest do
Current Stacktrace:
(lib/logger/)?test/logger/translator_test.exs:\d+: Logger.TranslatorTest.sleep/1
"""
assert_receive {:error, _pid, {Logger, ["Task " <> _ | _], _ts, task_metadata}}
assert [parent] == task_metadata[:callers]
end
test "translates Supervisor progress" do
+1
View File
@@ -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)"]
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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,
+4 -7
View File
@@ -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))
+4 -4
View File
@@ -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"
+19 -4
View File
@@ -161,14 +161,29 @@ defmodule Mix.Local do
contents
{:remote, message} ->
Mix.raise("""
#{message}
Mix.raise(
"""
#{message}
Could not install #{name} because Mix could not download metadata at #{path}.
""")
Could not install #{name} because Mix could not download metadata at #{path}.
""" <> suggestions(name)
)
end
end
defp suggestions("Hex") do
"""
Alternatively, you can compile and install Hex directly with this command:
mix archive.install github hexpm/hex branch latest
"""
end
defp suggestions(_) do
""
end
defp parse_csv(body) do
body
|> :binary.split("\n", [:global, :trim])
+4 -2
View File
@@ -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)
+24 -23
View File
@@ -402,7 +402,7 @@ defmodule Mix.Release do
args = [runtime_config?, sys_config]
format = "%% coding: utf-8~n%% RUNTIME_CONFIG=~s~n~tw.~n"
File.mkdir_p!(Path.dirname(path))
File.write!(path, :io_lib.format(format, args), [:utf8])
File.write!(path, IO.chardata_to_string(:io_lib.format(format, args)))
case :file.consult(path) do
{:ok, _} ->
@@ -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)
@@ -496,7 +512,7 @@ defmodule Mix.Release do
:ok | {:error, String.t()}
def make_boot_script(release, path, modes, prepend_paths \\ []) do
with {:ok, rel_spec} <- build_release_spec(release, modes) do
File.write!(path <> ".rel", consultable(rel_spec), [:utf8])
File.write!(path <> ".rel", consultable(rel_spec))
sys_path = String.to_charlist(path)
@@ -519,7 +535,7 @@ defmodule Mix.Release do
|> prepend_paths_to_script(prepend_paths)
script = {:script, rel_info, instructions}
File.write!(script_path, consultable(script), [:utf8])
File.write!(script_path, consultable(script))
:ok = :systools.script2boot(sys_path)
{:error, module, info} ->
@@ -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
@@ -657,7 +657,7 @@ defmodule Mix.Release do
end
defp consultable(term) do
:io_lib.format("%% coding: utf-8~n~tp.~n", [term])
IO.chardata_to_string(:io_lib.format("%% coding: utf-8~n~tp.~n", [term]))
end
@doc """
@@ -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)
+8 -1
View File
@@ -76,8 +76,12 @@ defmodule Mix.Shell do
## Options
* `:cd` - (since v1.11.0) the directory to run the command in
* `:stderr_to_stdout` - redirects stderr to stdout, defaults to true
* `:env` - a list of environment variables, defaults to `[]`
* `:quiet` - overrides the callback to no-op
"""
@@ -98,7 +102,10 @@ defmodule Mix.Shell do
[]
end
opts = [:stream, :binary, :exit_status, :hide, :use_stdio, {:env, env} | args]
opts =
[:stream, :binary, :exit_status, :hide, :use_stdio, {:env, env}] ++
args ++ Keyword.take(options, [:cd])
port = Port.open({:spawn, shell_command(command)}, opts)
port_read(port, callback)
end
+15 -2
View File
@@ -162,8 +162,8 @@ defmodule Mix.Tasks.App.Start do
{:error, {:file.format_error(:enoent), name}}
path ->
case :file.consult(path) do
{:ok, [{:application, _, properties} = application_data]} ->
case consult_app_file_maybe_in_archive(path) do
{:ok, {:application, _, properties} = application_data} ->
with :ok <- :application.load(application_data) do
if compile_env = validate_compile_env? && properties[:compile_env] do
# Unfortunately we can only check the current app here,
@@ -182,6 +182,19 @@ defmodule Mix.Tasks.App.Start do
end
end
defp consult_app_file_maybe_in_archive(path) do
# The path could be located in .ez archive
case :erl_prim_loader.get_file(path) do
{:ok, bin, _full_name} ->
with {:ok, tokens, _} <- :erl_scan.string(String.to_charlist(bin)) do
:erl_parse.parse_term(tokens)
end
:error ->
{:error, :enoent}
end
end
@doc false
def type(config, opts) do
cond do
+17 -12
View File
@@ -22,6 +22,13 @@ defmodule Mix.Tasks.Cmd do
This task is automatically reenabled, so it can be called multiple times
with different arguments.
## Command line options
* `--app` - limit running the command to the given app. This option
may be given multiple times
* `--cd` - (since v1.11.0) the directory to run the command in
## Zombie operating system processes
Beware that the Erlang VM does not terminate child processes
@@ -35,12 +42,20 @@ defmodule Mix.Tasks.Cmd do
of the `Port` module documentation.
"""
@switches [
app: :keep,
cd: :string
]
@impl true
def run(args) do
{args, apps} = parse_apps(args, [])
{opts, args} = OptionParser.parse_head!(args, strict: @switches)
apps = Enum.map(List.wrap(opts[:app]), &String.to_atom/1)
if apps == [] or Mix.Project.config()[:app] in apps do
case Mix.shell().cmd(Enum.join(args, " ")) do
cmd_opts = Keyword.take(opts, [:cd])
case Mix.shell().cmd(Enum.join(args, " "), cmd_opts) do
0 -> :ok
status -> exit(status)
end
@@ -48,14 +63,4 @@ defmodule Mix.Tasks.Cmd do
Mix.Task.reenable("cmd")
end
defp parse_apps(args, apps) do
case args do
["--app", app | tail] ->
parse_apps(tail, [String.to_atom(app) | apps])
args ->
{args, apps}
end
end
end
+5
View File
@@ -11,6 +11,11 @@ defmodule Mix.Tasks.Local.Hex do
mix local.hex
If installing a precompiled Hex does not work, you can compile and install
Hex directly with this command:
mix archive.install github hexpm/hex branch latest
## Command line options
* `--force` - forces installation without a shell prompt; primarily
+21 -5
View File
@@ -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
+2 -2
View File
@@ -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 (
+5 -3
View File
@@ -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
+1 -1
View File
@@ -1,4 +1,4 @@
import Config
config :release_test, :static, :was_set
config :release_test, :encoding, {:"£", "£", '£'}
config :release_test, :encoding, {:time_μs, :"£", "£", '£'}
+6 -2
View File
@@ -488,11 +488,15 @@ defmodule Mix.ReleaseTest do
end
test "writes sys_config with encoding" do
assert make_sys_config(release([]), [encoding: {:"£", "£", '£'}], "unused/runtime/path") ==
assert make_sys_config(
release([]),
[encoding: {:time_μs, :"£", "£", '£'}],
"unused/runtime/path"
) ==
:ok
{:ok, contents} = :file.consult(@sys_config)
assert contents == [[encoding: {:"£", "£", '£'}]]
assert contents == [[encoding: {:time_μs, :"£", "£", '£'}]]
end
test "writes the given sys_config with config providers" do
+12
View File
@@ -17,4 +17,16 @@ defmodule Mix.ShellTest do
after
Mix.shell(Mix.Shell.Process)
end
test "with :cd" do
Mix.shell(Mix.Shell.IO)
tmp_dir = System.tmp_dir()
{pwd, 0} = System.cmd("pwd", [], cd: tmp_dir)
assert ExUnit.CaptureIO.capture_io(fn ->
Mix.shell().cmd("pwd", cd: tmp_dir)
end) == pwd
after
Mix.shell(Mix.Shell.Process)
end
end

Some files were not shown because too many files have changed in this diff Show More