Compare commits
49
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
545dddf138 | ||
|
|
bc30e7d696 | ||
|
|
73130012bb | ||
|
|
f55abe5ff9 | ||
|
|
394ab4bead | ||
|
|
f491a7eee4 | ||
|
|
a64284921e | ||
|
|
6bef18d523 | ||
|
|
8516737ea0 | ||
|
|
c52eb2a323 | ||
|
|
527fb26bcd | ||
|
|
5c5909c2a1 | ||
|
|
3c7ee9ff20 | ||
|
|
51dce31a54 | ||
|
|
0dd4cb4e0b | ||
|
|
4d1de1d971 | ||
|
|
b098c0406c | ||
|
|
1ba39f3501 | ||
|
|
0f9072a1d4 | ||
|
|
6dcc4cd2dc | ||
|
|
40b6f012fe | ||
|
|
68267627dc | ||
|
|
57766dd29b | ||
|
|
1938d8a957 | ||
|
|
3bebe1599a | ||
|
|
2beac77406 | ||
|
|
85a23a1244 | ||
|
|
97c1e1ec0c | ||
|
|
64e6707233 | ||
|
|
657a7b394a | ||
|
|
ac76c108ce | ||
|
|
2e629aa414 | ||
|
|
2f2fc9a7f9 | ||
|
|
bb12270c6b | ||
|
|
6d2e8241c9 | ||
|
|
a371208104 | ||
|
|
3a366c2f0f | ||
|
|
d33ea6e963 | ||
|
|
853b7cd719 | ||
|
|
0bc04768bf | ||
|
|
c354a77946 | ||
|
|
766ece7e3f | ||
|
|
8bc9a2ed15 | ||
|
|
7150d493ad | ||
|
|
7a9ea30d9c | ||
|
|
3a038d1763 | ||
|
|
e5e66831ac | ||
|
|
285c15f1ab | ||
|
|
909903962d |
+102
-151
@@ -192,34 +192,60 @@ 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.2 (2026-06-23)
|
||||
|
||||
### 1. Enhancements
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Kernel] Perform type inference across applications
|
||||
* [Kernel.ParallelCompiler] Include per-module type checking times when compiler profiling is enabled with `profile: :time`
|
||||
|
||||
### 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] 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 +256,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 +356,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 +378,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).
|
||||
|
||||
@@ -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
@@ -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
@@ -6,7 +6,7 @@
|
||||
|
||||
set -e
|
||||
|
||||
ELIXIR_VERSION=1.20.0-rc.5
|
||||
ELIXIR_VERSION=1.20.2
|
||||
|
||||
if [ $# -eq 0 ] || { [ $# -eq 1 ] && { [ "$1" = "--help" ] || [ "$1" = "-h" ]; }; }; then
|
||||
cat <<USAGE >&2
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
:: SPDX-FileCopyrightText: 2021 The Elixir Team
|
||||
:: SPDX-FileCopyrightText: 2012 Plataformatec
|
||||
|
||||
set ELIXIR_VERSION=1.20.0-rc.5
|
||||
set ELIXIR_VERSION=1.20.2
|
||||
|
||||
if ""%1""=="""" if ""%2""=="""" goto documentation
|
||||
if /I ""%1""==""--help"" if ""%2""=="""" goto documentation
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -20,6 +20,7 @@ defmodule Module.ParallelChecker do
|
||||
{:max_concurrency, pos_integer()}
|
||||
| {:long_verification_threshold, pos_integer()}
|
||||
| {:each_long_verification, (module() -> term()) | (module(), pid() -> term())}
|
||||
| {:profile, :time}
|
||||
| {atom(), term()}
|
||||
]
|
||||
|
||||
@@ -117,13 +118,13 @@ defmodule Module.ParallelChecker do
|
||||
send(checker, {ref, :cached})
|
||||
|
||||
receive do
|
||||
{^ref, :check} ->
|
||||
{^ref, :check, profile} ->
|
||||
# Set the compiler info so we can collect warnings
|
||||
:erlang.put(:elixir_compiler_info, {pid, self()})
|
||||
|
||||
{warnings, errors} =
|
||||
if module_tuple do
|
||||
check_module(module_tuple, {checker, table}, log?)
|
||||
check_module(module_tuple, {checker, table}, log?, profile)
|
||||
else
|
||||
{[], []}
|
||||
end
|
||||
@@ -262,7 +263,7 @@ defmodule Module.ParallelChecker do
|
||||
|
||||
## Module checking
|
||||
|
||||
defp check_module(module_tuple, cache, log?) do
|
||||
defp check_module(module_tuple, cache, log?, profile) do
|
||||
{module, file, line, definitions, no_warn_undefined, behaviours, impls, attrs, after_verify} =
|
||||
module_tuple
|
||||
|
||||
@@ -276,9 +277,13 @@ defmodule Module.ParallelChecker do
|
||||
definitions
|
||||
)
|
||||
|
||||
type_warnings =
|
||||
profile(module, profile, fn ->
|
||||
Module.Types.warnings(module, file, attrs, definitions, no_warn_undefined, cache)
|
||||
end)
|
||||
|
||||
{warnings, errors} =
|
||||
module
|
||||
|> Module.Types.warnings(file, attrs, definitions, no_warn_undefined, cache)
|
||||
type_warnings
|
||||
|> Kernel.++(behaviour_warnings)
|
||||
|> group_diagnostics()
|
||||
|> emit_diagnostics(file, log?)
|
||||
@@ -577,6 +582,7 @@ defmodule Module.ParallelChecker do
|
||||
schedulers: schedulers,
|
||||
threshold: threshold,
|
||||
callback: callback,
|
||||
profile: Keyword.get(options, :profile),
|
||||
protocols: [],
|
||||
table: table
|
||||
}
|
||||
@@ -653,9 +659,24 @@ defmodule Module.ParallelChecker do
|
||||
end
|
||||
|
||||
defp run_checkers(%{modules: [{module, pid, ref} | modules]} = state) do
|
||||
send(pid, {ref, :check})
|
||||
send(pid, {ref, :check, state.profile})
|
||||
timer = :erlang.send_after(state.threshold, self(), {__MODULE__, :timeout, module, pid})
|
||||
spawned = Map.put(state.spawned, module, timer)
|
||||
run_checkers(%{state | modules: modules, spawned: spawned})
|
||||
end
|
||||
|
||||
defp profile(module, :time, fun) do
|
||||
{time, result} = :timer.tc(fun)
|
||||
|
||||
IO.puts(
|
||||
:stderr,
|
||||
"[profile] Type checked #{inspect(module)} in #{div(time, 1000)}ms"
|
||||
)
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
defp profile(_module, _profile, fun) do
|
||||
fun.()
|
||||
end
|
||||
end
|
||||
|
||||
@@ -136,8 +136,8 @@ defmodule Module.Types do
|
||||
defp impl_for(attrs) do
|
||||
case List.keyfind(attrs, :__impl__, 0) do
|
||||
{:__impl__, [protocol: protocol, for: for]} ->
|
||||
if Code.ensure_loaded?(protocol) and function_exported?(protocol, :behaviour_info, 1) do
|
||||
{for, protocol.behaviour_info(:callbacks)}
|
||||
if Code.ensure_loaded?(protocol) and function_exported?(protocol, :__protocol__, 1) do
|
||||
{for, protocol.__protocol__(:functions)}
|
||||
else
|
||||
nil
|
||||
end
|
||||
@@ -266,7 +266,7 @@ defmodule Module.Types do
|
||||
|
||||
Enum.reduce(unused_indexes, context, fn clause_index, context ->
|
||||
{meta, _args, _guards, _body} = Enum.fetch!(clauses, clause_index)
|
||||
stack = %{stack | function: fun_arity}
|
||||
stack = %{stack | function: fun_arity} |> with_file_meta(meta)
|
||||
Helpers.warn(__MODULE__, {:unused_clause, kind, fun_arity}, meta, stack, context)
|
||||
end)
|
||||
else
|
||||
@@ -299,11 +299,11 @@ defmodule Module.Types do
|
||||
|
||||
local_sigs ->
|
||||
case finder.(fun_arity) do
|
||||
{mode, {fun_arity, kind, meta, clauses}, expected} ->
|
||||
{mode, {fun_arity, kind, _meta, clauses}, expected} ->
|
||||
context = put_in(context.local_sigs, Map.put(local_sigs, fun_arity, kind))
|
||||
|
||||
{inferred, mapping, context} =
|
||||
local_handler(mode, fun_arity, kind, meta, clauses, expected, stack, context)
|
||||
local_handler(mode, fun_arity, kind, clauses, expected, stack, context)
|
||||
|
||||
context =
|
||||
update_in(context.local_sigs, &Map.put(&1, fun_arity, {kind, inferred, mapping}))
|
||||
@@ -316,9 +316,10 @@ defmodule Module.Types do
|
||||
end
|
||||
end
|
||||
|
||||
defp local_handler(:traverse, {_, arity}, _kind, _meta, clauses, _expected, stack, context) do
|
||||
defp local_handler(:traverse, {_, arity}, _kind, clauses, _expected, stack, context) do
|
||||
context =
|
||||
Enum.reduce(clauses, context, fn {_meta, _args, _guards, body}, context ->
|
||||
Enum.reduce(clauses, context, fn {meta, _args, _guards, body}, context ->
|
||||
stack = with_file_meta(stack, meta)
|
||||
Module.Types.Traverse.of_expr(body, stack, context)
|
||||
end)
|
||||
|
||||
@@ -326,9 +327,9 @@ defmodule Module.Types do
|
||||
{inferred, [{0, 0}], context}
|
||||
end
|
||||
|
||||
defp local_handler(mode, fun_arity, kind, meta, clauses, expected, stack, context) do
|
||||
defp local_handler(mode, fun_arity, kind, clauses, expected, stack, context) do
|
||||
{fun, _arity} = fun_arity
|
||||
stack = stack |> fresh_stack(mode, fun_arity) |> with_file_meta(meta)
|
||||
stack = fresh_stack(stack, mode, fun_arity)
|
||||
base_info = {:def, kind, fun, expected}
|
||||
|
||||
case clauses do
|
||||
@@ -341,6 +342,7 @@ defmodule Module.Types do
|
||||
end
|
||||
|
||||
defp default_local_handler(meta, args, body, base_info, kind, fun, expected, stack, context) do
|
||||
stack = with_file_meta(stack, meta)
|
||||
guards = []
|
||||
previous = Pattern.init_previous()
|
||||
fresh_context = fresh_context(context)
|
||||
@@ -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}
|
||||
|
||||
|
||||
@@ -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()}]},
|
||||
|
||||
@@ -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
|
||||
@@ -978,7 +979,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)
|
||||
@@ -1560,6 +1567,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
|
||||
@@ -2159,6 +2169,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 +2218,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,
|
||||
@@ -2282,7 +2294,7 @@ defmodule Module.Types.Descr do
|
||||
{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
|
||||
@@ -2573,7 +2585,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 +3104,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}
|
||||
@@ -5026,15 +5061,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 +5078,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 +5096,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 +5133,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} ->
|
||||
|
||||
@@ -641,7 +641,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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -786,8 +786,9 @@ defmodule Module.Types.Pattern do
|
||||
|
||||
# <<...>>>
|
||||
defp of_pattern({:<<>>, _meta, args} = node, _path, stack, context) do
|
||||
precise? = of_precise_bitstring?(node, context.vars)
|
||||
{type, context} = Of.bitstring(args, :match, stack, context)
|
||||
{type, of_precise_bitstring?(node), context}
|
||||
{type, precise?, context}
|
||||
end
|
||||
|
||||
# left ++ right
|
||||
@@ -987,12 +988,19 @@ defmodule Module.Types.Pattern do
|
||||
{type, precise?, put_in(context.subpatterns[key], of_pattern_tree(type, stack, context))}
|
||||
end
|
||||
|
||||
defp of_precise_bitstring?({:<<>>, _meta, [{:"::", _, [expr, {type, _, _}]}]})
|
||||
defp of_precise_bitstring?({:<<>>, _meta, [{:"::", _, [expr, {type, _, _}]}]}, vars)
|
||||
when type in [:binary, :bitstring, :bytes, :bits] do
|
||||
is_var(expr) or of_precise_bitstring?(expr)
|
||||
new_var?(expr, vars) or of_precise_bitstring?(expr, vars)
|
||||
end
|
||||
|
||||
defp of_precise_bitstring?(_), do: false
|
||||
defp of_precise_bitstring?(_expr, _vars), do: false
|
||||
|
||||
defp new_var?({_, meta, _} = var, vars) when is_var(var) do
|
||||
version = Keyword.fetch!(meta, :version)
|
||||
not is_map_key(vars, version)
|
||||
end
|
||||
|
||||
defp new_var?(_expr, _vars), do: false
|
||||
|
||||
## Guards
|
||||
#
|
||||
@@ -1564,7 +1572,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 +1740,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)}
|
||||
"""
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -562,16 +562,16 @@ format_error({recursive, Vars, TypeExpr}) ->
|
||||
Message =
|
||||
case lists:map(fun({Name, Context}) -> elixir_utils:var_info(Name, Context) end, lists:sort(Vars)) of
|
||||
[Var] ->
|
||||
io_lib:format("the variable ~ts is defined in function of itself", [Var]);
|
||||
io_lib:format("the variable ~ts depends on itself through the pattern", [Var]);
|
||||
[Var1, Var2] ->
|
||||
io_lib:format("the variable ~ts is defined recursively in function of ~ts", [Var1, Var2]);
|
||||
io_lib:format("the variable ~ts depends on ~ts through the pattern", [Var1, Var2]);
|
||||
[Head | Tail] ->
|
||||
List = lists:foldl(fun(X, Acc) -> [Acc, $,, $\s, X] end, Head, Tail),
|
||||
io_lib:format("the following variables form a cycle: ~ts", [List])
|
||||
end,
|
||||
|
||||
io_lib:format(
|
||||
"recursive variable definition in patterns:~n~n~ts~n~n~ts",
|
||||
"cyclic variable definition in patterns:~n~n~ts~n~n~ts",
|
||||
[Code, Message]
|
||||
);
|
||||
|
||||
|
||||
@@ -59,6 +59,9 @@ handle_call({acquire, Path}, From, Config) ->
|
||||
handle_call(required, _From, Config) ->
|
||||
{reply, [F || {F, true} <- maps:to_list(Config#elixir_code_server.required)], Config};
|
||||
|
||||
handle_call({release, Path}, _From, Config) ->
|
||||
{reply, ok, release(Path, Config)};
|
||||
|
||||
handle_call(retrieve_compiler_module, _From, Config) ->
|
||||
case Config#elixir_code_server.mod_pool of
|
||||
{Used, [Mod | Unused], Counter} ->
|
||||
@@ -140,6 +143,20 @@ terminate(_Reason, _Config) ->
|
||||
code_change(_Old, Config, _Extra) ->
|
||||
{ok, Config}.
|
||||
|
||||
release(Path, Config) ->
|
||||
Current = Config#elixir_code_server.required,
|
||||
case maps:find(Path, Current) of
|
||||
{ok, []} ->
|
||||
Released = maps:remove(Path, Current),
|
||||
Config#elixir_code_server{required=Released};
|
||||
{ok, [Next | Waiting]} ->
|
||||
_ = gen_server:reply(Next, proceed),
|
||||
Released = maps:put(Path, Waiting, Current),
|
||||
Config#elixir_code_server{required=Released};
|
||||
error ->
|
||||
Config
|
||||
end.
|
||||
|
||||
compiler_module(I) ->
|
||||
list_to_atom("elixir_compiler_" ++ integer_to_list(I)).
|
||||
|
||||
|
||||
@@ -14,12 +14,15 @@
|
||||
translate(Meta, Args, S) ->
|
||||
{Cases, [{do, Expr} | Opts]} = elixir_utils:split_last(Args),
|
||||
|
||||
% needs to be the original variables, excluding variables from generators
|
||||
InitVars = S#elixir_erl.var_names,
|
||||
|
||||
case lists:keyfind(reduce, 1, Opts) of
|
||||
{reduce, Reduce} -> translate_reduce(Meta, Cases, Expr, Reduce, S);
|
||||
false -> translate_into(Meta, Cases, Expr, Opts, S)
|
||||
{reduce, Reduce} -> translate_reduce(Meta, Cases, Expr, Reduce, InitVars, S);
|
||||
false -> translate_into(Meta, Cases, Expr, Opts, InitVars, S)
|
||||
end.
|
||||
|
||||
translate_reduce(Meta, Cases, Expr, Reduce, S) ->
|
||||
translate_reduce(Meta, Cases, Expr, Reduce, InitVars, S) ->
|
||||
Ann = ?ann(Meta),
|
||||
{TReduce, SR} = elixir_erl_pass:translate(Reduce, Ann, S),
|
||||
{TCases, SC} = translate_gen(Meta, Cases, [], SR),
|
||||
@@ -30,9 +33,9 @@ translate_reduce(Meta, Cases, Expr, Reduce, S) ->
|
||||
({'case', CaseAnn, _, CaseBlock}, InnerAcc) -> {'case', CaseAnn, InnerAcc, CaseBlock}
|
||||
end,
|
||||
|
||||
build_reduce(Ann, TCases, InnerFun, TExpr, TReduce, false, SE).
|
||||
build_reduce(Ann, TCases, InnerFun, TExpr, TReduce, false, InitVars, SE).
|
||||
|
||||
translate_into(Meta, Cases, Expr, Opts, S) ->
|
||||
translate_into(Meta, Cases, Expr, Opts, InitVars, S) ->
|
||||
Ann = ?ann(Meta),
|
||||
|
||||
{TInto, SI} =
|
||||
@@ -47,8 +50,8 @@ translate_into(Meta, Cases, Expr, Opts, S) ->
|
||||
{TExpr, SE} = elixir_erl_pass:translate(wrap_expr_if_unused(Expr, TInto), Ann, SC),
|
||||
|
||||
case inline_or_into(TInto) of
|
||||
inline -> build_inline(Ann, TCases, TExpr, TInto, TUniq, SE);
|
||||
into -> build_into(Ann, TCases, TExpr, TInto, TUniq, SE)
|
||||
inline -> build_inline(Ann, TCases, TExpr, TInto, TUniq, InitVars, SE);
|
||||
into -> build_into(Ann, TCases, TExpr, TInto, TUniq, InitVars, SE)
|
||||
end.
|
||||
|
||||
%% In case we have no return, we wrap the expression
|
||||
@@ -115,29 +118,29 @@ collect_filters([H | T], Acc) ->
|
||||
collect_filters([], Acc) ->
|
||||
{Acc, []}.
|
||||
|
||||
build_inline(Ann, Clauses, Expr, Into, Uniq, S) ->
|
||||
build_inline(Ann, Clauses, Expr, Into, Uniq, InitVars, S) ->
|
||||
case not Uniq and lists:all(fun(Clause) -> element(1, Clause) == bin end, Clauses) of
|
||||
true -> {build_comprehension(Ann, Clauses, Expr, Into), S};
|
||||
false -> build_inline_each(Ann, Clauses, Expr, Into, Uniq, S)
|
||||
false -> build_inline_each(Ann, Clauses, Expr, Into, Uniq, InitVars, S)
|
||||
end.
|
||||
|
||||
build_inline_each(Ann, Clauses, Expr, false, Uniq, S) ->
|
||||
build_inline_each(Ann, Clauses, Expr, false, Uniq, InitVars, S) ->
|
||||
InnerFun = fun(InnerExpr, _InnerAcc) -> InnerExpr end,
|
||||
build_reduce(Ann, Clauses, InnerFun, Expr, {nil, Ann}, Uniq, S);
|
||||
build_inline_each(Ann, [{enum, _, Left = {var, _, _}, Right, [] = _Filters}], Expr, {nil, _} = _Into, false, S) ->
|
||||
build_reduce(Ann, Clauses, InnerFun, Expr, {nil, Ann}, Uniq, InitVars, S);
|
||||
build_inline_each(Ann, [{enum, _, Left = {var, _, _}, Right, [] = _Filters}], Expr, {nil, _} = _Into, false, _InitVars, S) ->
|
||||
Clauses = [{clause, Ann, [Left], [], [Expr]}],
|
||||
Args = [Right, {'fun', Ann, {clauses, Clauses}}],
|
||||
{?remote(Ann, 'Elixir.Enum', map, Args), S};
|
||||
build_inline_each(Ann, [{enum, _, Left = {var, _, _}, Right, [] = _Filters}], Expr, {map, _, []} = _Into, false, S) ->
|
||||
build_inline_each(Ann, [{enum, _, Left = {var, _, _}, Right, [] = _Filters}], Expr, {map, _, []} = _Into, false, _InitVars, S) ->
|
||||
Clauses = [{clause, Ann, [Left], [], [Expr]}],
|
||||
Args = [Right, {'fun', Ann, {clauses, Clauses}}],
|
||||
List = ?remote(Ann, 'Elixir.Enum', map, Args),
|
||||
{?remote(Ann, maps, from_list, [List]), S};
|
||||
build_inline_each(Ann, Clauses, Expr, {nil, _} = Into, Uniq, S) ->
|
||||
build_inline_each(Ann, Clauses, Expr, {nil, _} = Into, Uniq, InitVars, S) ->
|
||||
InnerFun = fun(InnerExpr, InnerAcc) -> {cons, Ann, InnerExpr, InnerAcc} end,
|
||||
{ReduceExpr, SR} = build_reduce(Ann, Clauses, InnerFun, Expr, Into, Uniq, S),
|
||||
{ReduceExpr, SR} = build_reduce(Ann, Clauses, InnerFun, Expr, Into, Uniq, InitVars, S),
|
||||
{?remote(Ann, lists, reverse, [ReduceExpr]), SR};
|
||||
build_inline_each(Ann, Clauses, Expr, {bin, _, []}, Uniq, S) ->
|
||||
build_inline_each(Ann, Clauses, Expr, {bin, _, []}, Uniq, InitVars, S) ->
|
||||
{InnerValue, SV} = build_var(Ann, S),
|
||||
Generated = erl_anno:set_generated(true, Ann),
|
||||
|
||||
@@ -155,17 +158,17 @@ build_inline_each(Ann, Clauses, Expr, {bin, _, []}, Uniq, S) ->
|
||||
]}
|
||||
end,
|
||||
|
||||
{ReduceExpr, SR} = build_reduce(Ann, Clauses, InnerFun, Expr, {nil, Ann}, Uniq, SV),
|
||||
{ReduceExpr, SR} = build_reduce(Ann, Clauses, InnerFun, Expr, {nil, Ann}, Uniq, InitVars, SV),
|
||||
{?remote(Ann, erlang, list_to_bitstring, [ReduceExpr]), SR}.
|
||||
|
||||
build_into(Ann, Clauses, Expr, {map, _, []}, Uniq, S) ->
|
||||
{ReduceExpr, SR} = build_inline_each(Ann, Clauses, Expr, {nil, Ann}, Uniq, S),
|
||||
build_into(Ann, Clauses, Expr, {map, _, []}, Uniq, InitVars, S) ->
|
||||
{ReduceExpr, SR} = build_inline_each(Ann, Clauses, Expr, {nil, Ann}, Uniq, InitVars, S),
|
||||
{?remote(Ann, maps, from_list, [ReduceExpr]), SR};
|
||||
build_into(Ann, Clauses, Expr, ?empty_map_set_pattern = _Into, Uniq, S) ->
|
||||
build_into(Ann, Clauses, Expr, ?empty_map_set_pattern = _Into, Uniq, InitVars, S) ->
|
||||
InnerFun = fun(InnerExpr, InnerAcc) -> {cons, Ann, InnerExpr, InnerAcc} end,
|
||||
{ReduceExpr, SR} = build_reduce(Ann, Clauses, InnerFun, Expr, {nil, Ann}, Uniq, S),
|
||||
{ReduceExpr, SR} = build_reduce(Ann, Clauses, InnerFun, Expr, {nil, Ann}, Uniq, InitVars, S),
|
||||
{?remote(Ann, 'Elixir.MapSet', new, [ReduceExpr]), SR};
|
||||
build_into(Ann, Clauses, Expr, Into, Uniq, S) ->
|
||||
build_into(Ann, Clauses, Expr, Into, Uniq, InitVars, S) ->
|
||||
{Fun, SF} = build_var(Ann, S),
|
||||
{Acc, SA} = build_var(Ann, SF),
|
||||
{Kind, SK} = build_var(Ann, SA),
|
||||
@@ -182,7 +185,7 @@ build_into(Ann, Clauses, Expr, Into, Uniq, S) ->
|
||||
?remote(Ann, 'Elixir.Collectable', into, [Into])
|
||||
},
|
||||
|
||||
{IntoReduceExpr, SN} = build_reduce(Ann, Clauses, InnerFun, Expr, Acc, Uniq, SD),
|
||||
{IntoReduceExpr, SN} = build_reduce(Ann, Clauses, InnerFun, Expr, Acc, Uniq, InitVars, SD),
|
||||
|
||||
TryExpr =
|
||||
{'try', Ann,
|
||||
@@ -205,10 +208,10 @@ stacktrace_clause(Ann, Fun, Acc, Kind, Reason, Stack) ->
|
||||
|
||||
%% Helpers
|
||||
|
||||
build_reduce(Ann, Clauses, InnerFun, Expr, Into, false, S) ->
|
||||
build_reduce(Ann, Clauses, InnerFun, Expr, Into, false, InitVars, S) ->
|
||||
{Acc, SA} = build_var(Ann, S),
|
||||
{build_reduce_each(Clauses, InnerFun(Expr, Acc), Into, Acc, SA), SA};
|
||||
build_reduce(Ann, Clauses, InnerFun, Expr, Into, true, S) ->
|
||||
{build_reduce_each(Clauses, InnerFun(Expr, Acc), Into, Acc, InitVars, SA), SA};
|
||||
build_reduce(Ann, Clauses, InnerFun, Expr, Into, true, InitVars, S) ->
|
||||
%% Those variables are used only inside the anonymous function
|
||||
%% so we don't need to worry about returning the scope.
|
||||
{Acc, SA} = build_var(Ann, S),
|
||||
@@ -229,12 +232,12 @@ build_reduce(Ann, Clauses, InnerFun, Expr, Into, true, S) ->
|
||||
]}
|
||||
]},
|
||||
|
||||
EnumReduceCall = build_reduce_each(Clauses, InnerExpr, NewInto, Acc, SU),
|
||||
EnumReduceCall = build_reduce_each(Clauses, InnerExpr, NewInto, Acc, InitVars, SU),
|
||||
{?remote(Ann, erlang, element, [{integer, Ann, 1}, EnumReduceCall]), SU}.
|
||||
|
||||
build_reduce_each([{enum, Meta, Left, Right, Filters} | T], Expr, Arg, Acc, S) ->
|
||||
build_reduce_each([{enum, Meta, Left, Right, Filters} | T], Expr, Arg, Acc, InitVars, S) ->
|
||||
Ann = ?ann(Meta),
|
||||
True = build_reduce_each(T, Expr, Acc, Acc, S),
|
||||
True = build_reduce_each(T, Expr, Acc, Acc, InitVars, S),
|
||||
False = Acc,
|
||||
Generated = erl_anno:set_generated(true, Ann),
|
||||
|
||||
@@ -255,13 +258,13 @@ build_reduce_each([{enum, Meta, Left, Right, Filters} | T], Expr, Arg, Acc, S) -
|
||||
Args = [Right, Arg, {'fun', Ann, {clauses, Clauses1}}],
|
||||
?remote(Ann, 'Elixir.Enum', reduce, Args);
|
||||
|
||||
build_reduce_each([{bin, Meta, Left, Right, Filters} | T], Expr, Arg, Acc, S) ->
|
||||
build_reduce_each([{bin, Meta, Left, Right, Filters} | T], Expr, Arg, Acc, InitVars, S) ->
|
||||
Ann = ?ann(Meta),
|
||||
Generated = erl_anno:set_generated(true, Ann),
|
||||
{Tail, ST} = build_var(Ann, S),
|
||||
{Fun, SF} = build_var(Ann, ST),
|
||||
|
||||
True = build_reduce_each(T, Expr, Acc, Acc, SF),
|
||||
True = build_reduce_each(T, Expr, Acc, Acc, InitVars, SF),
|
||||
False = Acc,
|
||||
{bin, _, Elements} = Left,
|
||||
TailElement = {bin_element, Ann, Tail, default, [bitstring]},
|
||||
@@ -275,7 +278,7 @@ build_reduce_each([{bin, Meta, Left, Right, Filters} | T], Expr, Arg, Acc, S) ->
|
||||
[?remote(Ann, erlang, error, [pair(Ann, badarg, Tail)])]}],
|
||||
|
||||
NoVarClauses =
|
||||
case no_var(Generated, Elements) of
|
||||
case no_unbound_var(Generated, Elements, InitVars) of
|
||||
error ->
|
||||
Clauses;
|
||||
|
||||
@@ -294,7 +297,7 @@ build_reduce_each([{bin, Meta, Left, Right, Filters} | T], Expr, Arg, Acc, S) ->
|
||||
{named_fun, Ann, element(3, Fun), VarClauses},
|
||||
[Right, Arg]};
|
||||
|
||||
build_reduce_each([], Expr, _Arg, _Acc, _S) ->
|
||||
build_reduce_each([], Expr, _Arg, _Acc, _InitVars, _S) ->
|
||||
Expr.
|
||||
|
||||
is_var({var, _, _}) -> true;
|
||||
@@ -307,9 +310,10 @@ build_var(Ann, S) ->
|
||||
{Name, ST} = elixir_erl_var:build('_', S),
|
||||
{{var, Ann, Name}, ST}.
|
||||
|
||||
no_var(ParentAnn, Elements) ->
|
||||
no_unbound_var(ParentAnn, Elements, InitVars) ->
|
||||
Vars = #{V => K || K := V <- InitVars},
|
||||
try
|
||||
[{bin_element, Ann, NoVarExpr, no_var_size(Size), Types} ||
|
||||
[{bin_element, Ann, NoVarExpr, no_unbound_var_size(Size, Vars), Types} ||
|
||||
{bin_element, Ann, Expr, Size, Types} <- Elements,
|
||||
NoVarExpr <- no_var_expr(ParentAnn, Expr)]
|
||||
catch
|
||||
@@ -319,9 +323,17 @@ no_var(ParentAnn, Elements) ->
|
||||
no_var_expr(Ann, {string, _, String}) -> [{var, Ann, '_'} || _ <- String];
|
||||
no_var_expr(Ann, _) -> [{var, Ann, '_'}].
|
||||
|
||||
no_var_size(default) -> default;
|
||||
no_var_size(Size) when is_integer(Size) -> Size;
|
||||
no_var_size(_) -> throw(unbound_size).
|
||||
no_unbound_var_size(Size, Vars) ->
|
||||
valid_var_size(Size, Vars) orelse throw(unbound_size),
|
||||
Size.
|
||||
|
||||
valid_var_size({var, _, Var}, Vars) when is_map_key(Var, Vars) -> true;
|
||||
valid_var_size(default, _Vars) -> true;
|
||||
valid_var_size({integer, _, _}, _Vars) -> true;
|
||||
valid_var_size(Size, _Vars) when is_integer(Size) -> true;
|
||||
valid_var_size({op, _Ann, _Op, Left, Right}, Vars) ->
|
||||
valid_var_size(Left, Vars) andalso valid_var_size(Right, Vars);
|
||||
valid_var_size(_Size, _vars) -> false.
|
||||
|
||||
build_comprehension(Ann, Clauses, Expr, Into) ->
|
||||
{comprehension_kind(Into), Ann, Expr, comprehension_clause(Clauses)}.
|
||||
|
||||
@@ -576,6 +576,10 @@ translate_remote('Elixir.String.Chars', to_string, Meta, [Arg], S) ->
|
||||
{clause, Generated, [Var], [[Guard]], [Fast]},
|
||||
{clause, Generated, [Var], [], [Slow]}
|
||||
]}, VS};
|
||||
translate_remote(lists, member, Meta, [Expr, []], S) ->
|
||||
Ann = ?ann(Meta),
|
||||
{TExpr, S1} = translate(Expr, Ann, S),
|
||||
{{block, Ann, [{match, Ann, {var, Ann, '_'}, TExpr}, {atom, Ann, false}]}, S1};
|
||||
translate_remote(lists, member, Meta, [Expr, [Head | Tail] = List], S) ->
|
||||
Ann = ?ann(Meta),
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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)]);
|
||||
|
||||
@@ -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}])).
|
||||
|
||||
|
||||
@@ -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:",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -340,6 +340,32 @@ defmodule CalendarTest do
|
||||
assert Calendar.strftime(~N[2019-08-15 17:07:57], "%010A") == "00Thursday"
|
||||
end
|
||||
|
||||
test "limits width to at most 1024 characters" do
|
||||
assert Calendar.strftime(~D[2019-08-15], "%1024d") |> byte_size() == 1024
|
||||
|
||||
assert_raise ArgumentError, "invalid strftime format: width must be at most 1024", fn ->
|
||||
Calendar.strftime(~D[2019-08-15], "%1025d")
|
||||
end
|
||||
|
||||
assert_raise ArgumentError, "invalid strftime format: width must be at most 1024", fn ->
|
||||
Calendar.strftime(~D[2019-08-15], "%10000d")
|
||||
end
|
||||
end
|
||||
|
||||
test "limits width in preferred formats" do
|
||||
assert_raise ArgumentError, "invalid strftime format: width must be at most 1024", fn ->
|
||||
Calendar.strftime(~N[2019-08-15 17:07:57], "%c", preferred_datetime: "%1025d")
|
||||
end
|
||||
|
||||
assert_raise ArgumentError, "invalid strftime format: width must be at most 1024", fn ->
|
||||
Calendar.strftime(~N[2019-08-15 17:07:57], "%x", preferred_date: "%1025d")
|
||||
end
|
||||
|
||||
assert_raise ArgumentError, "invalid strftime format: width must be at most 1024", fn ->
|
||||
Calendar.strftime(~N[2019-08-15 17:07:57], "%X", preferred_time: "%1025H")
|
||||
end
|
||||
end
|
||||
|
||||
test "formats Epoch time with %s" do
|
||||
assert Calendar.strftime(~N[2019-08-15 17:07:57], "%s") == "1565888877"
|
||||
|
||||
|
||||
@@ -514,6 +514,25 @@ defmodule CodeTest do
|
||||
Code.unrequire_files([fixture_path("code_sample.exs")])
|
||||
end
|
||||
|
||||
test "require_file/1 releases the file when compilation fails" do
|
||||
path = tmp_path("bad_require_#{System.unique_integer([:positive])}.ex")
|
||||
|
||||
try do
|
||||
File.write!(path, ~s|raise "boom"|)
|
||||
|
||||
assert_raise RuntimeError, "boom", fn ->
|
||||
Code.require_file(path)
|
||||
end
|
||||
|
||||
assert_raise RuntimeError, "boom", fn ->
|
||||
Code.require_file(path)
|
||||
end
|
||||
after
|
||||
File.rm(path)
|
||||
Code.unrequire_files([path])
|
||||
end
|
||||
end
|
||||
|
||||
test "string_to_quoted!/2 errors take lines/columns/indentation into account" do
|
||||
assert_exception(
|
||||
SyntaxError,
|
||||
|
||||
@@ -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>>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -104,6 +104,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())
|
||||
|
||||
@@ -585,6 +593,14 @@ defmodule Module.Types.DescrTest do
|
||||
assert empty?(difference(dynamic(integer()), integer()))
|
||||
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
|
||||
assert empty?(difference(open_tuple([atom()]), open_tuple([term()])))
|
||||
refute empty?(difference(tuple(), empty_tuple()))
|
||||
@@ -674,6 +690,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 +930,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 +1001,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 +1137,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 +1257,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
|
||||
@@ -1656,6 +1717,7 @@ 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(non_empty_list(integer(), atom()))) == :badproperlist
|
||||
@@ -2114,7 +2176,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
|
||||
@@ -3219,6 +3281,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()))
|
||||
|
||||
@@ -1818,6 +1818,11 @@ defmodule Module.Types.ExprTest do
|
||||
end
|
||||
|
||||
test "Kernel.in/2" do
|
||||
assert typecheck!(
|
||||
[x],
|
||||
x in []
|
||||
) == atom([false])
|
||||
|
||||
assert typecheck!(
|
||||
[x],
|
||||
(
|
||||
@@ -2375,7 +2380,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 +2396,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 +2404,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 +2416,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 +3318,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,20 @@ defmodule Module.Types.InferTest do
|
||||
]
|
||||
end
|
||||
|
||||
test "from 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 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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").
|
||||
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 " <>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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", """
|
||||
|
||||
@@ -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
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -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 _ ->
|
||||
|
||||
Reference in New Issue
Block a user