diff --git a/CHANGELOG.md b/CHANGELOG.md index 87b1bda05e4..2064a9f7a85 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -192,37 +192,12 @@ 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 +## v1.20.0 (2026-06-03) This release requires Erlang/OTP 27+ and is compatible with Erlang/OTP 29. ### 1. Enhancements -#### Elixir - - * [Kernel] Perform type inference across applications - -### 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 - -#### Mix - - * [mix test] Respect --raise when mix test --warnings-as-errors passes with warnings - -### 3. Hard deprecations - -#### Mix - - * [mix compile.elixir] `xref: [exclude: ...]` in your `mix.exs` is deprecated in favor of `elixirc_options: [no_warn_undefined: ...]` - -## v1.20.0-rc.5 (2026-05-13) - -### 1. Enhancements - #### EEx * [EEx] Optimize compiler by flattening expr list only once @@ -230,195 +205,116 @@ 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` - -#### Logger - - * [Logger] Persist log level to app env in `Logger.configure/1` - -#### 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 + * [IEx] Ensure warnings emitted during IEx parsing are properly displayed/printed -#### Mix - - * [mix compile.erlang] Topsort Erlang modules before compilation for proper dependency resolution - -## 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] `<>` 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 +#### Logger - * [ExUnit.CaptureLog] Add `:formatter` option for custom log formatting + * [Logger] Persist log level to app env in `Logger.configure/1` #### 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 + * [Mix] Use `non_executable_binary_to_term` on loopback pubsub + * [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 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 -### 3. Hard deprecations +### 4. Hard deprecations #### Elixir @@ -431,6 +327,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). diff --git a/Makefile b/Makefile index 417aec2ed34..4ad98272574 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/SECURITY.md b/SECURITY.md index d132954cc5b..83d65b6e2a0 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -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 diff --git a/VERSION b/VERSION index b485d6dd84b..39893559155 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.20.0-rc.5 +1.20.0 diff --git a/bin/elixir b/bin/elixir index 8824f4c2df3..dc9f63890af 100755 --- a/bin/elixir +++ b/bin/elixir @@ -6,7 +6,7 @@ set -e -ELIXIR_VERSION=1.20.0-rc.5 +ELIXIR_VERSION=1.20.0 if [ $# -eq 0 ] || { [ $# -eq 1 ] && { [ "$1" = "--help" ] || [ "$1" = "-h" ]; }; }; then cat <&2 diff --git a/bin/elixir.bat b/bin/elixir.bat index d1225fa0854..36e0d010e6b 100644 --- a/bin/elixir.bat +++ b/bin/elixir.bat @@ -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.0 if ""%1""=="""" if ""%2""=="""" goto documentation if /I ""%1""==""--help"" if ""%2""=="""" goto documentation diff --git a/lib/elixir/lib/code.ex b/lib/elixir/lib/code.ex index 97fec1c9ced..5a8204e1eab 100644 --- a/lib/elixir/lib/code.ex +++ b/lib/elixir/lib/code.ex @@ -1634,13 +1634,19 @@ defmodule Code do nil :proceed -> - loaded = - Module.ParallelChecker.verify(fn -> - :elixir_compiler.string(charlist, file, fn _, _ -> :ok end) - end) - - :elixir_code_server.cast({:required, file}) - loaded + try do + loaded = + Module.ParallelChecker.verify(fn -> + :elixir_compiler.string(charlist, file, fn _, _ -> :ok end) + end) + + :elixir_code_server.cast({:required, file}) + loaded + catch + kind, reason -> + :elixir_code_server.call({:release, file}) + :erlang.raise(kind, reason, __STACKTRACE__) + end end end diff --git a/lib/elixir/lib/kernel.ex b/lib/elixir/lib/kernel.ex index d1bbc1403de..a6aea9008a3 100644 --- a/lib/elixir/lib/kernel.ex +++ b/lib/elixir/lib/kernel.ex @@ -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 diff --git a/lib/elixir/lib/kernel/utils.ex b/lib/elixir/lib/kernel/utils.ex index 8d6099ac34e..26c6f6f40f6 100644 --- a/lib/elixir/lib/kernel/utils.ex +++ b/lib/elixir/lib/kernel/utils.ex @@ -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) diff --git a/lib/elixir/lib/module/types/apply.ex b/lib/elixir/lib/module/types/apply.ex index d49b30cb34d..870d797705d 100644 --- a/lib/elixir/lib/module/types/apply.ex +++ b/lib/elixir/lib/module/types/apply.ex @@ -242,7 +242,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()}]}, diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex index 298e39786ed..c45e3f13c37 100644 --- a/lib/elixir/lib/module/types/descr.ex +++ b/lib/elixir/lib/module/types/descr.ex @@ -2205,14 +2205,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, @@ -5026,15 +5025,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 +5042,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 +5060,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 +5097,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} -> diff --git a/lib/elixir/lib/module/types/helpers.ex b/lib/elixir/lib/module/types/helpers.ex index 629cb26df9f..f83951cae17 100644 --- a/lib/elixir/lib/module/types/helpers.ex +++ b/lib/elixir/lib/module/types/helpers.ex @@ -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 diff --git a/lib/elixir/lib/module/types/pattern.ex b/lib/elixir/lib/module/types/pattern.ex index 346e937c356..f4c8193b5c2 100644 --- a/lib/elixir/lib/module/types/pattern.ex +++ b/lib/elixir/lib/module/types/pattern.ex @@ -1564,7 +1564,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 +1732,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)} """ diff --git a/lib/elixir/pages/references/compatibility-and-deprecations.md b/lib/elixir/pages/references/compatibility-and-deprecations.md index 7bfee6c3349..a6a0c4c54f6 100644 --- a/lib/elixir/pages/references/compatibility-and-deprecations.md +++ b/lib/elixir/pages/references/compatibility-and-deprecations.md @@ -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 diff --git a/lib/elixir/scripts/elixir_docs.exs b/lib/elixir/scripts/elixir_docs.exs index 05ed1ab2f10..ba507d65224 100644 --- a/lib/elixir/scripts/elixir_docs.exs +++ b/lib/elixir/scripts/elixir_docs.exs @@ -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: [ diff --git a/lib/elixir/scripts/mix_docs.exs b/lib/elixir/scripts/mix_docs.exs index bed7f14e700..a74a1d3dbf6 100644 --- a/lib/elixir/scripts/mix_docs.exs +++ b/lib/elixir/scripts/mix_docs.exs @@ -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: [ diff --git a/lib/elixir/src/elixir_code_server.erl b/lib/elixir/src/elixir_code_server.erl index e2baa51c331..bb563d6e4a2 100644 --- a/lib/elixir/src/elixir_code_server.erl +++ b/lib/elixir/src/elixir_code_server.erl @@ -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)). diff --git a/lib/elixir/src/elixir_erl_pass.erl b/lib/elixir/src/elixir_erl_pass.erl index 3a42a96c754..19c9bf9d358 100644 --- a/lib/elixir/src/elixir_erl_pass.erl +++ b/lib/elixir/src/elixir_erl_pass.erl @@ -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), diff --git a/lib/elixir/src/elixir_expand.erl b/lib/elixir/src/elixir_expand.erl index 5e6e2e8a618..4b8937fb404 100644 --- a/lib/elixir/src/elixir_expand.erl +++ b/lib/elixir/src/elixir_expand.erl @@ -287,9 +287,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 +970,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 +993,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 diff --git a/lib/elixir/src/elixir_quote.erl b/lib/elixir/src/elixir_quote.erl index f7acef2407d..8972d066818 100644 --- a/lib/elixir/src/elixir_quote.erl +++ b/lib/elixir/src/elixir_quote.erl @@ -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}])). diff --git a/lib/elixir/src/elixir_tokenizer.erl b/lib/elixir/src/elixir_tokenizer.erl index 0b92542a9f7..6592aed98ce 100644 --- a/lib/elixir/src/elixir_tokenizer.erl +++ b/lib/elixir/src/elixir_tokenizer.erl @@ -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:", diff --git a/lib/elixir/src/elixir_utils.erl b/lib/elixir/src/elixir_utils.erl index 556e4243582..a7d2bea6f94 100644 --- a/lib/elixir/src/elixir_utils.erl +++ b/lib/elixir/src/elixir_utils.erl @@ -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) diff --git a/lib/elixir/test/elixir/code_test.exs b/lib/elixir/test/elixir/code_test.exs index 18d024511d1..036c516401b 100644 --- a/lib/elixir/test/elixir/code_test.exs +++ b/lib/elixir/test/elixir/code_test.exs @@ -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, diff --git a/lib/elixir/test/elixir/kernel/errors_test.exs b/lib/elixir/test/elixir/kernel/errors_test.exs index 20323cfbb5a..2c1dc605ae7 100644 --- a/lib/elixir/test/elixir/kernel/errors_test.exs +++ b/lib/elixir/test/elixir/kernel/errors_test.exs @@ -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", diff --git a/lib/elixir/test/elixir/kernel/expansion_test.exs b/lib/elixir/test/elixir/kernel/expansion_test.exs index a5c407e6239..31fdac1288b 100644 --- a/lib/elixir/test/elixir/kernel/expansion_test.exs +++ b/lib/elixir/test/elixir/kernel/expansion_test.exs @@ -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 diff --git a/lib/elixir/test/elixir/kernel/guard_test.exs b/lib/elixir/test/elixir/kernel/guard_test.exs index 7cd704faaea..543c3dd2e41 100644 --- a/lib/elixir/test/elixir/kernel/guard_test.exs +++ b/lib/elixir/test/elixir/kernel/guard_test.exs @@ -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 diff --git a/lib/elixir/test/elixir/kernel/parser_test.exs b/lib/elixir/test/elixir/kernel/parser_test.exs index 9c7e56d3828..bf37abb5d28 100644 --- a/lib/elixir/test/elixir/kernel/parser_test.exs +++ b/lib/elixir/test/elixir/kernel/parser_test.exs @@ -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 diff --git a/lib/elixir/test/elixir/kernel_test.exs b/lib/elixir/test/elixir/kernel_test.exs index a74e0cef696..a481b061d39 100644 --- a/lib/elixir/test/elixir/kernel_test.exs +++ b/lib/elixir/test/elixir/kernel_test.exs @@ -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 diff --git a/lib/elixir/test/elixir/macro/env_test.exs b/lib/elixir/test/elixir/macro/env_test.exs index 9406462db08..b2ba307ab7d 100644 --- a/lib/elixir/test/elixir/macro/env_test.exs +++ b/lib/elixir/test/elixir/macro/env_test.exs @@ -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 diff --git a/lib/elixir/test/elixir/module/types/expr_test.exs b/lib/elixir/test/elixir/module/types/expr_test.exs index cd5c0f89b69..7f31e1aeb7f 100644 --- a/lib/elixir/test/elixir/module/types/expr_test.exs +++ b/lib/elixir/test/elixir/module/types/expr_test.exs @@ -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 diff --git a/lib/elixir/test/elixir/module/types/helpers_test.exs b/lib/elixir/test/elixir/module/types/helpers_test.exs index 144ce090f20..efa09afb32f 100644 --- a/lib/elixir/test/elixir/module/types/helpers_test.exs +++ b/lib/elixir/test/elixir/module/types/helpers_test.exs @@ -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 diff --git a/lib/elixir/test/elixir/module/types/infer_test.exs b/lib/elixir/test/elixir/module/types/infer_test.exs index facf6ba68d7..20929ab6664 100644 --- a/lib/elixir/test/elixir/module/types/infer_test.exs +++ b/lib/elixir/test/elixir/module/types/infer_test.exs @@ -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 diff --git a/lib/elixir/test/erlang/control_test.erl b/lib/elixir/test/erlang/control_test.erl index da310d53099..5648fb84d4a 100644 --- a/lib/elixir/test/erlang/control_test.erl +++ b/lib/elixir/test/erlang/control_test.erl @@ -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"). diff --git a/lib/mix/lib/mix/tasks/format.ex b/lib/mix/lib/mix/tasks/format.ex index 1dbc398563c..5cfad60e8d3 100644 --- a/lib/mix/lib/mix/tasks/format.ex +++ b/lib/mix/lib/mix/tasks/format.ex @@ -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 diff --git a/lib/mix/test/mix/tasks/format_test.exs b/lib/mix/test/mix/tasks/format_test.exs index eaf1aa1aade..1498d2a162f 100644 --- a/lib/mix/test/mix/tasks/format_test.exs +++ b/lib/mix/test/mix/tasks/format_test.exs @@ -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", """ diff --git a/lib/mix/test/mix/tasks/xref_test.exs b/lib/mix/test/mix/tasks/xref_test.exs index d5132031e9e..d925ad72e9b 100644 --- a/lib/mix/test/mix/tasks/xref_test.exs +++ b/lib/mix/test/mix/tasks/xref_test.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 } ]