Compare commits

...
Author SHA1 Message Date
José Valim 6efe0fc7cb Release v1.20.3 2026-08-04 16:31:51 +02:00
José Valim b3f2e0def7 Update CHANGELOG 2026-08-04 15:55:18 +02:00
Alexander Gubarev fa6205cc20 Fix elixirc --profile time switch argv parsing (#15647) 2026-08-04 15:49:27 +02:00
Łukasz Samson ea1519bcc0 Do not return {:ok, _} on maps with empty lines in map_put_static_value (#15597) 2026-08-04 15:19:29 +02:00
Łukasz Samson bfd4dd35fa Fix tuple_insert_static crash on non-normalized empty type (#15542)
Closes #15541.
2026-08-04 15:18:22 +02:00
Łukasz Samson 0564a597c6 Improve send return type (#15644)
Teach the type checker that Kernel.send/2 returns the message operand.
2026-08-04 15:16:47 +02:00
José Valim eded6248eb Optimize tuple and map emptiness checks (#15619)
Short-circuit disjoint and subtype tuple elements
before exploring the general difference branches.

This is a backport 59e257d3.

Assisted-by: Codex:GPT-5.6
2026-07-28 15:36:24 +02:00
José Valim 6bb46c30fc Update CHANGELOG 2026-07-19 13:21:16 +02:00
José Valim aa3dbdb289 Ensure %_{} is precise in guards, closes #15635 2026-07-19 10:06:36 +02:00
José Valim a55c1f7b6a Improve coverage of defguards 2026-07-17 16:20:35 +02:00
Łukasz Samson 76df4c6f58 Split tuple_insert_static into two paths (#15518)
- bdd_map on pure positive disjunction
- expand to the exact negation-free positive DNF, insert and union on negations and/or implicit :bdd_top

Fixes #15517
2026-07-16 17:50:51 +02:00
Alexander Gubarev 96f28d4362 Fix IEx.__break__!/3 fallback clause arity (#15595) 2026-07-12 13:52:34 +02:00
José Valim f894bc480e Provide proper signature for ++
And deal with compatibility checks in to_existing_atom/2.

Closes #15588.
2026-07-11 11:16:46 +02:00
José Valim 51d6cfdb93 Batch cachers in parallel checker (#15545) 2026-07-06 17:19:32 +02:00
José Valim 83ae474c61 Clarify protocols come later, closes #15529 2026-06-26 16:56:00 +02:00
Łukasz Samson 2fbf952cad Add missing Of.reset_vars call in receive/after badtimeout error path (#15527)
Variable refinement should not leak outside after block
2026-06-25 12:54:51 +02:00
Łukasz Samson 67772adcb2 Refer to the right function in remote_apply error on :maps.values/1 (#15526) 2026-06-25 12:54:51 +02:00
Łukasz Samson be1234799d Enable doctests on Descr, fix stale examples (#15528) 2026-06-25 12:54:51 +02:00
Łukasz Samson 4918e2d955 Thread full descr through fun_denormalize reduce (#15524)
Previously inner representation was threaded and returned, which broke expectations of caller `non_term_type_to_quoted`

Closes #15523.
2026-06-24 13:24:08 +02:00
José Valim cbf3836e8b Refine types for andalso/orelse in expressions, see #15450 2026-06-24 11:41:18 +02:00
Łukasz Samson 989165cabb Improve error returned from fun_apply (#15522)
- return badfun on empty type
- exclude call arity on badarity tuple

Fixes #15521

(cherry picked from commit 9a2029245d)
2026-06-24 10:34:45 +02:00
Guillaume Duboc 05d03543f6 Add put_dynamic for proper dynamic insertion (#15520) 2026-06-23 17:25:48 +02:00
José Valim 545dddf138 Release v1.20.2 2026-06-23 14:02:47 +02:00
Łukasz Samson bc30e7d696 Include bitstring in domain keys (#15516)
Fixes #15515
2026-06-23 10:44:27 +02:00
José Valim 73130012bb Better organize tests 2026-06-21 21:36:40 +02:00
Łukasz Samson f55abe5ff9 Correctly render list with negative leaves (#15514)
BDD leaves represent non empty lists

Fixes #15513.
2026-06-21 21:34:37 +02:00
Łukasz Samson 394ab4bead Return error from compatible_intersection with none (#15512)
Match fast path behaviour with `compatible?/2`
Fixes #15511
2026-06-21 21:33:45 +02:00
Łukasz Samson f491a7eee4 Report empty_list: true on dynamic() in list_of (#15506)
Fixes #15505
2026-06-19 21:43:35 +02:00
José Valim a64284921e Fix optimized map difference with open key 2026-06-19 21:42:31 +02:00
Łukasz Samson 6bef18d523 Preserve optional static part on difference (#15503)
Fixes #15502
2026-06-18 09:22:29 +02:00
José Valim 8516737ea0 Avoid unecessary umbrella recompilation when a path dependency's manifest is newer but unchanged, closes #15454 2026-06-17 15:56:22 +02:00
José Valim c52eb2a323 opt_union -> union 2026-06-17 08:56:07 +02:00
Łukasz Samson 527fb26bcd Fix definition of term_or_dynamic_optional type (#15498)
Fixes #15497
2026-06-17 08:47:50 +02:00
José Valim 5c5909c2a1 Preserve empty list in dynamic list type constructor
Closes #15488.
2026-06-15 20:30:22 +02:00
José Valim 3c7ee9ff20 Handle empty function type in fun_apply
Closes #15486.
2026-06-15 18:48:29 +02:00
José Valim 51dce31a54 Preserve file meta from each clause in type system warnings 2026-06-15 16:03:50 +02:00
José Valim 0dd4cb4e0b Update CHANGELOG 2026-06-15 13:58:58 +02:00
Łukasz Samson 4d1de1d971 Fix missed optimization of Kernel.put_elem/3 (#15480)
?reorder added in 87582af54 used wrong arity and wrong erlang call
2026-06-15 12:49:18 +02:00
Jean Klingler b098c0406c Fix bin comprehensions when option is used (#15473)
* Fix bin comprehensions - static size

* Fix bin comprehensions - pinned var size

* Fix bin comprehensions - operations

* Add tests for nested generators
2026-06-15 17:34:06 +09:00
José Valim 1ba39f3501 Raise better error messages when expected structs disappear, closes #15472 2026-06-14 13:46:51 +02:00
Jean Klingler 0f9072a1d4 Cleaner error message for unquote when quote is used in a pattern (#15469) 2026-06-13 21:16:40 +09:00
José Valim 6dcc4cd2dc Simplify precision tracking in bitstrings 2026-06-13 12:11:32 +02:00
José Valim 40b6f012fe Fix env outdated deps compilation
We should only avoid cleaning for path dependencies.
Fetch dependencies should go through the previous process.

Closes #15467.
2026-06-13 12:11:31 +02:00
José Valim 68267627dc Track reused variables in bitstring patterns 2026-06-13 11:17:37 +02:00
José Valim 57766dd29b Use cyclic instead of recursive, add more examples, closes #15462 2026-06-12 21:01:29 +02:00
José Valim 1938d8a957 Ignore additional callbacks in protocol implementation, closes #15456 2026-06-12 21:01:28 +02:00
José Valim 3bebe1599a Log times to type check modules when profiling 2026-06-12 13:57:20 +02:00
Łukasz Samson 2beac77406 Add missing required key to __info__(:struct) type (#15453) 2026-06-10 16:41:24 +02:00
José Valim 85a23a1244 Document false positives, closes #15451 2026-06-10 09:27:42 +02:00
José Valim 97c1e1ec0c Release v1.20.1 2026-06-09 15:36:40 +02:00
José Valim 64e6707233 Limit version numbers to 14 bytes
This avoids parsing too large integers.

CVE-2026-49762
GHSA-w2h8-8x3g-278p
2026-06-09 15:30:13 +02:00
José Valim 657a7b394a Add docs about URI encoding/to_string 2026-06-09 15:30:08 +02:00
José Valim ac76c108ce Validate paths and files when extracting archives 2026-06-09 15:30:03 +02:00
José Valim 2e629aa414 Clarify security considerations and disclaimers to archive/escript install 2026-06-09 15:30:00 +02:00
José Valim 2f2fc9a7f9 Cap width in strftime to 1024 characters 2026-06-09 15:29:55 +02:00
Łukasz Samson bb12270c6b Add capture_arg to Code.Fragment.cursor_context spec (#15441) 2026-06-09 15:29:49 +02:00
Aaron Tinio 6d2e8241c9 Honor --no-compile in mix format (#15443) 2026-06-07 21:34:50 +02:00
José Valim a371208104 Tag lists member as returning boolean 2026-06-07 11:07:18 +02:00
José Valim 3a366c2f0f Deal with empty canonical on doc generation 2026-06-05 15:08:07 +02:00
José Valim d33ea6e963 require_file releases the file if compilation fails 2026-06-04 19:42:49 +02:00
José Valim 853b7cd719 Release v1.20.0 2026-06-03 19:38:11 +02:00
José Valim 0bc04768bf Squash CHANGELOG 2026-06-02 18:45:40 +02:00
Jean Klingler c354a77946 Never suggest back invalid characters in tokenizer hint (#15421) 2026-05-27 10:23:43 +02:00
Jean Klingler 766ece7e3f Inline in empty list in erlang pass (#15413) 2026-05-24 09:06:11 +09:00
José Valim 8bc9a2ed15 Avoid false positives on cascading warnings
Only emit warnings for missing exports in defined
modules to avoid false positives with modules from
the current project which may not have been defined
yet.
2026-05-22 10:41:54 +02:00
José Valim 7150d493ad Improve tracking of g df within the type system 2026-05-22 10:14:46 +02:00
José Valim 7a9ea30d9c Revert expansion of or/and in guards
This partially reverts aad63173 as it caused
massive AST to generate when expanding guards
with `expr in [...]` checks in them, as each
`or` became a full-blown case expression.

Alternative approaches will be explored in
future releases.
2026-05-22 09:51:29 +02:00
José Valim 3a038d1763 Wrap quote in a function that will implement recursive types
Closes #15410.
2026-05-22 09:51:29 +02:00
José Valim e5e66831ac Avoid redundant emptiness checks on list/tuple emptiness (#15412) 2026-05-22 09:51:29 +02:00
José Valim 285c15f1ab Release v1.20.0-rc.6 2026-05-21 13:45:11 +02:00
José Valim 909903962d Branch out v1.20 2026-05-21 13:34:14 +02:00
69 changed files with 1799 additions and 638 deletions
+130 -151
View File
@@ -192,34 +192,88 @@ It also introduces a new compiler option called `:module_definition`, which if t
You can enable it by setting `elixirc_options: [module_definition: :interpreted]` in your `mix.exs`.
## v1.20.0-rc.6
This release requires Erlang/OTP 27+ and is compatible with Erlang/OTP 29.
## v1.20.3 (2026-08-04)
### 1. Enhancements
#### Elixir
* [Kernel] Perform type inference across applications
* [Kernel.ParallelCompiler] Batch type checker cache operations to improve compilation times
### 2. Bug fixes
#### Elixir
* [Kernel] Fix type checker bug when validating a `case` inside a `cond` condition (regression)
* [Kernel] Preserve evaluation order when rewriting function calls from Elixir modules into Erlang ones
* [Kernel] Improve type precision of the `++/2` operator
* [Kernel] Improve type precision of the `send/2` return type
* [Kernel] Improve type refinement for `:erlang.andalso/2` and `:erlang.orelse/2`
* [Kernel] Fix type checking of `Map.put/3` on empty map types
* [Kernel] Fix type checking of tuple insertion on equivalent tuple types
* [Kernel] Fix type checking of tuple insertion on gradual types with empty static parts
* [Kernel] Fix variable refinements leaking from invalid `receive/after` timeout expressions
* [Kernel] Fix rendering inferred types containing unions of static and dynamic functions
* [Kernel] Ensure typing errors in `:maps.values/1` reference the correct function
* [Kernel] Mark `%_{}` patterns as precise so subsequent redundant struct clauses are detected
* [elixirc] Fix parsing of the `--profile time` option
#### IEx
* [IEx.Helpers] Fix `break!/1` with unknown expressions
## v1.20.2 (2026-06-23)
### 1. Enhancements
#### Elixir
* [Kernel.ParallelCompiler] Include per-module type checking times when compiler profiling is enabled with `profile: :time`
### 2. Bug fixes
#### Elixir
* [Kernel] Fix binary comprehensions with sizes when options such as `:uniq` or `:into` are used
* [Kernel] Improve compiler error messages when `quote` with `unquote` is used inside a pattern or guard
* [Kernel] Restore the compiler optimization of `Kernel.put_elem/3` to emit `:erlang.setelement/3`
* [Module] Fix type checking when applying an empty function type
* [Module] Fix type checking of bitstring patterns that reuse variables
* [Module] Fix type information for `__info__(:struct)` to include the `:required` key
* [Module] Fix type operations on map and optional keys during difference/intersection, including open keys and empty intersections
* [Module] Fix typing of list types involving dynamic or empty lists
* [Module] Include bitstrings as a possible domain key in the type system
* [Module] Preserve file metadata from each clause in type system warnings
* [Module] Fix type warnings for protocol implementations whose protocol module defines additional callbacks
* [Module] Raise clearer type checking errors when an expected struct is removed or redefined during compilation
#### Mix
* [mix test] Respect --raise when mix test --warnings-as-errors passes with warnings
* [mix compile] Avoid unnecessary umbrella recompilation when a path dependency's manifest is newer but unchanged
* [mix deps.compile] Recompile fetched dependencies when their compile-time environment changes
### 3. Hard deprecations
## v1.20.1 (2026-06-09)
### 1. Security
#### Elixir
* [Version] Limit integer components in Version to 14 decimal bytes, to avoid parsing too large integers from untrusted user input. We strongly advise developers parsing versions from user input to limit the data size given to the `Version` module (CVE-2026-49762, GHSA-w2h8-8x3g-278p)
### 2. Bug fixes
#### Elixir
* [Calendar] Cap width in `Calendar.strftime/2` to 1024 characters
* [Code] Ensure `Code.require_file` releases the file if compilation fails
* [Kernel] Fix documentation generation to use the correct version in search
#### Mix
* [mix compile.elixir] `xref: [exclude: ...]` in your `mix.exs` is deprecated in favor of `elixirc_options: [no_warn_undefined: ...]`
* [mix archive.install] Validate paths and files when extracting archives
* [mix format] Honor `--no-compile` option when loading plugins
## v1.20.0-rc.5 (2026-05-13)
## v1.20.0 (2026-06-03)
This release requires Erlang/OTP 27+ and is compatible with Erlang/OTP 29.
### 1. Enhancements
@@ -230,45 +284,98 @@ This release requires Erlang/OTP 27+ and is compatible with Erlang/OTP 29.
#### Elixir
* [Base] Optimize Base validation functions by using SWAR techniques
* [Calendar] Optimize `date_from_iso_days` by using the Neri-Schneider algorithm
* [Code] Add `:dbg_callback` option to eval functions
* [Code] Add `module_definition: :interpreted` option to `Code` which allows module definitions to be evaluated instead of compiled. In some applications/architectures, this can lead to drastic improvements to compilation times. Note this does not affect the generated `.beam` file, which will have the same performance/behaviour as before
* [Code] Make module purging opt-in and move temporary module deletion to the background to speed up compilation times
* [Code.Fragment] Allow preserving sigil metadata in `container_cursor_to_quoted`
* [Enum] Add `Enum.min_max` sorter
* [File] Add support for `[:raw]` opts in `File.read/2`
* [File] Skip device, named pipes, etc in `File.cp_r/3` instead of erroring with reason `:eio`
* [Float] Optimize `Float.round/2` by avoiding big integers
* [Inspect] Increase inspect limit to help print deeply nested data structures
* [Inspect] Support printing Erlang records (using Erlang notation)
* [Integer] Add `Integer.ceil_div/2`
* [Integer] Add `Integer.popcount/1`
* [IO] Add `IO.iodata_empty?/1`
* [Kernel] Add type inference across clauses. For example, if one clause says `x when is_integer(x)`, then the next clause may no longer be an integer
* [Kernel] Add occurrence typing on `case`, `cond`, and `with`
* [Registry] Switch `{:duplicate, :key}` key_ets to ordered_set with composite keys
* [Kernel] Detect and warn on redundant clauses
* [Kernel] Perform type inference across applications
* [Kernel] Print intermediate results of `dbg` for pipes
* [Kernel] Show undefined function errors even when missing variables (this helps debug errors caused when the developer forgets to require a macro)
* [Kernel] Warn on unused requires
* [List] Add `List.first!/1` and `List.last!/1`
* [Module] Purge and delete modules if `after_compile/2` callback fails
* [PartitionSupervisor] Support via tuples in `count_children/1` and `stop/3`
* [Process] Add `Process.get_label/1`
* [Registry] Switch `keys: {:duplicate, :key}` to `ordered_set` with composite keys
* [Regex] Add `Regex.import/1` to import regexes defined with `/E`
* [String] SWAR-optimize ASCII fast paths in `String.length/1` and `String.slice/3`
* Add Software Bill of Materials guide to the Documentation
#### ExUnit
* [ExUnit] Show remaining runs when using `--repeat-until-failure`
* [ExUnit.CaptureLog] Add `:formatter` option for custom log formatting
#### IEx
* [IEx] Optimize autocompleting modules
* [IEx.Helpers] Add `source/1`
#### Mix
* [mix app.tree] Support `--output` option
* [mix compile] Add `module_definition: :interpreted` option to `Code` which allows module definitions to be evaluated instead of compiled. In some applications/architectures, this can lead to drastic improvements to compilation times. Note this does not affect the generated `.beam` file, which will have the same performance/behaviour as before
* [mix compile] Enforce `:elixirc_paths` to be a list of strings to avoid paths from being discarded (the only documented type was lists of strings)
* [mix deps] Parallelize dep lock status checks during `deps.loadpaths`, improving boot times in projects with many git dependencies
* [mix deps] Support filtering `mix deps` output
* [mix deps.tree] Support `--output` option
* [mix help] Support printing docs for types and callbacks
* [mix format] Support `--no-compile` option
* [mix help] Support printing docs for types and callbacks
* [mix source] Add `mix source MODULE` to print or open a given module/function location
* [mix test] Add `mix test --dry-run`
### 2. Potential breaking changes
#### Elixir
* [Kernel] Disallow raw CR line ending in strings, comments and after `?` for security reasons
* [Kernel] Disallow raw CR line ending in strings, comments, and after `?` for security reasons
* [Kernel] `require SomeModule` no longer expands to the given module at compile-time, but it still returns the module at runtime. Note Elixir does not guarantee macros will expand to certain constructs, only what its execution result, but since this can break code relying on the previous behaviour, such as `require(SomeMod).some_macro()`, we are adding this note to the CHANGELOG
### 3. Bug fixes
#### Elixir
* [Enum] Fix `Enum.slice/2` for ranges with step > 1 sliced by step > 1
* [File] Allowing preserving directory permissions in `File.cp_r/3`
* [File] Fix `File.cp_r/3` infinite loop with symlink cycles
* [File] Fix `File.cp_r/3` infinite loop when copying into subdirectory of source
* [File] Fix `File.Stream`'s `Enumerable.count` for files without trailing newline
* [File] Warn when defining `@type record()` for Erlang/OTP 29
* [Float] Fix `Float.parse/1` inconsistent error handling for non-scientific notation overflow
* [Integer] Fix `Integer.extended_gcd/2` returning negative GCD for zero base cases
* [Integer] Raise when negative out-of-range digits are given to `Integer.undigits/2`
* [Kernel] Fix a compiler crash when importing a module with `only: :sigils` option when the imported module exports non-sigil symbols with `sigil_` prefix
* [Kernel] Protocols should not add compile-time dependencies on `Any` implementation
* [Kernel] Preserve evaluation order when rewriting function calls from Elixir modules into Erlang ones
* [Kernel] Reject negative Duration in `to_timeout/1`
* [Keyword] Raise `ArgumentError` in `Keyword.from_keys/2` for non-atom keys
* [Macro] Fix generation of heredocs in `Macro.to_string/1` with escaped trailing newline
* [Path] Consistently return path as binary in `Path.relative_to_cwd/2`
* [Stream] Raise in `Stream.cycle/1` when enumerable reduce call yields no elements
* [String] Support empty pattern list in `String.count/2`
* [URI] Fix `URI.merge` leaking `:+` marker when base path is empty string
#### ExUnit
* [ExUnit.Diff] Avoid false positives when diffing bitstrings
#### IEx
* [IEx] Ensure pry works across remote nodes
* [IEx] Ensure warnings emitted during IEx parsing are properly displayed/printed
#### Logger
@@ -277,148 +384,16 @@ This release requires Erlang/OTP 27+ and is compatible with Erlang/OTP 29.
#### Mix
* [Mix] Use `non_executable_binary_to_term` on loopback pubsub
* [mix compile.elixir] Fix scenario where Elixir would tag mtimes in the future
## v1.20.0-rc.4 (2026-03-31)
This release requires Erlang/OTP 27+ and is compatible with Erlang/OTP 29.
### 1. Enhancements
#### Elixir
* [Code] Add `:dbg_callback` option to eval functions
* [Code.Fragment] Allow preserving sigil metadata in `container_cursor_to_quoted`
* [File] Add support for `[:raw]` opts in `File.read/2`
* [Kernel] Show undefined function errors even when missing variables (this helps debug errors caused when the developer forgets to require a macro)
* [Module] Purge and delete modules if `after_compile/2` callback fails
* [PartitionSupervisor] Support via tuples in `count_children/1` and `stop/3`
* [Process] Add `Process.get_label/1`
#### Mix
* [mix deps] Allow overriding specific dependencies in `:override`
### 2. Bug fixes
#### Elixir
* [Integer] Fix `Integer.extended_gcd/2` returning negative GCD for zero base cases
* [Integer] Raise when negative out-of-range digits are given to `Integer.undigits/2`
* [Kernel] Protocols should not add compile-time dependencies on `Any` implementation
* [Kernel] Ensure structs trigger recompilation for type checking purposes (regression)
* [Kernel] Ensure type information propagate across `hd/tl` in guards (regression)
* [Keyword] Raise `ArgumentError` in `Keyword.from_keys/2` for non-atom keys
* [URI] Fix `URI.merge` leaking `:+` marker when base path is empty string
#### ExUnit
* [ExUnit.Diff] Avoid false positives when diffing bitstrings
#### Mix
* [mix deps] Use config files to pass project state to avoid argv limits on Windows when using `MIX_OS_DEPS_COMPILE_PARTITION_COUNT`
* [mix compile] Fix compile env change triggering full recompilation of path dependencies
* [mix compile] Add a build lock around protocol consolidation in umbrellas
* [mix compile] Ensure compilation of sibling deps do not mark path deps as changed
* [mix test] Fix `--warnings-as-errors` not catching misnamed test file warnings
## v1.20.0-rc.3 (2026-03-09)
### 1. Enhancements
#### IEx
* [IEx] Optimize autocompleting modules
### 2. Bug fixes
#### Elixir
* [Enum] Fix `Enum.slice/2` for ranges with step > 1 sliced by step > 1
* [File] Allowing preserving directory permissions in `File.cp_r/3`
* [File] Fix `File.cp_r/3` infinite loop with symlink cycles
* [File] Fix `File.cp_r/3` infinite loop when copying into subdirectory of source
* [File] Warn when defining `@type record()`, fixes CI on Erlang/OTP 29
* [File] Fix `File.Stream` `Enumerable.count` for files without trailing newline
* [Float] Fix `Float.parse/1` inconsistent error handling for non-scientific notation overflow
* [Kernel] Process fields even when structs are unknown (regression)
* [Kernel] Improve performance on several corner cases in the type system (regression)
* [Kernel] Fix regression when using `Kernel.in/2` in defguard (regression)
## v1.20.0-rc.2 (2026-03-04)
### 1. Enhancements
#### Elixir
* [Code] Add `module_definition: :interpreted` option to `Code` which allows module definitions to be evaluated instead of compiled. In some applications/architectures, this can lead to drastic improvements to compilation times. Note this does not affect the generated `.beam` file, which will have the same performance/behaviour as before
* [Code] Make module purging opt-in and move temporary module deletion to the background to speed up compilation times
* [Integer] Add `Integer.popcount/1`
* [Kernel] Add type inference across clauses. For example, if one clause says `x when is_integer(x)`, then the next clause may no longer be an integer
* [Kernel] Detect and warn on redundant clauses
* [List] Add `List.first!/1` and `List.last!/1`
* Add Software Bill of Materials guide to the Documentation
#### Mix
* [mix compile] Add `module_definition: :interpreted` option to `Code` which allows module definitions to be evaluated instead of compiled. In some applications/architectures, this can lead to drastic improvements to compilation times. Note this does not affect the generated `.beam` file, which will have the same performance/behaviour as before
* [mix deps] Parallelize dep lock status checks during `deps.loadpaths`, improving boot times in projects with many git dependencies
### 2. Bug fixes
#### IEx
* [IEx] Ensure warnings emitted during IEx parsing are properly displayed/printed
* [IEx] Ensure pry works across remote nodes
#### Mix
* [mix compile] Fix compile env change triggering full recompilation of path dependencies
* [mix compile.elixir] Fix scenario where Elixir would tag mtimes in the future
* [mix compile.erlang] Topsort Erlang modules before compilation for proper dependency resolution
* [mix deps] Use config files to pass project state to avoid argv limits on Windows when using `MIX_OS_DEPS_COMPILE_PARTITION_COUNT`
* [mix test] Fix `--warnings-as-errors` not catching misnamed test file warnings
* [mix test] Respect `--raise` when `mix test --warnings-as-errors` passes with warnings
## v1.20.0-rc.1 (2026-01-13)
### 1. Bug fixes
#### Elixir
* [Kernel] Do not crash on map types with struct keys when performing type operations (regression)
* [Kernel] Mark the outcome of bitstring types as dynamic (regression)
* [Kernel] `<<expr::bitstring>>` will have type `binary` instead of `bitstring` if `expr` is a binary (regression)
* [Kernel] Do not crash on conditional variables when calling a function on a module which is represented by a variable (regression)
## v1.20.0-rc.0 (2026-01-09)
### 1. Enhancements
#### Elixir
* [Calendar] Optimize `date_from_iso_days` by using the Neri-Schneider algorithm
* [Enum] Add `Enum.min_max` sorter
* [Integer] Add `Integer.ceil_div/2`
* [IO] Add `IO.iodata_empty?/1`
* [File] Skip device, named pipes, etc in `File.cp_r/3` instead of erroring with reason `:eio`
* [Kernel] Print intermediate results of `dbg` for pipes
* [Kernel] Warn on unused requires
* [Regex] Add `Regex.import/1` to import regexes defined with `/E`
#### ExUnit
* [ExUnit.CaptureLog] Add `:formatter` option for custom log formatting
#### Mix
* [mix deps] Support filtering `mix deps` output
* [mix compile] Enforce `:elixirc_paths` to be a list of strings to avoid paths from being discarded (the only documented type was lists of strings)
* [mix test] Add `mix test --dry-run`
### 2. Potential breaking changes
#### Elixir
* `require SomeModule` no longer expands to the given module at compile-time, but it still returns the module at runtime. Note Elixir does not guarantee macros will expand to certain constructs, but since this can break code relying on the previous behaviour, such as `require(SomeMod).some_macro()`, we are adding this note to the CHANGELOG
### 3. Hard deprecations
### 4. Hard deprecations
#### Elixir
@@ -431,6 +406,10 @@ This release requires Erlang/OTP 27+ and is compatible with Erlang/OTP 29.
* [Logger] `Logger.*_backend` functions are deprecated in favor of handlers. If you really want to keep on using backends, see the `:logger_backends` package
* [Logger] `Logger.enable/1` and `Logger.disable/1` have been deprecated in favor of `Logger.put_process_level/2` and `Logger.delete_process_level/1`
#### Mix
* [mix compile.elixir] `xref: [exclude: ...]` in your `mix.exs` is deprecated in favor of `elixirc_options: [no_warn_undefined: ...]`
## v1.19
The CHANGELOG for v1.19 releases can be found [in the v1.19 branch](https://github.com/elixir-lang/elixir/blob/v1.19/CHANGELOG.md).
+1 -1
View File
@@ -6,7 +6,7 @@ PREFIX ?= /usr/local
TEST_FILES ?= "*_test.exs"
SHARE_PREFIX ?= $(PREFIX)/share
MAN_PREFIX ?= $(SHARE_PREFIX)/man
CANONICAL := main/
# CANONICAL := main/
ELIXIRC := bin/elixirc --ignore-module-conflict $(ELIXIRC_OPTS)
ELIXIRC_MIN_SIG := $(ELIXIRC) -e 'Code.put_compiler_option :infer_signatures, []'
ERLC := erlc -I lib/elixir/include
+2 -3
View File
@@ -12,12 +12,11 @@ Elixir applies bug fixes only to the latest minor branch. Security patches are a
Elixir version | Support
:------------- | :-----------------------------
1.20 | Development
1.19 | Bug fixes and security patches
1.20 | Bug fixes and security patches
1.19 | Security patches only
1.18 | Security patches only
1.17 | Security patches only
1.16 | Security patches only
1.15 | Security patches only
## Announcements
+1 -1
View File
@@ -1 +1 @@
1.20.0-rc.5
1.20.3
+1 -1
View File
@@ -6,7 +6,7 @@
set -e
ELIXIR_VERSION=1.20.0-rc.5
ELIXIR_VERSION=1.20.3
if [ $# -eq 0 ] || { [ $# -eq 1 ] && { [ "$1" = "--help" ] || [ "$1" = "-h" ]; }; }; then
cat <<USAGE >&2
+1 -1
View File
@@ -4,7 +4,7 @@
:: SPDX-FileCopyrightText: 2021 The Elixir Team
:: SPDX-FileCopyrightText: 2012 Plataformatec
set ELIXIR_VERSION=1.20.0-rc.5
set ELIXIR_VERSION=1.20.3
if ""%1""=="""" if ""%2""=="""" goto documentation
if /I ""%1""==""--help"" if ""%2""=="""" goto documentation
+9 -2
View File
@@ -3,6 +3,8 @@
# SPDX-FileCopyrightText: 2012 Plataformatec
defmodule Calendar do
@strftime_max_width 1024
@moduledoc """
This module defines the responsibilities for working with
calendars, dates, times and datetimes in Elixir.
@@ -529,6 +531,7 @@ defmodule Calendar do
* `%`: indicates the start of a formatted section
* `<padding>`: set the padding (see below)
* `<width>`: a number indicating the minimum size of the formatted section
(maximum #{@strftime_max_width})
* `<format>`: the format itself (see below)
### Accepted padding options
@@ -667,9 +670,13 @@ defmodule Calendar do
end
defp parse_modifiers(<<digit, rest::binary>>, width, pad, parser_data) when digit in ?0..?9 do
new_width = (width || 0) * 10 + (digit - ?0)
width = (width || 0) * 10 + (digit - ?0)
parse_modifiers(rest, new_width, pad, parser_data)
if width > @strftime_max_width do
raise ArgumentError, "invalid strftime format: width must be at most #{@strftime_max_width}"
end
parse_modifiers(rest, width, pad, parser_data)
end
# set default padding if none was specified
+12 -6
View File
@@ -1634,13 +1634,19 @@ defmodule Code do
nil
:proceed ->
loaded =
Module.ParallelChecker.verify(fn ->
:elixir_compiler.string(charlist, file, fn _, _ -> :ok end)
end)
try do
loaded =
Module.ParallelChecker.verify(fn ->
:elixir_compiler.string(charlist, file, fn _, _ -> :ok end)
end)
:elixir_code_server.cast({:required, file})
loaded
:elixir_code_server.cast({:required, file})
loaded
catch
kind, reason ->
:elixir_code_server.call({:release, file})
:erlang.raise(kind, reason, __STACKTRACE__)
end
end
end
+1
View File
@@ -204,6 +204,7 @@ defmodule Code.Fragment do
| {:local_arity, charlist}
| {:local_call, charlist}
| {:anonymous_call, inside_caller}
| {:capture_arg, charlist}
| {:module_attribute, charlist}
| {:operator, charlist}
| {:operator_arity, charlist}
+3 -5
View File
@@ -2109,7 +2109,7 @@ defmodule Kernel do
assert_no_match_or_guard_scope(__CALLER__.context, "!")
annotate_case(
[optimize_boolean: true, type_check: {:case, :!}],
[optimize_boolean: true, type_check: {:case, :"!!"}],
quote do
case unquote(value) do
x when unquote(x_is_false_or_nil()) -> false
@@ -4703,10 +4703,8 @@ defmodule Kernel do
false
[] ->
quote do
_ = unquote(left)
false
end
# inlined as false in erlang pass
quote(do: :lists.member(unquote(left), []))
[head | tail] = list ->
case in_body? do
+1 -1
View File
@@ -338,7 +338,7 @@ defmodule Kernel.CLI do
parse_argv(t, %{config | verbose_compile: true})
end
defp parse_argv([~c"--profile", "time" | t], %{mode: :elixirc} = config) do
defp parse_argv([~c"--profile", ~c"time" | t], %{mode: :elixirc} = config) do
parse_argv(t, %{config | profile: :time})
end
+2 -2
View File
@@ -197,8 +197,8 @@ defmodule Kernel.ParallelCompiler do
deprecation warnings, and type checking should run. Defaults to `true`.
We recommend disabling it only for debugging purposes.
* `:profile` - if set to `:time` measure the compilation time of each compilation cycle
and group pass checker
* `:profile` - if set to `:time`, measure the compilation time of each compilation cycle,
each module type check, and group pass checker
* `:purge_compiler_modules` - if set to `true`, automatically purge compilation modules
after compilation (see `Code.purge_compiler_modules/0`)
+1 -9
View File
@@ -369,9 +369,7 @@ defmodule Kernel.Utils do
end
# Prefaces `guard` with unquoted versions of `refs`.
defp unquote_refs_once(guard, refs, %{module: module} = env) do
env = %{env | context: nil}
defp unquote_refs_once(guard, refs, %{module: module}) do
{guard, used_refs} =
Macro.postwalk(guard, %{}, fn
{ref, meta, context} = var, acc when is_atom(ref) and is_atom(context) ->
@@ -393,12 +391,6 @@ defmodule Kernel.Utils do
{var, acc}
end
{{:., dot_meta, [:erlang, :orelse]}, meta, [left, right]}, acc ->
{Macro.expand({{:., dot_meta, [Kernel, :or]}, meta, [left, right]}, env), acc}
{{:., dot_meta, [:erlang, :andalso]}, meta, [left, right]}, acc ->
{Macro.expand({{:., dot_meta, [Kernel, :and]}, meta, [left, right]}, env), acc}
node, acc ->
{node, acc}
end)
+137 -81
View File
@@ -20,6 +20,7 @@ defmodule Module.ParallelChecker do
{:max_concurrency, pos_integer()}
| {:long_verification_threshold, pos_integer()}
| {:each_long_verification, (module() -> term()) | (module(), pid() -> term())}
| {:profile, :time}
| {atom(), term()}
]
@@ -67,17 +68,37 @@ defmodule Module.ParallelChecker do
def spawn({pid, {checker, table}}, module, module_map, signatures, beam_location, log?) do
# Protocols may have been consolidated. So if we know their beam location,
# we discard their module map on purpose and start from file.
info =
if beam_location != [] and Keyword.has_key?(module_map.attributes, :__protocol__) do
List.to_string(beam_location)
else
cache_from_module_map(table, module_map, signatures)
end
inner_spawn(pid, checker, table, module, info, log?)
if beam_location != [] and Keyword.has_key?(module_map.attributes, :__protocol__) do
spawn_and_register_cache(pid, checker, table, module, List.to_string(beam_location), log?)
else
{mode, module_tuple} = cache_from_module_map(table, module_map, signatures)
ref = make_ref()
spawned = spawn_checker(ref, pid, checker, table, module, module_tuple, log?)
register_cache_and_checker(checker, mode, module, spawned, ref)
:ok
end
end
defp inner_spawn(pid, checker, table, module, info, log?) do
defp spawn_checker(ref, pid, checker, table, module, module_tuple, log?) do
spawn(fn ->
mon_ref = Process.monitor(pid)
receive do
{^ref, :check, profile} ->
# Set the compiler info so we can collect warnings
Process.link(pid)
:erlang.put(:elixir_compiler_info, {pid, self()})
{warnings, errors} = check_module(module_tuple, {checker, table}, log?, profile)
send(pid, {__MODULE__, module, warnings, errors})
send(checker, {__MODULE__, :checked, ref})
{:DOWN, ^mon_ref, _, _, _} ->
:ok
end
end)
end
defp spawn_and_register_cache(pid, checker, table, module, info, log?) do
ref = make_ref()
spawned =
@@ -88,48 +109,28 @@ defmodule Module.ParallelChecker do
{^ref, :cache} ->
Process.link(pid)
{mode, module_tuple} =
cond do
is_binary(info) ->
location =
case :code.which(module) do
[_ | _] = path -> path
_ -> info
end
with {:ok, binary} <- File.read(location),
{:ok,
{_, [{:debug_info, {:debug_info_v1, backend, data}}, {~c"ExCk", checker}]}} <-
:beam_lib.chunks(binary, [:debug_info, ~c"ExCk"]),
{:ok, module_map} <- backend.debug_info(:elixir_v1, module, data, []),
{@elixir_checker_version, contents} <- :erlang.binary_to_term(checker) do
{cache_chunk(table, module, contents), module_map_to_module_tuple(module_map)}
else
_ -> {:uncached, nil}
end
is_tuple(info) ->
info
location =
case :code.which(module) do
[_ | _] = path -> path
_ -> info
end
# We only make the module available now, so they are not visible during inference
:ets.insert(table, {module, mode})
send(checker, {ref, :cached})
receive do
{^ref, :check} ->
# Set the compiler info so we can collect warnings
:erlang.put(:elixir_compiler_info, {pid, self()})
{warnings, errors} =
if module_tuple do
check_module(module_tuple, {checker, table}, log?)
else
{[], []}
end
send(pid, {__MODULE__, module, warnings, errors})
send(checker, {__MODULE__, :done, module})
with {:ok, binary} <- File.read(location),
{:ok, {_, [{:debug_info, {:debug_info_v1, backend, data}}, {~c"ExCk", exck}]}} <-
:beam_lib.chunks(binary, [:debug_info, ~c"ExCk"]),
{:ok, module_map} <- backend.debug_info(:elixir_v1, module, data, []),
{@elixir_checker_version, contents} <- :erlang.binary_to_term(exck) do
mode = cache_chunk(table, module, contents)
module_tuple = module_map_to_module_tuple(module_map)
spawned = spawn_checker(ref, pid, checker, table, module, module_tuple, log?)
:ets.insert(table, {module, mode})
send(checker, {__MODULE__, :cached, module, spawned, ref})
else
_ ->
# Nothing to check, so we notify everyone we are done
:ets.insert(table, {module, :uncached})
send(checker, {__MODULE__, :cached, module, nil, ref})
send(pid, {__MODULE__, module, [], []})
end
{:DOWN, ^mon_ref, _, _, _} ->
@@ -137,7 +138,7 @@ defmodule Module.ParallelChecker do
end
end)
register(checker, module, spawned, ref)
register_cache(checker, module, spawned, ref)
:ok
end
@@ -191,7 +192,7 @@ defmodule Module.ParallelChecker do
log? = not match?({_, false}, value)
for {module, file} <- runtime_files do
inner_spawn(self(), checker, table, module, file, log?)
spawn_and_register_cache(self(), checker, table, module, file, log?)
end
count = :gen_server.call(checker, :start, :infinity)
@@ -262,7 +263,7 @@ defmodule Module.ParallelChecker do
## Module checking
defp check_module(module_tuple, cache, log?) do
defp check_module(module_tuple, cache, log?, profile) do
{module, file, line, definitions, no_warn_undefined, behaviours, impls, attrs, after_verify} =
module_tuple
@@ -276,9 +277,13 @@ defmodule Module.ParallelChecker do
definitions
)
type_warnings =
profile(module, profile, fn ->
Module.Types.warnings(module, file, attrs, definitions, no_warn_undefined, cache)
end)
{warnings, errors} =
module
|> Module.Types.warnings(file, attrs, definitions, no_warn_undefined, cache)
type_warnings
|> Kernel.++(behaviour_warnings)
|> group_diagnostics()
|> emit_diagnostics(file, log?)
@@ -528,8 +533,12 @@ defmodule Module.ParallelChecker do
:gen_server.call(server, {:unlock, module, mode}, :infinity)
end
defp register(server, module, pid, ref) do
:gen_server.cast(server, {:register, module, pid, ref})
defp register_cache_and_checker(server, mode, module, pid, ref) do
:gen_server.cast(server, {:register_cache_and_checker, mode, module, pid, ref})
end
defp register_cache(server, module, pid, ref) do
:gen_server.cast(server, {:register_cache, module, pid, ref})
end
## Server callbacks
@@ -572,11 +581,13 @@ defmodule Module.ParallelChecker do
state = %{
waiting: %{},
modules: [],
caches: [],
checkers: [],
spawned: %{},
schedulers: schedulers,
threshold: threshold,
callback: callback,
profile: Keyword.get(options, :profile),
protocols: [],
table: table
}
@@ -584,20 +595,9 @@ defmodule Module.ParallelChecker do
:gen_server.enter_loop(__MODULE__, [], state)
end
def handle_call(:start, _from, %{modules: modules, protocols: protocols, table: table} = state) do
def handle_call(:start, _from, %{caches: caches, protocols: protocols, table: table} = state) do
:ets.insert(table, Enum.map(protocols, &{&1, :uncached}))
for {_module, pid, ref} <- modules do
send(pid, {ref, :cache})
end
for {_module, _pid, ref} <- modules do
receive do
{^ref, :cached} -> :ok
end
end
{:reply, length(modules), run_checkers(%{state | protocols: []})}
{:reply, length(caches), run_caches(%{state | protocols: []})}
end
def handle_call({:lock, module}, from, %{waiting: waiting} = state) do
@@ -627,10 +627,21 @@ defmodule Module.ParallelChecker do
{:noreply, state}
end
def handle_info({__MODULE__, :done, module}, state) do
# Unfortunately we cannot assume uniqueness because the same module
# may be defined by mistake several times
{timer, spawned} = Map.pop(state.spawned, module)
def handle_info({__MODULE__, :cached, module, pid, ref}, state) do
{_nil, spawned} = Map.pop(state.spawned, ref)
state =
if pid do
%{state | spawned: spawned, checkers: [{module, pid, ref} | state.checkers]}
else
%{state | spawned: spawned}
end
{:noreply, run_caches(state)}
end
def handle_info({__MODULE__, :checked, ref}, state) do
{timer, spawned} = Map.pop(state.spawned, ref)
timer && Process.cancel_timer(timer)
{:noreply, run_checkers(%{state | spawned: spawned})}
end
@@ -639,11 +650,41 @@ defmodule Module.ParallelChecker do
{:stop, :normal, state}
end
def handle_cast({:register, module, pid, ref}, %{modules: modules} = state) do
{:noreply, %{state | modules: [{module, pid, ref} | modules]}}
def handle_cast({:register_cache, module, pid, ref}, %{caches: caches} = state) do
{:noreply, %{state | caches: [{module, pid, ref} | caches]}}
end
defp run_checkers(%{modules: []} = state) do
def handle_cast(
{:register_cache_and_checker, mode, module, pid, ref},
%{caches: caches, checkers: checkers} = state
) do
{:noreply,
%{state | caches: [{module, mode} | caches], checkers: [{module, pid, ref} | checkers]}}
end
defp run_caches(%{caches: [], spawned: spawned} = state) do
if spawned == %{}, do: run_checkers(state), else: state
end
defp run_caches(%{spawned: spawned, schedulers: schedulers} = state)
when map_size(spawned) >= schedulers do
state
end
defp run_caches(%{caches: [cache | caches]} = state) do
case cache do
{_module, pid, ref} ->
send(pid, {ref, :cache})
spawned = Map.put(state.spawned, ref, nil)
run_caches(%{state | caches: caches, spawned: spawned})
{module, mode} ->
:ets.insert(state.table, {module, mode})
run_caches(%{state | caches: caches})
end
end
defp run_checkers(%{checkers: []} = state) do
state
end
@@ -652,10 +693,25 @@ defmodule Module.ParallelChecker do
state
end
defp run_checkers(%{modules: [{module, pid, ref} | modules]} = state) do
send(pid, {ref, :check})
defp run_checkers(%{checkers: [{module, pid, ref} | checkers]} = state) do
send(pid, {ref, :check, state.profile})
timer = :erlang.send_after(state.threshold, self(), {__MODULE__, :timeout, module, pid})
spawned = Map.put(state.spawned, module, timer)
run_checkers(%{state | modules: modules, spawned: spawned})
spawned = Map.put(state.spawned, ref, timer)
run_checkers(%{state | checkers: checkers, spawned: spawned})
end
defp profile(module, :time, fun) do
{time, result} = :timer.tc(fun)
IO.puts(
:stderr,
"[profile] Type checked #{inspect(module)} in #{div(time, 1000)}ms"
)
result
end
defp profile(_module, _profile, fun) do
fun.()
end
end
+12 -9
View File
@@ -136,8 +136,8 @@ defmodule Module.Types do
defp impl_for(attrs) do
case List.keyfind(attrs, :__impl__, 0) do
{:__impl__, [protocol: protocol, for: for]} ->
if Code.ensure_loaded?(protocol) and function_exported?(protocol, :behaviour_info, 1) do
{for, protocol.behaviour_info(:callbacks)}
if Code.ensure_loaded?(protocol) and function_exported?(protocol, :__protocol__, 1) do
{for, protocol.__protocol__(:functions)}
else
nil
end
@@ -266,7 +266,7 @@ defmodule Module.Types do
Enum.reduce(unused_indexes, context, fn clause_index, context ->
{meta, _args, _guards, _body} = Enum.fetch!(clauses, clause_index)
stack = %{stack | function: fun_arity}
stack = %{stack | function: fun_arity} |> with_file_meta(meta)
Helpers.warn(__MODULE__, {:unused_clause, kind, fun_arity}, meta, stack, context)
end)
else
@@ -299,11 +299,11 @@ defmodule Module.Types do
local_sigs ->
case finder.(fun_arity) do
{mode, {fun_arity, kind, meta, clauses}, expected} ->
{mode, {fun_arity, kind, _meta, clauses}, expected} ->
context = put_in(context.local_sigs, Map.put(local_sigs, fun_arity, kind))
{inferred, mapping, context} =
local_handler(mode, fun_arity, kind, meta, clauses, expected, stack, context)
local_handler(mode, fun_arity, kind, clauses, expected, stack, context)
context =
update_in(context.local_sigs, &Map.put(&1, fun_arity, {kind, inferred, mapping}))
@@ -316,9 +316,10 @@ defmodule Module.Types do
end
end
defp local_handler(:traverse, {_, arity}, _kind, _meta, clauses, _expected, stack, context) do
defp local_handler(:traverse, {_, arity}, _kind, clauses, _expected, stack, context) do
context =
Enum.reduce(clauses, context, fn {_meta, _args, _guards, body}, context ->
Enum.reduce(clauses, context, fn {meta, _args, _guards, body}, context ->
stack = with_file_meta(stack, meta)
Module.Types.Traverse.of_expr(body, stack, context)
end)
@@ -326,9 +327,9 @@ defmodule Module.Types do
{inferred, [{0, 0}], context}
end
defp local_handler(mode, fun_arity, kind, meta, clauses, expected, stack, context) do
defp local_handler(mode, fun_arity, kind, clauses, expected, stack, context) do
{fun, _arity} = fun_arity
stack = stack |> fresh_stack(mode, fun_arity) |> with_file_meta(meta)
stack = fresh_stack(stack, mode, fun_arity)
base_info = {:def, kind, fun, expected}
case clauses do
@@ -341,6 +342,7 @@ defmodule Module.Types do
end
defp default_local_handler(meta, args, body, base_info, kind, fun, expected, stack, context) do
stack = with_file_meta(stack, meta)
guards = []
previous = Pattern.init_previous()
fresh_context = fresh_context(context)
@@ -400,6 +402,7 @@ defmodule Module.Types do
Enum.reduce(clauses, {0, 0, Pattern.init_previous(), [], [], [], context}, fn
{meta, args, guards, body},
{index, total, previous, domain, mapping, inferred, acc_context} ->
stack = with_file_meta(stack, meta)
fresh_context = fresh_context(acc_context)
info = {base_info, args, guards}
+43 -3
View File
@@ -38,7 +38,9 @@ defmodule Module.Types.Apply do
end
fas = list(tuple([atom(), integer()]))
struct_info = list(closed_map(default: if_set(term()), field: atom()))
struct_info =
list(closed_map(default: if_set(term()), field: atom(), required: if_set(boolean())))
shared_info = [
attributes: list(tuple([atom(), list(term())])),
@@ -242,7 +244,11 @@ defmodule Module.Types.Apply do
{Kernel, :put_elem, [{[open_tuple([]), integer(), term()], dynamic(open_tuple([]))}]},
## Lists
{:lists, :member, [{[term(), list(term())], boolean()}]},
{:lists, :member,
[
{[term(), empty_list()], atom([false])},
{[term(), non_empty_list(term())], boolean()}
]},
## Map
{Map, :delete, [{[open_map(), term()], open_map()}]},
@@ -1081,6 +1087,13 @@ defmodule Module.Types.Apply do
end
end
defp remote_apply(:erlang, :send, info, [_dest, message] = args_types, stack) do
case remote_apply(info, args_types, stack) do
{:ok, _type} -> {:ok, return(message, args_types, stack)}
other -> other
end
end
defp remote_apply(:erlang, :tl, _info, [list], stack) do
case list_tl(list) do
{:ok, value_type} -> {:ok, return(value_type, [list], stack)}
@@ -1088,6 +1101,33 @@ defmodule Module.Types.Apply do
end
end
defp remote_apply(:erlang, :++, _info, [left, right], stack) do
# TODO: remove once we add parametric types, this will just be:
# empty_list(), a -> a
# non_empty_list(elem), a -> non_empty_list(elem, a)
case list_of(left) do
{empty_list?, list_of} ->
left_result =
if empty_list? do
right
else
none()
end
right_result =
if list_of do
non_empty_list(list_of, right)
else
none()
end
{:ok, return(union(left_result, right_result), [left, right], stack)}
:badproperlist ->
{:error, badremote(:erlang, :++, [left, right])}
end
end
@struct_key atom([:__struct__])
@nil_atom atom([nil])
@@ -1403,7 +1443,7 @@ defmodule Module.Types.Apply do
defp remote_apply(:maps, :values, _info, [map], stack) do
case map_to_list(map, fn _key, value -> value end) do
{:ok, list_type} -> {:ok, return(list_type, [map], stack)}
:badmap -> {:error, badremote(:maps, :keys, [map])}
:badmap -> {:error, badremote(:maps, :values, [map])}
end
end
+206 -94
View File
@@ -41,7 +41,7 @@ defmodule Module.Types.Descr do
defguardp fields_size(fields) when length(fields)
@domain_key_types :lists.sort(
[:binary, :integer, :float, :pid, :port, :reference] ++
[:binary, :bitstring, :integer, :float, :pid, :port, :reference] ++
[:fun, :atom, :tuple, :map, :list]
)
@@ -77,7 +77,7 @@ defmodule Module.Types.Descr do
@not_set %{optional: 1}
@term_or_optional Map.put(@term, :optional, 1)
@term_or_dynamic_optional Map.put(@term, :dynamic, %{optional: 1})
@term_or_dynamic_optional Map.put(@term, :dynamic, @term_or_optional)
@not_atom_or_optional Map.delete(@term_or_optional, :atom)
@empty_intersection [0, :bdd_bot]
@@ -334,6 +334,7 @@ defmodule Module.Types.Descr do
keep_optional: 1, remove_optional: 1, remove_optional_static: 1, optional_to_term: 1}
defp keep_optional(descr) do
case descr do
%{dynamic: %{optional: 1}, optional: 1} -> %{dynamic: %{optional: 1}, optional: 1}
%{dynamic: %{optional: 1}} -> %{dynamic: %{optional: 1}}
%{optional: 1} -> %{optional: 1}
_ -> @none
@@ -408,6 +409,11 @@ defmodule Module.Types.Descr do
defp pop_dynamic(:term), do: {:term, :term}
defp pop_dynamic(descr), do: Map.pop(descr, :dynamic, descr)
defp put_dynamic(:term, dynamic), do: optional_to_term(%{dynamic: dynamic})
defp put_dynamic(static, dynamic) when static == dynamic, do: static
defp put_dynamic(_static, dynamic) when dynamic == @none, do: @none
defp put_dynamic(static, dynamic), do: Map.put(static, :dynamic, dynamic)
@compile {:inline, maybe_union: 2}
defp maybe_union(nil, _fun), do: nil
defp maybe_union(descr, fun), do: union(descr, fun.())
@@ -507,7 +513,8 @@ defmodule Module.Types.Descr do
{right_dynamic, right_static} = pop_dynamic(right)
dynamic_part = difference_static(left_dynamic, right_static)
Map.put(difference_static(left_static, right_dynamic), :dynamic, dynamic_part)
difference_static(left_static, right_dynamic)
|> put_dynamic(dynamic_part)
else
difference_static(left, right)
end
@@ -978,7 +985,13 @@ defmodule Module.Types.Descr do
domain of a function. It is used to refine dynamic types
as we traverse the program.
"""
def compatible_intersection(other, :term), do: {:ok, remove_optional(other)}
def compatible_intersection(other, :term) do
if empty?(other) do
{:error, other}
else
{:ok, remove_optional(other)}
end
end
def compatible_intersection(left, right) do
{left_dynamic, left_static} = pop_dynamic(left)
@@ -1400,10 +1413,10 @@ defmodule Module.Types.Descr do
{:ok, atom()}
iex> fun_apply(fun([integer()], atom()), [float()])
:badarg
{:badarg, [integer()], false}
iex> fun_apply(fun([dynamic()], atom()), [dynamic()])
{:ok, atom()}
iex> fun_apply(dynamic(fun([integer()], atom())), [integer()])
{:ok, dynamic(atom())}
"""
def fun_apply(:term, _arguments), do: :badfun
@@ -1560,6 +1573,9 @@ defmodule Module.Types.Descr do
{:badarity, [arity | other]}
end
:badfun when fun_dynamic == nil ->
:badfun
:badfun ->
# No static arrows: dynamic-only path. Mixed-arity in the dynamic
# component is fine — we pick the matching-arity arrows and the
@@ -1616,7 +1632,7 @@ defmodule Module.Types.Descr do
defp fun_normalize(%{fun: {:union, bdds}}, arity) do
case :maps.take(arity, bdds) do
{bdd, _rest} ->
{bdd, rest} ->
{domain, arrows} =
Enum.reduce(fun_bdd_to_pos_dnf(arity, bdd), {term(), []}, fn pos_funs,
{domain, arrows} ->
@@ -1624,7 +1640,13 @@ defmodule Module.Types.Descr do
end)
if arrows == [] do
{:badarity, :maps.keys(bdds)}
# The function is empty at the requested arity. Report the *other*
# arities (never the called one, which would be self-contradictory),
# or :badfun when there are none, i.e. the function is empty.
case :maps.keys(rest) do
[] -> :badfun
other -> {:badarity, other}
end
else
{:ok, domain, arrows}
end
@@ -1942,25 +1964,33 @@ defmodule Module.Types.Descr do
# representation. The goal here is to do the opposite of fun_descr
# and put static and dynamic parts back together to improve
# pretty printing.
defp fun_denormalize(%{fun: {:union, static_repr}}, %{fun: {:union, dynamic_repr}}, opts) do
# Denormalize each arity
for {arity, static_bdd} <- static_repr,
{^arity, dynamic_bdd} <- dynamic_repr,
reduce: {static_repr, dynamic_repr, []} do
{statics, dynamics, acc} ->
with {:ok, quoted} <- fun_denormalize_arity(arity, static_bdd, dynamic_bdd, opts) do
{Map.delete(statics, arity), Map.delete(dynamics, arity), [quoted | acc]}
else
_ -> {statics, dynamics, acc}
end
end
defp fun_denormalize(
%{fun: {:union, static_repr}} = static,
%{fun: {:union, dynamic_repr}} = dynamic,
opts
) do
{static_repr, dynamic_repr, acc} =
Enum.reduce(static_repr, {static_repr, dynamic_repr, []}, fn
{arity, static_bdd}, {statics, dynamics, acc} ->
with %{^arity => dynamic_bdd} <- dynamics,
{:ok, quoted} <- fun_denormalize_arity(arity, static_bdd, dynamic_bdd, opts) do
{Map.delete(statics, arity), Map.delete(dynamics, arity), [quoted | acc]}
else
_ -> {statics, dynamics, acc}
end
end)
{fun_replace_arities(static, static_repr), fun_replace_arities(dynamic, dynamic_repr), acc}
end
# If not unions of functions, do not try to denormalize.
defp fun_denormalize(static_repr, dynamic_repr, _opts) do
{static_repr, dynamic_repr, []}
defp fun_denormalize(static, dynamic, _opts) do
{static, dynamic, []}
end
defp fun_replace_arities(descr, arities) when arities == %{}, do: Map.delete(descr, :fun)
defp fun_replace_arities(descr, arities), do: %{descr | fun: {:union, arities}}
defp fun_denormalize_arity(arity, static_bdd, dynamic_bdd, opts) do
static_pos = fun_bdd_to_pos_dnf(arity, static_bdd)
dynamic_pos = fun_bdd_to_pos_dnf(arity, dynamic_bdd)
@@ -2159,6 +2189,9 @@ defmodule Module.Types.Descr do
not dynamic? ->
dynamic_descr
static_empty? and empty? ->
%{bitmap: @bit_empty_list, dynamic: dynamic_descr}
static_empty? ->
%{dynamic: dynamic_descr}
@@ -2205,14 +2238,13 @@ defmodule Module.Types.Descr do
defp list_new(list_type, last_type), do: bdd_leaf_new(list_type, last_type)
defp non_empty_list_literals_intersection(list_literals) do
try do
Enum.reduce(list_literals, {:term, :term}, fn bdd_leaf(next_list, next_last),
{list, last} ->
{non_empty_intersection!(list, next_list), non_empty_intersection!(last, next_last)}
{list, last} =
Enum.reduce(list_literals, {:term, :term}, fn
bdd_leaf(next_list, next_last), {list, last} ->
{intersection(list, next_list), intersection(last, next_last)}
end)
catch
:empty -> :empty
end
if empty?(list) or empty?(last), do: :empty, else: {list, last}
end
# Takes all the lines from the root to the leaves finishing with a 1,
@@ -2260,7 +2292,7 @@ defmodule Module.Types.Descr do
It returns a two-element tuple. The first element dictates the
empty list type. The second element returns the value type.
{boolean(), t() or nil}
{true, nil} or {boolean(), t()}
If the value is `nil`, it means that component is missing.
Note `{false, nil}` is not a valid return type, instead it
@@ -2272,17 +2304,17 @@ defmodule Module.Types.Descr do
case :maps.take(:dynamic, descr) do
:error ->
with {empty_list?, value} <- list_of_static(descr) do
if empty?(value) and empty_list? == false do
:badproperlist
else
{empty_list?, value}
cond do
not empty?(value) -> {empty_list?, value}
empty_list? -> {true, nil}
true -> :badproperlist
end
end
{dynamic, static} ->
with {empty_list?, static_value} <- list_of_static(static) do
empty_list? =
empty_list? or
empty_list? or dynamic == :term or
match?(
%{bitmap: bitmap} when (bitmap &&& @bit_empty_list) != 0,
dynamic
@@ -2295,7 +2327,7 @@ defmodule Module.Types.Descr do
%{list: bdd} ->
Enum.reduce(list_bdd_to_pos_dnf(bdd), none(), fn {list, last, _negs}, acc ->
if last == @empty_list or subtype?(last, @empty_list) do
if empty_list_type?(last) do
union(acc, list)
else
acc
@@ -2573,7 +2605,7 @@ defmodule Module.Types.Descr do
[to_quoted(ty, opts), to_quoted(lst, opts)]
end
{name, [], args}
{:non_empty_list, [], args}
end)
|> Kernel.then(
&[
@@ -3092,7 +3124,30 @@ defmodule Module.Types.Descr do
defp map_difference(bdd1, bdd2),
do: bdd_difference(bdd1, bdd2, &map_leaf_difference/3)
defp map_leaf_difference(bdd_leaf(tag, fields), bdd_leaf(:open, [{key, v2}]), type) do
# This clause optimizes differences with an open single-key right side:
#
# %{b: atom(), c: pid()} \ %{..., c: if_set(pid() | binary())}
#
# Depending on the surrounding BDD shape, the difference formulas ask the
# leaf comparison for a direct difference plus an optional intersection
# or optional union.
#
# Because the right-hand side is an open map with a single key, this branch
# assumes the result can be represented by updating the matching key
# on the left-hand side. For this pair, the direct difference is empty and
# the intersection is `%{b: atom(), c: pid()}`, which are both correct.
#
# However, when the formula needs the union, this shortcut would produce
# `%{b: atom(), c: if_set(pid() or binary())}` by preserving the left shape.
# This is not the semantic union, because the right side also includes maps
# without `:b`. In other words, we cannot always union them by updating only
# the matching key.
#
# Therefore, this clause is only used when `type` is `:intersection` or `:none`.
# `:union` falls through to the general clause below. The reason we have
# this long comment is because this was a regression in the past.
defp map_leaf_difference(bdd_leaf(tag, fields), bdd_leaf(:open, [{key, v2}]), type)
when type != :union do
{found?, v1} =
case fields_find(key, fields) do
{:ok, value} -> {true, value}
@@ -4140,7 +4195,7 @@ defmodule Module.Types.Descr do
defp map_put_static_value(descr, split_keys, type) do
case :maps.take(:dynamic, descr) do
:error ->
if descr_key?(descr, :map) and map_only?(descr) do
if non_empty_map_only?(descr) do
{:ok, map_put_static(descr, split_keys, type)}
else
:badmap
@@ -4462,11 +4517,22 @@ defmodule Module.Types.Descr do
end
defp map_line_meet_empty?(key, type, neg_type, t1, t2, tag, neg_tag, acc_meet, negs) do
diff = difference(type, neg_type)
meet = intersection(type, neg_type)
(empty?(diff) or map_line_empty?(tag, Enum.reverse(acc_meet, [{key, diff} | t1]), negs)) and
(empty?(meet) or map_line_meet_empty?(t1, t2, tag, neg_tag, [{key, meet} | acc_meet], negs))
if empty?(meet) do
# This negative map is disjoint from the current line at this field.
map_line_empty?(tag, Enum.reverse(acc_meet, [{key, type} | t1]), negs)
else
diff = difference(type, neg_type)
if empty?(diff) do
# The field is a subtype of the negative field, so their intersection is type.
map_line_meet_empty?(t1, t2, tag, neg_tag, [{key, type} | acc_meet], negs)
else
map_line_empty?(tag, Enum.reverse(acc_meet, [{key, diff} | t1]), negs) and
map_line_meet_empty?(t1, t2, tag, neg_tag, [{key, meet} | acc_meet], negs)
end
end
end
defp map_line_fields_empty?([{k1, v1} | t1], [{k2, _} | _] = l2, tag, neg_tag, fields, negs)
@@ -5026,15 +5092,6 @@ defmodule Module.Types.Descr do
end
end
# Detecting tuples built with none() fields
defp tuple_literal_intersection(:open, [], tag, elements) do
if Enum.any?(elements, &empty?/1) do
:empty
else
{tag, elements}
end
end
defp tuple_literal_intersection(tag1, elements1, tag2, elements2) do
case tuple_sizes_strategy(tag1, length(elements1), tag2, length(elements2)) do
:disjoint ->
@@ -5052,21 +5109,9 @@ defmodule Module.Types.Descr do
end
end
defp tuple_sizes_strategy(:closed, n1, :closed, n2) when n1 != n2, do: :disjoint
defp tuple_sizes_strategy(:closed, n1, :closed, n2) when n1 == n2, do: :left_subtype_of_right
defp tuple_sizes_strategy(:closed, n1, :open, n2) when n1 < n2, do: :disjoint
defp tuple_sizes_strategy(_, n1, :open, n2) when n1 >= n2, do: :left_subtype_of_right
defp tuple_sizes_strategy(:open, n1, :closed, n2) when n1 > n2, do: :disjoint
defp tuple_sizes_strategy(_, _, _, _), do: :none
# Intersects two lists of types, and _appends_ the extra elements to the result.
defp zip_non_empty_intersection!([], types2, acc) do
if Enum.any?(types2, &empty?/1), do: throw(:empty), else: Enum.reverse(acc, types2)
end
defp zip_non_empty_intersection!(types1, [], acc) do
if Enum.any?(types1, &empty?/1), do: throw(:empty), else: Enum.reverse(acc, types1)
end
defp zip_non_empty_intersection!([], types2, acc), do: Enum.reverse(acc, types2)
defp zip_non_empty_intersection!(types1, [], acc), do: Enum.reverse(acc, types1)
defp zip_non_empty_intersection!([type1 | rest1], [type2 | rest2], acc) do
zip_non_empty_intersection!(rest1, rest2, [non_empty_intersection!(type1, type2) | acc])
@@ -5082,6 +5127,13 @@ defmodule Module.Types.Descr do
end
end
defp tuple_sizes_strategy(:closed, n1, :closed, n2) when n1 != n2, do: :disjoint
defp tuple_sizes_strategy(:closed, n1, :closed, n2) when n1 == n2, do: :left_subtype_of_right
defp tuple_sizes_strategy(:closed, n1, :open, n2) when n1 < n2, do: :disjoint
defp tuple_sizes_strategy(_, n1, :open, n2) when n1 >= n2, do: :left_subtype_of_right
defp tuple_sizes_strategy(:open, n1, :closed, n2) when n1 > n2, do: :disjoint
defp tuple_sizes_strategy(_, _, _, _), do: :none
defp tuple_difference(_, bdd_leaf(:open, [])),
do: :bdd_bot
@@ -5112,17 +5164,35 @@ defmodule Module.Types.Descr do
defp non_empty_tuple_literals_intersection(tuples) do
try do
Enum.reduce(tuples, {:open, []}, fn bdd_leaf(next_tag, next_elements), {tag, elements} ->
case tuple_literal_intersection(tag, elements, next_tag, next_elements) do
:empty -> throw(:empty)
next -> next
Enum.reduce(tuples, {:open, []}, fn bdd_leaf(tag1, elements1), {tag2, elements2} ->
case tuple_sizes_strategy(tag1, length(elements1), tag2, length(elements2)) do
:disjoint ->
throw(:empty)
_ ->
tag = if tag1 == :open and tag2 == :open, do: :open, else: :closed
{tag, zip_intersection(elements1, elements2, [])}
end
end)
catch
:empty -> :empty
else
{tag, elements} ->
if Enum.any?(elements, &empty?/1) do
:empty
else
{tag, elements}
end
end
end
defp zip_intersection([], types2, acc), do: Enum.reverse(acc, types2)
defp zip_intersection(types1, [], acc), do: Enum.reverse(acc, types1)
defp zip_intersection([type1 | rest1], [type2 | rest2], acc) do
zip_intersection(rest1, rest2, [intersection(type1, type2) | acc])
end
defp tuple_empty?(bdd) do
bdd_to_dnf(bdd)
|> Enum.all?(fn {pos, negs} ->
@@ -5164,13 +5234,22 @@ defmodule Module.Types.Descr do
defp tuple_elements_empty?(acc_meet, tag, elements, [neg_type | neg_elements], negs) do
# Handles the case where {tag, elements} is an open tuple, like {:open, []}
{ty, elements} = List.pop_at(elements, 0, term())
diff = difference(ty, neg_type)
meet = intersection(ty, neg_type)
# In this case, there is no intersection between the positive and this negative.
# So we should just "go next"
(empty?(diff) or tuple_line_empty?(tag, Enum.reverse(acc_meet, [diff | elements]), negs)) and
(empty?(meet) or tuple_elements_empty?([meet | acc_meet], tag, elements, neg_elements, negs))
if empty?(meet) do
# This negative tuple is disjoint from the current line at this element.
tuple_line_empty?(tag, Enum.reverse(acc_meet, [ty | elements]), negs)
else
diff = difference(ty, neg_type)
if empty?(diff) do
# The element is a subtype of the negative element, so their intersection is ty.
tuple_elements_empty?([ty | acc_meet], tag, elements, neg_elements, negs)
else
tuple_line_empty?(tag, Enum.reverse(acc_meet, [diff | elements]), negs) and
tuple_elements_empty?([meet | acc_meet], tag, elements, neg_elements, negs)
end
end
end
# Determines if the set difference is empty when:
@@ -5499,12 +5578,15 @@ defmodule Module.Types.Descr do
iex> tuple_fetch(tuple([integer(), atom()]), 0)
{false, integer()}
iex> tuple_fetch(union(tuple([integer()]), tuple([integer(), atom()])), 1)
{true, atom()}
iex> tuple_fetch(union(tuple([integer()]), tuple([atom()])), 0)
{false, union(integer(), atom())}
iex> tuple_fetch(dynamic(), 0)
{true, dynamic()}
iex> tuple_fetch(tuple([integer(), atom()]), 2)
:badindex
iex> tuple_fetch(integer(), 0)
:badtuple
@@ -5916,27 +5998,57 @@ defmodule Module.Types.Descr do
end
end
defp tuple_insert_static(descr, _, _) when descr == @none, do: none()
defp tuple_insert_static(descr, index, type) do
Map.update!(descr, :tuple, fn bdd ->
bdd_map(bdd, fn bdd_leaf(tag, elements) ->
# If the tuple is open, then we want List.insert_at to put the new element at the correct
# index, which requires filling the tuple with `term()` values first.
# Closed tuples of an incorrect size will be ignored (they are cancelled by the earlier
# intersection with `tuple_of_size_at_least`).
elements =
if tag == :open and length(elements) < index do
tuple_fill(elements, index)
defp tuple_insert_static(%{tuple: bdd} = descr, index, type) do
%{
descr
| tuple:
if tuple_bdd_positive?(bdd) do
# A pure disjunction of leaves: the insert distributes over the union, so
# we rewrite each leaf in place (preserving the structure callers assert on).
bdd_map(bdd, fn bdd_leaf(tag, elements) ->
tuple_insert_leaf(tag, elements, index, type)
end)
else
elements
# The bdd carries negations and/or implicit `:bdd_top` positive paths
# (e.g. from `tuple_difference(open_tuple([]), _) -> bdd_negation`).
# `bdd_map` rewrites only explicit leaves, so it would skip the implicit
# top (losing the insert) and wrongly transform negated leaves. Expand to
# the exact negation-free positive DNF first, then insert into each leaf.
bdd
|> tuple_bdd_to_dnf_no_negations()
|> Enum.reduce(:bdd_bot, fn {tag, elements}, acc ->
tuple_union(tuple_insert_leaf(tag, elements, index, type), acc)
end)
end
bdd_leaf_new(tag, List.insert_at(elements, index, type))
end)
end)
}
end
defp tuple_insert_static(_descr, _index, _type), do: none()
# Inserts `type` at `index` into a single tuple literal. If the tuple is open,
# `List.insert_at` needs the tuple filled with `term()` up to `index` first.
# Closed tuples of an incorrect size are cancelled before reaching here (the
# input is intersected/guarded with `tuple_of_size_at_least`).
defp tuple_insert_leaf(tag, elements, index, type) do
elements =
if tag == :open and length(elements) < index do
tuple_fill(elements, index)
else
elements
end
bdd_leaf_new(tag, List.insert_at(elements, index, type))
end
# `bdd_map` rewrites a tuple insert exactly only on a pure positive disjunction
# of leaves (every node keeps its literal on the constrained-top branch with no
# dual branch). Any dual branch (or implicit `:bdd_top` in negated position)
# would be transformed incorrectly or skipped.
defp tuple_bdd_positive?(:bdd_bot), do: true
defp tuple_bdd_positive?(bdd_leaf(_, _)), do: true
defp tuple_bdd_positive?({_, _lit, :bdd_top, u, :bdd_bot}), do: tuple_bdd_positive?(u)
defp tuple_bdd_positive?(_), do: false
@doc """
Replace an element in the tuple at the given (0-based) index.
+34 -2
View File
@@ -584,7 +584,8 @@ defmodule Module.Types.Expr do
{union(body_type, acc), Of.reset_vars(context, original)}
else
error = {:badtimeout, timeout_type, timeout, context}
{union(body_type, acc), error(__MODULE__, error, meta, stack, context)}
context = error(__MODULE__, error, meta, stack, context)
{union(body_type, acc), Of.reset_vars(context, original)}
end
end)
|> dynamic_unless_static(stack)
@@ -641,7 +642,6 @@ defmodule Module.Types.Expr do
end)
end
# TODO: with pat <- expr do expr end
def of_expr({:with, meta, [_ | _] = clauses}, expected, _expr, stack, original) do
cache_result(meta, stack, original, fn ->
{clauses, [[do: do_block] ++ options]} = Enum.split(clauses, -1)
@@ -688,6 +688,38 @@ defmodule Module.Types.Expr do
end
end
def of_expr(
{{:., _, [:erlang, fun]}, _meta, [left, right]} = call,
expected,
_expr,
stack,
context
)
when fun in [:andalso, :orelse] do
polarity = fun == :andalso
polarity_type = atom([polarity])
left_expected =
case booleaness(expected) do
{^polarity, _} -> polarity_type
_ -> boolean()
end
{left_type, context} = of_expr(left, left_expected, call, stack, context)
{right_type, right_context} = of_expr(right, expected, call, stack, context)
if subtype?(left_type, polarity_type) do
{right_type, right_context}
else
context =
right_context
|> Of.reset_vars(context)
|> reset_warnings(context)
{union(atom([not polarity]), right_type), context}
end
end
def of_expr({{:., _, [remote, name]}, meta, args} = call, expected, _expr, stack, context) do
{remote_type, context} = of_expr(remote, atom(), call, stack, context)
{mods, context} = Of.modules(remote_type, name, length(args), call, meta, stack, context)
+1 -1
View File
@@ -329,7 +329,7 @@ defmodule Module.Types.Helpers do
end
end
{{:., _, [:lists, :member]}, meta, [expr, [_ | _] = args]} = call ->
{{:., _, [:lists, :member]}, meta, [expr, args]} = call when is_list(args) ->
if Enum.any?(args, &match?({:|, _, [_, _]}, &1)) do
call
else
+35 -28
View File
@@ -467,32 +467,32 @@ defmodule Module.Types.Of do
# TODO: Type check the fields match the struct
def struct_instance(struct, args, expected, meta, stack, context, of_fun)
when is_atom(struct) do
{info, context} = struct_info(struct, :expr, meta, stack, context)
{info, context} = struct_info(struct, :expr, meta, stack, context, true)
if is_nil(info) do
raise "expected #{inspect(struct)} to return struct metadata, but got none"
{dynamic(), context}
else
# The compiler has already checked the keys are atoms and which ones are required.
{args_types, context} =
Enum.map_reduce(args, context, fn {key, value}, context when is_atom(key) ->
value_type =
case map_fetch_key(expected, key) do
{_, expected_value_type} -> expected_value_type
_ -> term()
end
{type, context} = of_fun.(value, value_type, stack, context)
{{key, type}, context}
end)
{closed_map([{:__struct__, atom([struct])} | args_types]), context}
end
# The compiler has already checked the keys are atoms and which ones are required.
{args_types, context} =
Enum.map_reduce(args, context, fn {key, value}, context when is_atom(key) ->
value_type =
case map_fetch_key(expected, key) do
{_, expected_value_type} -> expected_value_type
_ -> term()
end
{type, context} = of_fun.(value, value_type, stack, context)
{{key, type}, context}
end)
{closed_map([{:__struct__, atom([struct])} | args_types]), context}
end
@doc """
Returns `__info__(:struct)` information about a struct.
"""
def struct_info(struct, kind, meta, stack, context) do
def struct_info(struct, kind, meta, stack, context, must_exist? \\ false) do
case stack.no_warn_undefined do
%Macro.Env{} = env ->
case :elixir_map.maybe_load_struct_info(meta, struct, :soft, env) do
@@ -511,7 +511,7 @@ defmodule Module.Types.Of do
{info, context}
else
error = {:unknown_struct, kind, struct}
error = {:unknown_struct, kind, struct, must_exist?}
{nil, error(error, meta, stack, context)}
end
end
@@ -848,19 +848,26 @@ defmodule Module.Types.Of do
}
end
def format_diagnostic({:unknown_struct, kind, module}) do
message =
if Code.ensure_loaded?(module) do
"struct #{inspect(module)} is undefined (there is such module but it does not define a struct)"
else
"struct #{inspect(module)} is undefined " <>
"(module #{inspect(module)} is not available or is yet to be defined)"
def format_diagnostic({:unknown_struct, kind, module, must_exist?}) do
detail =
case {Code.ensure_loaded?(module), must_exist?} do
{true, false} ->
"there is such module but it does not define a struct"
{false, false} ->
"module #{inspect(module)} is not available or is yet to be defined"
{true, true} ->
"the module may have been redefined as it no longer defines a struct"
{false, true} ->
"the module was also only available but may have been removed during compilation"
end
%{
message: message,
message: "struct #{inspect(module)} is undefined (#{detail})",
group: true,
severity: if(kind == :pattern, do: :error, else: :warning)
severity: if(kind == :pattern or must_exist?, do: :error, else: :warning)
}
end
+38 -7
View File
@@ -772,7 +772,8 @@ defmodule Module.Types.Pattern do
{refined, context} = of_match_var(var, atom(), expr, stack, context)
if compatible?(refined, atom()) do
of_open_map(args, singleton?(refined), [__struct__: refined], [], path, stack, context)
precise? = match?({:_, _, _}, var) or singleton?(refined)
of_open_map(args, precise?, [__struct__: refined], [], path, stack, context)
else
error = {:badstruct, refined, expr, context}
{error_type(), false, error(__MODULE__, error, meta, stack, context)}
@@ -786,8 +787,9 @@ defmodule Module.Types.Pattern do
# <<...>>>
defp of_pattern({:<<>>, _meta, args} = node, _path, stack, context) do
precise? = of_precise_bitstring?(node, context.vars)
{type, context} = Of.bitstring(args, :match, stack, context)
{type, of_precise_bitstring?(node), context}
{type, precise?, context}
end
# left ++ right
@@ -987,12 +989,19 @@ defmodule Module.Types.Pattern do
{type, precise?, put_in(context.subpatterns[key], of_pattern_tree(type, stack, context))}
end
defp of_precise_bitstring?({:<<>>, _meta, [{:"::", _, [expr, {type, _, _}]}]})
defp of_precise_bitstring?({:<<>>, _meta, [{:"::", _, [expr, {type, _, _}]}]}, vars)
when type in [:binary, :bitstring, :bytes, :bits] do
is_var(expr) or of_precise_bitstring?(expr)
new_var?(expr, vars) or of_precise_bitstring?(expr, vars)
end
defp of_precise_bitstring?(_), do: false
defp of_precise_bitstring?(_expr, _vars), do: false
defp new_var?({_, meta, _} = var, vars) when is_var(var) do
version = Keyword.fetch!(meta, :version)
not is_map_key(vars, version)
end
defp new_var?(_expr, _vars), do: false
## Guards
#
@@ -1564,7 +1573,18 @@ defmodule Module.Types.Pattern do
#{expr_to_string({:!, [], [expr]}) |> indent(4)}
will always evaluate to true because the expression has type:
will always evaluate to true because its inner expression has type:
#{to_quoted_string(type) |> indent(4)}
"""
{:case, :"!!"} ->
"""
the following conditional expression:
#{expr_to_string({:!, [], [{:!, [], [expr]}]}) |> indent(4)}
will always evaluate to false because its inner expression has type:
#{to_quoted_string(type) |> indent(4)}
"""
@@ -1721,7 +1741,18 @@ defmodule Module.Types.Pattern do
#{expr_to_string({:!, [], [expr]}) |> indent(4)}
will always evaluate to false because the expression has type:
will always evaluate to false because its inner expression has type:
#{to_quoted_string(type) |> indent(4)}
"""
op == :"!!" ->
"""
the following conditional expression:
#{expr_to_string({:!, [], [{:!, [], [expr]}]}) |> indent(4)}
will always evaluate to true because its inner expression has type:
#{to_quoted_string(type) |> indent(4)}
"""
+9
View File
@@ -24,6 +24,12 @@ defmodule URI do
[scheme]://[userinfo]@[host]:[port][path]?[query]#[fragment]
The fields contain the encoded URI components as they appear in the URI
itself. For example, a slash inside the userinfo must be stored as `%2F`,
not as `/`. Functions such as `parse/1` and `new/1` preserve existing
percent-encoded sequences in those fields, and functions such as `to_string/1`
expects those fields to already be encoded as needed. Whenever setting or
modifying the fields directly, you must encode them accordingly.
Note the `authority` field is deprecated. `parse/1` will still
populate it for backwards compatibility but you should generally
@@ -897,6 +903,9 @@ defmodule URI do
@doc """
Returns the string representation of the given [URI struct](`t:t/0`).
This function assembles the URI components into a string, assuming each
field is valid and escaped as done by `parse/1` and `new/1`.
## Examples
iex> uri = URI.parse("http://google.com")
+18 -1
View File
@@ -18,12 +18,16 @@ defmodule Version do
MAJOR.MINOR.PATCH
Each numeric component is limited to at most 14 digits.
Pre-releases are supported by optionally appending a hyphen and a series of
period-separated identifiers immediately following the patch version.
Identifiers consist of only ASCII alphanumeric characters and hyphens (`[0-9A-Za-z-]`):
"1.0.0-alpha.3"
Numeric pre-release identifiers are also limited to at most 14 digits.
Build information can be added by appending a plus sign and a series of
dot-separated identifiers immediately following the patch or pre-release version.
Identifiers consist of only ASCII alphanumeric characters and hyphens (`[0-9A-Za-z-]`):
@@ -520,6 +524,8 @@ defmodule Version do
defmodule Parser do
@moduledoc false
@max_numeric_component_digits 14
operators = [
{">=", :>=},
{"<=", :<=},
@@ -621,7 +627,9 @@ defmodule Version do
defp require_digits(nil), do: :error
defp require_digits(string) do
if leading_zero?(string), do: :error, else: parse_digits(string, "")
if leading_zero?(string) or byte_size(string) > @max_numeric_component_digits,
do: :error,
else: parse_digits(string, "")
end
defp leading_zero?(<<?0, _, _::binary>>), do: true
@@ -649,6 +657,11 @@ defmodule Version do
end
end
defp convert_parts_to_integer([part | rest], acc)
when byte_size(part) > @max_numeric_component_digits do
if all_digits?(part), do: :error, else: convert_parts_to_integer(rest, [part | acc])
end
defp convert_parts_to_integer([part | rest], acc) do
case parse_digits(part, "") do
{:ok, integer} ->
@@ -667,6 +680,10 @@ defmodule Version do
{:ok, Enum.reverse(acc)}
end
defp all_digits?(<<char, rest::binary>>) when char in ?0..?9, do: all_digits?(rest)
defp all_digits?(<<>>), do: true
defp all_digits?(_other), do: false
defp valid_identifier?(<<char, rest::binary>>)
when char in ?0..?9
when char in ?a..?z
+2 -3
View File
@@ -109,19 +109,18 @@ iex> john.__struct__
User
```
However, structs do not inherit any of the protocols that maps do. For example, you can neither enumerate nor access a struct:
However, structs do not inherit any of the built-in features that maps do. For example, you can neither enumerate nor access a struct:
```elixir
iex> john = %User{}
%User{age: 27, name: "John"}
iex> john[:name]
** (UndefinedFunctionError) function User.fetch/2 is undefined (User does not implement the Access behaviour)
User.fetch(%User{age: 27, name: "John"}, :name)
iex> Enum.each(john, fn {field, value} -> IO.puts(value) end)
** (Protocol.UndefinedError) protocol Enumerable not implemented for %User{age: 27, name: "John"} of type User (a struct)
```
Structs alongside protocols provide one of the most important features for Elixir developers: data polymorphism. That's what we will explore in the next chapter.
As we will learn in future chapters, Elixir does allow developers to attach custom behaviours to structs based on their names, enabling custom data enumeration, pretty printing, and more. This dispatch mechanism is called [protocols](protocols.md) and enables extensible data polymorphism in Elixir.
## Default values and required keys
@@ -14,12 +14,11 @@ Elixir applies bug fixes only to the latest minor branch. Security patches are a
Elixir version | Support
:------------- | :-----------------------------
1.20 | Development
1.19 | Bug fixes and security patches
1.20 | Bug fixes and security patches
1.19 | Security patches only
1.18 | Security patches only
1.17 | Security patches only
1.16 | Security patches only
1.15 | Security patches only
New releases are announced in the read-only [announcements mailing list](https://groups.google.com/group/elixir-lang-ann). All security releases [will be tagged with `[security]`](https://groups.google.com/forum/#!searchin/elixir-lang-ann/%5Bsecurity%5D%7Csort:date).
@@ -243,4 +242,4 @@ Version | Deprecated feature | Replaced by (ava
[v1.17]: https://github.com/elixir-lang/elixir/blob/v1.17/CHANGELOG.md#4-hard-deprecations
[v1.18]: https://github.com/elixir-lang/elixir/blob/v1.18/CHANGELOG.md#4-hard-deprecations
[v1.19]: https://github.com/elixir-lang/elixir/blob/v1.19/CHANGELOG.md#4-hard-deprecations
[v1.20]: https://github.com/elixir-lang/elixir/blob/main/CHANGELOG.md#4-hard-deprecations
[v1.20]: https://github.com/elixir-lang/elixir/blob/v1.20/CHANGELOG.md#4-hard-deprecations
@@ -231,11 +231,47 @@ Inferring type signatures comes with a series of trade-offs:
* Cascading errors - when a user accidentally makes type errors or the code has conflicting assumptions, type inference may lead to less clear error messages as the type system tries to reconcile diverging type assumptions across code paths.
On the other hand, type inference offers the benefit of enabling type checking for functions and codebases without requiring the user to add type annotations. To balance these trade-offs, Elixir aims to provide "module type inference": our goal is to infer the types of functions considering the current module, Elixir's standard library and your dependencies, while calls to modules within the same project are assumed to be `dynamic()`. Once types are inferred, then the whole project is type checked considering all modules and all types (inferred or otherwise).
On the other hand, type inference offers the benefit of enabling type checking for functions and codebases without requiring the user to add type annotations. To balance these trade-offs, Elixir aims to provide type inference across dependencies: our goal is to infer the types of functions considering the current module, Elixir's standard library and your dependencies, while calls to modules within the same project are assumed to be `dynamic()`. Once types are inferred, then the whole project is type checked considering all modules and all types (inferred or otherwise).
Type inference in Elixir is best-effort: it doesn't guarantee it will find all possible type incompatibilities, only that it may find bugs where all combinations of a type _will_ fail, even in the absence of explicit type annotations. It is meant to be an efficient routine that brings developers some benefits of static typing, without requiring any effort from them and keeping the expressiveness of the language.
In the long term, Elixir developers who want static typing guarantees must explicitly add type signatures to their functions (see "Roadmap"). Any function with an explicit type signature will be typed checked against the user-provided annotations, as in other statically typed languages.
In the long term, Elixir developers who want static typing guarantees may explicitly add type signatures to their functions (see "Roadmap"). Any function with an explicit type signature will be typed checked against the user-provided annotations, as in other statically typed languages.
### False positives
Elixir's type inference generally avoids emitting false positive type violations: which are warnings emitted by the type checker when there are no runtime errors. However, in some situations, those may happen and are documented below.
#### `for`-comprehensions assume they are executed at least once
For comprehensions in Elixir assume they are executed at least once. Take this code:
```elixir
def example(x, list) do
for _i <- list do
Atom.to_string(x)
end
x + 1
end
```
`x + 1` will fail because it assumes `x` is an atom from the `Atom.to_string(x)` call, even though the function may raise no runtime error if `list` is an empty list. This is intentional, as it helps find discrepancies inside and outside comprehensions. You can address this by explicitly wrapping the comprehension in a `if list != [] do` block (or similar condition).
#### Struct update syntax must be statically proven
Elixir will warn if you use the struct update syntax and it is not statically proven that the given value does not have said struct type. For example:
```elixir
user = find_user_by_id(42)
%User{user | name: "John Doe"}
```
Even though it is guaranteed at runtime that user is always a `User` struct. If the type system cannot prove it, it will emit a typing violation. This is how stuct updates work by design. In such cases, you can address it by matching on the struct when the user variable is defined:
```elixir
%User{} = user = find_user_by_id(42)
%User{user | name: "John Doe"}
```
## Roadmap
@@ -72,6 +72,15 @@ iex> {x, x} = {1, 2}
** (MatchError) no match of right hand side value: {1, 2}
```
However, a variable cannot be defined through itself in the same pattern. This includes cases where the variable appears on both sides of a nested match operator:
```elixir
{:ok, x} = {x, :ok}
%{a: value} = %{b: value}
```
Such patterns are rejected as cyclic variable definitions. You can rewrite them by matching once and then using guards or separate comparisons for the additional constraints.
The underscore variable (`_`) has a special meaning as it can never be bound to any value. It is especially useful when you don't care about certain value in a pattern:
```iex
+5 -1
View File
@@ -3,7 +3,11 @@
# SPDX-FileCopyrightText: 2012 Plataformatec
# Returns config for Elixir docs (exclusively)
canonical = System.fetch_env!("CANONICAL")
canonical =
case System.fetch_env!("CANONICAL") do
"" -> System.version() <> "/"
canonical -> canonical
end
[
search: [
+5 -1
View File
@@ -2,7 +2,11 @@
# SPDX-FileCopyrightText: 2021 The Elixir Team
# Returns config for other apps except Elixir
canonical = System.fetch_env!("CANONICAL")
canonical =
case System.fetch_env!("CANONICAL") do
"" -> System.version() <> "/"
canonical -> canonical
end
[
search: [
+3 -3
View File
@@ -562,16 +562,16 @@ format_error({recursive, Vars, TypeExpr}) ->
Message =
case lists:map(fun({Name, Context}) -> elixir_utils:var_info(Name, Context) end, lists:sort(Vars)) of
[Var] ->
io_lib:format("the variable ~ts is defined in function of itself", [Var]);
io_lib:format("the variable ~ts depends on itself through the pattern", [Var]);
[Var1, Var2] ->
io_lib:format("the variable ~ts is defined recursively in function of ~ts", [Var1, Var2]);
io_lib:format("the variable ~ts depends on ~ts through the pattern", [Var1, Var2]);
[Head | Tail] ->
List = lists:foldl(fun(X, Acc) -> [Acc, $,, $\s, X] end, Head, Tail),
io_lib:format("the following variables form a cycle: ~ts", [List])
end,
io_lib:format(
"recursive variable definition in patterns:~n~n~ts~n~n~ts",
"cyclic variable definition in patterns:~n~n~ts~n~n~ts",
[Code, Message]
);
+17
View File
@@ -59,6 +59,9 @@ handle_call({acquire, Path}, From, Config) ->
handle_call(required, _From, Config) ->
{reply, [F || {F, true} <- maps:to_list(Config#elixir_code_server.required)], Config};
handle_call({release, Path}, _From, Config) ->
{reply, ok, release(Path, Config)};
handle_call(retrieve_compiler_module, _From, Config) ->
case Config#elixir_code_server.mod_pool of
{Used, [Mod | Unused], Counter} ->
@@ -140,6 +143,20 @@ terminate(_Reason, _Config) ->
code_change(_Old, Config, _Extra) ->
{ok, Config}.
release(Path, Config) ->
Current = Config#elixir_code_server.required,
case maps:find(Path, Current) of
{ok, []} ->
Released = maps:remove(Path, Current),
Config#elixir_code_server{required=Released};
{ok, [Next | Waiting]} ->
_ = gen_server:reply(Next, proceed),
Released = maps:put(Path, Waiting, Current),
Config#elixir_code_server{required=Released};
error ->
Config
end.
compiler_module(I) ->
list_to_atom("elixir_compiler_" ++ integer_to_list(I)).
+50 -38
View File
@@ -14,12 +14,15 @@
translate(Meta, Args, S) ->
{Cases, [{do, Expr} | Opts]} = elixir_utils:split_last(Args),
% needs to be the original variables, excluding variables from generators
InitVars = S#elixir_erl.var_names,
case lists:keyfind(reduce, 1, Opts) of
{reduce, Reduce} -> translate_reduce(Meta, Cases, Expr, Reduce, S);
false -> translate_into(Meta, Cases, Expr, Opts, S)
{reduce, Reduce} -> translate_reduce(Meta, Cases, Expr, Reduce, InitVars, S);
false -> translate_into(Meta, Cases, Expr, Opts, InitVars, S)
end.
translate_reduce(Meta, Cases, Expr, Reduce, S) ->
translate_reduce(Meta, Cases, Expr, Reduce, InitVars, S) ->
Ann = ?ann(Meta),
{TReduce, SR} = elixir_erl_pass:translate(Reduce, Ann, S),
{TCases, SC} = translate_gen(Meta, Cases, [], SR),
@@ -30,9 +33,9 @@ translate_reduce(Meta, Cases, Expr, Reduce, S) ->
({'case', CaseAnn, _, CaseBlock}, InnerAcc) -> {'case', CaseAnn, InnerAcc, CaseBlock}
end,
build_reduce(Ann, TCases, InnerFun, TExpr, TReduce, false, SE).
build_reduce(Ann, TCases, InnerFun, TExpr, TReduce, false, InitVars, SE).
translate_into(Meta, Cases, Expr, Opts, S) ->
translate_into(Meta, Cases, Expr, Opts, InitVars, S) ->
Ann = ?ann(Meta),
{TInto, SI} =
@@ -47,8 +50,8 @@ translate_into(Meta, Cases, Expr, Opts, S) ->
{TExpr, SE} = elixir_erl_pass:translate(wrap_expr_if_unused(Expr, TInto), Ann, SC),
case inline_or_into(TInto) of
inline -> build_inline(Ann, TCases, TExpr, TInto, TUniq, SE);
into -> build_into(Ann, TCases, TExpr, TInto, TUniq, SE)
inline -> build_inline(Ann, TCases, TExpr, TInto, TUniq, InitVars, SE);
into -> build_into(Ann, TCases, TExpr, TInto, TUniq, InitVars, SE)
end.
%% In case we have no return, we wrap the expression
@@ -115,29 +118,29 @@ collect_filters([H | T], Acc) ->
collect_filters([], Acc) ->
{Acc, []}.
build_inline(Ann, Clauses, Expr, Into, Uniq, S) ->
build_inline(Ann, Clauses, Expr, Into, Uniq, InitVars, S) ->
case not Uniq and lists:all(fun(Clause) -> element(1, Clause) == bin end, Clauses) of
true -> {build_comprehension(Ann, Clauses, Expr, Into), S};
false -> build_inline_each(Ann, Clauses, Expr, Into, Uniq, S)
false -> build_inline_each(Ann, Clauses, Expr, Into, Uniq, InitVars, S)
end.
build_inline_each(Ann, Clauses, Expr, false, Uniq, S) ->
build_inline_each(Ann, Clauses, Expr, false, Uniq, InitVars, S) ->
InnerFun = fun(InnerExpr, _InnerAcc) -> InnerExpr end,
build_reduce(Ann, Clauses, InnerFun, Expr, {nil, Ann}, Uniq, S);
build_inline_each(Ann, [{enum, _, Left = {var, _, _}, Right, [] = _Filters}], Expr, {nil, _} = _Into, false, S) ->
build_reduce(Ann, Clauses, InnerFun, Expr, {nil, Ann}, Uniq, InitVars, S);
build_inline_each(Ann, [{enum, _, Left = {var, _, _}, Right, [] = _Filters}], Expr, {nil, _} = _Into, false, _InitVars, S) ->
Clauses = [{clause, Ann, [Left], [], [Expr]}],
Args = [Right, {'fun', Ann, {clauses, Clauses}}],
{?remote(Ann, 'Elixir.Enum', map, Args), S};
build_inline_each(Ann, [{enum, _, Left = {var, _, _}, Right, [] = _Filters}], Expr, {map, _, []} = _Into, false, S) ->
build_inline_each(Ann, [{enum, _, Left = {var, _, _}, Right, [] = _Filters}], Expr, {map, _, []} = _Into, false, _InitVars, S) ->
Clauses = [{clause, Ann, [Left], [], [Expr]}],
Args = [Right, {'fun', Ann, {clauses, Clauses}}],
List = ?remote(Ann, 'Elixir.Enum', map, Args),
{?remote(Ann, maps, from_list, [List]), S};
build_inline_each(Ann, Clauses, Expr, {nil, _} = Into, Uniq, S) ->
build_inline_each(Ann, Clauses, Expr, {nil, _} = Into, Uniq, InitVars, S) ->
InnerFun = fun(InnerExpr, InnerAcc) -> {cons, Ann, InnerExpr, InnerAcc} end,
{ReduceExpr, SR} = build_reduce(Ann, Clauses, InnerFun, Expr, Into, Uniq, S),
{ReduceExpr, SR} = build_reduce(Ann, Clauses, InnerFun, Expr, Into, Uniq, InitVars, S),
{?remote(Ann, lists, reverse, [ReduceExpr]), SR};
build_inline_each(Ann, Clauses, Expr, {bin, _, []}, Uniq, S) ->
build_inline_each(Ann, Clauses, Expr, {bin, _, []}, Uniq, InitVars, S) ->
{InnerValue, SV} = build_var(Ann, S),
Generated = erl_anno:set_generated(true, Ann),
@@ -155,17 +158,17 @@ build_inline_each(Ann, Clauses, Expr, {bin, _, []}, Uniq, S) ->
]}
end,
{ReduceExpr, SR} = build_reduce(Ann, Clauses, InnerFun, Expr, {nil, Ann}, Uniq, SV),
{ReduceExpr, SR} = build_reduce(Ann, Clauses, InnerFun, Expr, {nil, Ann}, Uniq, InitVars, SV),
{?remote(Ann, erlang, list_to_bitstring, [ReduceExpr]), SR}.
build_into(Ann, Clauses, Expr, {map, _, []}, Uniq, S) ->
{ReduceExpr, SR} = build_inline_each(Ann, Clauses, Expr, {nil, Ann}, Uniq, S),
build_into(Ann, Clauses, Expr, {map, _, []}, Uniq, InitVars, S) ->
{ReduceExpr, SR} = build_inline_each(Ann, Clauses, Expr, {nil, Ann}, Uniq, InitVars, S),
{?remote(Ann, maps, from_list, [ReduceExpr]), SR};
build_into(Ann, Clauses, Expr, ?empty_map_set_pattern = _Into, Uniq, S) ->
build_into(Ann, Clauses, Expr, ?empty_map_set_pattern = _Into, Uniq, InitVars, S) ->
InnerFun = fun(InnerExpr, InnerAcc) -> {cons, Ann, InnerExpr, InnerAcc} end,
{ReduceExpr, SR} = build_reduce(Ann, Clauses, InnerFun, Expr, {nil, Ann}, Uniq, S),
{ReduceExpr, SR} = build_reduce(Ann, Clauses, InnerFun, Expr, {nil, Ann}, Uniq, InitVars, S),
{?remote(Ann, 'Elixir.MapSet', new, [ReduceExpr]), SR};
build_into(Ann, Clauses, Expr, Into, Uniq, S) ->
build_into(Ann, Clauses, Expr, Into, Uniq, InitVars, S) ->
{Fun, SF} = build_var(Ann, S),
{Acc, SA} = build_var(Ann, SF),
{Kind, SK} = build_var(Ann, SA),
@@ -182,7 +185,7 @@ build_into(Ann, Clauses, Expr, Into, Uniq, S) ->
?remote(Ann, 'Elixir.Collectable', into, [Into])
},
{IntoReduceExpr, SN} = build_reduce(Ann, Clauses, InnerFun, Expr, Acc, Uniq, SD),
{IntoReduceExpr, SN} = build_reduce(Ann, Clauses, InnerFun, Expr, Acc, Uniq, InitVars, SD),
TryExpr =
{'try', Ann,
@@ -205,10 +208,10 @@ stacktrace_clause(Ann, Fun, Acc, Kind, Reason, Stack) ->
%% Helpers
build_reduce(Ann, Clauses, InnerFun, Expr, Into, false, S) ->
build_reduce(Ann, Clauses, InnerFun, Expr, Into, false, InitVars, S) ->
{Acc, SA} = build_var(Ann, S),
{build_reduce_each(Clauses, InnerFun(Expr, Acc), Into, Acc, SA), SA};
build_reduce(Ann, Clauses, InnerFun, Expr, Into, true, S) ->
{build_reduce_each(Clauses, InnerFun(Expr, Acc), Into, Acc, InitVars, SA), SA};
build_reduce(Ann, Clauses, InnerFun, Expr, Into, true, InitVars, S) ->
%% Those variables are used only inside the anonymous function
%% so we don't need to worry about returning the scope.
{Acc, SA} = build_var(Ann, S),
@@ -229,12 +232,12 @@ build_reduce(Ann, Clauses, InnerFun, Expr, Into, true, S) ->
]}
]},
EnumReduceCall = build_reduce_each(Clauses, InnerExpr, NewInto, Acc, SU),
EnumReduceCall = build_reduce_each(Clauses, InnerExpr, NewInto, Acc, InitVars, SU),
{?remote(Ann, erlang, element, [{integer, Ann, 1}, EnumReduceCall]), SU}.
build_reduce_each([{enum, Meta, Left, Right, Filters} | T], Expr, Arg, Acc, S) ->
build_reduce_each([{enum, Meta, Left, Right, Filters} | T], Expr, Arg, Acc, InitVars, S) ->
Ann = ?ann(Meta),
True = build_reduce_each(T, Expr, Acc, Acc, S),
True = build_reduce_each(T, Expr, Acc, Acc, InitVars, S),
False = Acc,
Generated = erl_anno:set_generated(true, Ann),
@@ -255,13 +258,13 @@ build_reduce_each([{enum, Meta, Left, Right, Filters} | T], Expr, Arg, Acc, S) -
Args = [Right, Arg, {'fun', Ann, {clauses, Clauses1}}],
?remote(Ann, 'Elixir.Enum', reduce, Args);
build_reduce_each([{bin, Meta, Left, Right, Filters} | T], Expr, Arg, Acc, S) ->
build_reduce_each([{bin, Meta, Left, Right, Filters} | T], Expr, Arg, Acc, InitVars, S) ->
Ann = ?ann(Meta),
Generated = erl_anno:set_generated(true, Ann),
{Tail, ST} = build_var(Ann, S),
{Fun, SF} = build_var(Ann, ST),
True = build_reduce_each(T, Expr, Acc, Acc, SF),
True = build_reduce_each(T, Expr, Acc, Acc, InitVars, SF),
False = Acc,
{bin, _, Elements} = Left,
TailElement = {bin_element, Ann, Tail, default, [bitstring]},
@@ -275,7 +278,7 @@ build_reduce_each([{bin, Meta, Left, Right, Filters} | T], Expr, Arg, Acc, S) ->
[?remote(Ann, erlang, error, [pair(Ann, badarg, Tail)])]}],
NoVarClauses =
case no_var(Generated, Elements) of
case no_unbound_var(Generated, Elements, InitVars) of
error ->
Clauses;
@@ -294,7 +297,7 @@ build_reduce_each([{bin, Meta, Left, Right, Filters} | T], Expr, Arg, Acc, S) ->
{named_fun, Ann, element(3, Fun), VarClauses},
[Right, Arg]};
build_reduce_each([], Expr, _Arg, _Acc, _S) ->
build_reduce_each([], Expr, _Arg, _Acc, _InitVars, _S) ->
Expr.
is_var({var, _, _}) -> true;
@@ -307,9 +310,10 @@ build_var(Ann, S) ->
{Name, ST} = elixir_erl_var:build('_', S),
{{var, Ann, Name}, ST}.
no_var(ParentAnn, Elements) ->
no_unbound_var(ParentAnn, Elements, InitVars) ->
Vars = #{V => K || K := V <- InitVars},
try
[{bin_element, Ann, NoVarExpr, no_var_size(Size), Types} ||
[{bin_element, Ann, NoVarExpr, no_unbound_var_size(Size, Vars), Types} ||
{bin_element, Ann, Expr, Size, Types} <- Elements,
NoVarExpr <- no_var_expr(ParentAnn, Expr)]
catch
@@ -319,9 +323,17 @@ no_var(ParentAnn, Elements) ->
no_var_expr(Ann, {string, _, String}) -> [{var, Ann, '_'} || _ <- String];
no_var_expr(Ann, _) -> [{var, Ann, '_'}].
no_var_size(default) -> default;
no_var_size(Size) when is_integer(Size) -> Size;
no_var_size(_) -> throw(unbound_size).
no_unbound_var_size(Size, Vars) ->
valid_var_size(Size, Vars) orelse throw(unbound_size),
Size.
valid_var_size({var, _, Var}, Vars) when is_map_key(Var, Vars) -> true;
valid_var_size(default, _Vars) -> true;
valid_var_size({integer, _, _}, _Vars) -> true;
valid_var_size(Size, _Vars) when is_integer(Size) -> true;
valid_var_size({op, _Ann, _Op, Left, Right}, Vars) ->
valid_var_size(Left, Vars) andalso valid_var_size(Right, Vars);
valid_var_size(_Size, _vars) -> false.
build_comprehension(Ann, Clauses, Expr, Into) ->
{comprehension_kind(Into), Ann, Expr, comprehension_clause(Clauses)}.
+5 -1
View File
@@ -576,6 +576,10 @@ translate_remote('Elixir.String.Chars', to_string, Meta, [Arg], S) ->
{clause, Generated, [Var], [[Guard]], [Fast]},
{clause, Generated, [Var], [], [Slow]}
]}, VS};
translate_remote(lists, member, Meta, [Expr, []], S) ->
Ann = ?ann(Meta),
{TExpr, S1} = translate(Expr, Ann, S),
{{block, Ann, [{match, Ann, {var, Ann, '_'}, TExpr}, {atom, Ann, false}]}, S1};
translate_remote(lists, member, Meta, [Expr, [Head | Tail] = List], S) ->
Ann = ?ann(Meta),
@@ -713,7 +717,7 @@ rewrite_strategy(Left, Right, Args) ->
).
?reorder('Elixir.Kernel', elem, 2, [Tuple, Index], erlang, element, [increment(Ann, Index), Tuple]);
?reorder('Elixir.Kernel', put_elem, 2, [Tuple, Index, Term], erlang, element, [increment(Ann, Index), Tuple, Term]);
?reorder('Elixir.Kernel', put_elem, 3, [Tuple, Index, Term], erlang, setelement, [increment(Ann, Index), Tuple, Term]);
?reorder('Elixir.Kernel', is_map_key, 2, [Map, Key], erlang, is_map_key, [Key, Map]);
?reorder('Elixir.Map', delete, 2, [Map, Key], maps, remove, [Key, Map]);
?reorder('Elixir.Map', fetch, 2, [Map, Key], maps, find, [Key, Map]);
+17 -6
View File
@@ -266,6 +266,9 @@ expand({quote, Meta, [Opts, Do]}, S, E) when is_list(Do) ->
Unquote = proplists:get_value(unquote, EOpts, DefaultUnquote),
Generated = proplists:get_value(generated, EOpts, false),
(map_get(context, E) /= nil) andalso Unquote andalso elixir_quote:has_unquotes(Exprs) andalso
file_error(Meta, E, ?MODULE, quote_in_pattern_with_unquote),
{Q, QContext, QPrelude} = elixir_quote:build(Meta, Line, File, Context, Unquote, Generated, ET),
{EPrelude, SP, EP} = expand(QPrelude, ST, ET),
{EContext, SC, EC} = expand(QContext, SP, EP),
@@ -287,9 +290,15 @@ expand({quote, Meta, [Opts, Do]}, S, E) when is_list(Do) ->
_ -> {'{}', [], ['__block__', [], EBinding ++ [EQuoted]]}
end,
case EPrelude of
[] -> {EBindingQuoted, ES, EQ};
_ -> {{'__block__', [], EPrelude ++ [EBindingQuoted]}, ES, EQ}
EBlock =
case EPrelude of
[] -> EBindingQuoted;
_ -> {'__block__', [], EPrelude ++ [EBindingQuoted]}
end,
case ?key(E, context) of
nil -> {{{'.', Meta, [elixir_quote, validate_quote]}, Meta, [EBlock]}, ES, EQ};
_ -> {EBlock, ES, EQ}
end;
expand({quote, Meta, [_, _]}, _S, E) ->
@@ -964,7 +973,7 @@ expand_remote(Receiver, DotMeta, Right, Meta, Args, S, SL, #{context := Context}
{EArgs, {SA, _}, EA} = mapfold(fun expand_arg/3, {SL, S}, E, Args),
SA#elixir_ex.tainted_function andalso is_atom(Receiver) andalso
(not is_loaded_and_exported(Receiver, Right, Args)) andalso
is_loaded_and_not_exported(Receiver, Right, Args) andalso
elixir_errors:file_warn(Meta, E, ?MODULE, {undefined_function, Receiver, Right, length(Args)}),
Rewritten = elixir_rewrite:rewrite(Receiver, DotMeta, Right, AttachedMeta, EArgs),
@@ -987,9 +996,9 @@ expand_remote(Receiver, DotMeta, Right, Meta, Args, _, _, E) ->
Call = {{'.', DotMeta, [Receiver, Right]}, Meta, Args},
file_error(Meta, E, ?MODULE, {invalid_call, Call}).
is_loaded_and_exported(Receiver, Fun, Args) ->
is_loaded_and_not_exported(Receiver, Fun, Args) ->
(code:ensure_loaded(Receiver) =:= {module, Receiver}) andalso
erlang:function_exported(Receiver, Fun, length(Args)).
not erlang:function_exported(Receiver, Fun, length(Args)).
attach_runtime_module(Receiver, Meta, S, _E) ->
case lists:member(Receiver, S#elixir_ex.runtime_modules) of
@@ -1210,6 +1219,8 @@ format_error({expected_compile_time_module, Kind, GivenTerm}) ->
format_error({unquote_outside_quote, Unquote}) ->
%% Unquote can be "unquote" or "unquote_splicing".
io_lib:format("~p called outside quote", [Unquote]);
format_error(quote_in_pattern_with_unquote) ->
"unquote is not allowed when quote is used inside a pattern or guard";
format_error({invalid_bind_quoted_for_quote, BQ}) ->
io_lib:format("invalid :bind_quoted for quote, expected a keyword list of variable names, got: ~ts",
['Elixir.Macro':to_string(BQ)]);
+43 -32
View File
@@ -7,7 +7,9 @@
-feature(maybe_expr, enable).
-export([escape/3, linify/3, linify_with_context_counter/3, build/7, quote/2, has_unquotes/1, fun_to_quoted/1]).
-export([dot/5, tail_list/3, list/2, validate_runtime/2, shallow_validate_ast/1]). %% Quote callbacks
%% Quote callbacks (appear in code, must be handled by the type system)
-export([dot/5, tail_list/3, list/2, unquote/1, validate_quote/1, validate_runtime/2, shallow_validate_ast/1]).
-include("elixir.hrl").
-define(defs(Kind), Kind == def; Kind == defp; Kind == defmacro; Kind == defmacrop; Kind == '@').
@@ -23,7 +25,7 @@
imports_hygiene=nil,
unquote=true,
generated=false,
shallow_validate=false
validate=false
}).
%% fun_to_quoted
@@ -273,11 +275,14 @@ build(Meta, Line, File, Context, Unquote, Generated, E) ->
unquote=Unquote,
context=VContext,
generated=Generated,
shallow_validate=true
validate=true
},
{Q, VContext, Acc3}.
validate_quote(Expr) ->
Expr.
validate_compile(_Meta, line, Value, Acc) when is_boolean(Value) ->
{Value, Acc};
validate_compile(_Meta, file, nil, Acc) ->
@@ -312,27 +317,6 @@ is_valid(context, Context) -> is_atom(Context) andalso (Context /= nil);
is_valid(generated, Generated) -> is_boolean(Generated);
is_valid(unquote, Unquote) -> is_boolean(Unquote).
shallow_validate_ast(Expr) ->
case shallow_valid_ast(Expr) of
true -> Expr;
false -> argument_error(
<<"tried to unquote invalid AST: ", ('Elixir.Kernel':inspect(Expr))/binary,
"\nDid you forget to escape term using Macro.escape/1?">>)
end.
shallow_valid_ast(Expr) when is_list(Expr) -> valid_ast_list(Expr);
shallow_valid_ast(Expr) -> valid_ast_elem(Expr).
valid_ast_list([]) -> true;
valid_ast_list([Head | Tail]) -> valid_ast_elem(Head) andalso valid_ast_list(Tail);
valid_ast_list(_Improper) -> false.
valid_ast_elem(Expr) when is_list(Expr); is_atom(Expr); is_binary(Expr); is_number(Expr); is_pid(Expr); is_function(Expr) -> true;
valid_ast_elem({Left, Right}) -> valid_ast_elem(Left) andalso valid_ast_elem(Right);
valid_ast_elem({Atom, Meta, Args}) when is_atom(Atom), is_list(Meta), is_atom(Args) orelse is_list(Args) -> true;
valid_ast_elem({Call, Meta, Args}) when is_list(Meta), is_list(Args) -> shallow_valid_ast(Call);
valid_ast_elem(_Term) -> false.
quote({unquote_splicing, _, [_]}, #elixir_quote{unquote=true}) ->
argument_error(<<"unquote_splicing only works inside arguments and block contexts, "
"wrap it in parens if you want it to work with one-liners">>);
@@ -362,9 +346,9 @@ do_quote({quote, Meta, [Opts, Arg]}, Q) when is_list(Meta) ->
{'{}', [], [quote, meta(NewMeta, Q), [TOpts, TArg]]};
do_quote({unquote, Meta, [Expr]}, #elixir_quote{unquote=true, shallow_validate=Validate}) when is_list(Meta) ->
do_quote({unquote, Meta, [Expr]}, #elixir_quote{unquote=true, validate=Validate}) when is_list(Meta) ->
case Validate of
true -> {{'.', Meta, [?MODULE, shallow_validate_ast]}, Meta, [Expr]};
true -> {{'.', Meta, [?MODULE, unquote]}, Meta, [Expr]};
false -> Expr
end;
@@ -490,7 +474,7 @@ collect_trace_import_quoted([], _Mod, Acc, Arities) ->
do_quote_call(Left, Meta, Expr, Args, Q) ->
All = [Left, {unquote, Meta, [Expr]}, Args, Q#elixir_quote.context],
TAll = [do_quote(X, Q) || X <- All],
{{'.', Meta, [elixir_quote, dot]}, Meta, [meta(Meta, Q) | TAll]}.
{{'.', Meta, [?MODULE, dot]}, Meta, [meta(Meta, Q) | TAll]}.
do_quote_tuple({Left, Meta, Right}, Q) ->
do_quote_tuple(Left, Meta, Right, Q).
@@ -533,12 +517,14 @@ do_list_concat([], Right) -> Right;
do_list_concat(Left, Right) -> {{'.', [], [erlang, '++']}, [], [Left, Right]}.
do_runtime_list(Meta, Fun, Args) ->
{{'.', Meta, [elixir_quote, Fun]}, Meta, Args}.
{{'.', Meta, [?MODULE, Fun]}, Meta, Args}.
%% Callbacks
%% Unquote validation callbacks
%%
%% They perform shallow runtime validation for performance
%% reasons but will be type checked in the future for full
%% validation.
%% Some expressions cannot be unquoted at compilation time.
%% This function is responsible for doing runtime unquoting.
dot(Meta, Left, Right, Args, Context) ->
annotate(dot(Meta, Left, Right, Args), Context).
@@ -589,12 +575,37 @@ tail_list(Left, Right, Tail) when is_list(Left) ->
end.
validate_list(List) ->
case valid_ast_list(List) of
case shallow_valid_list(List) of
true -> ok;
false -> argument_error(<<"expected a list with quoted expressions in unquote_splicing/1, got: ",
('Elixir.Kernel':inspect(List))/binary>>)
end.
unquote(Expr) ->
case shallow_valid_ast(Expr) of
true -> Expr;
false -> argument_error(
<<"tried to unquote invalid AST: ", ('Elixir.Kernel':inspect(Expr))/binary,
"\nDid you forget to escape term using Macro.escape/1?">>)
end.
shallow_valid_ast(Expr) when is_list(Expr) -> shallow_valid_list(Expr);
shallow_valid_ast(Expr) -> shallow_valid_elem(Expr).
shallow_valid_list([]) -> true;
shallow_valid_list([Head | Tail]) -> shallow_valid_elem(Head) andalso shallow_valid_list(Tail);
shallow_valid_list(_Improper) -> false.
shallow_valid_elem(Expr) when is_list(Expr); is_atom(Expr); is_binary(Expr); is_number(Expr); is_pid(Expr); is_function(Expr) -> true;
shallow_valid_elem({Left, Right}) -> shallow_valid_elem(Left) andalso shallow_valid_elem(Right);
shallow_valid_elem({Atom, Meta, Args}) when is_atom(Atom), is_list(Meta), is_atom(Args) orelse is_list(Args) -> true;
shallow_valid_elem({Call, Meta, Args}) when is_list(Meta), is_list(Args) -> shallow_valid_ast(Call);
shallow_valid_elem(_Term) -> false.
%% TODO: We keep this with backwards compatibility in previous compiled Elixirw code.
shallow_validate_ast(Expr) ->
unquote(Expr).
argument_error(Message) ->
error('Elixir.ArgumentError':exception([{message, Message}])).
+1 -1
View File
@@ -1438,7 +1438,7 @@ suggest_simpler_unexpected_token_in_error(Wrong, Line, WrongColumn, Scope) ->
{error, _Reason} ->
ConfusableSkeleton = 'Elixir.String.Tokenizer.Security':confusable_skeleton(Wrong),
case (Scope#elixir_tokenizer.identifier_tokenizer):tokenize(ConfusableSkeleton) of
{_, Simpler, _, _, _, _} ->
{_, Simpler, _, _, _, _} when Simpler =/= Wrong ->
Message = suggest_change("Codepoint failed identifier tokenization, but a simpler form was found.",
Wrong,
"You could write the above in a similar way that is accepted by Elixir:",
+3
View File
@@ -233,6 +233,9 @@ returns_boolean({{'.', _, [erlang, Fun]}, _, [_, _]}) when
returns_boolean({{'.', _, [erlang, Fun]}, _, [_, _, _]}) when
Fun == function_exported; Fun == is_record -> true;
returns_boolean({{'.', _, [lists, member]}, _, [_, _]}) ->
true;
returns_boolean({'case', _, [_, [{do, Clauses}]]}) ->
lists:all(fun
({'->', _, [_, Expr]}) -> returns_boolean(Expr)
+26
View File
@@ -340,6 +340,32 @@ defmodule CalendarTest do
assert Calendar.strftime(~N[2019-08-15 17:07:57], "%010A") == "00Thursday"
end
test "limits width to at most 1024 characters" do
assert Calendar.strftime(~D[2019-08-15], "%1024d") |> byte_size() == 1024
assert_raise ArgumentError, "invalid strftime format: width must be at most 1024", fn ->
Calendar.strftime(~D[2019-08-15], "%1025d")
end
assert_raise ArgumentError, "invalid strftime format: width must be at most 1024", fn ->
Calendar.strftime(~D[2019-08-15], "%10000d")
end
end
test "limits width in preferred formats" do
assert_raise ArgumentError, "invalid strftime format: width must be at most 1024", fn ->
Calendar.strftime(~N[2019-08-15 17:07:57], "%c", preferred_datetime: "%1025d")
end
assert_raise ArgumentError, "invalid strftime format: width must be at most 1024", fn ->
Calendar.strftime(~N[2019-08-15 17:07:57], "%x", preferred_date: "%1025d")
end
assert_raise ArgumentError, "invalid strftime format: width must be at most 1024", fn ->
Calendar.strftime(~N[2019-08-15 17:07:57], "%X", preferred_time: "%1025H")
end
end
test "formats Epoch time with %s" do
assert Calendar.strftime(~N[2019-08-15 17:07:57], "%s") == "1565888877"
+19
View File
@@ -514,6 +514,25 @@ defmodule CodeTest do
Code.unrequire_files([fixture_path("code_sample.exs")])
end
test "require_file/1 releases the file when compilation fails" do
path = tmp_path("bad_require_#{System.unique_integer([:positive])}.ex")
try do
File.write!(path, ~s|raise "boom"|)
assert_raise RuntimeError, "boom", fn ->
Code.require_file(path)
end
assert_raise RuntimeError, "boom", fn ->
Code.require_file(path)
end
after
File.rm(path)
Code.unrequire_files([path])
end
end
test "string_to_quoted!/2 errors take lines/columns/indentation into account" do
assert_exception(
SyntaxError,
@@ -520,6 +520,33 @@ defmodule Kernel.ComprehensionTest do
assert for(<<s, x::size(s * 8) <- bin>>, into: %{}, do: {s, x}) == %{1 => 1, 2 => 515}
end
test "binary for comprehensions with chunk matching" do
bin = <<0, 1, 255, 2, 0, 3, 0, 1>>
# static sizes
assert for(<<0::8, x::8 <- bin>>, do: x) == [1, 3, 1]
assert for(<<0::8, x::8 <- bin>>, uniq: true, do: x) == [1, 3]
assert for(<<0::8, x::8 <- bin>>, into: "", do: <<x>>) == <<1, 3, 1>>
assert for(<<0::8, x::8 <- bin>>, into: %{}, do: {x, x}) == %{1 => 1, 3 => 3}
# size from pinned variable
s = 8
assert for(<<0::size(^s), x::size(^s) <- bin>>, do: x) == [1, 3, 1]
assert for(<<0::size(^s), x::size(^s) <- bin>>, uniq: true, do: x) == [1, 3]
assert for(<<0::size(^s), x::size(^s) <- bin>>, into: "", do: <<x>>) == <<1, 3, 1>>
assert for(<<0::size(^s), x::size(^s) <- bin>>, into: %{}, do: {x, x}) == %{1 => 1, 3 => 3}
# operation using fixed integers and pinned variables
assert for(<<0::size(^s * 1), x::size(^s * 1) <- bin>>, do: x) == [1, 3, 1]
assert for(<<0::size(^s * 1), x::size(^s * 1) <- bin>>, uniq: true, do: x) == [1, 3]
assert for(<<0::size(^s * 1), x::size(^s * 1) <- bin>>, into: "", do: <<x>>) == <<1, 3, 1>>
# nested generators
assert for(b <- [bin], <<0::size(^s), x::size(^s) <- b>>, do: x) == [1, 3, 1]
assert for(b <- [bin], <<0::size(^s), x::size(^s) <- b>>, uniq: true, do: x) == [1, 3]
assert for(b <- [bin], <<0::size(^s), x::size(^s) <- b>>, into: "", do: <<x>>) == <<1, 3, 1>>
end
test "binary for comprehensions where value is not used" do
bin = <<1, 2, 3>>
+18 -19
View File
@@ -161,23 +161,7 @@ defmodule Kernel.ErrorsTest do
test "cascading from undefined variables" do
# Test that we show undefined modules/functions/macros on variable failure,
# as sometimes the variable failure come from a missing module or require
assert_compile_error(
[
"nofile:3:23",
"undefined variable \"bar\"",
"nofile:3:19",
"function UnknownModule.foo/1 is undefined (module UnknownModule is not available)"
],
~c"""
defmodule Sample do
def foo do
UnknownModule.foo(bar)
end
end
"""
)
# as sometimes the variable failure come from a missing require/export
assert_compile_error(
[
"nofile:3:20",
@@ -212,11 +196,11 @@ defmodule Kernel.ErrorsTest do
)
end
test "recursive variables on definition" do
test "cyclic variables on definition" do
assert_compile_error(
[
"nofile:2:7: ",
"recursive variable definition in patterns:",
"cyclic variable definition in patterns:",
"foo(x = y, y = z, z = x)",
"the following variables form a cycle: \"x\", \"y\", \"z\""
],
@@ -472,6 +456,21 @@ defmodule Kernel.ErrorsTest do
"""
end
test "invalid unquote when quote/1 is in a pattern" do
assert_compile_error(
["unquote is not allowed when quote is used inside a pattern or guard"],
~c"""
defmodule Kernel.ErrorsTest.InvalidUnquoteInQuotePattern do
def my_fun(ast) do
case ast do
quote(do: foo(unquote(x))) -> x
end
end
end
"""
)
end
test "invalid attribute" do
msg = ~r"cannot inject attribute @foo into function/macro because cannot escape "
@@ -199,43 +199,43 @@ defmodule Kernel.ExpansionTest do
assert Macro.Env.vars(env) == []
end
test "errors on directly recursive definitions" do
test "errors on directly cyclic definitions" do
assert_compile_error(
~r"""
recursive variable definition in patterns:
cyclic variable definition in patterns:
\{x = \{:ok, x\}\}
the variable "x" \(context Kernel.ExpansionTest\) is defined in function of itself
the variable "x" \(context Kernel.ExpansionTest\) depends on itself through the pattern
""",
fn -> expand(quote(do: {x = {:ok, x}} = :ok)) end
)
assert_compile_error(
~r"""
recursive variable definition in patterns:
cyclic variable definition in patterns:
\{\{x, y\} = \{y, x\}\}
the variable "x" \(context Kernel.ExpansionTest\) is defined in function of itself
the variable "x" \(context Kernel.ExpansionTest\) depends on itself through the pattern
""",
fn -> expand(quote(do: {{x, y} = {y, x}} = :ok)) end
)
assert_compile_error(
~r"""
recursive variable definition in patterns:
cyclic variable definition in patterns:
\{\{:x, y\} = \{x, :y\}, x = y\}
the variable "x" \(context Kernel.ExpansionTest\) is defined recursively in function of "y" \(context Kernel.ExpansionTest\)
the variable "x" \(context Kernel.ExpansionTest\) depends on "y" \(context Kernel.ExpansionTest\) through the pattern
""",
fn -> expand(quote(do: {{:x, y} = {x, :y}, x = y} = :ok)) end
)
assert_compile_error(
~r"""
recursive variable definition in patterns:
cyclic variable definition in patterns:
\{x = y, y = z, z = x\}
@@ -245,7 +245,7 @@ defmodule Kernel.ExpansionTest do
)
end
test "complex recursive variable definitions" do
test "complex cyclic variable definitions" do
assert expand(
quote do:
{%{type: type, client_id: client_id} = message,
@@ -253,7 +253,7 @@ defmodule Kernel.ExpansionTest do
)
assert_compile_error(
~r"recursive variable definition in patterns",
~r"cyclic variable definition in patterns",
fn ->
expand(
quote do:
@@ -679,7 +679,8 @@ defmodule Kernel.ExpansionTest do
describe "quote" do
test "expanded to raw forms" do
assert expand(quote(do: quote(do: hello)), []) == {:{}, [], [:hello, [], __MODULE__]}
assert {{:., _, [:elixir_quote, :validate_quote]}, _, [{:{}, [], [:hello, [], __MODULE__]}]} =
expand(quote(do: quote(do: hello)), [])
end
test "raises if the :bind_quoted option is invalid" do
+1 -24
View File
@@ -494,30 +494,7 @@ defmodule Kernel.GuardTest do
assert expand_defguard_to_string(:with_or_and_or, args, nil) == """
{arg1, arg2, arg3} = {1 + 1, 2 + 2, 3 + 3}
case arg1 do
false ->
case arg2 do
false ->
false
true ->
case arg3 do
false -> arg1
true -> true
other -> :erlang.error({:badbool, :or, other})
end
other ->
:erlang.error({:badbool, :and, other})
end
true ->
true
other ->
:erlang.error({:badbool, :or, other})
end
:erlang.orelse(arg1, :erlang.andalso(arg2, :erlang.orelse(arg3, arg1)))
"""
end
@@ -57,6 +57,7 @@ defmodule Kernel.ParallelCompilerTest do
~r"\[profile\] [\s\d]{6}ms compiling \+ 0ms waiting while compiling .*tmp/profile_time/bar.ex"
assert profile =~ ~r"\[profile\] Finished compilation cycle of 1 modules in \d+ms"
assert profile =~ ~r"\[profile\] Type checked HelloWorld in \d+ms"
assert profile =~ ~r"\[profile\] Finished group pass check of 1 modules in \d+ms"
after
purge([HelloWorld])
@@ -1353,6 +1353,12 @@ defmodule Kernel.ParserTest do
]
assert_syntax_error(message, ~c"fooی𝚳")
# regression test: ǜ (should not suggest back the wrong character)
assert_syntax_error(
["nofile:1:4:", ~s/unexpected token: "#{"\u01DC"}" (column 4, code point U+01DC)/],
~c":fooǜ"
)
end
test "keyword missing space" do
+6 -1
View File
@@ -461,6 +461,11 @@ defmodule KernelTest do
refute 2 in []
refute false in []
refute true in []
# make sure optimization still evaluates the left-hand side
# (do not use assert/refute which handle in/2 differently)
send(self(), :foo) in []
assert_received :foo
end
test "with expressions on right side" do
@@ -697,7 +702,7 @@ defmodule KernelTest do
"""
# Empty list
assert expand_to_string(quote(do: :x in [])) =~ "_ = :x\nfalse"
assert expand_to_string(quote(do: :x in [])) =~ ":lists.member(:x, [])"
assert expand_to_string(quote(do: :x in []), :guard) == "false"
# Lists
+1 -1
View File
@@ -75,7 +75,7 @@ defmodule Macro.EnvTest do
test "to_match/1" do
quote = quote(do: x in [])
assert {:__block__, [], [{:=, [], [{:_, [], Kernel}, {:x, [], Macro.EnvTest}]}, false]} =
assert {{:., [], [:lists, :member]}, [], [{:x, [], Macro.EnvTest}, []]} =
Macro.expand_once(quote, __ENV__)
assert Macro.expand_once(quote, Macro.Env.to_match(__ENV__)) == false
@@ -15,6 +15,7 @@ defmodule Module.Types.DescrTest do
use ExUnit.Case, async: true
import Module.Types.Descr
doctest Module.Types.Descr, import: true
defmacro domain_key(arg) when is_atom(arg), do: [arg]
defp number(), do: union(integer(), float())
@@ -104,6 +105,14 @@ defmodule Module.Types.DescrTest do
refute equal?(union(term(), dynamic(if_set(integer()))), dynamic(union(term(), not_set())))
end
test "optional" do
# Test that union preserves optional properties
t = union(term(), dynamic(not_set()))
assert subtype?(term(), t)
assert subtype?(dynamic(), t)
assert subtype?(dynamic(integer()), t)
end
test "tuple" do
assert equal?(union(tuple(), tuple()), tuple())
@@ -582,7 +591,18 @@ defmodule Module.Types.DescrTest do
assert equal?(dynamic(), difference(term(), dynamic()))
assert empty?(difference(dynamic(), term()))
assert empty?(difference(none(), dynamic()))
assert empty?(difference(dynamic(integer()), integer()))
assert difference(dynamic(integer()), integer()) == none()
# Covers assembling a result with static :term and dynamic unfolded term.
assert difference(term(), %{dynamic: none(), optional: 1}) == term()
end
test "optional" do
# Test that difference preserves optional properties
x = if_set(dynamic(integer()))
term_map = union(integer(), difference(term(), integer()))
assert equal?(term_map, term())
assert equal?(difference(x, term()), difference(x, term_map))
end
test "tuple" do
@@ -674,6 +694,27 @@ defmodule Module.Types.DescrTest do
assert difference(closed_map(a: integer()), open_map(b: if_set(integer()))) == none()
end
test "map difference of subtype" do
for {k1, k2} <- [{:a, :b}, {:b, :a}, {:b, :c}, {:c, :a}, {:c, :b}, {:b, :d}] do
a = closed_map([{k1, atom([:x, :y])}, {k2, pid()}])
b =
closed_map([
{k1, if_set(union(atom([:y]), float()))},
{k2, union(pid(), union(binary(), integer()))}
])
c = open_map([{k2, if_set(union(pid(), binary()))}])
# a <= c, while b is unrelated to both a and c.
# Therefore (a \ b) <= a <= c and (a \ b) \ c is empty.
ab = difference(a, b)
assert empty?(difference(ab, c)),
"difference failed for keys #{inspect({k1, k2})}"
end
end
test "map double negation with redundant empty map" do
type = closed_map(a: atom()) |> union(open_map(a: if_set(integer()))) |> union(empty_map())
@@ -893,6 +934,7 @@ defmodule Module.Types.DescrTest do
{closed_map([{domain_key(:integer), gradual}]),
closed_map([{domain_key(:integer), static}]),
closed_map([{domain_key(:integer), upper_bound}])},
{list(dynamic()), empty_list(), list(term())},
{non_empty_list(gradual), non_empty_list(static), non_empty_list(upper_bound)},
{non_empty_list(head, gradual), non_empty_list(head, static),
non_empty_list(head, upper_bound)}
@@ -963,6 +1005,13 @@ defmodule Module.Types.DescrTest do
assert subtype?(t2, t1)
# An open map is open in every domain key, including :bitstring.
open_pid = open_map([{domain_key(:pid), pid()}])
for d <- [:bitstring, :binary, :integer, :float, :atom, :tuple, :map, :list] do
assert subtype?(closed_map([{[d], atom([:x])}]), open_pid)
end
t1_minus_t2 = difference(t1, t2)
refute empty?(t1_minus_t2)
@@ -1092,6 +1141,21 @@ defmodule Module.Types.DescrTest do
end
end
describe "compatible_intersection" do
test "none" do
refute compatible?(none(), term())
assert compatible_intersection(none(), term()) == {:error, none()}
refute compatible?(dynamic(none()), term())
assert compatible_intersection(dynamic(none()), term()) == {:error, dynamic(none())}
end
test "dynamic" do
assert compatible_intersection(integer(), term()) == {:ok, integer()}
assert compatible_intersection(dynamic(integer()), term()) == {:ok, dynamic(integer())}
end
end
describe "empty?" do
test "tuple" do
assert tuple([none()]) |> empty?()
@@ -1197,6 +1261,7 @@ defmodule Module.Types.DescrTest do
test "non funs" do
assert fun_apply(term(), [integer()]) == :badfun
assert fun_apply(integer(), [integer()]) == :badfun
assert fun_apply(none(), [integer()]) == :badfun
assert fun_apply(union(integer(), none_fun(1)), [integer()]) == :badfun
assert fun_apply(union(integer(), fun([integer()], atom())), [integer()]) == :badfun
assert fun_apply(union(integer(), dynamic()), [integer()]) == :badfun
@@ -1250,6 +1315,28 @@ defmodule Module.Types.DescrTest do
assert fun_apply(fun_mixed, [integer()]) == {:badarity, [1, 2]}
assert fun_apply(fun_mixed, [integer(), atom()]) == {:badarity, [2, 1]}
# A function that is empty at the called arity is :badfun, not a
# self-contradictory {:badarity, [called_arity]}.
empty_fun =
difference(fun([union(integer(), float())], atom()), fun([integer()], atom()))
assert empty?(empty_fun)
assert fun_apply(empty_fun, [integer()]) == :badfun
# When the function is empty at the called arity but usable at another, the
# badarity must list only the *other* arity, never the (empty) called one.
# `empty_fun` keeps a structured empty arity-1 entry, which union preserves,
# so `usable_at_2` is equal? to a plain 2-arity function yet carries it.
usable_at_2 = union(empty_fun, fun([integer(), atom()], boolean()))
refute empty?(usable_at_2)
assert equal?(usable_at_2, fun([integer(), atom()], boolean()))
# Congruent with the normalized form, and excludes the empty arity 1.
assert fun_apply(usable_at_2, [integer()]) == {:badarity, [2]}
assert fun_apply(fun([integer(), atom()], boolean()), [integer()]) == {:badarity, [2]}
assert fun_apply(usable_at_2, [integer(), atom()]) == {:ok, boolean()}
# Function intersection tests (no overlap)
fun0 = intersection(fun([integer()], atom()), fun([float()], binary()))
assert fun_apply(fun0, [integer()]) == {:ok, atom()}
@@ -1648,7 +1735,7 @@ defmodule Module.Types.DescrTest do
test "list_of" do
assert list_of(term()) == :badproperlist
assert list_of(none()) == :badproperlist
assert list_of(empty_list()) == {true, none()}
assert list_of(empty_list()) == {true, nil}
assert list_of(union(empty_list(), integer())) == :badproperlist
assert list_of(non_empty_list(integer())) == {false, integer()}
assert list_of(non_empty_list(integer(), atom())) == :badproperlist
@@ -1656,9 +1743,12 @@ defmodule Module.Types.DescrTest do
assert list_of(non_empty_list(integer(), list(term()))) == {false, term()}
assert list_of(list(integer()) |> union(list(integer(), integer()))) == :badproperlist
assert list_of(list(integer()) |> union(integer())) == :badproperlist
assert list_of(dynamic()) == {true, dynamic()}
assert list_of(dynamic(list(integer()))) == {true, dynamic(integer())}
assert list_of(dynamic(list(integer(), atom()))) == {true, nil}
assert list_of(dynamic(list(integer(), term()))) == {true, dynamic()}
assert list_of(dynamic(non_empty_list(integer(), atom()))) == :badproperlist
assert list_of(dynamic(non_empty_list(integer(), term()))) == {false, dynamic()}
assert list_of(dynamic(union(empty_list(), integer()))) == {true, nil}
# A list that the difference resolves to nothing
@@ -1893,6 +1983,18 @@ defmodule Module.Types.DescrTest do
|> tuple_insert_at(1, float())
|> equal?(tuple([integer(), float(), atom(), boolean()]))
# Inserting must be a congruence wrt equal?, even when the positive side is
# the implicit top (a negation with :bdd_top branches) rather than an
# explicit open leaf. Here t2 == open_tuple([term(), term()]).
t2 = difference(open_tuple([]), union(tuple([]), tuple([term()])))
assert equal?(t2, open_tuple([term(), term()]))
assert tuple_insert_at(t2, 2, float())
|> equal?(tuple_insert_at(open_tuple([term(), term()]), 2, float()))
# The inserted index is actually constrained to float().
refute subtype?(tuple([integer(), integer(), atom()]), tuple_insert_at(t2, 2, float()))
# Test inserting into a complex union involving dynamic
assert union(tuple([integer(), atom()]), dynamic(tuple([float(), binary()])))
|> tuple_insert_at(1, boolean())
@@ -1912,6 +2014,20 @@ defmodule Module.Types.DescrTest do
# Errors must propagate even when the inserted value is dynamic
assert tuple_insert_at(integer(), 0, dynamic()) == :badtuple
assert tuple_insert_at(tuple([atom([:ok])]), 2, dynamic()) == :badindex
# Must not crash when a gradual descr's static part is a non-normalized
# empty (semantically empty but syntactically present) non-tuple component.
a1 = union(dynamic(), non_empty_list(integer(), none()))
assert equal?(a1, dynamic())
assert tuple_insert_at(a1, 1, atom([:x]))
|> equal?(tuple_insert_at(dynamic(), 1, atom([:x])))
a2 = union(tuple([dynamic()]), non_empty_list(none()))
assert equal?(a2, tuple([dynamic()]))
assert tuple_insert_at(a2, 1, atom([:x]))
|> equal?(tuple_insert_at(tuple([dynamic()]), 1, atom([:x])))
end
test "tuple_replace_at" do
@@ -2114,7 +2230,7 @@ defmodule Module.Types.DescrTest do
assert list(
Enum.reduce(
[binary(), float(), pid(), port(), reference()] ++
[binary(), bitstring_no_binary(), float(), pid(), port(), reference()] ++
[fun(), atom(), tuple(), open_map(), list(term(), term())],
tuple([integer(), binary()]),
fn domain, acc -> union(acc, tuple([domain, term()])) end
@@ -3100,6 +3216,16 @@ defmodule Module.Types.DescrTest do
assert map_put(map, atom([:k]), binary()) == {:ok, open_map(k: binary(), x: term())}
end
test "is consistent across representations of an empty type" do
# An empty map component that survives syntactically (open_map(c: none())
# is a non-normalized empty, equal to none()) must report :badmap like
# none(), not {:ok, <inhabited>}.
a2 = open_map(c: none())
assert equal?(none(), a2)
assert map_put(none(), atom([:a]), integer()) == :badmap
assert map_put(a2, atom([:a]), integer()) == :badmap
end
end
describe "disjoint" do
@@ -3196,6 +3322,8 @@ defmodule Module.Types.DescrTest do
test "dynamic (negation)" do
assert dynamic(negation(integer())) |> to_quoted_string() == "dynamic(not integer())"
assert negation(dynamic(none())) == term()
assert negation(dynamic(integer())) |> to_quoted_string() == "dynamic() or not integer()"
assert union(atom(), dynamic(integer())) |> negation() |> to_quoted_string() ==
@@ -3219,6 +3347,9 @@ defmodule Module.Types.DescrTest do
assert list(term()) |> difference(list(integer())) |> to_quoted_string() ==
"non_empty_list(term()) and not non_empty_list(integer())"
assert difference(list(term()), non_empty_list(integer())) |> to_quoted_string() ==
"list(term()) and not non_empty_list(integer())"
assert list(term())
|> difference(list(integer()))
|> difference(list(atom()))
@@ -3469,6 +3600,34 @@ defmodule Module.Types.DescrTest do
"""
end
test "fun union of static and dynamic keeps non-fun components" do
# Denormalizing a static/dynamic fun union must not drop the other parts of
# the type, nor crash when an arity fails to denormalize.
# (a) must not raise when denormalization fails for an arity
assert difference(fun([term()], atom()), fun([integer()], dynamic(atom())))
|> to_quoted_string() ==
"dynamic((term() -> atom()) and (integer() -> atom()))"
# (b) non-fun components must be preserved alongside the denormalized fun
assert union(
integer(),
union(fun([integer()], atom()), dynamic(fun([integer()], atom())))
)
|> to_quoted_string() == "(integer() -> atom()) or integer()"
# several components, with a leftover non-denormalized dynamic arity
assert union(
atom([:tag]),
union(
fun([integer()], atom()),
dynamic(union(fun([integer()], atom()), fun([integer(), integer()], atom())))
)
)
|> to_quoted_string() ==
"dynamic((integer(), integer() -> atom())) or :tag or (integer() -> atom())"
end
test "fun (negation)" do
assert fun([integer()], atom()) |> negation() |> to_quoted_string() ==
"not (integer() -> atom())"
@@ -148,6 +148,30 @@ defmodule Module.Types.ExprTest do
non_empty_list(term(), term())
"""
end
test "++" do
assert typecheck!([x], [] ++ String.to_integer(x)) == integer()
assert typecheck!([x], [x] ++ []) == non_empty_list(dynamic())
assert typeerror!([x], String.to_integer(x) ++ []) |> strip_ansi() =~
~l"""
incompatible types given to Kernel.++/2:
String.to_integer(x) ++ []
given types:
integer(), empty_list()
but expected one of:
#1
empty_list(), term()
#2
non_empty_list(term()), term()
"""
end
end
describe "funs" do
@@ -419,6 +443,13 @@ defmodule Module.Types.ExprTest do
) == dynamic(tuple([integer(), integer(), binary()]))
end
test "send returns the message" do
assert typecheck!(send(self(), {:msg, 1})) == tuple([atom([:msg]), integer()])
assert typeerror!(send(123, {:msg, 1})) =~
"incompatible types given to Kernel.send/2"
end
test "undefined function warnings" do
assert typewarn!(URI.unknown("foo")) ==
{dynamic(), "URI.unknown/1 is undefined or private"}
@@ -1818,6 +1849,11 @@ defmodule Module.Types.ExprTest do
end
test "Kernel.in/2" do
assert typecheck!(
[x],
x in []
) == atom([false])
assert typecheck!(
[x],
(
@@ -2271,6 +2307,42 @@ defmodule Module.Types.ExprTest do
assert typecheck!(false or true) == atom([true])
end
test "andalso/orelse do not leak right-hand side refinements" do
assert typecheck!(
[x],
cond do
:erlang.andalso(is_tuple(x), tuple_size(x) == 3) -> {:tuple, x}
is_atom(x) -> {:atom, x}
true -> {:other, x}
end
)
|> equal?(
dynamic(
tuple([atom([:tuple]), tuple([term(), term(), term()])])
|> union(tuple([atom([:atom]), atom()]))
|> union(tuple([atom([:other]), negation(atom())]))
)
)
assert typecheck!(
[x],
cond do
:erlang.orelse(is_atom(x), is_tuple(x)) -> {:atom_or_tuple, x}
is_binary(x) -> {:binary, x}
true -> {:other, x}
end
)
|> equal?(
dynamic(
tuple([atom([:atom_or_tuple]), term()])
|> union(tuple([atom([:binary]), binary()]))
|> union(
tuple([atom([:other]), negation(union(binary(), union(atom(), tuple())))])
)
)
)
end
test "and reports violations" do
assert typeerror!([x = 123], x and true) =~ """
the following conditional expression will always fail:
@@ -2375,7 +2447,7 @@ defmodule Module.Types.ExprTest do
!x
will always evaluate to false because the expression has type:
will always evaluate to false because its inner expression has type:
integer()
@@ -2391,7 +2463,7 @@ defmodule Module.Types.ExprTest do
!x
will always evaluate to true because the expression has type:
will always evaluate to true because its inner expression has type:
dynamic(nil)
"""
@@ -2399,9 +2471,9 @@ defmodule Module.Types.ExprTest do
assert typeerror!([x = 123], !!x) =~ ~l"""
the following conditional expression:
!x
!!x
will always evaluate to false because the expression has type:
will always evaluate to true because its inner expression has type:
integer()
@@ -2411,6 +2483,16 @@ defmodule Module.Types.ExprTest do
# from: types_test.ex:LINE
x = 123
"""
assert typewarn!([x = nil], !!x) |> elem(1) =~ ~l"""
the following conditional expression:
!!x
will always evaluate to false because its inner expression has type:
dynamic(nil)
"""
end
end
@@ -3303,10 +3385,15 @@ defmodule Module.Types.ExprTest do
assert typecheck!(GenServer.__info__(:struct)) == atom([nil])
assert typecheck!(URI.__info__(:struct)) ==
list(closed_map(default: if_set(term()), field: atom()))
list(
closed_map(default: if_set(term()), field: atom(), required: if_set(boolean()))
)
assert typecheck!([x], x.__info__(:struct)) ==
list(closed_map(default: if_set(term()), field: atom())) |> union(atom([nil]))
list(
closed_map(default: if_set(term()), field: atom(), required: if_set(boolean()))
)
|> union(atom([nil]))
end
test "behaviour_info/1" do
@@ -20,6 +20,8 @@ defmodule Module.Types.HelpersTest do
assert expr_to_string(quote(do: :erlang.list_to_atom(a))) == "List.to_atom(a)"
assert expr_to_string(quote(do: :erlang.element(1, a))) == "elem(a, 0)"
assert expr_to_string(quote(do: :erlang.element(:erlang.+(a, 1), b))) == "elem(b, a)"
assert expr_to_string(quote(do: :lists.member(a, []))) == "a in []"
assert expr_to_string(quote(do: :lists.member(a, [:foo, :bar]))) == "a in [:foo, :bar]"
end
test "Kernel macros" do
@@ -320,6 +320,46 @@ defmodule Module.Types.InferTest do
]
end
test "from single defguard (regression with large code generation)", config do
# As long as it type checks in time, we are fine,
# but it should infer Macro.t in the future.
infer config do
defguard is_erlang_app(app) when app in ~w(
inets ftp os_mon parsetools mnesia eldap eunit observer
dialyzer runtime_tools edoc diameter wx debugger ssh et
sasl ssl asn1 snmp erts tools stdlib reltool kernel crypto
tftp erl_interface syntax_tools megaco public_key
common_test xmerl compiler jinterface
)
end
end
test "from multiple defguard (regression with large code generation)", config do
infer config do
defguardp is_a(meta) when meta.m != :ppppp or meta.t != :nnnnn
defguardp is_b(meta) when is_nil(meta.s) or not meta.config.s.tunable
defguardp is_c(meta) when is_nil(meta.k) or not meta.config.k.tunable
defguardp is_d(meta) when is_nil(meta.t) or not meta.config.t.tunable
defguardp is_e(meta) when not meta.config.m.tunable
defguardp is_f(meta) when not meta.config.v.tunable
defguardp is_all_1(meta)
when is_a(meta) and is_b(meta) and is_c(meta) and is_d(meta) and
is_e(meta) and is_f(meta)
defguardp is_all_2(meta)
when (meta.m != :ppppp or meta.t != :nnnnn) and
(is_nil(meta.s) or not meta.config.s.tunable) and
(is_nil(meta.k) or not meta.config.k.tunable) and
(is_nil(meta.t) or not meta.config.t.tunable) and
not meta.config.m.tunable and
not meta.config.v.tunable
def fun_1(%{meta: meta} = _data) when is_all_1(meta), do: :something
def fun_2(%{meta: meta} = _data) when is_all_2(meta), do: :something
end
end
test "from defaults (regression with multiple clauses)", config do
types =
infer config do
@@ -154,6 +154,50 @@ defmodule Module.Types.IntegrationTest do
assert itself_arg.(Itself.Unknown) == dynamic(open_map(__struct__: atom([Unknown])))
end
test "ignores additional callbacks on implementations" do
files = %{
"p.ex" => """
defmodule InjectCallback do
defmacro __before_compile__(_env) do
quote do
@callback extra() :: term()
end
end
end
defprotocol Injected do
@before_compile InjectCallback
def f(x)
end
defimpl Injected, for: Atom do
def f(_), do: :ok
def extra(), do: :extra
end
defprotocol Explicit do
@callback extra() :: term()
def f(x)
end
defimpl Explicit, for: Atom do
def f(_), do: :ok
def extra(), do: :extra
end
"""
}
assert capture_compile_warnings(files, []) == """
warning: cannot define @callback extra/0 inside protocol, use def/1 to outline your protocol definition
│
20 │ @callback extra() :: term()
│ ~~~~~~~~~~~~~~~~~~~~~~~~~~~
│
└─ p.ex:20: Explicit (module)
"""
end
end
describe "type checking" do
@@ -289,6 +333,39 @@ defmodule Module.Types.IntegrationTest do
assert_warnings(files, warnings)
end
test "warning location respects file metadata in clauses" do
files = %{
"bug.ex" => """
defmodule Bug do
def render(x)
@file "template.heex"
def render(x) do
label = describe(x)
_ = label == :only
x
end
defp describe(_), do: "page"
end
"""
}
warnings = [
"""
warning: comparison between distinct types found:
label == :only
""",
"binary() == :only",
"# type: binary()",
"# from: template.heex:6:11",
"└─ template.heex:7:15: Bug.render/1"
]
assert_warnings(files, warnings)
end
test "unused private clauses" do
files = %{
"a.ex" => """
@@ -705,6 +782,44 @@ defmodule Module.Types.IntegrationTest do
purge(A)
end
test "does not crash on redefined module with newly added struct" do
Code.compile_string("""
defmodule RedefinedNestedStruct.Inner do
def foo(), do: :nothing
end
""")
files = %{
"redefined.ex" => """
defmodule RedefinedNestedStruct.Inner do
defstruct [:value]
def foo(), do: %__MODULE__{value: 1}
end
"""
}
in_tmp(fn ->
paths = generate_files(files)
{result, _stderr} =
with_io(:stderr, fn ->
Kernel.ParallelCompiler.compile_to_path(paths, ".", return_diagnostics: true)
end)
assert {:error, errors, %{compile_warnings: warnings, runtime_warnings: []}} = result
assert [%{message: "struct RedefinedNestedStruct.Inner is undefined " <> _}] = errors
assert Enum.any?(
warnings,
&(&1.message =~ "redefining module RedefinedNestedStruct.Inner")
)
end)
after
purge(RedefinedNestedStruct.Inner)
end
@tag :require_ast
test "regressions" do
files = %{
@@ -1370,6 +1370,14 @@ defmodule Module.Types.PatternTest do
refute precise?([<<_::binary-size(8)>>])
refute precise?([<<_::bitstring-size(8)>>])
refute precise?([<<(<<123>>)::bits>>])
refute precise?([x, <<x::binary>>])
refute precise?([x, <<x>>])
refute precise?([x, <<y, x>>])
refute precise?([x, <<(<<x::binary>>)::binary>>])
refute precise?([<<x::binary>>, x])
refute precise?([<<x>>, x])
refute precise?([<<y, x>>, x])
refute precise?([<<(<<x::binary>>)::binary>>, x])
end
test "tuples in patterns" do
@@ -1378,7 +1386,11 @@ defmodule Module.Types.PatternTest do
end
test "maps in patterns" do
assert precise?([%{}])
assert precise?([%{ok: _}])
assert precise?([%_{}])
assert precise?([%x{}])
assert precise?([%URI{}])
assert precise?([%URI{path: _}])
refute precise?([%{ok: 123}])
+14
View File
@@ -84,9 +84,15 @@ defmodule VersionTest do
assert {:ok, %Version{major: 1, minor: 4, patch: 5, pre: [6, 7, "eight"]}} =
Version.parse("1.4.5-6.7.eight")
assert {:ok, %Version{major: 99_999_999_999_999, minor: 0, patch: 0}} =
Version.parse("99999999999999.0.0")
assert {:ok, %Version{major: 1, minor: 4, patch: 5, pre: ["6-g3318bd5"]}} =
Version.parse("1.4.5-6-g3318bd5+ignore")
assert {:ok, %Version{major: 1, minor: 0, patch: 0, pre: ["100000000000000-alpha"]}} =
Version.parse("1.0.0-100000000000000-alpha")
assert Version.parse("foobar") == :error
assert Version.parse("2") == :error
assert Version.parse("2.") == :error
@@ -105,6 +111,13 @@ defmodule VersionTest do
assert Version.parse("02.3.0") == :error
assert Version.parse("0. 0.0") == :error
assert Version.parse("0.1.0-&&pre") == :error
assert Version.parse("100000000000000.0.0") == :error
assert Version.parse("1.100000000000000.0") == :error
assert Version.parse("1.0.100000000000000") == :error
assert Version.parse("1.0.0-100000000000000") == :error
assert Version.parse("1.0.0+100000000000000") ==
{:ok, %Version{major: 1, minor: 0, patch: 0, build: "100000000000000"}}
end
test "to_string/1" do
@@ -338,6 +351,7 @@ defmodule VersionTest do
assert Version.parse_requirement("1.2.3 and or 4.5.6") == :error
assert Version.parse_requirement(">= 1") == :error
assert Version.parse_requirement("1.2.3 >=") == :error
assert Version.parse_requirement("100000000000000.0.0") == :error
end
test "inspect/1" do
+17
View File
@@ -60,6 +60,23 @@ optimized_or_test() ->
{clause, _, [{atom, _, true}], [], [{atom, _, true}]}]
} = to_erl("is_list([]) or :done").
optimized_in_test() ->
{'block', _,
[{match,_,
{var, _, '_'},
{call, _, {remote, _, {atom, _, 'Elixir.IO'}, {atom, _, puts}}, [{atom, _, hi}]}},
{atom, _, false}]
} = to_erl("IO.puts(:hi) in []"),
{'block', _,
[{match,_,
{var, _, '_1'},
{call, _, {remote, _, {atom, _, 'Elixir.IO'}, {atom, _, puts}}, [{atom, _, hi}]}},
{op, _, 'orelse',
{op, _, '=:=', {var, _ , '_1'}, {integer, _, 1}},
{op, _, '=:=', {var, _ , '_1'}, {integer, _, 2}}
}]
} = to_erl("IO.puts(:hi) in [1, 2]").
no_after_in_try_test() ->
{'try', _, [_], [], [_], []} = to_erl("try do :foo.bar() catch _ -> :ok end").
+1 -1
View File
@@ -785,7 +785,7 @@ defmodule IEx do
__break__!(ast, module, fun, args, guards, stops, env)
end
def __break__!(ast, _stops) do
def __break__!(ast, _stops, _env) do
raise_unknown_break_ast!(ast)
end
+6
View File
@@ -102,6 +102,12 @@ defmodule IEx.HelpersTest do
fn -> break!(PryExampleModule, :unknown, 2) end
end
test "errors when setting up a break for unknown expression" do
assert_raise ArgumentError, ~r"unknown expression to break on", fn ->
break!(123)
end
end
test "errors for non-Elixir modules" do
assert_raise RuntimeError,
"could not set breakpoint, module :maps was not written in Elixir",
+37 -38
View File
@@ -658,6 +658,20 @@ defmodule Mix.Compilers.Elixir do
reduce: {stale_modules, stale_modules, deps_exports, protocols_and_impls()} do
{modules, exports, deps_exports, protocols_and_impls} ->
{manifest_modules, manifest_sources} = read_manifest(manifest)
all_modules = Enum.map(manifest_modules, fn {mod, _} -> mod end)
dep_exports = Map.get(deps_exports, app, %{})
# Update modules, exports, and dep exports based on removed modules
{modules, exports, dep_exports} =
Enum.reduce(dep_exports, {modules, exports, dep_exports}, fn
{mod, _}, {modules, exports, dep_exports} ->
if mod in all_modules do
{modules, exports, dep_exports}
else
{Map.put(modules, mod, true), Map.put(exports, mod, true),
Map.delete(dep_exports, mod)}
end
end)
dep_modules =
for {module, module(timestamp: timestamp)} <- manifest_modules,
@@ -671,47 +685,32 @@ defmodule Mix.Compilers.Elixir do
dep_modules =
fixpoint_non_compile_modules(manifest_sources, Map.from_keys(dep_modules, true))
old_exports = Map.get(deps_exports, app, %{})
{exports, dep_exports} =
Enum.reduce(dep_modules, {exports, dep_exports}, fn {mod, _}, {exports, dep_exports} ->
export =
if Code.ensure_loaded?(mod) and function_exported?(mod, :__info__, 1) do
mod.__info__(:exports_md5)
end
# Update exports
{exports, new_exports} =
for {module, _} <- dep_modules, reduce: {exports, []} do
{exports, new_exports} ->
export =
if Code.ensure_loaded?(module) and function_exported?(module, :__info__, 1) do
module.__info__(:exports_md5)
end
# If the exports are the same, then the API did not change,
# so we do not mark the export as stale. Note this has to
# be very conservative. If the module is not loaded or if
# the exports were not there, we need to consider it a stale export.
case dep_exports do
# They still match, nothing to change
%{^mod => ^export} ->
{exports, dep_exports}
# If the exports are the same, then the API did not change,
# so we do not mark the export as stale. Note this has to
# be very conservative. If the module is not loaded or if
# the exports were not there, we need to consider it a stale
# export.
exports =
if export && old_exports[module] == export,
do: exports,
else: Map.put(exports, module, true)
# Now, it either matched and they are different, which means there was
# an export at some point OR it didn't exist before, which means adding
# it as an export is likely a no-op anyway
_ ->
{Map.put(exports, mod, true), Map.put(dep_exports, mod, export)}
end
end)
# Then we store the new export if any
new_exports =
if export,
do: [{module, export} | new_exports],
else: new_exports
{exports, new_exports}
end
new_exports = Map.new(new_exports)
removed =
for {module, _} <- old_exports,
not is_map_key(new_exports, module),
do: {module, true},
into: %{}
modules = modules |> Map.merge(dep_modules) |> Map.merge(removed)
exports = Map.merge(exports, removed)
deps_exports = Map.put(deps_exports, app, new_exports)
modules = Map.merge(modules, dep_modules)
deps_exports = Map.put(deps_exports, app, dep_exports)
protocols_and_impls =
protocols_and_impls_from_modules(manifest_modules, protocols_and_impls)
+5 -13
View File
@@ -486,20 +486,12 @@ defmodule Mix.Dep do
@doc """
Returns `true` if the dependency is compilable.
"""
def compilable?(%Mix.Dep{status: {:vsnlock, _}}), do: true
def compilable?(%Mix.Dep{status: {:noappfile, {_, _}}}), do: true
def compilable?(%Mix.Dep{status: {:scmlock, _}}), do: true
def compilable?(%Mix.Dep{status: :compile}), do: true
def compilable?(%Mix.Dep{status: :envoutdated}), do: true
def compilable?(dep), do: force_compilable?(dep)
@doc """
Returns `true` if the dependency is force compilable.
This is a subset of compilable. This is used in `deps.compile` to
clean the build path before compiling.
"""
def force_compilable?(%Mix.Dep{status: {:vsnlock, _}}), do: true
def force_compilable?(%Mix.Dep{status: {:noappfile, {_, _}}}), do: true
def force_compilable?(%Mix.Dep{status: {:scmlock, _}}), do: true
def force_compilable?(%Mix.Dep{status: :compile}), do: true
def force_compilable?(_), do: false
def compilable?(_), do: false
@doc """
Formats a dependency for printing.
+5 -3
View File
@@ -181,15 +181,17 @@ defmodule Mix.Local.Installer do
message =
case previous_files do
[] ->
"Are you sure you want to install #{inspect(src)}?"
"Do you trust and want to install #{inspect(src)}?"
[file] ->
"Found existing entry: #{file}\n" <>
"Are you sure you want to replace it with #{inspect(src)}?"
"The existing entry will be replaced.\n" <>
"Do you trust and want to install #{inspect(src)}?"
files ->
"Found existing entries: #{Enum.map_join(files, ", ", &Path.basename/1)}\n" <>
"Are you sure you want to replace them with #{inspect(src)}?"
"The existing entries will be replaced.\n" <>
"Do you trust and want to install #{inspect(src)}?"
end
Mix.shell().yes?(message)
+62 -7
View File
@@ -40,6 +40,15 @@ defmodule Mix.Tasks.Archive.Install do
Note that installing via Git, GitHub, or Hex fetches the source
of the archive and builds it, while using local path uses a pre-built archive.
## Security
Archives must be installed only from sources you trust.
Installing an archive from Git, GitHub, or Hex executes code from the source
during installation, unless a pre-built archive is given. Once an archive is
installed, Mix may load code from it as a plugin on any Mix command, even if
no archive command is executed.
## Command line options
* `--sha512` - checks the archive matches the given SHA-512 checksum. Only
@@ -119,7 +128,8 @@ defmodule Mix.Tasks.Archive.Install do
@impl true
def install(basename, contents, previous) do
ez_path = Path.join(Mix.path_for(:archives), basename)
dir_dest = resolve_destination(ez_path, contents)
archive_name = archive_name!(contents)
dir_dest = Path.join(Path.dirname(ez_path), archive_name)
remove_previous_versions(previous)
@@ -149,17 +159,62 @@ defmodule Mix.Tasks.Archive.Install do
### Private helpers
defp resolve_destination(ez_path, contents) do
with {:ok, [_comment, zip_first_file | _]} <- :zip.list_dir(contents),
{:zip_file, zip_first_path, _, _, _, _} = zip_first_file,
[zip_root_dir | _] = Path.split(zip_first_path) do
Path.join(Path.dirname(ez_path), zip_root_dir)
defp archive_name!(contents) do
with {:ok, files} <- :zip.list_dir(contents),
zip_files = Enum.filter(files, &match?({:zip_file, _, _, _, _, _}, &1)),
true <- zip_files != [] do
Enum.reduce(zip_files, nil, fn zip_file, root ->
validate_archive_path!(zip_file, root)
end)
else
_ ->
Mix.raise("Installation failed: invalid archive file")
Mix.raise("Installation failed: invalid archive file, no files found")
end
end
defp validate_archive_path!({:zip_file, path, file_info, _, _, _}, root) do
type = elem(file_info, 2)
path = zip_path_to_string(path)
unless type in [:regular, :directory] do
Mix.raise(
"Installation failed: invalid archive file, #{inspect(path)} is not a regular file or directory"
)
end
cond do
Path.type(path) != :relative ->
Mix.raise(
"Installation failed: invalid archive file, #{inspect(path)} is an absolute path"
)
String.contains?(path, ["..", "\\", <<0>>]) ->
Mix.raise("Installation failed: invalid archive file, #{inspect(path)} is an unsafe path")
true ->
:ok
end
case String.split(path, "/", trim: true) do
[new_root | _] ->
cond do
root && root != new_root ->
Mix.raise(
"Installation failed: invalid archive file, #{inspect(path)} is outside archive root #{inspect(root)}"
)
true ->
new_root
end
[] ->
Mix.raise("Installation failed: invalid archive file, #{inspect(path)} is empty")
end
end
defp zip_path_to_string(path) when is_list(path), do: List.to_string(path)
defp zip_path_to_string(path) when is_binary(path), do: path
defp archives(name) do
Mix.path_for(:archives)
|> Path.join(name)
+15 -1
View File
@@ -125,7 +125,7 @@ defmodule Mix.Tasks.Deps.Compile do
# If a dependency was marked as fetched or with an out of date lock
# or missing the app file, we always compile it from scratch.
if force? or Mix.Dep.force_compilable?(dep) do
if force? or clean_before_compile?(dep) do
File.rm_rf!(Path.join([Mix.Project.build_path(), "lib", Atom.to_string(dep.app)]))
end
@@ -402,6 +402,20 @@ defmodule Mix.Tasks.Deps.Compile do
end
end
# Most compilable statuses mean the dependency source, lock, app file, or build
# metadata changed in a way that requires removing the old build before
# compiling. :envoutdated is different: path dependencies can be incrementally
# recompiled from their existing build, but fetched dependencies must be cleaned
# first so stale BEAM files do not validate against the new compile environment
# before the dependency has a chance to rebuild.
defp clean_before_compile?(%Mix.Dep{status: :envoutdated, scm: scm}) do
scm.fetchable?()
end
defp clean_before_compile?(dep) do
Mix.Dep.compilable?(dep)
end
defp deps_compile_feedback(app) do
if Mix.install?() do
"Errors may have been logged above. You may run Mix.install/2 to try again or " <>
+8
View File
@@ -40,6 +40,14 @@ defmodule Mix.Tasks.Escript.Install do
`$PATH` environment variable. For more information, check the wikipedia
article on PATH: https://en.wikipedia.org/wiki/PATH_(variable)
## Security
Escripts must be installed only from sources you trust.
Installing an escript from Git, GitHub, or Hex executes code from the source
during installation, unless a pre-built escript is given. Once an escript is
installed, running it executes code on your machine.
## Command line options
* `--sha512` - checks the escript matches the given SHA-512 checksum. Only
+9 -5
View File
@@ -78,6 +78,10 @@ defmodule Mix.Tasks.Format do
* `--dry-run` - does not save files after formatting.
* `--no-compile` - does not compile, even if compilation is required
to load formatter plugins. If a plugin cannot be loaded, an error
is raised.
* `--verbose` - prints the names of files that were formatted.
* `--dot-formatter` - path to the file with formatter configuration.
@@ -152,7 +156,7 @@ defmodule Mix.Tasks.Format do
]
Notice that, when running the formatter with plugins, your code will be
compiled first.
compiled first, unless the `--no-compile` flag is given.
In addition, the order by which you input your plugins is the format order.
So, in the above `.formatter.exs`, the `MixMarkdownFormatter` will format
@@ -316,7 +320,7 @@ defmodule Mix.Tasks.Format do
plugins =
if plugins != [] do
Keyword.get(opts, :plugin_loader, &plugin_loader/1).(plugins)
Keyword.get(opts, :plugin_loader, &plugin_loader(&1, opts)).(plugins)
else
[]
end
@@ -357,12 +361,12 @@ defmodule Mix.Tasks.Format do
end)}
end
defp plugin_loader(plugins) do
defp plugin_loader(plugins, opts) do
if plugins != [] do
Mix.Task.run("loadpaths", [])
Mix.Task.run("loadpaths", if(opts[:no_compile], do: ["--no-compile"], else: []))
end
if not Enum.all?(plugins, &Code.ensure_loaded?/1) do
if !opts[:no_compile] and not Enum.all?(plugins, &Code.ensure_loaded?/1) do
Mix.Task.run("compile", [])
end
+39
View File
@@ -119,6 +119,45 @@ defmodule Mix.Tasks.ArchiveTest do
end)
end
test "archive install rejects parent directory entries" do
in_tmp("archive install rejects parent directory entries", fn ->
assert {:ok, _} =
:zip.create(~c"bad-0.1.0.ez", [
{~c"../outside", "bad"},
{~c"bad-0.1.0/ebin/bad", "bad"}
])
send(self(), {:mix_shell_input, :yes?, true})
assert_raise Mix.Error, ~r/invalid archive file/, fn ->
Mix.Tasks.Archive.Install.run(["bad-0.1.0.ez"])
end
refute File.exists?(tmp_path("userhome/outside"))
refute File.exists?(tmp_path("userhome/.mix/outside"))
refute File.dir?(tmp_path("userhome/.mix/archives/bad-0.1.0"))
end)
end
test "archive install rejects entries outside the archive root" do
in_tmp("archive install rejects entries outside the archive root", fn ->
assert {:ok, _} =
:zip.create(~c"bad-0.1.0.ez", [
{~c"bad-0.1.0/ebin/bad", "bad"},
{~c"other-0.1.0/ebin/bad", "bad"}
])
send(self(), {:mix_shell_input, :yes?, true})
assert_raise Mix.Error, ~r/invalid archive file/, fn ->
Mix.Tasks.Archive.Install.run(["bad-0.1.0.ez"])
end
refute File.dir?(tmp_path("userhome/.mix/archives/bad-0.1.0"))
refute File.dir?(tmp_path("userhome/.mix/archives/other-0.1.0"))
end)
end
test "archive install missing file" do
message = ~r[Expected "./unlikely-to-exist-0.1.0.ez" to be a local file path]
+57 -1
View File
@@ -73,6 +73,18 @@ defmodule Mix.Tasks.DepsTest do
end
end
defmodule FetchableGitRepoDepApp do
def project do
[
app: :git_sample,
version: "0.1.0",
deps: [
{:git_repo, "0.1.0", git: MixTest.Case.fixture_path("git_repo")}
]
]
end
end
## deps
test "prints list of dependencies and their status alphabetically" do
@@ -879,7 +891,51 @@ defmodule Mix.Tasks.DepsTest do
assert Application.spec(:raw_repo, :vsn)
end)
after
Application.delete_env(:raw_repo, :compile_env, persistent: true)
Application.delete_env(:anyapp, :anything, persistent: true)
end
test "recompiles fetchable dependencies when compile env changed" do
in_fixture("deps_status", fn ->
File.mkdir_p!("config")
File.write!("config/config.exs", "import Config\n")
Mix.Project.push(FetchableGitRepoDepApp)
Mix.Tasks.Deps.Get.run([])
File.write!("deps/git_repo/lib/git_repo.ex", """
Application.compile_env(:anyapp, :anything)
defmodule GitRepo do
def hello do
"World"
end
end
""")
Mix.Tasks.Loadconfig.load_compile("config/config.exs")
Mix.Task.run("compile", [])
assert Application.spec(:git_repo, :vsn)
File.write!("config/config.exs", """
import Config
config :anyapp, :anything, :anyvalue
""")
Application.unload(:git_repo)
Mix.ProjectStack.pop()
Mix.Task.clear()
Mix.Project.push(FetchableGitRepoDepApp)
purge([GitRepo])
Mix.Tasks.Loadconfig.load_compile("config/config.exs")
Mix.Task.run("compile", [])
assert_receive {:mix_shell, :info, ["Generated git_repo app"]}
assert Application.spec(:git_repo, :vsn)
end)
after
Application.delete_env(:anyapp, :anything, persistent: true)
purge([GitRepo, GitRepo.MixProject])
end
test "does not compile deps that have explicit option" do
+45
View File
@@ -597,6 +597,51 @@ defmodule Mix.Tasks.FormatTest do
end)
end
defmodule FormatWithPluginApp do
def project do
[app: :format_with_plugin, version: "0.1.0"]
end
end
test "doesn't compile plugins with --no-compile", context do
in_tmp(context.test, fn ->
Mix.Project.push(__MODULE__.FormatWithPluginApp)
on_exit(fn -> purge([UncompiledPlugin]) end)
File.write!(".formatter.exs", """
[
inputs: ["a.ex"],
plugins: [UncompiledPlugin]
]
""")
File.mkdir_p!("lib")
File.write!("lib/uncompiled_plugin.ex", """
defmodule UncompiledPlugin do
@behaviour Mix.Tasks.Format
def features(_opts), do: [extensions: [".ex"]]
def format(contents, _opts), do: "# formatted\\n" <> contents
end
""")
File.write!("a.ex", """
foo bar
""")
assert_raise Mix.Error, "Formatter plugin UncompiledPlugin cannot be found", fn ->
Mix.Tasks.Format.run(["--no-compile"])
end
refute_received {:mix_shell, :info, ["Compiling" <> _]}
assert File.read!("a.ex") == """
foo bar
"""
end)
end
test "uses extension plugins with --stdin-filename", context do
in_tmp(context.test, fn ->
File.write!(".formatter.exs", """
+4 -3
View File
@@ -67,12 +67,13 @@ defmodule Mix.Tasks.XrefTest do
}
output = [
%{callee: {A, :b, 1}, caller_module: B, file: "lib/b.ex", line: 3},
%{line: 3, file: "lib/b.ex", callee: {A, :b, 1}, caller_module: B},
%{callee: {:elixir_quote, :unquote, 1}, caller_module: A, file: "lib/a.ex", line: 4},
%{
callee: {:elixir_quote, :shallow_validate_ast, 1},
callee: {:elixir_quote, :validate_quote, 1},
caller_module: A,
file: "lib/a.ex",
line: 4
line: 3
}
]
+35
View File
@@ -479,6 +479,41 @@ defmodule Mix.UmbrellaTest do
end)
end
test "does not recompile after unchanged struct dependency and touched project files" do
in_fixture("umbrella_dep/deps/umbrella", fn ->
Mix.Project.in_project(:umbrella, ".", fn _ ->
File.mkdir_p!("config")
File.write!("config/config.exs", "import Config\n")
File.write!("apps/foo/lib/foo.ex", "defmodule Foo, do: defstruct [:bar]")
File.write!("apps/bar/lib/bar.ex", """
defmodule Bar do
def foo_bar(), do: %Foo{bar: true}
end
""")
Mix.Task.run("compile", ["--verbose"])
assert_received {:mix_shell, :info, ["Compiled lib/bar.ex"]}
File.write!("apps/foo/lib/foo.ex", File.read!("apps/foo/lib/foo.ex") <> "\n")
ensure_touched("apps/foo/lib/foo.ex", "_build/dev/lib/bar/.mix/compile.elixir")
Mix.Task.clear()
assert Mix.Task.run("compile", ["--verbose"]) == {:ok, []}
refute_received {:mix_shell, :info, ["Compiled lib/bar.ex"]}
ensure_touched("mix.exs", "_build/dev/lib/bar/.mix/compile.elixir")
ensure_touched("apps/foo/mix.exs", "_build/dev/lib/bar/.mix/compile.elixir")
ensure_touched("apps/bar/mix.exs", "_build/dev/lib/bar/.mix/compile.elixir")
ensure_touched("config/config.exs", "_build/dev/lib/bar/.mix/compile.elixir")
Mix.Task.clear()
assert Mix.Task.run("compile", ["--verbose"]) == {:ok, []}
refute_received {:mix_shell, :info, ["Compiled lib/bar.ex"]}
end)
end)
end
test "recompiles after compile through runtime path dependency changes" do
in_fixture("umbrella_dep/deps/umbrella/apps", fn ->
Mix.Project.in_project(:bar, "bar", fn _ ->