Compare commits

...
Author SHA1 Message Date
José Valim 47abe2d107 Release v1.17.2 2024-07-06 23:10:25 +02:00
José Valim e254e7e68e Fix regression when fetching repo with a ref
Also note that depth and ref cannot be used in combination.
2024-07-02 20:09:54 +02:00
Wojtek Mach d3c0b38c5d Improvements to generated rel/bin/app.bat (#13679)
* Validate value of `!RELEASE_MODE!`

  * Exit with `ERRORLEVEL` set to `1` on errors.

    Previously we did not and so apps that use releases and rely on exit
    codes to report errors were not doing so (e.g. Livebook).

  * Rename rpc node name from `rem-(...)` to `rpc-(...)`

  * Update generated `rel/env.bat.exs`
2024-07-02 15:21:38 +02:00
José Valim 662dd6b8d9 Add CHANGELOG entry on jaro_distance, closes #13696 2024-06-27 09:14:53 +02:00
José Valim cadd501cbd Clarify cookie generation, closes #13694 2024-06-26 13:28:57 +02:00
Alexey Nikitin ddf84aba7e Fix vm.args.eex generated file when remote? is false (#13688)
before
```
-start_epmd false -erl_epmd_port 6789false
```

after
```
-start_epmd false -erl_epmd_port 6789
```
2024-06-24 09:47:21 +02:00
Jean Klingler 2d808ec652 Fix logger crash when :gen_statem format_status/2 returns non-tuple (#13684) 2024-06-22 20:14:56 +09:00
José Valim 6bfb95ab88 Release v1.17.1 2024-06-18 13:11:00 +02:00
José Valim d53977002d Print type violation slogan 2024-06-18 12:46:59 +02:00
José Valim 776e5a34ea Do not flip async/sync modules in --repeat-until-failure 2024-06-15 18:00:29 +02:00
Jean Klingler ae5707f90a Fix bug when using pinned variable in with else block (#13667) 2024-06-15 18:49:37 +09:00
José Valim 67617e5d8c Do not warn for assignment with blocks in EEx 2024-06-15 11:05:45 +02:00
José Valim 6f5fc94276 Clarify docs, closes #13666 2024-06-15 10:20:40 +02:00
José Valim 6c935ca6ec Clarify charlists intro, closes #13663 2024-06-14 17:34:27 +02:00
José Valim d1076e487c Do not run fixpoint computations on runtime deps
If A depends on B and B depends on C, all as runtime
dependencies, and C changes, there is no change that
can happen in C that will change the result of running
the checker on A.

This could change in the future in two ways:

1. If we introduce inference of return types, then we
   need to infer and check the types for A again. This
   may be an argument for not introducing inference of
   return types (only of patterns and guards)

2. We will need to track "type exports" once we introduce
   type aliases
2024-06-14 12:07:02 +02:00
Jonatan Kłosko b9e447757d Update RELEASE_DISTRIBUTION docs (#13658) 2024-06-13 16:15:45 +02:00
Jean Klingler b7943e113d Fix dialyzer error when with else clause is calling a no_return function (#13659)
Close #13656
2024-06-13 16:15:32 +02:00
Ben Murden 1a320bb60d Minor grammatical fixes on warning message (#13654) 2024-06-13 16:15:25 +02:00
Ian Harris 64e63b1b9d Update compatibility-and-deprecations.md (#13657)
Fixed link for changelog to v1.17.
2024-06-12 19:12:55 +02:00
José Valim 5a8b28427c Update CHANGELOG 2024-06-12 15:20:57 +02:00
José Valim a1eb59d035 Remove /main link from CHANGELOG 2024-06-12 13:54:10 +02:00
José Valim 0b8623a032 Release v1.17.0 2024-06-12 12:23:37 +02:00
José Valim 1f187b529e Clarify typespecs handling 2024-06-11 21:13:57 +02:00
José Valim bde5b2d2fc s/detail/details 2024-06-10 22:38:23 +02:00
Maksym Verbovyi 357ffc497d Correct example in the docs (#13638) 2024-06-10 16:38:08 +02:00
Lukasz Samson 48338bc093 Fix crash when capturing ambiguous import 2024-06-10 16:37:34 +02:00
José Valim d547721c41 Add detail and span to typing diagnostics
Closes #13646.
2024-06-10 16:14:27 +02:00
Aleksei Matiushkin 615bb55e81 format_elixir: true option for file generation (#13644) 2024-06-06 18:35:54 +02:00
Jean Klingler 278cf65bb1 Revert types from #13552 (#13645) 2024-06-06 23:03:39 +09:00
Jean Klingler 125d55527d Add more information to anonymous rescue hint (#13641) 2024-06-06 21:15:48 +09:00
Travis Vander Hoop 9ffe44758c Update case.ex with correct :since for register_test/6 (#13642)
while addressing the `ExUnit.Case.register_test/3` deprecation in https://github.com/elixir-wallaby/wallaby/pull/776, I wondered how long `register_test/6` had been around. In looking at the source and the docs, I uncovered what I believe is an incorrect `since` attribute
2024-06-06 14:05:44 +02:00
Theodor Fiedler e2032e8dc4 Extend Duration shift documentation (#13640) 2024-06-06 00:05:22 +02:00
José Valim ac64fba4eb Release v1.17.0-rc.1 2024-06-03 14:40:04 +02:00
José Valim 78b7239eb7 Update CHANGELOG 2024-06-03 14:01:25 +02:00
Jean Klingler 021a698d5d Add :report option to aggregate results over multiple processes (#13632) 2024-06-03 20:48:15 +09:00
José Valim b3c3e1399d Use __info__ callback directly (#13630) 2024-06-02 16:17:27 +02:00
Łukasz Samson 18200af534 Allow to override the info callback in Macro.Env.define_import (#13628) 2024-06-02 16:17:24 +02:00
José Valim b63312ac2b Optimize :term (#13625) 2024-06-02 15:00:08 +02:00
José Valim db89cbf7e6 Avoid discarding information on conversion to #elixir_ex 2024-06-01 17:57:54 +02:00
José Valim 53c93b91da Avoid nesting of capture inside macros
Unfortunately this makes it so the unused capture
warnings emit false positives, so this particular
warning was removed.

Closes #13609.
2024-06-01 17:09:47 +02:00
José Valim a7bf120756 Fix race in registry tests from duplicate names 2024-05-31 22:37:50 +02:00
José Valim b56a907297 Type check the function type on fun.() 2024-05-30 20:37:41 +02:00
José Valim c7dfc33705 Simplify iso conversion, prefer Duration.new! 2024-05-29 00:06:37 +02:00
José Valim 4b1b852880 Remove code comment from release example 2024-05-29 00:06:34 +02:00
Tangui f9939a7029 Implement Duration.to_iso8601/1 (#13604) 2024-05-29 00:06:27 +02:00
Jean Klingler 0d6ae23e39 Add sort :per_call option to tprof (#13611) 2024-05-29 07:02:20 +09:00
Theodor Fiedler cf84b7b042 Support plus sign prefix in Calendar.ISO.parse_duration/1 (#13613) 2024-05-28 21:13:12 +02:00
Theodor Fiedler b84dd54ca1 Support negative period prefix in Calendar.ISO.parse_duration/1 (#13608) 2024-05-27 23:05:57 +02:00
Jean Klingler bb779221ff Implement mix profile.tprof (#13605) 2024-05-27 21:33:06 +02:00
Łukasz Samson be6ed1709d Keep position meta on & capture special variables in expanded AST (#13607) 2024-05-27 21:32:55 +02:00
Theodor Fiedler 79c19dcb04 Add Duration.from_iso8601/1 (#13473) 2024-05-27 11:48:07 +02:00
José Valim 772c7b0884 Add Keyword.intersect/2-3 2024-05-26 10:59:18 +02:00
José Valim 6250a17d8c Update CHANGELOG.md 2024-05-26 09:50:37 +02:00
José Valim 73d7762563 Avoid double tracing on captures 2024-05-25 11:13:29 +02:00
Yuri Pereira Constante ebe347410c Generate cover HTML files in parallel (#13596) 2024-05-24 20:27:34 +02:00
Juergen Braungardt d74e6314e4 Fix documentation of option :stderr_to_stdout for System.cmd (#13597) 2024-05-24 19:58:12 +02:00
José Valim c9a2afc0ce Use Mix.shell().error() for compiler tasks warnings 2024-05-24 19:57:24 +02:00
José Valim e4a455eedc Update CHANGELOG.md 2024-05-24 14:33:37 +02:00
José Valim a2600eace1 Release v1.17.0-rc.0 2024-05-24 12:38:20 +02:00
José Valim 27b6ea82ae Branch out v1.17 2024-05-24 12:37:32 +02:00
73 changed files with 2234 additions and 801 deletions
+58 -10
View File
@@ -1,8 +1,10 @@
# Changelog for Elixir v1.17 # Changelog for Elixir v1.17
This release includes type inference of patterns to provide warnings for an initial set of constructs (binaries, maps, and atoms) within the same function. It also includes a new Duration data type to interact with Calendar types, support for Erlang/OTP 27, and many other improvements.
## Warnings from gradual set-theoretic types ## Warnings from gradual set-theoretic types
This release introduces gradual set-theoretic types to infer types from patterns and guards and use them to type check programs, enabling the Elixir compiler to find faults and bugs in codebases without requiring changes to existing software. The underlying principles, theory, and roadmap of our work have been outlined in ["The Design Principles of the Elixir Type System" by Giuseppe Castagna, Guillaume Duboc, José Valim](https://arxiv.org/abs/2306.06391). This release introduces gradual set-theoretic types to infer types from patterns and use them to type check programs, enabling the Elixir compiler to find faults and bugs in codebases without requiring changes to existing software. The underlying principles, theory, and roadmap of our work have been outlined in ["The Design Principles of the Elixir Type System" by Giuseppe Castagna, Guillaume Duboc, José Valim](https://arxiv.org/abs/2306.06391).
At the moment, Elixir developers will interact with set-theoretic types only through warnings found by the type system. The current implementation models all data types in the language: At the moment, Elixir developers will interact with set-theoretic types only through warnings found by the type system. The current implementation models all data types in the language:
@@ -10,7 +12,7 @@ At the moment, Elixir developers will interact with set-theoretic types only thr
* `atom()` - it represents all atoms and it is divisible. For instance, the atom `:foo` and `:hello_world` are also valid (distinct) types. * `atom()` - it represents all atoms and it is divisible. For instance, the atom `:foo` and `:hello_world` are also valid (distinct) types.
* `map()` and structs - maps can be "closed" or "open". Closed maps only allow the specified allows keys, such as `%{key: atom(), value: integer()}`. Open maps support any other keys in addition to the ones listed and their definition starts with `...`, such as `%{..., key: atom(), value: integer()}`. Structs are closed maps with the `__struct__` key. * `map()` and structs - maps can be "closed" or "open". Closed maps only allow the specified keys, such as `%{key: atom(), value: integer()}`. Open maps support any other keys in addition to the ones listed and their definition starts with `...`, such as `%{..., key: atom(), value: integer()}`. Structs are closed maps with the `__struct__` key.
* `tuple()`, `list()`, and `function()` - currently they are modelled as indivisible types. The next Elixir versions will also introduce fine-grained support to them. * `tuple()`, `list()`, and `function()` - currently they are modelled as indivisible types. The next Elixir versions will also introduce fine-grained support to them.
@@ -20,25 +22,23 @@ We focused on atoms and maps on this initial release as they are respectively th
* Accessing a key on a map or a struct that does not have the given key, such as `user.adress` * Accessing a key on a map or a struct that does not have the given key, such as `user.adress`
* Updating a struct or a map that does not define the given key, such as `%{user | adress: ...}`
* Invoking a function on non-modules, such as `user.address()` * Invoking a function on non-modules, such as `user.address()`
* Capturing a function on non-modules, such as `&user.address/0` * Capturing a function on non-modules, such as `&user.address/0`
* Attempting to invoke to call an anonymous function without an actual function, such as `user.()`
* Performing structural comparisons with structs, such as `my_date < ~D[2010-04-17]` * Performing structural comparisons with structs, such as `my_date < ~D[2010-04-17]`
* Performing structural comparisons between non-overlapping types, such as `integer >= string` * Performing structural comparisons between non-overlapping types, such as `integer >= string`
* Building and pattern matching on binaries without the relevant specifiers, such as `<<string>>` (this warns because by default it expects an integer) * Building and pattern matching on binaries without the relevant specifiers, such as `<<name>>` (this warns because by default it expects an integer, it should have been `<<name::binary>>` instead)
* Attempting to rescue an undefined exception or an exception that is not a struct * Attempting to rescue an undefined exception or a struct that is not an exception
* Accessing a field that is not defined in a rescued exception * Accessing a field that is not defined in a rescued exception
These new warnings help Elixir developers find bugs earlier and give more confidence when refactoring code, especially around maps and structs. While some of these warnings were emitted in the past, they were discovered using syntax analysis. The new warnings are more reliable, precise, and with better error messages. Keep in mind that not all maps have statically known keys, and the Elixir typechecker only infers types from patterns at the moment. These new warnings help Elixir developers find bugs earlier and give more confidence when refactoring code, especially around maps and structs. While some of these warnings were emitted in the past, they were discovered using syntax analysis. The new warnings are more reliable, precise, and with better error messages. Keep in mind, however, that the Elixir typechecker only infers types from patterns within the same function at the moment. Analysis from guards and across function boundaries will be added in future relases. For more details, see our new [reference document on gradual set-theoretic types](https://hexdocs.pm/elixir/gradual-set-theoretic-types.html).
Future Elixir versions will infer and type check more constructs, bringing Elixir developers more warnings and quality of life improvements without changes to code. For more details, see our new [reference document on gradual set-theoretic types](https://hexdocs.pm/elixir/main/gradual-set-theoretic-types.html).
The type system was made possible thanks to a partnership between [CNRS](https://www.cnrs.fr/) and [Remote](https://remote.com/). The development work is currently sponsored by [Fresha](https://www.fresha.com/), [Starfish*](https://starfish.team/), and [Dashbit](https://dashbit.co/). The type system was made possible thanks to a partnership between [CNRS](https://www.cnrs.fr/) and [Remote](https://remote.com/). The development work is currently sponsored by [Fresha](https://www.fresha.com/), [Starfish*](https://starfish.team/), and [Dashbit](https://dashbit.co/).
@@ -70,7 +70,44 @@ Finally, a new `Kernel.to_timeout/1` function has been added, which helps develo
Process.send_after(pid, :wake_up, to_timeout(hour: 1)) Process.send_after(pid, :wake_up, to_timeout(hour: 1))
``` ```
## v1.17.0-dev ## v1.17.2 (2024-07-06)
### 1. Bug fixes
#### Logger
* [Logger.Translator] Fix logger crash when `:gen_statem`'s `format_status/2` returns non-tuple
#### Mix
* [mix deps.get] Fix regression when fetching a git repository with a `:ref`
* [mix release] Validate `RELEASE_MODE` and set ERRORLEVEL on `.bat` scripts
* [mix release] Fix invalid example in code comment inside the generated vm.args.eex
## v1.17.1 (2024-06-18)
### 1. Enhancements
#### Mix
* [mix compile.elixir] Do not run fixpoint computation on runtime dependencies. This should considerably improve compilation times for large projects when changing only one or two files
### 2. Bug fixes
#### EEx
* [EEx] Do not warn for assignment with blocks in EEx
#### Elixir
* [Kernel] Fix bug when using pinned variables inside `with`'s `else` patterns
* [Kernel] Fix Dialyzer error when with else clause is calling a `no_return` function
#### ExUnit
* [ExUnit] Do not alternative sync/async suites on `--repeat-until-failure`
## v1.17.0 (2024-06-12)
### 1. Enhancements ### 1. Enhancements
@@ -88,6 +125,7 @@ Process.send_after(pid, :wake_up, to_timeout(hour: 1))
* [Kernel] Add `Kernel.to_timeout/1` * [Kernel] Add `Kernel.to_timeout/1`
* [Kernel] Emit warnings for undefined functions from modules defined within the same context as the caller code * [Kernel] Emit warnings for undefined functions from modules defined within the same context as the caller code
* [Kernel] Support integers in uppercase sigils * [Kernel] Support integers in uppercase sigils
* [Keyword] Add `Keyword.intersect/2-3` to mirror the `Map` API
* [Macro] Add `Macro.Env.define_alias/4`, `Macro.Env.define_import/4`, `Macro.Env.define_require/4`, `Macro.Env.expand_alias/4`, `Macro.Env.expand_import/5`, and `Macro.Env.expand_require/6` to aid the implementation of language servers and embedded languages * [Macro] Add `Macro.Env.define_alias/4`, `Macro.Env.define_import/4`, `Macro.Env.define_require/4`, `Macro.Env.expand_alias/4`, `Macro.Env.expand_import/5`, and `Macro.Env.expand_require/6` to aid the implementation of language servers and embedded languages
* [NaiveDateTime] Add `NaiveDateTime.shift/2` to shift naive datetimes with duration and calendar-specific semantics * [NaiveDateTime] Add `NaiveDateTime.shift/2` to shift naive datetimes with duration and calendar-specific semantics
* [Process] Add `Process.set_label/1` * [Process] Add `Process.set_label/1`
@@ -117,9 +155,11 @@ Process.send_after(pid, :wake_up, to_timeout(hour: 1))
* [mix deps] Warn if `:optional` is used in combination with `:in_umbrella` * [mix deps] Warn if `:optional` is used in combination with `:in_umbrella`
* [mix deps.get] Do not add optional dependency requirements if its parent dep was skipped * [mix deps.get] Do not add optional dependency requirements if its parent dep was skipped
* [mix deps.tree] Add `--umbrella-only` to `mix deps.tree` * [mix deps.tree] Add `--umbrella-only` to `mix deps.tree`
* [mix profile.tprof] Add a new profiler, available on Erlang/OTP 27+, which can measure count, time, and heap usage
* [mix test] Add `mix test --breakpoints` that sets up a breakpoint before each test that will run * [mix test] Add `mix test --breakpoints` that sets up a breakpoint before each test that will run
* [mix test] Add `mix test --repeat-until-failure` to rerun tests until a failure occurs * [mix test] Add `mix test --repeat-until-failure` to rerun tests until a failure occurs
* [mix test] Add `mix test --slowest-modules` to print slowest modules based on all of the tests they hold * [mix test] Add `mix test --slowest-modules` to print slowest modules based on all of the tests they hold
* [mix test] Generate cover HTML files in parallel
### 2. Bug fixes ### 2. Bug fixes
@@ -132,7 +172,10 @@ Process.send_after(pid, :wake_up, to_timeout(hour: 1))
* [Kernel] Do not expand code in `quote bind_quoted: ...` twice * [Kernel] Do not expand code in `quote bind_quoted: ...` twice
* [Kernel] Respect `:line` property when `:file` is given as option to `quote` * [Kernel] Respect `:line` property when `:file` is given as option to `quote`
* [Kernel] Do not crash on `Macro.escape/2` when passing a quote triplet without valid meta * [Kernel] Do not crash on `Macro.escape/2` when passing a quote triplet without valid meta
* [Kernel] Avoid double tracing events when capturing a function
* [Kernel] Fix a bug where captured arguments would conflict when a capture included a macro that also used captures
* [Module] Return default value in `Module.get_attribute/3` for persisted attributes which have not yet been written to * [Module] Return default value in `Module.get_attribute/3` for persisted attributes which have not yet been written to
* [String] Properly handle transpositions in `jaro_distance`. This will correct the distance result in certain cases
#### IEx #### IEx
@@ -148,6 +191,11 @@ Process.send_after(pid, :wake_up, to_timeout(hour: 1))
* [GenServer] Deprecate `c:GenServer.format_status/2` callback to align with Erlang/OTP 25+ * [GenServer] Deprecate `c:GenServer.format_status/2` callback to align with Erlang/OTP 25+
#### Mix
* [mix profile.cprof] Deprecated in favor of the new `mix profile.tprof`
* [mix profile.eprof] Deprecated in favor of the new `mix profile.tprof`
### 4. Hard deprecations ### 4. Hard deprecations
#### Elixir #### Elixir
+1 -1
View File
@@ -2,7 +2,7 @@ PREFIX ?= /usr/local
TEST_FILES ?= "*_test.exs" TEST_FILES ?= "*_test.exs"
SHARE_PREFIX ?= $(PREFIX)/share SHARE_PREFIX ?= $(PREFIX)/share
MAN_PREFIX ?= $(SHARE_PREFIX)/man MAN_PREFIX ?= $(SHARE_PREFIX)/man
CANONICAL := main/ # CANONICAL := main/
ELIXIRC := bin/elixirc --ignore-module-conflict $(ELIXIRC_OPTS) ELIXIRC := bin/elixirc --ignore-module-conflict $(ELIXIRC_OPTS)
ERLC := erlc -I lib/elixir/include ERLC := erlc -I lib/elixir/include
ERL_MAKE := if [ -n "$(ERLC_OPTS)" ]; then ERL_COMPILER_OPTIONS=$(ERLC_OPTS) erl -make; else erl -make; fi ERL_MAKE := if [ -n "$(ERLC_OPTS)" ]; then ERL_COMPILER_OPTIONS=$(ERLC_OPTS) erl -make; else erl -make; fi
+7 -5
View File
@@ -8,15 +8,17 @@
3. Update "Compatibility and Deprecations" if a new OTP version is supported 3. Update "Compatibility and Deprecations" if a new OTP version is supported
4. Commit changes above with title "Release vVERSION", generate a new tag, and push it 4. Commit changes above with title "Release vVERSION" and push it
5. Wait until GitHub Actions publish artifacts to the draft release and the CI is green 6. Once GitHub actions completes, generate a new tag, and push it
6. Copy the relevant bits from /CHANGELOG.md to the GitHub release and publish it (link to the announcement if there is one) 7. Wait until GitHub Actions publish artifacts to the draft release
7. Update `_data/elixir-versions.yml` (except for RCs) in `elixir-lang/elixir-lang.github.com` 8. Copy the relevant bits from /CHANGELOG.md to the GitHub release and publish it (link to the announcement if there is one)
## Creating a new vMAJOR.MINOR branch (after first rc) 9. Update `_data/elixir-versions.yml` (except for RCs) in `elixir-lang/elixir-lang.github.com`
## Creating a new vMAJOR.MINOR branch (before first rc)
### In the new branch ### In the new branch
+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 Elixir version | Support
:------------- | :----------------------------- :------------- | :-----------------------------
1.17 | Development 1.17 | Bug fixes and security patches
1.16 | Bug fixes and security patches 1.16 | Security patches only
1.15 | Security patches only 1.15 | Security patches only
1.14 | Security patches only 1.14 | Security patches only
1.13 | Security patches only 1.13 | Security patches only
1.12 | Security patches only
## Announcements ## Announcements
+1 -1
View File
@@ -1 +1 @@
1.17.0-dev 1.17.2
+1 -1
View File
@@ -1,7 +1,7 @@
#!/bin/sh #!/bin/sh
set -e set -e
ELIXIR_VERSION=1.17.0-dev ELIXIR_VERSION=1.17.2
if [ $# -eq 0 ] || { [ $# -eq 1 ] && { [ "$1" = "--help" ] || [ "$1" = "-h" ]; }; }; then if [ $# -eq 0 ] || { [ $# -eq 1 ] && { [ "$1" = "--help" ] || [ "$1" = "-h" ]; }; }; then
cat <<USAGE >&2 cat <<USAGE >&2
+1 -1
View File
@@ -1,6 +1,6 @@
@echo off @echo off
set ELIXIR_VERSION=1.17.0-dev set ELIXIR_VERSION=1.17.2
if ""%1""=="""" if ""%2""=="""" goto documentation if ""%1""=="""" if ""%2""=="""" goto documentation
if /I ""%1""==""--help"" if ""%2""=="""" goto documentation if /I ""%1""==""--help"" if ""%2""=="""" goto documentation
+7 -7
View File
@@ -340,13 +340,6 @@ defmodule EEx.Compiler do
scope, scope,
state state
) do ) do
if mark == ~c"" do
message =
"the contents of this expression won't be output unless the EEx block starts with \"<%=\""
:elixir_errors.erl_warn({meta.line, meta.column}, state.file, message)
end
{rest, line, contents} = look_ahead_middle(rest, meta.line, chars) || {rest, meta.line, chars} {rest, line, contents} = look_ahead_middle(rest, meta.line, chars) || {rest, meta.line, chars}
start_line = meta.line start_line = meta.line
start_column = column(meta.column, mark) start_column = column(meta.column, mark)
@@ -359,6 +352,13 @@ defmodule EEx.Compiler do
%{state | quoted: [], line: line} %{state | quoted: [], line: line}
) )
if mark == ~c"" and not match?({:=, _, [_, _]}, contents) do
message =
"the contents of this expression won't be output unless the EEx block starts with \"<%=\""
:elixir_errors.erl_warn({meta.line, meta.column}, state.file, message)
end
buffer = state.engine.handle_expr(buffer, IO.chardata_to_string(mark), contents) buffer = state.engine.handle_expr(buffer, IO.chardata_to_string(mark), contents)
generate_buffer(rest, buffer, scope, state) generate_buffer(rest, buffer, scope, state)
end end
-9
View File
@@ -43,15 +43,6 @@ defmodule EEx.SmartEngineTest do
assert_received :found assert_received :found
end end
test "error with unused \"do\" block without \"<%=\" modifier" do
stderr =
ExUnit.CaptureIO.capture_io(:stderr, fn ->
assert_eval("", "<% if true do %>I'm invisible!<% end %>", assigns: %{})
end)
assert stderr =~ "the contents of this expression won't be output"
end
defp assert_eval(expected, actual, binding \\ []) do defp assert_eval(expected, actual, binding \\ []) do
result = EEx.eval_string(actual, binding, file: __ENV__.file, engine: EEx.SmartEngine) result = EEx.eval_string(actual, binding, file: __ENV__.file, engine: EEx.SmartEngine)
assert result == expected assert result == expected
+10
View File
@@ -543,6 +543,16 @@ defmodule EExTest do
~s[unexpected beginning of EEx tag \"<%=\" on \"<%= end %>\"] ~s[unexpected beginning of EEx tag \"<%=\" on \"<%= end %>\"]
end end
test "unused \"do\" block without \"<%=\" modifier" do
assert ExUnit.CaptureIO.capture_io(:stderr, fn ->
EEx.compile_string("<% if true do %>I'm invisible!<% end %>")
end) =~ "the contents of this expression won't be output"
# These are fine though
EEx.compile_string("<% foo = fn -> %>Hello<% end %>")
EEx.compile_string("<% foo = if true do %>Hello<% end %>")
end
test "from tokenizer" do test "from tokenizer" do
warning = warning =
ExUnit.CaptureIO.capture_io(:stderr, fn -> ExUnit.CaptureIO.capture_io(:stderr, fn ->
+3 -2
View File
@@ -764,8 +764,9 @@ defmodule Date do
When using the default ISO calendar, durations are collapsed and When using the default ISO calendar, durations are collapsed and
applied in the order of months and then days: applied in the order of months and then days:
- when shifting by 1 year and 2 months the date is actually shifted by 14 months
- when shifting by 2 weeks and 3 days the date is shifted by 17 days * when shifting by 1 year and 2 months the date is actually shifted by 14 months
* when shifting by 2 weeks and 3 days the date is shifted by 17 days
When shifting by month, days are rounded down to the nearest valid date. When shifting by month, days are rounded down to the nearest valid date.
+3 -2
View File
@@ -1708,8 +1708,9 @@ defmodule DateTime do
When using the default ISO calendar, durations are collapsed and When using the default ISO calendar, durations are collapsed and
applied in the order of months, then seconds and microseconds: applied in the order of months, then seconds and microseconds:
- when shifting by 1 year and 2 months the date is actually shifted by 14 months
- weeks, days and smaller units are collapsed into seconds and microseconds * when shifting by 1 year and 2 months the date is actually shifted by 14 months
* weeks, days and smaller units are collapsed into seconds and microseconds
When shifting by month, days are rounded down to the nearest valid date. When shifting by month, days are rounded down to the nearest valid date.
+159 -9
View File
@@ -26,7 +26,7 @@ defmodule Duration do
iex> Date.shift(~D[2016-01-03], Duration.new!(month: 2)) iex> Date.shift(~D[2016-01-03], Duration.new!(month: 2))
~D[2016-03-03] ~D[2016-03-03]
It is important to notice that shifting is not an arithmetic operation. It is important to note that shifting is not an arithmetic operation.
For example, adding `date + 1 month + 1 month` does not yield the same For example, adding `date + 1 month + 1 month` does not yield the same
result as `date + 2 months`. Let's see an example: result as `date + 2 months`. Let's see an example:
@@ -46,9 +46,19 @@ defmodule Duration do
* larger units (such as years and months) are applied before * larger units (such as years and months) are applied before
smaller ones (such as weeks, hours, days, and so on) smaller ones (such as weeks, hours, days, and so on)
* units are collapsed into months (`:year` and `:month`),
seconds (`:week`, `:day`, `:hour`, `:minute`, `:second`)
and microseconds (`:microsecond`) before they are applied
* 1 year is equivalent to 12 months, 1 week is equivalent to 7 days.
Therefore, 4 weeks _are not_ equivalent to 1 month
* in case of non-existing dates, the results are rounded down to the * in case of non-existing dates, the results are rounded down to the
nearest valid date nearest valid date
As the `shift/2` functions are calendar aware, they are guaranteed to return
valid date/times, considering leap years as well as DST in applicable time zones.
## Intervals ## Intervals
Durations in Elixir can be combined with stream operations to build intervals. Durations in Elixir can be combined with stream operations to build intervals.
@@ -124,6 +134,8 @@ defmodule Duration do
""" """
@type duration :: t | [unit_pair] @type duration :: t | [unit_pair]
@microseconds_per_second 1_000_000
@doc """ @doc """
Creates a new `Duration` struct from given `unit_pairs`. Creates a new `Duration` struct from given `unit_pairs`.
@@ -181,9 +193,9 @@ defmodule Duration do
## Examples ## Examples
iex> Duration.add(%Duration{week: 2, day: 1}, %Duration{day: 2}) iex> Duration.add(Duration.new!(week: 2, day: 1), Duration.new!(day: 2))
%Duration{week: 2, day: 3} %Duration{week: 2, day: 3}
iex> Duration.add(%Duration{microsecond: {400, 3}}, %Duration{microsecond: {600, 6}}) iex> Duration.add(Duration.new!(microsecond: {400, 3}), Duration.new!(microsecond: {600, 6}))
%Duration{microsecond: {1000, 6}} %Duration{microsecond: {1000, 6}}
""" """
@@ -211,9 +223,9 @@ defmodule Duration do
## Examples ## Examples
iex> Duration.subtract(%Duration{week: 2, day: 1}, %Duration{day: 2}) iex> Duration.subtract(Duration.new!(week: 2, day: 1), Duration.new!(day: 2))
%Duration{week: 2, day: -1} %Duration{week: 2, day: -1}
iex> Duration.subtract(%Duration{microsecond: {400, 6}}, %Duration{microsecond: {600, 3}}) iex> Duration.subtract(Duration.new!(microsecond: {400, 6}), Duration.new!(microsecond: {600, 3}))
%Duration{microsecond: {-200, 6}} %Duration{microsecond: {-200, 6}}
""" """
@@ -239,9 +251,9 @@ defmodule Duration do
## Examples ## Examples
iex> Duration.multiply(%Duration{day: 1, minute: 15, second: -10}, 3) iex> Duration.multiply(Duration.new!(day: 1, minute: 15, second: -10), 3)
%Duration{day: 3, minute: 45, second: -30} %Duration{day: 3, minute: 45, second: -30}
iex> Duration.multiply(%Duration{microsecond: {200, 4}}, 3) iex> Duration.multiply(Duration.new!(microsecond: {200, 4}), 3)
%Duration{microsecond: {600, 4}} %Duration{microsecond: {600, 4}}
""" """
@@ -264,9 +276,9 @@ defmodule Duration do
## Examples ## Examples
iex> Duration.negate(%Duration{day: 1, minute: 15, second: -10}) iex> Duration.negate(Duration.new!(day: 1, minute: 15, second: -10))
%Duration{day: -1, minute: -15, second: 10} %Duration{day: -1, minute: -15, second: 10}
iex> Duration.negate(%Duration{microsecond: {500000, 4}}) iex> Duration.negate(Duration.new!(microsecond: {500000, 4}))
%Duration{microsecond: {-500000, 4}} %Duration{microsecond: {-500000, 4}}
""" """
@@ -283,4 +295,142 @@ defmodule Duration do
microsecond: {-ms, p} microsecond: {-ms, p}
} }
end end
@doc """
Parses an [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601#Durations) formatted duration string to a `Duration` struct.
Duration strings, as well as individual units, may be prefixed with plus/minus signs so that:
- `-PT6H3M` parses as `%Duration{hour: -6, minute: -3}`
- `-PT6H-3M` parses as `%Duration{hour: -6, minute: 3}`
- `+PT6H3M` parses as `%Duration{hour: 6, minute: 3}`
- `+PT6H-3M` parses as `%Duration{hour: 6, minute: -3}`
Duration designators must be provided in order of magnitude: `P[n]Y[n]M[n]W[n]DT[n]H[n]M[n]S`.
Only seconds may be specified with a decimal fraction, using either a comma or a full stop: `P1DT4,5S`.
## Examples
iex> Duration.from_iso8601("P1Y2M3DT4H5M6S")
{:ok, %Duration{year: 1, month: 2, day: 3, hour: 4, minute: 5, second: 6}}
iex> Duration.from_iso8601("P3Y-2MT3H")
{:ok, %Duration{year: 3, month: -2, hour: 3}}
iex> Duration.from_iso8601("-PT10H-30M")
{:ok, %Duration{hour: -10, minute: 30}}
iex> Duration.from_iso8601("PT4.650S")
{:ok, %Duration{second: 4, microsecond: {650000, 3}}}
"""
@spec from_iso8601(String.t()) :: {:ok, t} | {:error, atom}
def from_iso8601(string) when is_binary(string) do
case Calendar.ISO.parse_duration(string) do
{:ok, duration} ->
{:ok, new!(duration)}
error ->
error
end
end
@doc """
Same as `from_iso8601/1` but raises an `ArgumentError`.
## Examples
iex> Duration.from_iso8601!("P1Y2M3DT4H5M6S")
%Duration{year: 1, month: 2, day: 3, hour: 4, minute: 5, second: 6}
iex> Duration.from_iso8601!("P10D")
%Duration{day: 10}
"""
@spec from_iso8601!(String.t()) :: t
def from_iso8601!(string) when is_binary(string) do
case from_iso8601(string) do
{:ok, duration} ->
duration
{:error, reason} ->
raise ArgumentError, ~s/failed to parse duration "#{string}". reason: #{inspect(reason)}/
end
end
@doc """
Converts the given `duration` to an [ISO 8601-2:2019](https://en.wikipedia.org/wiki/ISO_8601) formatted string.
Note this function implements the *extension* of ISO 8601:2019. This extensions allows weeks to
appear between months and days: `P3M3W3D`, making it fully compatible with any `Duration` struct.
## Examples
iex> Duration.to_iso8601(Duration.new!(year: 3))
"P3Y"
iex> Duration.to_iso8601(Duration.new!(day: 40, hour: 12, minute: 42, second: 12))
"P40DT12H42M12S"
iex> Duration.to_iso8601(Duration.new!(second: 30))
"PT30S"
iex> Duration.to_iso8601(Duration.new!([]))
"PT0S"
iex> Duration.to_iso8601(Duration.new!(second: 1, microsecond: {2_200, 3}))
"PT1.002S"
iex> Duration.to_iso8601(Duration.new!(second: 1, microsecond: {-1_200_000, 4}))
"PT-0.2000S"
"""
@spec to_iso8601(t) :: String.t()
def to_iso8601(%Duration{} = duration) do
case {to_iso8601_duration_date(duration), to_iso8601_duration_time(duration)} do
{[], []} -> "PT0S"
{date, time} -> IO.iodata_to_binary([?P, date, time])
end
end
defp to_iso8601_duration_date(%{year: 0, month: 0, week: 0, day: 0}) do
[]
end
defp to_iso8601_duration_date(%{year: year, month: month, week: week, day: day}) do
[pair(year, ?Y), pair(month, ?M), pair(week, ?W), pair(day, ?D)]
end
defp to_iso8601_duration_time(%{hour: 0, minute: 0, second: 0, microsecond: {0, _}}) do
[]
end
defp to_iso8601_duration_time(%{hour: hour, minute: minute} = d) do
[?T, pair(hour, ?H), pair(minute, ?M), second_component(d)]
end
defp second_component(%{second: 0, microsecond: {0, _}}) do
[]
end
defp second_component(%{second: 0, microsecond: {_, 0}}) do
~c"0S"
end
defp second_component(%{second: second, microsecond: {_, 0}}) do
[Integer.to_string(second), ?S]
end
defp second_component(%{second: second, microsecond: {ms, p}}) do
total_ms = second * @microseconds_per_second + ms
second = total_ms |> div(@microseconds_per_second) |> abs()
ms = total_ms |> rem(@microseconds_per_second) |> abs()
sign = if total_ms < 0, do: ?-, else: []
[
sign,
Integer.to_string(second),
?.,
ms |> Integer.to_string() |> String.pad_leading(6, "0") |> binary_part(0, p),
?S
]
end
@compile {:inline, pair: 2}
defp pair(0, _key), do: []
defp pair(num, key), do: [Integer.to_string(num), key]
end end
+76 -2
View File
@@ -18,7 +18,8 @@ defmodule Calendar.ISO do
The standard library supports a minimal set of possible ISO 8601 features. The standard library supports a minimal set of possible ISO 8601 features.
Specifically, the parser only supports calendar dates and does not support Specifically, the parser only supports calendar dates and does not support
ordinal and week formats. ordinal and week formats. Additionally, it supports parsing ISO 8601
formatted durations, including negative time units and fractional seconds.
By default Elixir only parses extended-formatted date/times. You can opt-in By default Elixir only parses extended-formatted date/times. You can opt-in
to parse basic-formatted date/times. to parse basic-formatted date/times.
@@ -29,7 +30,7 @@ defmodule Calendar.ISO do
Elixir does not support reduced accuracy formats (for example, a date without Elixir does not support reduced accuracy formats (for example, a date without
the day component) nor decimal precisions in the lowest component (such as the day component) nor decimal precisions in the lowest component (such as
`10:01:25,5`). No functions exist to parse ISO 8601 durations or time intervals. `10:01:25,5`).
#### Examples #### Examples
@@ -663,6 +664,79 @@ defmodule Calendar.ISO do
end end
end end
@doc """
Parses an ISO 8601 formatted duration string to a list of `Duration` compabitble unit pairs.
See `Duration.from_iso8601/1`.
"""
@doc since: "1.17.0"
@spec parse_duration(String.t()) :: {:ok, [Duration.unit_pair()]} | {:error, atom}
def parse_duration("P" <> string) when byte_size(string) > 0 do
parse_duration_date(string, [], year: ?Y, month: ?M, week: ?W, day: ?D)
end
def parse_duration("+P" <> string) when byte_size(string) > 0 do
parse_duration_date(string, [], year: ?Y, month: ?M, week: ?W, day: ?D)
end
def parse_duration("-P" <> string) when byte_size(string) > 0 do
with {:ok, fields} <- parse_duration_date(string, [], year: ?Y, month: ?M, week: ?W, day: ?D) do
{:ok,
Enum.map(fields, fn
{:microsecond, {value, precision}} -> {:microsecond, {-value, precision}}
{unit, value} -> {unit, -value}
end)}
end
end
def parse_duration(_) do
{:error, :invalid_duration}
end
defp parse_duration_date("", acc, _allowed), do: {:ok, acc}
defp parse_duration_date("T" <> string, acc, _allowed) when byte_size(string) > 0 do
parse_duration_time(string, acc, hour: ?H, minute: ?M, second: ?S)
end
defp parse_duration_date(string, acc, allowed) do
with {integer, <<next, rest::binary>>} <- Integer.parse(string),
{key, allowed} <- find_unit(allowed, next) do
parse_duration_date(rest, [{key, integer} | acc], allowed)
else
_ -> {:error, :invalid_date_component}
end
end
defp parse_duration_time("", acc, _allowed), do: {:ok, acc}
defp parse_duration_time(string, acc, allowed) do
case Integer.parse(string) do
{second, <<delimiter, _::binary>> = rest} when delimiter in [?., ?,] ->
case parse_microsecond(rest) do
{{ms, precision}, "S"} ->
ms = if second > 0, do: ms, else: -ms
{:ok, [second: second, microsecond: {ms, precision}] ++ acc}
_ ->
{:error, :invalid_time_component}
end
{integer, <<next, rest::binary>>} ->
case find_unit(allowed, next) do
{key, allowed} -> parse_duration_time(rest, [{key, integer} | acc], allowed)
false -> {:error, :invalid_time_component}
end
_ ->
{:error, :invalid_time_component}
end
end
defp find_unit([{key, unit} | rest], unit), do: {key, rest}
defp find_unit([_ | rest], unit), do: find_unit(rest, unit)
defp find_unit([], _unit), do: false
@doc """ @doc """
Returns the `t:Calendar.iso_days/0` format of the specified date. Returns the `t:Calendar.iso_days/0` format of the specified date.
+3 -2
View File
@@ -578,8 +578,9 @@ defmodule NaiveDateTime do
When using the default ISO calendar, durations are collapsed and When using the default ISO calendar, durations are collapsed and
applied in the order of months, then seconds and microseconds: applied in the order of months, then seconds and microseconds:
- when shifting by 1 year and 2 months the date is actually shifted by 14 months
- weeks, days and smaller units are collapsed into seconds and microseconds * when shifting by 1 year and 2 months the date is actually shifted by 14 months
* weeks, days and smaller units are collapsed into seconds and microseconds
When shifting by month, days are rounded down to the nearest valid date. When shifting by month, days are rounded down to the nearest valid date.
+3 -3
View File
@@ -1212,7 +1212,7 @@ defmodule SyntaxError do
}) })
when not is_nil(snippet) and not is_nil(column) do when not is_nil(snippet) and not is_nil(column) do
snippet = snippet =
:elixir_errors.format_snippet({line, column}, file, description, snippet, :error, [], nil) :elixir_errors.format_snippet(:error, {line, column}, file, description, snippet, %{})
format_message(file, line, column, snippet) format_message(file, line, column, snippet)
end end
@@ -1225,7 +1225,7 @@ defmodule SyntaxError do
description: description description: description
}) do }) do
snippet = snippet =
:elixir_errors.format_snippet({line, column}, file, description, nil, :error, [], nil) :elixir_errors.format_snippet(:error, {line, column}, file, description, nil, %{})
padded = " " <> String.replace(snippet, "\n", "\n ") padded = " " <> String.replace(snippet, "\n", "\n ")
format_message(file, line, column, padded) format_message(file, line, column, padded)
@@ -1316,7 +1316,7 @@ defmodule TokenMissingError do
description: description description: description
}) do }) do
snippet = snippet =
:elixir_errors.format_snippet({line, column}, file, description, snippet, :error, [], nil) :elixir_errors.format_snippet(:error, {line, column}, file, description, snippet, %{})
format_message(file, line, column, snippet) format_message(file, line, column, snippet)
end end
+30
View File
@@ -960,6 +960,36 @@ defmodule Keyword do
:lists.sort(left) === :lists.sort(right) :lists.sort(left) === :lists.sort(right)
end end
@doc """
Intersects two keyword lists, returning a keyword with the common keys.
By default, it returns the values of the intersected keys in `keyword2`.
The keys are returned in the order found in `keyword1`.
## Examples
iex> Keyword.intersect([a: 1, b: 2], [b: "b", c: "c"])
[b: "b"]
iex> Keyword.intersect([a: 1, b: 2], [b: 2, c: 3], fn _k, v1, v2 ->
...> v1 + v2
...> end)
[b: 4]
"""
@doc since: "1.17.0"
@spec intersect(keyword, keyword, (key, value, value -> value)) :: keyword
def intersect(keyword1, keyword2, fun \\ fn _key, _v1, v2 -> v2 end)
def intersect([{k, v1} | keyword1], keyword2, fun) do
case :lists.keyfind(k, 1, keyword2) do
{_, v2} -> [{k, fun.(k, v1, v2)} | intersect(keyword1, keyword2, fun)]
false -> intersect(keyword1, keyword2, fun)
end
end
def intersect([], _keyword2, _fun), do: []
@doc """ @doc """
Merges two keyword lists into one. Merges two keyword lists into one.
+2 -4
View File
@@ -495,10 +495,8 @@ defmodule Macro do
""" """
@doc since: "1.11.3" @doc since: "1.11.3"
@spec generate_unique_arguments(0, context :: atom) :: [] @spec generate_unique_arguments(0, context :: atom) :: []
@spec generate_unique_arguments(pos_integer, context) :: [ @spec generate_unique_arguments(pos_integer, context) ::
{atom, [counter: integer], context}, [{atom, [counter: integer], context}, ...]
...
]
when context: atom when context: atom
def generate_unique_arguments(amount, context), def generate_unique_arguments(amount, context),
do: generate_arguments(amount, context, &unique_var/2) do: generate_arguments(amount, context, &unique_var/2)
+20 -3
View File
@@ -349,6 +349,11 @@ defmodule Macro.Env do
* #{trace_option} * #{trace_option}
* `:info_callback` - a function to use instead of `c:Module.__info__/1`.
The function will be invoked with `:functions` or `:macros` argument.
It has to return a list of `{function, arity}` key value pairs.
If it fails, it defaults to using module metadata based on `module_info/1`.
## Examples ## Examples
iex> env = __ENV__ iex> env = __ENV__
@@ -367,15 +372,27 @@ defmodule Macro.Env do
iex> Macro.Env.lookup_import(env, {:is_odd, 1}) iex> Macro.Env.lookup_import(env, {:is_odd, 1})
[{:macro, Integer}] [{:macro, Integer}]
## Info callback override
iex> env = __ENV__
iex> Macro.Env.lookup_import(env, {:flatten, 1})
[]
iex> {:ok, env} = Macro.Env.define_import(env, [line: 10], SomeModule, [info_callback: fn :functions -> [{:flatten, 1}]; :macros -> [{:some, 2}]; end])
iex> Macro.Env.lookup_import(env, {:flatten, 1})
[{:function, SomeModule}]
iex> Macro.Env.lookup_import(env, {:some, 2})
[{:macro, SomeModule}]
""" """
@doc since: "1.17.0" @doc since: "1.17.0"
@spec define_import(t, Macro.metadata(), module) :: {:ok, t} | {:error, String.t()} @spec define_import(t, Macro.metadata(), module, keyword) :: {:ok, t} | {:error, String.t()}
def define_import(env, meta, module, opts \\ []) def define_import(env, meta, module, opts \\ [])
when is_list(meta) and is_atom(module) and is_list(opts) do when is_list(meta) and is_atom(module) and is_list(opts) do
{trace, opts} = Keyword.pop(opts, :trace, true) {trace, opts} = Keyword.pop(opts, :trace, true)
{warnings, opts} = Keyword.pop(opts, :emit_warnings, true) {warnings, opts} = Keyword.pop(opts, :emit_warnings, true)
{info_callback, opts} = Keyword.pop(opts, :info_callback, &module.__info__/1)
result = :elixir_import.import(meta, module, opts, env, warnings, trace) result = :elixir_import.import(meta, module, opts, env, warnings, trace, info_callback)
maybe_define_error(result, :elixir_import) maybe_define_error(result, :elixir_import)
end end
@@ -583,7 +600,7 @@ defmodule Macro.Env do
:elixir_dispatch.check_deprecated(:macro, meta, receiver, name, arity, env) :elixir_dispatch.check_deprecated(:macro, meta, receiver, name, arity, env)
end end
quoted = expander.(args, :elixir_env.env_to_ex(env)) quoted = expander.(args, env)
next = :elixir_module.next_counter(env.module) next = :elixir_module.next_counter(env.module)
:elixir_quote.linify_with_context_counter(expansion_meta, {receiver, next}, quoted) :elixir_quote.linify_with_context_counter(expansion_meta, {receiver, next}, quoted)
end end
+18 -14
View File
@@ -284,7 +284,11 @@ defmodule Module.Behaviour do
end end
end end
def format_warning({:undefined_behaviour, module, behaviour}) do def format_diagnostic(warning) do
%{message: IO.iodata_to_binary(format_warning(warning))}
end
defp format_warning({:undefined_behaviour, module, behaviour}) do
[ [
"@behaviour ", "@behaviour ",
inspect(behaviour), inspect(behaviour),
@@ -294,12 +298,12 @@ defmodule Module.Behaviour do
] ]
end end
def format_warning({:module_does_not_define_behaviour, module, behaviour}) do defp format_warning({:module_does_not_define_behaviour, module, behaviour}) do
["module ", inspect(behaviour), " is not a behaviour (in module ", inspect(module), ")"] ["module ", inspect(behaviour), " is not a behaviour (in module ", inspect(module), ")"]
end end
def format_warning({:duplicate_behaviour, module, behaviour, conflict, kind, callback}) defp format_warning({:duplicate_behaviour, module, behaviour, conflict, kind, callback})
when conflict == behaviour do when conflict == behaviour do
[ [
"the behaviour ", "the behaviour ",
inspect(behaviour), inspect(behaviour),
@@ -311,7 +315,7 @@ defmodule Module.Behaviour do
] ]
end end
def format_warning({:duplicate_behaviour, module, behaviour, conflict, kind, callback}) do defp format_warning({:duplicate_behaviour, module, behaviour, conflict, kind, callback}) do
[ [
"conflicting behaviours found. Callback ", "conflicting behaviours found. Callback ",
format_definition(kind, callback), format_definition(kind, callback),
@@ -325,7 +329,7 @@ defmodule Module.Behaviour do
] ]
end end
def format_warning({:missing_callback, module, callback, kind, behaviour}) do defp format_warning({:missing_callback, module, callback, kind, behaviour}) do
[ [
format_callback(callback, kind, behaviour), format_callback(callback, kind, behaviour),
" is not implemented (in module ", " is not implemented (in module ",
@@ -334,7 +338,7 @@ defmodule Module.Behaviour do
] ]
end end
def format_warning({:callback_mismatch, module, callback, kind, wrong_kind, behaviour}) do defp format_warning({:callback_mismatch, module, callback, kind, wrong_kind, behaviour}) do
[ [
format_callback(callback, kind, behaviour), format_callback(callback, kind, behaviour),
" was implemented as \"", " was implemented as \"",
@@ -347,14 +351,14 @@ defmodule Module.Behaviour do
] ]
end end
def format_warning({:private_function, callback, kind}) do defp format_warning({:private_function, callback, kind}) do
[ [
format_definition(kind, callback), format_definition(kind, callback),
" is private, @impl attribute is always discarded for private functions/macros" " is private, @impl attribute is always discarded for private functions/macros"
] ]
end end
def format_warning({:no_behaviours, callback, kind, value}) do defp format_warning({:no_behaviours, callback, kind, value}) do
[ [
"got \"@impl ", "got \"@impl ",
inspect(value), inspect(value),
@@ -364,7 +368,7 @@ defmodule Module.Behaviour do
] ]
end end
def format_warning({:impl_not_defined, callback, kind, {_fa, behaviour}}) do defp format_warning({:impl_not_defined, callback, kind, {_fa, behaviour}}) do
[ [
"got \"@impl false\" for ", "got \"@impl false\" for ",
format_definition(kind, callback), format_definition(kind, callback),
@@ -373,7 +377,7 @@ defmodule Module.Behaviour do
] ]
end end
def format_warning({:impl_defined, callback, kind, callbacks}) do defp format_warning({:impl_defined, callback, kind, callbacks}) do
[ [
"got \"@impl true\" for ", "got \"@impl true\" for ",
format_definition(kind, callback), format_definition(kind, callback),
@@ -382,7 +386,7 @@ defmodule Module.Behaviour do
] ]
end end
def format_warning({:behaviour_not_declared, callback, kind, behaviour}) do defp format_warning({:behaviour_not_declared, callback, kind, behaviour}) do
[ [
"got \"@impl ", "got \"@impl ",
inspect(behaviour), inspect(behaviour),
@@ -392,7 +396,7 @@ defmodule Module.Behaviour do
] ]
end end
def format_warning({:behaviour_not_defined, callback, kind, behaviour, callbacks}) do defp format_warning({:behaviour_not_defined, callback, kind, behaviour, callbacks}) do
[ [
"got \"@impl ", "got \"@impl ",
inspect(behaviour), inspect(behaviour),
@@ -403,7 +407,7 @@ defmodule Module.Behaviour do
] ]
end end
def format_warning({:missing_impl, callback, kind, behaviour}) do defp format_warning({:missing_impl, callback, kind, behaviour}) do
[ [
"module attribute @impl was not set for ", "module attribute @impl was not set for ",
format_definition(kind, callback), format_definition(kind, callback),
+11 -11
View File
@@ -259,7 +259,7 @@ defmodule Module.ParallelChecker do
definitions definitions
) )
warnings = diagnostics =
module module
|> Module.Types.warnings(file, definitions, no_warn_undefined, cache) |> Module.Types.warnings(file, definitions, no_warn_undefined, cache)
|> Kernel.++(behaviour_warnings) |> Kernel.++(behaviour_warnings)
@@ -270,7 +270,7 @@ defmodule Module.ParallelChecker do
|> Map.get(:after_verify, []) |> Map.get(:after_verify, [])
|> Enum.each(fn {verify_mod, verify_fun} -> apply(verify_mod, verify_fun, [module]) end) |> Enum.each(fn {verify_mod, verify_fun} -> apply(verify_mod, verify_fun, [module]) end)
warnings diagnostics
end end
defp extract_no_warn_undefined(compile_opts) do defp extract_no_warn_undefined(compile_opts) do
@@ -302,31 +302,31 @@ defmodule Module.ParallelChecker do
defp emit_warnings(warnings, log?) do defp emit_warnings(warnings, log?) do
Enum.flat_map(warnings, fn {module, warning, locations} -> Enum.flat_map(warnings, fn {module, warning, locations} ->
message = module.format_warning(warning) %{message: _} = diagnostic = module.format_diagnostic(warning)
diagnostics = Enum.map(locations, &to_diagnostic(message, &1)) diagnostics = Enum.map(locations, &to_diagnostic(diagnostic, &1))
log? and print_warning(message, diagnostics) log? and print_diagnostics(diagnostics)
diagnostics diagnostics
end) end)
end end
defp print_warning(message, [diagnostic]) do defp print_diagnostics([diagnostic]) do
:elixir_errors.print_warning(message, diagnostic) :elixir_errors.print_diagnostic(diagnostic, true)
end end
defp print_warning(message, grouped_warnings) do defp print_diagnostics(diagnostics) do
:elixir_errors.print_warning_group(message, grouped_warnings) :elixir_errors.print_diagnostics(diagnostics)
end end
defp to_diagnostic(message, {file, position, mfa}) when is_list(position) do defp to_diagnostic(diagnostic, {file, position, mfa}) when is_list(position) do
%{ %{
severity: :warning, severity: :warning,
source: file, source: file,
file: file, file: file,
position: position_to_tuple(position), position: position_to_tuple(position),
message: IO.iodata_to_binary(message),
stacktrace: [to_stacktrace(file, position, mfa)], stacktrace: [to_stacktrace(file, position, mfa)],
span: nil span: nil
} }
|> Map.merge(diagnostic)
end end
defp position_to_tuple(position) do defp position_to_tuple(position) do
+194 -88
View File
@@ -34,18 +34,17 @@ defmodule Module.Types.Descr do
@atom_top {:negation, :sets.new(version: 2)} @atom_top {:negation, :sets.new(version: 2)}
@map_top [{:open, %{}, []}] @map_top [{:open, %{}, []}]
@map_empty [{:closed, %{}, []}] @map_empty [{:closed, %{}, []}]
# Guard helpers
@term %{bitmap: @bit_top, atom: @atom_top, map: @map_top}
@none %{} @none %{}
@dynamic %{dynamic: @term}
# Type definitions # Type definitions
def dynamic(), do: @dynamic def dynamic(), do: %{dynamic: :term}
def term(), do: @term
def none(), do: @none def none(), do: @none
def term(), do: :term
defp unfold(:term), do: unfolded_term()
defp unfold(other), do: other
defp unfolded_term, do: %{bitmap: @bit_top, atom: @atom_top, map: @map_top}
def atom(as), do: %{atom: atom_new(as)} def atom(as), do: %{atom: atom_new(as)}
def atom(), do: %{atom: @atom_top} def atom(), do: %{atom: @atom_top}
@@ -84,14 +83,27 @@ defmodule Module.Types.Descr do
@term_or_optional %{bitmap: @bit_top ||| @bit_optional, atom: @atom_top, map: @map_top} @term_or_optional %{bitmap: @bit_top ||| @bit_optional, atom: @atom_top, map: @map_top}
def not_set(), do: @not_set def not_set(), do: @not_set
def if_set(type), do: Map.update(type, :bitmap, @bit_optional, &(&1 ||| @bit_optional))
defp term_or_optional(), do: @term_or_optional defp term_or_optional(), do: @term_or_optional
def if_set(:term), do: term_or_optional()
def if_set(type), do: Map.update(type, :bitmap, @bit_optional, &(&1 ||| @bit_optional))
defguardp is_optional(map)
when is_map(map) and
((is_map_key(map, :bitmap) and (map.bitmap &&& @bit_optional) != 0) or
(is_map_key(map, :dynamic) and is_map(map.dynamic) and
is_map_key(map.dynamic, :bitmap) and
(map.dynamic.bitmap &&& @bit_optional) != 0))
defguardp is_optional_static(map)
when is_map(map) and is_map_key(map, :bitmap) and (map.bitmap &&& @bit_optional) != 0
## Set operations ## Set operations
def term_type?(@term), do: true def term_type?(:term), do: true
def term_type?(descr), do: subtype_static(@term, Map.delete(descr, :dynamic)) def term_type?(descr), do: subtype_static(unfolded_term(), Map.delete(descr, :dynamic))
def gradual?(:term), do: false
def gradual?(descr), do: is_map_key(descr, :dynamic) def gradual?(descr), do: is_map_key(descr, :dynamic)
@doc """ @doc """
@@ -102,14 +114,19 @@ defmodule Module.Types.Descr do
def dynamic(descr) do def dynamic(descr) do
case descr do case descr do
%{dynamic: dynamic} -> %{dynamic: dynamic} %{dynamic: dynamic} -> %{dynamic: dynamic}
%{} -> %{dynamic: descr} _ -> %{dynamic: descr}
end end
end end
@doc """ @doc """
Computes the union of two descrs. Computes the union of two descrs.
""" """
def union(%{} = left, %{} = right) do def union(:term, other) when not is_optional(other), do: :term
def union(other, :term) when not is_optional(other), do: :term
def union(left, right) do
left = unfold(left)
right = unfold(right)
is_gradual_left = gradual?(left) is_gradual_left = gradual?(left)
is_gradual_right = gradual?(right) is_gradual_right = gradual?(right)
@@ -136,7 +153,12 @@ defmodule Module.Types.Descr do
@doc """ @doc """
Computes the intersection of two descrs. Computes the intersection of two descrs.
""" """
def intersection(%{} = left, %{} = right) do def intersection(:term, other) when not is_optional(other), do: other
def intersection(other, :term) when not is_optional(other), do: other
def intersection(left, right) do
left = unfold(left)
right = unfold(right)
is_gradual_left = gradual?(left) is_gradual_left = gradual?(left)
is_gradual_right = gradual?(right) is_gradual_right = gradual?(right)
@@ -164,14 +186,19 @@ defmodule Module.Types.Descr do
@doc """ @doc """
Computes the difference between two types. Computes the difference between two types.
""" """
def difference(left = %{}, right = %{}) do def difference(other, :term) when not is_optional(other), do: none()
def difference(left, right) do
left = unfold(left)
right = unfold(right)
if gradual?(left) or gradual?(right) do if gradual?(left) or gradual?(right) do
{left_dynamic, left_static} = Map.pop(left, :dynamic, left) {left_dynamic, left_static} = Map.pop(left, :dynamic, left)
{right_dynamic, right_static} = Map.pop(right, :dynamic, right) {right_dynamic, right_static} = Map.pop(right, :dynamic, right)
dynamic_part = difference_static(left_dynamic, right_static) dynamic_part = difference_static(left_dynamic, right_static)
if empty?(dynamic_part), if empty?(dynamic_part),
do: @none, do: none(),
else: Map.put(difference_static(left_static, right_dynamic), :dynamic, dynamic_part) else: Map.put(difference_static(left_static, right_dynamic), :dynamic, dynamic_part)
else else
difference_static(left, right) difference_static(left, right)
@@ -179,8 +206,10 @@ defmodule Module.Types.Descr do
end end
# For static types, the difference is component-wise. # For static types, the difference is component-wise.
defp difference_static(left, :term) when not is_optional_static(left), do: none()
defp difference_static(left, right) do defp difference_static(left, right) do
iterator_difference(:maps.next(:maps.iterator(right)), left) iterator_difference(:maps.next(:maps.iterator(unfold(right))), unfold(left))
end end
# Returning 0 from the callback is taken as none() for that subtype. # Returning 0 from the callback is taken as none() for that subtype.
@@ -193,7 +222,8 @@ defmodule Module.Types.Descr do
@doc """ @doc """
Compute the negation of a type. Compute the negation of a type.
""" """
def negation(%{} = descr), do: difference(term(), descr) def negation(:term), do: none()
def negation(%{} = descr), do: difference(unfolded_term(), descr)
@doc """ @doc """
Check if a type is empty. Check if a type is empty.
@@ -203,18 +233,26 @@ defmodule Module.Types.Descr do
(bitmap, atom) are checked first for speed since, if they are present, (bitmap, atom) are checked first for speed since, if they are present,
the type is non-empty as we normalize then during construction. the type is non-empty as we normalize then during construction.
""" """
def empty?(%{} = descr) do def empty?(:term), do: false
descr = Map.get(descr, :dynamic, descr)
descr == @none or def empty?(%{} = descr) do
(not Map.has_key?(descr, :bitmap) and not Map.has_key?(descr, :atom) and case Map.get(descr, :dynamic, descr) do
(not Map.has_key?(descr, :map) or map_empty?(descr.map))) :term ->
false
value when value == @none ->
true
descr ->
not Map.has_key?(descr, :bitmap) and not Map.has_key?(descr, :atom) and
(not Map.has_key?(descr, :map) or map_empty?(descr.map))
end
end end
@doc """ @doc """
Converts a descr to its quoted representation. Converts a descr to its quoted representation.
""" """
def to_quoted(%{} = descr) do def to_quoted(descr) do
if term_type?(descr) do if term_type?(descr) do
{:term, [], []} {:term, [], []}
else else
@@ -260,7 +298,11 @@ defmodule Module.Types.Descr do
Because of the dynamic/static invariant in the `descr`, subtyping can be Because of the dynamic/static invariant in the `descr`, subtyping can be
simplified in several cases according to which type is gradual or not. simplified in several cases according to which type is gradual or not.
""" """
def subtype?(%{} = left, %{} = right) do def subtype?(left, :term) when not is_optional(left), do: true
def subtype?(left, right) do
left = unfold(left)
right = unfold(right)
is_grad_left = gradual?(left) is_grad_left = gradual?(left)
is_grad_right = gradual?(right) is_grad_right = gradual?(right)
@@ -278,6 +320,7 @@ defmodule Module.Types.Descr do
end end
end end
defp subtype_static(same, same), do: true
defp subtype_static(left, right), do: empty?(difference_static(left, right)) defp subtype_static(left, right), do: empty?(difference_static(left, right))
@doc """ @doc """
@@ -305,14 +348,18 @@ defmodule Module.Types.Descr do
include `dynamic()`, `integer()`, but also `dynamic() and (integer() or atom())`. include `dynamic()`, `integer()`, but also `dynamic() and (integer() or atom())`.
Incompatible subtypes include `integer() or list()`, `dynamic() and atom()`. Incompatible subtypes include `integer() or list()`, `dynamic() and atom()`.
""" """
def compatible?(input_type, expected_type) do def compatible?(left, :term) when not is_optional(left), do: true
{input_dynamic, input_static} = Map.pop(input_type, :dynamic, input_type)
expected_dynamic = Map.get(expected_type, :dynamic, expected_type)
if empty?(input_static) do def compatible?(left, right) do
not empty?(intersection(input_dynamic, expected_dynamic)) left = unfold(left)
right = unfold(right)
{left_dynamic, left_static} = Map.pop(left, :dynamic, left)
right_dynamic = Map.get(right, :dynamic, right)
if empty?(left_static) do
not empty?(intersection(left_dynamic, right_dynamic))
else else
subtype_static(input_static, expected_dynamic) subtype_static(left_static, right_dynamic)
end end
end end
@@ -321,30 +368,38 @@ defmodule Module.Types.Descr do
@doc """ @doc """
Optimized version of `not empty?(intersection(binary(), type))`. Optimized version of `not empty?(intersection(binary(), type))`.
""" """
def binary_type?(:term), do: true
def binary_type?(%{dynamic: :term}), do: true
def binary_type?(%{dynamic: %{bitmap: bitmap}}) when (bitmap &&& @bit_binary) != 0, do: true def binary_type?(%{dynamic: %{bitmap: bitmap}}) when (bitmap &&& @bit_binary) != 0, do: true
def binary_type?(%{bitmap: bitmap}) when (bitmap &&& @bit_binary) != 0, do: true def binary_type?(%{bitmap: bitmap}) when (bitmap &&& @bit_binary) != 0, do: true
def binary_type?(%{}), do: false def binary_type?(_), do: false
@doc """ @doc """
Optimized version of `not empty?(intersection(integer(), type))`. Optimized version of `not empty?(intersection(integer(), type))`.
""" """
def integer_type?(:term), do: true
def integer_type?(%{dynamic: :term}), do: true
def integer_type?(%{dynamic: %{bitmap: bitmap}}) when (bitmap &&& @bit_integer) != 0, do: true def integer_type?(%{dynamic: %{bitmap: bitmap}}) when (bitmap &&& @bit_integer) != 0, do: true
def integer_type?(%{bitmap: bitmap}) when (bitmap &&& @bit_integer) != 0, do: true def integer_type?(%{bitmap: bitmap}) when (bitmap &&& @bit_integer) != 0, do: true
def integer_type?(%{}), do: false def integer_type?(_), do: false
@doc """ @doc """
Optimized version of `not empty?(intersection(float(), type))`. Optimized version of `not empty?(intersection(float(), type))`.
""" """
def float_type?(:term), do: true
def float_type?(%{dynamic: :term}), do: true
def float_type?(%{dynamic: %{bitmap: bitmap}}) when (bitmap &&& @bit_float) != 0, do: true def float_type?(%{dynamic: %{bitmap: bitmap}}) when (bitmap &&& @bit_float) != 0, do: true
def float_type?(%{bitmap: bitmap}) when (bitmap &&& @bit_float) != 0, do: true def float_type?(%{bitmap: bitmap}) when (bitmap &&& @bit_float) != 0, do: true
def float_type?(%{}), do: false def float_type?(_), do: false
@doc """ @doc """
Optimized version of `not empty?(intersection(integer() or float(), type))`. Optimized version of `not empty?(intersection(integer() or float(), type))`.
""" """
def number_type?(:term), do: true
def number_type?(%{dynamic: :term}), do: true
def number_type?(%{dynamic: %{bitmap: bitmap}}) when (bitmap &&& @bit_number) != 0, do: true def number_type?(%{dynamic: %{bitmap: bitmap}}) when (bitmap &&& @bit_number) != 0, do: true
def number_type?(%{bitmap: bitmap}) when (bitmap &&& @bit_number) != 0, do: true def number_type?(%{bitmap: bitmap}) when (bitmap &&& @bit_number) != 0, do: true
def number_type?(%{}), do: false def number_type?(_), do: false
defp bitmap_union(v1, v2), do: v1 ||| v2 defp bitmap_union(v1, v2), do: v1 ||| v2
defp bitmap_intersection(v1, v2), do: v1 &&& v2 defp bitmap_intersection(v1, v2), do: v1 &&& v2
@@ -370,6 +425,29 @@ defmodule Module.Types.Descr do
do: {type, [], []} do: {type, [], []}
end end
## Funs
@doc """
Checks there is a function type (and only functions) with said arity.
"""
def fun_fetch(:term, _arity), do: :error
def fun_fetch(%{} = descr, _arity) do
{static_or_dynamic, static} = Map.pop(descr, :dynamic, descr)
if fun_only?(static) do
case static_or_dynamic do
:term -> :ok
%{bitmap: bitmap} when (bitmap &&& @bit_fun) != 0 -> :ok
%{} -> :error
end
else
:error
end
end
defp fun_only?(descr), do: empty?(difference(descr, fun()))
## Atoms ## Atoms
# The atom component of a type consists of pairs `{tag, set}` where `set` is a # The atom component of a type consists of pairs `{tag, set}` where `set` is a
@@ -388,25 +466,18 @@ defmodule Module.Types.Descr do
# an empty list of atoms. It is simplified to `0` in set operations, and the key # an empty list of atoms. It is simplified to `0` in set operations, and the key
# is removed from the map. # is removed from the map.
@doc """
Optimized version of `not empty?(intersection(atom(), type))`.
"""
def atom_type?(%{dynamic: %{atom: _}}), do: true
def atom_type?(%{atom: _}), do: true
def atom_type?(%{}), do: false
@doc """ @doc """
Optimized version of `not empty?(intersection(atom([atom]), type))`. Optimized version of `not empty?(intersection(atom([atom]), type))`.
""" """
def atom_type?(%{} = descr, atom) do def atom_type?(:term, _atom), do: true
{static_or_dynamic, static} = Map.pop(descr, :dynamic, descr)
atom_only?(static) and def atom_type?(%{} = descr, atom) do
case static_or_dynamic do case Map.get(descr, :dynamic, descr) do
%{atom: {:union, set}} -> :sets.is_element(atom, set) :term -> true
%{atom: {:negation, set}} -> not :sets.is_element(atom, set) %{atom: {:union, set}} -> :sets.is_element(atom, set)
%{} -> false %{atom: {:negation, set}} -> not :sets.is_element(atom, set)
end %{} -> false
end
end end
@doc """ @doc """
@@ -416,11 +487,14 @@ defmodule Module.Types.Descr do
`:error` otherwise. Notice `known_set` may be empty in infinite `:error` otherwise. Notice `known_set` may be empty in infinite
cases, due to negations. cases, due to negations.
""" """
def atom_fetch(:term), do: :error
def atom_fetch(%{} = descr) do def atom_fetch(%{} = descr) do
{static_or_dynamic, static} = Map.pop(descr, :dynamic, descr) {static_or_dynamic, static} = Map.pop(descr, :dynamic, descr)
if atom_only?(static) do if atom_only?(static) do
case static_or_dynamic do case static_or_dynamic do
:term -> {:infinite, []}
%{atom: {:union, set}} -> {:finite, :sets.to_list(set)} %{atom: {:union, set}} -> {:finite, :sets.to_list(set)}
%{atom: {:negation, _}} -> {:infinite, []} %{atom: {:negation, _}} -> {:infinite, []}
%{} -> :error %{} -> :error
@@ -547,19 +621,21 @@ defmodule Module.Types.Descr do
# `:dynamic` field is not_set, or it contains a type equal to the static component # `:dynamic` field is not_set, or it contains a type equal to the static component
# (that is, there are no extra dynamic values). # (that is, there are no extra dynamic values).
defp dynamic_intersection(left, right) do defp dynamic_union(:term, other) when not is_optional_static(other), do: :term
inter = symmetrical_intersection(left, right, &intersection/3) defp dynamic_union(other, :term) when not is_optional_static(other), do: :term
if empty?(inter), do: 0, else: inter
end
defp dynamic_difference(left, right) do defp dynamic_union(left, right),
diff = difference_static(left, right) do: symmetrical_merge(unfold(left), unfold(right), &union/3)
if empty?(diff), do: 0, else: diff
end
defp dynamic_union(left, right), do: symmetrical_merge(left, right, &union/3) defp dynamic_intersection(:term, other) when not is_optional_static(other), do: other
defp dynamic_intersection(other, :term) when not is_optional_static(other), do: other
defp dynamic_to_quoted(%{} = descr) do defp dynamic_intersection(left, right),
do: symmetrical_intersection(unfold(left), unfold(right), &intersection/3)
defp dynamic_difference(left, right), do: difference_static(left, right)
defp dynamic_to_quoted(descr) do
cond do cond do
term_type?(descr) -> [{:dynamic, [], []}] term_type?(descr) -> [{:dynamic, [], []}]
single = indivisible_bitmap(descr) -> [single] single = indivisible_bitmap(descr) -> [single]
@@ -607,6 +683,10 @@ defmodule Module.Types.Descr do
end end
end end
defp map_descr_pairs([{key, :term} | rest], acc, dynamic?) do
map_descr_pairs(rest, [{key, :term} | acc], dynamic?)
end
defp map_descr_pairs([{key, value} | rest], acc, dynamic?) do defp map_descr_pairs([{key, value} | rest], acc, dynamic?) do
case :maps.take(:dynamic, value) do case :maps.take(:dynamic, value) do
:error -> map_descr_pairs(rest, [{key, value} | acc], dynamic?) :error -> map_descr_pairs(rest, [{key, value} | acc], dynamic?)
@@ -618,9 +698,6 @@ defmodule Module.Types.Descr do
{acc, dynamic?} {acc, dynamic?}
end end
defp optional?(%{bitmap: bitmap}) when (bitmap &&& @bit_optional) != 0, do: true
defp optional?(_), do: false
defp map_tag_to_type(:open), do: term_or_optional() defp map_tag_to_type(:open), do: term_or_optional()
defp map_tag_to_type(:closed), do: not_set() defp map_tag_to_type(:closed), do: not_set()
@@ -635,11 +712,13 @@ defmodule Module.Types.Descr do
In static mode, we likely want to raise if `map.field` In static mode, we likely want to raise if `map.field`
(or pattern matching?) is called on an optional key. (or pattern matching?) is called on an optional key.
""" """
def map_fetch(:term, _key), do: :badmap
def map_fetch(%{} = descr, key) do def map_fetch(%{} = descr, key) do
case :maps.take(:dynamic, descr) do case :maps.take(:dynamic, descr) do
:error -> :error ->
if is_map_key(descr, :map) and map_only?(descr) do if is_map_key(descr, :map) and map_only?(descr) do
{static_optional?, static_type} = map_fetch_static(descr, key) |> pop_optional() {static_optional?, static_type} = map_fetch_static(descr, key)
if static_optional? or empty?(static_type) do if static_optional? or empty?(static_type) do
:badkey :badkey
@@ -650,13 +729,13 @@ defmodule Module.Types.Descr do
:badmap :badmap
end end
{%{map: {:open, fields, []}}, static} when fields == %{} and static == @none -> {:term, _static} ->
{true, dynamic()} {true, dynamic()}
{dynamic, static} -> {dynamic, static} ->
if is_map_key(dynamic, :map) and map_only?(static) do if is_map_key(dynamic, :map) and map_only?(static) do
{dynamic_optional?, dynamic_type} = map_fetch_static(dynamic, key) |> pop_optional() {dynamic_optional?, dynamic_type} = map_fetch_static(dynamic, key)
{static_optional?, static_type} = map_fetch_static(static, key) |> pop_optional() {static_optional?, static_type} = map_fetch_static(static, key)
if static_optional? or empty?(dynamic_type) do if static_optional? or empty?(dynamic_type) do
:badkey :badkey
@@ -673,12 +752,26 @@ defmodule Module.Types.Descr do
defp map_fetch_static(descr, key) when is_atom(key) do defp map_fetch_static(descr, key) when is_atom(key) do
case descr do case descr do
%{map: map} -> Enum.reduce(map_split_on_key(map, key), none(), &union/2) # Optimization: if the key does not exist in the map,
%{} -> none() # avoid building if_set/not_set pairs and return the
# popped value directly.
%{map: [{tag, fields, []}]} when not is_map_key(fields, key) ->
case tag do
:open -> {true, term()}
:closed -> {true, none()}
end
%{map: map} ->
map_split_on_key(map, key)
|> Enum.reduce(none(), &union/2)
|> pop_optional_static()
%{} ->
{false, none()}
end end
end end
defp pop_optional(type) do defp pop_optional_static(type) do
case type do case type do
%{bitmap: @bit_optional} -> %{bitmap: @bit_optional} ->
{true, Map.delete(type, :bitmap)} {true, Map.delete(type, :bitmap)}
@@ -707,6 +800,10 @@ defmodule Module.Types.Descr do
:empty -> acc :empty -> acc
end end
end end
|> case do
[] -> 0
acc -> acc
end
end end
# Intersects two map literals; throws if their intersection is empty. # Intersects two map literals; throws if their intersection is empty.
@@ -776,6 +873,10 @@ defmodule Module.Types.Descr do
end) end)
end) end)
end) end)
|> case do
[] -> 0
acc -> acc
end
end end
# Emptiness checking for maps. # Emptiness checking for maps.
@@ -800,7 +901,7 @@ defmodule Module.Types.Descr do
# The key is not shared between positive and negative maps, # The key is not shared between positive and negative maps,
# and because the negative type is required, there is no value in common # and because the negative type is required, there is no value in common
tag == :closed and not optional?(neg_type) -> tag == :closed and not is_optional_static(neg_type) ->
false false
# The key is not shared between positive and negative maps, # The key is not shared between positive and negative maps,
@@ -821,7 +922,7 @@ defmodule Module.Types.Descr do
empty?(diff) or map_empty?(tag, Map.put(fields, key, diff), negs) empty?(diff) or map_empty?(tag, Map.put(fields, key, diff), negs)
%{} -> %{} ->
if neg_tag == :closed and not optional?(type) do if neg_tag == :closed and not is_optional_static(type) do
false false
else else
# an absent key in a open negative map can be ignored # an absent key in a open negative map can be ignored
@@ -835,19 +936,24 @@ defmodule Module.Types.Descr do
# Takes a map dnf and a key and returns a list of unions of types # Takes a map dnf and a key and returns a list of unions of types
# for that key. It has to traverse both fields and negative entries. # for that key. It has to traverse both fields and negative entries.
defp map_split_on_key(dnf, key) do defp map_split_on_key(dnf, key) do
Enum.flat_map(dnf, fn {tag, fields, negs} -> Enum.flat_map(dnf, fn
# %{...} the open map in a positive intersection can be ignored # Optimization: if there are no negatives,
{fst, snd} = # we can return the value directly.
if tag == :open and fields == %{} do {_tag, %{^key => value}, []} ->
{term_or_optional(), term_or_optional()} [value]
else
map_pop_key(tag, fields, key)
end
case map_split_negative(negs, key, []) do # Optimization: if there are no negatives
:empty -> [] # and the key does not exist, return the default one.
negative -> negative |> pair_make_disjoint() |> pair_eliminate_negations(fst, snd) {tag, %{}, []} ->
end [map_tag_to_type(tag)]
{tag, fields, negs} ->
{fst, snd} = map_pop_key(tag, fields, key)
case map_split_negative(negs, key, []) do
:empty -> []
negative -> negative |> pair_make_disjoint() |> pair_eliminate_negations(fst, snd)
end
end) end)
end end
@@ -883,11 +989,11 @@ defmodule Module.Types.Descr do
defp map_empty_negation?(tag, fields, {neg_tag, neg_fields}) do defp map_empty_negation?(tag, fields, {neg_tag, neg_fields}) do
(tag == :closed and (tag == :closed and
Enum.any?(neg_fields, fn {neg_key, neg_type} -> Enum.any?(neg_fields, fn {neg_key, neg_type} ->
not is_map_key(fields, neg_key) and not optional?(neg_type) not is_map_key(fields, neg_key) and not is_optional_static(neg_type)
end)) or end)) or
(neg_tag == :closed and (neg_tag == :closed and
Enum.any?(fields, fn {key, type} -> Enum.any?(fields, fn {key, type} ->
not is_map_key(neg_fields, key) and not optional?(type) not is_map_key(neg_fields, key) and not is_optional_static(type)
end)) end))
end end
@@ -944,7 +1050,7 @@ defmodule Module.Types.Descr do
keyword? = Inspect.List.keyword?(sorted) keyword? = Inspect.List.keyword?(sorted)
for {key, type} <- sorted, for {key, type} <- sorted,
not (tag == :open and optional?(type) and term_type?(type)) do not (tag == :open and is_optional_static(type) and term_type?(type)) do
key = key =
if keyword? do if keyword? do
{:__block__, [format: :keyword], [key]} {:__block__, [format: :keyword], [key]}
@@ -953,7 +1059,7 @@ defmodule Module.Types.Descr do
end end
cond do cond do
not optional?(type) -> {key, to_quoted(type)} not is_optional_static(type) -> {key, to_quoted(type)}
empty?(type) -> {key, {:not_set, [], []}} empty?(type) -> {key, {:not_set, [], []}}
true -> {key, {:if_set, [], [to_quoted(type)]}} true -> {key, {:if_set, [], [to_quoted(type)]}}
end end
@@ -1096,7 +1202,7 @@ defmodule Module.Types.Descr do
%{^key => v2} -> %{^key => v2} ->
case fun.(key, v1, v2) do case fun.(key, v1, v2) do
0 -> acc 0 -> acc
[] -> acc value when value == @none -> acc
value -> [{key, value} | acc] value -> [{key, value} | acc]
end end
+27 -20
View File
@@ -301,9 +301,15 @@ defmodule Module.Types.Expr do
# TODO: fun.(args) # TODO: fun.(args)
def of_expr({{:., _meta1, [fun]}, _meta2, args}, stack, context) do def of_expr({{:., _meta1, [fun]}, _meta2, args}, stack, context) do
with {:ok, _fun_type, context} <- of_expr(fun, stack, context), with {:ok, fun_type, context} <- of_expr(fun, stack, context),
{:ok, _arg_types, context} <- {:ok, _args_types, context} <-
map_reduce_ok(args, context, &of_expr(&1, stack, &2)) do map_reduce_ok(args, context, &of_expr(&1, stack, &2)) do
context =
case fun_fetch(fun_type, length(args)) do
:ok -> context
:error -> Of.incompatible_warn(fun, fun(), fun_type, stack, context)
end
{:ok, dynamic(), context} {:ok, dynamic(), context}
end end
end end
@@ -393,11 +399,7 @@ defmodule Module.Types.Expr do
expected = if structs == [], do: @exception, else: Enum.reduce(structs, &union/2) expected = if structs == [], do: @exception, else: Enum.reduce(structs, &union/2)
formatter = fn expr -> formatter = fn expr ->
[ {"rescue #{expr_to_string(expr)} ->", hints}
"rescue #{expr_to_string(expr)} ->" |> indent(4),
?\n,
format_hints(hints)
]
end end
{:ok, _type, context} = Of.refine_var(var, expected, expr, formatter, stack, context) {:ok, _type, context} = Of.refine_var(var, expected, expr, formatter, stack, context)
@@ -531,23 +533,28 @@ defmodule Module.Types.Expr do
## Warning formatting ## Warning formatting
def format_warning({:badupdate, type, expr, expected_type, actual_type, context}) do def format_diagnostic({:badupdate, type, expr, expected_type, actual_type, context}) do
[ traces = Of.collect_traces(expr, context)
"""
incompatible types in #{type} update:
#{expr_to_string(expr) |> indent(4)} %{
details: %{typing_traces: traces},
message:
IO.iodata_to_binary([
"""
incompatible types in #{type} update:
expected type: #{expr_to_string(expr) |> indent(4)}
#{to_quoted_string(expected_type) |> indent(4)} expected type:
but got type: #{to_quoted_string(expected_type) |> indent(4)}
#{to_quoted_string(actual_type) |> indent(4)} but got type:
""",
Of.format_traces(expr, context), #{to_quoted_string(actual_type) |> indent(4)}
"\ntyping violation found at:" """,
] Of.format_traces(traces)
])
}
end end
end end
+3 -1
View File
@@ -41,7 +41,9 @@ defmodule Module.Types.Helpers do
""" """
#{hint()} when you rescue without specifying exception names, \ #{hint()} when you rescue without specifying exception names, \
the variable is assigned a type of a struct but all of its fields are unknown the variable is assigned a type of a struct but all of its fields are unknown. \
If you are trying to access an exception's :message key, either specify the \
exception names or use `Exception.message/1`.
""" """
end) end)
end end
+227 -153
View File
@@ -47,8 +47,7 @@ defmodule Module.Types.Of do
# We need to return error otherwise it leads to cascading errors # We need to return error otherwise it leads to cascading errors
if empty?(new_type) do if empty?(new_type) do
{:error, {:error, warn({:refine_var, old_type, type, var, context}, meta, stack, context)}
warn(__MODULE__, {:refine_var, old_type, type, var, context}, meta, stack, context)}
else else
{:ok, new_type, context} {:ok, new_type, context}
end end
@@ -420,7 +419,7 @@ defmodule Module.Types.Of do
meta = get_meta(expr) || stack.meta meta = get_meta(expr) || stack.meta
hints = if meta[:inferred_bitstring_spec], do: [:inferred_bitstring_spec], else: [] hints = if meta[:inferred_bitstring_spec], do: [:inferred_bitstring_spec], else: []
warning = {:incompatible, expr, expected_type, actual_type, hints, context} warning = {:incompatible, expr, expected_type, actual_type, hints, context}
warn(__MODULE__, warning, meta, stack, context) warn(warning, meta, stack, context)
end end
defp warn(warning, meta, stack, context) do defp warn(warning, meta, stack, context) do
@@ -429,15 +428,24 @@ defmodule Module.Types.Of do
## Traces ## Traces
def format_traces(expr, %{vars: vars}) do def collect_traces(expr, %{vars: vars}) do
{_, versions} = {_, versions} =
Macro.prewalk(expr, %{}, fn Macro.prewalk(expr, %{}, fn
{var_name, meta, var_context}, versions when is_atom(var_name) and is_atom(var_context) -> {var_name, meta, var_context}, versions when is_atom(var_name) and is_atom(var_context) ->
version = meta[:version] version = meta[:version]
case vars do case vars do
%{^version => data} -> {:ok, Map.put(versions, version, data)} %{^version => %{off_traces: [_ | _] = off_traces, name: name, context: context}} ->
%{} -> {:ok, versions} {:ok,
Map.put(versions, version, %{
type: :variable,
name: name,
context: context,
traces: collect_var_traces(off_traces)
})}
_ ->
{:ok, versions}
end end
node, versions -> node, versions ->
@@ -447,40 +455,55 @@ defmodule Module.Types.Of do
versions versions
|> Map.values() |> Map.values()
|> Enum.sort_by(& &1.name) |> Enum.sort_by(& &1.name)
|> Enum.map(&format_trace/1)
end end
defp format_trace(%{off_traces: []}) do defp collect_var_traces(traces) do
[] traces
|> Enum.reverse()
|> Enum.map(fn {expr, file, type, formatter} ->
meta = get_meta(expr)
{formatted_expr, formatter_hints} =
case formatter do
:default -> {expr_to_string(expr), []}
formatter -> formatter.(expr)
end
%{
file: file,
meta: meta,
formatted_expr: formatted_expr,
formatted_hints: format_hints(formatter_hints ++ expr_hints(expr)),
formatted_type: to_quoted_string(type)
}
end)
end end
defp format_trace(%{name: name, context: context, off_traces: traces}) do def format_traces(traces) do
Enum.map(traces, &format_trace/1)
end
defp format_trace(%{type: :variable, name: name, context: context, traces: traces}) do
traces = traces =
traces for trace <- traces do
|> Enum.reverse()
|> Enum.map(fn {expr, file, type, formatter} ->
meta = get_meta(expr)
location = location =
file trace.file
|> Path.relative_to_cwd() |> Path.relative_to_cwd()
|> Exception.format_file_line(meta[:line]) |> Exception.format_file_line(trace.meta[:line])
|> String.replace_suffix(":", "") |> String.replace_suffix(":", "")
[ [
""" """
# type: #{to_quoted_string(type) |> indent(4)} # type: #{indent(trace.formatted_type, 4)}
# from: #{location} # from: #{location}
\ \
""", """,
case formatter do indent(trace.formatted_expr, 4),
:default -> [expr |> expr_to_string() |> indent(4), ?\n] ?\n,
formatter -> formatter.(expr) trace.formatted_hints
end,
format_hints(expr_hints(expr))
] ]
end) end
type_or_types = pluralize(traces, "type", "types") type_or_types = pluralize(traces, "type", "types")
["\nwhere #{format_var(name, context)} was given the #{type_or_types}:\n" | traces] ["\nwhere #{format_var(name, context)} was given the #{type_or_types}:\n" | traces]
@@ -500,96 +523,169 @@ defmodule Module.Types.Of do
## Warning formatting ## Warning formatting
def format_warning({:refine_var, old_type, new_type, var, context}) do def format_diagnostic({:refine_var, old_type, new_type, var, context}) do
[ traces = collect_traces(var, context)
"""
incompatible types assigned to #{format_var(var)}:
#{to_quoted_string(old_type)} !~ #{to_quoted_string(new_type)} %{
""", details: %{typing_traces: traces},
format_traces(var, context), message:
"\ntyping violation found at:" IO.iodata_to_binary([
] """
incompatible types assigned to #{format_var(var)}:
#{to_quoted_string(old_type)} !~ #{to_quoted_string(new_type)}
""",
format_traces(traces)
])
}
end end
def format_warning({:incompatible, expr, expected_type, actual_type, hints, context}) do def format_diagnostic({:incompatible, expr, expected_type, actual_type, hints, context}) do
[ traces = collect_traces(expr, context)
"""
incompatible types in expression:
#{expr_to_string(expr) |> indent(4)} %{
details: %{typing_traces: traces},
message:
IO.iodata_to_binary([
"""
incompatible types in expression:
expected type: #{expr_to_string(expr) |> indent(4)}
#{to_quoted_string(expected_type) |> indent(4)} expected type:
but got type: #{to_quoted_string(expected_type) |> indent(4)}
#{to_quoted_string(actual_type) |> indent(4)} but got type:
""",
format_traces(expr, context), #{to_quoted_string(actual_type) |> indent(4)}
format_hints(hints), """,
"\ntyping violation found at:" format_traces(traces),
] format_hints(hints)
])
}
end end
def format_warning({:badmap, expr, type, key, context}) do def format_diagnostic({:badmap, expr, type, key, context}) do
[ traces = collect_traces(expr, context)
"""
expected a map or struct when accessing .#{key} in expression:
#{expr_to_string(expr) |> indent(4)} %{
""", details: %{typing_traces: traces},
empty_if(dot_var?(expr), """ message:
IO.iodata_to_binary([
"""
expected a map or struct when accessing .#{key} in expression:
but got type: #{expr_to_string(expr) |> indent(4)}
""",
empty_if(dot_var?(expr), """
#{to_quoted_string(type) |> indent(4)} but got type:
"""),
format_traces(expr, context), #{to_quoted_string(type) |> indent(4)}
format_hints([:dot]), """),
"\ntyping violation found at:" format_traces(traces),
] format_hints([:dot])
])
}
end end
def format_warning({:badkey, expr, type, key, context}) do def format_diagnostic({:badkey, expr, type, key, context}) do
[ traces = collect_traces(expr, context)
"""
unknown key .#{key} in expression:
#{expr_to_string(expr) |> indent(4)} %{
""", details: %{typing_traces: traces},
empty_if(dot_var?(expr), """ span: expr |> get_meta() |> :elixir_env.calculate_span(key) |> Keyword.get(:span),
message:
IO.iodata_to_binary([
"""
unknown key .#{key} in expression:
the given type does not have the given key: #{expr_to_string(expr) |> indent(4)}
""",
empty_if(dot_var?(expr), """
#{to_quoted_string(type) |> indent(4)} the given type does not have the given key:
"""),
format_traces(expr, context), #{to_quoted_string(type) |> indent(4)}
"\ntyping violation found at:" """),
] format_traces(traces)
])
}
end end
def format_warning({:badmodule, expr, type, fun, arity, hints, context}) do def format_diagnostic({:badmodule, expr, type, fun, arity, hints, context}) do
[ traces = collect_traces(expr, context)
"""
expected a module (an atom) when invoking #{fun}/#{arity} in expression:
#{expr_to_string(expr) |> indent(4)} %{
""", details: %{typing_traces: traces},
empty_if(dot_var?(expr), """ message:
IO.iodata_to_binary([
"""
expected a module (an atom) when invoking #{fun}/#{arity} in expression:
but got type: #{expr_to_string(expr) |> indent(4)}
""",
empty_if(dot_var?(expr), """
#{to_quoted_string(type) |> indent(4)} but got type:
"""),
format_traces(expr, context), #{to_quoted_string(type) |> indent(4)}
format_hints(hints), """),
"\ntyping violation found at:" format_traces(traces),
] format_hints(hints)
])
}
end end
def format_warning({:undefined_module, module, fun, arity}) do def format_diagnostic({:mismatched_comparison, expr, context}) do
traces = collect_traces(expr, context)
%{
details: %{typing_traces: traces},
message:
IO.iodata_to_binary([
"""
comparison between incompatible types found:
#{expr_to_string(expr) |> indent(4)}
""",
format_traces(traces),
"""
While Elixir can compare across all types, you are comparing \
across types which are always distinct, and the result is either \
always true or always false
"""
])
}
end
def format_diagnostic({:struct_comparison, expr, context}) do
traces = collect_traces(expr, context)
%{
details: %{typing_traces: traces},
message:
IO.iodata_to_binary([
"""
comparison with structs found:
#{expr_to_string(expr) |> indent(4)}
""",
format_traces(traces),
"""
Comparison operators (>, <, >=, <=, min, and max) perform structural \
and not semantic comparison. Comparing with a struct won't give meaningful \
results. Structs that can be compared typically define a compare/2 function \
within their modules that can be used for semantic comparison.
"""
])
}
end
def format_diagnostic({:undefined_module, module, fun, arity}) do
top = top =
if fun == :__struct__ and arity == 0 do if fun == :__struct__ and arity == 0 do
"struct #{inspect(module)}" "struct #{inspect(module)}"
@@ -597,78 +693,56 @@ defmodule Module.Types.Of do
Exception.format_mfa(module, fun, arity) Exception.format_mfa(module, fun, arity)
end end
[ %{
top, message:
" is undefined (module ", IO.iodata_to_binary([
inspect(module), top,
" is not available or is yet to be defined)" " is undefined (module ",
] inspect(module),
" is not available or is yet to be defined)"
])
}
end end
def format_warning({:undefined_function, module, :__struct__, 0, _exports}) do def format_diagnostic({:undefined_function, module, :__struct__, 0, _exports}) do
"struct #{inspect(module)} is undefined (there is such module but it does not define a struct)" %{
message:
"struct #{inspect(module)} is undefined (there is such module but it does not define a struct)"
}
end end
def format_warning({:undefined_function, module, fun, arity, exports}) do def format_diagnostic({:undefined_function, module, fun, arity, exports}) do
[ %{
Exception.format_mfa(module, fun, arity), message:
" is undefined or private", IO.iodata_to_binary([
UndefinedFunctionError.hint_for_loaded_module(module, fun, arity, exports) Exception.format_mfa(module, fun, arity),
] " is undefined or private",
UndefinedFunctionError.hint_for_loaded_module(module, fun, arity, exports)
])
}
end end
def format_warning({:deprecated, module, fun, arity, reason}) do def format_diagnostic({:deprecated, module, fun, arity, reason}) do
[ %{
Exception.format_mfa(module, fun, arity), message:
" is deprecated. ", IO.iodata_to_binary([
reason Exception.format_mfa(module, fun, arity),
] " is deprecated. ",
reason
])
}
end end
def format_warning({:unrequired_module, module, fun, arity}) do def format_diagnostic({:unrequired_module, module, fun, arity}) do
[ %{
"you must require ", message:
inspect(module), IO.iodata_to_binary([
" before invoking the macro ", "you must require ",
Exception.format_mfa(module, fun, arity) inspect(module),
] " before invoking the macro ",
end Exception.format_mfa(module, fun, arity)
])
def format_warning({:mismatched_comparison, expr, context}) do }
[
"""
comparison between incompatible types found:
#{expr_to_string(expr) |> indent(4)}
""",
format_traces(expr, context),
"""
While Elixir can compare across all types, you are comparing \
across types which are always distinct, and the result is either \
always true or always false
""",
"\ntyping violation found at:"
]
end
def format_warning({:struct_comparison, expr, context}) do
[
"""
comparison with structs found:
#{expr_to_string(expr) |> indent(4)}
""",
format_traces(expr, context),
"""
Comparison operators (>, <, >=, <=, min, and max) perform structural \
and not semantic comparison. Comparing with a struct won't give meaningful \
results. Struct that can be compared typically define a compare/2 function \
within their modules that can be used for semantic comparison
""",
"\ntyping violation found at:"
]
end end
defp dot_var?(expr) do defp dot_var?(expr) do
+8 -33
View File
@@ -186,27 +186,7 @@ defmodule Regex do
defstruct re_pattern: nil, source: "", opts: [], re_version: "" defstruct re_pattern: nil, source: "", opts: [], re_version: ""
@type re_option :: @type t :: %__MODULE__{re_pattern: term, source: binary, opts: binary | [term]}
:unicode
| :caseless
| :dotall
| :multiline
| :extended
| :firstline
| :ungreedy
| :anchored
| :dollar_endonly
| :no_auto_capture
| :newline
@type t :: %__MODULE__{re_pattern: term, source: binary, opts: binary | [re_option()]}
@type capture_option ::
:all | :first | :all_but_first | :none | :all_names | [binary() | atom()]
@type run_option ::
{:return, :binary | :index}
| {:capture, capture_option()}
| {:offset, non_neg_integer()}
defmodule CompileError do defmodule CompileError do
@moduledoc """ @moduledoc """
@@ -242,7 +222,7 @@ defmodule Regex do
{:ok, Regex.compile!("foo", [:caseless])} {:ok, Regex.compile!("foo", [:caseless])}
""" """
@spec compile(binary, binary | [re_option()]) :: {:ok, t} | {:error, any} @spec compile(binary, binary | [term]) :: {:ok, t} | {:error, any}
def compile(source, opts \\ "") when is_binary(source) do def compile(source, opts \\ "") when is_binary(source) do
compile(source, opts, version()) compile(source, opts, version())
end end
@@ -270,7 +250,7 @@ defmodule Regex do
@doc """ @doc """
Compiles the regular expression and raises `Regex.CompileError` in case of errors. Compiles the regular expression and raises `Regex.CompileError` in case of errors.
""" """
@spec compile!(binary, binary | [re_option()]) :: t @spec compile!(binary, binary | [term]) :: t
def compile!(source, options \\ "") when is_binary(source) do def compile!(source, options \\ "") when is_binary(source) do
case compile(source, options) do case compile(source, options) do
{:ok, regex} -> regex {:ok, regex} -> regex
@@ -377,7 +357,7 @@ defmodule Regex do
["d", ""] ["d", ""]
""" """
@spec run(t, binary, [run_option()]) :: nil | [binary] | [{integer, integer}] @spec run(t, binary, [term]) :: nil | [binary] | [{integer, integer}]
def run(regex, string, options \\ []) def run(regex, string, options \\ [])
def run(%Regex{} = regex, string, options) when is_binary(string) do def run(%Regex{} = regex, string, options) when is_binary(string) do
@@ -412,7 +392,7 @@ defmodule Regex do
nil nil
""" """
@spec named_captures(t, String.t(), [{:return, :binary | :index}]) :: map | nil @spec named_captures(t, String.t(), [term]) :: map | nil
def named_captures(regex, string, options \\ []) when is_binary(string) do def named_captures(regex, string, options \\ []) when is_binary(string) do
names = names(regex) names = names(regex)
options = Keyword.put(options, :capture, names) options = Keyword.put(options, :capture, names)
@@ -456,7 +436,7 @@ defmodule Regex do
[:caseless] [:caseless]
""" """
@spec opts(t) :: [re_option()] @spec opts(t) :: [term]
def opts(%Regex{opts: opts}) do def opts(%Regex{opts: opts}) do
opts opts
end end
@@ -528,7 +508,7 @@ defmodule Regex do
[["cd"], ["ce"]] [["cd"], ["ce"]]
""" """
@spec scan(t(), String.t(), [run_option()]) :: [[String.t()]] | [[{integer(), integer()}]] @spec scan(t(), String.t(), [term()]) :: [[String.t()]] | [[{integer(), integer()}]]
def scan(regex, string, options \\ []) def scan(regex, string, options \\ [])
def scan(%Regex{} = regex, string, options) when is_binary(string) do def scan(%Regex{} = regex, string, options) when is_binary(string) do
@@ -615,12 +595,7 @@ defmodule Regex do
["a", "b", "c"] ["a", "b", "c"]
""" """
@spec split(t, String.t(), @spec split(t, String.t(), [term]) :: [String.t()]
parts: pos_integer() | :infinity,
trim: boolean(),
on: capture_option(),
include_captures: boolean()
) :: [String.t()]
def split(regex, string, options \\ []) def split(regex, string, options \\ [])
def split(%Regex{}, "", opts) do def split(%Regex{}, "", opts) do
+12 -12
View File
@@ -757,28 +757,28 @@ defmodule Registry do
In the example below we register the current process and look it up In the example below we register the current process and look it up
both from itself and other processes: both from itself and other processes:
iex> Registry.start_link(keys: :unique, name: Registry.UniqueLookupTest) iex> Registry.start_link(keys: :unique, name: Registry.UniqueValuesTest)
iex> Registry.values(Registry.UniqueLookupTest, "hello", self()) iex> Registry.values(Registry.UniqueValuesTest, "hello", self())
[] []
iex> {:ok, _} = Registry.register(Registry.UniqueLookupTest, "hello", :world) iex> {:ok, _} = Registry.register(Registry.UniqueValuesTest, "hello", :world)
iex> Registry.values(Registry.UniqueLookupTest, "hello", self()) iex> Registry.values(Registry.UniqueValuesTest, "hello", self())
[:world] [:world]
iex> Task.async(fn -> Registry.values(Registry.UniqueLookupTest, "hello", self()) end) |> Task.await() iex> Task.async(fn -> Registry.values(Registry.UniqueValuesTest, "hello", self()) end) |> Task.await()
[] []
iex> parent = self() iex> parent = self()
iex> Task.async(fn -> Registry.values(Registry.UniqueLookupTest, "hello", parent) end) |> Task.await() iex> Task.async(fn -> Registry.values(Registry.UniqueValuesTest, "hello", parent) end) |> Task.await()
[:world] [:world]
The same applies to duplicate registries: The same applies to duplicate registries:
iex> Registry.start_link(keys: :duplicate, name: Registry.DuplicateLookupTest) iex> Registry.start_link(keys: :duplicate, name: Registry.DuplicateValuesTest)
iex> Registry.values(Registry.DuplicateLookupTest, "hello", self()) iex> Registry.values(Registry.DuplicateValuesTest, "hello", self())
[] []
iex> {:ok, _} = Registry.register(Registry.DuplicateLookupTest, "hello", :world) iex> {:ok, _} = Registry.register(Registry.DuplicateValuesTest, "hello", :world)
iex> Registry.values(Registry.DuplicateLookupTest, "hello", self()) iex> Registry.values(Registry.DuplicateValuesTest, "hello", self())
[:world] [:world]
iex> {:ok, _} = Registry.register(Registry.DuplicateLookupTest, "hello", :another) iex> {:ok, _} = Registry.register(Registry.DuplicateValuesTest, "hello", :another)
iex> Enum.sort(Registry.values(Registry.DuplicateLookupTest, "hello", self())) iex> Enum.sort(Registry.values(Registry.DuplicateValuesTest, "hello", self()))
[:another, :world] [:another, :world]
""" """
+1 -1
View File
@@ -1054,7 +1054,7 @@ defmodule System do
* `:arg0` - sets the command arg0 * `:arg0` - sets the command arg0
* `:stderr_to_stdout` - redirects stderr to stdout when `true`, no effect * `:stderr_to_stdout` - redirects stderr to stdout when `true`, no effect
if `use_stdio` is `false``. if `use_stdio` is `false`.
* `:use_stdio` - `true` by default, setting it to false allows direct * `:use_stdio` - `true` by default, setting it to false allows direct
interaction with the terminal from the callee interaction with the terminal from the callee
@@ -17,12 +17,12 @@ defmodule AlternativeInteger do
@spec parse(String.t(), keyword()) :: integer() | {integer(), String.t()} | :error @spec parse(String.t(), keyword()) :: integer() | {integer(), String.t()} | :error
def parse(string, options \\ []) when is_list(options) do def parse(string, options \\ []) when is_list(options) do
if Keyword.get(options, :discard_rest, false) do if Keyword.get(options, :discard_rest, false) do
Integer.parse(string)
else
case Integer.parse(string) do case Integer.parse(string) do
{int, _rest} -> int {int, _rest} -> int
:error -> :error :error -> :error
end end
else
Integer.parse(string)
end end
end end
end end
@@ -30,11 +30,11 @@ end
```elixir ```elixir
iex> AlternativeInteger.parse("13") iex> AlternativeInteger.parse("13")
13 {13, ""}
iex> AlternativeInteger.parse("13", discard_rest: true)
13
iex> AlternativeInteger.parse("13", discard_rest: false) iex> AlternativeInteger.parse("13", discard_rest: false)
{13, ""} {13, ""}
iex> AlternativeInteger.parse("13", discard_rest: true)
13
``` ```
#### Refactoring #### Refactoring
@@ -9,7 +9,9 @@ iex> is_binary(string)
true true
``` ```
In this chapter, we will gain clarity on what exactly binaries are, how they relate to strings, and what single-quoted values, `'like this'`, mean in Elixir. Although strings are one of the most common data types in computer languages, they are subtly complex and are often misunderstood. To understand strings in Elixir, we have to educate ourselves about [Unicode](https://en.wikipedia.org/wiki/Unicode) and character encodings, specifically the [UTF-8](https://en.wikipedia.org/wiki/UTF-8) encoding. In this chapter, we will gain clarity on what exactly binaries are and how they relate to strings. We will also learn about charlists, `~c"like this"`, which are often used for interoperability with Erlang.
Although strings are one of the most common data types in computer languages, they are subtly complex and are often misunderstood. To understand strings in Elixir, let's first discuss [Unicode](https://en.wikipedia.org/wiki/Unicode) and character encodings, specifically the [UTF-8](https://en.wikipedia.org/wiki/UTF-8) encoding.
## Unicode and Code Points ## Unicode and Code Points
@@ -8,12 +8,11 @@ Elixir applies bug fixes only to the latest minor branch. Security patches are a
Elixir version | Support Elixir version | Support
:------------- | :----------------------------- :------------- | :-----------------------------
1.17 | Development 1.17 | Bug fixes and security patches
1.16 | Bug fixes and security patches 1.16 | Security patches only
1.15 | Security patches only 1.15 | Security patches only
1.14 | Security patches only 1.14 | Security patches only
1.13 | Security patches only 1.13 | Security patches only
1.12 | 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). 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).
@@ -214,4 +213,4 @@ Version | Deprecated feature | Replaced by (ava
[v1.14]: https://github.com/elixir-lang/elixir/blob/v1.14/CHANGELOG.md#4-hard-deprecations [v1.14]: https://github.com/elixir-lang/elixir/blob/v1.14/CHANGELOG.md#4-hard-deprecations
[v1.15]: https://github.com/elixir-lang/elixir/blob/v1.15/CHANGELOG.md#4-hard-deprecations [v1.15]: https://github.com/elixir-lang/elixir/blob/v1.15/CHANGELOG.md#4-hard-deprecations
[v1.16]: https://github.com/elixir-lang/elixir/blob/v1.16/CHANGELOG.md#4-hard-deprecations [v1.16]: https://github.com/elixir-lang/elixir/blob/v1.16/CHANGELOG.md#4-hard-deprecations
[v1.17]: https://github.com/elixir-lang/elixir/blob/main/CHANGELOG.md#4-hard-deprecations [v1.17]: https://github.com/elixir-lang/elixir/blob/v1.17/CHANGELOG.md#4-hard-deprecations
@@ -72,7 +72,7 @@ The current milestone is to implement type inference and type checking of Elixir
If the results are satisfactory, the next milestone will include a mechanism for defining typed structs. Elixir programs frequently pattern match on structs, which reveals information about the struct fields, but it knows nothing about their respective types. By propagating types from structs and their fields throughout the program, we will increase the type system’s ability to find errors while further straining our type system implementation. Proposals including the required changes to the language surface will be sent to the community once we reach this stage. If the results are satisfactory, the next milestone will include a mechanism for defining typed structs. Elixir programs frequently pattern match on structs, which reveals information about the struct fields, but it knows nothing about their respective types. By propagating types from structs and their fields throughout the program, we will increase the type system’s ability to find errors while further straining our type system implementation. Proposals including the required changes to the language surface will be sent to the community once we reach this stage.
The third milestone is to introduce set-theoretic type signatures for functions. Unfortunately, the existing typespecs are not precise enough for set-theoretic types and they will be phased out of the language and moved into a separate library once this stage concludes. The third milestone is to introduce set-theoretic type signatures for functions. Unfortunately, the existing Erlang Typespecs are not precise enough for set-theoretic types and they will be phased out of the language and have their postprocessing moved into a separate library once this stage concludes.
## Acknowledgements ## Acknowledgements
+12 -13
View File
@@ -77,7 +77,7 @@ import_function(Meta, Name, Arity, E) ->
false; false;
{import, Receiver} -> {import, Receiver} ->
require_function(Meta, Receiver, Name, Arity, E); require_function(Meta, Receiver, Name, Arity, E);
{ambiguous, Ambiguous} -> {ambiguous, _} = Ambiguous ->
elixir_errors:file_error(Meta, E, ?MODULE, {import, Ambiguous, Name, Arity}); elixir_errors:file_error(Meta, E, ?MODULE, {import, Ambiguous, Name, Arity});
false -> false ->
case elixir_import:special_form(Name, Arity) of case elixir_import:special_form(Name, Arity) of
@@ -131,7 +131,8 @@ dispatch_import(Meta, Name, Args, S, E, Callback) ->
case expand_import(Meta, Name, Arity, E, [], AllowLocals, true) of case expand_import(Meta, Name, Arity, E, [], AllowLocals, true) of
{macro, Receiver, Expander} -> {macro, Receiver, Expander} ->
check_deprecated(macro, Meta, Receiver, Name, Arity, E), check_deprecated(macro, Meta, Receiver, Name, Arity, E),
expand_quoted(Meta, Receiver, Name, Arity, Expander(Args, S), S, E); Caller = {?line(Meta), S, E},
expand_quoted(Meta, Receiver, Name, Arity, Expander(Args, Caller), S, E);
{function, Receiver, NewName} -> {function, Receiver, NewName} ->
elixir_expand:expand({{'.', Meta, [Receiver, NewName]}, Meta, Args}, S, E); elixir_expand:expand({{'.', Meta, [Receiver, NewName]}, Meta, Args}, S, E);
not_found -> not_found ->
@@ -151,7 +152,8 @@ dispatch_require(Meta, Receiver, Name, Args, S, E, Callback) when is_atom(Receiv
case expand_require(Meta, Receiver, Name, Arity, E, true) of case expand_require(Meta, Receiver, Name, Arity, E, true) of
{macro, Receiver, Expander} -> {macro, Receiver, Expander} ->
check_deprecated(macro, Meta, Receiver, Name, Arity, E), check_deprecated(macro, Meta, Receiver, Name, Arity, E),
expand_quoted(Meta, Receiver, Name, Arity, Expander(Args, S), S, E); Caller = {?line(Meta), S, E},
expand_quoted(Meta, Receiver, Name, Arity, Expander(Args, Caller), S, E);
error -> error ->
check_deprecated(function, Meta, Receiver, Name, Arity, E), check_deprecated(function, Meta, Receiver, Name, Arity, E),
elixir_env:trace({remote_function, Meta, Receiver, Name, Arity}, E), elixir_env:trace({remote_function, Meta, Receiver, Name, Arity}, E),
@@ -246,26 +248,23 @@ expand_require(Required, Meta, Receiver, Name, Arity, E, Trace) ->
%% Expansion helpers %% Expansion helpers
expander_macro_fun(Meta, Fun, Receiver, Name, E) -> expander_macro_fun(Meta, Fun, Receiver, Name, E) ->
fun(Args, S) -> expand_macro_fun(Meta, Fun, Receiver, Name, Args, S, E) end. fun(Args, Caller) -> expand_macro_fun(Meta, Fun, Receiver, Name, Args, Caller, E) end.
expander_macro_named(Meta, Receiver, Name, Arity, E) -> expander_macro_named(Meta, Receiver, Name, Arity, E) ->
ProperName = elixir_utils:macro_name(Name), ProperName = elixir_utils:macro_name(Name),
ProperArity = Arity + 1, ProperArity = Arity + 1,
Fun = fun Receiver:ProperName/ProperArity, Fun = fun Receiver:ProperName/ProperArity,
fun(Args, S) -> expand_macro_fun(Meta, Fun, Receiver, Name, Args, S, E) end. fun(Args, Caller) -> expand_macro_fun(Meta, Fun, Receiver, Name, Args, Caller, E) end.
expand_macro_fun(Meta, Fun, Receiver, Name, Args, S, E) ->
Line = ?line(Meta),
EArg = {Line, S, E},
expand_macro_fun(Meta, Fun, Receiver, Name, Args, Caller, E) ->
try try
apply(Fun, [EArg | Args]) apply(Fun, [Caller | Args])
catch catch
Kind:Reason:Stacktrace -> Kind:Reason:Stacktrace ->
Arity = length(Args), Arity = length(Args),
MFA = {Receiver, elixir_utils:macro_name(Name), Arity+1}, MFA = {Receiver, elixir_utils:macro_name(Name), Arity+1},
Info = [{Receiver, Name, Arity, [{file, "expanding macro"}]}, caller(Line, E)], Info = [{Receiver, Name, Arity, [{file, "expanding macro"}]}, caller(?line(Meta), E)],
erlang:raise(Kind, Reason, prune_stacktrace(Stacktrace, MFA, Info, {ok, EArg})) erlang:raise(Kind, Reason, prune_stacktrace(Stacktrace, MFA, Info, {ok, Caller}))
end. end.
expand_quoted(Meta, Receiver, Name, Arity, Quoted, S, E) -> expand_quoted(Meta, Receiver, Name, Arity, Quoted, S, E) ->
@@ -342,7 +341,7 @@ is_import(Meta, Arity) ->
end. end.
% %% We've reached the macro wrapper fun, skip it with the rest % %% We've reached the macro wrapper fun, skip it with the rest
prune_stacktrace([{_, _, [E | _], _} | _], _MFA, Info, {ok, E}) -> prune_stacktrace([{_, _, [Caller | _], _} | _], _MFA, Info, {ok, Caller}) ->
Info; Info;
%% We've reached the invoked macro, skip it %% We've reached the invoked macro, skip it
prune_stacktrace([{M, F, A, _} | _], {M, F, A}, Info, _E) -> prune_stacktrace([{M, F, A, _} | _], {M, F, A}, Info, _E) ->
+4 -4
View File
@@ -32,7 +32,7 @@ trace(Event, #{tracers := Tracers} = E) ->
to_caller({Line, #elixir_ex{vars={Read, _}}, Env}) -> to_caller({Line, #elixir_ex{vars={Read, _}}, Env}) ->
Env#{line := Line, versioned_vars := Read}; Env#{line := Line, versioned_vars := Read};
to_caller(#{} = Env) -> to_caller(#{'__struct__' := 'Elixir.Macro.Env'} = Env) ->
Env. Env.
with_vars(Env, Vars) when is_list(Vars) -> with_vars(Env, Vars) when is_list(Vars) ->
@@ -91,7 +91,7 @@ reset_unused_vars(#elixir_ex{unused={_Unused, Version}} = S) ->
check_unused_vars(#elixir_ex{unused={Unused, _Version}}, E) -> check_unused_vars(#elixir_ex{unused={Unused, _Version}}, E) ->
[elixir_errors:file_warn(calculate_span(Meta, Name), E, ?MODULE, {unused_var, Name, Overridden}) || [elixir_errors:file_warn(calculate_span(Meta, Name), E, ?MODULE, {unused_var, Name, Overridden}) ||
{{{Name, nil}, _}, {Meta, Overridden}} <- maps:to_list(Unused), is_unused_var(Name)], {{{Name, _Kind}, _Count}, {Meta, Overridden}} <- maps:to_list(Unused), is_unused_var(Name)],
E. E.
calculate_span(Meta, Name) -> calculate_span(Meta, Name) ->
@@ -120,8 +120,8 @@ merge_and_check_unused_vars(Current, Unused, ClauseUnused, E) ->
Acc Acc
end; end;
({{Name, Kind}, _Count}, {Meta, Overridden}, Acc) -> ({{Name, _Kind}, _Count}, {Meta, Overridden}, Acc) ->
case (Kind == nil) andalso is_unused_var(Name) of case is_unused_var(Name) of
true -> true ->
Warn = {unused_var, Name, Overridden}, Warn = {unused_var, Name, Overridden},
elixir_errors:file_warn(Meta, E, ?MODULE, Warn); elixir_errors:file_warn(Meta, E, ?MODULE, Warn);
+4 -4
View File
@@ -425,16 +425,16 @@ translate_with_else(Meta, [{'else', Else}], S) ->
RaiseClause = {'->', Generated, [[RaiseVar], RaiseExpr]}, RaiseClause = {'->', Generated, [[RaiseVar], RaiseExpr]},
Clauses = elixir_erl_clauses:get_clauses('else', [{'else', Else ++ [RaiseClause]}], match), Clauses = elixir_erl_clauses:get_clauses('else', [{'else', Else ++ [RaiseClause]}], match),
{TranslatedClauses, SC} = elixir_erl_clauses:clauses(Clauses, SV), {TranslatedClauses, SC} = elixir_erl_clauses:clauses(Clauses, SV#elixir_erl{extra=pin_guard}),
with_else_closure(Meta, TranslatedClauses, SC). with_else_closure(Meta, TranslatedClauses, SC#elixir_erl{extra=SV#elixir_erl.extra}).
with_else_closure(Meta, TranslatedClauses, S) -> with_else_closure(Meta, TranslatedClauses, S) ->
Ann = ?ann(Meta), Ann = ?ann(Meta),
{_, FunErlVar, SC} = elixir_erl_var:assign(Meta, S), {_, FunErlVar, SC} = elixir_erl_var:assign(Meta, S),
{_, ArgErlVar, SA} = elixir_erl_var:assign(Meta, SC), {_, ArgErlVar, SA} = elixir_erl_var:assign(Meta, SC),
FunAssign = {match, Ann, FunErlVar, {'fun', Ann, {clauses, TranslatedClauses}}},
FunCall = {call, Ann, FunErlVar, [ArgErlVar]},
Generated = erl_anno:set_generated(true, Ann), Generated = erl_anno:set_generated(true, Ann),
FunAssign = {match, Ann, FunErlVar, {'fun', Generated, {clauses, TranslatedClauses}}},
FunCall = {call, Ann, FunErlVar, [ArgErlVar]},
{{clause, Generated, [ArgErlVar], [], [FunCall]}, FunAssign, SA}. {{clause, Generated, [ArgErlVar], [], [FunCall]}, FunAssign, SA}.
translate_with_do([{'<-', Meta, [{Var, _, Ctx} = Left, Expr]} | Rest], Ann, Do, Else, S) when is_atom(Var), is_atom(Ctx) -> translate_with_do([{'<-', Meta, [{Var, _, Ctx} = Left, Expr]} | Rest], Ann, Do, Else, S) when is_atom(Var), is_atom(Ctx) ->
+37 -46
View File
@@ -6,12 +6,11 @@
-module(elixir_errors). -module(elixir_errors).
-export([compile_error/1, compile_error/3, parse_error/5]). -export([compile_error/1, compile_error/3, parse_error/5]).
-export([function_error/4, module_error/4, file_error/4]). -export([function_error/4, module_error/4, file_error/4]).
-export([format_snippet/7]). -export([format_snippet/6]).
-export([erl_warn/3, file_warn/4]). -export([erl_warn/3, file_warn/4]).
-export([prefix/1]). -export([prefix/1]).
-export([print_diagnostic/2, emit_diagnostic/6]). -export([print_diagnostics/1, print_diagnostic/2, emit_diagnostic/6]).
-export([print_warning/2, print_warning/3]). -export([print_warning/3]).
-export([print_warning_group/2]).
-include("elixir.hrl"). -include("elixir.hrl").
-type location() :: non_neg_integer() | {non_neg_integer(), non_neg_integer()}. -type location() :: non_neg_integer() | {non_neg_integer(), non_neg_integer()}.
@@ -20,35 +19,9 @@
%% TODO: Remove me on Elixir v2.0. %% TODO: Remove me on Elixir v2.0.
%% Called by deprecated Kernel.ParallelCompiler.print_warning. %% Called by deprecated Kernel.ParallelCompiler.print_warning.
print_warning(Position, File, Message) -> print_warning(Position, File, Message) ->
Output = format_snippet(Position, File, Message, nil, warning, [], nil), Output = format_snippet(warning, Position, File, Message, nil, #{}),
io:put_chars(standard_error, [Output, $\n, $\n]). io:put_chars(standard_error, [Output, $\n, $\n]).
%% Called by Module.ParallelChecker.
print_warning(Message, Diagnostic) ->
#{file := File, position := Position, stacktrace := S} = Diagnostic,
Snippet = read_snippet(File, Position),
Span = get_span(Diagnostic),
Output = format_snippet(Position, File, Message, Snippet, warning, S, Span),
io:put_chars(standard_error, [Output, $\n, $\n]).
%% Called by Module.ParallelChecker.
print_warning_group(Message, [Diagnostic | Others]) ->
#{file := File, position := Position, stacktrace := S} = Diagnostic,
Snippet = read_snippet(File, Position),
Span = get_span(Diagnostic),
Formatted = format_snippet(Position, File, Message, Snippet, warning, S, Span),
LineNumber = extract_line(Position),
LineDigits = get_line_number_digits(LineNumber, 1),
Padding = case Snippet of
nil -> 0;
_ -> max(4, LineDigits + 2)
end,
Locations = [["\n", n_spaces(Padding), "└─ ", 'Elixir.Exception':format_stacktrace_entry(ES)] || #{stacktrace := [ES]} <- Others],
io:put_chars(standard_error, [Formatted, Locations, $\n, $\n]).
get_span(#{span := nil}) -> nil;
get_span(#{span := Span}) -> Span.
read_snippet(nil, _Position) -> read_snippet(nil, _Position) ->
nil; nil;
read_snippet(<<"nofile">>, _Position) -> read_snippet(<<"nofile">>, _Position) ->
@@ -77,20 +50,33 @@ traverse_file_line(IoDevice, N) ->
file:read_line(IoDevice), file:read_line(IoDevice),
traverse_file_line(IoDevice, N - 1). traverse_file_line(IoDevice, N - 1).
print_diagnostic(#{severity := Severity, message := M, stacktrace := Stacktrace, position := P, file := F} = Diagnostic, ReadSnippet) -> %% Used by Module.ParallelChecker.
print_diagnostics([Diagnostic | Others]) ->
#{file := File, position := Position, message := Message} = Diagnostic,
Snippet = read_snippet(File, Position),
Formatted = format_snippet(warning, Position, File, Message, Snippet, Diagnostic),
LineNumber = extract_line(Position),
LineDigits = get_line_number_digits(LineNumber, 1),
Padding = case Snippet of
nil -> 0;
_ -> max(4, LineDigits + 2)
end,
Locations = [["\n", n_spaces(Padding), "└─ ", 'Elixir.Exception':format_stacktrace_entry(ES)] || #{stacktrace := [ES]} <- Others],
io:put_chars(standard_error, [Formatted, Locations, $\n, $\n]).
print_diagnostic(#{severity := S, message := M, position := P, file := F} = Diagnostic, ReadSnippet) ->
Snippet = Snippet =
case ReadSnippet of case ReadSnippet of
true -> read_snippet(F, P); true -> read_snippet(F, P);
false -> nil false -> nil
end, end,
Span = get_span(Diagnostic), Output = format_snippet(S, P, F, M, Snippet, Diagnostic),
Output = format_snippet(P, F, M, Snippet, Severity, Stacktrace, Span),
MaybeStack = MaybeStack =
case (F /= nil) orelse elixir_config:is_bootstrap() of case (F /= nil) orelse elixir_config:is_bootstrap() of
true -> []; true -> [];
false -> [["\n ", 'Elixir.Exception':format_stacktrace_entry(E)] || E <- Stacktrace] false -> [["\n ", 'Elixir.Exception':format_stacktrace_entry(E)] || E <- ?key(Diagnostic, stacktrace)]
end, end,
io:put_chars(standard_error, [Output, MaybeStack, $\n, $\n]), io:put_chars(standard_error, [Output, MaybeStack, $\n, $\n]),
@@ -139,12 +125,12 @@ extract_column(_) -> nil.
%% Format snippets %% Format snippets
%% "Snippet" here refers to the source code line where the diagnostic/error occurred %% "Snippet" here refers to the source code line where the diagnostic/error occurred
format_snippet(_Position, nil, Message, nil, Severity, _Stacktrace, _Span) -> format_snippet(Severity, _Position, nil, Message, nil, _Diagnostic) ->
Formatted = [prefix(Severity), " ", Message], Formatted = [prefix(Severity), " ", Message],
unicode:characters_to_binary(Formatted); unicode:characters_to_binary(Formatted);
format_snippet(Position, File, Message, nil, Severity, Stacktrace, _Span) -> format_snippet(Severity, Position, File, Message, nil, Diagnostic) ->
Location = location_format(Position, File, Stacktrace), Location = location_format(Position, File, maps:get(stacktrace, Diagnostic, [])),
Formatted = io_lib:format( Formatted = io_lib:format(
"~ts ~ts\n" "~ts ~ts\n"
@@ -154,20 +140,22 @@ format_snippet(Position, File, Message, nil, Severity, Stacktrace, _Span) ->
unicode:characters_to_binary(Formatted); unicode:characters_to_binary(Formatted);
format_snippet(Position, File, Message, Snippet, Severity, Stacktrace, Span) -> format_snippet(Severity, Position, File, Message, Snippet, Diagnostic) ->
Column = extract_column(Position), Column = extract_column(Position),
LineNumber = extract_line(Position), LineNumber = extract_line(Position),
LineDigits = get_line_number_digits(LineNumber, 1), LineDigits = get_line_number_digits(LineNumber, 1),
Spacing = n_spaces(max(2, LineDigits) + 1), Spacing = n_spaces(max(2, LineDigits) + 1),
LineNumberSpacing = if LineDigits =:= 1 -> 1; true -> 0 end, LineNumberSpacing = if LineDigits =:= 1 -> 1; true -> 0 end,
{FormattedLine, ColumnsTrimmed} = format_line(Snippet), {FormattedLine, ColumnsTrimmed} = format_line(Snippet),
Location = location_format(Position, File, Stacktrace), Location = location_format(Position, File, maps:get(stacktrace, Diagnostic, [])),
MessageDetail = format_detail(Diagnostic, Message),
Highlight = Highlight =
case Column of case Column of
nil -> highlight_below_line(FormattedLine, Severity); nil ->
highlight_below_line(FormattedLine, Severity);
_ -> _ ->
Length = calculate_span_length({LineNumber, Column}, Span), Length = calculate_span_length({LineNumber, Column}, Diagnostic),
highlight_at_position(Column - ColumnsTrimmed, Severity, Length) highlight_at_position(Column - ColumnsTrimmed, Severity, Length)
end, end,
@@ -179,7 +167,7 @@ format_snippet(Position, File, Message, Snippet, Severity, Stacktrace, Span) ->
" ~ts│\n" " ~ts│\n"
" ~ts└─ ~ts", " ~ts└─ ~ts",
[ [
Spacing, prefix(Severity), format_message(Message, LineDigits, 2 + LineNumberSpacing), Spacing, prefix(Severity), format_message(MessageDetail, LineDigits, 2 + LineNumberSpacing),
Spacing, Spacing,
n_spaces(LineNumberSpacing), LineNumber, FormattedLine, n_spaces(LineNumberSpacing), LineNumber, FormattedLine,
Spacing, Highlight, Spacing, Highlight,
@@ -189,9 +177,12 @@ format_snippet(Position, File, Message, Snippet, Severity, Stacktrace, Span) ->
unicode:characters_to_binary(Formatted). unicode:characters_to_binary(Formatted).
calculate_span_length({StartLine, StartCol}, {StartLine, EndCol}) -> EndCol - StartCol; format_detail(#{details := #{typing_traces := _}}, Message) -> [Message | "\ntyping violation found at:"];
calculate_span_length({StartLine, _}, {EndLine, _}) when EndLine > StartLine -> 1; format_detail(_, Message) -> Message.
calculate_span_length({_, _}, nil) -> 1.
calculate_span_length({StartLine, StartCol}, #{span := {StartLine, EndCol}}) -> EndCol - StartCol;
calculate_span_length({StartLine, _}, #{span := {EndLine, _}}) when EndLine > StartLine -> 1;
calculate_span_length({_, _}, #{}) -> 1.
format_line(Line) -> format_line(Line) ->
case trim_line(Line, 0) of case trim_line(Line, 0) of
+3 -5
View File
@@ -340,7 +340,7 @@ expand({Name, Meta, Kind}, S, #{context := match} = E) when is_atom(Name), is_at
%% Variable was already overridden %% Variable was already overridden
#{Pair := VarVersion} when VarVersion >= PrematchVersion -> #{Pair := VarVersion} when VarVersion >= PrematchVersion ->
maybe_warn_underscored_var_repeat(Meta, Name, Kind, E), maybe_warn_underscored_var_repeat(Meta, Name, Kind, E),
NewUnused = var_used(Meta, Pair, VarVersion, Unused), NewUnused = var_used(Pair, Meta, VarVersion, Unused),
Var = {Name, [{version, VarVersion} | Meta], Kind}, Var = {Name, [{version, VarVersion} | Meta], Kind},
{Var, S#elixir_ex{unused={NewUnused, Version}}, E}; {Var, S#elixir_ex{unused={NewUnused, Version}}, E};
@@ -396,7 +396,7 @@ expand({Name, Meta, Kind}, S, E) when is_atom(Name), is_atom(Kind) ->
{ok, PairVersion} -> {ok, PairVersion} ->
maybe_warn_underscored_var_access(Meta, Name, Kind, E), maybe_warn_underscored_var_access(Meta, Name, Kind, E),
Var = {Name, [{version, PairVersion} | Meta], Kind}, Var = {Name, [{version, PairVersion} | Meta], Kind},
{Var, S#elixir_ex{unused={var_used(Meta, Pair, PairVersion, Unused), Version}}, E}; {Var, S#elixir_ex{unused={var_used(Pair, Meta, PairVersion, Unused), Version}}, E};
Error -> Error ->
case lists:keyfind(if_undefined, 1, Meta) of case lists:keyfind(if_undefined, 1, Meta) of
@@ -545,8 +545,6 @@ resolve_super(Meta, Arity, E) ->
expand_fn_capture(Meta, Arg, S, E) -> expand_fn_capture(Meta, Arg, S, E) ->
case elixir_fn:capture(Meta, Arg, S, E) of case elixir_fn:capture(Meta, Arg, S, E) of
{{remote, Remote, Fun, Arity}, RequireMeta, DotMeta, SE, EE} -> {{remote, Remote, Fun, Arity}, RequireMeta, DotMeta, SE, EE} ->
is_atom(Remote) andalso
elixir_env:trace({remote_function, RequireMeta, Remote, Fun, Arity}, E),
AttachedMeta = attach_runtime_module(Remote, RequireMeta, S, E), AttachedMeta = attach_runtime_module(Remote, RequireMeta, S, E),
{{'&', Meta, [{'/', [], [{{'.', DotMeta, [Remote, Fun]}, AttachedMeta, []}, Arity]}]}, SE, EE}; {{'&', Meta, [{'/', [], [{{'.', DotMeta, [Remote, Fun]}, AttachedMeta, []}, Arity]}]}, SE, EE};
{{local, Fun, Arity}, _, _, _SE, #{function := nil}} -> {{local, Fun, Arity}, _, _, _SE, #{function := nil}} ->
@@ -658,7 +656,7 @@ var_unused({_, Kind} = Pair, Meta, Version, Unused, Override) ->
false -> Unused false -> Unused
end. end.
var_used(Meta, {_, Kind} = Pair, Version, Unused) -> var_used({_, Kind} = Pair, Meta, Version, Unused) ->
KeepUnused = lists:keymember(keep_unused, 1, Meta), KeepUnused = lists:keymember(keep_unused, 1, Meta),
if if
+9 -3
View File
@@ -128,12 +128,18 @@ validate(Meta, [{Pos, _} | _], Expected, E) ->
validate(_Meta, [], _Pos, _E) -> validate(_Meta, [], _Pos, _E) ->
[]. [].
escape({'&', _, [Pos]}, _E, Dict) when is_integer(Pos), Pos > 0 -> escape({'&', Meta, [Pos]}, E, Dict) when is_integer(Pos), Pos > 0 ->
% Using a nil context here to emit warnings when variable is unused. % Using a nil context here to emit warnings when variable is unused.
% This might pollute user space but is unlikely because variables % This might pollute user space but is unlikely because variables
% named :"&1" are not valid syntax. % named :"&1" are not valid syntax.
Var = {list_to_atom([$& | integer_to_list(Pos)]), [], nil}, case orddict:find(Pos, Dict) of
{Var, orddict:store(Pos, Var, Dict)}; {ok, Var} ->
{Var, Dict};
error ->
Next = elixir_module:next_counter(?key(E, module)),
Var = {capture, [{counter, Next} | Meta], nil},
{Var, orddict:store(Pos, Var, Dict)}
end;
escape({'&', Meta, [Pos]}, E, _Dict) when is_integer(Pos) -> escape({'&', Meta, [Pos]}, E, _Dict) when is_integer(Pos) ->
file_error(Meta, E, ?MODULE, {invalid_arity_for_capture, Pos}); file_error(Meta, E, ?MODULE, {invalid_arity_for_capture, Pos});
escape({'&', Meta, _} = Arg, E, _Dict) -> escape({'&', Meta, _} = Arg, E, _Dict) ->
+33 -30
View File
@@ -2,12 +2,15 @@
%% between local functions and imports. %% between local functions and imports.
%% For imports dispatch, please check elixir_dispatch. %% For imports dispatch, please check elixir_dispatch.
-module(elixir_import). -module(elixir_import).
-export([import/6, special_form/2, format_error/1]). -export([import/6, import/7, special_form/2, format_error/1]).
-compile(inline_list_funcs). -compile(inline_list_funcs).
-include("elixir.hrl"). -include("elixir.hrl").
import(Meta, Ref, Opts, E, Warn, Trace) -> import(Meta, Ref, Opts, E, Warn, Trace) ->
case import_only_except(Meta, Ref, Opts, E, Warn) of import(Meta, Ref, Opts, E, Warn, Trace, fun Ref:'__info__'/1).
import(Meta, Ref, Opts, E, Warn, Trace, InfoCallback) ->
case import_only_except(Meta, Ref, Opts, E, Warn, InfoCallback) of
{Functions, Macros, Added} -> {Functions, Macros, Added} ->
Trace andalso elixir_env:trace({import, [{imported, Added} | Meta], Ref, Opts}, E), Trace andalso elixir_env:trace({import, [{imported, Added} | Meta], Ref, Opts}, E),
EI = E#{functions := Functions, macros := Macros}, EI = E#{functions := Functions, macros := Macros},
@@ -17,18 +20,18 @@ import(Meta, Ref, Opts, E, Warn, Trace) ->
{error, Reason} {error, Reason}
end. end.
import_only_except(Meta, Ref, Opts, E, Warn) -> import_only_except(Meta, Ref, Opts, E, Warn, InfoCallback) ->
MaybeOnly = lists:keyfind(only, 1, Opts), MaybeOnly = lists:keyfind(only, 1, Opts),
case lists:keyfind(except, 1, Opts) of case lists:keyfind(except, 1, Opts) of
false -> false ->
import_only_except(Meta, Ref, MaybeOnly, false, E, Warn); import_only_except(Meta, Ref, MaybeOnly, false, E, Warn, InfoCallback);
{except, DupExcept} when is_list(DupExcept) -> {except, DupExcept} when is_list(DupExcept) ->
case ensure_keyword_list(DupExcept) of case ensure_keyword_list(DupExcept) of
ok -> ok ->
Except = ensure_no_duplicates(DupExcept, except, Meta, E, Warn), Except = ensure_no_duplicates(DupExcept, except, Meta, E, Warn),
import_only_except(Meta, Ref, MaybeOnly, Except, E, Warn); import_only_except(Meta, Ref, MaybeOnly, Except, E, Warn, InfoCallback);
error -> error ->
{error, {invalid_option, except, DupExcept}} {error, {invalid_option, except, DupExcept}}
@@ -38,27 +41,27 @@ import_only_except(Meta, Ref, Opts, E, Warn) ->
{error, {invalid_option, except, Other}} {error, {invalid_option, except, Other}}
end. end.
import_only_except(Meta, Ref, MaybeOnly, Except, E, Warn) -> import_only_except(Meta, Ref, MaybeOnly, Except, E, Warn, InfoCallback) ->
case MaybeOnly of case MaybeOnly of
{only, functions} -> {only, functions} ->
{Added1, _Used1, Funs} = import_functions(Meta, Ref, Except, E, Warn), {Added1, _Used1, Funs} = import_functions(Meta, Ref, Except, E, Warn, InfoCallback),
{Funs, keydelete(Ref, ?key(E, macros)), Added1}; {Funs, keydelete(Ref, ?key(E, macros)), Added1};
{only, macros} -> {only, macros} ->
{Added2, _Used2, Macs} = import_macros(Meta, Ref, Except, E, Warn), {Added2, _Used2, Macs} = import_macros(Meta, Ref, Except, E, Warn, InfoCallback),
{keydelete(Ref, ?key(E, functions)), Macs, Added2}; {keydelete(Ref, ?key(E, functions)), Macs, Added2};
{only, sigils} -> {only, sigils} ->
{Added1, _Used1, Funs} = import_sigil_functions(Meta, Ref, Except, E, Warn), {Added1, _Used1, Funs} = import_sigil_functions(Meta, Ref, Except, E, Warn, InfoCallback),
{Added2, _Used2, Macs} = import_sigil_macros(Meta, Ref, Except, E, Warn), {Added2, _Used2, Macs} = import_sigil_macros(Meta, Ref, Except, E, Warn, InfoCallback),
{Funs, Macs, Added1 or Added2}; {Funs, Macs, Added1 or Added2};
{only, DupOnly} when is_list(DupOnly) -> {only, DupOnly} when is_list(DupOnly) ->
case ensure_keyword_list(DupOnly) of case ensure_keyword_list(DupOnly) of
ok when Except =:= false -> ok when Except =:= false ->
Only = ensure_no_duplicates(DupOnly, only, Meta, E, Warn), Only = ensure_no_duplicates(DupOnly, only, Meta, E, Warn),
{Added1, Used1, Funs} = import_listed_functions(Meta, Ref, Only, E, Warn), {Added1, Used1, Funs} = import_listed_functions(Meta, Ref, Only, E, Warn, InfoCallback),
{Added2, Used2, Macs} = import_listed_macros(Meta, Ref, Only, E, Warn), {Added2, Used2, Macs} = import_listed_macros(Meta, Ref, Only, E, Warn, InfoCallback),
[Warn andalso elixir_errors:file_warn(Meta, E, ?MODULE, {invalid_import, {Ref, Name, Arity}}) || [Warn andalso elixir_errors:file_warn(Meta, E, ?MODULE, {invalid_import, {Ref, Name, Arity}}) ||
{Name, Arity} <- (Only -- Used1) -- Used2], {Name, Arity} <- (Only -- Used1) -- Used2],
{Funs, Macs, Added1 or Added2}; {Funs, Macs, Added1 or Added2};
@@ -74,37 +77,37 @@ import_only_except(Meta, Ref, MaybeOnly, Except, E, Warn) ->
{error, {invalid_option, only, Other}}; {error, {invalid_option, only, Other}};
false -> false ->
{Added1, _Used1, Funs} = import_functions(Meta, Ref, Except, E, Warn), {Added1, _Used1, Funs} = import_functions(Meta, Ref, Except, E, Warn, InfoCallback),
{Added2, _Used2, Macs} = import_macros(Meta, Ref, Except, E, Warn), {Added2, _Used2, Macs} = import_macros(Meta, Ref, Except, E, Warn, InfoCallback),
{Funs, Macs, Added1 or Added2} {Funs, Macs, Added1 or Added2}
end. end.
import_listed_functions(Meta, Ref, Only, E, Warn) -> import_listed_functions(Meta, Ref, Only, E, Warn, InfoCallback) ->
New = intersection(Only, get_functions(Ref)), New = intersection(Only, get_functions(Ref, InfoCallback)),
calculate_key(Meta, Ref, ?key(E, functions), New, E, Warn). calculate_key(Meta, Ref, ?key(E, functions), New, E, Warn).
import_listed_macros(Meta, Ref, Only, E, Warn) -> import_listed_macros(Meta, Ref, Only, E, Warn, InfoCallback) ->
New = intersection(Only, get_macros(Ref)), New = intersection(Only, get_macros(InfoCallback)),
calculate_key(Meta, Ref, ?key(E, macros), New, E, Warn). calculate_key(Meta, Ref, ?key(E, macros), New, E, Warn).
import_functions(Meta, Ref, Except, E, Warn) -> import_functions(Meta, Ref, Except, E, Warn, InfoCallback) ->
calculate_except(Meta, Ref, Except, ?key(E, functions), E, Warn, fun() -> calculate_except(Meta, Ref, Except, ?key(E, functions), E, Warn, fun() ->
get_functions(Ref) get_functions(Ref, InfoCallback)
end). end).
import_macros(Meta, Ref, Except, E, Warn) -> import_macros(Meta, Ref, Except, E, Warn, InfoCallback) ->
calculate_except(Meta, Ref, Except, ?key(E, macros), E, Warn, fun() -> calculate_except(Meta, Ref, Except, ?key(E, macros), E, Warn, fun() ->
get_macros(Ref) get_macros(InfoCallback)
end). end).
import_sigil_functions(Meta, Ref, Except, E, Warn) -> import_sigil_functions(Meta, Ref, Except, E, Warn, InfoCallback) ->
calculate_except(Meta, Ref, Except, ?key(E, functions), E, Warn, fun() -> calculate_except(Meta, Ref, Except, ?key(E, functions), E, Warn, fun() ->
filter_sigils(get_functions(Ref)) filter_sigils(InfoCallback(functions))
end). end).
import_sigil_macros(Meta, Ref, Except, E, Warn) -> import_sigil_macros(Meta, Ref, Except, E, Warn, InfoCallback) ->
calculate_except(Meta, Ref, Except, ?key(E, macros), E, Warn, fun() -> calculate_except(Meta, Ref, Except, ?key(E, macros), E, Warn, fun() ->
filter_sigils(get_macros(Ref)) filter_sigils(InfoCallback(macros))
end). end).
calculate_except(Meta, Key, false, Old, E, Warn, Existing) -> calculate_except(Meta, Key, false, Old, E, Warn, Existing) ->
@@ -134,16 +137,16 @@ calculate_key(Meta, Key, Old, New, E, Warn) ->
%% Retrieve functions and macros from modules %% Retrieve functions and macros from modules
get_functions(Module) -> get_functions(Module, InfoCallback) ->
try try
Module:'__info__'(functions) InfoCallback(functions)
catch catch
error:undef -> remove_internals(Module:module_info(exports)) error:undef -> remove_internals(Module:module_info(exports))
end. end.
get_macros(Module) -> get_macros(InfoCallback) ->
try try
Module:'__info__'(macros) InfoCallback(macros)
catch catch
error:undef -> [] error:undef -> []
end. end.
@@ -220,4 +220,137 @@ defmodule DurationTest do
microsecond: {0, 0} microsecond: {0, 0}
} }
end end
test "from_iso8601/1" do
assert Duration.from_iso8601("P1Y2M3DT4H5M6S") ==
{:ok, %Duration{year: 1, month: 2, day: 3, hour: 4, minute: 5, second: 6}}
assert Duration.from_iso8601("P3WT5H3M") == {:ok, %Duration{week: 3, hour: 5, minute: 3}}
assert Duration.from_iso8601("PT5H3M") == {:ok, %Duration{hour: 5, minute: 3}}
assert Duration.from_iso8601("P1Y2M3D") == {:ok, %Duration{year: 1, month: 2, day: 3}}
assert Duration.from_iso8601("PT4H5M6S") == {:ok, %Duration{hour: 4, minute: 5, second: 6}}
assert Duration.from_iso8601("P1Y2M") == {:ok, %Duration{year: 1, month: 2}}
assert Duration.from_iso8601("P3D") == {:ok, %Duration{day: 3}}
assert Duration.from_iso8601("PT4H5M") == {:ok, %Duration{hour: 4, minute: 5}}
assert Duration.from_iso8601("PT6S") == {:ok, %Duration{second: 6}}
assert Duration.from_iso8601("P2M4Y") == {:error, :invalid_date_component}
assert Duration.from_iso8601("P4Y2W3Y") == {:error, :invalid_date_component}
assert Duration.from_iso8601("P5HT4MT3S") == {:error, :invalid_date_component}
assert Duration.from_iso8601("P5H3HT4M") == {:error, :invalid_date_component}
assert Duration.from_iso8601("PT1D") == {:error, :invalid_time_component}
assert Duration.from_iso8601("PT.6S") == {:error, :invalid_time_component}
assert Duration.from_iso8601("invalid") == {:error, :invalid_duration}
end
test "from_iso8601!/1" do
assert Duration.from_iso8601!("P1Y2M3DT4H5M6S") == %Duration{
year: 1,
month: 2,
day: 3,
hour: 4,
minute: 5,
second: 6
}
assert Duration.from_iso8601!("P3WT5H3M") == %Duration{week: 3, hour: 5, minute: 3}
assert Duration.from_iso8601!("PT5H3M") == %Duration{hour: 5, minute: 3}
assert Duration.from_iso8601!("P1Y2M3D") == %Duration{year: 1, month: 2, day: 3}
assert Duration.from_iso8601!("PT4H5M6S") == %Duration{hour: 4, minute: 5, second: 6}
assert Duration.from_iso8601!("P1Y2M") == %Duration{year: 1, month: 2}
assert Duration.from_iso8601!("P3D") == %Duration{day: 3}
assert Duration.from_iso8601!("PT4H5M") == %Duration{hour: 4, minute: 5}
assert Duration.from_iso8601!("PT6S") == %Duration{second: 6}
assert Duration.from_iso8601!("PT1,6S") == %Duration{second: 1, microsecond: {600_000, 1}}
assert Duration.from_iso8601!("PT-1.6S") == %Duration{second: -1, microsecond: {-600_000, 1}}
assert Duration.from_iso8601!("-P10DT4H") == %Duration{day: -10, hour: -4}
assert Duration.from_iso8601!("-P10DT-4H") == %Duration{day: -10, hour: 4}
assert Duration.from_iso8601!("P-10D") == %Duration{day: -10}
assert Duration.from_iso8601!("+P10DT-4H") == %Duration{day: 10, hour: -4}
assert Duration.from_iso8601!("P+10D") == %Duration{day: 10}
assert Duration.from_iso8601!("-P+10D") == %Duration{day: -10}
assert Duration.from_iso8601!("PT-1.234567S") == %Duration{
second: -1,
microsecond: {-234_567, 6}
}
assert Duration.from_iso8601!("PT1.12345678S") == %Duration{
second: 1,
microsecond: {123_456, 6}
}
assert Duration.from_iso8601!("P3Y4W-3DT-6S") == %Duration{
year: 3,
week: 4,
day: -3,
second: -6
}
assert Duration.from_iso8601!("PT-4.23S") == %Duration{second: -4, microsecond: {-230_000, 2}}
assert_raise ArgumentError,
~s/failed to parse duration "P5H3HT4M". reason: :invalid_date_component/,
fn ->
Duration.from_iso8601!("P5H3HT4M")
end
assert_raise ArgumentError,
~s/failed to parse duration "P4Y2W3Y". reason: :invalid_date_component/,
fn ->
Duration.from_iso8601!("P4Y2W3Y")
end
assert_raise ArgumentError,
~s/failed to parse duration "invalid". reason: :invalid_duration/,
fn ->
Duration.from_iso8601!("invalid")
end
assert_raise ArgumentError,
~s/failed to parse duration "P4.5YT6S". reason: :invalid_date_component/,
fn ->
Duration.from_iso8601!("P4.5YT6S")
end
end
test "to_iso8601/1" do
assert %Duration{year: 1, month: 2, day: 3, hour: 4, minute: 5, second: 6}
|> Duration.to_iso8601() == "P1Y2M3DT4H5M6S"
assert %Duration{week: 3, hour: 5, minute: 3} |> Duration.to_iso8601() == "P3WT5H3M"
assert %Duration{hour: 5, minute: 3} |> Duration.to_iso8601() == "PT5H3M"
assert %Duration{year: 1, month: 2, day: 3} |> Duration.to_iso8601() == "P1Y2M3D"
assert %Duration{hour: 4, minute: 5, second: 6} |> Duration.to_iso8601() == "PT4H5M6S"
assert %Duration{year: 1, month: 2} |> Duration.to_iso8601() == "P1Y2M"
assert %Duration{day: 3} |> Duration.to_iso8601() == "P3D"
assert %Duration{hour: 4, minute: 5} |> Duration.to_iso8601() == "PT4H5M"
assert %Duration{second: 6} |> Duration.to_iso8601() == "PT6S"
assert %Duration{second: 1, microsecond: {600_000, 1}} |> Duration.to_iso8601() == "PT1.6S"
assert %Duration{second: -1, microsecond: {-600_000, 1}} |> Duration.to_iso8601() == "PT-1.6S"
assert %Duration{second: -1, microsecond: {-234_567, 6}} |> Duration.to_iso8601() ==
"PT-1.234567S"
assert %Duration{second: 1, microsecond: {123_456, 6}} |> Duration.to_iso8601() ==
"PT1.123456S"
assert %Duration{year: 3, week: 4, day: -3, second: -6} |> Duration.to_iso8601() ==
"P3Y4W-3DT-6S"
assert %Duration{second: -4, microsecond: {-230_000, 2}} |> Duration.to_iso8601() ==
"PT-4.23S"
assert %Duration{second: -4, microsecond: {230_000, 2}} |> Duration.to_iso8601() ==
"PT-3.77S"
assert %Duration{second: 2, microsecond: {-1_200_000, 4}} |> Duration.to_iso8601() ==
"PT0.8000S"
assert %Duration{second: 1, microsecond: {-1_200_000, 3}} |> Duration.to_iso8601() ==
"PT-0.200S"
assert %Duration{microsecond: {-800_000, 2}} |> Duration.to_iso8601() == "PT-0.80S"
assert %Duration{microsecond: {-800_000, 0}} |> Duration.to_iso8601() == "PT0S"
assert %Duration{microsecond: {-1_200_000, 2}} |> Duration.to_iso8601() == "PT-1.20S"
end
end end
@@ -0,0 +1,13 @@
defmodule Dialyzer.WithNoReturn do
def with_no_return(list) do
no_return = fn -> throw(:no_return) end
with [] <- list do
:ok
else
# note: throwing here directly wouldn't be caught in the first place,
# calling a no_return function is what could cause an issue.
_ -> no_return.()
end
end
end
@@ -159,6 +159,11 @@ defmodule Kernel.DialyzerTest do
assert_dialyze_no_warnings!(context) assert_dialyze_no_warnings!(context)
end end
test "no warnings on with when else has a no_return type", context do
copy_beam!(context, Dialyzer.WithNoReturn)
assert_dialyze_no_warnings!(context)
end
test "no warnings on defmacrop", context do test "no warnings on defmacrop", context do
copy_beam!(context, Dialyzer.Defmacrop) copy_beam!(context, Dialyzer.Defmacrop)
assert_dialyze_no_warnings!(context) assert_dialyze_no_warnings!(context)
@@ -634,6 +634,16 @@ defmodule Kernel.ErrorsTest do
end end
""" """
) )
assert_compile_error(
["nofile:3:17", "function exit/1 imported from both :erlang and Kernel, call is ambiguous"],
~c"""
defmodule Kernel.ErrorsTest.FunctionImportConflict do
import :erlang, only: [exit: 1], warn: false
def foo, do: &exit/1
end
"""
)
end end
test "ensure valid import :only option" do test "ensure valid import :only option" do
@@ -621,23 +621,6 @@ defmodule Kernel.ExpansionTest do
assert expand(quote(do: quote(do: hello)), []) == {:{}, [], [:hello, [], __MODULE__]} assert expand(quote(do: quote(do: hello)), []) == {:{}, [], [:hello, [], __MODULE__]}
end end
test "expand bind_quoted once" do
expand_env(
quote do
var = 123
quote(bind_quoted: [var: var], do: var)
end,
__ENV__,
[]
)
|> elem(0)
|> Macro.prewalk(fn
{:var, [version: 0], Kernel.ExpansionTest} -> :ok
{:var, _, Kernel.ExpansionTest} = invalid -> flunk("unexpected node #{inspect(invalid)}")
node -> node
end)
end
test "raises if the :bind_quoted option is invalid" do test "raises if the :bind_quoted option is invalid" do
assert_compile_error(~r"invalid :bind_quoted for quote", fn -> assert_compile_error(~r"invalid :bind_quoted for quote", fn ->
expand(quote(do: quote(bind_quoted: self(), do: :ok))) expand(quote(do: quote(bind_quoted: self(), do: :ok)))
@@ -1150,6 +1133,12 @@ defmodule Kernel.ExpansionTest do
assert expand(quote(do: &unknown(&1, &2))) == {:&, [], [{:/, [], [{:unknown, [], nil}, 2]}]} assert expand(quote(do: &unknown(&1, &2))) == {:&, [], [{:/, [], [{:unknown, [], nil}, 2]}]}
end end
test "keeps position meta on & variables" do
assert expand(Code.string_to_quoted!("& &1")) |> clean_meta([:counter]) ==
{:fn, [{:line, 1}],
[{:->, [{:line, 1}], [[{:capture, [line: 1], nil}], {:capture, [line: 1], nil}]}]}
end
test "expands remotes" do test "expands remotes" do
assert expand(quote(do: &List.flatten/2)) == assert expand(quote(do: &List.flatten/2)) ==
quote(do: &:"Elixir.List".flatten/2) quote(do: &:"Elixir.List".flatten/2)
+11 -5
View File
@@ -88,10 +88,6 @@ defmodule Kernel.FnTest do
assert is_function(&and/2) assert is_function(&and/2)
end end
test "capture precedence in cons" do
assert [(&IO.puts/1) | &IO.puts/2] == [(&IO.puts/1) | &IO.puts/2]
end
test "capture with variable module" do test "capture with variable module" do
mod = List mod = List
assert (&mod.flatten(&1)).([1, [2], 3]) == [1, 2, 3] assert (&mod.flatten(&1)).([1, [2], 3]) == [1, 2, 3]
@@ -140,13 +136,23 @@ defmodule Kernel.FnTest do
assert (&(!is_atom(&1))).(:foo) == false assert (&(!is_atom(&1))).(:foo) == false
end end
test "capture other" do test "capture with function call" do
assert (& &1).(:ok) == :ok assert (& &1).(:ok) == :ok
fun = fn a, b -> a + b end fun = fn a, b -> a + b end
assert (&fun.(&1, 2)).(1) == 3 assert (&fun.(&1, 2)).(1) == 3
end end
defmacro c(x) do
quote do
&(unquote(x) <> &1)
end
end
test "capture within capture through macro" do
assert (&c(&1).("b")).("a") == "ab"
end
defp atom?(atom) when is_atom(atom), do: true defp atom?(atom) when is_atom(atom), do: true
defp atom?(_), do: false defp atom?(_), do: false
+56 -32
View File
@@ -27,8 +27,8 @@ defmodule Kernel.TracersTest do
Foo Foo
""") """)
assert_receive {:start, %{lexical_tracker: pid}} when is_pid(pid) assert_received {:start, %{lexical_tracker: pid}} when is_pid(pid)
assert_receive {:stop, %{lexical_tracker: pid}} when is_pid(pid) assert_received {:stop, %{lexical_tracker: pid}} when is_pid(pid)
end end
test "traces alias references" do test "traces alias references" do
@@ -36,7 +36,7 @@ defmodule Kernel.TracersTest do
Foo Foo
""") """)
assert_receive {{:alias_reference, meta, Foo}, _} assert_received {{:alias_reference, meta, Foo}, _}
assert meta[:line] == 1 assert meta[:line] == 1
assert meta[:column] == 1 assert meta[:column] == 1
end end
@@ -50,17 +50,17 @@ defmodule Kernel.TracersTest do
Bar Bar
""") """)
assert_receive {{:alias, meta, Hello.World, World, []}, _} assert_received {{:alias, meta, Hello.World, World, []}, _}
assert meta[:line] == 1 assert meta[:line] == 1
assert meta[:column] == 1 assert meta[:column] == 1
assert_receive {{:alias_expansion, meta, World, Hello.World}, _} assert_received {{:alias_expansion, meta, World, Hello.World}, _}
assert meta[:line] == 2 assert meta[:line] == 2
assert meta[:column] == 1 assert meta[:column] == 1
assert_receive {{:alias, meta, Foo, Bar, [as: Bar, warn: true]}, _} assert_received {{:alias, meta, Foo, Bar, [as: Bar, warn: true]}, _}
assert meta[:line] == 4 assert meta[:line] == 4
assert meta[:column] == 1 assert meta[:column] == 1
assert_receive {{:alias_expansion, meta, Bar, Foo}, _} assert_received {{:alias_expansion, meta, Bar, Foo}, _}
assert meta[:line] == 5 assert meta[:line] == 5
assert meta[:column] == 1 assert meta[:column] == 1
end end
@@ -72,15 +72,19 @@ defmodule Kernel.TracersTest do
{1, ""} = parse("1") {1, ""} = parse("1")
""") """)
assert_receive {{:import, meta, Integer, only: [is_odd: 1, parse: 1]}, _} assert_received {{:import, meta, Integer, only: [is_odd: 1, parse: 1]}, _}
assert meta[:line] == 1 assert meta[:line] == 1
assert meta[:column] == 1 assert meta[:column] == 1
assert_receive {{:imported_macro, meta, Integer, :is_odd, 1}, _} assert_received {{:imported_macro, meta, Integer, :is_odd, 1}, _}
assert meta[:line] == 2 assert meta[:line] == 2
assert meta[:column] == 8 assert meta[:column] == 8
assert_receive {{:imported_function, meta, Integer, :parse, 1}, _} assert_received {{:imported_function, meta, Integer, :parse, 1}, _}
assert meta[:line] == 3
assert meta[:column] == 11
assert_received {{:remote_function, meta, Integer, :parse, 1}, _}
assert meta[:line] == 3 assert meta[:line] == 3
assert meta[:column] == 11 assert meta[:column] == 11
end end
@@ -92,15 +96,15 @@ defmodule Kernel.TracersTest do
&parse/1 &parse/1
""") """)
assert_receive {{:import, meta, Integer, only: [is_odd: 1, parse: 1]}, _} assert_received {{:import, meta, Integer, only: [is_odd: 1, parse: 1]}, _}
assert meta[:line] == 1 assert meta[:line] == 1
assert meta[:column] == 1 assert meta[:column] == 1
assert_receive {{:imported_macro, meta, Integer, :is_odd, 1}, _} assert_received {{:imported_macro, meta, Integer, :is_odd, 1}, _}
assert meta[:line] == 2 assert meta[:line] == 2
assert meta[:column] == 2 assert meta[:column] == 2
assert_receive {{:imported_function, meta, Integer, :parse, 1}, _} assert_received {{:imported_function, meta, Integer, :parse, 1}, _}
assert meta[:line] == 3 assert meta[:line] == 3
assert meta[:column] == 2 assert meta[:column] == 2
end end
@@ -110,7 +114,7 @@ defmodule Kernel.TracersTest do
%URI{path: "/"} %URI{path: "/"}
""") """)
assert_receive {{:struct_expansion, meta, URI, [:path]}, _} assert_received {{:struct_expansion, meta, URI, [:path]}, _}
assert meta[:line] == 1 assert meta[:line] == 1
assert meta[:column] == 1 assert meta[:column] == 1
end end
@@ -123,15 +127,15 @@ defmodule Kernel.TracersTest do
"foo" = Atom.to_string(:foo) "foo" = Atom.to_string(:foo)
""") """)
assert_receive {{:remote_macro, meta, Integer, :is_odd, 1}, _} assert_received {{:remote_macro, meta, Integer, :is_odd, 1}, _}
assert meta[:line] == 2 assert meta[:line] == 2
assert meta[:column] == 16 assert meta[:column] == 16
assert_receive {{:remote_function, meta, Integer, :parse, 1}, _} assert_received {{:remote_function, meta, Integer, :parse, 1}, _}
assert meta[:line] == 3 assert meta[:line] == 3
assert meta[:column] == 19 assert meta[:column] == 19
assert_receive {{:remote_function, meta, Atom, :to_string, 1}, _} assert_received {{:remote_function, meta, Atom, :to_string, 1}, _}
assert meta[:line] == 4 assert meta[:line] == 4
assert meta[:column] == 14 assert meta[:column] == 14
end end
@@ -143,11 +147,11 @@ defmodule Kernel.TracersTest do
&Integer.parse/1 &Integer.parse/1
""") """)
assert_receive {{:remote_macro, meta, Integer, :is_odd, 1}, _} assert_received {{:remote_macro, meta, Integer, :is_odd, 1}, _}
assert meta[:line] == 2 assert meta[:line] == 2
assert meta[:column] == 10 assert meta[:column] == 10
assert_receive {{:remote_function, meta, Integer, :parse, 1}, _} assert_received {{:remote_function, meta, Integer, :parse, 1}, _}
assert meta[:line] == 3 assert meta[:line] == 3
assert meta[:column] == 10 assert meta[:column] == 10
end end
@@ -161,11 +165,11 @@ defmodule Kernel.TracersTest do
end end
""") """)
assert_receive {{:local_macro, meta, :foo, 1}, _} assert_received {{:local_macro, meta, :foo, 1}, _}
assert meta[:line] == 4 assert meta[:line] == 4
assert meta[:column] == 21 assert meta[:column] == 21
assert_receive {{:local_function, meta, :bar, 1}, _} assert_received {{:local_function, meta, :bar, 1}, _}
assert meta[:line] == 4 assert meta[:line] == 4
assert meta[:column] == 32 assert meta[:column] == 32
after after
@@ -182,11 +186,11 @@ defmodule Kernel.TracersTest do
end end
""") """)
assert_receive {{:local_macro, meta, :foo, 1}, _} assert_received {{:local_macro, meta, :foo, 1}, _}
assert meta[:line] == 4 assert meta[:line] == 4
assert meta[:column] == 21 assert meta[:column] == 21
assert_receive {{:local_function, meta, :bar, 1}, _} assert_received {{:local_function, meta, :bar, 1}, _}
assert meta[:line] == 4 assert meta[:line] == 4
assert meta[:column] == 29 assert meta[:column] == 29
after after
@@ -201,8 +205,8 @@ defmodule Kernel.TracersTest do
end end
""") """)
assert_receive {:defmodule, %{module: Sample, function: nil}} assert_received {:defmodule, %{module: Sample, function: nil}}
assert_receive {{:on_module, <<_::binary>>, :none}, %{module: Sample, function: nil}} assert_received {{:on_module, <<_::binary>>, :none}, %{module: Sample, function: nil}}
after after
:code.purge(Sample) :code.purge(Sample)
:code.delete(Sample) :code.delete(Sample)
@@ -213,8 +217,8 @@ defmodule Kernel.TracersTest do
Module.create(Sample, :ok, __ENV__) Module.create(Sample, :ok, __ENV__)
""") """)
assert_receive {:defmodule, %{module: Sample, function: nil}} assert_received {:defmodule, %{module: Sample, function: nil}}
assert_receive {{:on_module, <<_::binary>>, :none}, %{module: Sample, function: nil}} assert_received {{:on_module, <<_::binary>>, :none}, %{module: Sample, function: nil}}
after after
:code.purge(Sample) :code.purge(Sample)
:code.delete(Sample) :code.delete(Sample)
@@ -226,7 +230,7 @@ defmodule Kernel.TracersTest do
"foo\#{arg}" "foo\#{arg}"
""") """)
assert_receive {{:remote_macro, meta, Kernel, :to_string, 1}, _env} assert_received {{:remote_macro, meta, Kernel, :to_string, 1}, _env}
assert meta[:from_interpolation] assert meta[:from_interpolation]
end end
@@ -236,7 +240,7 @@ defmodule Kernel.TracersTest do
foo[:bar] foo[:bar]
""") """)
assert_receive {{:remote_function, meta, Access, :get, 2}, _env} assert_received {{:remote_function, meta, Access, :get, 2}, _env}
assert meta[:from_brackets] assert meta[:from_brackets]
compile_string(""" compile_string("""
@@ -248,17 +252,37 @@ defmodule Kernel.TracersTest do
end end
""") """)
assert_receive {{:remote_function, meta, Access, :get, 2}, _env} assert_received {{:remote_function, meta, Access, :get, 2}, _env}
assert meta[:from_brackets] assert meta[:from_brackets]
compile_string(""" compile_string("""
%{bar: 3}[:bar] %{bar: 3}[:bar]
""") """)
assert_receive {{:remote_function, meta, Access, :get, 2}, _env} assert_received {{:remote_function, meta, Access, :get, 2}, _env}
assert meta[:from_brackets] assert meta[:from_brackets]
end end
test "does not trace bind quoted twice" do
compile_string("""
quote bind_quoted: [foo: List.flatten([])] do
foo
end
""")
assert_received {{:remote_function, _, List, :flatten, 1}, _}
refute_received {{:remote_function, _, List, :flatten, 1}, _}
end
test "does not trace captures twice" do
compile_string("""
&List.flatten/1
""")
assert_received {{:remote_function, _, List, :flatten, 1}, _}
refute_received {{:remote_function, _, List, :flatten, 1}, _}
end
""" """
# Make sure this module is compiled with column information # Make sure this module is compiled with column information
defmodule MacroWithColumn do defmodule MacroWithColumn do
@@ -278,7 +302,7 @@ defmodule Kernel.TracersTest do
MacroWithColumn.some_macro(["hello", "world", "!"]) MacroWithColumn.some_macro(["hello", "world", "!"])
""") """)
assert_receive {{:alias_reference, meta, Enum}, _env} assert_received {{:alias_reference, meta, Enum}, _env}
refute meta[:column] refute meta[:column]
end end
end end
@@ -463,17 +463,6 @@ defmodule Kernel.WarningTest do
fn x -> match?(x, :value) end fn x -> match?(x, :value) end
""" """
) )
assert_warn_eval(
[
"nofile:1",
"variable \"&1\" is unused (this might happen when using a capture argument as a pattern)",
"variable \"&1\" is unused (this might happen when using a capture argument as a pattern)"
],
"""
&match?(&1, :value)
"""
)
end end
test "useless literal" do test "useless literal" do
@@ -25,6 +25,30 @@ defmodule Kernel.WithTest do
assert with({^key, res} <- ok(42), do: res) == 42 assert with({^key, res} <- ok(42), do: res) == 42
end end
test "pin matching with multiple else" do
key = :error
first_else =
with nil <- error() do
:ok
else
^key -> :pinned
_other -> :other
end
assert first_else == :pinned
second_else =
with nil <- ok(42) do
:ok
else
^key -> :pinned
_other -> :other
end
assert second_else == :other
end
test "two levels with" do test "two levels with" do
result = result =
with {:ok, n1} <- ok(11), with {:ok, n1} <- ok(11),
+1 -5
View File
@@ -21,11 +21,7 @@ defmodule KeywordTest do
end end
test "implements (almost) all functions in Map" do test "implements (almost) all functions in Map" do
assert Map.__info__(:functions) -- Keyword.__info__(:functions) == [ assert Map.__info__(:functions) -- Keyword.__info__(:functions) == [from_struct: 1]
from_struct: 1,
intersect: 2,
intersect: 3
]
end end
test "get_and_update/3 raises on bad return value from the argument function" do test "get_and_update/3 raises on bad return value from the argument function" do
@@ -13,6 +13,7 @@ defmodule Module.Types.DescrTest do
test "term" do test "term" do
assert union(term(), float()) == term() assert union(term(), float()) == term()
assert union(term(), binary()) == term() assert union(term(), binary()) == term()
assert union(term(), if_set(binary())) == if_set(term())
end end
test "none" do test "none" do
@@ -45,7 +46,7 @@ defmodule Module.Types.DescrTest do
reference() reference()
] ]
assert Enum.reduce(all, &union/2) == term() assert Enum.reduce(all, &union/2) |> equal?(term())
end end
test "dynamic" do test "dynamic" do
@@ -214,6 +215,8 @@ defmodule Module.Types.DescrTest do
# optional # optional
refute subtype?(closed_map(a: if_set(integer())), closed_map(a: integer())) refute subtype?(closed_map(a: if_set(integer())), closed_map(a: integer()))
assert subtype?(closed_map(a: integer()), closed_map(a: if_set(integer()))) assert subtype?(closed_map(a: integer()), closed_map(a: if_set(integer())))
refute subtype?(closed_map(a: if_set(term())), closed_map(a: term()))
assert subtype?(closed_map(a: term()), closed_map(a: if_set(term())))
end end
end end
@@ -253,12 +256,34 @@ defmodule Module.Types.DescrTest do
end end
end end
describe "queries" do
test "atom_type?" do
assert atom_type?(term(), :foo)
assert atom_type?(dynamic(), :foo)
assert atom_type?(atom([:foo, :bar]), :foo)
refute atom_type?(atom([:foo, :bar]), :baz)
assert atom_type?(negation(atom([:foo, :bar])), :baz)
refute atom_type?(union(atom([:foo, :bar]), integer()), :baz)
refute atom_type?(dynamic(union(atom([:foo, :bar]), integer())), :baz)
end
end
describe "projections" do describe "projections" do
test "fun_fetch" do
assert fun_fetch(term(), 1) == :error
assert fun_fetch(union(term(), dynamic(fun())), 1) == :error
assert fun_fetch(fun(), 1) == :ok
assert fun_fetch(dynamic(), 1) == :ok
end
test "atom_fetch" do test "atom_fetch" do
assert atom_fetch(term()) == :error assert atom_fetch(term()) == :error
assert atom_fetch(union(term(), dynamic(atom([:foo, :bar])))) == :error assert atom_fetch(union(term(), dynamic(atom([:foo, :bar])))) == :error
assert atom_fetch(atom()) == {:infinite, []} assert atom_fetch(atom()) == {:infinite, []}
assert atom_fetch(dynamic()) == {:infinite, []}
assert atom_fetch(atom([:foo, :bar])) == assert atom_fetch(atom([:foo, :bar])) ==
{:finite, [:foo, :bar] |> :sets.from_list(version: 2) |> :sets.to_list()} {:finite, [:foo, :bar] |> :sets.from_list(version: 2) |> :sets.to_list()}
@@ -21,6 +21,32 @@ defmodule Module.Types.ExprTest do
assert typecheck!(fn -> :ok end) == fun() assert typecheck!(fn -> :ok end) == fun()
end end
describe "funs" do
test "incompatible" do
assert typewarn!([%x{}], x.(1, 2)) ==
{dynamic(),
~l"""
incompatible types in expression:
x
expected type:
fun()
but got type:
dynamic(atom())
where "x" was given the type:
# type: dynamic(atom())
# from: types_test.ex:LINE-2
%x{}
"""}
end
end
describe "remotes" do describe "remotes" do
test "dynamic calls" do test "dynamic calls" do
assert typecheck!([%x{}], x.foo_bar()) == dynamic() assert typecheck!([%x{}], x.foo_bar()) == dynamic()
@@ -52,8 +78,6 @@ defmodule Module.Types.ExprTest do
<<x::integer>> <<x::integer>>
#{hints(:dot)} #{hints(:dot)}
typing violation found at:\
"""} """}
end end
@@ -70,8 +94,6 @@ defmodule Module.Types.ExprTest do
# type: integer() # type: integer()
# from: types_test.ex:LINE-2 # from: types_test.ex:LINE-2
<<x::integer>> <<x::integer>>
typing violation found at:\
"""} """}
end end
@@ -92,8 +114,6 @@ defmodule Module.Types.ExprTest do
# type: integer() # type: integer()
# from: types_test.ex:LINE-2 # from: types_test.ex:LINE-2
<<x::integer>> <<x::integer>>
typing violation found at:\
"""} """}
end end
end end
@@ -120,8 +140,6 @@ defmodule Module.Types.ExprTest do
# type: binary() # type: binary()
# from: types_test.ex:LINE-2 # from: types_test.ex:LINE-2
<<x::binary-size(2)>> <<x::binary-size(2)>>
typing violation found at:\
"""} """}
assert typewarn!([<<x::binary>>], <<x>>) == assert typewarn!([<<x::binary>>], <<x>>) ==
@@ -146,8 +164,6 @@ defmodule Module.Types.ExprTest do
<<x::binary>> <<x::binary>>
#{hints(:inferred_bitstring_spec)} #{hints(:inferred_bitstring_spec)}
typing violation found at:\
"""} """}
assert typewarn!([<<x>>], <<x::binary>>) == assert typewarn!([<<x>>], <<x::binary>>) ==
@@ -172,8 +188,6 @@ defmodule Module.Types.ExprTest do
<<x>> <<x>>
#{hints(:inferred_bitstring_spec)} #{hints(:inferred_bitstring_spec)}
typing violation found at:\
"""} """}
end end
end end
@@ -240,8 +254,6 @@ defmodule Module.Types.ExprTest do
# type: :foo # type: :foo
# from: types_test.ex:LINE-2 # from: types_test.ex:LINE-2
x = :foo x = :foo
typing violation found at:\
"""} """}
end end
@@ -264,52 +276,52 @@ defmodule Module.Types.ExprTest do
<<x::integer>> <<x::integer>>
#{hints(:dot)} #{hints(:dot)}
typing violation found at:\
"""} """}
end end
test "accessing an unknown field on struct" do test "accessing an unknown field on struct with diagnostic" do
assert typewarn!(%Point{}.foo_bar) == {type, diagnostic} = typediag!(%Point{}.foo_bar)
{dynamic(), assert type == dynamic()
~l""" assert diagnostic.span == {__ENV__.line - 2, 54}
unknown key .foo_bar in expression:
%Point{x: nil, y: nil, z: 0}.foo_bar assert diagnostic.message == ~l"""
unknown key .foo_bar in expression:
the given type does not have the given key: %Point{x: nil, y: nil, z: 0}.foo_bar
dynamic(%Point{x: nil, y: nil, z: integer()}) the given type does not have the given key:
typing violation found at:\ dynamic(%Point{x: nil, y: nil, z: integer()})
"""} """
end end
test "accessing an unknown field on struct in a var" do test "accessing an unknown field on struct in a var with diagnostic" do
assert typewarn!([x = %URI{}], x.foo_bar) == {type, diagnostic} = typediag!([x = %URI{}], x.foo_bar)
{dynamic(), assert type == dynamic()
~l""" assert diagnostic.span == {__ENV__.line - 2, 61}
unknown key .foo_bar in expression:
x.foo_bar assert diagnostic.message == ~l"""
unknown key .foo_bar in expression:
where "x" was given the type: x.foo_bar
# type: dynamic(%URI{ where "x" was given the type:
authority: term(),
fragment: term(),
host: term(),
path: term(),
port: term(),
query: term(),
scheme: term(),
userinfo: term()
})
# from: types_test.ex:LINE-2
x = %URI{}
typing violation found at:\ # type: dynamic(%URI{
"""} authority: term(),
fragment: term(),
host: term(),
path: term(),
port: term(),
query: term(),
scheme: term(),
userinfo: term()
})
# from: types_test.ex:LINE-4
x = %URI{}
"""
assert [%{type: :variable, name: :x}] = diagnostic.details.typing_traces
end end
end end
@@ -341,8 +353,6 @@ defmodule Module.Types.ExprTest do
While Elixir can compare across all types, you are comparing across types \ While Elixir can compare across all types, you are comparing across types \
which are always distinct, and the result is either always true or always false which are always distinct, and the result is either always true or always false
typing violation found at:\
"""} """}
end end
@@ -372,9 +382,7 @@ defmodule Module.Types.ExprTest do
# from: types_test.ex:LINE-2 # from: types_test.ex:LINE-2
y = %Point{} y = %Point{}
Comparison operators (>, <, >=, <=, min, and max) perform structural and not semantic comparison. Comparing with a struct won't give meaningful results. Struct that can be compared typically define a compare/2 function within their modules that can be used for semantic comparison Comparison operators (>, <, >=, <=, min, and max) perform structural and not semantic comparison. Comparing with a struct won't give meaningful results. Structs that can be compared typically define a compare/2 function within their modules that can be used for semantic comparison.
typing violation found at:\
"""} """}
end end
end end
@@ -433,8 +441,6 @@ defmodule Module.Types.ExprTest do
) )
# from: types_test.ex:LINE-5 # from: types_test.ex:LINE-5
rescue e in [SyntaxError, RuntimeError] -> rescue e in [SyntaxError, RuntimeError] ->
typing violation found at:\
"""} """}
end end
@@ -460,8 +466,6 @@ defmodule Module.Types.ExprTest do
rescue e -> rescue e ->
#{hints(:anonymous_rescue)} #{hints(:anonymous_rescue)}
typing violation found at:\
"""} """}
end end
end end
@@ -56,8 +56,6 @@ defmodule Module.Types.PatternTest do
# type: integer() # type: integer()
# from: types_test.ex:LINE-1 # from: types_test.ex:LINE-1
m = 123 m = 123
typing violation found at:\
""" """
end end
@@ -74,8 +72,6 @@ defmodule Module.Types.PatternTest do
# type: dynamic(%Point{x: term(), y: term(), z: term()}) # type: dynamic(%Point{x: term(), y: term(), z: term()})
# from: types_test.ex:LINE-2 # from: types_test.ex:LINE-2
x = %Point{} x = %Point{}
typing violation found at:\
"""} """}
end end
end end
@@ -115,8 +111,6 @@ defmodule Module.Types.PatternTest do
# type: float() # type: float()
# from: types_test.ex:LINE # from: types_test.ex:LINE
<<..., x::float>> <<..., x::float>>
typing violation found at:\
""" """
assert typeerror!([<<x::float, x>>], x) == ~l""" assert typeerror!([<<x::float, x>>], x) == ~l"""
@@ -135,8 +129,6 @@ defmodule Module.Types.PatternTest do
<<..., x>> <<..., x>>
#{hints(:inferred_bitstring_spec)} #{hints(:inferred_bitstring_spec)}
typing violation found at:\
""" """
end end
end end
@@ -38,6 +38,16 @@ defmodule TypeHelper do
end end
end end
@doc """
Main helper for checking the diagnostic of a given AST.
"""
defmacro typediag!(patterns \\ [], guards \\ [], body) do
quote do
unquote(typecheck(patterns, guards, body, __CALLER__))
|> TypeHelper.__typediag__!()
end
end
@doc false @doc false
def __typecheck__!({:ok, type, %{warnings: []}}), do: type def __typecheck__!({:ok, type, %{warnings: []}}), do: type
@@ -49,24 +59,30 @@ defmodule TypeHelper do
@doc false @doc false
def __typeerror__!({:error, %{warnings: [{module, warning, _locs} | _]}}), def __typeerror__!({:error, %{warnings: [{module, warning, _locs} | _]}}),
do: warning |> module.format_warning() |> IO.iodata_to_binary() do: module.format_diagnostic(warning).message
def __typeerror__!({:ok, type, _context}), def __typeerror__!({:ok, type, _context}),
do: raise("type checking ok but expected error: #{Descr.to_quoted_string(type)}") do: raise("type checking ok but expected error: #{Descr.to_quoted_string(type)}")
@doc false @doc false
def __typewarn__!({:ok, type, %{warnings: [{module, warning, _locs}]}}), def __typediag__!({:ok, type, %{warnings: [{module, warning, _locs}]}}),
do: {type, warning |> module.format_warning() |> IO.iodata_to_binary()} do: {type, module.format_diagnostic(warning)}
def __typewarn__!({:ok, type, %{warnings: []}}), def __typediag__!({:ok, type, %{warnings: []}}),
do: raise("type checking ok without warnings: #{Descr.to_quoted_string(type)}") do: raise("type checking ok without warnings: #{Descr.to_quoted_string(type)}")
def __typewarn__!({:ok, _type, %{warnings: warnings}}), def __typediag__!({:ok, _type, %{warnings: warnings}}),
do: raise("type checking ok but many warnings: #{inspect(warnings)}") do: raise("type checking ok but many warnings: #{inspect(warnings)}")
def __typewarn__!({:error, %{warnings: warnings}}), def __typediag__!({:error, %{warnings: warnings}}),
do: raise("type checking errored with warnings: #{inspect(warnings)}") do: raise("type checking errored with warnings: #{inspect(warnings)}")
@doc false
def __typewarn__!(result) do
{type, %{message: message}} = __typediag__!(result)
{type, message}
end
@doc """ @doc """
Building block for typechecking a given AST. Building block for typechecking a given AST.
""" """
+1 -1
View File
@@ -500,7 +500,7 @@ defmodule ExUnit do
defp maybe_repeated_run(options, seed, load_us, repeat) do defp maybe_repeated_run(options, seed, load_us, repeat) do
case ExUnit.Runner.run(options, load_us) do case ExUnit.Runner.run(options, load_us) do
{%{failures: 0}, {sync_modules, async_modules}} {%{failures: 0}, {async_modules, sync_modules}}
when repeat > 0 and (sync_modules != [] or async_modules != []) -> when repeat > 0 and (sync_modules != [] or async_modules != []) ->
ExUnit.Server.restore_modules(async_modules, sync_modules) ExUnit.Server.restore_modules(async_modules, sync_modules)
+7 -9
View File
@@ -535,15 +535,13 @@ defmodule ExUnit.Callbacks do
test, as simply shutting down the process would cause it to be restarted test, as simply shutting down the process would cause it to be restarted
according to its `:restart` value. according to its `:restart` value.
Another advantage is that the test process will act as both an ancestor Finally, since Elixir v1.17.0, the test supervisor has both `$ancestors`
as well as a caller to the supervised processes. When a process is started and `$callers` key in its process dictionary pointing to the test process.
under a supervision tree, it typically populates the `$ancestors` key in This means developers can invoke `Process.get(:"$callers", [])` in their
its process dictionary with all of its ancestors, which will include the test `start_link` function and forward it to the spawned process, which may set
process. Additionally, `start_supervised/2` will also store the test process `Process.put(:"$callers", callers)` during its initialization. This may be
in the `$callers` key of the started process, allowing tools that perform useful in projects who track process ownership during tests. You can learn
either ancestor or caller tracking to reach the test process. You can learn more about these keys in [the `Task` module](`Task#module-ancestor-and-caller-tracking`).
more about these keys in
[the `Task` module](`Task#module-ancestor-and-caller-tracking`).
""" """
@doc since: "1.5.0" @doc since: "1.5.0"
@spec start_supervised(Supervisor.child_spec() | module | {module, term}, keyword) :: @spec start_supervised(Supervisor.child_spec() | module | {module, term}, keyword) ::
+1 -1
View File
@@ -578,7 +578,7 @@ defmodule ExUnit.Case do
display. You can use `ExUnit.plural_rule/2` to set a custom display. You can use `ExUnit.plural_rule/2` to set a custom
pluralization. pluralization.
""" """
@doc since: "1.10.0" @doc since: "1.11.0"
def register_test(mod, file, line, test_type, name, tags) do def register_test(mod, file, line, test_type, name, tags) do
unless Module.has_attribute?(mod, :ex_unit_tests) do unless Module.has_attribute?(mod, :ex_unit_tests) do
raise "cannot define #{test_type}. Please make sure you have invoked " <> raise "cannot define #{test_type}. Please make sure you have invoked " <>
+1 -3
View File
@@ -298,7 +298,7 @@ defmodule Logger.Translator do
client_info: client, client_info: client,
name: name, name: name,
reason: {kind, reason, stack}, reason: {kind, reason, stack},
state: {state, data}, state: state,
queue: queue, queue: queue,
postponed: postponed, postponed: postponed,
callback_mode: callback_mode, callback_mode: callback_mode,
@@ -328,8 +328,6 @@ defmodule Logger.Translator do
msg, msg,
"\nState: ", "\nState: ",
inspect(state, inspect_opts), inspect(state, inspect_opts),
"\nData: ",
inspect(data, inspect_opts),
"\nCallback mode: ", "\nCallback mode: ",
"#{inspect(callback_mode, inspect_opts)}, state_enter: #{state_enter?}" "#{inspect(callback_mode, inspect_opts)}, state_enter: #{state_enter?}"
| format_client_info(client) | format_client_info(client)
+56 -10
View File
@@ -100,6 +100,28 @@ defmodule Logger.TranslatorTest do
end end
end end
defmodule MyGenStatemHandleEvent do
@behaviour :gen_statem
@impl true
def callback_mode, do: :handle_event_function
@impl true
def init(state) do
{:ok, :no_state, state}
end
@impl true
def handle_event({:call, _}, :error, :no_state, _data) do
raise "oops"
end
@impl :gen_statem
def format_status(_opts, [_pdict, _, state]) do
state
end
end
defmodule MyBridge do defmodule MyBridge do
@behaviour :supervisor_bridge @behaviour :supervisor_bridge
@@ -393,7 +415,7 @@ defmodule Logger.TranslatorTest do
assert {%RuntimeError{message: "oops"}, [_ | _]} = process_metadata[:crash_reason] assert {%RuntimeError{message: "oops"}, [_ | _]} = process_metadata[:crash_reason]
refute Map.has_key?(gen_statem_metadata, :initial_call) refute Map.has_key?(gen_statem_metadata, :initial_call)
assert process_metadata[:initial_call] == {Logger.TranslatorTest.MyGenStatem, :init, 1} assert process_metadata[:initial_call] == {MyGenStatem, :init, 1}
refute Map.has_key?(gen_statem_metadata, :registered_name) refute Map.has_key?(gen_statem_metadata, :registered_name)
refute Map.has_key?(process_metadata, :registered_name) refute Map.has_key?(process_metadata, :registered_name)
@@ -445,7 +467,7 @@ defmodule Logger.TranslatorTest do
assert capture_log(:debug, fn -> assert capture_log(:debug, fn ->
catch_exit(:gen_statem.call(pid, :error)) catch_exit(:gen_statem.call(pid, :error))
end) =~ """ end) =~ """
[:ok, :ok, :ok, ...] State: {:started, [:ok, ...]}
""" """
after after
Application.put_env(:logger, :translator_inspect_opts, []) Application.put_env(:logger, :translator_inspect_opts, [])
@@ -462,8 +484,7 @@ defmodule Logger.TranslatorTest do
.* .*
Queue: .* Queue: .*
Postponed: \[\] Postponed: \[\]
State: :started State: {:started, :ok}
Data: :ok
Callback mode: :state_functions, state_enter: false Callback mode: :state_functions, state_enter: false
Client #PID<\d+\.\d+\.\d+> is alive Client #PID<\d+\.\d+\.\d+> is alive
.* .*
@@ -488,8 +509,7 @@ defmodule Logger.TranslatorTest do
.* .*
Queue: .* Queue: .*
Postponed: \[\] Postponed: \[\]
State: :started State: {:started, :ok}
Data: :ok
Callback mode: :state_functions, state_enter: false Callback mode: :state_functions, state_enter: false
Client :named_client is alive Client :named_client is alive
.* .*
@@ -513,8 +533,7 @@ defmodule Logger.TranslatorTest do
.* .*
Queue: .* Queue: .*
Postponed: \[\] Postponed: \[\]
State: :started State: {:started, :ok}
Data: :ok
Callback mode: :state_functions, state_enter: false Callback mode: :state_functions, state_enter: false
Client #PID<\d+\.\d+\.\d+> is dead Client #PID<\d+\.\d+\.\d+> is dead
"""s """s
@@ -533,8 +552,7 @@ defmodule Logger.TranslatorTest do
.* .*
Queue: .* Queue: .*
Postponed: \[\] Postponed: \[\]
State: :started State: {:started, :ok}
Data: :ok
Callback mode: :state_functions, state_enter: false Callback mode: :state_functions, state_enter: false
"""s """s
end end
@@ -552,6 +570,34 @@ defmodule Logger.TranslatorTest do
assert_receive {:event, {:string, ["Process " | _]}, _process_metadata} assert_receive {:event, {:string, ["Process " | _]}, _process_metadata}
end end
test "translates :gen_statem crashes when callback_mode is :handle_event_function" do
{:ok, pid} = :gen_statem.start(MyGenStatemHandleEvent, :ok, [])
assert capture_log(:debug, fn ->
catch_exit(:gen_statem.call(pid, :error))
end) =~ ~r"""
\[error\] :gen_statem #PID<\d+\.\d+\.\d+> terminating
\*\* \(RuntimeError\) oops
.*
Queue: .*
Postponed: \[\]
State: :ok
Callback mode: .*, state_enter: false
"""s
assert_receive {:event, {:string, [[":gen_statem " <> _ | _] | _]}, gen_statem_metadata}
assert_receive {:event, {:string, ["Process " | _]}, process_metadata}
assert {%RuntimeError{message: "oops"}, [_ | _]} = gen_statem_metadata[:crash_reason]
assert {%RuntimeError{message: "oops"}, [_ | _]} = process_metadata[:crash_reason]
refute Map.has_key?(gen_statem_metadata, :initial_call)
assert process_metadata[:initial_call] == {MyGenStatemHandleEvent, :init, 1}
refute Map.has_key?(gen_statem_metadata, :registered_name)
refute Map.has_key?(process_metadata, :registered_name)
end
test "translates Task crashes" do test "translates Task crashes" do
{:ok, pid} = Task.start_link(__MODULE__, :task, [self()]) {:ok, pid} = Task.start_link(__MODULE__, :task, [self()])
parent = self() parent = self()
+43 -28
View File
@@ -609,8 +609,8 @@ defmodule Mix.Compilers.Elixir do
# within the dependency, they will be recompiled. However, export # within the dependency, they will be recompiled. However, export
# and runtime dependencies won't have recompiled so we need to # and runtime dependencies won't have recompiled so we need to
# propagate them to the parent app. # propagate them to the parent app.
{dep_modules, _, _} = dep_modules =
fixpoint_runtime_modules(manifest_sources, Map.from_keys(dep_modules, true)) fixpoint_non_compile_modules(manifest_sources, Map.from_keys(dep_modules, true))
old_exports = Map.get(deps_exports, app, %{}) old_exports = Map.get(deps_exports, app, %{})
@@ -654,21 +654,19 @@ defmodule Mix.Compilers.Elixir do
end end
end end
defp fixpoint_runtime_modules(sources, modules) when modules != %{} do defp fixpoint_non_compile_modules(sources, modules) when modules != %{} do
fixpoint_runtime_modules(Map.to_list(sources), modules, false, [], [], sources) fixpoint_non_compile_modules(Map.to_list(sources), modules, false, [])
end end
defp fixpoint_runtime_modules(sources, modules) do defp fixpoint_non_compile_modules(_sources, modules) do
{modules, [], sources} modules
end end
defp fixpoint_runtime_modules( defp fixpoint_non_compile_modules(
[{source_path, source_entry} = pair | sources], [{_source_path, source_entry} = pair | sources],
modules, modules,
new?, new?,
pending_sources, pending_sources
acc_modules,
acc_sources
) do ) do
source(export_references: export_refs, runtime_references: runtime_refs) = source_entry source(export_references: export_refs, runtime_references: runtime_refs) = source_entry
@@ -676,24 +674,19 @@ defmodule Mix.Compilers.Elixir do
new_modules = Enum.reject(source(source_entry, :modules), &Map.has_key?(modules, &1)) new_modules = Enum.reject(source(source_entry, :modules), &Map.has_key?(modules, &1))
modules = Enum.reduce(new_modules, modules, &Map.put(&2, &1, true)) modules = Enum.reduce(new_modules, modules, &Map.put(&2, &1, true))
new? = new? or new_modules != [] new? = new? or new_modules != []
acc_modules = new_modules ++ acc_modules fixpoint_non_compile_modules(sources, modules, new?, pending_sources)
acc_sources =
Map.replace!(acc_sources, source_path, source(source_entry, runtime_warnings: []))
fixpoint_runtime_modules(sources, modules, new?, pending_sources, acc_modules, acc_sources)
else else
pending_sources = [pair | pending_sources] pending_sources = [pair | pending_sources]
fixpoint_runtime_modules(sources, modules, new?, pending_sources, acc_modules, acc_sources) fixpoint_non_compile_modules(sources, modules, new?, pending_sources)
end end
end end
defp fixpoint_runtime_modules([], modules, new?, pending_sources, acc_modules, acc_sources) defp fixpoint_non_compile_modules([], modules, new?, pending_sources)
when new? == false or pending_sources == [], when new? == false or pending_sources == [],
do: {modules, acc_modules, acc_sources} do: modules
defp fixpoint_runtime_modules([], modules, true, pending_sources, acc_modules, acc_sources), defp fixpoint_non_compile_modules([], modules, true, pending_sources),
do: fixpoint_runtime_modules(pending_sources, modules, false, [], acc_modules, acc_sources) do: fixpoint_non_compile_modules(pending_sources, modules, false, [])
defp exports_md5(module, use_attributes?) do defp exports_md5(module, use_attributes?) do
cond do cond do
@@ -1068,7 +1061,7 @@ defmodule Mix.Compilers.Elixir do
end end
end end
defp each_cycle(runtime_modules, compile_path, timestamp, state) do defp each_cycle(stale_modules, compile_path, timestamp, state) do
{modules, _exports, sources, changed, pending_modules, stale_exports} = state {modules, _exports, sources, changed, pending_modules, stale_exports} = state
{pending_modules, exports, changed} = {pending_modules, exports, changed} =
@@ -1081,11 +1074,33 @@ defmodule Mix.Compilers.Elixir do
end end
if changed == [] do if changed == [] do
# We merge runtime_modules (which is a map of %{module => true}) into # We merge stale_modules (which is a map of %{module => true} that the user changed)
# a map of modules (which is a map of %{module => record}). This is fine # into a map of modules we compiled (which is a map of %{module => record}). This is
# since fixpoint_runtime_modules only cares about map keys. # fine because we only care about the keys.
{_, runtime_modules, sources} = runtime_modules = Map.merge(modules, stale_modules)
fixpoint_runtime_modules(sources, Map.merge(modules, runtime_modules))
# Now we do a simple pass finding anything that directly depends on the modules that
# changed. We don't need to compute a fixpoint, because now only the directly affected
# matter.
{sources, runtime_modules} =
Enum.reduce(sources, {sources, Map.keys(runtime_modules)}, fn
{source_path, source_entry}, {acc_sources, acc_modules} ->
source(export_references: export_refs, runtime_references: runtime_refs) =
source_entry
if has_any_key?(runtime_modules, export_refs) or
has_any_key?(runtime_modules, runtime_refs) do
acc_sources =
Map.replace!(acc_sources, source_path, source(source_entry, runtime_warnings: []))
new_modules =
Enum.reject(source(source_entry, :modules), &Map.has_key?(runtime_modules, &1))
{acc_sources, new_modules ++ acc_modules}
else
{acc_sources, acc_modules}
end
end)
runtime_paths = runtime_paths =
Enum.map(runtime_modules, &{&1, Path.join(compile_path, Atom.to_string(&1) <> ".beam")}) Enum.map(runtime_modules, &{&1, Path.join(compile_path, Atom.to_string(&1) <> ".beam")})
+8
View File
@@ -27,6 +27,13 @@ defmodule Mix.Generator do
if opts[:force] || overwrite?(path, contents) do if opts[:force] || overwrite?(path, contents) do
File.mkdir_p!(Path.dirname(path)) File.mkdir_p!(Path.dirname(path))
contents =
case opts[:format_elixir] do
true -> [Code.format_string!(contents), ?\n]
_ -> contents
end
File.write!(path, contents) File.write!(path, contents)
true true
else else
@@ -94,6 +101,7 @@ defmodule Mix.Generator do
* `:force` - forces copying without a shell prompt * `:force` - forces copying without a shell prompt
* `:quiet` - does not log command output * `:quiet` - does not log command output
* `:format_elixir` (since v1.18.0) - if `true`, apply formatter to the generated file
## Examples ## Examples
+18 -16
View File
@@ -9,7 +9,7 @@ defmodule Mix.SCM.Git do
@impl true @impl true
def format(opts) do def format(opts) do
if rev = get_opts_rev(opts) do if rev = opts[:ref] || opts[:branch] || opts[:tag] do
"#{redact_uri(opts[:git])} - #{rev}" "#{redact_uri(opts[:git])} - #{rev}"
else else
redact_uri(opts[:git]) redact_uri(opts[:git])
@@ -22,7 +22,7 @@ defmodule Mix.SCM.Git do
{:git, _, lock_rev, lock_opts} -> {:git, _, lock_rev, lock_opts} ->
lock = String.slice(lock_rev, 0, 7) lock = String.slice(lock_rev, 0, 7)
case Enum.find_value([:branch, :ref, :tag], &List.keyfind(lock_opts, &1, 0)) do case Enum.find_value([:ref, :branch, :tag], &List.keyfind(lock_opts, &1, 0)) do
{:ref, _} -> lock <> " (ref)" {:ref, _} -> lock <> " (ref)"
{key, val} -> lock <> " (#{key}: #{val})" {key, val} -> lock <> " (#{key}: #{val})"
nil -> lock nil -> lock
@@ -125,18 +125,18 @@ defmodule Mix.SCM.Git do
sparse_toggle(opts) sparse_toggle(opts)
update_origin(opts[:git]) update_origin(opts[:git])
rev = get_lock_rev(opts[:lock], opts) || get_opts_rev(opts)
# Fetch external data # Fetch external data
branch_or_tag = opts[:branch] || opts[:tag]
["--git-dir=.git", "fetch", "--force", "--quiet"] ["--git-dir=.git", "fetch", "--force", "--quiet"]
|> Kernel.++(progress_switch(git_version())) |> Kernel.++(progress_switch(git_version()))
|> Kernel.++(tags_switch(opts[:tag])) |> Kernel.++(tags_switch(opts[:tag]))
|> Kernel.++(depth_switch(opts[:depth])) |> Kernel.++(depth_switch(opts[:depth]))
|> Kernel.++(if rev, do: ["origin", rev], else: []) |> Kernel.++(if branch_or_tag, do: ["origin", branch_or_tag], else: [])
|> git!() |> git!()
# Migrate the Git repo # Migrate the Git repo
rev = rev || default_branch() rev = get_lock_rev(opts[:lock], opts) || opts[:ref] || branch_or_tag || default_branch()
git!(["--git-dir=.git", "checkout", "--quiet", rev]) git!(["--git-dir=.git", "checkout", "--quiet", rev])
if opts[:submodules] do if opts[:submodules] do
@@ -250,18 +250,24 @@ defmodule Mix.SCM.Git do
end end
defp validate_depth(opts) do defp validate_depth(opts) do
case Keyword.take(opts, [:depth]) do case Keyword.take(opts, [:depth, :ref]) do
[] -> [_, _] ->
Mix.raise(
"Cannot specify :depth and :ref at the same time. " <>
"Error on Git dependency: #{redact_uri(opts[:git])}"
)
[depth: depth] when is_integer(depth) and depth > 0 ->
opts opts
[{:depth, depth}] when is_integer(depth) and depth > 0 -> [depth: invalid_depth] ->
opts
invalid_depth ->
Mix.raise( Mix.raise(
"The depth must be a positive integer, and be specified only once, got: #{inspect(invalid_depth)}. " <> "The depth must be a positive integer, and be specified only once, got: #{inspect(invalid_depth)}. " <>
"Error on Git dependency: #{redact_uri(opts[:git])}" "Error on Git dependency: #{redact_uri(opts[:git])}"
) )
_ ->
opts
end end
end end
@@ -290,10 +296,6 @@ defmodule Mix.SCM.Git do
end end
end end
defp get_opts_rev(opts) do
opts[:branch] || opts[:ref] || opts[:tag]
end
defp redact_uri(git) do defp redact_uri(git) do
case URI.parse(git) do case URI.parse(git) do
%{userinfo: nil} -> git %{userinfo: nil} -> git
+2 -2
View File
@@ -66,8 +66,8 @@ defmodule Mix.Tasks.Compile.Leex do
defp preload(project) do defp preload(project) do
# TODO: Remove me in Elixir v2.0 # TODO: Remove me in Elixir v2.0
unless :leex in List.wrap(project[:compilers]) do unless :leex in List.wrap(project[:compilers]) do
IO.warn( Mix.shell().error(
"in order to compile .xrl files, you must add \"compilers: [:leex] ++ Mix.compilers()\" to the \"def project\" section of #{project[:app]}'s mix.exs" "warning: in order to compile .xrl files, you must add \"compilers: [:leex] ++ Mix.compilers()\" to the \"def project\" section of #{project[:app]}'s mix.exs"
) )
end end
+2 -2
View File
@@ -67,8 +67,8 @@ defmodule Mix.Tasks.Compile.Yecc do
defp preload(project) do defp preload(project) do
# TODO: Remove me in Elixir v2.0 # TODO: Remove me in Elixir v2.0
unless :yecc in List.wrap(project[:compilers]) do unless :yecc in List.wrap(project[:compilers]) do
IO.warn( Mix.shell().error(
"in order to compile .yrl files, you must add \"compilers: [:yecc] ++ Mix.compilers()\" to the \"def project\" section of #{project[:app]}'s mix.exs" "warning: in order to compile .yrl files, you must add \"compilers: [:yecc] ++ Mix.compilers()\" to the \"def project\" section of #{project[:app]}'s mix.exs"
) )
end end
+2 -1
View File
@@ -125,7 +125,8 @@ defmodule Mix.Tasks.Deps do
* `:depth` *(since v1.17.0)* - creates a shallow clone of the Git repository, * `:depth` *(since v1.17.0)* - creates a shallow clone of the Git repository,
limiting the history to the specified number of commits. This can significantly limiting the history to the specified number of commits. This can significantly
improve clone speed for large repositories when full history is not needed. improve clone speed for large repositories when full history is not needed.
The value must be a positive integer, typically `1`. The value must be a positive integer, typically `1`. Cannot be used with the
`:ref` option.
If your Git repository requires authentication, such as basic username:password If your Git repository requires authentication, such as basic username:password
HTTP authentication via URLs, it can be achieved via Git configuration, keeping HTTP authentication via URLs, it can be achieved via Git configuration, keeping
+4 -9
View File
@@ -224,9 +224,9 @@ defmodule Mix.Tasks.Profile.Eprof do
calls_opt = Keyword.get(opts, :calls, 0) calls_opt = Keyword.get(opts, :calls, 0)
time_opt = Keyword.get(opts, :time, 0) time_opt = Keyword.get(opts, :time, 0)
call_results Enum.filter(call_results, fn {_mfa, {count, time}} ->
|> Stream.filter(fn {_mfa, {count, _time}} -> count >= calls_opt end) count >= calls_opt and time >= time_opt
|> Stream.filter(fn {_mfa, {_count, time}} -> time >= time_opt end) end)
end end
defp sort_results(call_results, opts) do defp sort_results(call_results, opts) do
@@ -307,15 +307,10 @@ defmodule Mix.Tasks.Profile.Eprof do
max_lengths = Enum.map(header, &String.length/1) max_lengths = Enum.map(header, &String.length/1)
Enum.reduce(rows, max_lengths, fn row, max_lengths -> Enum.reduce(rows, max_lengths, fn row, max_lengths ->
Stream.map(row, &String.length/1) Enum.zip_with(row, max_lengths, fn cell, length -> String.length(cell) |> max(length) end)
|> Stream.zip(max_lengths)
|> Enum.map(&max/1)
end) end)
end end
defp max({a, b}) when a >= b, do: a
defp max({_, b}), do: b
@format "~-*s ~*s ~*s ~*s ~*s~n" @format "~-*s ~*s ~*s ~*s ~*s~n"
defp print_row(row, column_lengths) do defp print_row(row, column_lengths) do
+455
View File
@@ -0,0 +1,455 @@
defmodule Mix.Tasks.Profile.Tprof do
use Mix.Task
@shortdoc "Profiles the given file or expression with tprof"
@moduledoc """
Profiles the given file or expression using Erlang's `tprof` tool.
Requires Erlang/OTP27 or above.
[`:tprof`](`:tprof`) is an experimental module introduced in Erlang/OTP 27 which
provides a unified API for measuring call count, time, and allocation, and aims to
replace [`:eprof`](`:eprof`) and [`:cprof`](`:cprof`).
It can be useful when you want to discover the bottlenecks related to any of these
measurements.
Before running the code, it invokes the `app.start` task which compiles
and loads your project. After that, the target expression is profiled together
with all matching function calls using the Erlang trace BIFs. The tracing of
the function calls for that is enabled when the profiling is begun, and
disabled when profiling is stopped.
To profile the code, you can use syntax similar to the `mix run` task:
$ mix profile.tprof -e Hello.world
$ mix profile.tprof -e "[1, 2, 3] |> Enum.reverse |> Enum.map(&Integer.to_string/1)"
$ mix profile.tprof my_script.exs arg1 arg2 arg3
By default, tprof uses the `time` type, but you can profile memory too:
$ mix profile.tprof -e "Enum.map([1, 2, 3], &Integer.to_string/1)" --type memory
Call count is present with both type `time` and `memory`, but if you only need
the call count information, you can use the type `calls` which has the lowest footprint:
$ mix profile.tprof -e "Enum.map([1, 2, 3], &Integer.to_string/1)" --type calls
This task is automatically re-enabled, so you can profile multiple times
in the same Mix invocation.
## Command line options
* `--matching` - only profile calls matching the given `Module.function/arity` pattern
* `--type` - the type of profiling, `calls`, `time` or `memory` (default: `time`)
* `--calls` - filters out any results with a call count lower than this
* `--time` - filters out any results that took lower than specified (in µs), the `type` needs to be `time`
* `--memory` - filters out any results that used less memory than specified (in words), the `type` needs to be `memory`
* `--sort` - sorts the results by `calls`, `per_call` or by the value of `type` (default: the value of `type`)
* `--report` - returns the per-process breakdown when `process`, or the total for all processes when `total` (default: `process`).
Always `total` when `type` is `calls`.
* `--eval`, `-e` - evaluates the given code
* `--require`, `-r` - requires pattern before running the command
* `--parallel`, `-p` - makes all requires parallel
* `--no-warmup` - skips the warmup step before profiling
* `--no-compile` - does not compile even if files require compilation
* `--no-deps-check` - does not check dependencies
* `--no-archives-check` - does not check archives
* `--no-halt` - does not halt the system after running the command
* `--no-start` - does not start applications after compilation
* `--no-elixir-version-check` - does not check the Elixir version from mix.exs
## Profile output
Example output (`time` type):
Profile results of #PID<0.107.0>
# CALLS % TIME µS/CALL
Total 20 100.00 2 0.10
String.Chars.Integer.to_string/1 5 0.00 0 0.00
anonymous fn/0 in :elixir_compiler_1.__FILE__/1 1 0.00 0 0.00
Enum.each/2 1 0.00 0 0.00
Enum.reduce_range/5 3 0.00 0 0.00
:erlang.integer_to_binary/1 5 50.00 1 0.20
anonymous fn/3 in Enum.each/2 5 50.00 1 0.20
Profile done over 6 matching functions
Example output (`memory` type):
Profile results of #PID<0.107.0>
# CALLS % WORDS PER CALL
Total 6 100.00 19 3.17
Enum.each/2 1 21.05 4 4.00
:erlang.integer_to_binary/1 5 78.95 15 3.00
Profile done over 2 matching functions
Example output (`calls` type)
Profile results over all processes
# CALLS %
Total 20 100.00
anonymous fn/0 in :elixir_compiler_1.__FILE__/1 1 5.00
Enum.each/2 1 5.00
Enum.reduce_range/5 3 15.00
:erlang.integer_to_binary/1 5 25.00
String.Chars.Integer.to_string/1 5 25.00
anonymous fn/3 in Enum.each/2 5 25.00
Profile done over 6 matching functions
The default output contains data gathered from all matching functions. The first
row after the header contains the sums of the partial results and the average time
or memory usage for all the function calls listed.
The following rows contain the function call, followed by the number of times that
the function was called, then by the percentage of time/memory that the call uses,
then the total time/memory for that function in microseconds/words, and, finally,
the average time/memory per call in microseconds/words.
When `--matching` option is specified, call count tracing will be started only for
the functions matching the given pattern:
Profile results of #PID<0.106.0>
# CALLS % TIME µS/CALL
Total 5 100.00 1 0.20
String.Chars.Integer.to_string/1 5 100.00 1 0.20
Profile done over 1 matching functions
The pattern can be a module name, such as `String` to count all calls to that module,
a call without arity, such as `String.split`, to count all calls to that function
regardless of arity, or a call with arity, such as `String.split/3`, to count all
calls to that exact module, function and arity.
## Caveats
You should be aware that the code being profiled is running in an anonymous
function which is invoked by [`:tprof` module](https://www.erlang.org/doc/man/tprof.html).
Thus, you'll see some additional entries in your profile output. It is also
important to note that the profiler is stopped as soon as the code has finished running,
and this may need special attention, when: running asynchronous code as function calls which were
called before the profiler stopped will not be counted; running synchronous code as long
running computations and a profiler without a proper MFA trace pattern or filter may
lead to a result set which is difficult to comprehend.
You should expect a slowdown in your code execution using this tool since `:tprof` has
some performance impact on the execution, but the impact is considerably lower than
`Mix.Tasks.Profile.Fprof`. If you have a large system try to profile a limited
scenario or focus on the main modules or processes. The `calls` type can also be used,
which is more limited but has a lower footprint.
"""
@switches [
parallel: :boolean,
require: :keep,
eval: :keep,
config: :keep,
matching: :string,
halt: :boolean,
compile: :boolean,
deps_check: :boolean,
type: :string,
calls: :integer,
time: :integer,
memory: :integer,
sort: :string,
report: :string,
start: :boolean,
archives_check: :boolean,
warmup: :boolean,
elixir_version_check: :boolean,
parallel_require: :keep
]
@aliases [
r: :require,
p: :parallel,
e: :eval,
c: :config
]
@impl true
def run(args) do
{opts, head} = OptionParser.parse_head!(args, aliases: @aliases, strict: @switches)
Mix.Task.reenable("profile.tprof")
Mix.Tasks.Run.run(
["--no-mix-exs" | args],
opts,
head,
&profile_code(&1, opts),
&profile_code(File.read!(&1), opts)
)
end
defp profile_code(code_string, opts) do
opts = Enum.map(opts, &parse_opt/1)
content =
quote do
unquote(__MODULE__).profile(
fn ->
unquote(Code.string_to_quoted!(code_string))
end,
unquote(Macro.escape(opts))
)
end
# Use compile_quoted since it leaves less noise than eval_quoted
Code.compile_quoted(content)
end
defp parse_opt({:matching, matching}) do
case Mix.Utils.parse_mfa(matching) do
{:ok, [m, f, a]} -> {:matching, {m, f, a}}
{:ok, [m, f]} -> {:matching, {m, f, :_}}
{:ok, [m]} -> {:matching, {m, :_, :_}}
:error -> Mix.raise("Invalid matching pattern: #{matching}")
end
end
defp parse_opt({:type, "time"}), do: {:type, :time}
defp parse_opt({:type, "calls"}), do: {:type, :calls}
defp parse_opt({:type, "memory"}), do: {:type, :memory}
defp parse_opt({:type, other}), do: Mix.raise("Invalid type option: #{other}")
defp parse_opt({:report, "process"}), do: {:report, :process}
defp parse_opt({:report, "total"}), do: {:report, :total}
defp parse_opt({:report, other}), do: Mix.raise("Invalid report option: #{other}")
defp parse_opt({:sort, "time"}), do: {:sort, :time}
defp parse_opt({:sort, "calls"}), do: {:sort, :calls}
defp parse_opt({:sort, "memory"}), do: {:sort, :memory}
defp parse_opt({:sort, "per_call"}), do: {:sort, :per_call}
defp parse_opt({:sort, other}), do: Mix.raise("Invalid sort option: #{other}")
defp parse_opt(other), do: other
@doc """
Allows to programmatically run the `tprof` profiler on expression in `fun`.
Returns the return value of `fun`.
## Options
* `:matching` - only profile calls matching the given pattern in form of
`{module, function, arity}`, where each element may be replaced by `:_`
to allow any value
* `:type` - the type of profiling, possible values are `:time`, `:memory` or `:calls`,
(default: `:time`), see [moduledoc](`Mix.Tasks.Profile.Tprof`) for more information
* `:calls` - filters out any results with a call count lower than this
* `:time` - filters out any results that took lower than specified (in µs),
`type` needs to be `:time`
* `:memory` - filters out any results that used less memory than specified (in words),
`type` needs to be `:memory`
* `:sort` - sort the results by `:calls`, `:per_call` or by the value of `type`
(default: the value of `type`)
* `:report` - returns the per-process breakdown when `:process`, or the total for all
processes when `:total` (default: `:process`). Always `:total` when `type` is `:calls`.
* `:warmup` - if the code should be warmed up before profiling (default: `true`)
* `:set_on_spawn` - if newly spawned processes should be measured (default: `true`)
"""
@spec profile((-> result), keyword()) :: result when result: any()
def profile(fun, opts \\ []) when is_function(fun, 0) do
Mix.ensure_application!(:tools)
{type, return_value, results} = profile_and_analyse(fun, opts)
print_output(type, results)
return_value
end
defp profile_and_analyse(fun, opts) do
if Keyword.get(opts, :warmup, true) do
IO.puts("Warmup...\n")
fun.()
end
matching = Keyword.get(opts, :matching, {:_, :_, :_})
set_on_spawn = Keyword.get(opts, :set_on_spawn, true)
type = Keyword.get(opts, :type, :time)
report = Keyword.get(opts, :report, :process)
sort_by =
case Keyword.get(opts, :sort) do
nil ->
:measurement
:calls ->
:calls
:per_call ->
:measurement_per_call
^type ->
:measurement
other ->
Mix.raise("Incompatible sort option #{inspect(other)} with type #{inspect(type)}")
end
tprof_type = to_tprof_type(type)
{return_value, {^tprof_type, traces}} =
tprof_module().profile(fun, %{
set_on_spawn: set_on_spawn,
pattern: matching,
type: tprof_type,
report: :return
})
inspected = tprof_module().inspect({tprof_type, traces}, report, sort_by)
results =
inspected
|> Enum.map(fn {pid, {^tprof_type, measurement_total, call_results}} ->
parsed_calls =
call_results
|> filter_results(type, opts)
|> add_totals(measurement_total)
{pid, parsed_calls}
end)
{type, return_value, results}
end
defp to_tprof_type(:calls), do: :call_count
defp to_tprof_type(:time), do: :call_time
defp to_tprof_type(:memory), do: :call_memory
defp filter_results(call_results, type, opts) do
calls_opt = Keyword.get(opts, :calls, 0)
measurement_opt =
get_filter_value!(type, Keyword.get(opts, :time), Keyword.get(opts, :memory))
Enum.filter(call_results, fn {_module, _fa, count, measurement, _, _} ->
count >= calls_opt and measurement >= measurement_opt
end)
end
defp get_filter_value!(type, time, _memory) when is_integer(time) and type != :time do
Mix.raise("Incompatible use of time option with type #{inspect(type)}")
end
defp get_filter_value!(type, _time, memory) when is_integer(memory) and type != :memory do
Mix.raise("Incompatible use of memory option with type #{inspect(type)}")
end
defp get_filter_value!(:time, time, nil) when is_integer(time), do: time
defp get_filter_value!(:memory, nil, memory) when is_integer(memory), do: memory
defp get_filter_value!(_, nil, nil), do: 0
defp add_totals(call_results, measurement_total) do
{function_count, calls} =
Enum.reduce(call_results, {0, 0}, fn {_mod, _fa, count, _, _, _}, acc ->
{function_count, calls} = acc
{function_count + 1, calls + count}
end)
{function_count, call_results, calls, measurement_total}
end
defp print_output(_type, []) do
print_function_count(0)
end
defp print_output(type, results) do
Enum.each(results, &print_result(type, &1))
end
defp print_result(type, {pid, {function_count, call_results, calls, total_measurement}}) do
header = header(type)
formatted_rows = Enum.map(call_results, &format_row/1)
formatted_total = format_total(total_measurement, calls)
column_lengths = column_lengths(header, [formatted_total | formatted_rows])
IO.puts("")
print_pid_row(pid)
print_row(header, column_lengths, type)
print_row(formatted_total, column_lengths, type)
Enum.each(formatted_rows, &print_row(&1, column_lengths, type))
IO.puts("")
print_function_count(function_count)
end
defp header(:calls), do: ["#", "CALLS", "%"]
defp header(:time), do: ["#", "CALLS", "%", "TIME", "µS/CALL"]
defp header(:memory), do: ["#", "CALLS", "%", "WORDS", "PER CALL"]
defp print_pid_row(:all) do
IO.puts("Profile results over all processes")
end
defp print_pid_row(pid) when is_pid(pid) do
IO.puts("Profile results of #{inspect(pid)}")
end
defp format_row({module, {function, arity}, count, measurement, per_call, percentage}) do
mfa = Exception.format_mfa(module, function, arity)
percentage = :erlang.float_to_binary(percentage, [{:decimals, 2}])
per_call = :erlang.float_to_binary(per_call, [{:decimals, 2}])
count = Integer.to_string(count)
measurement = Integer.to_string(measurement)
[mfa, count, percentage, measurement, per_call]
end
defp format_total(total_measurement, total_count) do
per_call = :erlang.float_to_binary(divide(total_measurement, total_count), [{:decimals, 2}])
[
"Total",
Integer.to_string(total_count),
"100.00",
Integer.to_string(total_measurement),
per_call
]
end
defp divide(_, 0), do: 0.0
defp divide(t, n), do: t / n
defp column_lengths(header, rows) do
max_lengths = Enum.map(header, &String.length/1)
Enum.reduce(rows, max_lengths, fn row, max_lengths ->
Enum.zip_with(row, max_lengths, fn cell, length -> String.length(cell) |> max(length) end)
end)
end
@call_format "~-*s ~*s ~*s~n"
@measurement_format "~-*s ~*s ~*s ~*s ~*s~n"
defp print_row(row, column_lengths, type) do
to_print =
column_lengths
|> Stream.zip(Stream.map(row, &String.to_charlist/1))
|> Enum.flat_map(&Tuple.to_list/1)
case type do
:calls -> :io.format(@call_format, to_print)
_ -> :io.format(@measurement_format, to_print)
end
end
defp print_function_count(count) do
IO.puts("Profile done over #{count} matching functions")
end
# TODO remove once we require Erlang/OTP 27+
defp tprof_module do
if Code.ensure_loaded?(:tprof) do
:tprof
else
Mix.raise("mix profile.tprof requires Erlang/OTP 27 or above")
end
end
end
+6 -6
View File
@@ -402,8 +402,8 @@ defmodule Mix.Tasks.Release do
If you are setting this option manually, we recommend the cookie option If you are setting this option manually, we recommend the cookie option
to be a long and randomly generated string, such as: to be a long and randomly generated string, such as:
`Base.url_encode64(:crypto.strong_rand_bytes(40))`. We also recommend to restrict `Base.encode32(:crypto.strong_rand_bytes(40))`. We also recommend restricting
the characters in the cookie to the subset returned by `Base.url_encode64/1`. the characters in the cookie to only alphanumeric characters and underscore.
* `:validate_compile_env` - by default a release will match all runtime * `:validate_compile_env` - by default a release will match all runtime
configuration against any configuration that was marked at compile time configuration against any configuration that was marked at compile time
@@ -601,7 +601,7 @@ defmodule Mix.Tasks.Release do
-start_epmd false -erl_epmd_port 6789 -start_epmd false -erl_epmd_port 6789
# In remote.vm.args.eex # In remote.vm.args.eex
# -start_epmd false -erl_epmd_port 6789 -dist_listen false -start_epmd false -erl_epmd_port 6789 -dist_listen false
You can pick any port of your choice. You can pick any port of your choice.
@@ -833,9 +833,9 @@ defmodule Mix.Tasks.Release do
* `RELEASE_DISTRIBUTION` - how do we want to run the distribution. * `RELEASE_DISTRIBUTION` - how do we want to run the distribution.
May be `name` (long names), `sname` (short names) or `none` May be `name` (long names), `sname` (short names) or `none`
(distribution is not started automatically). Defaults to (distribution is not started automatically). Defaults to `sname`.
`sname` which allows access only within the current system. When connecting nodes across hosts, you typically want to set
`name` allows external connections this to `name` (required to use IPs as host names)
* `RELEASE_BOOT_SCRIPT` - the name of the boot script to use when starting * `RELEASE_BOOT_SCRIPT` - the name of the boot script to use when starting
the release. This script is used when running commands such as `start` and the release. This script is used when running commands such as `start` and
+31 -8
View File
@@ -53,7 +53,7 @@ defmodule Mix.Tasks.Release.Init do
## Enable deployment without epmd ## Enable deployment without epmd
## (requires changing both vm.args and remote.vm.args) ## (requires changing both vm.args and remote.vm.args)
##-start_epmd false -erl_epmd_port 6789#{remote? && " -dist_listen false"} ##-start_epmd false -erl_epmd_port 6789#{if(remote?, do: " -dist_listen false")}
""" """
@doc false @doc false
@@ -136,7 +136,7 @@ defmodule Mix.Tasks.Release.Init do
;; ;;
*) *)
echo "ERROR: Expected sname, name, or none in RELEASE_DISTRIBUTION, got: $RELEASE_DISTRIBUTION" >&2 echo "ERROR: Expected RELEASE_DISTRIBUTION to be sname, name, or none, got: $RELEASE_DISTRIBUTION" >&2
exit 1 exit 1
;; ;;
esac esac
@@ -280,7 +280,7 @@ defmodule Mix.Tasks.Release.Init do
rem set RELEASE_MODE=interactive rem set RELEASE_MODE=interactive
rem Set the release to work across nodes. rem Set the release to work across nodes.
rem RELEASE_DISTRIBUTION must be "sname" (local), "name" (distributed) or "none". rem RELEASE_DISTRIBUTION must be sname (local), name (distributed) or none.
rem set RELEASE_DISTRIBUTION=name rem set RELEASE_DISTRIBUTION=name
rem set RELEASE_NODE=<%= @release.name %> rem set RELEASE_NODE=<%= @release.name %>
""" """
@@ -314,13 +314,33 @@ defmodule Mix.Tasks.Release.Init do
if not defined RELEASE_BOOT_SCRIPT_CLEAN (set RELEASE_BOOT_SCRIPT_CLEAN=start_clean) if not defined RELEASE_BOOT_SCRIPT_CLEAN (set RELEASE_BOOT_SCRIPT_CLEAN=start_clean)
if not defined RELEASE_SYS_CONFIG (set RELEASE_SYS_CONFIG=!REL_VSN_DIR!\sys) if not defined RELEASE_SYS_CONFIG (set RELEASE_SYS_CONFIG=!REL_VSN_DIR!\sys)
if "!RELEASE_DISTRIBUTION!" == "none" (
rem
) else if "!RELEASE_DISTRIBUTION!" == "name" (
rem
) else if "!RELEASE_DISTRIBUTION!" == "sname" (
rem
) else (
echo ERROR: Expected RELEASE_DISTRIBUTION to be sname, name, or none, got: !RELEASE_DISTRIBUTION!
exit /B 1
)
if "!RELEASE_MODE!" == "embedded" (
rem
) else if "!RELEASE_MODE!" == "interactive" (
rem
) else (
echo ERROR: Expected RELEASE_MODE to be embedded or interactive, got: !RELEASE_MODE!
exit /B 1
)
if "%~1" == "start" (set "REL_EXEC=elixir" && set "REL_EXTRA=--no-halt" && set "REL_GOTO=start") if "%~1" == "start" (set "REL_EXEC=elixir" && set "REL_EXTRA=--no-halt" && set "REL_GOTO=start")
if "%~1" == "start_iex" (set "REL_EXEC=iex" && set "REL_EXTRA=--werl" && set "REL_GOTO=start") if "%~1" == "start_iex" (set "REL_EXEC=iex" && set "REL_EXTRA=--werl" && set "REL_GOTO=start")
if "%~1" == "install" (set "REL_GOTO=install") if "%~1" == "install" (set "REL_GOTO=install")
if "%~1" == "eval" ( if "%~1" == "eval" (
if "%~2" == "" ( if "%~2" == "" (
echo ERROR: EVAL expects an expression as argument echo ERROR: EVAL expects an expression as argument
goto end exit /B 1
) )
set "REL_GOTO=eval" set "REL_GOTO=eval"
) )
@@ -348,7 +368,7 @@ defmodule Mix.Tasks.Release.Init do
if "%~1" == "rpc" ( if "%~1" == "rpc" (
if "%~2" == "" ( if "%~2" == "" (
echo ERROR: RPC expects an expression as argument echo ERROR: RPC expects an expression as argument
goto end exit /B 1
) )
set "REL_RPC=%~2" set "REL_RPC=%~2"
goto rpc goto rpc
@@ -369,7 +389,10 @@ defmodule Mix.Tasks.Release.Init do
echo pid Prints the operating system PID of the running system via a remote command echo pid Prints the operating system PID of the running system via a remote command
echo version Prints the release name and version to be booted echo version Prints the release name and version to be booted
echo. echo.
if not "%~1" == "" (echo ERROR: Unknown command %~1) if not "%~1" == "" (
echo ERROR: Unknown command %~1
exit /B 1
)
goto end goto end
:start :start
@@ -427,7 +450,7 @@ defmodule Mix.Tasks.Release.Init do
if "!RELEASE_DISTRIBUTION!" == "none" ( if "!RELEASE_DISTRIBUTION!" == "none" (
set RELEASE_DISTRIBUTION_FLAG= set RELEASE_DISTRIBUTION_FLAG=
) else ( ) else (
set RELEASE_DISTRIBUTION_FLAG=--!RELEASE_DISTRIBUTION! "rem-!RANDOM!-!RELEASE_NODE!" set RELEASE_DISTRIBUTION_FLAG=--!RELEASE_DISTRIBUTION! "rpc-!RANDOM!-!RELEASE_NODE!"
) )
"!REL_VSN_DIR!\elixir.bat" ^ "!REL_VSN_DIR!\elixir.bat" ^
@@ -452,7 +475,7 @@ defmodule Mix.Tasks.Release.Init do
if "!RELEASE_DISTRIBUTION!" == "none" ( if "!RELEASE_DISTRIBUTION!" == "none" (
echo ERROR: RELEASE_DISTRIBUTION is required in install command echo ERROR: RELEASE_DISTRIBUTION is required in install command
goto end exit /B 1
) )
"!ERLSRV!" add "!RELEASE_NAME!_!RELEASE_NAME!" ^ "!ERLSRV!" add "!RELEASE_NAME!_!RELEASE_NAME!" ^
+11 -3
View File
@@ -288,9 +288,17 @@ defmodule Mix.Tasks.Test.Coverage do
output = Keyword.get(opts, :output, "cover") output = Keyword.get(opts, :output, "cover")
File.mkdir_p!(output) File.mkdir_p!(output)
for mod <- modules do modules
{:ok, _} = :cover.analyse_to_file(mod, ~c"#{output}/#{mod}.html", [:html]) |> Enum.map(fn mod ->
end pid = :cover.async_analyse_to_file(mod, ~c"#{output}/#{mod}.html", [:html])
Process.monitor(pid)
end)
|> Enum.each(fn ref ->
receive do
{:DOWN, ^ref, :process, _pid, _reason} ->
:ok
end
end)
Mix.shell().info("Generated HTML coverage results in #{inspect(output)} directory") Mix.shell().info("Generated HTML coverage results in #{inspect(output)} directory")
end end
+7
View File
@@ -147,6 +147,13 @@ defmodule Mix.GeneratorTest do
assert_received {:mix_shell, :yes?, ["foo already exists, overwrite?"]} assert_received {:mix_shell, :yes?, ["foo already exists, overwrite?"]}
end) end)
end end
test "with `format_elixir: true`" do
in_tmp("create_file", fn ->
create_file("foo", "%{ foo: :bar }", format_elixir: true)
assert File.read!("foo") == "%{foo: :bar}\n"
end)
end
end end
describe "copy_file/3" do describe "copy_file/3" do
+5 -10
View File
@@ -1597,11 +1597,7 @@ defmodule Mix.Tasks.Compile.ElixirTest do
def foo(), do: B.foo() def foo(), do: B.foo()
def bar(), do: B.bar() def bar(), do: B.bar()
def __after_verify__(__MODULE__) do def __after_verify__(__MODULE__) do
if Code.ensure_loaded?(B) and not function_exported?(B, :foo, 0) do IO.warn("AFTER_VERIFY", __ENV__)
:ok
else
IO.warn("AFTER_VERIFY", __ENV__)
end
end end
end end
""") """)
@@ -1630,10 +1626,9 @@ defmodule Mix.Tasks.Compile.ElixirTest do
end) end)
# Check B due to direct dependency on A # Check B due to direct dependency on A
# Check C due to transient dependency on A
assert output =~ "A.foo/0 is undefined or private" assert output =~ "A.foo/0 is undefined or private"
assert output =~ "B.bar/0 is undefined or private" refute output =~ "B.bar/0 is undefined or private"
assert output =~ "AFTER_VERIFY" refute output =~ "AFTER_VERIFY"
# Ensure only A was recompiled # Ensure only A was recompiled
assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]} assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]}
@@ -1650,7 +1645,7 @@ defmodule Mix.Tasks.Compile.ElixirTest do
assert output =~ "B.bar/0 is undefined or private" assert output =~ "B.bar/0 is undefined or private"
assert output =~ "AFTER_VERIFY" assert output =~ "AFTER_VERIFY"
# Now we change B and it must no longer emit an AFTER_VERIFY warning # Now we change B and it must emit an AFTER_VERIFY warning
File.write!("lib/b.ex", """ File.write!("lib/b.ex", """
defmodule B do defmodule B do
end end
@@ -1663,7 +1658,7 @@ defmodule Mix.Tasks.Compile.ElixirTest do
assert output =~ "B.foo/0 is undefined or private" assert output =~ "B.foo/0 is undefined or private"
assert output =~ "B.bar/0 is undefined or private" assert output =~ "B.bar/0 is undefined or private"
refute output =~ "AFTER_VERIFY" assert output =~ "AFTER_VERIFY"
end) end)
end end
-37
View File
@@ -530,43 +530,6 @@ defmodule Mix.Tasks.DepsGitTest do
end) end)
end end
test "with ref" do
[last, _ | _] = get_git_repo_revs("git_repo")
Process.put(:git_repo_opts, depth: 1, ref: last)
in_fixture("no_mixfile", fn ->
Mix.Project.push(GitApp)
Mix.Tasks.Deps.Get.run([])
message = "* Getting git_repo (#{fixture_path("git_repo")} - #{last})"
assert_received {:mix_shell, :info, [^message]}
assert_shallow("deps/git_repo", 1)
end)
end
test "changing refspec updates retaining depth" do
[last, first | _] = get_git_repo_revs("git_repo")
Process.put(:git_repo_opts, ref: first, depth: 1)
in_fixture("no_mixfile", fn ->
Mix.Project.push(GitApp)
Mix.Tasks.Deps.Get.run([])
message = "* Getting git_repo (#{fixture_path("git_repo")} - #{first})"
assert_received {:mix_shell, :info, [^message]}
assert_shallow("deps/git_repo", 1)
assert File.read!("mix.lock") =~ first
# Change refspec
update_dep(ref: last, depth: 1)
Mix.Tasks.Deps.Get.run([])
assert_shallow("deps/git_repo", 1)
assert File.read!("mix.lock") =~ last
end)
end
test "removing depth retains shallow repository" do test "removing depth retains shallow repository" do
# For compatibility and simplicity, we follow Git's behavior and do not # For compatibility and simplicity, we follow Git's behavior and do not
# attempt to unshallow an existing repository. This should not be a # attempt to unshallow an existing repository. This should not be a
@@ -0,0 +1,205 @@
Code.require_file("../../test_helper.exs", __DIR__)
defmodule Mix.Tasks.Profile.TprofTest do
use MixTest.Case
import ExUnit.CaptureIO
alias Mix.Tasks.Profile.Tprof
# TODO remove once we require Erlang/OTP 27+
@moduletag skip: System.otp_release() < "27"
@expr "Enum.each(1..5, &String.Chars.Integer.to_string/1)"
test "profiles evaluated expression", context do
in_tmp(context.test, fn ->
assert capture_io(fn ->
Tprof.run(["-e", @expr])
end) =~ ~r/String\.Chars\.Integer\.to_string\/1\s+\d/
end)
end
test "profiles evaluated expression in multiple processes", context do
in_tmp(context.test, fn ->
assert capture_io(fn ->
Tprof.run(["-e", "spawn(fn -> #{@expr} end)"])
end) =~ ~r/String\.Chars\.Integer\.to_string\/1\s+\d/
end)
end
test "profiles the script", context do
in_tmp(context.test, fn ->
profile_script_name = "profile_script.ex"
File.write!(profile_script_name, @expr)
assert capture_io(fn ->
Tprof.run([profile_script_name])
end) =~ ~r/String\.Chars\.Integer\.to_string\/1\s+\d/
end)
end
test "filters based on calls count", context do
in_tmp(context.test, fn ->
result =
capture_io(fn ->
Tprof.run(["--calls", "5", "-e", @expr])
end)
assert result =~ "\nString.Chars.Integer.to_string/1"
refute result =~ "\nEnum.each/2"
end)
end
test "filters based on time", context do
in_tmp(context.test, fn ->
result =
capture_io(fn ->
Tprof.run(["--time", "50", "-e", @expr])
end)
refute result =~ "\nEnum.each/2"
end)
end
test "filters based on memory", context do
in_tmp(context.test, fn ->
result =
capture_io(fn ->
Tprof.run(["--type", "memory", "--memory", "10", "-e", @expr])
end)
assert result =~ "\n:erlang.integer_to_binary/1"
refute result =~ "\nEnum.each/2"
end)
end
test "sorts based on calls count", context do
in_tmp(context.test, fn ->
assert capture_io(fn ->
Tprof.run(["--sort", "calls", "-e", @expr])
end) =~ ~r/\nEnum\.each\/2.*\nString\.Chars\.Integer\.to_string\/1/s
end)
end
test "sorts based on memory usage", context do
in_tmp(context.test, fn ->
assert capture_io(fn ->
Tprof.run(["--type", "memory", "--sort", "calls", "-e", @expr])
end) =~ ~r/\nEnum\.each\/2.*\n:erlang\.integer_to_binary\/1/s
end)
end
test "sorts based on memory per call", context do
in_tmp(context.test, fn ->
assert capture_io(fn ->
Tprof.run(["--type", "memory", "--sort", "per_call", "-e", @expr])
end) =~ ~r/\n:erlang\.integer_to_binary\/1.*\nEnum\.each\/2/s
end)
end
test "aggregates totals over all processes", context do
in_tmp(context.test, fn ->
assert capture_io(fn ->
Tprof.run(["--report", "total", "-e", @expr])
end) =~ "Profile results over all processes"
end)
end
test "Module matching", context do
in_tmp(context.test, fn ->
refute capture_io(fn ->
Tprof.run(["--matching", "Enum", "-e", @expr])
end) =~ ~r/String\.Chars\.Integer\.to_string\/1/
end)
end
test "Module.function matching", context do
in_tmp(context.test, fn ->
refute capture_io(fn ->
Tprof.run(["--matching", "Enum.each", "-e", @expr])
end) =~ ~r/anonymous fn\/3 in Enum\.each\/2/
end)
end
test "Module.function/arity matching", context do
in_tmp(context.test, fn ->
assert capture_io(fn ->
Tprof.run(["--matching", "Enum.each/8", "-e", @expr])
end) =~ ~r/Profile done over 0 matching functions/
end)
end
test "errors on missing files", context do
in_tmp(context.test, fn ->
message = "No files matched pattern \"non-existent\" given to --require"
assert_raise Mix.Error, message, fn ->
capture_io(fn -> Tprof.run(["-r", "non-existent"]) end)
end
message = "No files matched pattern \"non-existent\" given to --require"
assert_raise Mix.Error, message, fn ->
capture_io(fn -> Tprof.run(["-pr", "non-existent"]) end)
end
assert_raise Mix.Error, "No such file: non-existent", fn ->
capture_io(fn -> Tprof.run(["non-existent"]) end)
end
File.mkdir_p!("lib")
assert_raise Mix.Error, "No such file: lib", fn ->
capture_io(fn -> Tprof.run(["lib"]) end)
end
end)
end
test "warmup", context do
in_tmp(context.test, fn ->
assert capture_io(fn ->
Tprof.run(["-e", @expr])
end) =~ "Warmup..."
refute capture_io(fn ->
Tprof.run(["-e", @expr, "--no-warmup"])
end) =~ "Warmup..."
end)
end
test "errors on incompatible options", context do
in_tmp(context.test, fn ->
message = "Incompatible sort option :memory with type :time"
assert_raise Mix.Error, message, fn ->
capture_io(fn -> Tprof.run(["-e", @expr, "--sort", "memory"]) end)
end
message = "Incompatible sort option :time with type :calls"
assert_raise Mix.Error, message, fn ->
capture_io(fn -> Tprof.run(["-e", @expr, "--type", "calls", "--sort", "time"]) end)
end
message = "Incompatible use of memory option with type :time"
assert_raise Mix.Error, message, fn ->
capture_io(fn -> Tprof.run(["-e", @expr, "--time", "1", "--memory", "2"]) end)
end
message = "Incompatible use of time option with type :calls"
assert_raise Mix.Error, message, fn ->
capture_io(fn -> Tprof.run(["-e", @expr, "--type", "calls", "--time", "1"]) end)
end
end)
end
describe ".profile/2" do
test "returns the return value of the function call" do
capture_io(fn ->
assert 42 == Tprof.profile(fn -> 42 end)
end)
end
end
end