From 6935f747e3dd1e05b168d26791507385c3986ed1 Mon Sep 17 00:00:00 2001 From: Andrea Leopardi Date: Mon, 25 Sep 2023 08:39:12 +0200 Subject: [PATCH 0001/1886] Fix Markdown in docs for the Config module --- lib/elixir/lib/config.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/lib/config.ex b/lib/elixir/lib/config.ex index 9b885c6ad9..cc39e43baa 100644 --- a/lib/elixir/lib/config.ex +++ b/lib/elixir/lib/config.ex @@ -86,7 +86,7 @@ defmodule Config do the `mix.exs` file and inside custom Mix tasks, which always within the `Mix.Tasks` namespace. - ## config/runtime.exs + ## `config/runtime.exs` For runtime configuration, you can use the `config/runtime.exs` file. It is executed right before applications start in both Mix and releases From c7bd0fe7e77eeae31a07eef0ff9bdd31a3259dfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 25 Sep 2023 12:55:17 +0200 Subject: [PATCH 0002/1886] Do not emit duplicate warnings from tokenizer, closes #12961 --- lib/eex/lib/eex/compiler.ex | 8 ++-- lib/eex/test/eex/tokenizer_test.exs | 6 --- lib/eex/test/eex_test.exs | 72 ++++++++++++++++------------- 3 files changed, 44 insertions(+), 42 deletions(-) diff --git a/lib/eex/lib/eex/compiler.ex b/lib/eex/lib/eex/compiler.ex index 292ddf8663..4ace24fde1 100644 --- a/lib/eex/lib/eex/compiler.ex +++ b/lib/eex/lib/eex/compiler.ex @@ -71,11 +71,9 @@ defmodule EEx.Compiler do {:ok, expr, new_line, new_column, rest} -> {key, expr} = case :elixir_tokenizer.tokenize(expr, 1, file: "eex", check_terminators: false) do - {:ok, _line, _column, warnings, tokens} -> - Enum.each(Enum.reverse(warnings), fn {location, msg} -> - :elixir_errors.erl_warn(location, state.file, msg) - end) - + {:ok, _line, _column, _warnings, tokens} -> + # We ignore warnings because the code will be tokenized + # again later with the right line+column info token_key(tokens, expr) {:error, _, _, _, _} -> diff --git a/lib/eex/test/eex/tokenizer_test.exs b/lib/eex/test/eex/tokenizer_test.exs index 328bf6dc3b..f452bfbcf2 100644 --- a/lib/eex/test/eex/tokenizer_test.exs +++ b/lib/eex/test/eex/tokenizer_test.exs @@ -5,12 +5,6 @@ defmodule EEx.TokenizerTest do @opts [indentation: 0, trim: false] - test "tokenizer warning" do - assert ExUnit.CaptureIO.capture_io(:stderr, fn -> - EEx.tokenize(~c"foo <% :'bar' %>", @opts) - end) =~ "found quoted atom \"bar\" but the quotes are not required" - end - test "simple charlists" do assert EEx.tokenize(~c"foo", @opts) == {:ok, [{:text, ~c"foo", %{column: 1, line: 1}}, {:eof, %{column: 4, line: 1}}]} diff --git a/lib/eex/test/eex_test.exs b/lib/eex/test/eex_test.exs index 739e572e56..2e60d90354 100644 --- a/lib/eex/test/eex_test.exs +++ b/lib/eex/test/eex_test.exs @@ -286,6 +286,19 @@ defmodule EExTest do end end + test "when <%!-- is not closed" do + message = """ + my_file.eex:1:5: expected closing '--%>' for EEx expression + | + 1 | foo <%!-- bar + | ^\ + """ + + assert_raise EEx.SyntaxError, message, fn -> + EEx.compile_string("foo <%!-- bar", file: "my_file.eex") + end + end + test "when the token is invalid" do message = """ nofile:1:5: expected closing '%>' for EEx expression @@ -476,39 +489,24 @@ defmodule EExTest do end end - test "when middle expression has a modifier" do - assert ExUnit.CaptureIO.capture_io(:stderr, fn -> - EEx.compile_string("foo <%= if true do %>true<%= else %>false<% end %>") - end) =~ ~s[unexpected beginning of EEx tag \"<%=\" on \"<%= else %>\"] - end - - test "when end expression has a modifier" do - assert ExUnit.CaptureIO.capture_io(:stderr, fn -> - EEx.compile_string("foo <%= if true do %>true<% else %>false<%= end %>") - end) =~ - ~s[unexpected beginning of EEx tag \"<%=\" on \"<%= end %>\"] - end - - test "when trying to use marker '/' without implementation" do + test "when trying to use marker '|' without implementation" do msg = - ~r/unsupported EEx syntax <%\/ %> \(the syntax is valid but not supported by the current EEx engine\)/ + ~r/unsupported EEx syntax <%| %> \(the syntax is valid but not supported by the current EEx engine\)/ assert_raise EEx.SyntaxError, msg, fn -> - EEx.compile_string("<%/ true %>") + EEx.compile_string("<%| true %>") end end - test "when trying to use marker '|' without implementation" do + test "when trying to use marker '/' without implementation" do msg = - ~r/unsupported EEx syntax <%| %> \(the syntax is valid but not supported by the current EEx engine\)/ + ~r/unsupported EEx syntax <%\/ %> \(the syntax is valid but not supported by the current EEx engine\)/ assert_raise EEx.SyntaxError, msg, fn -> - EEx.compile_string("<%| true %>") + EEx.compile_string("<%/ true %>") end end - end - describe "error messages" do test "honor line numbers" do assert_raise EEx.SyntaxError, "nofile:100:6: expected closing '%>' for EEx expression", @@ -529,18 +527,30 @@ defmodule EExTest do EEx.compile_string("foo <%= bar", file: "my_file.eex") end end + end - test "when <%!-- is not closed" do - message = """ - my_file.eex:1:5: expected closing '--%>' for EEx expression - | - 1 | foo <%!-- bar - | ^\ - """ + describe "warnings" do + test "when middle expression has a modifier" do + assert ExUnit.CaptureIO.capture_io(:stderr, fn -> + EEx.compile_string("foo <%= if true do %>true<%= else %>false<% end %>") + end) =~ ~s[unexpected beginning of EEx tag \"<%=\" on \"<%= else %>\"] + end - assert_raise EEx.SyntaxError, message, fn -> - EEx.compile_string("foo <%!-- bar", file: "my_file.eex") - end + test "when end expression has a modifier" do + assert ExUnit.CaptureIO.capture_io(:stderr, fn -> + EEx.compile_string("foo <%= if true do %>true<% else %>false<%= end %>") + end) =~ + ~s[unexpected beginning of EEx tag \"<%=\" on \"<%= end %>\"] + end + + test "from tokenizer" do + warning = + ExUnit.CaptureIO.capture_io(:stderr, fn -> + EEx.compile_string(~s'<%= :"foo" %>', file: "tokenizer.ex") + end) + + assert warning =~ "found quoted atom \"foo\" but the quotes are not required" + assert warning =~ "tokenizer.ex:1:5" end end From e23c34db978f455a482f71458dde3ade186fa68e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Sep 2023 23:52:14 +0200 Subject: [PATCH 0003/1886] Bump DavidAnson/markdownlint-cli2-action from 11.0.0 to 13.0.0 (#12963) Bumps [DavidAnson/markdownlint-cli2-action](https://github.com/davidanson/markdownlint-cli2-action) from 11.0.0 to 13.0.0. - [Release notes](https://github.com/davidanson/markdownlint-cli2-action/releases) - [Commits](https://github.com/davidanson/markdownlint-cli2-action/compare/v11.0.0...v13.0.0) --- updated-dependencies: - dependency-name: DavidAnson/markdownlint-cli2-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci-markdown.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-markdown.yml b/.github/workflows/ci-markdown.yml index 0fb12d49b1..de96eeb629 100644 --- a/.github/workflows/ci-markdown.yml +++ b/.github/workflows/ci-markdown.yml @@ -27,7 +27,7 @@ jobs: fetch-depth: 10 - name: Run markdownlint - uses: DavidAnson/markdownlint-cli2-action@v11.0.0 + uses: DavidAnson/markdownlint-cli2-action@v13.0.0 with: globs: | lib/elixir/pages/**/*.md From fd7ea5beb07513a1c9dd05cc0d49458b86d09977 Mon Sep 17 00:00:00 2001 From: Lucas Francisco da Matta Vegi Date: Tue, 26 Sep 2023 13:35:46 -0300 Subject: [PATCH 0004/1886] "Primitive Obsession" added (Anti-pattern documentation) (#12962) --- .../anti-patterns/design-anti-patterns.md | 36 ++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/lib/elixir/pages/anti-patterns/design-anti-patterns.md b/lib/elixir/pages/anti-patterns/design-anti-patterns.md index 376b5182ca..1ab379438b 100644 --- a/lib/elixir/pages/anti-patterns/design-anti-patterns.md +++ b/lib/elixir/pages/anti-patterns/design-anti-patterns.md @@ -5,7 +5,41 @@ play within a codebase. ## Primitive obsession -TODO +#### Problem + +This anti-pattern can be felt when Elixir basic types (for example, *integer*, *float*, and *string*) are abusively used in function parameters and code variables, rather than creating specific composite data types (for example, *tuples* and *structs*) that can better represent a domain. + +#### Example + +An example of this anti-pattern is the use of a single *string* to represent an `Address`. An `Address` is a more complex structure than a simple basic (aka, primitive) value. + +```elixir +defmodule MyApp do + def process_address(address) when is_binary(address) do + # Do something with address... + end +end +``` + +Another example of this anti-pattern is using floating numbers to model money and currency, when [richer data structures should be preferred](https://hexdocs.pm/ex_money/). + +#### Refactoring + +We can create an `Address` struct to remove this anti-pattern, better representing this domain through a composite type. Additionally, we can modify the `process_address/1` function to accept a parameter of type `Address` instead of a *string*. With this modification, we can extract each field of this composite type individually when needed. + +```elixir +defmodule Address do + defstruct [:street, :city, :state, :postal_code, :country] +end +``` + +```elixir +defmodule MyApp do + def process_address(%Address{} = address) do + # Do something with address... + end +end +``` ## Boolean obsession From bc3c915563a59c40c476fd2d512d43ccdac793e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 26 Sep 2023 22:22:05 +0200 Subject: [PATCH 0005/1886] Boolean obsession (#12965) --- .../anti-patterns/design-anti-patterns.md | 72 ++++++++++++++++--- 1 file changed, 62 insertions(+), 10 deletions(-) diff --git a/lib/elixir/pages/anti-patterns/design-anti-patterns.md b/lib/elixir/pages/anti-patterns/design-anti-patterns.md index 1ab379438b..d897be535d 100644 --- a/lib/elixir/pages/anti-patterns/design-anti-patterns.md +++ b/lib/elixir/pages/anti-patterns/design-anti-patterns.md @@ -7,17 +7,17 @@ play within a codebase. #### Problem -This anti-pattern can be felt when Elixir basic types (for example, *integer*, *float*, and *string*) are abusively used in function parameters and code variables, rather than creating specific composite data types (for example, *tuples* and *structs*) that can better represent a domain. +This anti-pattern happens when Elixir basic types (for example, *integer*, *float*, and *string*) are abusively used in function parameters and code variables, rather than creating specific composite data types (for example, *tuples* and *structs*) that can better represent a domain. #### Example An example of this anti-pattern is the use of a single *string* to represent an `Address`. An `Address` is a more complex structure than a simple basic (aka, primitive) value. ```elixir -defmodule MyApp do +defmodule MyApp do def process_address(address) when is_binary(address) do # Do something with address... - end + end end ``` @@ -25,25 +25,77 @@ Another example of this anti-pattern is using floating numbers to model money an #### Refactoring -We can create an `Address` struct to remove this anti-pattern, better representing this domain through a composite type. Additionally, we can modify the `process_address/1` function to accept a parameter of type `Address` instead of a *string*. With this modification, we can extract each field of this composite type individually when needed. +We can create an `Address` struct to remove this anti-pattern, better representing this domain through a composite type. Additionally, we can modify the `process_address/1` function to accept a parameter of type `Address` instead of a *string*. With this modification, we can extract each field of this composite type individually when needed. ```elixir -defmodule Address do +defmodule Address do defstruct [:street, :city, :state, :postal_code, :country] end ``` ```elixir -defmodule MyApp do +defmodule MyApp do def process_address(%Address{} = address) do # Do something with address... - end + end end ``` ## Boolean obsession -TODO +#### Problem + +This anti-pattern happens when booleans are used instead of atoms to encode information. The usage of booleans themselves is not an anti-pattern, but whenever multiple booleans are used with overlapping states, replacing the booleans by atoms (or composite data types such as *tuples*) may lead to clearer code. + +This is a special case of [*Primitive obsession*](#primitive-obsession), specific to boolean values. + +#### Example + +An example of this anti-pattern is a function that receives two or more options, such as `editor: true` and `admin: true`, to configure its behaviour in overlapping ways. In the code below, the `:editor` option has no effect if `:admin` is set, meaning that the `:admin` option has higher priority than `:editor`, and they are ultimately related. + +```elixir +defmodule MyApp do + def process(invoice, options \\ []) do + cond do + options[:admin] -> # Is an admin + options[:editor] -> # Is an editor + true -> # Is none + end + end +end +``` + +#### Refactoring + +Instead of using multiple options, the code above could be refactored to receive a single option, called `:role`, that can be either `:admin`, `:editor`, or `:default`: + +```elixir +defmodule MyApp do + def process(invoice, options \\ []) do + case Keyword.get(options, :role, :default) do + :admin -> # Is an admin + :editor -> # Is an editor + :default -> # Is none + end + end +end +``` + +This anti-pattern may also happen in our own data structures. For example, we may define a `User` struct with two boolean fields, `:editor` and `:admin`, while a single field named `:role` may be preferred. + +Finally, it is worth noting that using atoms may be preferred even when we have a single boolean argument/option. For example, imagine an invoice may be set as approved/unapproved. One option is to provide a function that expects a boolean: + +```elixir +MyApp.update(invoice, approved: true) +``` + +However, using atoms may read better and make it simpler to add further states (such as pending) in the future: + +```elixir +MyApp.update(invoice, status: :approved) +``` + +Remember booleans are internally represented as atoms. Therefore there is no performance penalty in one approach over the other. ## Working with invalid data @@ -146,7 +198,7 @@ defmodule AlternativeInteger do def parse(string) do Integer.parse(string) end - + @spec parse_discard_rest(String.t()) :: integer() | :error def parse_discard_rest(string) do case Integer.parse(string) do @@ -280,7 +332,7 @@ defmodule OrderItem do total = (item.price + item.taxes) * item.amount discount = find_discount(item) - unless is_nil(discount) do + unless is_nil(discount) do total - total * discount else total From f23b28374b9670601956f4c699e73d7ca4f2276b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 27 Sep 2023 22:54:19 +0200 Subject: [PATCH 0006/1886] Move unescape map to Kernel for simplicity --- lib/elixir/lib/kernel.ex | 13 +++++++++++-- lib/elixir/lib/regex.ex | 13 ------------- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/lib/elixir/lib/kernel.ex b/lib/elixir/lib/kernel.ex index a8c8ca5018..c06253fc00 100644 --- a/lib/elixir/lib/kernel.ex +++ b/lib/elixir/lib/kernel.ex @@ -6188,16 +6188,25 @@ defmodule Kernel do defmacro sigil_r(term, modifiers) defmacro sigil_r({:<<>>, _meta, [string]}, options) when is_binary(string) do - binary = :elixir_interpolation.unescape_string(string, &Regex.unescape_map/1) + binary = :elixir_interpolation.unescape_string(string, ®ex_unescape_map/1) regex = Regex.compile!(binary, :binary.list_to_bin(options)) Macro.escape(regex) end defmacro sigil_r({:<<>>, meta, pieces}, options) do - binary = {:<<>>, meta, unescape_tokens(pieces, &Regex.unescape_map/1)} + binary = {:<<>>, meta, unescape_tokens(pieces, ®ex_unescape_map/1)} quote(do: Regex.compile!(unquote(binary), unquote(:binary.list_to_bin(options)))) end + defp regex_unescape_map(:newline), do: true + defp regex_unescape_map(?f), do: ?\f + defp regex_unescape_map(?n), do: ?\n + defp regex_unescape_map(?r), do: ?\r + defp regex_unescape_map(?t), do: ?\t + defp regex_unescape_map(?v), do: ?\v + defp regex_unescape_map(?a), do: ?\a + defp regex_unescape_map(_), do: false + @doc ~S""" Handles the sigil `~R` for regular expressions. diff --git a/lib/elixir/lib/regex.ex b/lib/elixir/lib/regex.ex index 247c6b4a98..7d18c1a1a6 100644 --- a/lib/elixir/lib/regex.ex +++ b/lib/elixir/lib/regex.ex @@ -852,19 +852,6 @@ defmodule Regex do # Helpers - @doc false - # Unescape map function used by Macro.unescape_string. - def unescape_map(:newline), do: true - def unescape_map(?f), do: ?\f - def unescape_map(?n), do: ?\n - def unescape_map(?r), do: ?\r - def unescape_map(?t), do: ?\t - def unescape_map(?v), do: ?\v - def unescape_map(?a), do: ?\a - def unescape_map(_), do: false - - # Private Helpers - defp translate_options(<>, acc), do: translate_options(t, [:unicode, :ucp | acc]) defp translate_options(<>, acc), do: translate_options(t, [:caseless | acc]) defp translate_options(<>, acc), do: translate_options(t, [:extended | acc]) From 0ba36abc978b8ba108aa403fd9671362f7ad28d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 28 Sep 2023 09:01:50 +0200 Subject: [PATCH 0007/1886] Add namespace trespassing anti-pattern (#12966) --- .../pages/anti-patterns/code-anti-patterns.md | 36 ++++++++++++++++++- .../anti-patterns/design-anti-patterns.md | 6 +--- 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/lib/elixir/pages/anti-patterns/code-anti-patterns.md b/lib/elixir/pages/anti-patterns/code-anti-patterns.md index fa2c36977c..b554970520 100644 --- a/lib/elixir/pages/anti-patterns/code-anti-patterns.md +++ b/lib/elixir/pages/anti-patterns/code-anti-patterns.md @@ -445,7 +445,41 @@ However, keep in mind using a module attribute or defining the atoms in the modu ## Namespace trespassing -TODO. +#### Problem + +This anti-pattern manifests when a package author or a library defines modules outside of its "namespace". A library should use its name as a "prefix" for all of its modules. For example, a package named `:my_lib` should define all of its modules within the `MyLib` namespace, such as `MyLib.User`, `MyLib.SubModule`, `MyLib.Application`, and `MyLib` itself. + +This is important because the Erlang VM can only load one instance of a module at a time. So if there are multiple libraries that define the same module, then they are incompatible with each other due to this limitation. By always using the library name as a prefix, it avoids module name clashes due to the unique prefix. + +#### Example + +This problem commonly manifests when writing an extension of another library. For example, imagine you are writing a package that adds authentication to [Plug](https://github.com/elixir-plug/plug) called `:plug_auth`. You must avoid defining modules within the `Plug` namespace: + +```elixir +defmodule Plug.Auth do + # ... +end +``` + +Even if `Plug` does not currently define a `Plug.Auth` module, it may add such a module in the future, which would ultimately conflict with `plug_auth`'s definition. + +#### Refactoring + +Given the package is named `:plug_auth`, it must define modules inside the `Plug.Auth` namespace: + +```elixir +defmodule PlugAuth do + # ... +end +``` + +There are few known exceptions to this anti-pattern: + + * [Protocol implementations](`Kernel.defimpl/2`) are, by design, defined under the protocol namespace + + * [Custom Mix tasks](`Mix.Task`) are always defined under the `Mix.Tasks` namespace, such as `Mix.Tasks.PlugAuth` + + * If you are the maintainer for both `plug` and `plug_auth`, then you may allow `plug_auth` to define modules with the `Plug` namespace, such as `Plug.Auth`. However, you are responsible for avoiding or managing any conflicts that may arise in the future ## Speculative assumptions diff --git a/lib/elixir/pages/anti-patterns/design-anti-patterns.md b/lib/elixir/pages/anti-patterns/design-anti-patterns.md index d897be535d..e8cab4e1b5 100644 --- a/lib/elixir/pages/anti-patterns/design-anti-patterns.md +++ b/lib/elixir/pages/anti-patterns/design-anti-patterns.md @@ -83,7 +83,7 @@ end This anti-pattern may also happen in our own data structures. For example, we may define a `User` struct with two boolean fields, `:editor` and `:admin`, while a single field named `:role` may be preferred. -Finally, it is worth noting that using atoms may be preferred even when we have a single boolean argument/option. For example, imagine an invoice may be set as approved/unapproved. One option is to provide a function that expects a boolean: +Finally, it is worth noting that using atoms may be preferred even when we have a single boolean argument/option. For example, consider an invoice which may be set as approved/unapproved. One option is to provide a function that expects a boolean: ```elixir MyApp.update(invoice, approved: true) @@ -343,10 +343,6 @@ end This refactoring is only possible when you own both modules. If the module you are invoking belongs to another application, then it is not possible to add new functions to it, and your only option is to define an additional module that augments the third-party module. -## Excessive side-effects - -TODO - ## Using exceptions for control-flow #### Problem From dc5f8de3f92e59027bf61e1a9e6794c87a7f651f Mon Sep 17 00:00:00 2001 From: Wojtek Mach Date: Thu, 28 Sep 2023 11:11:09 +0200 Subject: [PATCH 0008/1886] Update elixir/pages/anti-patterns/code-anti-patterns.md (#12968) --- lib/elixir/pages/anti-patterns/code-anti-patterns.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/pages/anti-patterns/code-anti-patterns.md b/lib/elixir/pages/anti-patterns/code-anti-patterns.md index b554970520..053bca5d82 100644 --- a/lib/elixir/pages/anti-patterns/code-anti-patterns.md +++ b/lib/elixir/pages/anti-patterns/code-anti-patterns.md @@ -465,7 +465,7 @@ Even if `Plug` does not currently define a `Plug.Auth` module, it may add such a #### Refactoring -Given the package is named `:plug_auth`, it must define modules inside the `Plug.Auth` namespace: +Given the package is named `:plug_auth`, it must define modules inside the `PlugAuth` namespace: ```elixir defmodule PlugAuth do From bd6ada3be89385462fda1c67f3dbfaf6f8f90b21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 28 Sep 2023 18:14:08 +0200 Subject: [PATCH 0009/1886] Update SECURITY.md --- SECURITY.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index 395cf95c69..69252d3ad7 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -15,10 +15,10 @@ Elixir version | Support ## Announcements -New releases are announced in the read-only [announcements mailing list](https://groups.google.com/group/elixir-lang-ann). You can subscribe by sending an email to elixir-lang-ann+subscribe@googlegroups.com and replying to the confirmation email. +New releases are announced in the read-only [announcements mailing list](https://groups.google.com/group/elixir-lang-ann). You can subscribe by sending an email to elixir-lang-ann+subscribe@googlegroups.com and replying to the confirmation email. Security notifications [will be tagged with `[security]`](https://groups.google.com/forum/#!searchin/elixir-lang-ann/%5Bsecurity%5D%7Csort:date). -Security notifications [will be tagged with `[security]`](https://groups.google.com/forum/#!searchin/elixir-lang-ann/%5Bsecurity%5D%7Csort:date). +You may also see [all releases](https://github.com/elixir-lang/elixir/releases) and [consult all disclosed vulnerabilities](https://github.com/elixir-lang/elixir/security) on GitHub. ## Reporting a vulnerability -Please disclose security vulnerabilities privately at elixir-security@googlegroups.com +[Please disclose security vulnerabilities privately via GitHub](https://github.com/elixir-lang/elixir/security). From d89a2a448e5ac088f59d1a0a758f23868205cb0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 28 Sep 2023 21:34:22 +0200 Subject: [PATCH 0010/1886] Ensure full directory match on mix format, closes #12969 --- lib/mix/lib/mix/tasks/format.ex | 11 +++++++++-- lib/mix/test/mix/tasks/format_test.exs | 9 ++++++++- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/lib/mix/lib/mix/tasks/format.ex b/lib/mix/lib/mix/tasks/format.ex index 4616b403b5..1e24fed462 100644 --- a/lib/mix/lib/mix/tasks/format.ex +++ b/lib/mix/lib/mix/tasks/format.ex @@ -583,8 +583,15 @@ defmodule Mix.Tasks.Format do defp recur_formatter_opts_for_file(file, {formatter_opts, subs}) do Enum.find_value(subs, formatter_opts, fn {sub, formatter_opts_and_subs} -> - if String.starts_with?(file, sub) do - recur_formatter_opts_for_file(file, formatter_opts_and_subs) + size = byte_size(sub) + + case file do + <> + when prefix == sub and dir_separator in [?\\, ?/] -> + recur_formatter_opts_for_file(file, formatter_opts_and_subs) + + _ -> + nil end end) end diff --git a/lib/mix/test/mix/tasks/format_test.exs b/lib/mix/test/mix/tasks/format_test.exs index 70ca3d36f0..a5d43fa295 100644 --- a/lib/mix/test/mix/tasks/format_test.exs +++ b/lib/mix/test/mix/tasks/format_test.exs @@ -573,11 +573,18 @@ defmodule Mix.Tasks.FormatTest do test "reads exported configuration from subdirectories", context do in_tmp(context.test, fn -> File.write!(".formatter.exs", """ - [subdirectories: ["lib"]] + [subdirectories: ["li", "lib"]] """) + # We also create a directory called li to ensure files + # from lib won't accidentally match on li. + File.mkdir_p!("li") File.mkdir_p!("lib") + File.write!("li/.formatter.exs", """ + [inputs: "**/*", locals_without_parens: [other_fun: 2]] + """) + File.write!("lib/.formatter.exs", """ [inputs: "a.ex", locals_without_parens: [my_fun: 2]] """) From 883631f5a6e164f34a576fb4b33c16348c940eeb Mon Sep 17 00:00:00 2001 From: Artem Solomatin Date: Sat, 30 Sep 2023 10:34:25 +0300 Subject: [PATCH 0011/1886] Add typespec for rel_templates_path (#12970) --- lib/mix/lib/mix/release.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/mix/lib/mix/release.ex b/lib/mix/lib/mix/release.ex index d2271b10cb..be395713e6 100644 --- a/lib/mix/lib/mix/release.ex +++ b/lib/mix/lib/mix/release.ex @@ -755,6 +755,7 @@ defmodule Mix.Release do @doc """ Finds a template path for the release. """ + @spec rel_templates_path(t, Path.t()) :: binary def rel_templates_path(release, path) do Path.join(release.options[:rel_templates_path] || "rel", path) end From 3ed3bf9108e4c878e878b4ad040f4e5a8d1212e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sat, 30 Sep 2023 11:10:08 +0200 Subject: [PATCH 0012/1886] Deprecate ~R/.../ in favor of ~r/.../ --- CHANGELOG.md | 1 + lib/elixir/lib/kernel.ex | 24 +++++-------------- lib/elixir/lib/regex.ex | 2 +- .../compatibility-and-deprecations.md | 1 + lib/elixir/test/elixir/inspect_test.exs | 2 -- lib/elixir/test/elixir/kernel_test.exs | 7 +----- lib/elixir/test/elixir/regex_test.exs | 4 ---- 7 files changed, 10 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca7bf0e2b9..86bac9fc87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -100,6 +100,7 @@ TODO: Guides, diagrams, anti-patterns, cheatsheets. * [Date] Deprecate inferring a range with negative step, call `Date.range/3` with a negative step instead * [Enum] Deprecate passing a range with negative step on `Enum.slice/2`, give `first..last//1` instead + * [Kernel] `~R/.../` is deprecated in favor of `~r/.../`. This is because `~R/.../` still allowed escape codes, which did not fit the definition of uppercase sigils * [String] Deprecate passing a range with negative step on `String.slice/2`, give `first..last//1` instead #### ExUnit diff --git a/lib/elixir/lib/kernel.ex b/lib/elixir/lib/kernel.ex index c06253fc00..158273238d 100644 --- a/lib/elixir/lib/kernel.ex +++ b/lib/elixir/lib/kernel.ex @@ -6207,25 +6207,13 @@ defmodule Kernel do defp regex_unescape_map(?a), do: ?\a defp regex_unescape_map(_), do: false - @doc ~S""" - Handles the sigil `~R` for regular expressions. - - It returns a regular expression pattern without interpolations and - without escape characters. Note it still supports escape of Regex - tokens (such as escaping `+` or `?`) and it also requires you to - escape the closing sigil character itself if it appears on the Regex. - - More information on regexes can be found in the `Regex` module. - - ## Examples - - iex> Regex.match?(~R(f#{1,3}o), "f#o") - true - - """ - defmacro sigil_R(term, modifiers) - + @doc false defmacro sigil_R({:<<>>, _meta, [string]}, options) when is_binary(string) do + IO.warn( + "~R/.../ is deprecated, use ~r/.../ instead", + Macro.Env.stacktrace(__CALLER__) + ) + regex = Regex.compile!(string, :binary.list_to_bin(options)) Macro.escape(regex) end diff --git a/lib/elixir/lib/regex.ex b/lib/elixir/lib/regex.ex index 7d18c1a1a6..c0d426e87e 100644 --- a/lib/elixir/lib/regex.ex +++ b/lib/elixir/lib/regex.ex @@ -7,7 +7,7 @@ defmodule Regex do in the [`:re` module documentation](`:re`). Regular expressions in Elixir can be created using the sigils - `~r` (see `sigil_r/2`) or `~R` (see `sigil_R/2`): + `~r` (see `sigil_r/2`): # A simple regular expression that matches foo anywhere in the string ~r/foo/ diff --git a/lib/elixir/pages/references/compatibility-and-deprecations.md b/lib/elixir/pages/references/compatibility-and-deprecations.md index 25385c752d..9159ae1620 100644 --- a/lib/elixir/pages/references/compatibility-and-deprecations.md +++ b/lib/elixir/pages/references/compatibility-and-deprecations.md @@ -80,6 +80,7 @@ The first column is the version the feature was hard deprecated. The second colu Version | Deprecated feature | Replaced by (available since) :-------| :-------------------------------------------------- | :--------------------------------------------------------------- +[v1.16] | `~R/.../` | `~r/.../` (v1.0) [v1.16] | Ranges with negative steps in `Enum.slice/2` | Explicit steps in ranges (v1.11) [v1.16] | Ranges with negative steps in `String.slice/2` | Explicit steps in ranges (v1.11) [v1.15] | `Calendar.ISO.day_of_week/3` | `Calendar.ISO.day_of_week/4` (v1.11) diff --git a/lib/elixir/test/elixir/inspect_test.exs b/lib/elixir/test/elixir/inspect_test.exs index 1087483e67..2338c78b40 100644 --- a/lib/elixir/test/elixir/inspect_test.exs +++ b/lib/elixir/test/elixir/inspect_test.exs @@ -865,8 +865,6 @@ defmodule Inspect.OthersTest do test "regex" do assert inspect(~r(foo)m) == "~r/foo/m" - - assert inspect(~R'#{2,}') == ~S"~r/\#{2,}/" assert inspect(~r[\\\#{2,}]iu) == ~S"~r/\\\#{2,}/iu" assert inspect(Regex.compile!("a\\/b")) == "~r/a\\/b/" diff --git a/lib/elixir/test/elixir/kernel_test.exs b/lib/elixir/test/elixir/kernel_test.exs index f7a6527980..c75fa2062b 100644 --- a/lib/elixir/test/elixir/kernel_test.exs +++ b/lib/elixir/test/elixir/kernel_test.exs @@ -86,24 +86,19 @@ defmodule KernelTest do test "=~/2" do assert "abcd" =~ ~r/c(d)/ == true assert "abcd" =~ ~r/e/ == false - assert "abcd" =~ ~R/c(d)/ == true - assert "abcd" =~ ~R/e/ == false string = "^ab+cd*$" assert string =~ "ab+" == true assert string =~ "bb" == false assert "abcd" =~ ~r// == true - assert "abcd" =~ ~R// == true assert "abcd" =~ "" == true assert "" =~ ~r// == true - assert "" =~ ~R// == true assert "" =~ "" == true assert "" =~ "abcd" == false assert "" =~ ~r/abcd/ == false - assert "" =~ ~R/abcd/ == false assert_raise FunctionClauseError, "no function clause matching in Kernel.=~/2", fn -> 1234 =~ "hello" @@ -114,7 +109,7 @@ defmodule KernelTest do end assert_raise FunctionClauseError, "no function clause matching in Kernel.=~/2", fn -> - 1234 =~ ~R"hello" + 1234 =~ ~r"hello" end assert_raise FunctionClauseError, "no function clause matching in Kernel.=~/2", fn -> diff --git a/lib/elixir/test/elixir/regex_test.exs b/lib/elixir/test/elixir/regex_test.exs index 6511bb6fd9..df12dc2dba 100644 --- a/lib/elixir/test/elixir/regex_test.exs +++ b/lib/elixir/test/elixir/regex_test.exs @@ -159,10 +159,6 @@ defmodule RegexTest do assert Regex.named_captures(~r/c(.)/, "cat") == %{} end - test "sigil R" do - assert Regex.match?(~R/f#{1,3}o/, "f#o") - end - test "run/2" do assert Regex.run(~r"c(d)", "abcd") == ["cd", "d"] assert Regex.run(~r"e", "abcd") == nil From c2abb107d664715d8d608da37b9f76c6b5529dce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sat, 30 Sep 2023 20:36:08 +0200 Subject: [PATCH 0013/1886] Order anti-patterns alphabetically --- .../pages/anti-patterns/code-anti-patterns.md | 286 +++++++------ .../anti-patterns/design-anti-patterns.md | 386 +++++++++--------- .../anti-patterns/macro-anti-patterns.md | 96 ++--- 3 files changed, 382 insertions(+), 386 deletions(-) diff --git a/lib/elixir/pages/anti-patterns/code-anti-patterns.md b/lib/elixir/pages/anti-patterns/code-anti-patterns.md index 053bca5d82..c0033841d9 100644 --- a/lib/elixir/pages/anti-patterns/code-anti-patterns.md +++ b/lib/elixir/pages/anti-patterns/code-anti-patterns.md @@ -45,41 +45,6 @@ We removed the unnecessary comments. We also added a `@five_min_in_seconds` modu Elixir makes a clear distinction between **documentation** and code comments. The language has built-in first-class support for documentation through `@doc`, `@moduledoc`, and more. See the ["Writing documentation"](../getting-started/writing-documentation.md) guide for more information. -## Long parameter list - -#### Problem - -In a functional language like Elixir, functions tend to explicitly receive all inputs and return all relevant outputs, instead of relying on mutations or side-effects. As functions grow in complexity, the amount of arguments (parameters) they need to work with may grow, to a point the function's interface becomes confusing and prone to errors during use. - -#### Example - -In the following example, the `loan/6` functions takes too many arguments, causing its interface to be confusing and potentially leading developers to introduce errors during calls to this function. - -```elixir -defmodule Library do - # Too many parameters that can be grouped! - def loan(user_name, email, password, user_alias, book_title, book_ed) do - ... - end -end -``` - -#### Refactoring - -To address this anti-pattern, related arguments can be grouped using maps, structs, or even tuples. This effectively reduces the number of arguments, simplifying the function's interface. In the case of `loan/6`, its arguments were grouped into two different maps, thereby reducing its arity to `loan/2`: - -```elixir -defmodule Library do - def loan(%{name: name, email: email, password: password, alias: alias} = user, %{title: title, ed: ed} = book) do - ... - end -end -``` - -In some cases, the function with too many arguments may be a private function, which gives us more flexibility over how to separate the function arguments. One possible suggestion for such scenarios is to split the arguments in two maps (or tuples): one map keeps the data that may change, and the other keeps the data that won't change (read-only). This gives us a mechanical option to refactor the code. - -Other times, a function may legitimately take half a dozen or more completely unrelated arguments. This may suggest the function is trying to do too much and would be better broken into multiple functions, each responsible for a smaller piece of the overall responsibility. - ## Complex branching #### Problem @@ -238,116 +203,6 @@ def drive(%User{age: age} = user) when age < 18 do end ``` -## Dynamic map fields access - -#### Problem - -In Elixir, it is possible to access values from `Map`s, which are key-value data structures, either statically or dynamically. When trying to dynamically access the value of a key from a map, if the informed key does not exist, `nil` is returned. This return can be confusing and does not allow developers to conclude whether the key is non-existent in the map or just has no bound value. In this way, this anti-pattern may cause bugs in the code. - -#### Example - -The function `plot/1` tries to draw a graphic to represent the position of a point in a cartesian plane. This function receives a parameter of `Map` type with the point attributes, which can be a point of a 2D or 3D cartesian coordinate system. This function uses dynamic access to retrieve values for the map keys: - -```elixir -defmodule Graphics do - def plot(point) do - # Some other code... - - # Dynamic access to use point values - {point[:x], point[:y], point[:z]} - end -end -``` - -```elixir -iex> point_2d = %{x: 2, y: 3} -%{x: 2, y: 3} -iex> point_3d = %{x: 5, y: 6, z: nil} -%{x: 5, y: 6, z: nil} -iex> Graphics.plot(point_2d) -{2, 3, nil} # <= ambiguous return -iex> Graphics.plot(point_3d) -{5, 6, nil} -``` - -As can be seen in the example above, even when the key `:z` does not exist in the map (`point_2d`), dynamic access returns the value `nil`. This return can be dangerous because of its ambiguity. It is not possible to conclude from it whether the map has the key `:z` or not. If the function relies on the return value to make decisions about how to plot a point, this can be problematic and even cause errors when testing the code. - -#### Refactoring - -To remove this anti-pattern, whenever a map has keys of `Atom` type, replace the dynamic access to its values by the `map.field`syntax. When a non-existent key is statically accessed, Elixir raises an error immediately, allowing developers to find bugs faster. The next code illustrates the refactoring of `plot/1`, removing this anti-pattern: - -```elixir -defmodule Graphics do - def plot(point) do - # Some other code... - - # Strict access to use point values - {point.x, point.y, point.z} - end -end -``` - -```elixir -iex> point_2d = %{x: 2, y: 3} -%{x: 2, y: 3} -iex> point_3d = %{x: 5, y: 6, z: nil} -%{x: 5, y: 6, z: nil} -iex> Graphics.plot(point_2d) -** (KeyError) key :z not found in: %{x: 2, y: 3} # <= explicitly warns that - graphic.ex:6: Graphics.plot/1 # <= the :z key does not exist! -iex> Graphics.plot(point_3d) -{5, 6, nil} -``` - -As shown below, another alternative to refactor this anti-pattern is to use pattern matching: - -```elixir -defmodule Graphics do - def plot(%{x: x, y: y, z: z}) do - # Some other code... - - # Strict access to use point values - {x, y, z} - end -end -``` - -```elixir -iex> point_2d = %{x: 2, y: 3} -%{x: 2, y: 3} -iex> point_3d = %{x: 5, y: 6, z: nil} -%{x: 5, y: 6, z: nil} -iex> Graphics.plot(point_2d) -** (FunctionClauseError) no function clause matching in Graphics.plot/1 - graphic.ex:2: Graphics.plot/1 # <= the :z key does not exist! -iex> Graphics.plot(point_3d) -{5, 6, nil} -``` - -Another alternative is to use structs. By default, structs only support static access to its fields, promoting cleaner patterns: - -```elixir -defmodule Point.2D do - @enforce_keys [:x, :y] - defstruct [x: nil, y: nil] -end -``` - -```elixir -iex> point = %Point.2D{x: 2, y: 3} -%Point.2D{x: 2, y: 3} -iex> point.x # <= strict access to use point values -2 -iex> point.z # <= trying to access a non-existent key -** (KeyError) key :z not found in: %Point{x: 2, y: 3} -iex> point[:x] # <= by default, struct does not support dynamic access -** (UndefinedFunctionError) ... (Point does not implement the Access behaviour) -``` - -#### Additional remarks - -This anti-pattern was formerly known as [Accessing non-existent Map/Struct fields](https://github.com/lucasvegi/Elixir-Code-Smells#accessing-non-existent-mapstruct-fields). - ## Dynamic atom creation #### Problem @@ -443,6 +298,41 @@ end However, keep in mind using a module attribute or defining the atoms in the module body, outside of a function, are not sufficient, as the module body is only executed during compilation and it is not necessarily part of the compiled module loaded at runtime. +## Long parameter list + +#### Problem + +In a functional language like Elixir, functions tend to explicitly receive all inputs and return all relevant outputs, instead of relying on mutations or side-effects. As functions grow in complexity, the amount of arguments (parameters) they need to work with may grow, to a point the function's interface becomes confusing and prone to errors during use. + +#### Example + +In the following example, the `loan/6` functions takes too many arguments, causing its interface to be confusing and potentially leading developers to introduce errors during calls to this function. + +```elixir +defmodule Library do + # Too many parameters that can be grouped! + def loan(user_name, email, password, user_alias, book_title, book_ed) do + ... + end +end +``` + +#### Refactoring + +To address this anti-pattern, related arguments can be grouped using maps, structs, or even tuples. This effectively reduces the number of arguments, simplifying the function's interface. In the case of `loan/6`, its arguments were grouped into two different maps, thereby reducing its arity to `loan/2`: + +```elixir +defmodule Library do + def loan(%{name: name, email: email, password: password, alias: alias} = user, %{title: title, ed: ed} = book) do + ... + end +end +``` + +In some cases, the function with too many arguments may be a private function, which gives us more flexibility over how to separate the function arguments. One possible suggestion for such scenarios is to split the arguments in two maps (or tuples): one map keeps the data that may change, and the other keeps the data that won't change (read-only). This gives us a mechanical option to refactor the code. + +Other times, a function may legitimately take half a dozen or more completely unrelated arguments. This may suggest the function is trying to do too much and would be better broken into multiple functions, each responsible for a smaller piece of the overall responsibility. + ## Namespace trespassing #### Problem @@ -481,6 +371,112 @@ There are few known exceptions to this anti-pattern: * If you are the maintainer for both `plug` and `plug_auth`, then you may allow `plug_auth` to define modules with the `Plug` namespace, such as `Plug.Auth`. However, you are responsible for avoiding or managing any conflicts that may arise in the future +## Non-existent map keys + +#### Problem + +In Elixir, it is possible to access values from `Map`s, which are key-value data structures, either statically or dynamically. When the keys are known upfront, they must be accessed using the `map.key` notation, instead of `map[:key]`. When the latter is used, if the informed key does not exist, `nil` is returned. This return can be confusing and does not allow developers to conclude whether the key is non-existent in the map or just has no bound value. In this way, this anti-pattern may cause bugs in the code. + +#### Example + +The function `plot/1` tries to draw a graphic to represent the position of a point in a cartesian plane. This function receives a parameter of `Map` type with the point attributes, which can be a point of a 2D or 3D cartesian coordinate system. This function uses dynamic access to retrieve values for the map keys: + +```elixir +defmodule Graphics do + def plot(point) do + # Some other code... + + # Dynamic access to use point values + {point[:x], point[:y], point[:z]} + end +end +``` + +```elixir +iex> point_2d = %{x: 2, y: 3} +%{x: 2, y: 3} +iex> point_3d = %{x: 5, y: 6, z: nil} +%{x: 5, y: 6, z: nil} +iex> Graphics.plot(point_2d) +{2, 3, nil} # <= ambiguous return +iex> Graphics.plot(point_3d) +{5, 6, nil} +``` + +As can be seen in the example above, even when the key `:z` does not exist in the map (`point_2d`), dynamic access returns the value `nil`. This return can be dangerous because of its ambiguity. It is not possible to conclude from it whether the map has the key `:z` or not. If the function relies on the return value to make decisions about how to plot a point, this can be problematic and even cause errors when testing the code. + +#### Refactoring + +To remove this anti-pattern, whenever accessing a known key of `Atom` type, replace the dynamic `map[:key]` syntax by the static `map.key` notation. This way, when a non-existent key is accessed, Elixir raises an error immediately, allowing developers to find bugs faster. The next code illustrates the refactoring of `plot/1`, removing this anti-pattern: + +```elixir +defmodule Graphics do + def plot(point) do + # Some other code... + + # Strict access to use point values + {point.x, point.y, point.z} + end +end +``` + +```elixir +iex> point_2d = %{x: 2, y: 3} +%{x: 2, y: 3} +iex> point_3d = %{x: 5, y: 6, z: nil} +%{x: 5, y: 6, z: nil} +iex> Graphics.plot(point_2d) +** (KeyError) key :z not found in: %{x: 2, y: 3} # <= explicitly warns that + graphic.ex:6: Graphics.plot/1 # <= the :z key does not exist! +iex> Graphics.plot(point_3d) +{5, 6, nil} +``` + +As shown below, another alternative to refactor this anti-pattern is to use pattern matching: + +```elixir +defmodule Graphics do + def plot(%{x: x, y: y, z: z}) do + # Some other code... + + # Strict access to use point values + {x, y, z} + end +end +``` + +```elixir +iex> point_2d = %{x: 2, y: 3} +%{x: 2, y: 3} +iex> point_3d = %{x: 5, y: 6, z: nil} +%{x: 5, y: 6, z: nil} +iex> Graphics.plot(point_2d) +** (FunctionClauseError) no function clause matching in Graphics.plot/1 + graphic.ex:2: Graphics.plot/1 # <= the :z key does not exist! +iex> Graphics.plot(point_3d) +{5, 6, nil} +``` + +Another alternative is to use structs. By default, structs only support static access to its fields, promoting cleaner patterns: + +```elixir +defmodule Point.2D do + @enforce_keys [:x, :y] + defstruct [x: nil, y: nil] +end +``` + +```elixir +iex> point = %Point.2D{x: 2, y: 3} +%Point.2D{x: 2, y: 3} +iex> point.x # <= strict access to use point values +2 +iex> point.z # <= trying to access a non-existent key +** (KeyError) key :z not found in: %Point{x: 2, y: 3} +iex> point[:x] # <= by default, struct does not support dynamic access +** (UndefinedFunctionError) ... (Point does not implement the Access behaviour) +``` + ## Speculative assumptions #### Problem diff --git a/lib/elixir/pages/anti-patterns/design-anti-patterns.md b/lib/elixir/pages/anti-patterns/design-anti-patterns.md index e8cab4e1b5..c60d4df712 100644 --- a/lib/elixir/pages/anti-patterns/design-anti-patterns.md +++ b/lib/elixir/pages/anti-patterns/design-anti-patterns.md @@ -3,42 +3,67 @@ This document outlines anti-patterns related to your modules, functions, and the role they play within a codebase. -## Primitive obsession +## Alternative return types #### Problem -This anti-pattern happens when Elixir basic types (for example, *integer*, *float*, and *string*) are abusively used in function parameters and code variables, rather than creating specific composite data types (for example, *tuples* and *structs*) that can better represent a domain. +This anti-pattern refers to functions that receive options (typically as a *keyword list*) parameters that drastically change their return type. Because options are optional and sometimes set dynamically, if they also change the return type, it may be hard to understand what the function actually returns. #### Example -An example of this anti-pattern is the use of a single *string* to represent an `Address`. An `Address` is a more complex structure than a simple basic (aka, primitive) value. +An example of this anti-pattern, as shown below, is when a function has many alternative return types, depending on the options received as a parameter. ```elixir -defmodule MyApp do - def process_address(address) when is_binary(address) do - # Do something with address... +defmodule AlternativeInteger do + @spec parse(String.t(), keyword()) :: integer() | {integer(), String.t()} | :error + def parse(string, options \\ []) when is_list(options) do + if Keyword.get(options, :discard_rest, false) do + Integer.parse(string) + else + case Integer.parse(string) do + {int, _rest} -> int + :error -> :error + end + end end end ``` -Another example of this anti-pattern is using floating numbers to model money and currency, when [richer data structures should be preferred](https://hexdocs.pm/ex_money/). +```elixir +iex> AlternativeInteger.parse("13") +{13, ""} +iex> AlternativeInteger.parse("13", discard_rest: true) +13 +iex> AlternativeInteger.parse("13", discard_rest: false) +{13, ""} +``` #### Refactoring -We can create an `Address` struct to remove this anti-pattern, better representing this domain through a composite type. Additionally, we can modify the `process_address/1` function to accept a parameter of type `Address` instead of a *string*. With this modification, we can extract each field of this composite type individually when needed. +To refactor this anti-pattern, as shown next, add a specific function for each return type (for example, `parse_discard_rest/1`), no longer delegating this to options passed as arguments. ```elixir -defmodule Address do - defstruct [:street, :city, :state, :postal_code, :country] +defmodule AlternativeInteger do + @spec parse(String.t()) :: {integer(), String.t()} | :error + def parse(string) do + Integer.parse(string) + end + + @spec parse_discard_rest(String.t()) :: integer() | :error + def parse_discard_rest(string) do + case Integer.parse(string) do + {int, _rest} -> int + :error -> :error + end + end end ``` ```elixir -defmodule MyApp do - def process_address(%Address{} = address) do - # Do something with address... - end -end +iex> AlternativeInteger.parse("13") +{13, ""} +iex> AlternativeInteger.parse_discard_rest("13") +13 ``` ## Boolean obsession @@ -97,123 +122,214 @@ MyApp.update(invoice, status: :approved) Remember booleans are internally represented as atoms. Therefore there is no performance penalty in one approach over the other. -## Working with invalid data +## Exceptions for control-flow #### Problem -This anti-pattern refers to a function that does not validate its parameters' types and therefore can produce internal unexpected behavior. When an error is raised inside a function due to an invalid parameter value, it can be confusing for developers and make it harder to locate and fix the error. +This anti-pattern refers to code that uses exceptions for control flow. Exception handling itself does not represent an anti-pattern, but developers must prefer to use `case` and pattern matching to change the flow of their code, instead of `try/rescue`. In turn, library authors should provide developers with APIs to handle errors without relying on exception handling. When developers have no freedom to decide if an error is exceptional or not, this is considered an anti-pattern. #### Example -An example of this anti-pattern is when a function receives an invalid parameter and then passes it to other functions, either in the same library or in a third-party library. This can cause an error to be raised deep inside the call stack, which may be confusing for the developer who is working with invalid data. As shown next, the function `foo/1` is a user-facing API which doesn't validate its parameters at the boundary. In this way, it is possible that invalid data will be passed through, causing an error that is obscure and hard to debug. +An example of this anti-pattern, as shown below, is using `try/rescue` to deal with file operations: ```elixir -defmodule MyLibrary do - def foo(invalid_data) do - # Some other code... - - MyLibrary.Internal.sum(1, invalid_data) +defmodule MyModule do + def print_file(file) do + try do + IO.puts(File.read!(file)) + rescue + e -> IO.puts(:stderr, Exception.message(e)) + end end end ``` ```elixir -iex> MyLibrary.foo(2) -3 -iex> MyLibrary.foo("José") # With invalid data -** (ArithmeticError) bad argument in arithmetic expression: 1 + "José" - :erlang.+(1, "José") - my_library.ex:4: MyLibrary.Internal.sum/2 +iex> MyModule.print_file("valid_file") +This is a valid file! +:ok +iex> MyModule.print_file("invalid_file") +could not read file "invalid_file": no such file or directory +:ok ``` #### Refactoring -To remove this anti-pattern, the client code must validate input parameters at the boundary with the user, via guard clauses, pattern matching, or conditionals. This prevents errors from occurring elsewhere in the call stack, making them easier to understand and debug. This refactoring also allows libraries to be implemented without worrying about creating internal protection mechanisms. The next code snippet illustrates the refactoring of `foo/1`, removing this anti-pattern: +To refactor this anti-pattern, as shown next, use `File.read/1`, which returns tuples instead of raising when a file cannot be read: ```elixir -defmodule MyLibrary do - def foo(data) when is_integer(data) do - # Some other code - - MyLibrary.Internal.sum(1, data) +defmodule MyModule do + def print_file(file) do + case File.read(file) do + {:ok, binary} -> IO.puts(binary) + {:error, reason} -> IO.puts(:stderr, "could not read file #{file}: #{reason}") + end end end ``` -```elixir -iex> MyLibrary.foo(2) # With valid data -3 -iex> MyLibrary.foo("José") # With invalid data -** (FunctionClauseError) no function clause matching in MyLibrary.foo/1. -The following arguments were given to MyLibrary.foo/1: +This is only possible because the `File` module provides APIs for reading files with tuples as results (`File.read/1`), as well as a version that raises an exception (`File.read!/1`). The bang (exclamation point) is effectively part of [Elixir's naming conventions](naming-conventions.html#trailing-bang-foo). - # 1 - "José" +Library authors are encouraged to follow the same practices. In practice, the bang variant is implemented on top of the non-raising version of the code. For example, `File.read/1` is implemented as: - my_library.ex:2: MyLibrary.foo/1 +```elixir +def read!(path) do + case read(path) do + {:ok, binary} -> + binary + + {:error, reason} -> + raise File.Error, reason: reason, action: "read file", path: IO.chardata_to_string(path) + end +end ``` -## Alternative return types +A common practice followed by the community is to make the non-raising version to return `{:ok, result}` or `{:error, Exception.t}`. For example, an HTTP client may return `{:ok, %HTTP.Response{}}` on success cases and a `{:error, %HTTP.Error{}}` for failures, where `HTTP.Error` is [implemented as an exception](`Kernel.defexception/1`). This makes it convenient for anyone to raise an exception by simply calling `Kernel.raise/1`. + +## Feature envy #### Problem -This anti-pattern refers to functions that receive options (for example, *keyword list*) parameters that drastically change their return type. Because options are optional and sometimes set dynamically, if they change the return type it may be hard to understand what the function actually returns. +This anti-pattern occurs when a function accesses more data or calls more functions from another module than from its own. The presence of this anti-pattern can make a module less cohesive and increase code coupling. #### Example -An example of this anti-pattern, as shown below, is when a function has many alternative return types, depending on the options received as a parameter. +In the following code, all the data used in the `calculate_total_item/1` function of the module `Order` comes from the `OrderItem` module. This increases coupling and decreases code cohesion unnecessarily. ```elixir -defmodule AlternativeInteger do - @spec parse(String.t(), keyword()) :: integer() | {integer(), String.t()} | :error - def parse(string, options \\ []) when is_list(options) do - if Keyword.get(options, :discard_rest, false) do - Integer.parse(string) +defmodule Order do + # Some functions... + + def calculate_total_item(id) do + item = OrderItem.find_item(id) + total = (item.price + item.taxes) * item.amount + + if discount = OrderItem.find_discount(item) do + total - total * discount else - case Integer.parse(string) do - {int, _rest} -> int - :error -> :error - end + total + end + end +end +``` + +#### Refactoring + +To remove this anti-pattern we can move `calculate_total_item/1` to `OrderItem`, decreasing coupling: + +```elixir +defmodule OrderItem do + def find_item(id) + def find_discount(item) + + def calculate_total_item(id) do # <= function moved from Order! + item = find_item(id) + total = (item.price + item.taxes) * item.amount + discount = find_discount(item) + + unless is_nil(discount) do + total - total * discount + else + total end end end ``` +This refactoring is only possible when you own both modules. If the module you are invoking belongs to another application, then it is not possible to add new functions to it, and your only option is to define an additional module that augments the third-party module. + +## Primitive obsession + +#### Problem + +This anti-pattern happens when Elixir basic types (for example, *integer*, *float*, and *string*) are abusively used in function parameters and code variables, rather than creating specific composite data types (for example, *tuples* and *structs*) that can better represent a domain. + +#### Example + +An example of this anti-pattern is the use of a single *string* to represent an `Address`. An `Address` is a more complex structure than a simple basic (aka, primitive) value. + ```elixir -iex> AlternativeInteger.parse("13") -{13, ""} -iex> AlternativeInteger.parse("13", discard_rest: true) -13 -iex> AlternativeInteger.parse("13", discard_rest: false) -{13, ""} +defmodule MyApp do + def process_address(address) when is_binary(address) do + # Do something with address... + end +end ``` +Another example of this anti-pattern is using floating numbers to model money and currency, when [richer data structures should be preferred](https://hexdocs.pm/ex_money/). + #### Refactoring -To refactor this anti-pattern, as shown next, add a specific function for each return type (for example, `parse_discard_rest/1`), no longer delegating this to options passed as arguments. +We can create an `Address` struct to remove this anti-pattern, better representing this domain through a composite type. Additionally, we can modify the `process_address/1` function to accept a parameter of type `Address` instead of a *string*. With this modification, we can extract each field of this composite type individually when needed. ```elixir -defmodule AlternativeInteger do - @spec parse(String.t()) :: {integer(), String.t()} | :error - def parse(string) do - Integer.parse(string) +defmodule Address do + defstruct [:street, :city, :state, :postal_code, :country] +end +``` + +```elixir +defmodule MyApp do + def process_address(%Address{} = address) do + # Do something with address... end +end +``` - @spec parse_discard_rest(String.t()) :: integer() | :error - def parse_discard_rest(string) do - case Integer.parse(string) do - {int, _rest} -> int - :error -> :error - end +## Propagating invalid data + +#### Problem + +This anti-pattern refers to a function that does not validate its parameters and propagates it to other functions, which can produce internal unexpected behavior. When an error is raised inside a function due to an invalid parameter value, it can be confusing for developers and make it harder to locate and fix the error. + +#### Example + +An example of this anti-pattern is when a function receives an invalid parameter and then passes it to other functions, either in the same library or in a third-party library. This can cause an error to be raised deep inside the call stack, which may be confusing for the developer who is working with invalid data. As shown next, the function `foo/1` is a user-facing API which doesn't validate its parameters at the boundary. In this way, it is possible that invalid data will be passed through, causing an error that is obscure and hard to debug. + +```elixir +defmodule MyLibrary do + def foo(invalid_data) do + # Some other code... + + MyLibrary.Internal.sum(1, invalid_data) end end ``` ```elixir -iex> AlternativeInteger.parse("13") -{13, ""} -iex> AlternativeInteger.parse_discard_rest("13") -13 +iex> MyLibrary.foo(2) +3 +iex> MyLibrary.foo("José") # With invalid data +** (ArithmeticError) bad argument in arithmetic expression: 1 + "José" + :erlang.+(1, "José") + my_library.ex:4: MyLibrary.Internal.sum/2 +``` + +#### Refactoring + +To remove this anti-pattern, the client code must validate input parameters at the boundary with the user, via guard clauses, pattern matching, or conditionals. This prevents errors from occurring elsewhere in the call stack, making them easier to understand and debug. This refactoring also allows libraries to be implemented without worrying about creating internal protection mechanisms. The next code snippet illustrates the refactoring of `foo/1`, removing this anti-pattern: + +```elixir +defmodule MyLibrary do + def foo(data) when is_integer(data) do + # Some other code + + MyLibrary.Internal.sum(1, data) + end +end +``` + +```elixir +iex> MyLibrary.foo(2) # With valid data +3 +iex> MyLibrary.foo("José") # With invalid data +** (FunctionClauseError) no function clause matching in MyLibrary.foo/1. +The following arguments were given to MyLibrary.foo/1: + + # 1 + "José" + + my_library.ex:2: MyLibrary.foo/1 ``` ## Unrelated multi-clause function @@ -291,122 +407,6 @@ def update_animal(%Animal{count: 1, skin: skin}) end ``` -## Feature envy - -#### Problem - -This anti-pattern occurs when a function accesses more data or calls more functions from another module than from its own. The presence of this anti-pattern can make a module less cohesive and increase code coupling. - -#### Example - -In the following code, all the data used in the `calculate_total_item/1` function of the module `Order` comes from the `OrderItem` module. This increases coupling and decreases code cohesion unnecessarily. - -```elixir -defmodule Order do - # Some functions... - - def calculate_total_item(id) do - item = OrderItem.find_item(id) - total = (item.price + item.taxes) * item.amount - - if discount = OrderItem.find_discount(item) do - total - total * discount - else - total - end - end -end -``` - -#### Refactoring - -To remove this anti-pattern we can move `calculate_total_item/1` to `OrderItem`, decreasing coupling: - -```elixir -defmodule OrderItem do - def find_item(id) - def find_discount(item) - - def calculate_total_item(id) do # <= function moved from Order! - item = find_item(id) - total = (item.price + item.taxes) * item.amount - discount = find_discount(item) - - unless is_nil(discount) do - total - total * discount - else - total - end - end -end -``` - -This refactoring is only possible when you own both modules. If the module you are invoking belongs to another application, then it is not possible to add new functions to it, and your only option is to define an additional module that augments the third-party module. - -## Using exceptions for control-flow - -#### Problem - -This anti-pattern refers to code that uses exceptions for control flow. Exception handling itself does not represent an anti-pattern, but developers must prefer to use `case` and pattern matching to change the flow of their code, instead of `try/rescue`. In turn, library authors should provide developers with APIs to handle errors without relying on exception handling. When developers have no freedom to decide if an error is exceptional or not, this is considered an anti-pattern. - -#### Example - -An example of this anti-pattern, as shown below, is using `try/rescue` to deal with file operations: - -```elixir -defmodule MyModule do - def print_file(file) do - try do - IO.puts(File.read!(file)) - rescue - e -> IO.puts(:stderr, Exception.message(e)) - end - end -end -``` - -```elixir -iex> MyModule.print_file("valid_file") -This is a valid file! -:ok -iex> MyModule.print_file("invalid_file") -could not read file "invalid_file": no such file or directory -:ok -``` - -#### Refactoring - -To refactor this anti-pattern, as shown next, use `File.read/1`, which returns tuples instead of raising when a file cannot be read: - -```elixir -defmodule MyModule do - def print_file(file) do - case File.read(file) do - {:ok, binary} -> IO.puts(binary) - {:error, reason} -> IO.puts(:stderr, "could not read file #{file}: #{reason}") - end - end -end -``` - -This is only possible because the `File` module provides APIs for reading files with tuples as results (`File.read/1`), as well as a version that raises an exception (`File.read!/1`). The bang (exclamation point) is effectively part of [Elixir's naming conventions](naming-conventions.html#trailing-bang-foo). - -Library authors are encouraged to follow the same practices. In practice, the bang variant is implemented on top of the non-raising version of the code. For example, `File.read/1` is implemented as: - -```elixir -def read!(path) do - case read(path) do - {:ok, binary} -> - binary - - {:error, reason} -> - raise File.Error, reason: reason, action: "read file", path: IO.chardata_to_string(path) - end -end -``` - -A common practice followed by the community is to make the non-raising version to return `{:ok, result}` or `{:error, Exception.t}`. For example, an HTTP client may return `{:ok, %HTTP.Response{}}` on success cases and a `{:error, %HTTP.Error{}}` for failures, where `HTTP.Error` is [implemented as an exception](`Kernel.defexception/1`). This makes it convenient for anyone to raise an exception by simply calling `Kernel.raise/1`. - ## Using application configuration for libraries #### Problem diff --git a/lib/elixir/pages/anti-patterns/macro-anti-patterns.md b/lib/elixir/pages/anti-patterns/macro-anti-patterns.md index a07ad0ebbc..1784c7318e 100644 --- a/lib/elixir/pages/anti-patterns/macro-anti-patterns.md +++ b/lib/elixir/pages/anti-patterns/macro-anti-patterns.md @@ -2,54 +2,6 @@ This document outlines anti-patterns related to meta-programming. -## Unnecessary macros - -#### Problem - -**Macros** are powerful meta-programming mechanisms that can be used in Elixir to extend the language. While using macros is not an anti-pattern in itself, this meta-programming mechanism should only be used when absolutely necessary. Whenever a macro is used, but it would have been possible to solve the same problem using functions or other existing Elixir structures, the code becomes unnecessarily more complex and less readable. Because macros are more difficult to implement and reason about, their indiscriminate use can compromise the evolution of a system, reducing its maintainability. - -#### Example - -The `MyMath` module implements the `sum/2` macro to perform the sum of two numbers received as parameters. While this code has no syntax errors and can be executed correctly to get the desired result, it is unnecessarily more complex. By implementing this functionality as a macro rather than a conventional function, the code became less clear: - -```elixir -defmodule MyMath do - defmacro sum(v1, v2) do - quote do - unquote(v1) + unquote(v2) - end - end -end -``` - -```elixir -iex> require MyMath -MyMath -iex> MyMath.sum(3, 5) -8 -iex> MyMath.sum(3 + 1, 5 + 6) -15 -``` - -#### Refactoring - -To remove this anti-pattern, the developer must replace the unnecessary macro with structures that are simpler to write and understand, such as named functions. The code shown below is the result of the refactoring of the previous example. Basically, the `sum/2` macro has been transformed into a conventional named function. Note that the `require/2` call is no longer needed: - -```elixir -defmodule MyMath do - def sum(v1, v2) do # <= The macro became a named function - v1 + v2 - end -end -``` - -```elixir -iex> MyMath.sum(3, 5) -8 -iex> MyMath.sum(3+1, 5+6) -15 -``` - ## Large code generation by macros #### Problem @@ -107,6 +59,54 @@ defmodule Routes do end ``` +## Unnecessary macros + +#### Problem + +**Macros** are powerful meta-programming mechanisms that can be used in Elixir to extend the language. While using macros is not an anti-pattern in itself, this meta-programming mechanism should only be used when absolutely necessary. Whenever a macro is used, but it would have been possible to solve the same problem using functions or other existing Elixir structures, the code becomes unnecessarily more complex and less readable. Because macros are more difficult to implement and reason about, their indiscriminate use can compromise the evolution of a system, reducing its maintainability. + +#### Example + +The `MyMath` module implements the `sum/2` macro to perform the sum of two numbers received as parameters. While this code has no syntax errors and can be executed correctly to get the desired result, it is unnecessarily more complex. By implementing this functionality as a macro rather than a conventional function, the code became less clear: + +```elixir +defmodule MyMath do + defmacro sum(v1, v2) do + quote do + unquote(v1) + unquote(v2) + end + end +end +``` + +```elixir +iex> require MyMath +MyMath +iex> MyMath.sum(3, 5) +8 +iex> MyMath.sum(3 + 1, 5 + 6) +15 +``` + +#### Refactoring + +To remove this anti-pattern, the developer must replace the unnecessary macro with structures that are simpler to write and understand, such as named functions. The code shown below is the result of the refactoring of the previous example. Basically, the `sum/2` macro has been transformed into a conventional named function. Note that the `require/2` call is no longer needed: + +```elixir +defmodule MyMath do + def sum(v1, v2) do # <= The macro became a named function + v1 + v2 + end +end +``` + +```elixir +iex> MyMath.sum(3, 5) +8 +iex> MyMath.sum(3+1, 5+6) +15 +``` + ## `use` instead of `import` #### Problem From 8033c90778fae0ab70189a0a64082ae73ffe96de Mon Sep 17 00:00:00 2001 From: Artem Solomatin Date: Sun, 1 Oct 2023 12:36:42 +0300 Subject: [PATCH 0014/1886] Add missing spec and test for Macro.path (#12972) --- lib/elixir/lib/macro.ex | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/elixir/lib/macro.ex b/lib/elixir/lib/macro.ex index 1c091ec477..ef9fa5c20b 100644 --- a/lib/elixir/lib/macro.ex +++ b/lib/elixir/lib/macro.ex @@ -464,6 +464,9 @@ defmodule Macro do iex> Macro.path(quote(do: [1, 2, 3]), & &1 == 3) [3, [1, 2, 3]] + iex> Macro.path(quote(do: [1, 2]), & &1 == 5) + nil + iex> Macro.path(quote(do: Foo.bar(3)), & &1 == 3) [3, quote(do: Foo.bar(3))] @@ -478,6 +481,7 @@ defmodule Macro do """ @doc since: "1.14.0" + @spec path(t, (t -> as_boolean(term))) :: [t] | nil def path(ast, fun) when is_function(fun, 1) do path(ast, [], fun) end From 186d1ab201519b35bdaeb69ca720833c983c539b Mon Sep 17 00:00:00 2001 From: Jean Klingler Date: Sun, 1 Oct 2023 19:29:56 +0900 Subject: [PATCH 0015/1886] Improve Macro.path/2 docs (#12973) --- lib/elixir/lib/macro.ex | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/elixir/lib/macro.ex b/lib/elixir/lib/macro.ex index ef9fa5c20b..a65d816750 100644 --- a/lib/elixir/lib/macro.ex +++ b/lib/elixir/lib/macro.ex @@ -450,10 +450,12 @@ defmodule Macro do def generate_arguments(amount, context), do: generate_arguments(amount, context, &var/2) @doc """ - Returns the path to the node in `ast` which `fun` returns `true`. + Returns the path to the node in `ast` for which `fun` returns a truthy value. The path is a list, starting with the node in which `fun` returns - true, followed by all of its parents. + a truthy value, followed by all of its parents. + + Returns `nil` if `fun` returns only falsy values. Computing the path can be an efficient operation when you want to find a particular node in the AST within its context and then From 3afe4d6dfcc2e2600dfd8be0969da85baf03214a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sun, 1 Oct 2023 13:57:48 +0200 Subject: [PATCH 0016/1886] Provide tips for custom inspect, closes #12974 --- lib/elixir/lib/inspect.ex | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/lib/elixir/lib/inspect.ex b/lib/elixir/lib/inspect.ex index d09322511f..d5753b8d84 100644 --- a/lib/elixir/lib/inspect.ex +++ b/lib/elixir/lib/inspect.ex @@ -117,13 +117,22 @@ defprotocol Inspect do In case there is an error while your structure is being inspected, Elixir will raise an `ArgumentError` error and will automatically fall back - to a raw representation for printing the structure. + to a raw representation for printing the structure. Furthermore, you + must be careful when debugging your own Inspect implementation, as calls + to `IO.inspect/2` or `dbg/1` may trigger an infinite loop (as in order to + inspect/debug the data structure, you must call `inspect` itself). - You can, however, access the underlying error by invoking the `Inspect` - implementation directly. For example, to test `Inspect.MapSet` above, - you can invoke it as: + Here are some tips: - Inspect.MapSet.inspect(MapSet.new(), %Inspect.Opts{}) + * For debugging, use `IO.inspect/2` with the `structs: false` option, + which disables custom printing and avoids calling the Inspect + implementation recursively + + * To access the underlying error on your custom `Inspect` implementation, + you may invoke the protocol directly. For example, we could invoke the + `Inspect.MapSet` implementation above as: + + Inspect.MapSet.inspect(MapSet.new(), %Inspect.Opts{}) """ From 60efbf751d3bedbe1851ac8c701a807d66fe340d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sun, 1 Oct 2023 15:46:17 +0200 Subject: [PATCH 0017/1886] Address tests on Erlang/OTP 26.1, closes #12975 --- lib/ex_unit/lib/ex_unit/callbacks.ex | 9 +----- lib/ex_unit/test/ex_unit/formatter_test.exs | 31 +++++++++----------- lib/ex_unit/test/ex_unit/supervised_test.exs | 11 ++----- 3 files changed, 18 insertions(+), 33 deletions(-) diff --git a/lib/ex_unit/lib/ex_unit/callbacks.ex b/lib/ex_unit/lib/ex_unit/callbacks.ex index b7b08c5203..cbebdd31af 100644 --- a/lib/ex_unit/lib/ex_unit/callbacks.ex +++ b/lib/ex_unit/lib/ex_unit/callbacks.ex @@ -549,14 +549,7 @@ defmodule ExUnit.Callbacks do end child_spec = Supervisor.child_spec(child_spec_or_module, opts) - - case Supervisor.start_child(sup, child_spec) do - {:error, {:already_started, _pid}} -> - {:error, {:duplicate_child_name, child_spec.id}} - - other -> - other - end + Supervisor.start_child(sup, child_spec) end @doc """ diff --git a/lib/ex_unit/test/ex_unit/formatter_test.exs b/lib/ex_unit/test/ex_unit/formatter_test.exs index afb66a5243..bb9642fbcc 100644 --- a/lib/ex_unit/test/ex_unit/formatter_test.exs +++ b/lib/ex_unit/test/ex_unit/formatter_test.exs @@ -491,28 +491,25 @@ defmodule ExUnit.FormatterTest do test "inspect failure" do failure = [{:error, catch_assertion(assert :will_fail == %BadInspect{}), []}] - message = ~S''' - got FunctionClauseError with message: - - """ - no function clause matching in Inspect.ExUnit.FormatterTest.BadInspect.inspect/2 - """ - - while inspecting: - - %{__struct__: ExUnit.FormatterTest.BadInspect, key: 0} - - Stacktrace: - ''' - - assert format_test_failure(test(), failure, 1, 80, &formatter/2) =~ """ + assert format_test_failure(test(), failure, 1, 80, &formatter/2) =~ ~s''' 1) world (Hello) test/ex_unit/formatter_test.exs:1 Assertion with == failed code: assert :will_fail == %BadInspect{} left: :will_fail - right: #Inspect.Error<\n#{message}\ - """ + right: #Inspect.Error< + got FunctionClauseError with message: + + """ + no function clause matching in Inspect.ExUnit.FormatterTest.BadInspect.inspect/2 + """ + + while inspecting: + + #{inspect(%BadInspect{}, structs: false)} + + Stacktrace: + ''' end defmodule BadMessage do diff --git a/lib/ex_unit/test/ex_unit/supervised_test.exs b/lib/ex_unit/test/ex_unit/supervised_test.exs index 577ae6b967..a02dfe3c0c 100644 --- a/lib/ex_unit/test/ex_unit/supervised_test.exs +++ b/lib/ex_unit/test/ex_unit/supervised_test.exs @@ -73,19 +73,14 @@ defmodule ExUnit.SupervisedTest do test "starts a supervised process with ID checks" do {:ok, pid} = start_supervised({MyAgent, 0}) + assert is_pid(pid) - assert {:error, {:duplicate_child_name, ExUnit.SupervisedTest.MyAgent}} = - start_supervised({MyAgent, 0}) - - assert {:error, {{:already_started, ^pid}, _}} = start_supervised({MyAgent, 0}, id: :another) + assert {:error, _} = start_supervised({MyAgent, 0}) + assert {:error, _} = start_supervised({MyAgent, 0}, id: :another) assert_raise RuntimeError, ~r"Reason: bad child specification", fn -> start_supervised!(%{id: 1, start: :oops}) end - - assert_raise RuntimeError, ~r"Reason: already started", fn -> - start_supervised!({MyAgent, 0}, id: :another) - end end test "stops a supervised process" do From 78c9666ea17718febba6b25aea56e15e78350ba8 Mon Sep 17 00:00:00 2001 From: Art Kay Date: Sun, 1 Oct 2023 17:49:11 -0400 Subject: [PATCH 0018/1886] Fix typo in the Enum cheatsheet (#12976) --- lib/elixir/pages/cheatsheets/enum-cheat.cheatmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/pages/cheatsheets/enum-cheat.cheatmd b/lib/elixir/pages/cheatsheets/enum-cheat.cheatmd index 7a9dd04794..dc5cd72a35 100644 --- a/lib/elixir/pages/cheatsheets/enum-cheat.cheatmd +++ b/lib/elixir/pages/cheatsheets/enum-cheat.cheatmd @@ -636,7 +636,7 @@ iex> Enum.group_by(cart, &String.last(&1.fruit), & &1.fruit) } ``` -## Joining & interpersing +## Joining & interspersing {: .col-2} ### [join(enum, joiner \\\\ "")](`Enum.join/2`) From e4431b8589989cdc8083418a64225d6f86ef8b82 Mon Sep 17 00:00:00 2001 From: Stevo-S <4288648+Stevo-S@users.noreply.github.com> Date: Mon, 2 Oct 2023 15:44:23 +0300 Subject: [PATCH 0019/1886] Clarify Any Protocol implementation derivation (#12978) A small edit on the "Deriving" section of the "Protocols" chapter in "Getting Started". Changing "we should be fine with the implementation of Any" to "should we be fine with the implementation of any" hopefully better communicates that we can use the Any implementation of the protocol if we wish to but not that we have to. --- lib/elixir/pages/getting-started/protocols.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/pages/getting-started/protocols.md b/lib/elixir/pages/getting-started/protocols.md index 2fba35d7f0..ba06b0eb0e 100644 --- a/lib/elixir/pages/getting-started/protocols.md +++ b/lib/elixir/pages/getting-started/protocols.md @@ -162,7 +162,7 @@ end The implementation above is arguably not a reasonable one. For example, it makes no sense to say that the size of a `PID` or an `Integer` is `0`. -However, we should be fine with the implementation for `Any`, in order to use such implementation we would need to tell our struct to explicitly derive the `Size` protocol: +However, should we be fine with the implementation for `Any`, in order to use such implementation we would need to tell our struct to explicitly derive the `Size` protocol: ```elixir defmodule OtherUser do From 7410cd370aad40216e8531d3547a20eecd123e38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonatan=20K=C5=82osko?= Date: Mon, 2 Oct 2023 22:01:42 +0200 Subject: [PATCH 0020/1886] Update docs on binary syntax modifiers (#12980) --- lib/elixir/lib/kernel/special_forms.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/lib/kernel/special_forms.ex b/lib/elixir/lib/kernel/special_forms.ex index d19e154dac..9b06165734 100644 --- a/lib/elixir/lib/kernel/special_forms.ex +++ b/lib/elixir/lib/kernel/special_forms.ex @@ -297,7 +297,7 @@ defmodule Kernel.SpecialForms do `unsigned` (default) | `integer` `little` | `integer`, `float`, `utf16`, `utf32` `big` (default) | `integer`, `float`, `utf16`, `utf32` - `native` | `integer`, `utf16`, `utf32` + `native` | `integer`, `float`, `utf16`, `utf32` ### Sign From 61b5aafd2422bc54b1ff2cc2ebd8cad754535587 Mon Sep 17 00:00:00 2001 From: Bruce Wong Date: Mon, 2 Oct 2023 17:05:48 -0400 Subject: [PATCH 0021/1886] Update code-anti-patterns.md (#12981) Correct "string according to with the planned" --- lib/elixir/pages/anti-patterns/code-anti-patterns.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/elixir/pages/anti-patterns/code-anti-patterns.md b/lib/elixir/pages/anti-patterns/code-anti-patterns.md index c0033841d9..8ffc6486b8 100644 --- a/lib/elixir/pages/anti-patterns/code-anti-patterns.md +++ b/lib/elixir/pages/anti-patterns/code-anti-patterns.md @@ -501,7 +501,7 @@ end ``` ```elixir -# URL query string according to with the planned format - OK! +# URL query string with the planned format - OK! iex> Extract.get_value("name=Lucas&university=UFMG&lab=ASERG", "lab") "ASERG" iex> Extract.get_value("name=Lucas&university=UFMG&lab=ASERG", "university") @@ -529,7 +529,7 @@ end ``` ```elixir -# URL query string according to with the planned format - OK! +# URL query string with the planned format - OK! iex> Extract.get_value("name=Lucas&university=UFMG&lab=ASERG", "name") "Lucas" # Unplanned URL query string format - Crash explaining the problem to the client! From f9cca8fd133206ce151dc8e47843533de6c58fe7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 4 Oct 2023 09:41:53 +0200 Subject: [PATCH 0022/1886] Remove duplicate sigil docs now that guides are included --- lib/elixir/lib/macro.ex | 69 ---------------------- lib/elixir/pages/getting-started/sigils.md | 2 +- 2 files changed, 1 insertion(+), 70 deletions(-) diff --git a/lib/elixir/lib/macro.ex b/lib/elixir/lib/macro.ex index a65d816750..844ed55201 100644 --- a/lib/elixir/lib/macro.ex +++ b/lib/elixir/lib/macro.ex @@ -61,75 +61,6 @@ defmodule Macro do > The functions in this module do not evaluate code. In fact, > evaluating code from macros is often an anti-pattern. For code > evaluation, see the `Code` module. - - ## Custom Sigils - - Macros are also commonly used to implement custom sigils. - - Sigils start with `~` and are followed by one lowercase letter or by one - or more uppercase letters, and then a separator - (see the [Syntax Reference](syntax-reference.md)). One example is - `~D[2020-10-13]` to define a date. - - To create a custom sigil, define a macro with the name `sigil_{identifier}` - that takes two arguments. The first argument will be the string, the second - will be a charlist containing any modifiers. If the sigil is lower case - (such as `sigil_x`) then the string argument will allow interpolation. - If the sigil is one or more upper case letters (such as `sigil_X` and - `sigil_EXAMPLE`) then the string will not be interpolated. - - Valid modifiers are ASCII letters and digits. Any other character will - cause a syntax error. - - Single-letter sigils are typically reserved to the language. Multi-letter - sigils are uppercased and extensively used by the community to embed - alternative markups and data-types within Elixir source code. - - The module containing the custom sigil must be imported before the sigil - syntax can be used. - - ### Examples - - As an example, let's define a sigil `~x` and sigil `~X` which - return its contents as a string. However, if the `r` modifier - is given, it reverses the string instead: - - defmodule MySigils do - defmacro sigil_x(term, [?r]) do - quote do - unquote(term) |> String.reverse() - end - end - - defmacro sigil_x(term, _modifiers) do - term - end - - defmacro sigil_X(term, [?r]) do - quote do - unquote(term) |> String.reverse() - end - end - - defmacro sigil_X(term, _modifiers) do - term - end - end - - import MySigils - - ~x(with #{"inter" <> "polation"}) - #=> "with interpolation" - - ~x(with #{"inter" <> "polation"})r - #=> "noitalopretni htiw" - - ~X(without #{"interpolation"}) - #=> "without \#{"interpolation"}" - - ~X(without #{"interpolation"})r - #=> "}\"noitalopretni\"{# tuohtiw" - """ alias Code.Identifier diff --git a/lib/elixir/pages/getting-started/sigils.md b/lib/elixir/pages/getting-started/sigils.md index 6b0b99ee78..2b4cc50f36 100644 --- a/lib/elixir/pages/getting-started/sigils.md +++ b/lib/elixir/pages/getting-started/sigils.md @@ -237,4 +237,4 @@ iex> ~i(42)n Custom sigils may be either a single lowercase character or several uppercase characters. -Sigils can also be used to do compile-time work with the help of macros. For example, regular expressions in Elixir are compiled into an efficient representation during compilation of the source code, therefore skipping this step at runtime. If you're interested in the subject, we recommend you learn more about macros and check out how sigils are implemented in the `Kernel` module (where the `sigil_*` functions are defined). +Sigils can also be used to do compile-time work with the help of macros. For example, regular expressions in Elixir are compiled into an efficient representation during compilation of the source code, therefore skipping this step at runtime. If you're interested in the subject, you can learn more about macros and check out how sigils are implemented in the `Kernel` module (where the `sigil_*` functions are defined). From 8f7416d9d941fde5ec06612511be2caa8aec4f2a Mon Sep 17 00:00:00 2001 From: Artem Solomatin Date: Wed, 4 Oct 2023 17:12:31 +0300 Subject: [PATCH 0023/1886] Add typespecs to Exception module (#12983) --- lib/elixir/lib/exception.ex | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/lib/elixir/lib/exception.ex b/lib/elixir/lib/exception.ex index 39047275d7..1b37d88675 100644 --- a/lib/elixir/lib/exception.ex +++ b/lib/elixir/lib/exception.ex @@ -58,6 +58,7 @@ defmodule Exception do @doc """ Gets the message for an `exception`. """ + @spec message(t) :: String.t() def message(%module{__exception__: true} = exception) do try do module.message(exception) @@ -647,6 +648,7 @@ defmodule Exception do A stacktrace must be given as an argument. If not, the stacktrace is retrieved from `Process.info/2`. """ + @spec format_stacktrace(stacktrace) :: String.t() def format_stacktrace(trace \\ nil) do trace = if trace do @@ -673,6 +675,7 @@ defmodule Exception do #=> "#Function<...>/1" """ + @spec format_fa(fun, arity) :: String.t() def format_fa(fun, arity) when is_function(fun) do "#{inspect(fun)}#{format_arity(arity)}" end @@ -697,6 +700,7 @@ defmodule Exception do where func is the name of the enclosing function. Convert to "anonymous fn in func/arity" """ + @spec format_mfa(module, fun, arity) :: String.t() def format_mfa(module, fun, arity) when is_atom(module) and is_atom(fun) do case Code.Identifier.extract_anonymous_fun_parent(fun) do {outer_name, outer_arity} -> @@ -736,6 +740,7 @@ defmodule Exception do "" """ + @spec format_file_line(String.t() | nil, non_neg_integer | nil, String.t()) :: String.t() def format_file_line(file, line, suffix \\ "") do cond do is_nil(file) -> "" @@ -767,6 +772,12 @@ defmodule Exception do "" """ + @spec format_file_line_column( + String.t() | nil, + non_neg_integer | nil, + non_neg_integer | nil, + String.t() + ) :: String.t() def format_file_line_column(file, line, column, suffix \\ "") do cond do is_nil(file) -> "" From 1bbcf67366783aef407bd21fc02ce8c61038c8f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 4 Oct 2023 17:07:46 +0200 Subject: [PATCH 0024/1886] Keep File.stream! options as last argument --- lib/elixir/lib/file.ex | 32 +++++++++++++++++---- lib/elixir/lib/file/stream.ex | 2 +- lib/elixir/test/elixir/file/stream_test.exs | 28 ++++++++++-------- 3 files changed, 43 insertions(+), 19 deletions(-) diff --git a/lib/elixir/lib/file.ex b/lib/elixir/lib/file.ex index c603e9d1ad..f43cce1ec2 100644 --- a/lib/elixir/lib/file.ex +++ b/lib/elixir/lib/file.ex @@ -1676,6 +1676,18 @@ defmodule File do :file.close(io_device) end + @doc """ + Shortcut for `File.stream!/3`. + """ + @spec stream!(Path.t(), :line | pos_integer | [stream_mode]) :: File.Stream.t() + def stream!(path, line_or_bytes_modes \\ []) + + def stream!(path, modes) when is_list(modes), + do: stream!(path, :line, modes) + + def stream!(path, line_or_bytes) when is_integer(line_or_bytes) or line_or_bytes == :line, + do: stream!(path, line_or_bytes, []) + @doc ~S""" Returns a `File.Stream` for the given `path` with the given `modes`. @@ -1723,17 +1735,25 @@ defmodule File do ## Examples + # Read a utf8 text file which may include BOM + File.stream!("./test/test.txt", encoding: :utf8, trim_bom: true) + # Read in 2048 byte chunks rather than lines - File.stream!("./test/test.data", [], 2048) - #=> %File.Stream{line_or_bytes: 2048, modes: [:raw, :read_ahead, :binary], - #=> path: "./test/test.data", raw: true} + File.stream!("./test/test.data", 2048) See `Stream.run/1` for an example of streaming into a file. """ - @spec stream!(Path.t(), [stream_mode], :line | pos_integer) :: File.Stream.t() - def stream!(path, modes \\ [], line_or_bytes \\ :line) do + @spec stream!(Path.t(), :line | pos_integer, [stream_mode]) :: File.Stream.t() + def stream!(path, line_or_bytes, modes) + + def stream!(path, modes, line_or_bytes) when is_list(modes) do + # TODO: Deprecate this on Elixir v1.20 + stream!(path, line_or_bytes, modes) + end + + def stream!(path, line_or_bytes, modes) do modes = normalize_modes(modes, true) - File.Stream.__build__(IO.chardata_to_string(path), modes, line_or_bytes) + File.Stream.__build__(IO.chardata_to_string(path), line_or_bytes, modes) end @doc """ diff --git a/lib/elixir/lib/file/stream.ex b/lib/elixir/lib/file/stream.ex index c2be465dde..cd23f2b0b5 100644 --- a/lib/elixir/lib/file/stream.ex +++ b/lib/elixir/lib/file/stream.ex @@ -17,7 +17,7 @@ defmodule File.Stream do @type t :: %__MODULE__{} @doc false - def __build__(path, modes, line_or_bytes) do + def __build__(path, line_or_bytes, modes) do raw = :lists.keyfind(:encoding, 1, modes) == false modes = diff --git a/lib/elixir/test/elixir/file/stream_test.exs b/lib/elixir/test/elixir/file/stream_test.exs index b8c3bc7a34..2aa7703066 100644 --- a/lib/elixir/test/elixir/file/stream_test.exs +++ b/lib/elixir/test/elixir/file/stream_test.exs @@ -10,8 +10,12 @@ defmodule File.StreamTest do :ok end - defp stream!(node, src, modes \\ [], lines_or_bytes \\ :line) do - :erpc.call(node, File, :stream!, [src, modes, lines_or_bytes]) + defp stream!(node, src, lines_or_bytes_or_modes \\ []) do + :erpc.call(node, File, :stream!, [src, lines_or_bytes_or_modes]) + end + + defp stream!(node, src, lines_or_bytes, modes) do + :erpc.call(node, File, :stream!, [src, lines_or_bytes, modes]) end distributed_node = :"secondary@#{node() |> Atom.to_string() |> :binary.split("@") |> tl()}" @@ -54,7 +58,7 @@ defmodule File.StreamTest do stream = stream!(@node, src, [:utf8]) assert Enum.count(stream) == 1 - stream = stream!(@node, src, [], 2) + stream = stream!(@node, src, 2) assert Enum.count(stream) == 2 end @@ -82,7 +86,7 @@ defmodule File.StreamTest do dest = tmp_path("tmp_test.txt") try do - stream = stream!(@node, src, [], 1) + stream = stream!(@node, src, 1) File.open(dest, [:write], fn target -> Enum.each(stream, fn <> -> @@ -145,11 +149,11 @@ defmodule File.StreamTest do |> Enum.take(1) == [<<239, 187, 191>> <> "Русский\n"] assert @node - |> stream!(src, [], 1) + |> stream!(src, 1) |> Enum.take(5) == [<<239>>, <<187>>, <<191>>, <<208>>, <<160>>] assert @node |> stream!(src, []) |> Enum.count() == 2 - assert @node |> stream!(src, [], 1) |> Enum.count() == 22 + assert @node |> stream!(src, 1) |> Enum.count() == 22 end test "trims BOM via option when raw" do @@ -164,7 +168,7 @@ defmodule File.StreamTest do |> Enum.take(5) == [<<208>>, <<160>>, <<209>>, <<131>>, <<209>>] assert @node |> stream!(src, [:trim_bom]) |> Enum.count() == 2 - assert @node |> stream!(src, [:trim_bom], 1) |> Enum.count() == 19 + assert @node |> stream!(src, 1, [:trim_bom]) |> Enum.count() == 19 end test "keeps BOM with utf8 encoding" do @@ -175,7 +179,7 @@ defmodule File.StreamTest do |> Enum.take(1) == [<<239, 187, 191>> <> "Русский\n"] assert @node - |> stream!(src, [{:encoding, :utf8}], 1) + |> stream!(src, 1, [{:encoding, :utf8}]) |> Enum.take(9) == ["\uFEFF", "Р", "у", "с", "с", "к", "и", "й", "\n"] end @@ -187,7 +191,7 @@ defmodule File.StreamTest do |> Enum.take(1) == ["Русский\n"] assert @node - |> stream!(src, [{:encoding, :utf8}, :trim_bom], 1) + |> stream!(src, 1, [{:encoding, :utf8}, :trim_bom]) |> Enum.take(8) == ["Р", "у", "с", "с", "к", "и", "й", "\n"] end @@ -215,7 +219,7 @@ defmodule File.StreamTest do |> Enum.take(1) == ["Русский\n"] assert @node - |> stream!(src, [{:encoding, {:utf16, :big}}, :trim_bom], 1) + |> stream!(src, 1, [{:encoding, {:utf16, :big}}, :trim_bom]) |> Enum.take(8) == ["Р", "у", "с", "с", "к", "и", "й", "\n"] end @@ -227,7 +231,7 @@ defmodule File.StreamTest do |> Enum.take(1) == ["Русский\n"] assert @node - |> stream!(src, [{:encoding, {:utf16, :little}}, :trim_bom], 1) + |> stream!(src, 1, [{:encoding, {:utf16, :little}}, :trim_bom]) |> Enum.take(8) == ["Р", "у", "с", "с", "к", "и", "й", "\n"] end @@ -255,7 +259,7 @@ defmodule File.StreamTest do dest = tmp_path("tmp_test.txt") try do - stream = stream!(@node, src, [:utf8], 1) + stream = stream!(@node, src, 1, [:utf8]) File.open(dest, [:write], fn target -> Enum.each(stream, fn <> -> From d1a218b893d0d31bff24b82670491d46eefdcb8a Mon Sep 17 00:00:00 2001 From: Artem Solomatin Date: Wed, 4 Oct 2023 22:37:57 +0300 Subject: [PATCH 0025/1886] Add missing typespecs for String.normalize (#12988) --- lib/elixir/lib/string.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/elixir/lib/string.ex b/lib/elixir/lib/string.ex index c5c15142aa..29f0166916 100644 --- a/lib/elixir/lib/string.ex +++ b/lib/elixir/lib/string.ex @@ -759,6 +759,7 @@ defmodule String do "fi" """ + @spec normalize(t, :nfd | :nfc | :nfkd | :nfkc) :: t def normalize(string, form) def normalize(string, :nfd) when is_binary(string) do From 0886847c4ca6ee72b2a659ff7e91a855bfad4897 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20=C5=81=C4=99picki?= Date: Thu, 5 Oct 2023 09:49:08 +0200 Subject: [PATCH 0026/1886] Fix Exception.format_stacktrace/1 spec (#12991) --- lib/elixir/lib/exception.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/lib/exception.ex b/lib/elixir/lib/exception.ex index 1b37d88675..d775f4509d 100644 --- a/lib/elixir/lib/exception.ex +++ b/lib/elixir/lib/exception.ex @@ -648,7 +648,7 @@ defmodule Exception do A stacktrace must be given as an argument. If not, the stacktrace is retrieved from `Process.info/2`. """ - @spec format_stacktrace(stacktrace) :: String.t() + @spec format_stacktrace(stacktrace | nil) :: String.t() def format_stacktrace(trace \\ nil) do trace = if trace do From f1d7f096488b9df84a2a5383a13ff5036685f254 Mon Sep 17 00:00:00 2001 From: Artem Solomatin Date: Thu, 5 Oct 2023 10:50:00 +0300 Subject: [PATCH 0027/1886] Add missing typespecs IO.stream and IO.binstream (#12989) --- lib/elixir/lib/io.ex | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/elixir/lib/io.ex b/lib/elixir/lib/io.ex index ed6745cabc..d5d0788b6d 100644 --- a/lib/elixir/lib/io.ex +++ b/lib/elixir/lib/io.ex @@ -567,6 +567,7 @@ defmodule IO do """ @doc since: "1.12.0" + @spec stream() :: Enumerable.t(String.t()) def stream, do: stream(:stdio, :line) @doc """ @@ -620,6 +621,7 @@ defmodule IO do """ @doc since: "1.12.0" + @spec binstream() :: Enumerable.t(binary) def binstream, do: binstream(:stdio, :line) @doc """ From e1edece58c84713568791352424a088435d90c89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20=C5=81=C4=99picki?= Date: Thu, 5 Oct 2023 09:55:08 +0200 Subject: [PATCH 0028/1886] Fix Exception.format_mfa/3 spec (#12990) --- lib/elixir/lib/exception.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/lib/exception.ex b/lib/elixir/lib/exception.ex index d775f4509d..bf871c361a 100644 --- a/lib/elixir/lib/exception.ex +++ b/lib/elixir/lib/exception.ex @@ -700,7 +700,7 @@ defmodule Exception do where func is the name of the enclosing function. Convert to "anonymous fn in func/arity" """ - @spec format_mfa(module, fun, arity) :: String.t() + @spec format_mfa(module, atom, arity) :: String.t() def format_mfa(module, fun, arity) when is_atom(module) and is_atom(fun) do case Code.Identifier.extract_anonymous_fun_parent(fun) do {outer_name, outer_arity} -> From a19140a479a3a263fa4769b58aa7c7bffd01da25 Mon Sep 17 00:00:00 2001 From: Artem Solomatin Date: Fri, 6 Oct 2023 06:36:34 +0300 Subject: [PATCH 0029/1886] Add typespec for Module.reserved_attributes (#12993) --- lib/elixir/lib/module.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/elixir/lib/module.ex b/lib/elixir/lib/module.ex index 215cd46cf9..3df0a1402e 100644 --- a/lib/elixir/lib/module.ex +++ b/lib/elixir/lib/module.ex @@ -629,6 +629,7 @@ defmodule Module do """ @doc since: "1.12.0" + @spec reserved_attributes() :: map def reserved_attributes() do %{ after_compile: %{ From 4d69bd7059ef5ae15783163510cde363b61ab7db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20=C5=81=C4=99picki?= Date: Fri, 6 Oct 2023 08:35:27 +0200 Subject: [PATCH 0030/1886] Fix Exception.format_mfa spec again (#12994) > The arity may also be a list of arguments. --- lib/elixir/lib/exception.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/lib/exception.ex b/lib/elixir/lib/exception.ex index bf871c361a..64f0105c27 100644 --- a/lib/elixir/lib/exception.ex +++ b/lib/elixir/lib/exception.ex @@ -700,7 +700,7 @@ defmodule Exception do where func is the name of the enclosing function. Convert to "anonymous fn in func/arity" """ - @spec format_mfa(module, atom, arity) :: String.t() + @spec format_mfa(module, atom, arity_or_args) :: String.t() def format_mfa(module, fun, arity) when is_atom(module) and is_atom(fun) do case Code.Identifier.extract_anonymous_fun_parent(fun) do {outer_name, outer_arity} -> From b710c8822ce3c16ceebcbeab990a6b0bb6abe210 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 6 Oct 2023 14:39:01 +0200 Subject: [PATCH 0031/1886] Delegate escape characters to regex --- lib/elixir/lib/kernel.ex | 6 ------ lib/elixir/test/elixir/regex_test.exs | 9 +++------ 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/lib/elixir/lib/kernel.ex b/lib/elixir/lib/kernel.ex index 158273238d..954932307e 100644 --- a/lib/elixir/lib/kernel.ex +++ b/lib/elixir/lib/kernel.ex @@ -6199,12 +6199,6 @@ defmodule Kernel do end defp regex_unescape_map(:newline), do: true - defp regex_unescape_map(?f), do: ?\f - defp regex_unescape_map(?n), do: ?\n - defp regex_unescape_map(?r), do: ?\r - defp regex_unescape_map(?t), do: ?\t - defp regex_unescape_map(?v), do: ?\v - defp regex_unescape_map(?a), do: ?\a defp regex_unescape_map(_), do: false @doc false diff --git a/lib/elixir/test/elixir/regex_test.exs b/lib/elixir/test/elixir/regex_test.exs index df12dc2dba..46b85a9778 100644 --- a/lib/elixir/test/elixir/regex_test.exs +++ b/lib/elixir/test/elixir/regex_test.exs @@ -71,15 +71,12 @@ defmodule RegexTest do assert Regex.re_pattern(Regex.compile!("foo")) == Regex.re_pattern(~r"foo") assert Regex.source(Regex.compile!("\a\b\d\e\f\n\r\s\t\v")) == "\a\b\d\e\f\n\r\s\t\v" - assert Regex.source(~r<\a\b\d\e\f\n\r\s\t\v>) == "\a\\b\\d\\e\f\n\r\\s\t\v" + assert Regex.source(~r<\a\b\d\e\f\n\r\s\t\v>) == "\\a\\b\\d\\e\\f\\n\\r\\s\\t\\v" assert Regex.re_pattern(Regex.compile!("\a\b\d\e\f\n\r\s\t\v")) == - Regex.re_pattern(~r"\a\010\177\033\f\n\r \t\v") + Regex.re_pattern(~r"\x07\x08\x7F\x1B\x0C\x0A\x0D\x20\x09\x0B") - assert Regex.source(Regex.compile!("\a\\b\\d\e\f\n\r\\s\t\v")) == "\a\\b\\d\e\f\n\r\\s\t\v" - assert Regex.source(~r<\a\\b\\d\\e\f\n\r\\s\t\v>) == "\a\\\\b\\\\d\\\\e\f\n\r\\\\s\t\v" - - assert Regex.re_pattern(Regex.compile!("\a\\b\\d\e\f\n\r\\s\t\v")) == + assert Regex.re_pattern(Regex.compile!("\\a\\b\\d\e\f\\n\\r\\s\\t\\v")) == Regex.re_pattern(~r"\a\b\d\e\f\n\r\s\t\v") end From 1f4f0aba3b9c574b36e790baa0cf19647f782a37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 6 Oct 2023 14:43:41 +0200 Subject: [PATCH 0032/1886] Delegate and document escape sequences to regexes --- lib/elixir/lib/regex.ex | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/lib/elixir/lib/regex.ex b/lib/elixir/lib/regex.ex index c0d426e87e..21dcbf59da 100644 --- a/lib/elixir/lib/regex.ex +++ b/lib/elixir/lib/regex.ex @@ -34,6 +34,37 @@ defmodule Regex do ~r/(?.)(?.)/.source == ~r/(?.)(?.)/.source + ## Escapes + + Escape sequences are split into two categories. + + ### Non-printing characters + + * `\a` - Alarm, that is, the BEL character (hex 07) + * `\e` - Escape (hex 1B) + * `\f` - Form feed (hex 0C) + * `\n` - Line feed (hex 0A) + * `\r` - Carriage return (hex 0D) + * `\t` - Tab (hex 09) + * `\xhh` - Character with hex code hh + * `\x{hhh..}` - Character with hex code hhh.. + + `\u` and `\U` are not supported. Other escape sequences, such as `\ddd` + for octals, are supported but discouraged. + + ### Generic character types + + * `\d` - Any decimal digit + * `\D` - Any character that is not a decimal digit + * `\h` - Any horizontal whitespace character + * `\H` - Any character that is not a horizontal whitespace character + * `\s` - Any whitespace character + * `\S` - Any character that is not a whitespace character + * `\v` - Any vertical whitespace character + * `\V` - Any character that is not a vertical whitespace character + * `\w` - Any "word" character + * `\W` - Any "non-word" character + ## Modifiers The modifiers available when creating a Regex are: From e3d5bb718e1aca25c8370205f044dc41409f1e63 Mon Sep 17 00:00:00 2001 From: Adam Millerchip Date: Sat, 7 Oct 2023 01:28:43 +0900 Subject: [PATCH 0033/1886] Add example of seeking within a file to File docs (#12996) --- lib/elixir/lib/file.ex | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/lib/elixir/lib/file.ex b/lib/elixir/lib/file.ex index f43cce1ec2..f9f928146f 100644 --- a/lib/elixir/lib/file.ex +++ b/lib/elixir/lib/file.ex @@ -74,6 +74,29 @@ defmodule File do Check `:file.open/2` for more information about such options and other performance considerations. + + ## Seeking within a file + + You may also use any of the functions from the [`:file`](`:file`) + module to interact with files returned by Elixir. For example, + to read from a specific position in a file, use `:file.pread/3`: + + File.write!("example.txt", "Eats, Shoots & Leaves") + file = File.open!("example.txt") + :file.pread(file, 15, 6) + #=> {:ok, "Leaves"} + + Alternatively, if you need to keep track of the current position, + use `:file.position/2` and `:file.read/2`: + + :file.position(file, 6) + #=> {:ok, 6} + :file.read(file, 6) + #=> {:ok, "Shoots"} + :file.position(file, {:cur, -12}) + #=> {:ok, 0} + :file.read(file, 4) + #=> {:ok, "Eats"} """ @type posix :: :file.posix() From a4c700b23b035b2fc534cf75d17af08b5e4e9780 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sat, 7 Oct 2023 17:23:11 +0200 Subject: [PATCH 0034/1886] Unify caret position in diagnostics Closes #12995. --- lib/elixir/lib/module/types/expr.ex | 6 +- lib/elixir/src/elixir_expand.erl | 22 +++---- lib/elixir/src/elixir_fn.erl | 28 ++++----- lib/elixir/test/elixir/code_test.exs | 2 +- .../test/elixir/kernel/diagnostics_test.exs | 28 ++++----- .../test/elixir/kernel/expansion_test.exs | 4 +- .../elixir/module/types/integration_test.exs | 58 +++++++++---------- 7 files changed, 74 insertions(+), 74 deletions(-) diff --git a/lib/elixir/lib/module/types/expr.ex b/lib/elixir/lib/module/types/expr.ex index 47e86fa228..9931e2906b 100644 --- a/lib/elixir/lib/module/types/expr.ex +++ b/lib/elixir/lib/module/types/expr.ex @@ -391,10 +391,10 @@ defmodule Module.Types.Expr do end # expr.fun(arg) - def of_expr({{:., meta1, [expr1, fun]}, _meta2, args} = expr2, _expected, stack, context) do + def of_expr({{:., _meta1, [expr1, fun]}, meta2, args} = expr2, _expected, stack, context) do # TODO: Use expected type to infer intersection return type - context = Of.remote(expr1, fun, length(args), meta1, context) + context = Of.remote(expr1, fun, length(args), meta2, context) stack = push_expr_stack(expr2, stack) with {:ok, _expr_type, context} <- of_expr(expr1, :dynamic, stack, context), @@ -407,7 +407,7 @@ defmodule Module.Types.Expr do # &Foo.bar/1 def of_expr( - {:&, meta, [{:/, _, [{{:., _, [module, fun]}, _, []}, arity]}]}, + {:&, _, [{:/, _, [{{:., _, [module, fun]}, meta, []}, arity]}]}, _expected, _stack, context diff --git a/lib/elixir/src/elixir_expand.erl b/lib/elixir/src/elixir_expand.erl index 3b6d67a491..2c4420cccf 100644 --- a/lib/elixir/src/elixir_expand.erl +++ b/lib/elixir/src/elixir_expand.erl @@ -209,12 +209,12 @@ expand({'&', Meta, [{super, SuperMeta, Args} = Expr]}, S, E) when is_list(Args) expand_fn_capture(Meta, Expr, S, E) end; -expand({'&', Meta, [{'/', _, [{super, _, Context}, Arity]} = Expr]}, S, E) when is_atom(Context), is_integer(Arity) -> +expand({'&', Meta, [{'/', ArityMeta, [{super, SuperMeta, Context}, Arity]} = Expr]}, S, E) when is_atom(Context), is_integer(Arity) -> assert_no_match_or_guard_scope(Meta, "&", S, E), case resolve_super(Meta, Arity, E) of {Kind, Name, _} when Kind == def; Kind == defp -> - {{'&', Meta, [{'/', [], [{Name, [], Context}, Arity]}]}, S, E}; + {{'&', Meta, [{'/', ArityMeta, [{Name, SuperMeta, Context}, Arity]}]}, S, E}; _ -> expand_fn_capture(Meta, Expr, S, E) end; @@ -510,15 +510,15 @@ resolve_super(Meta, Arity, E) -> expand_fn_capture(Meta, Arg, S, E) -> case elixir_fn:capture(Meta, Arg, S, E) of - {{remote, Remote, Fun, Arity}, RemoteMeta, SE, EE} -> + {{remote, Remote, Fun, Arity}, RequireMeta, DotMeta, SE, EE} -> is_atom(Remote) andalso - elixir_env:trace({remote_function, RemoteMeta, Remote, Fun, Arity}, E), - AttachedMeta = attach_context_module(Remote, Meta, E), - {{'&', AttachedMeta, [{'/', [], [{{'.', [], [Remote, Fun]}, [], []}, Arity]}]}, SE, EE}; - {{local, Fun, Arity}, _LocalMeta, _SE, #{function := nil}} -> + elixir_env:trace({remote_function, RequireMeta, Remote, Fun, Arity}, E), + AttachedMeta = attach_context_module(Remote, RequireMeta, E), + {{'&', Meta, [{'/', [], [{{'.', DotMeta, [Remote, Fun]}, AttachedMeta, []}, Arity]}]}, SE, EE}; + {{local, Fun, Arity}, _, _, _SE, #{function := nil}} -> file_error(Meta, E, ?MODULE, {undefined_local_capture, Fun, Arity}); - {{local, Fun, Arity}, _LocalMeta, SE, EE} -> - {{'&', Meta, [{'/', [], [{Fun, [], nil}, Arity]}]}, SE, EE}; + {{local, Fun, Arity}, LocalMeta, _, SE, EE} -> + {{'&', Meta, [{'/', [], [{Fun, LocalMeta, nil}, Arity]}]}, SE, EE}; {expand, Expr, SE, EE} -> expand(Expr, SE, EE) end. @@ -864,10 +864,10 @@ expand_remote(Receiver, DotMeta, Right, Meta, NoParens, Args, S, SL, #{context : {{{'.', DotMeta, [Receiver, Right]}, Meta, []}, SL, E}; true -> - AttachedDotMeta = attach_context_module(Receiver, DotMeta, E), + AttachedMeta = attach_context_module(Receiver, Meta, E), {EArgs, {SA, _}, EA} = mapfold(fun expand_arg/3, {SL, S}, E, Args), - case rewrite(Context, Receiver, AttachedDotMeta, Right, Meta, EArgs, S) of + case rewrite(Context, Receiver, DotMeta, Right, AttachedMeta, EArgs, S) of {ok, Rewritten} -> maybe_warn_comparison(Rewritten, Args, E), {Rewritten, elixir_env:close_write(SA, S), EA}; diff --git a/lib/elixir/src/elixir_fn.erl b/lib/elixir/src/elixir_fn.erl index 5b24356892..a7ef530830 100644 --- a/lib/elixir/src/elixir_fn.erl +++ b/lib/elixir/src/elixir_fn.erl @@ -41,14 +41,14 @@ fn_arity(Args) -> length(Args). capture(Meta, {'/', _, [{{'.', _, [M, F]} = Dot, RequireMeta, []}, A]}, S, E) when is_atom(F), is_integer(A) -> Args = args_from_arity(Meta, A, E), handle_capture_possible_warning(Meta, RequireMeta, M, F, A, E), - capture_require(Meta, {Dot, RequireMeta, Args}, S, E, true); + capture_require({Dot, RequireMeta, Args}, S, E, true); capture(Meta, {'/', _, [{F, ImportMeta, C}, A]}, S, E) when is_atom(F), is_integer(A), is_atom(C) -> Args = args_from_arity(Meta, A, E), - capture_import(Meta, {F, ImportMeta, Args}, S, E, true); + capture_import({F, ImportMeta, Args}, S, E, true); -capture(Meta, {{'.', _, [_, Fun]}, _, Args} = Expr, S, E) when is_atom(Fun), is_list(Args) -> - capture_require(Meta, Expr, S, E, is_sequential_and_not_empty(Args)); +capture(_Meta, {{'.', _, [_, Fun]}, _, Args} = Expr, S, E) when is_atom(Fun), is_list(Args) -> + capture_require(Expr, S, E, is_sequential_and_not_empty(Args)); capture(Meta, {{'.', _, [_]}, _, Args} = Expr, S, E) when is_list(Args) -> capture_expr(Meta, Expr, S, E, false); @@ -59,8 +59,8 @@ capture(Meta, {'__block__', _, [Expr]}, S, E) -> capture(Meta, {'__block__', _, _} = Expr, _S, E) -> file_error(Meta, E, ?MODULE, {block_expr_in_capture, Expr}); -capture(Meta, {Atom, _, Args} = Expr, S, E) when is_atom(Atom), is_list(Args) -> - capture_import(Meta, Expr, S, E, is_sequential_and_not_empty(Args)); +capture(_Meta, {Atom, _, Args} = Expr, S, E) when is_atom(Atom), is_list(Args) -> + capture_import(Expr, S, E, is_sequential_and_not_empty(Args)); capture(Meta, {Left, Right}, S, E) -> capture(Meta, {'{}', Meta, [Left, Right]}, S, E); @@ -74,12 +74,12 @@ capture(Meta, Integer, _S, E) when is_integer(Integer) -> capture(Meta, Arg, _S, E) -> invalid_capture(Meta, Arg, E). -capture_import(Meta, {Atom, ImportMeta, Args} = Expr, S, E, Sequential) -> +capture_import({Atom, ImportMeta, Args} = Expr, S, E, Sequential) -> Res = Sequential andalso elixir_dispatch:import_function(ImportMeta, Atom, length(Args), E), - handle_capture(Res, Meta, Expr, S, E, Sequential). + handle_capture(Res, ImportMeta, ImportMeta, Expr, S, E, Sequential). -capture_require(Meta, {{'.', DotMeta, [Left, Right]}, RequireMeta, Args}, S, E, Sequential) -> +capture_require({{'.', DotMeta, [Left, Right]}, RequireMeta, Args}, S, E, Sequential) -> case escape(Left, E, []) of {EscLeft, []} -> {ELeft, SE, EE} = elixir_expand:expand(EscLeft, S, E), @@ -94,17 +94,17 @@ capture_require(Meta, {{'.', DotMeta, [Left, Right]}, RequireMeta, Args}, S, E, end, Dot = {{'.', DotMeta, [ELeft, Right]}, RequireMeta, Args}, - handle_capture(Res, RequireMeta, Dot, SE, EE, Sequential); + handle_capture(Res, RequireMeta, DotMeta, Dot, SE, EE, Sequential); {EscLeft, Escaped} -> Dot = {{'.', DotMeta, [EscLeft, Right]}, RequireMeta, Args}, - capture_expr(Meta, Dot, S, E, Escaped, Sequential) + capture_expr(RequireMeta, Dot, S, E, Escaped, Sequential) end. -handle_capture(false, Meta, Expr, S, E, Sequential) -> +handle_capture(false, Meta, _DotMeta, Expr, S, E, Sequential) -> capture_expr(Meta, Expr, S, E, Sequential); -handle_capture(LocalOrRemote, Meta, _Expr, S, E, _Sequential) -> - {LocalOrRemote, Meta, S, E}. +handle_capture(LocalOrRemote, Meta, DotMeta, _Expr, S, E, _Sequential) -> + {LocalOrRemote, Meta, DotMeta, S, E}. capture_expr(Meta, Expr, S, E, Sequential) -> capture_expr(Meta, Expr, S, E, [], Sequential). diff --git a/lib/elixir/test/elixir/code_test.exs b/lib/elixir/test/elixir/code_test.exs index f7f6a5a62e..0b2fc119d6 100644 --- a/lib/elixir/test/elixir/code_test.exs +++ b/lib/elixir/test/elixir/code_test.exs @@ -100,7 +100,7 @@ defmodule CodeTest do end """ - assert {_, [%{position: {3, 17}}]} = + assert {_, [%{position: {3, 18}}]} = Code.with_diagnostics(fn -> quoted = Code.string_to_quoted!(sample, columns: true) Code.eval_quoted(quoted, []) diff --git a/lib/elixir/test/elixir/kernel/diagnostics_test.exs b/lib/elixir/test/elixir/kernel/diagnostics_test.exs index e074475d5f..34bd9a5cd4 100644 --- a/lib/elixir/test/elixir/kernel/diagnostics_test.exs +++ b/lib/elixir/test/elixir/kernel/diagnostics_test.exs @@ -489,9 +489,9 @@ defmodule Kernel.DiagnosticsTest do warning: Unknown.b/0 is undefined (module Unknown is not available or is yet to be defined) │ 3 │ defp a, do: Unknown.b() - │ ~ + │ ~ │ - └─ #{path}:3:22: Sample.a/0 + └─ #{path}:3:23: Sample.a/0 """ assert capture_eval(source) =~ expected @@ -535,7 +535,7 @@ defmodule Kernel.DiagnosticsTest do expected = """ warning: Unknown.b/0 is undefined (module Unknown is not available or is yet to be defined) - └─ nofile:2:22: Sample.a/0 + └─ nofile:2:23: Sample.a/0 """ assert capture_eval(source) =~ expected @@ -661,9 +661,9 @@ defmodule Kernel.DiagnosticsTest do warning: Unknown.bar/1 is undefined (module Unknown is not available or is yet to be defined) │ 5 │ ... Unknown.bar(:test) - │ ~ + │ ~ │ - └─ #{path}:5:52: Sample.a/0 + └─ #{path}:5:53: Sample.a/0 """ @@ -710,10 +710,10 @@ defmodule Kernel.DiagnosticsTest do expected = """ warning: Unknown.bar/0 is undefined (module Unknown is not available or is yet to be defined) - └─ nofile:3:12: Sample.a/0 - └─ nofile:4:12: Sample.a/0 - └─ nofile:5:12: Sample.a/0 - └─ nofile:6:12: Sample.a/0 + └─ nofile:3:13: Sample.a/0 + └─ nofile:4:13: Sample.a/0 + └─ nofile:5:13: Sample.a/0 + └─ nofile:6:13: Sample.a/0 """ @@ -745,12 +745,12 @@ defmodule Kernel.DiagnosticsTest do warning: Unknown.bar/0 is undefined (module Unknown is not available or is yet to be defined) │ 5 │ Unknown.bar() - │ ~ + │ ~ │ - └─ #{path}:5:12: Sample.a/0 - └─ #{path}:6:12: Sample.a/0 - └─ #{path}:7:12: Sample.a/0 - └─ #{path}:8:12: Sample.a/0 + └─ #{path}:5:13: Sample.a/0 + └─ #{path}:6:13: Sample.a/0 + └─ #{path}:7:13: Sample.a/0 + └─ #{path}:8:13: Sample.a/0 """ diff --git a/lib/elixir/test/elixir/kernel/expansion_test.exs b/lib/elixir/test/elixir/kernel/expansion_test.exs index 317b2b7b15..eb14aa602c 100644 --- a/lib/elixir/test/elixir/kernel/expansion_test.exs +++ b/lib/elixir/test/elixir/kernel/expansion_test.exs @@ -1143,10 +1143,10 @@ defmodule Kernel.ExpansionTest do test "expands remotes" do assert expand(quote(do: &List.flatten/2)) == quote(do: &:"Elixir.List".flatten/2) - |> clean_meta([:imports, :context, :no_parens]) + |> clean_meta([:imports, :context]) assert expand(quote(do: &Kernel.is_atom/1)) == - quote(do: &:erlang.is_atom/1) |> clean_meta([:imports, :context, :no_parens]) + quote(do: &:erlang.is_atom/1) |> clean_meta([:imports, :context]) end test "expands macros" do diff --git a/lib/elixir/test/elixir/module/types/integration_test.exs b/lib/elixir/test/elixir/module/types/integration_test.exs index 59fc5b0b7f..d38a6ce617 100644 --- a/lib/elixir/test/elixir/module/types/integration_test.exs +++ b/lib/elixir/test/elixir/module/types/integration_test.exs @@ -67,9 +67,9 @@ defmodule Module.Types.IntegrationTest do warnings = [ ":not_a_module.no_module/0 is undefined (module :not_a_module is not available or is yet to be defined)", - "a.ex:2:27: A.a/0", + "a.ex:2:28: A.a/0", ":lists.no_func/0 is undefined or private", - "a.ex:3:20: A.b/0" + "a.ex:3:21: A.b/0" ] assert_warnings(files, warnings) @@ -90,7 +90,7 @@ defmodule Module.Types.IntegrationTest do warnings = [ "Kernel.behaviour_info/1 is undefined or private", - "a.ex:6:20: A.e/0" + "a.ex:6:21: A.e/0" ] assert_warnings(files, warnings) @@ -125,7 +125,7 @@ defmodule Module.Types.IntegrationTest do "List.old_flatten/1 is undefined or private. Did you mean:", "* flatten/1", "* flatten/2", - "a.ex:15:32: A.flatten2/1" + "a.ex:15:33: A.flatten2/1" ] assert_warnings(files, warnings) @@ -146,9 +146,9 @@ defmodule Module.Types.IntegrationTest do warnings = [ "A.no_func/0 is undefined or private", - "a.ex:2:15: A.a/0", + "a.ex:2:16: A.a/0", "A.no_func/1 is undefined or private", - "external_source.ex:6:14: A.c/0" + "external_source.ex:6:17: A.c/0" ] assert_warnings(files, warnings) @@ -170,10 +170,10 @@ defmodule Module.Types.IntegrationTest do warnings = [ "A.a/1 is undefined or private. Did you mean:", "* a/0", - "a.ex:3:15: A.b/0", + "a.ex:3:16: A.b/0", "A.b/1 is undefined or private. Did you mean:", "* b/0", - "external_source.ex:6:15: A.c/0" + "external_source.ex:6:16: A.c/0" ] assert_warnings(files, warnings) @@ -195,11 +195,11 @@ defmodule Module.Types.IntegrationTest do warnings = [ "D.no_module/0 is undefined (module D is not available or is yet to be defined)", - "a.ex:2:15: A.a/0", + "a.ex:2:16: A.a/0", "E.no_module/0 is undefined (module E is not available or is yet to be defined)", - "external_source.ex:5:15: A.c/0", + "external_source.ex:5:16: A.c/0", "Io.puts/1 is undefined (module Io is not available or is yet to be defined)", - "a.ex:7:16: A.i/0" + "a.ex:7:17: A.i/0" ] assert_warnings(files, warnings) @@ -219,9 +219,9 @@ defmodule Module.Types.IntegrationTest do warnings = [ "A.no_func/0 is undefined or private", - "a.ex:2:14: A.a/0", + "a.ex:2:17: A.a/0", "A.no_func/1 is undefined or private", - "external_source.ex:5:14: A.c/0" + "external_source.ex:5:17: A.c/0" ] assert_warnings(files, warnings) @@ -261,9 +261,9 @@ defmodule Module.Types.IntegrationTest do warnings = [ "B.no_func/0 is undefined or private", - "a.ex:2:15: A.a/0", + "a.ex:2:16: A.a/0", "A.no_func/0 is undefined or private", - "b.ex:2:15: B.a/0" + "b.ex:2:16: B.a/0" ] assert_warnings(files, warnings) @@ -286,11 +286,11 @@ defmodule Module.Types.IntegrationTest do warnings = [ "A2.no_func/0 is undefined (module A2 is not available or is yet to be defined)", - "└─ a.ex:8:16: A.d/0", - "└─ external_source.ex:5:16: A.b/0", + "└─ a.ex:8:17: A.d/0", + "└─ external_source.ex:5:17: A.b/0", "A.no_func/0 is undefined or private", - "└─ a.ex:2:15: A.a/0", - "└─ a.ex:7:15: A.c/0" + "└─ a.ex:2:16: A.a/0", + "└─ a.ex:7:16: A.c/0" ] assert_warnings(files, warnings) @@ -313,7 +313,7 @@ defmodule Module.Types.IntegrationTest do warnings = [ "B.no_func/0 is undefined (module B is not available or is yet to be defined)", - "a.ex:7:23: AProtocol.AImplementation.func/1" + "a.ex:7:24: AProtocol.AImplementation.func/1" ] assert_warnings(files, warnings) @@ -349,7 +349,7 @@ defmodule Module.Types.IntegrationTest do warnings = [ "A.to_list/1 is undefined or private. Did you mean:", "* to_charlist/1", - "a.ex:7:18: A.c/1" + "a.ex:7:19: A.c/1" ] assert_warnings(files, warnings) @@ -388,7 +388,7 @@ defmodule Module.Types.IntegrationTest do warnings = [ "you must require B before invoking the macro B.b/0", - "ab.ex:2:17: A.a/0" + "ab.ex:2:18: A.a/0" ] assert_warnings(files, warnings) @@ -419,12 +419,12 @@ defmodule Module.Types.IntegrationTest do warnings = [ "MissingModule2.func/1 is undefined (module MissingModule2 is not available or is yet to be defined)", - "a.ex:7:28: A.c/0", + "a.ex:7:29: A.c/0", "MissingModule3.func/2 is undefined (module MissingModule3 is not available or is yet to be defined)", - "a.ex:8:28: A.d/0", + "a.ex:8:29: A.d/0", "B.func/3 is undefined or private. Did you mean:", "* func/1", - "a.ex:11:15: A.g/0" + "a.ex:11:16: A.g/0" ] assert_warnings(files, warnings) @@ -462,12 +462,12 @@ defmodule Module.Types.IntegrationTest do warnings = [ "MissingModule2.func/1 is undefined (module MissingModule2 is not available or is yet to be defined)", - "a.ex:7:28: A.c/0", + "a.ex:7:29: A.c/0", "MissingModule3.func/2 is undefined (module MissingModule3 is not available or is yet to be defined)", - "a.ex:8:28: A.d/0", + "a.ex:8:29: A.d/0", "B.func/3 is undefined or private. Did you mean:", "* func/1", - "a.ex:11:15: A.g/0" + "a.ex:11:16: A.g/0" ] assert_warnings(files, warnings) @@ -588,7 +588,7 @@ defmodule Module.Types.IntegrationTest do warnings = [ "A.a/0 is deprecated. oops", - "a.ex:3:15: A.a/0" + "a.ex:3:16: A.a/0" ] assert_warnings(files, warnings) From 47d706734c477257b63fba6ca762f0b206c334a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20M=C3=BCller?= Date: Sat, 23 Sep 2023 00:43:37 -0300 Subject: [PATCH 0035/1886] Add error span for unknown local call --- lib/elixir/lib/module/locals_tracker.ex | 36 +++++++++---------- lib/elixir/src/elixir_locals.erl | 4 +-- .../test/elixir/kernel/diagnostics_test.exs | 31 ++++++++++++++++ lib/elixir/test/elixir/kernel/errors_test.exs | 2 +- .../elixir/module/locals_tracker_test.exs | 16 +++++---- 5 files changed, 62 insertions(+), 27 deletions(-) diff --git a/lib/elixir/lib/module/locals_tracker.ex b/lib/elixir/lib/module/locals_tracker.ex index f3e350c1b7..b389bc3d03 100644 --- a/lib/elixir/lib/module/locals_tracker.ex +++ b/lib/elixir/lib/module/locals_tracker.ex @@ -102,12 +102,21 @@ defmodule Module.LocalsTracker do @doc """ Collect undefined functions based on local calls and existing definitions. """ - def collect_undefined_locals({set, bag}, all_defined) do + def collect_undefined_locals({set, bag}, all_defined, file) do undefined = for {pair, _, meta, _} <- all_defined, - {local, position, macro_dispatch?} <- out_neighbours(bag, {:local, pair}), - error = undefined_local_error(set, local, macro_dispatch?), - do: {pair, build_meta(position, meta), local, error} + {{local_name, _} = local, position, macro_dispatch?} <- + out_neighbours(bag, {:local, pair}), + error = undefined_local_error(set, local, macro_dispatch?) do + file = + case Keyword.get(meta, :file) do + {keep_file, _keep_line} -> keep_file + nil -> file + end + + meta = build_meta(position, local_name) + {pair, meta, file, local, error} + end :lists.usort(undefined) end @@ -212,21 +221,12 @@ defmodule Module.LocalsTracker do end defp get_line(meta), do: Keyword.get(meta, :line) + defp get_position(meta), do: {get_line(meta), meta[:column]} - defp get_position(meta) do - {get_line(meta), meta[:column]} - end - - defp build_meta(nil, _meta), do: [] - - # We need to transform any file annotation in the function - # definition into a keep annotation that is used by the - # error handling system in order to respect line/file. - defp build_meta(position, meta) do - case {position, Keyword.get(meta, :file)} do - {{line, _col}, {file, _}} -> [keep: {file, line}] - {{line, nil}, nil} -> [line: line] - {{line, col}, nil} -> [line: line, column: col] + defp build_meta(position, function_name) do + case position do + {line, nil} -> [line: line] + {line, col} -> :elixir_env.calculate_span([line: line, column: col], function_name) end end diff --git a/lib/elixir/src/elixir_locals.erl b/lib/elixir/src/elixir_locals.erl index 0e8d1fbe94..5e28302adf 100644 --- a/lib/elixir/src/elixir_locals.erl +++ b/lib/elixir/src/elixir_locals.erl @@ -99,8 +99,8 @@ ensure_no_import_conflict(Module, All, E) -> ensure_no_undefined_local(Module, All, E) -> if_tracker(Module, [], fun(Tracker) -> - [elixir_errors:module_error(Meta, E#{function := Function}, ?MODULE, {Error, Tuple, Module}) - || {Function, Meta, Tuple, Error} <- ?tracker:collect_undefined_locals(Tracker, All)], + [elixir_errors:module_error(Meta, E#{function := Function, file := File}, ?MODULE, {Error, Tuple, Module}) + || {Function, Meta, File, Tuple, Error} <- ?tracker:collect_undefined_locals(Tracker, All, ?key(E, file))], ok end). diff --git a/lib/elixir/test/elixir/kernel/diagnostics_test.exs b/lib/elixir/test/elixir/kernel/diagnostics_test.exs index 34bd9a5cd4..c2cf1a98aa 100644 --- a/lib/elixir/test/elixir/kernel/diagnostics_test.exs +++ b/lib/elixir/test/elixir/kernel/diagnostics_test.exs @@ -889,6 +889,37 @@ defmodule Kernel.DiagnosticsTest do purge(Sample) end + @tag :tmp_dir + test "shows span for unknown local function calls", %{tmp_dir: tmp_dir} do + path = make_relative_tmp(tmp_dir, "unknown_local_function_call.ex") + + source = """ + defmodule Sample do + @file "#{path}" + + def foo do + _result = unknown_func_call!(:hello!) + end + end + """ + + File.write!(path, source) + + expected = """ + error: undefined function unknown_func_call!/1 (expected Sample to define such a function or for it to be imported, but none are available) + │ + 5 │ _result = unknown_func_call!(:hello!) + │ ^^^^^^^^^^^^^^^^^^ + │ + └─ #{path}:5:15: Sample.foo/0 + + """ + + assert capture_compile(source) == expected + after + purge(Sample) + end + @tag :tmp_dir test "line + column", %{tmp_dir: tmp_dir} do path = make_relative_tmp(tmp_dir, "error_line_column.ex") diff --git a/lib/elixir/test/elixir/kernel/errors_test.exs b/lib/elixir/test/elixir/kernel/errors_test.exs index 46f8cdc411..8efde8486c 100644 --- a/lib/elixir/test/elixir/kernel/errors_test.exs +++ b/lib/elixir/test/elixir/kernel/errors_test.exs @@ -49,7 +49,7 @@ defmodule Kernel.ErrorsTest do test "undefined function" do assert_compile_error( [ - "hello.ex:4: ", + "hello.ex:4:5: ", "undefined function bar/0 (expected Kernel.ErrorsTest.BadForm to define such a function or for it to be imported, but none are available)" ], ~c""" diff --git a/lib/elixir/test/elixir/module/locals_tracker_test.exs b/lib/elixir/test/elixir/module/locals_tracker_test.exs index e92810eded..d50a237993 100644 --- a/lib/elixir/test/elixir/module/locals_tracker_test.exs +++ b/lib/elixir/test/elixir/module/locals_tracker_test.exs @@ -50,8 +50,12 @@ defmodule Module.LocalsTrackerTest do test "preserves column information on retrieval", config do D.add_local(config[:ref], {:public, 1}, {:private, 1}, [line: 1, column: 1], false) - undefined = D.collect_undefined_locals(config[:ref], @used) - assert undefined == [{{:public, 1}, [line: 1, column: 1], {:private, 1}, :undefined_function}] + undefined = D.collect_undefined_locals(config[:ref], @used, "foo.exs") + + assert undefined == [ + {{:public, 1}, [span: {1, 8}, line: 1, column: 1], "foo.exs", {:private, 1}, + :undefined_function} + ] end test "private definitions with unused default arguments", config do @@ -80,8 +84,8 @@ defmodule Module.LocalsTrackerTest do test "undefined functions are marked as so", config do D.add_local(config[:ref], {:public, 1}, {:private, 1}, [line: 1], false) - undefined = D.collect_undefined_locals(config[:ref], @used) - assert undefined == [{{:public, 1}, [line: 1], {:private, 1}, :undefined_function}] + undefined = D.collect_undefined_locals(config[:ref], @used, "foo.exs") + assert undefined == [{{:public, 1}, [line: 1], "foo.exs", {:private, 1}, :undefined_function}] end ### Incorrect dispatches @@ -93,8 +97,8 @@ defmodule Module.LocalsTrackerTest do D.add_local(config[:ref], {:public, 1}, {:macro, 1}, [line: 5], false) - undefined = D.collect_undefined_locals(config[:ref], definitions) - assert undefined == [{{:public, 1}, [line: 5], {:macro, 1}, :incorrect_dispatch}] + undefined = D.collect_undefined_locals(config[:ref], definitions, "foo.exs") + assert undefined == [{{:public, 1}, [line: 5], "foo.exs", {:macro, 1}, :incorrect_dispatch}] end ## Defaults From a0b42e8cc18fba4ff52cfafdbe5db3408ec227c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aziz=20K=C3=B6ksal?= Date: Sun, 8 Oct 2023 10:45:39 +0200 Subject: [PATCH 0036/1886] Allow mix test multi locations (#12959) --- lib/ex_unit/lib/ex_unit/filters.ex | 101 +++++++++++++-------- lib/ex_unit/test/ex_unit/filters_test.exs | 104 ++++++++++++++-------- lib/mix/lib/mix/tasks/test.ex | 30 ++----- lib/mix/test/mix/tasks/test_test.exs | 26 ++++-- 4 files changed, 157 insertions(+), 104 deletions(-) diff --git a/lib/ex_unit/lib/ex_unit/filters.ex b/lib/ex_unit/lib/ex_unit/filters.ex index c363a801f0..75be72f80c 100644 --- a/lib/ex_unit/lib/ex_unit/filters.ex +++ b/lib/ex_unit/lib/ex_unit/filters.ex @@ -6,6 +6,8 @@ defmodule ExUnit.Filters do """ @type t :: list({atom, Regex.t() | String.Chars.t()} | atom) + @type location :: {:location, {String.t(), pos_integer | [pos_integer, ...]}} + @type ex_unit_opts :: [exclude: [:test], include: [location, ...]] | [] @doc """ Parses filters out of a path. @@ -14,51 +16,64 @@ defmodule ExUnit.Filters do on the command line) includes a line number filter, and if so returns the appropriate ExUnit configuration options. """ - @spec parse_path(String.t()) :: {String.t(), Keyword.t()} - def parse_path(file) do - case extract_line_numbers(file) do - {path, []} -> {path, []} - {path, line_numbers} -> {path, exclude: [:test], include: line_numbers} - end + @spec parse_path(String.t()) :: {String.t(), ex_unit_opts} + # TODO: Deprecate this on Elixir v1.20 + def parse_path(file_path) do + {[parsed_path], ex_unit_opts} = parse_paths([file_path]) + {parsed_path, ex_unit_opts} end - defp extract_line_numbers(file) do - case String.split(file, ":") do - [part] -> - {part, []} - - parts -> - {reversed_line_numbers, reversed_path_parts} = - parts - |> Enum.reverse() - |> Enum.split_while(&match?({_, ""}, Integer.parse(&1))) - - line_numbers = - for line_number <- reversed_line_numbers, - valid_line_number?(line_number), - reduce: [], - do: (acc -> [line: line_number] ++ acc) - - path = - reversed_path_parts - |> Enum.reverse() - |> Enum.join(":") - - {path, line_numbers} - end + @doc """ + Like `parse_path/1` but for multiple paths. + + ExUnit filter options are combined. + """ + @spec parse_paths([String.t()]) :: {[String.t()], ex_unit_opts} + def parse_paths(file_paths) do + {parsed_paths, locations} = + Enum.map_reduce(file_paths, [], fn file_path, locations -> + case extract_line_numbers(file_path) do + {path, []} -> {path, locations} + {path, [line]} -> {path, [{:location, {path, line}} | locations]} + {path, lines} -> {path, [{:location, {path, lines}} | locations]} + end + end) + + ex_unit_opts = + if locations == [], do: [], else: [exclude: [:test], include: Enum.reverse(locations)] + + {parsed_paths, ex_unit_opts} end - defp valid_line_number?(arg) do - case Integer.parse(arg) do - {num, ""} when num > 0 -> - true + @spec extract_path(String.t()) :: String.t() + def extract_path(file_path), do: elem(extract_line_numbers(file_path), 0) - _ -> - IO.warn("invalid line number given as ExUnit filter: #{arg}", []) - false + defp extract_line_numbers(file_path) do + case String.split(file_path, ":") do + [path] -> + {path, []} + + [path | parts] -> + {path_parts, line_numbers} = Enum.split_while(parts, &(to_line_number(&1) == nil)) + path = Enum.join([path | path_parts], ":") + lines = for n <- line_numbers, valid_number = validate_line_number(n), do: valid_number + {path, lines} end end + defp to_line_number(str) do + case Integer.parse(str) do + {x, ""} when x > 0 -> x + _ -> nil + end + end + + defp validate_line_number(str) do + number = to_line_number(str) + number == nil && IO.warn("invalid line number given as ExUnit filter: #{str}", []) + number + end + @doc """ Normalizes `include` and `exclude` filters to remove duplicates and keep precedence. @@ -199,11 +214,19 @@ defmodule ExUnit.Filters do end end - defp has_tag({:line, line}, %{line: _, describe_line: describe_line} = tags, collection) do + defp has_tag({:location, {path, lines}}, %{line: _, describe_line: _} = tags, collection) do + if path && not String.ends_with?(tags.file, path) do + false + else + List.wrap(lines) |> Enum.any?(&has_tag({:line, &1}, tags, collection)) + end + end + + defp has_tag({:line, line}, %{line: _, describe_line: _} = tags, collection) do line = to_integer(line) cond do - describe_line == line -> + tags.describe_line == line -> true describe_block?(line, collection) -> diff --git a/lib/ex_unit/test/ex_unit/filters_test.exs b/lib/ex_unit/test/ex_unit/filters_test.exs index bbf1670a9c..f6de038b57 100644 --- a/lib/ex_unit/test/ex_unit/filters_test.exs +++ b/lib/ex_unit/test/ex_unit/filters_test.exs @@ -192,49 +192,83 @@ defmodule ExUnit.FiltersTest do end test "file paths with line numbers" do - assert ExUnit.Filters.parse_path("test/some/path.exs:123") == - {"test/some/path.exs", [exclude: [:test], include: [line: "123"]]} + unix_path = "test/some/path.exs" + windows_path = "C:\\some\\path.exs" - assert ExUnit.Filters.parse_path("test/some/path.exs") == {"test/some/path.exs", []} + for path <- [unix_path, windows_path] do + assert ExUnit.Filters.parse_path("#{path}:123") == + {path, [exclude: [:test], include: [location: {path, 123}]]} - assert ExUnit.Filters.parse_path("test/some/path.exs:123notreallyalinenumber123") == - {"test/some/path.exs:123notreallyalinenumber123", []} + assert ExUnit.Filters.parse_path(path) == {path, []} - assert ExUnit.Filters.parse_path("C:\\some\\path.exs:123") == - {"C:\\some\\path.exs", [exclude: [:test], include: [line: "123"]]} + assert ExUnit.Filters.parse_path("#{path}:123notreallyalinenumber123") == + {"#{path}:123notreallyalinenumber123", []} - assert ExUnit.Filters.parse_path("C:\\some\\path.exs") == {"C:\\some\\path.exs", []} + assert ExUnit.Filters.parse_path("#{path}:123:456") == + {path, [exclude: [:test], include: [location: {path, [123, 456]}]]} - assert ExUnit.Filters.parse_path("C:\\some\\path.exs:123notreallyalinenumber123") == - {"C:\\some\\path.exs:123notreallyalinenumber123", []} + assert ExUnit.Filters.parse_path("#{path}:123notalinenumber123:456") == + {"#{path}:123notalinenumber123", + [exclude: [:test], include: [location: {"#{path}:123notalinenumber123", 456}]]} - assert ExUnit.Filters.parse_path("test/some/path.exs:123:456") == - {"test/some/path.exs", [exclude: [:test], include: [line: "123", line: "456"]]} + output = + ExUnit.CaptureIO.capture_io(:stderr, fn -> + assert ExUnit.Filters.parse_path("#{path}:123:456notalinenumber456") == + {path, [{:exclude, [:test]}, {:include, [location: {path, 123}]}]} - assert ExUnit.Filters.parse_path("C:\\some\\path.exs:123:456") == - {"C:\\some\\path.exs", [exclude: [:test], include: [line: "123", line: "456"]]} + assert ExUnit.Filters.parse_path("#{path}:123:0:-789:456") == + {path, [exclude: [:test], include: [location: {path, [123, 456]}]]} + end) - assert ExUnit.Filters.parse_path("test/some/path.exs:123notalinenumber123:456") == - {"test/some/path.exs:123notalinenumber123", - [exclude: [:test], include: [line: "456"]]} - - assert ExUnit.Filters.parse_path("test/some/path.exs:123:456notalinenumber456") == - {"test/some/path.exs:123:456notalinenumber456", []} - - assert ExUnit.Filters.parse_path("C:\\some\\path.exs:123notalinenumber123:456") == - {"C:\\some\\path.exs:123notalinenumber123", - [exclude: [:test], include: [line: "456"]]} - - assert ExUnit.Filters.parse_path("C:\\some\\path.exs:123:456notalinenumber456") == - {"C:\\some\\path.exs:123:456notalinenumber456", []} - - output = - ExUnit.CaptureIO.capture_io(:stderr, fn -> - assert ExUnit.Filters.parse_path("test/some/path.exs:123:0:-789:456") == - {"test/some/path.exs", [exclude: [:test], include: [line: "123", line: "456"]]} - end) + assert output =~ "invalid line number given as ExUnit filter: 456notalinenumber456" + assert output =~ "invalid line number given as ExUnit filter: 0" + assert output =~ "invalid line number given as ExUnit filter: -789" + end + end - assert output =~ "invalid line number given as ExUnit filter: 0" - assert output =~ "invalid line number given as ExUnit filter: -789" + test "multiple file paths with line numbers" do + unix_path = "test/some/path.exs" + windows_path = "C:\\some\\path.exs" + other_unix_path = "test/some/other_path.exs" + other_windows_path = "C:\\some\\other_path.exs" + + for {path, other_path} <- [ + {unix_path, other_unix_path}, + {windows_path, other_windows_path} + ] do + assert ExUnit.Filters.parse_paths([path, "#{other_path}:456:789"]) == + {[path, other_path], + [ + exclude: [:test], + include: [location: {other_path, [456, 789]}] + ]} + + assert ExUnit.Filters.parse_paths(["#{path}:123", "#{other_path}:456"]) == + {[path, other_path], + [ + exclude: [:test], + include: [location: {path, 123}, location: {other_path, 456}] + ]} + + output = + ExUnit.CaptureIO.capture_io(:stderr, fn -> + assert ExUnit.Filters.parse_paths([ + "#{path}:123:0:-789:456", + "#{other_path}:321:0:-987:654" + ]) == + {[path, other_path], + [ + exclude: [:test], + include: [ + location: {path, [123, 456]}, + location: {other_path, [321, 654]} + ] + ]} + end) + + assert output =~ "invalid line number given as ExUnit filter: 0" + assert output =~ "invalid line number given as ExUnit filter: -789" + assert output =~ "invalid line number given as ExUnit filter: -987" + end end end diff --git a/lib/mix/lib/mix/tasks/test.ex b/lib/mix/lib/mix/tasks/test.ex index 11c2db84d5..09502f133b 100644 --- a/lib/mix/lib/mix/tasks/test.ex +++ b/lib/mix/lib/mix/tasks/test.ex @@ -475,10 +475,8 @@ defmodule Mix.Tasks.Test do end end - defp relative_app_file_exists?(file) do - {file, _} = ExUnit.Filters.parse_path(file) - File.exists?(Path.join("../..", file)) - end + defp relative_app_file_exists?(file), + do: File.exists?(Path.join("../..", ExUnit.Filters.extract_path(file))) defp do_run(opts, args, files) do _ = Mix.Project.get!() @@ -555,7 +553,7 @@ defmodule Mix.Tasks.Test do # Finally parse, require and load the files test_elixirc_options = project[:test_elixirc_options] || [] - test_files = parse_files(files, shell, test_paths) + test_files = if files != [], do: parse_file_paths(files), else: test_paths test_pattern = project[:test_pattern] || "*_test.exs" warn_test_pattern = project[:warn_test_pattern] || "*_test.ex" @@ -692,24 +690,10 @@ defmodule Mix.Tasks.Test do [autorun: false] ++ opts end - defp parse_files([], _shell, test_paths) do - test_paths - end - - defp parse_files([single_file], _shell, _test_paths) do - # Check if the single file path matches test/path/to_test.exs:123. If it does, - # apply "--only line:123" and trim the trailing :123 part. - {single_file, opts} = ExUnit.Filters.parse_path(single_file) - ExUnit.configure(opts) - [single_file] - end - - defp parse_files(files, shell, _test_paths) do - if Enum.any?(files, &match?({_, [_ | _]}, ExUnit.Filters.parse_path(&1))) do - raise_with_shell(shell, "Line numbers can only be used when running a single test file") - else - files - end + defp parse_file_paths(file_paths) do + {parsed_file_paths, ex_unit_opts} = ExUnit.Filters.parse_paths(file_paths) + ExUnit.configure(ex_unit_opts) + parsed_file_paths end defp parse_filters(opts, key) do diff --git a/lib/mix/test/mix/tasks/test_test.exs b/lib/mix/test/mix/tasks/test_test.exs index e737c9c71e..b3b4a68640 100644 --- a/lib/mix/test/mix/tasks/test_test.exs +++ b/lib/mix/test/mix/tasks/test_test.exs @@ -438,14 +438,14 @@ defmodule Mix.Tasks.TestTest do end) end - test "raises an exception if line numbers are given with multiple files" do + test "runs multiple test files if line numbers are given" do in_fixture("test_stale", fn -> assert_run_output( - [ - "test/a_test_stale.exs", - "test/b_test_stale.exs:4" - ], - "Line numbers can only be used when running a single test file" + ["test/a_test_stale.exs:2", "test/b_test_stale.exs:4"], + """ + Excluding tags: [:test] + Including tags: [location: {"test/a_test_stale.exs", 2}, location: {"test/b_test_stale.exs", 4}] + """ ) end) end @@ -501,13 +501,25 @@ defmodule Mix.Tasks.TestTest do assert output =~ """ ==> bar Excluding tags: [:test] - Including tags: [line: \"10\"] + Including tags: [location: {"test/bar_tests.exs", 10}] . """ refute output =~ "==> foo" refute output =~ "Paths given to \"mix test\" did not match any directory/file" + + output = mix(["test", "apps/foo/test/foo_tests.exs:9", "apps/bar/test/bar_tests.exs:5"]) + + assert output =~ """ + Excluding tags: [:test] + Including tags: [location: {"test/foo_tests.exs", 9}] + """ + + assert output =~ """ + Excluding tags: [:test] + Including tags: [location: {"test/bar_tests.exs", 5}] + """ end) end end From 4971af9fc9848b3c8e081a62f586014eda23f446 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sun, 8 Oct 2023 11:06:50 +0200 Subject: [PATCH 0037/1886] Parse filter values on ExUnit.Filters.parse/1 --- lib/ex_unit/lib/ex_unit/filters.ex | 36 +++++++++++------------ lib/ex_unit/lib/ex_unit/formatter.ex | 7 +++-- lib/ex_unit/test/ex_unit/filters_test.exs | 26 ++++++++-------- lib/mix/lib/mix/tasks/test.ex | 6 ++-- 4 files changed, 40 insertions(+), 35 deletions(-) diff --git a/lib/ex_unit/lib/ex_unit/filters.ex b/lib/ex_unit/lib/ex_unit/filters.ex index 75be72f80c..471a33a909 100644 --- a/lib/ex_unit/lib/ex_unit/filters.ex +++ b/lib/ex_unit/lib/ex_unit/filters.ex @@ -34,7 +34,6 @@ defmodule ExUnit.Filters do Enum.map_reduce(file_paths, [], fn file_path, locations -> case extract_line_numbers(file_path) do {path, []} -> {path, locations} - {path, [line]} -> {path, [{:location, {path, line}} | locations]} {path, lines} -> {path, [{:location, {path, lines}} | locations]} end end) @@ -45,9 +44,6 @@ defmodule ExUnit.Filters do {parsed_paths, ex_unit_opts} end - @spec extract_path(String.t()) :: String.t() - def extract_path(file_path), do: elem(extract_line_numbers(file_path), 0) - defp extract_line_numbers(file_path) do case String.split(file_path, ":") do [path] -> @@ -57,7 +53,11 @@ defmodule ExUnit.Filters do {path_parts, line_numbers} = Enum.split_while(parts, &(to_line_number(&1) == nil)) path = Enum.join([path | path_parts], ":") lines = for n <- line_numbers, valid_number = validate_line_number(n), do: valid_number - {path, lines} + + case lines do + [line] -> {path, line} + lines -> {path, lines} + end end end @@ -129,19 +129,24 @@ defmodule ExUnit.Filters do ## Examples iex> ExUnit.Filters.parse(["foo:bar", "baz", "line:9", "bool:true"]) - [{:foo, "bar"}, :baz, {:line, "9"}, {:bool, "true"}] + [{:foo, "bar"}, :baz, {:line, 9}, {:bool, "true"}] """ @spec parse([String.t()]) :: t def parse(filters) do Enum.map(filters, fn filter -> - case String.split(filter, ":", parts: 2) do - [key, value] -> {String.to_atom(key), value} + case :binary.split(filter, ":") do + [key, value] -> parse_kv(String.to_atom(key), value) [key] -> String.to_atom(key) end end) end + defp parse_kv(:line, line) when is_integer(line), do: {:line, line} + defp parse_kv(:line, line) when is_binary(line), do: {:line, String.to_integer(line)} + defp parse_kv(:location, loc) when is_binary(loc), do: {:location, extract_line_numbers(loc)} + defp parse_kv(key, value), do: {key, value} + @doc """ Returns a tuple containing useful information about test failures from the manifest. The tuple contains: @@ -215,16 +220,12 @@ defmodule ExUnit.Filters do end defp has_tag({:location, {path, lines}}, %{line: _, describe_line: _} = tags, collection) do - if path && not String.ends_with?(tags.file, path) do - false - else - List.wrap(lines) |> Enum.any?(&has_tag({:line, &1}, tags, collection)) - end + String.ends_with?(tags.file, path) and + lines |> List.wrap() |> Enum.any?(&has_tag({:line, &1}, tags, collection)) end - defp has_tag({:line, line}, %{line: _, describe_line: _} = tags, collection) do - line = to_integer(line) - + defp has_tag({:line, line}, %{line: _, describe_line: _} = tags, collection) + when is_integer(line) do cond do tags.describe_line == line -> true @@ -258,9 +259,6 @@ defmodule ExUnit.Filters do defp has_tag(key, tags) when is_atom(key), do: Map.has_key?(tags, key) and key - defp to_integer(integer) when is_integer(integer), do: integer - defp to_integer(integer) when is_binary(integer), do: String.to_integer(integer) - defp compare("Elixir." <> tag1, tag2), do: compare(tag1, tag2) defp compare(tag1, "Elixir." <> tag2), do: compare(tag1, tag2) defp compare(tag, tag), do: true diff --git a/lib/ex_unit/lib/ex_unit/formatter.ex b/lib/ex_unit/lib/ex_unit/formatter.ex index d03680c525..ed639cea4b 100644 --- a/lib/ex_unit/lib/ex_unit/formatter.ex +++ b/lib/ex_unit/lib/ex_unit/formatter.ex @@ -140,12 +140,15 @@ defmodule ExUnit.Formatter do iex> format_filters([run: true, slow: false], :include) "Including tags: [run: true, slow: false]" + iex> format_filters([list: [61, 62, 63]], :exclude) + "Excluding tags: [list: [61, 62, 63]]" + """ @spec format_filters(keyword, atom) :: String.t() def format_filters(filters, type) do case type do - :exclude -> "Excluding tags: #{inspect(filters)}" - :include -> "Including tags: #{inspect(filters)}" + :exclude -> "Excluding tags: #{inspect(filters, charlists: :as_lists)}" + :include -> "Including tags: #{inspect(filters, charlists: :as_lists)}" end end diff --git a/lib/ex_unit/test/ex_unit/filters_test.exs b/lib/ex_unit/test/ex_unit/filters_test.exs index f6de038b57..44df58a7ba 100644 --- a/lib/ex_unit/test/ex_unit/filters_test.exs +++ b/lib/ex_unit/test/ex_unit/filters_test.exs @@ -166,21 +166,21 @@ defmodule ExUnit.FiltersTest do %ExUnit.Test{tags: %{line: 13, describe_line: 12}} ] - assert ExUnit.Filters.eval([line: "3"], [:line], %{line: 3, describe_line: 2}, tests) == :ok - assert ExUnit.Filters.eval([line: "4"], [:line], %{line: 3, describe_line: 2}, tests) == :ok - assert ExUnit.Filters.eval([line: "5"], [:line], %{line: 5, describe_line: nil}, tests) == :ok - assert ExUnit.Filters.eval([line: "6"], [:line], %{line: 5, describe_line: nil}, tests) == :ok - assert ExUnit.Filters.eval([line: "2"], [:line], %{line: 3, describe_line: 2}, tests) == :ok - assert ExUnit.Filters.eval([line: "7"], [:line], %{line: 8, describe_line: 7}, tests) == :ok - assert ExUnit.Filters.eval([line: "7"], [:line], %{line: 10, describe_line: 7}, tests) == :ok - - assert ExUnit.Filters.eval([line: "1"], [:line], %{line: 3, describe_line: 2}, tests) == + assert ExUnit.Filters.eval([line: 3], [:line], %{line: 3, describe_line: 2}, tests) == :ok + assert ExUnit.Filters.eval([line: 4], [:line], %{line: 3, describe_line: 2}, tests) == :ok + assert ExUnit.Filters.eval([line: 5], [:line], %{line: 5, describe_line: nil}, tests) == :ok + assert ExUnit.Filters.eval([line: 6], [:line], %{line: 5, describe_line: nil}, tests) == :ok + assert ExUnit.Filters.eval([line: 2], [:line], %{line: 3, describe_line: 2}, tests) == :ok + assert ExUnit.Filters.eval([line: 7], [:line], %{line: 8, describe_line: 7}, tests) == :ok + assert ExUnit.Filters.eval([line: 7], [:line], %{line: 10, describe_line: 7}, tests) == :ok + + assert ExUnit.Filters.eval([line: 1], [:line], %{line: 3, describe_line: 2}, tests) == {:excluded, "due to line filter"} - assert ExUnit.Filters.eval([line: "7"], [:line], %{line: 3, describe_line: 2}, tests) == + assert ExUnit.Filters.eval([line: 7], [:line], %{line: 3, describe_line: 2}, tests) == {:excluded, "due to line filter"} - assert ExUnit.Filters.eval([line: "7"], [:line], %{line: 5, describe_line: nil}, tests) == + assert ExUnit.Filters.eval([line: 7], [:line], %{line: 5, describe_line: nil}, tests) == {:excluded, "due to line filter"} end @@ -188,7 +188,9 @@ defmodule ExUnit.FiltersTest do assert ExUnit.Filters.parse(["run"]) == [:run] assert ExUnit.Filters.parse(["run:true"]) == [run: "true"] assert ExUnit.Filters.parse(["run:test"]) == [run: "test"] - assert ExUnit.Filters.parse(["line:9"]) == [line: "9"] + assert ExUnit.Filters.parse(["line:9"]) == [line: 9] + assert ExUnit.Filters.parse(["location:foo.exs:9"]) == [location: {"foo.exs", 9}] + assert ExUnit.Filters.parse(["location:foo.exs:9:11"]) == [location: {"foo.exs", [9, 11]}] end test "file paths with line numbers" do diff --git a/lib/mix/lib/mix/tasks/test.ex b/lib/mix/lib/mix/tasks/test.ex index 09502f133b..0e5fbb3f0b 100644 --- a/lib/mix/lib/mix/tasks/test.ex +++ b/lib/mix/lib/mix/tasks/test.ex @@ -475,8 +475,10 @@ defmodule Mix.Tasks.Test do end end - defp relative_app_file_exists?(file), - do: File.exists?(Path.join("../..", ExUnit.Filters.extract_path(file))) + defp relative_app_file_exists?(file) do + {[file], _} = ExUnit.Filters.parse_paths([file]) + File.exists?(Path.join("../..", file)) + end defp do_run(opts, args, files) do _ = Mix.Project.get!() From a7adda21fd957260babc2ae32a724adab50af1e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sun, 8 Oct 2023 13:40:37 +0200 Subject: [PATCH 0038/1886] Remove always true otp_release checks --- lib/elixir/lib/kernel.ex | 18 ++++-------------- lib/elixir/src/elixir_bitstring.erl | 7 +------ 2 files changed, 5 insertions(+), 20 deletions(-) diff --git a/lib/elixir/lib/kernel.ex b/lib/elixir/lib/kernel.ex index 954932307e..2c9ab81c7c 100644 --- a/lib/elixir/lib/kernel.ex +++ b/lib/elixir/lib/kernel.ex @@ -2122,20 +2122,10 @@ defmodule Kernel do end erlang_error = - case :erlang.system_info(:otp_release) >= [?2, ?4] do - true -> - fn x -> - quote do - :erlang.error(unquote(x), :none, error_info: %{module: Exception}) - end - end - - false -> - fn x -> - quote do - :erlang.error(unquote(x)) - end - end + fn x -> + quote do + :erlang.error(unquote(x), :none, error_info: %{module: Exception}) + end end case message do diff --git a/lib/elixir/src/elixir_bitstring.erl b/lib/elixir/src/elixir_bitstring.erl index 083bf9717e..96d4cd92c4 100644 --- a/lib/elixir/src/elixir_bitstring.erl +++ b/lib/elixir/src/elixir_bitstring.erl @@ -385,12 +385,7 @@ format_error(bittype_signed) -> format_error(bittype_unit) -> "integer and float types require a size specifier if the unit specifier is given"; format_error({bittype_float_size, Other}) -> - Message = - case erlang:system_info(otp_release) >= "24" of - true -> "16, 32, or 64"; - false -> "32 or 64" - end, - io_lib:format("float requires size*unit to be ~s (default), got: ~p", [Message, Other]); + io_lib:format("float requires size*unit to be 16, 32, or 64 (default), got: ~p", [Other]); format_error({invalid_literal, Literal}) -> io_lib:format("invalid literal ~ts in <<>>", ['Elixir.Macro':to_string(Literal)]); format_error({undefined_bittype, Expr}) -> From 320477cf5e254da5b3a29900bc883339334e39e8 Mon Sep 17 00:00:00 2001 From: Jean Klingler Date: Sun, 8 Oct 2023 21:59:46 +0900 Subject: [PATCH 0039/1886] Add local_handler to erl_eval (#12998) --- lib/elixir/lib/exception.ex | 30 ++++++++++++++++++---------- lib/elixir/src/elixir.erl | 21 ++++++++++++++++--- lib/elixir/test/elixir/code_test.exs | 15 ++++++++++++++ 3 files changed, 52 insertions(+), 14 deletions(-) diff --git a/lib/elixir/lib/exception.ex b/lib/elixir/lib/exception.ex index 64f0105c27..68bb06bc6a 100644 --- a/lib/elixir/lib/exception.ex +++ b/lib/elixir/lib/exception.ex @@ -1541,7 +1541,7 @@ defmodule UndefinedFunctionError do @impl true def message(%{message: nil} = exception) do %{reason: reason, module: module, function: function, arity: arity} = exception - {message, _loaded?} = message(reason, module, function, arity) + {message, _hint_type} = message(reason, module, function, arity) message end @@ -1552,11 +1552,11 @@ defmodule UndefinedFunctionError do defp message(nil, module, function, arity) do cond do is_nil(function) or is_nil(arity) -> - {"undefined function", false} + {"undefined function", :suggest_module} is_nil(module) -> formatted_fun = Exception.format_mfa(module, function, arity) - {"function #{formatted_fun} is undefined", false} + {"function #{formatted_fun} is undefined", :suggest_module} function_exported?(module, :module_info, 0) -> message(:"function not exported", module, function, arity) @@ -1568,39 +1568,47 @@ defmodule UndefinedFunctionError do defp message(:"module could not be loaded", module, function, arity) do formatted_fun = Exception.format_mfa(module, function, arity) - {"function #{formatted_fun} is undefined (module #{inspect(module)} is not available)", false} + + {"function #{formatted_fun} is undefined (module #{inspect(module)} is not available)", + :suggest_module} end defp message(:"function not exported", module, function, arity) do formatted_fun = Exception.format_mfa(module, function, arity) - {"function #{formatted_fun} is undefined or private", true} + {"function #{formatted_fun} is undefined or private", :suggest_function} + end + + defp message(:"undefined local", nil, function, arity) do + {"function #{function}/#{arity} is undefined (there is no such import)", :no_hint} end defp message(reason, module, function, arity) do formatted_fun = Exception.format_mfa(module, function, arity) - {"function #{formatted_fun} is undefined (#{reason})", false} + {"function #{formatted_fun} is undefined (#{reason})", :suggest_module} end @impl true def blame(exception, stacktrace) do %{reason: reason, module: module, function: function, arity: arity} = exception - {message, loaded?} = message(reason, module, function, arity) - message = message <> hint(module, function, arity, loaded?) + {message, hint_type} = message(reason, module, function, arity) + message = message <> hint(module, function, arity, hint_type) {%{exception | message: message}, stacktrace} end - defp hint(nil, _function, 0, _loaded?) do + defp hint(_, _, _, :no_hint), do: "" + + defp hint(nil, _function, 0, _hint_type) do ". If you are using the dot syntax, such as module.function(), " <> "make sure the left-hand side of the dot is a module atom" end - defp hint(module, function, arity, true) do + defp hint(module, function, arity, :suggest_function) do behaviour_hint(module, function, arity) <> hint_for_loaded_module(module, function, arity, nil) end @max_suggestions 5 - defp hint(module, function, arity, _loaded?) do + defp hint(module, function, arity, :suggest_module) do downcased_module = downcase_module_name(module) stripped_module = module |> Atom.to_string() |> String.replace_leading("Elixir.", "") diff --git a/lib/elixir/src/elixir.erl b/lib/elixir/src/elixir.erl index 575756fa65..ac763a6026 100644 --- a/lib/elixir/src/elixir.erl +++ b/lib/elixir/src/elixir.erl @@ -359,7 +359,8 @@ eval_forms(Tree, Binding, OrigE, Opts) -> end, ExternalHandler = eval_external_handler(NewE), - {value, Value, NewBinding} = erl_eval:exprs(Exprs, ErlBinding, none, ExternalHandler), + LocalHandler = eval_local_handler(NewE), + {value, Value, NewBinding} = erl_eval:exprs(Exprs, ErlBinding, LocalHandler, ExternalHandler), PruneBefore = if Prune -> length(Binding); true -> -1 end, {DumpedBinding, DumpedVars} = @@ -370,8 +371,20 @@ eval_forms(Tree, Binding, OrigE, Opts) -> %% TODO: Remove conditional once we require Erlang/OTP 25+. -if(?OTP_RELEASE >= 25). +eval_local_handler(#{function := Function}) when Function /= nil -> + Handler = fun(FunName, Args) -> + {current_stacktrace, Stack} = + erlang:process_info(self(), current_stacktrace), + Opts = [{module, nil}, {function, FunName}, {arity, length(Args)}, + {reason, 'undefined local'}], + Exception = 'Elixir.UndefinedFunctionError':exception(Opts), + erlang:raise(error, Exception, Stack) + end, + {value, Handler}; +eval_local_handler(_Env) -> + none. eval_external_handler(Env) -> - Fun = fun(Ann, FunOrModFun, Args) -> + Handler = fun(Ann, FunOrModFun, Args) -> try case FunOrModFun of {Mod, Fun} -> apply(Mod, Fun, Args); @@ -415,7 +428,7 @@ eval_external_handler(Env) -> erlang:raise(Kind, Reason, Custom) end end, - {value, Fun}. + {value, Handler}. %% We need to check if we have dropped any frames. %% If we have not dropped frames, then we need to drop one @@ -428,6 +441,8 @@ drop_common([], [{?MODULE, _, _, _} | T2], _ToDrop) -> T2; drop_common([], [_ | T2], true) -> T2; drop_common([], T2, _) -> T2. -else. +eval_local_handler(_Env) -> + none. eval_external_handler(_Env) -> none. -endif. diff --git a/lib/elixir/test/elixir/code_test.exs b/lib/elixir/test/elixir/code_test.exs index 0b2fc119d6..b9be631cb7 100644 --- a/lib/elixir/test/elixir/code_test.exs +++ b/lib/elixir/test/elixir/code_test.exs @@ -338,6 +338,21 @@ defmodule CodeTest do {[{{:x, :foo}, 2}], [x: :foo]} end + if :erlang.system_info(:otp_release) >= ~c"25" do + test "undefined function" do + env = Code.env_for_eval(__ENV__) + quoted = quote do: foo() + + assert_exception( + UndefinedFunctionError, + ["** (UndefinedFunctionError) function foo/0 is undefined (there is no such import)"], + fn -> + Code.eval_quoted_with_env(quoted, [], env) + end + ) + end + end + defmodule Tracer do def trace(event, env) do send(self(), {:trace, event, env}) From d732ee485198758162ca61db55d439550f49c69c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 9 Oct 2023 13:54:44 +0200 Subject: [PATCH 0040/1886] Non-assertive anti-patterns (#12997) --- .../pages/anti-patterns/code-anti-patterns.md | 72 +++++++++++++++++-- 1 file changed, 66 insertions(+), 6 deletions(-) diff --git a/lib/elixir/pages/anti-patterns/code-anti-patterns.md b/lib/elixir/pages/anti-patterns/code-anti-patterns.md index 8ffc6486b8..d87a39b9cc 100644 --- a/lib/elixir/pages/anti-patterns/code-anti-patterns.md +++ b/lib/elixir/pages/anti-patterns/code-anti-patterns.md @@ -371,11 +371,11 @@ There are few known exceptions to this anti-pattern: * If you are the maintainer for both `plug` and `plug_auth`, then you may allow `plug_auth` to define modules with the `Plug` namespace, such as `Plug.Auth`. However, you are responsible for avoiding or managing any conflicts that may arise in the future -## Non-existent map keys +## Non-assertive map access #### Problem -In Elixir, it is possible to access values from `Map`s, which are key-value data structures, either statically or dynamically. When the keys are known upfront, they must be accessed using the `map.key` notation, instead of `map[:key]`. When the latter is used, if the informed key does not exist, `nil` is returned. This return can be confusing and does not allow developers to conclude whether the key is non-existent in the map or just has no bound value. In this way, this anti-pattern may cause bugs in the code. +In Elixir, it is possible to access values from `Map`s, which are key-value data structures, either statically or dynamically. When the keys are known upfront, they must be accessed using the `map.key` notation, which asserts the key exists. If `map[:key]` is used and the informed key does not exist, `nil` is returned. This return can be confusing and does not allow developers to conclude whether the key is non-existent in the map or just has a bound `nil` value. In this way, this anti-pattern may cause bugs in the code. #### Example @@ -477,11 +477,15 @@ iex> point[:x] # <= by default, struct does not support dynamic access ** (UndefinedFunctionError) ... (Point does not implement the Access behaviour) ``` -## Speculative assumptions +#### Additional remarks + +This anti-pattern was formerly known as [Accessing non-existent map/struct fields](https://github.com/lucasvegi/Elixir-Code-Smells#accessing-non-existent-mapstruct-fields). + +## Non-assertive pattern matching #### Problem -Overall, Elixir systems are composed of many supervised processes, so the effects of an error are localized to a single process, not propagating to the entire application. A supervisor will detect the failing process, report it, and possibly restart it. This means Elixir developers do not need to program defensively, making assumptions we have not really planned for, such as being able to return incorrect values instead of forcing a crash. These speculative assumptions can give a false impression that the code is working correctly. +Overall, Elixir systems are composed of many supervised processes, so the effects of an error are localized to a single process, and don't propagate to the entire application. A supervisor detects the failing process, reports it, and possibly restarts it. This anti-pattern arises when developers write defensive or imprecise code, capable of returning incorrect values which were not planned for, instead of programming in an assertive style through pattern matching and guards. #### Example @@ -513,7 +517,7 @@ iex> Extract.get_value("name=Lucas&university=institution=UFMG&lab=ASERG", "univ #### Refactoring -To remove this anti-pattern, `get_value/2` can be refactored through the use of pattern matching. So, if an unexpected URL query string format is used, the function will crash instead of returning an invalid value. This behaviour, shown below, will allow clients to decide how to handle these errors and will not give a false impression that the code is working correctly when unexpected values are extracted: +To remove this anti-pattern, `get_value/2` can be refactored through the use of pattern matching. So, if an unexpected URL query string format is used, the function will crash instead of returning an invalid value. This behaviour, shown below, allows clients to decide how to handle these errors and doesn't give a false impression that the code is working correctly when unexpected values are extracted: ```elixir defmodule Extract do @@ -541,4 +545,60 @@ iex> Extract.get_value("name=Lucas&university&lab=ASERG", "university") extract.ex:7: anonymous fn/2 in Extract.get_value/2 # <= left hand: [key, value] pair ``` -The goal is to promote an assertive style of programming where you handle the known cases. Once an unexpected scenario arises in production, you can decide to address it accordingly, based on the needs of the code using actual examples, or conclude the scenario is indeed expected and the exception is the desired choice. +Elixir and pattern matching promote an assertive style of programming where you handle the known cases. Once an unexpected scenario arises, you can decide to address it accordingly based on practical examples, or conclude the scenario is indeed invalid and the exception is the desired choice. + +`case/2` is another important construct in Elixir that help us write assertive code, by matching on specific patterns. For example, if a function returns `{:ok, ...}` or `{:error, ...}`, prefer to explicitly match on both patterns: + +```elixir +case some_function(arg) do + {:ok, value} -> # ... + {:error, _} -> # ... +end +``` + +In particular, avoid matching solely on `_`, as shown below, as it is less clear in intent and it may hide bugs if `some_function/1` adds new return values in the future: + +```elixir +case some_function(arg) do + {:ok, value} -> # ... + _ -> # ... +end +``` + +#### Additional remarks + +This anti-pattern was formerly known as [Speculative assumptions](https://github.com/lucasvegi/Elixir-Code-Smells#speculative-assumptions). + +## Non-assertive truthiness + +#### Problem + +Elixir provides the concept of truthiness: `nil` and `false` are considered "falsy" and all other values are "truthy". Many constructs in the language, such as `&&/2`, `||/2`, and `!/1` handle truthy and falsy values. Using those operators is not an anti-pattern. However, using those operators when all operands are expected to be booleans, may be an anti-pattern. + +#### Example + +The simplest scenario where this anti-pattern manifests is in conditionals, such as: + +```elixir +if is_binary(name) && is_integer(age) do + # ... +else + # ... +end +``` + +Given both operands of `&&/2` are booleans, the code is more generic than necessary, and potentially unclear. + +#### Refactoring + +To remove this anti-pattern, we can replace `&&/2`, `||/2`, and `!/1` by `and/2`, `or/2`, and `not/1` respectively. These operators assert at least their first argument is a boolean: + +```elixir +if is_binary(name) or is_integer(age) do + # ... +else + # ... +end +``` + +This technique may be particularly important when working with Erlang code. Erlang does not have the concept of truthiness. It never returns `nil`, instead its functions may return `:error` or `:undefined` in places an Elixir developer would return `nil`. Therefore, to avoid accidentally interpreting `:undefined` or `:error` as a truthy value, you may prefer to use `and/2`, `or/2`, and `not/1` exclusively when interfacing with Erlang APIs. From a029cd22bd6f2689205b638d9f4fde19fcfcf71f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 9 Oct 2023 16:53:11 +0200 Subject: [PATCH 0041/1886] Clarify docs on tasks requirements --- lib/mix/lib/mix/task.ex | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/lib/mix/lib/mix/task.ex b/lib/mix/lib/mix/task.ex index 5fc34beb82..8b901436be 100644 --- a/lib/mix/lib/mix/task.ex +++ b/lib/mix/lib/mix/task.ex @@ -69,14 +69,22 @@ defmodule Mix.Task do @requirements ["app.config"] - Tasks typically depend on the `"app.config"` task, when they - need to access code from the current project with all apps - already configured, or the "app.start" task, when they also - need those apps to be already started: - - @requirements ["app.start"] - - You can also run tasks directly with `run/2`. + A task will typically depend on one of the following tasks: + + * "loadpaths" - this ensures dependencies are available + and compiled. If you are publishing a task as part of + a library to be used by others, and your task does not + need to interact with the user code in any way, this is + the recommended requirement + + * "app.config" - additionally compiles and loads the runtime + configuration for the current project. If you are creating + a task to be used within your application or as part of a + library, which must invoke or interact with the user code, + this is the minimum recommended requirement + + * "app.start" - additionally starts the supervision tree of + the current project and its dependencies ### `@recursive` From a2bd1f29e2ef00bf0871724b4aa7e74fcfce41a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 10 Oct 2023 23:38:20 +0200 Subject: [PATCH 0042/1886] No longer version per minor branch --- Makefile | 3 +-- RELEASE.md | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 2617938373..1dd0b18fdc 100644 --- a/Makefile +++ b/Makefile @@ -2,8 +2,7 @@ PREFIX ?= /usr/local TEST_FILES ?= "*_test.exs" SHARE_PREFIX ?= $(PREFIX)/share MAN_PREFIX ?= $(SHARE_PREFIX)/man -#CANONICAL := MAJOR.MINOR/ -CANONICAL ?= main/ +CANONICAL := main/ ELIXIRC := bin/elixirc --ignore-module-conflict $(ELIXIRC_OPTS) ERLC := erlc -I lib/elixir/include ERL_MAKE := if [ -n "$(ERLC_OPTS)" ]; then ERL_COMPILER_OPTIONS=$(ERLC_OPTS) erl -make; else erl -make; fi diff --git a/RELEASE.md b/RELEASE.md index 6b224e87c0..ad3a3384ef 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -20,7 +20,7 @@ ### In the new branch -1. Set `CANONICAL=` in /Makefile +1. Comment out `CANONICAL=` in /Makefile 2. Update tables in /SECURITY.md and "Compatibility and Deprecations" From ee7f0c22b817fd34ce7364c5137bb47f5628487d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 11 Oct 2023 10:30:38 +0200 Subject: [PATCH 0043/1886] Do not tie eval functions to Elixir version --- lib/elixir/src/elixir.erl | 148 +++++++++++++++++++++----------------- 1 file changed, 82 insertions(+), 66 deletions(-) diff --git a/lib/elixir/src/elixir.erl b/lib/elixir/src/elixir.erl index ac763a6026..b18ce9ac25 100644 --- a/lib/elixir/src/elixir.erl +++ b/lib/elixir/src/elixir.erl @@ -11,6 +11,7 @@ ]). -include("elixir.hrl"). -define(system, 'Elixir.System'). +-define(elixir_eval_env, {elixir, eval_env}). %% Top level types %% TODO: Remove char_list type on v2.0 @@ -358,9 +359,26 @@ eval_forms(Tree, Binding, OrigE, Opts) -> _ -> [Erl] end, - ExternalHandler = eval_external_handler(NewE), - LocalHandler = eval_local_handler(NewE), - {value, Value, NewBinding} = erl_eval:exprs(Exprs, ErlBinding, LocalHandler, ExternalHandler), + LocalHandler = {value, fun eval_local_handler/2}, + ExternalHandler = eval_external_handler(), + + {value, Value, NewBinding} = + try + %% ?elixir_eval_env is used by the external handler. + %% + %% The reason why we use the process dictionary to pass the environment + %% is because we want to avoid passing closures to erl_eval, as that + %% would effectively tie the eval code to the Elixir version and it is + %% best if it depends solely on Erlang/OTP. + %% + %% The downside is that functions that escape the eval context will no + %% longer have the original environment they came from. + erlang:put(?elixir_eval_env, NewE), + erl_eval:exprs(Exprs, ErlBinding, LocalHandler, ExternalHandler) + after + erlang:erase(?elixir_eval_env) + end, + PruneBefore = if Prune -> length(Binding); true -> -1 end, {DumpedBinding, DumpedVars} = @@ -371,64 +389,68 @@ eval_forms(Tree, Binding, OrigE, Opts) -> %% TODO: Remove conditional once we require Erlang/OTP 25+. -if(?OTP_RELEASE >= 25). -eval_local_handler(#{function := Function}) when Function /= nil -> - Handler = fun(FunName, Args) -> - {current_stacktrace, Stack} = - erlang:process_info(self(), current_stacktrace), - Opts = [{module, nil}, {function, FunName}, {arity, length(Args)}, - {reason, 'undefined local'}], - Exception = 'Elixir.UndefinedFunctionError':exception(Opts), - erlang:raise(error, Exception, Stack) - end, - {value, Handler}; -eval_local_handler(_Env) -> - none. -eval_external_handler(Env) -> - Handler = fun(Ann, FunOrModFun, Args) -> - try - case FunOrModFun of - {Mod, Fun} -> apply(Mod, Fun, Args); - Fun -> apply(Fun, Args) - end - catch - Kind:Reason:Stacktrace -> - %% Take everything up to the Elixir module - Pruned = - lists:takewhile(fun - ({elixir,_,_,_}) -> false; - (_) -> true - end, Stacktrace), - - Caller = - lists:dropwhile(fun - ({elixir,_,_,_}) -> false; - (_) -> true - end, Stacktrace), - - %% Now we prune any shared code path from erl_eval - {current_stacktrace, Current} = - erlang:process_info(self(), current_stacktrace), - - %% We need to make sure that we don't generate more - %% frames than supported. So we do our best to drop - %% from the Caller, but if the caller has no frames, - %% we need to drop from Pruned. - {DroppedCaller, ToDrop} = - case Caller of - [] -> {[], true}; - _ -> {lists:droplast(Caller), false} - end, - - Reversed = drop_common(lists:reverse(Current), lists:reverse(Pruned), ToDrop), - File = elixir_utils:characters_to_list(?key(Env, file)), - Location = [{file, File}, {line, erl_anno:line(Ann)}], - - %% Add file+line information at the bottom - Custom = lists:reverse([{elixir_eval, '__FILE__', 1, Location} | Reversed], DroppedCaller), - erlang:raise(Kind, Reason, Custom) + eval_external_handler() -> {value, fun eval_external_handler/3}. +-else. + eval_external_handler() -> none. +-endif. + +eval_local_handler(FunName, Args) -> + {current_stacktrace, Stack} = erlang:process_info(self(), current_stacktrace), + Opts = [{module, nil}, {function, FunName}, {arity, length(Args)}, {reason, 'undefined local'}], + Exception = 'Elixir.UndefinedFunctionError':exception(Opts), + erlang:raise(error, Exception, Stack). + +eval_external_handler(Ann, FunOrModFun, Args) -> + try + case FunOrModFun of + {Mod, Fun} -> apply(Mod, Fun, Args); + Fun -> apply(Fun, Args) end - end, - {value, Handler}. + catch + Kind:Reason:Stacktrace -> + %% Take everything up to the Elixir module + Pruned = + lists:takewhile(fun + ({elixir,_,_,_}) -> false; + (_) -> true + end, Stacktrace), + + Caller = + lists:dropwhile(fun + ({elixir,_,_,_}) -> false; + (_) -> true + end, Stacktrace), + + %% Now we prune any shared code path from erl_eval + {current_stacktrace, Current} = + erlang:process_info(self(), current_stacktrace), + + %% We need to make sure that we don't generate more + %% frames than supported. So we do our best to drop + %% from the Caller, but if the caller has no frames, + %% we need to drop from Pruned. + {DroppedCaller, ToDrop} = + case Caller of + [] -> {[], true}; + _ -> {lists:droplast(Caller), false} + end, + + Reversed = drop_common(lists:reverse(Current), lists:reverse(Pruned), ToDrop), + + %% Add file+line information at the bottom + Bottom = + case erlang:get(?elixir_eval_env) of + #{file := File} -> + [{elixir_eval, '__FILE__', 1, + [{file, elixir_utils:characters_to_list(File)}, {line, erl_anno:line(Ann)}]}]; + + _ -> + [] + end, + + Custom = lists:reverse(Bottom ++ Reversed, DroppedCaller), + erlang:raise(Kind, Reason, Custom) + end. %% We need to check if we have dropped any frames. %% If we have not dropped frames, then we need to drop one @@ -440,12 +462,6 @@ drop_common([_ | T1], T2, ToDrop) -> drop_common(T1, T2, ToDrop); drop_common([], [{?MODULE, _, _, _} | T2], _ToDrop) -> T2; drop_common([], [_ | T2], true) -> T2; drop_common([], T2, _) -> T2. --else. -eval_local_handler(_Env) -> - none. -eval_external_handler(_Env) -> - none. --endif. %% Converts a quoted expression to Erlang abstract format From 44d3faad45a32d9648792b54e46e8ac27283ffb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 11 Oct 2023 15:53:41 +0200 Subject: [PATCH 0044/1886] No longer track module as context inside after_compile --- lib/elixir/src/elixir_module.erl | 1 + lib/elixir/test/elixir/module_test.exs | 8 +++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/elixir/src/elixir_module.erl b/lib/elixir/src/elixir_module.erl index 06840d301d..ed87b3f520 100644 --- a/lib/elixir/src/elixir_module.erl +++ b/lib/elixir/src/elixir_module.erl @@ -190,6 +190,7 @@ compile(Line, Module, ModuleAsCharlist, Block, Vars, Prune, E) -> end), Autoload andalso code:load_binary(Module, beam_location(ModuleAsCharlist), Binary), + put_compiler_modules(CompilerModules), eval_callbacks(Line, DataBag, after_compile, [CallbackE, Binary], CallbackE), elixir_env:trace({on_module, Binary, none}, ModuleE), warn_unused_attributes(DataSet, DataBag, PersistedAttributes, E), diff --git a/lib/elixir/test/elixir/module_test.exs b/lib/elixir/test/elixir/module_test.exs index b08edb70f4..0938410f21 100644 --- a/lib/elixir/test/elixir/module_test.exs +++ b/lib/elixir/test/elixir/module_test.exs @@ -18,8 +18,10 @@ defmodule ModuleTest.ToBeUsed do defmacro __after_compile__(%Macro.Env{module: ModuleTest.ToUse} = env, bin) when is_binary(bin) do + # Ensure module is not longer tracked as being loaded + false = __MODULE__ in :elixir_module.compiler_modules() [] = Macro.Env.vars(env) - # IO.puts "HELLO" + :ok end defmacro callback(env) do @@ -33,7 +35,7 @@ end defmodule ModuleTest.ToUse do # Moving the next line around can make tests fail - 36 = __ENV__.line + 38 = __ENV__.line var = 1 # Not available in callbacks _ = var @@ -147,7 +149,7 @@ defmodule ModuleTest do end test "retrieves line from use callsite" do - assert ModuleTest.ToUse.line() == 41 + assert ModuleTest.ToUse.line() == 43 end ## Callbacks From 8c2471809076ee92b3977f1d886ea27b9d4dffcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20=C5=81=C4=99picki?= Date: Thu, 12 Oct 2023 21:59:53 +0200 Subject: [PATCH 0045/1886] Clean up unreachable function clause in ex_unit/filters.ex (#13003) The parse_kv function always gets called with a binary second argument, from line 139 This function clause was kept after refactoring in https://github.com/elixir-lang/elixir/commit/4971af9fc9848b3c8e081a62f586014eda23f446 --- lib/ex_unit/lib/ex_unit/filters.ex | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/ex_unit/lib/ex_unit/filters.ex b/lib/ex_unit/lib/ex_unit/filters.ex index 471a33a909..9b3f459802 100644 --- a/lib/ex_unit/lib/ex_unit/filters.ex +++ b/lib/ex_unit/lib/ex_unit/filters.ex @@ -142,7 +142,6 @@ defmodule ExUnit.Filters do end) end - defp parse_kv(:line, line) when is_integer(line), do: {:line, line} defp parse_kv(:line, line) when is_binary(line), do: {:line, String.to_integer(line)} defp parse_kv(:location, loc) when is_binary(loc), do: {:location, extract_line_numbers(loc)} defp parse_kv(key, value), do: {key, value} From 388ce16d86fc3217f7730684ba9c45fcb4e4b1c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 13 Oct 2023 11:14:38 +0200 Subject: [PATCH 0046/1886] Link to #security-wg EEF work --- lib/elixir/pages/anti-patterns/what-anti-patterns.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/elixir/pages/anti-patterns/what-anti-patterns.md b/lib/elixir/pages/anti-patterns/what-anti-patterns.md index f2f3341bf5..5a152dc49a 100644 --- a/lib/elixir/pages/anti-patterns/what-anti-patterns.md +++ b/lib/elixir/pages/anti-patterns/what-anti-patterns.md @@ -36,3 +36,5 @@ Each anti-pattern is documented using the following structure: The initial catalog of anti-patterns was proposed by Lucas Vegi and Marco Tulio Valente, from [ASERG/DCC/UFMG](http://aserg.labsoft.dcc.ufmg.br/). For more info, see [Understanding Code Smells in Elixir Functional Language](https://github.com/lucasvegi/Elixir-Code-Smells/blob/main/etc/2023-emse-code-smells-elixir.pdf) and [the associated code repository](https://github.com/lucasvegi/Elixir-Code-Smells). + +Additionally, the Security Working Group of the [Erlang Ecosystem Foundation](https://erlef.github.io/security-wg/) publishes [documents with security resources and best-practices of both Erland and Elixir, including detailed guides for web applications](https://erlef.github.io/security-wg/). From c7d2c375658f8ad862fd62ed03fb2473cab6fda4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 13 Oct 2023 11:23:34 +0200 Subject: [PATCH 0047/1886] Update sigil_r example --- lib/elixir/pages/getting-started/sigils.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/pages/getting-started/sigils.md b/lib/elixir/pages/getting-started/sigils.md index 2b4cc50f36..7897f277a4 100644 --- a/lib/elixir/pages/getting-started/sigils.md +++ b/lib/elixir/pages/getting-started/sigils.md @@ -210,7 +210,7 @@ iex> time_zone As hinted at the beginning of this chapter, sigils in Elixir are extensible. In fact, using the sigil `~r/foo/i` is equivalent to calling `sigil_r` with a binary and a char list as the argument: ```elixir -iex> sigil_r(<<"foo">>, ~c"i") +iex> sigil_r(<<"foo">>, [?i]) ~r"foo"i ``` From b09758bcfee2b1a6ec544a5e581f75d2432f0bd4 Mon Sep 17 00:00:00 2001 From: Juha Date: Fri, 13 Oct 2023 13:11:50 +0300 Subject: [PATCH 0048/1886] Document need of Tasks to trap exits for Task.Supervisor :shutdown to have effect (#13004) --- lib/elixir/lib/task/supervisor.ex | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/elixir/lib/task/supervisor.ex b/lib/elixir/lib/task/supervisor.ex index cc648a40de..7dacc2d6ff 100644 --- a/lib/elixir/lib/task/supervisor.ex +++ b/lib/elixir/lib/task/supervisor.ex @@ -162,6 +162,7 @@ defmodule Task.Supervisor do * `:shutdown` - `:brutal_kill` if the tasks must be killed directly on shutdown or an integer indicating the timeout value, defaults to 5000 milliseconds. + The tasks must trap exits for the timeout to have an effect. """ @spec async(Supervisor.supervisor(), (-> any), Keyword.t()) :: Task.t() @@ -183,6 +184,7 @@ defmodule Task.Supervisor do * `:shutdown` - `:brutal_kill` if the tasks must be killed directly on shutdown or an integer indicating the timeout value, defaults to 5000 milliseconds. + The tasks must trap exits for the timeout to have an effect. """ @spec async(Supervisor.supervisor(), module, atom, [term], Keyword.t()) :: Task.t() @@ -208,6 +210,7 @@ defmodule Task.Supervisor do * `:shutdown` - `:brutal_kill` if the tasks must be killed directly on shutdown or an integer indicating the timeout value, defaults to 5000 milliseconds. + The tasks must trap exits for the timeout to have an effect. ## Compatibility with OTP behaviours @@ -342,6 +345,7 @@ defmodule Task.Supervisor do * `:shutdown` - `:brutal_kill` if the tasks must be killed directly on shutdown or an integer indicating the timeout value. Defaults to `5000` milliseconds. + The tasks must trap exits for the timeout to have an effect. ## Examples @@ -460,6 +464,7 @@ defmodule Task.Supervisor do * `:shutdown` - `:brutal_kill` if the task must be killed directly on shutdown or an integer indicating the timeout value, defaults to 5000 milliseconds. + The task must trap exits for the timeout to have an effect. """ @spec start_child(Supervisor.supervisor(), (-> any), keyword) :: From 3e47910f392b042b128c451846213e65d04e9046 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 13 Oct 2023 22:48:02 +0200 Subject: [PATCH 0049/1886] Address compilation warnings on Erlang/OTP 24 --- lib/elixir/src/elixir.erl | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/elixir/src/elixir.erl b/lib/elixir/src/elixir.erl index b18ce9ac25..26e54fa2af 100644 --- a/lib/elixir/src/elixir.erl +++ b/lib/elixir/src/elixir.erl @@ -387,19 +387,16 @@ eval_forms(Tree, Binding, OrigE, Opts) -> {Value, DumpedBinding, NewE#{versioned_vars := DumpedVars}} end. -%% TODO: Remove conditional once we require Erlang/OTP 25+. --if(?OTP_RELEASE >= 25). - eval_external_handler() -> {value, fun eval_external_handler/3}. --else. - eval_external_handler() -> none. --endif. - eval_local_handler(FunName, Args) -> {current_stacktrace, Stack} = erlang:process_info(self(), current_stacktrace), Opts = [{module, nil}, {function, FunName}, {arity, length(Args)}, {reason, 'undefined local'}], Exception = 'Elixir.UndefinedFunctionError':exception(Opts), erlang:raise(error, Exception, Stack). +%% TODO: Remove conditional once we require Erlang/OTP 25+. +-if(?OTP_RELEASE >= 25). +eval_external_handler() -> {value, fun eval_external_handler/3}. + eval_external_handler(Ann, FunOrModFun, Args) -> try case FunOrModFun of @@ -462,6 +459,9 @@ drop_common([_ | T1], T2, ToDrop) -> drop_common(T1, T2, ToDrop); drop_common([], [{?MODULE, _, _, _} | T2], _ToDrop) -> T2; drop_common([], [_ | T2], true) -> T2; drop_common([], T2, _) -> T2. +-else. +eval_external_handler() -> none. +-endif. %% Converts a quoted expression to Erlang abstract format From 218d2e1a09bebeaf60b126a1d8b4b1fc44c5fb58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sat, 14 Oct 2023 11:29:13 +0200 Subject: [PATCH 0050/1886] Use remote version of erl_eval callbacks This allows us to execute erl eval instructions across nodes with different Elixir versions. --- lib/elixir/src/elixir.erl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/elixir/src/elixir.erl b/lib/elixir/src/elixir.erl index 26e54fa2af..3ae30733c9 100644 --- a/lib/elixir/src/elixir.erl +++ b/lib/elixir/src/elixir.erl @@ -7,7 +7,7 @@ -export([ string_to_tokens/5, tokens_to_quoted/3, 'string_to_quoted!'/5, env_for_eval/1, quoted_to_erl/2, eval_forms/3, eval_quoted/3, - eval_quoted/4 + eval_quoted/4, eval_local_handler/2, eval_external_handler/3 ]). -include("elixir.hrl"). -define(system, 'Elixir.System'). @@ -359,7 +359,7 @@ eval_forms(Tree, Binding, OrigE, Opts) -> _ -> [Erl] end, - LocalHandler = {value, fun eval_local_handler/2}, + LocalHandler = {value, fun ?MODULE:eval_local_handler/2}, ExternalHandler = eval_external_handler(), {value, Value, NewBinding} = @@ -395,7 +395,7 @@ eval_local_handler(FunName, Args) -> %% TODO: Remove conditional once we require Erlang/OTP 25+. -if(?OTP_RELEASE >= 25). -eval_external_handler() -> {value, fun eval_external_handler/3}. +eval_external_handler() -> {value, fun ?MODULE:eval_external_handler/3}. eval_external_handler(Ann, FunOrModFun, Args) -> try From 03605cc3ed3124d175f15200f7ae3c0a5f8dbc35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sat, 14 Oct 2023 11:36:22 +0200 Subject: [PATCH 0051/1886] Address compilation on Erlang/OTP 24 --- lib/elixir/src/elixir.erl | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/elixir/src/elixir.erl b/lib/elixir/src/elixir.erl index 3ae30733c9..6f2abbdd02 100644 --- a/lib/elixir/src/elixir.erl +++ b/lib/elixir/src/elixir.erl @@ -359,8 +359,9 @@ eval_forms(Tree, Binding, OrigE, Opts) -> _ -> [Erl] end, + %% We use remote names so eval works across Elixir versions. LocalHandler = {value, fun ?MODULE:eval_local_handler/2}, - ExternalHandler = eval_external_handler(), + ExternalHandler = {value, fun ?MODULE:eval_external_handler/3}, {value, Value, NewBinding} = try @@ -395,8 +396,6 @@ eval_local_handler(FunName, Args) -> %% TODO: Remove conditional once we require Erlang/OTP 25+. -if(?OTP_RELEASE >= 25). -eval_external_handler() -> {value, fun ?MODULE:eval_external_handler/3}. - eval_external_handler(Ann, FunOrModFun, Args) -> try case FunOrModFun of @@ -460,7 +459,11 @@ drop_common([], [{?MODULE, _, _, _} | T2], _ToDrop) -> T2; drop_common([], [_ | T2], true) -> T2; drop_common([], T2, _) -> T2. -else. -eval_external_handler() -> none. +eval_external_handler(_Ann, FunOrModFun, Args) -> + case FunOrModFun of + {Mod, Fun} -> apply(Mod, Fun, Args); + Fun -> apply(Fun, Args) + end. -endif. %% Converts a quoted expression to Erlang abstract format From bd5f02ad8d820a95da77cdd2b2552aa46e1b5502 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sat, 14 Oct 2023 11:39:02 +0200 Subject: [PATCH 0052/1886] Address compilation on Erlang/OTP 24 --- lib/elixir/src/elixir.erl | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/elixir/src/elixir.erl b/lib/elixir/src/elixir.erl index 6f2abbdd02..a7a1e7451e 100644 --- a/lib/elixir/src/elixir.erl +++ b/lib/elixir/src/elixir.erl @@ -361,7 +361,7 @@ eval_forms(Tree, Binding, OrigE, Opts) -> %% We use remote names so eval works across Elixir versions. LocalHandler = {value, fun ?MODULE:eval_local_handler/2}, - ExternalHandler = {value, fun ?MODULE:eval_external_handler/3}, + ExternalHandler = eval_external_handler(), {value, Value, NewBinding} = try @@ -396,6 +396,9 @@ eval_local_handler(FunName, Args) -> %% TODO: Remove conditional once we require Erlang/OTP 25+. -if(?OTP_RELEASE >= 25). +eval_external_handler() -> + {value, fun ?MODULE:eval_external_handler/3}. + eval_external_handler(Ann, FunOrModFun, Args) -> try case FunOrModFun of @@ -459,11 +462,8 @@ drop_common([], [{?MODULE, _, _, _} | T2], _ToDrop) -> T2; drop_common([], [_ | T2], true) -> T2; drop_common([], T2, _) -> T2. -else. -eval_external_handler(_Ann, FunOrModFun, Args) -> - case FunOrModFun of - {Mod, Fun} -> apply(Mod, Fun, Args); - Fun -> apply(Fun, Args) - end. +eval_external_handler() -> none. +eval_external_handler(_Ann, _FunOrModFun, _Args) -> error(unused). -endif. %% Converts a quoted expression to Erlang abstract format From d7013b19c1767934eb2171bbeb05edf44b7dae1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sat, 14 Oct 2023 12:46:03 +0200 Subject: [PATCH 0053/1886] Update CHANGELOG --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 86bac9fc87..3e5484a1f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,8 @@ error: function names should start with lowercase characters or underscore, inva └─ lib/sample.ex:3 ``` +A huge thank you to Vinícius Muller for working on the new diagnostics. + ## Revamped documentation TODO: Guides, diagrams, anti-patterns, cheatsheets. @@ -73,6 +75,10 @@ TODO: Guides, diagrams, anti-patterns, cheatsheets. * [Macro] Add `Macro.compile_apply/4` * [String] Update to Unicode 15.1.0 +#### Mix + + * [mix test] Allow testing multiple file:line at once, such as `mix test test/foo_test.exs:13 test/bar_test.exs:27` + ### 2. Bug fixes #### Elixir @@ -81,6 +87,7 @@ TODO: Guides, diagrams, anti-patterns, cheatsheets. * [Kernel] Do not expand aliases recursively (the alias stored in Macro.Env is already expanded) * [Kernel] Ensure `dbg` module is a compile-time dependency * [Kernel] Warn when a private function or macro uses `unquote/1` and the function/macro itself is unused + * [Kernel.ParallelCompiler] Consider a module has been defined in `@after_compile` callbacks to avoid deadlocks * [Path] Ensure `Path.relative_to/2` returns a relative path when the given argument does not share a common prefix with `cwd` #### ExUnit @@ -91,6 +98,7 @@ TODO: Guides, diagrams, anti-patterns, cheatsheets. #### Elixir + * [File] Deprecate `File.stream!(file, options, line_or_bytes)` in favor of keeping the options as last argument, as in `File.stream!(file, line_or_bytes, options)` * [Kernel.ParallelCompiler] Deprecate `Kernel.ParallelCompiler.async/1` in favor of `Kernel.ParallelCompiler.pmap/2` * [Path] Deprecate `Path.safe_relative_to/2` in favor of `Path.safe_relative/2` From 87b5ee077db1bacbd063e257463dc272b5be352f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sun, 15 Oct 2023 11:40:45 +0200 Subject: [PATCH 0054/1886] Update CHANGELOG --- CHANGELOG.md | 12 +- .../pages/anti-patterns/what-anti-patterns.md | 3 +- .../pages/cheatsheets/enum-cheat.cheatmd | 160 +++++++++--------- 3 files changed, 90 insertions(+), 85 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e5484a1f1..c4af4b21c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ Elixir v1.15 introduced a new compiler diagnostic format and the ability to prin With Elixir v1.16, we also include code snippets in exceptions and diagnostics raised by the compiler. For example, a syntax error now includes a pointer to where the error happened: ``` -** (SyntaxError) invalid syntax found on nofile:1:17: +** (SyntaxError) invalid syntax found on lib/my_app.ex:1:17: error: syntax error before: '*' │ 1 │ [1, 2, 3, 4, 5, *] @@ -19,7 +19,7 @@ With Elixir v1.16, we also include code snippets in exceptions and diagnostics r For mismatched delimiters, it now shows both delimiters: ``` -** (MismatchedDelimiterError) mismatched delimiter found on nofile:1:18: +** (MismatchedDelimiterError) mismatched delimiter found on lib/my_app.ex:1:18: error: unexpected token: ) │ 1 │ [1, 2, 3, 4, 5, 6) @@ -55,7 +55,13 @@ A huge thank you to Vinícius Muller for working on the new diagnostics. ## Revamped documentation -TODO: Guides, diagrams, anti-patterns, cheatsheets. +Elixir's Getting Started guided has been made part of the Elixir repository and incorporated into ExDoc. This was an opportunity to revisit and unify all official guides and references. + +We have also incorporated and extended the work on [Understanding Code Smells in Elixir Functional Language](https://github.com/lucasvegi/Elixir-Code-Smells/blob/main/etc/2023-emse-code-smells-elixir.pdf), by Lucas Vegi and Marco Tulio Valente, from [ASERG/DCC/UFMG](http://aserg.labsoft.dcc.ufmg.br/), into the official document in the form of anti-patterns. The anti-patterns are divided into four categories: code-related, design-related, process-related, and meta-programming. Our goal is to give all developers with both positive and negative examples of Elixir code, with context and examples on how to improve their codebases. + +Another [ExDoc](https://github.com/elixir-lang/ex_doc) feature we have incorporated in this release is the addition of cheatsheets, starting with [a cheatsheet for the Enum module](https://hexdocs.pm/elixir/main/enum-cheat.html). If you would like to contribute future cheatsheets to Elixir itself, feel free to start a discussion with an issue. + +Finally, we have started enriching our documentation with [Mermaid.js](https://mermaid.js.org/) diagrams. You can find examples in the [GenServer](https://hexdocs.pm/elixir/main/GenServer.html) and [Supervisor](https://hexdocs.pm/elixir/main/Supervisor.html) docs. ## v1.16.0-dev diff --git a/lib/elixir/pages/anti-patterns/what-anti-patterns.md b/lib/elixir/pages/anti-patterns/what-anti-patterns.md index 5a152dc49a..41c7cef509 100644 --- a/lib/elixir/pages/anti-patterns/what-anti-patterns.md +++ b/lib/elixir/pages/anti-patterns/what-anti-patterns.md @@ -34,7 +34,6 @@ Each anti-pattern is documented using the following structure: * **Refactoring:** Ways to change your code to improve its qualities. Examples of refactored code are presented to illustrate these changes. -The initial catalog of anti-patterns was proposed by Lucas Vegi and Marco Tulio Valente, from [ASERG/DCC/UFMG](http://aserg.labsoft.dcc.ufmg.br/). For more info, see [Understanding Code Smells in Elixir Functional Language](https://github.com/lucasvegi/Elixir-Code-Smells/blob/main/etc/2023-emse-code-smells-elixir.pdf) -and [the associated code repository](https://github.com/lucasvegi/Elixir-Code-Smells). +The initial catalog of anti-patterns was proposed by Lucas Vegi and Marco Tulio Valente, from [ASERG/DCC/UFMG](http://aserg.labsoft.dcc.ufmg.br/). For more info, see [Understanding Code Smells in Elixir Functional Language](https://github.com/lucasvegi/Elixir-Code-Smells/blob/main/etc/2023-emse-code-smells-elixir.pdf) and [the associated code repository](https://github.com/lucasvegi/Elixir-Code-Smells). Additionally, the Security Working Group of the [Erlang Ecosystem Foundation](https://erlef.github.io/security-wg/) publishes [documents with security resources and best-practices of both Erland and Elixir, including detailed guides for web applications](https://erlef.github.io/security-wg/). diff --git a/lib/elixir/pages/cheatsheets/enum-cheat.cheatmd b/lib/elixir/pages/cheatsheets/enum-cheat.cheatmd index dc5cd72a35..a419c78dc9 100644 --- a/lib/elixir/pages/cheatsheets/enum-cheat.cheatmd +++ b/lib/elixir/pages/cheatsheets/enum-cheat.cheatmd @@ -15,7 +15,7 @@ Some examples use the [`string =~ part`](`=~/2`) operator, which checks the stri ## Predicates {: .col-2} -### [any?(enum, fun)](`Enum.any?/2`) +### [`any?(enum, fun)`](`Enum.any?/2`) ```elixir iex> Enum.any?(cart, & &1.fruit == "orange") @@ -31,7 +31,7 @@ iex> Enum.any?([], & &1.fruit == "orange") false ``` -### [all?(enum, fun)](`Enum.all?/2`) +### [`all?(enum, fun)`](`Enum.all?/2`) ```elixir iex> Enum.all?(cart, & &1.count > 0) @@ -47,7 +47,7 @@ iex> Enum.all?([], & &1.count > 0) true ``` -### [member?(enum, value)](`Enum.member?/2`) +### [`member?(enum, value)`](`Enum.member?/2`) ```elixir iex> Enum.member?(cart, %{fruit: "apple", count: 3}) @@ -65,7 +65,7 @@ iex> :something_else in cart false ``` -### [empty?(enum)](`Enum.empty?/1`) +### [`empty?(enum)`](`Enum.empty?/1`) ```elixir iex> Enum.empty?(cart) @@ -77,7 +77,7 @@ true ## Filtering {: .col-2} -### [filter(enum, fun)](`Enum.filter/2`) +### [`filter(enum, fun)`](`Enum.filter/2`) ```elixir iex> Enum.filter(cart, &(&1.fruit =~ "o")) @@ -89,7 +89,7 @@ iex> Enum.filter(cart, &(&1.fruit =~ "e")) ] ``` -### [reject(enum, fun)](`Enum.reject/2`) +### [`reject(enum, fun)`](`Enum.reject/2`) ```elixir iex> Enum.reject(cart, &(&1.fruit =~ "o")) @@ -99,7 +99,7 @@ iex> Enum.reject(cart, &(&1.fruit =~ "o")) ] ``` -### [Comprehension](`for/1`) +### [`Comprehension`](`for/1`) Filtering can also be done with comprehensions: @@ -125,7 +125,7 @@ iex> for %{count: 1, fruit: fruit} <- cart do ## Mapping {: .col-2} -### [map(enum, fun)](`Enum.map/2`) +### [`map(enum, fun)`](`Enum.map/2`) ```elixir iex> Enum.map(cart, & &1.fruit) @@ -140,7 +140,7 @@ iex> Enum.map(cart, fn item -> ] ``` -### [map_every(enum, nth, fun)](`Enum.map_every/3`) +### [`map_every(enum, nth, fun)`](`Enum.map_every/3`) ```elixir iex> Enum.map_every(cart, 2, fn item -> @@ -153,7 +153,7 @@ iex> Enum.map_every(cart, 2, fn item -> ] ``` -### [Comprehension](`for/1`) +### [`Comprehension`](`for/1`) Mapping can also be done with comprehensions: @@ -176,7 +176,7 @@ iex> for item <- cart, item.fruit =~ "e" do ## Side-effects {: .col-2} -### [each(enum, fun)](`Enum.each/2`) +### [`each(enum, fun)`](`Enum.each/2`) ```elixir iex> Enum.each(cart, &IO.puts(&1.fruit)) @@ -191,7 +191,7 @@ orange ## Accumulating {: .col-2} -### [reduce(enum, acc, fun)](`Enum.reduce/3`) +### [`reduce(enum, acc, fun)`](`Enum.reduce/3`) ```elixir iex> Enum.reduce(cart, 0, fn item, acc -> @@ -200,7 +200,7 @@ iex> Enum.reduce(cart, 0, fn item, acc -> 10 ``` -### [map_reduce(enum, acc, fun)](`Enum.map_reduce/3`) +### [`map_reduce(enum, acc, fun)`](`Enum.map_reduce/3`) ```elixir iex> Enum.map_reduce(cart, 0, fn item, acc -> @@ -209,7 +209,7 @@ iex> Enum.map_reduce(cart, 0, fn item, acc -> {["apple", "banana", "orange"], 10} ``` -### [scan(enum, acc, fun)](`Enum.scan/3`) +### [`scan(enum, acc, fun)`](`Enum.scan/3`) ```elixir iex> Enum.scan(cart, 0, fn item, acc -> @@ -218,7 +218,7 @@ iex> Enum.scan(cart, 0, fn item, acc -> [3, 4, 10] ``` -### [reduce_while(enum, acc, fun)](`Enum.reduce_while/3`) +### [`reduce_while(enum, acc, fun)`](`Enum.reduce_while/3`) ```elixir iex> Enum.reduce_while(cart, 0, fn item, acc -> @@ -231,7 +231,7 @@ iex> Enum.reduce_while(cart, 0, fn item, acc -> 4 ``` -### [Comprehension](`for/1`) +### [`Comprehension`](`for/1`) Reducing can also be done with comprehensions: @@ -254,7 +254,7 @@ iex> for item <- cart, item.fruit =~ "e", reduce: 0 do ## Aggregations {: .col-2} -### [count(enum)](`Enum.count/1`) +### [`count(enum)`](`Enum.count/1`) ```elixir iex> Enum.count(cart) @@ -263,14 +263,14 @@ iex> Enum.count(cart) See `Enum.count_until/2` to count until a limit. -### [frequencies(enum)](`Enum.frequencies/1`) +### [`frequencies(enum)`](`Enum.frequencies/1`) ```elixir iex> Enum.frequencies(["apple", "banana", "orange", "apple"]) %{"apple" => 2, "banana" => 1, "orange" => 1} ``` -### [frequencies_by(enum, key_fun)](`Enum.frequencies_by/2`) +### [`frequencies_by(enum, key_fun)`](`Enum.frequencies_by/2`) Frequencies of the last letter of the fruit: @@ -279,7 +279,7 @@ iex> Enum.frequencies_by(cart, &String.last(&1.fruit)) %{"a" => 1, "e" => 2} ``` -### [count(enum, fun)](`Enum.count/2`) +### [`count(enum, fun)`](`Enum.count/2`) ```elixir iex> Enum.count(cart, &(&1.fruit =~ "e")) @@ -290,14 +290,14 @@ iex> Enum.count(cart, &(&1.fruit =~ "y")) See `Enum.count_until/3` to count until a limit with a function. -### [sum(enum)](`Enum.sum/1`) +### [`sum(enum)`](`Enum.sum/1`) ```elixir iex> cart |> Enum.map(& &1.count) |> Enum.sum() 10 ``` -### [product(enum)](`Enum.product/1`) +### [`product(enum)`](`Enum.product/1`) ```elixir iex> cart |> Enum.map(& &1.count) |> Enum.product() @@ -307,7 +307,7 @@ iex> cart |> Enum.map(& &1.count) |> Enum.product() ## Sorting {: .col-2} -### [sort(enum, sorter \\\\ :asc)](`Enum.sort/2`) +### [`sort(enum, sorter \\\\ :asc)`](`Enum.sort/2`) ```elixir iex> cart |> Enum.map(& &1.fruit) |> Enum.sort() @@ -318,7 +318,7 @@ iex> cart |> Enum.map(& &1.fruit) |> Enum.sort(:desc) When sorting structs, use `Enum.sort/2` with a module as sorter. -### [sort_by(enum, mapper, sorter \\\\ :asc)](`Enum.sort_by/2`) +### [`sort_by(enum, mapper, sorter \\\\ :asc)`](`Enum.sort_by/2`) ```elixir iex> Enum.sort_by(cart, & &1.count) @@ -337,7 +337,7 @@ iex> Enum.sort_by(cart, & &1.count, :desc) When the sorted by value is a struct, use `Enum.sort_by/3` with a module as sorter. -### [min(enum)](`Enum.min/1`) +### [`min(enum)`](`Enum.min/1`) ```elixir iex> cart |> Enum.map(& &1.count) |> Enum.min() @@ -346,7 +346,7 @@ iex> cart |> Enum.map(& &1.count) |> Enum.min() When comparing structs, use `Enum.min/2` with a module as sorter. -### [min_by(enum, mapper)](`Enum.min_by/2`) +### [`min_by(enum, mapper)`](`Enum.min_by/2`) ```elixir iex> Enum.min_by(cart, & &1.count) @@ -355,7 +355,7 @@ iex> Enum.min_by(cart, & &1.count) When comparing structs, use `Enum.min_by/3` with a module as sorter. -### [max(enum)](`Enum.max/1`) +### [`max(enum)`](`Enum.max/1`) ```elixir iex> cart |> Enum.map(& &1.count) |> Enum.max() @@ -364,7 +364,7 @@ iex> cart |> Enum.map(& &1.count) |> Enum.max() When comparing structs, use `Enum.max/2` with a module as sorter. -### [max_by(enum, mapper)](`Enum.max_by/2`) +### [`max_by(enum, mapper)`](`Enum.max_by/2`) ```elixir iex> Enum.max_by(cart, & &1.count) @@ -376,21 +376,21 @@ When comparing structs, use `Enum.max_by/3` with a module as sorter. ## Concatenating & flattening {: .col-2} -### [concat(enums)](`Enum.concat/1`) +### [`concat(enums)`](`Enum.concat/1`) ```elixir iex> Enum.concat([[1, 2, 3], [4, 5, 6], [7, 8, 9]]) [1, 2, 3, 4, 5, 6, 7, 8, 9] ``` -### [concat(left, right)](`Enum.concat/2`) +### [`concat(left, right)`](`Enum.concat/2`) ```elixir iex> Enum.concat([1, 2, 3], [4, 5, 6]) [1, 2, 3, 4, 5, 6] ``` -### [flat_map(enum, fun)](`Enum.flat_map/2`) +### [`flat_map(enum, fun)`](`Enum.flat_map/2`) ```elixir iex> Enum.flat_map(cart, fn item -> @@ -400,7 +400,7 @@ iex> Enum.flat_map(cart, fn item -> "orange", "orange", "orange", "orange", "orange"] ``` -### [flat_map_reduce(enum, acc, fun)](`Enum.flat_map_reduce/3`) +### [`flat_map_reduce(enum, acc, fun)`](`Enum.flat_map_reduce/3`) ```elixir iex> Enum.flat_map_reduce(cart, 0, fn item, acc -> @@ -412,7 +412,7 @@ iex> Enum.flat_map_reduce(cart, 0, fn item, acc -> "orange", "orange", "orange", "orange", "orange"], 10} ``` -### [Comprehension](`for/1`) +### [`Comprehension`](`for/1`) Flattening can also be done with comprehensions: @@ -428,7 +428,7 @@ iex> for item <- cart, ## Conversion {: .col-2} -### [into(enum, collectable)](`Enum.into/2`) +### [`into(enum, collectable)`](`Enum.into/2`) ```elixir iex> pairs = [{"apple", 3}, {"banana", 1}, {"orange", 6}] @@ -436,7 +436,7 @@ iex> Enum.into(pairs, %{}) %{"apple" => 3, "banana" => 1, "orange" => 6} ``` -### [into(enum, collectable, transform)](`Enum.into/3`) +### [`into(enum, collectable, transform)`](`Enum.into/3`) ```elixir iex> Enum.into(cart, %{}, fn item -> @@ -445,14 +445,14 @@ iex> Enum.into(cart, %{}, fn item -> %{"apple" => 3, "banana" => 1, "orange" => 6} ``` -### [to_list(enum)](`Enum.to_list/1`) +### [`to_list(enum)`](`Enum.to_list/1`) ```elixir iex> Enum.to_list(1..5) [1, 2, 3, 4, 5] ``` -### [Comprehension](`for/1`) +### [`Comprehension`](`for/1`) Conversion can also be done with comprehensions: @@ -466,7 +466,7 @@ iex> for item <- cart, into: %{} do ## Duplicates & uniques {: .col-2} -### [dedup(enum)](`Enum.dedup/1`) +### [`dedup(enum)`](`Enum.dedup/1`) `dedup` only removes contiguous duplicates: @@ -475,7 +475,7 @@ iex> Enum.dedup([1, 2, 2, 3, 3, 3, 1, 2, 3]) [1, 2, 3, 1, 2, 3] ``` -### [dedup_by(enum, fun)](`Enum.dedup_by/2`) +### [`dedup_by(enum, fun)`](`Enum.dedup_by/2`) Remove contiguous entries given a property: @@ -489,7 +489,7 @@ iex> Enum.dedup_by(cart, & &1.count < 5) ] ``` -### [uniq(enum)](`Enum.uniq/1`) +### [`uniq(enum)`](`Enum.uniq/1`) `uniq` applies to the whole collection: @@ -500,7 +500,7 @@ iex> Enum.uniq([1, 2, 2, 3, 3, 3, 1, 2, 3]) Comprehensions also support the `uniq: true` option. -### [uniq_by(enum, fun)](`Enum.uniq_by/2`) +### [`uniq_by(enum, fun)`](`Enum.uniq_by/2`) Get entries which are unique by the last letter of the fruit: @@ -515,7 +515,7 @@ iex> Enum.uniq_by(cart, &String.last(&1.fruit)) ## Indexing {: .col-2} -### [at(enum, index, default \\\\ nil)](`Enum.at/2`) +### [`at(enum, index, default \\\\ nil)`](`Enum.at/2`) ```elixir iex> Enum.at(cart, 0) @@ -528,7 +528,7 @@ iex> Enum.at(cart, 10, :none) Accessing a list by index in a loop is discouraged. -### [fetch(enum, index)](`Enum.fetch/2`) +### [`fetch(enum, index)`](`Enum.fetch/2`) ```elixir iex> Enum.fetch(cart, 0) @@ -537,7 +537,7 @@ iex> Enum.fetch(cart, 10) :error ``` -### [fetch!(enum, index)](`Enum.fetch!/2`) +### [`fetch!(enum, index)`](`Enum.fetch!/2`) ```elixir iex> Enum.fetch!(cart, 0) @@ -546,7 +546,7 @@ iex> Enum.fetch!(cart, 10) ** (Enum.OutOfBoundsError) out of bounds error ``` -### [with_index(enum)](`Enum.with_index/1`) +### [`with_index(enum)`](`Enum.with_index/1`) ```elixir iex> Enum.with_index(cart) @@ -557,7 +557,7 @@ iex> Enum.with_index(cart) ] ``` -### [with_index(enum, fun)](`Enum.with_index/2`) +### [`with_index(enum, fun)`](`Enum.with_index/2`) ```elixir iex> Enum.with_index(cart, fn item, index -> @@ -573,7 +573,7 @@ iex> Enum.with_index(cart, fn item, index -> ## Finding {: .col-2} -### [find(enum, default \\\\ nil, fun)](`Enum.find/2`) +### [`find(enum, default \\\\ nil, fun)`](`Enum.find/2`) ```elixir iex> Enum.find(cart, &(&1.fruit =~ "o")) @@ -584,7 +584,7 @@ iex> Enum.find(cart, :none, &(&1.fruit =~ "y")) :none ``` -### [find_index(enum, fun)](`Enum.find_index/2`) +### [`find_index(enum, fun)`](`Enum.find_index/2`) ```elixir iex> Enum.find_index(cart, &(&1.fruit =~ "o")) @@ -593,7 +593,7 @@ iex> Enum.find_index(cart, &(&1.fruit =~ "y")) nil ``` -### [find_value(enum, default \\\\ nil, fun)](`Enum.find_value/2`) +### [`find_value(enum, default \\\\ nil, fun)`](`Enum.find_value/2`) ```elixir iex> Enum.find_value(cart, fn item -> @@ -609,7 +609,7 @@ iex> Enum.find_value(cart, :none, fn item -> ## Grouping {: .col-2} -### [group_by(enum, key_fun)](`Enum.group_by/2`) +### [`group_by(enum, key_fun)`](`Enum.group_by/2`) Group by the last letter of the fruit: @@ -624,7 +624,7 @@ iex> Enum.group_by(cart, &String.last(&1.fruit)) } ``` -### [group_by(enum, key_fun, value_fun)](`Enum.group_by/3`) +### [`group_by(enum, key_fun, value_fun)`](`Enum.group_by/3`) Group by the last letter of the fruit with custom value: @@ -639,28 +639,28 @@ iex> Enum.group_by(cart, &String.last(&1.fruit), & &1.fruit) ## Joining & interspersing {: .col-2} -### [join(enum, joiner \\\\ "")](`Enum.join/2`) +### [`join(enum, joiner \\\\ "")`](`Enum.join/2`) ```elixir iex> Enum.join(["apple", "banana", "orange"], ", ") "apple, banana, orange" ``` -### [map_join(enum, joiner \\\\ "", mapper)](`Enum.map_join/3`) +### [`map_join(enum, joiner \\\\ "", mapper)`](`Enum.map_join/3`) ```elixir iex> Enum.map_join(cart, ", ", & &1.fruit) "apple, banana, orange" ``` -### [intersperse(enum, separator \\\\ "")](`Enum.intersperse/2`) +### [`intersperse(enum, separator \\\\ "")`](`Enum.intersperse/2`) ```elixir iex> Enum.intersperse(["apple", "banana", "orange"], ", ") ["apple", ", ", "banana", ", ", "orange"] ``` -### [map_intersperse(enum, separator \\\\ "", mapper)](`Enum.map_intersperse/3`) +### [`map_intersperse(enum, separator \\\\ "", mapper)`](`Enum.map_intersperse/3`) ```elixir iex> Enum.map_intersperse(cart, ", ", & &1.fruit) @@ -670,7 +670,7 @@ iex> Enum.map_intersperse(cart, ", ", & &1.fruit) ## Slicing {: .col-2} -### [slice(enum, index_range)](`Enum.slice/2`) +### [`slice(enum, index_range)`](`Enum.slice/2`) ```elixir iex> Enum.slice(cart, 0..1) @@ -690,7 +690,7 @@ iex> Enum.slice(cart, -2..-1) ] ``` -### [slice(enum, start_index, amount)](`Enum.slice/3`) +### [`slice(enum, start_index, amount)`](`Enum.slice/3`) ```elixir iex> Enum.slice(cart, 1, 2) @@ -700,7 +700,7 @@ iex> Enum.slice(cart, 1, 2) ] ``` -### [slide(enum, range_or_single_index, insertion_index)](`Enum.slide/3`) +### [`slide(enum, range_or_single_index, insertion_index)`](`Enum.slide/3`) ```elixir fruits = ["apple", "banana", "grape", "orange", "pear"] @@ -717,7 +717,7 @@ iex> Enum.slide(fruits, 1..3, 4) ## Reversing {: .col-2} -### [reverse(enum)](`Enum.reverse/1`) +### [`reverse(enum)`](`Enum.reverse/1`) ```elixir iex> Enum.reverse(cart) @@ -728,7 +728,7 @@ iex> Enum.reverse(cart) ] ``` -### [reverse(enum, tail)](`Enum.reverse/2`) +### [`reverse(enum, tail)`](`Enum.reverse/2`) ```elixir iex> Enum.reverse(cart, [:this_will_be, :the_tail]) @@ -741,7 +741,7 @@ iex> Enum.reverse(cart, [:this_will_be, :the_tail]) ] ``` -### [reverse_slice(enum, start_index, count)](`Enum.reverse_slice/3`) +### [`reverse_slice(enum, start_index, count)`](`Enum.reverse_slice/3`) ```elixir iex> Enum.reverse_slice(cart, 1, 2) @@ -755,7 +755,7 @@ iex> Enum.reverse_slice(cart, 1, 2) ## Splitting {: .col-2} -### [split(enum, amount)](`Enum.split/2`) +### [`split(enum, amount)`](`Enum.split/2`) ```elixir iex> Enum.split(cart, 1) @@ -777,7 +777,7 @@ iex> Enum.split(cart, -1) [%{fruit: "orange", count: 6}]} ``` -### [split_while(enum, fun)](`Enum.split_while/2`) +### [`split_while(enum, fun)`](`Enum.split_while/2`) Stops splitting as soon as it is false: @@ -790,7 +790,7 @@ iex> Enum.split_while(cart, &(&1.fruit =~ "e")) ]} ``` -### [split_with(enum, fun)](`Enum.split_with/2`) +### [`split_with(enum, fun)`](`Enum.split_with/2`) Splits the whole collection: @@ -806,7 +806,7 @@ iex> Enum.split_with(cart, &(&1.fruit =~ "e")) ## Splitting (drop and take) {: .col-2} -### [drop(enum, amount)](`Enum.drop/2`) +### [`drop(enum, amount)`](`Enum.drop/2`) ```elixir iex> Enum.drop(cart, 1) @@ -823,14 +823,14 @@ iex> Enum.drop(cart, -1) [%{fruit: "orange", count: 6}] ``` -### [drop_every(enum, nth)](`Enum.drop_every/2`) +### [`drop_every(enum, nth)`](`Enum.drop_every/2`) ```elixir iex> Enum.drop_every(cart, 2) [%{fruit: "banana", count: 1}] ``` -### [drop_while(enum, fun)](`Enum.drop_while/2`) +### [`drop_while(enum, fun)`](`Enum.drop_while/2`) ```elixir iex> Enum.drop_while(cart, &(&1.fruit =~ "e")) @@ -840,7 +840,7 @@ iex> Enum.drop_while(cart, &(&1.fruit =~ "e")) ] ``` -### [take(enum, amount)](`Enum.take/2`) +### [`take(enum, amount)`](`Enum.take/2`) ```elixir iex> Enum.take(cart, 1) @@ -857,7 +857,7 @@ iex> Enum.take(cart, -1) ] ``` -### [take_every(enum, nth)](`Enum.take_every/2`) +### [`take_every(enum, nth)`](`Enum.take_every/2`) ```elixir iex> Enum.take_every(cart, 2) @@ -867,7 +867,7 @@ iex> Enum.take_every(cart, 2) ] ``` -### [take_while(enum, fun)](`Enum.take_while/2`) +### [`take_while(enum, fun)`](`Enum.take_while/2`) ```elixir iex> Enum.take_while(cart, &(&1.fruit =~ "e")) @@ -877,7 +877,7 @@ iex> Enum.take_while(cart, &(&1.fruit =~ "e")) ## Random {: .col-2} -### [random(enum)](`Enum.random/1`) +### [`random(enum)`](`Enum.random/1`) Results will vary on every call: @@ -886,7 +886,7 @@ iex> Enum.random(cart) %{fruit: "orange", count: 6} ``` -### [take_random(enum, count)](`Enum.take_random/2`) +### [`take_random(enum, count)`](`Enum.take_random/2`) Results will vary on every call: @@ -898,7 +898,7 @@ iex> Enum.take_random(cart, 2) ] ``` -### [shuffle(enum)](`Enum.shuffle/1`) +### [`shuffle(enum)`](`Enum.shuffle/1`) Results will vary on every call: @@ -914,7 +914,7 @@ iex> Enum.shuffle(cart) ## Chunking {: .col-2} -### [chunk_by(enum, fun)](`Enum.chunk_by/2`) +### [`chunk_by(enum, fun)`](`Enum.chunk_by/2`) ```elixir iex> Enum.chunk_by(cart, &String.length(&1.fruit)) @@ -927,7 +927,7 @@ iex> Enum.chunk_by(cart, &String.length(&1.fruit)) ] ``` -### [chunk_every(enum, count)](`Enum.chunk_every/2`) +### [`chunk_every(enum, count)`](`Enum.chunk_every/2`) ```elixir iex> Enum.chunk_every(cart, 2) @@ -940,7 +940,7 @@ iex> Enum.chunk_every(cart, 2) ] ``` -### [chunk_every(enum, count, step, leftover \\\\ [])](`Enum.chunk_every/2`) +### [`chunk_every(enum, count, step, leftover \\\\ [])`](`Enum.chunk_every/2`) ```elixir iex> Enum.chunk_every(cart, 2, 2, [:elements, :to_complete]) @@ -972,7 +972,7 @@ See `Enum.chunk_while/4` for custom chunking. ## Zipping {: .col-2} -### [zip(enum1, enum2)](`Enum.zip/2`) +### [`zip(enum1, enum2)`](`Enum.zip/2`) ```elixir iex> fruits = ["apple", "banana", "orange"] @@ -983,7 +983,7 @@ iex> Enum.zip(fruits, counts) See `Enum.zip/1` for zipping many collections at once. -### [zip_with(enum1, enum2, fun)](`Enum.zip_with/2`) +### [`zip_with(enum1, enum2, fun)`](`Enum.zip_with/2`) ```elixir iex> fruits = ["apple", "banana", "orange"] @@ -1000,7 +1000,7 @@ iex> Enum.zip_with(fruits, counts, fn fruit, count -> See `Enum.zip_with/2` for zipping many collections at once. -### [zip_reduce(left, right, acc, fun)](`Enum.zip_reduce/4`) +### [`zip_reduce(left, right, acc, fun)`](`Enum.zip_reduce/4`) ```elixir iex> fruits = ["apple", "banana", "orange"] @@ -1014,7 +1014,7 @@ iex> Enum.zip_reduce(fruits, counts, 0, fn fruit, count, acc -> See `Enum.zip_reduce/3` for zipping many collections at once. -### [unzip(list)](`Enum.unzip/1`) +### [`unzip(list)`](`Enum.unzip/1`) ```elixir iex> cart |> Enum.map(&{&1.fruit, &1.count}) |> Enum.unzip() From 88ade1d312f5961e824c04c92cf41754c197d8dd Mon Sep 17 00:00:00 2001 From: Jean Klingler Date: Sun, 15 Oct 2023 20:10:46 +0900 Subject: [PATCH 0055/1886] Fix warnings on conflicting callbacks (#13006) * Improve imprecise message * Remove false positive warnings on conflicting behaviours --- lib/elixir/lib/module/types/behaviour.ex | 37 ++++++++++++------- .../test/elixir/kernel/warning_test.exs | 35 +++++++++++++++++- 2 files changed, 58 insertions(+), 14 deletions(-) diff --git a/lib/elixir/lib/module/types/behaviour.ex b/lib/elixir/lib/module/types/behaviour.ex index 4f4ef0d562..0e5cf3a0d8 100644 --- a/lib/elixir/lib/module/types/behaviour.ex +++ b/lib/elixir/lib/module/types/behaviour.ex @@ -69,7 +69,7 @@ defmodule Module.Types.Behaviour do context = case context.callbacks do - %{^callback => {_kind, conflict, _optional?}} -> + %{^callback => [{_kind, conflict, _optional?} | _]} -> warn( context, {:duplicate_behaviour, context.module, behaviour, conflict, kind, callback} @@ -79,11 +79,15 @@ defmodule Module.Types.Behaviour do context end - put_in(context.callbacks[callback], {kind, behaviour, original in optional_callbacks}) + new_callback = {kind, behaviour, original in optional_callbacks} + callbacks = context.callbacks[callback] || [] + + put_in(context.callbacks[callback], [new_callback | callbacks]) end defp check_callbacks(context, all_definitions) do - for {callback, {kind, behaviour, optional?}} <- context.callbacks, + for {callback, callbacks} <- context.callbacks, + {kind, behaviour, optional?} <- callbacks, reduce: context do context -> case :lists.keyfind(callback, 1, all_definitions) do @@ -186,10 +190,10 @@ defmodule Module.Types.Behaviour do end defp behaviour_callbacks_for_impls([fa | tail], behaviour, callbacks) do - case callbacks[fa] do - {_, ^behaviour, _} -> - [{fa, behaviour} | behaviour_callbacks_for_impls(tail, behaviour, callbacks)] - + with list when is_list(list) <- callbacks[fa], + true <- Enum.any?(list, &match?({_, ^behaviour, _}, &1)) do + [{fa, behaviour} | behaviour_callbacks_for_impls(tail, behaviour, callbacks)] + else _ -> behaviour_callbacks_for_impls(tail, behaviour, callbacks) end @@ -201,8 +205,12 @@ defmodule Module.Types.Behaviour do defp callbacks_for_impls([fa | tail], callbacks) do case callbacks[fa] do - {_, behaviour, _} -> [{fa, behaviour} | callbacks_for_impls(tail, callbacks)] - nil -> callbacks_for_impls(tail, callbacks) + list when is_list(list) -> + Enum.map(list, fn {_, behaviour, _} -> {fa, behaviour} end) ++ + callbacks_for_impls(tail, callbacks) + + nil -> + callbacks_for_impls(tail, callbacks) end end @@ -214,8 +222,11 @@ defmodule Module.Types.Behaviour do defp warn_missing_impls(context, impl_contexts, defs) do for {pair, kind, meta, _clauses} <- defs, kind in [:def, :defmacro], reduce: context do context -> - with {:ok, {_, behaviour, _}} <- Map.fetch(context.callbacks, pair), - true <- missing_impl_in_context?(meta, behaviour, impl_contexts) do + with {:ok, callbacks} <- Map.fetch(context.callbacks, pair), + {_, behaviour, _} <- + Enum.find(callbacks, fn {_, behaviour, _} -> + missing_impl_in_context?(meta, behaviour, impl_contexts) + end) do warn(context, {:missing_impl, pair, kind, behaviour}, line: :elixir_utils.get_line(meta) ) @@ -302,9 +313,9 @@ defmodule Module.Types.Behaviour do def format_warning({:duplicate_behaviour, module, behaviour, conflict, kind, callback}) do [ - "conflicting behaviours found. ", + "conflicting behaviours found. Callback ", format_definition(kind, callback), - " is required by ", + " is defined by both ", inspect(conflict), " and ", inspect(behaviour), diff --git a/lib/elixir/test/elixir/kernel/warning_test.exs b/lib/elixir/test/elixir/kernel/warning_test.exs index 92b8edd28b..7cba3fabd6 100644 --- a/lib/elixir/test/elixir/kernel/warning_test.exs +++ b/lib/elixir/test/elixir/kernel/warning_test.exs @@ -1379,7 +1379,7 @@ defmodule Kernel.WarningTest do assert_warn_eval( [ "nofile:9: ", - "conflicting behaviours found. function foo/0 is required by Sample1 and Sample2 (in module Sample3)" + "conflicting behaviours found. Callback function foo/0 is defined by both Sample1 and Sample2 (in module Sample3)" ], """ defmodule Sample1 do @@ -1400,6 +1400,39 @@ defmodule Kernel.WarningTest do purge([Sample1, Sample2, Sample3]) end + test "conflicting behaviour (but one optional callback)" do + message = + capture_compile(""" + defmodule Sample1 do + @callback foo :: term + end + + defmodule Sample2 do + @callback foo :: term + @callback bar :: term + @optional_callbacks foo: 0 + end + + defmodule Sample3 do + @behaviour Sample1 + @behaviour Sample2 + + @impl Sample1 + def foo, do: 1 + @impl Sample2 + def bar, do: 2 + end + """) + + assert message =~ + "conflicting behaviours found. Callback function foo/0 is defined by both Sample1 and Sample2 (in module Sample3)" + + refute message =~ "module attribute @impl was not set" + refute message =~ "this behaviour does not specify such callback" + after + purge([Sample1, Sample2, Sample3]) + end + test "duplicate behaviour" do assert_warn_eval( [ From 8a0961c9dcecd8135a8108c20866de515c0951a2 Mon Sep 17 00:00:00 2001 From: Jean Klingler Date: Sun, 15 Oct 2023 21:11:23 +0900 Subject: [PATCH 0056/1886] Fix known callbacks suggestions (#13007) --- lib/elixir/lib/module/types/behaviour.ex | 2 +- lib/elixir/test/elixir/kernel/impl_test.exs | 49 +++++++++++++-------- 2 files changed, 32 insertions(+), 19 deletions(-) diff --git a/lib/elixir/lib/module/types/behaviour.ex b/lib/elixir/lib/module/types/behaviour.ex index 0e5cf3a0d8..e57c650009 100644 --- a/lib/elixir/lib/module/types/behaviour.ex +++ b/lib/elixir/lib/module/types/behaviour.ex @@ -259,7 +259,7 @@ defmodule Module.Types.Behaviour do defp known_callbacks(callbacks) do formatted_callbacks = - for {{name, arity}, {kind, module, _}} <- callbacks do + for {{name, arity}, list} <- callbacks, {kind, module, _} <- list do "\n * " <> Exception.format_mfa(module, name, arity) <> " (#{format_definition(kind)})" end diff --git a/lib/elixir/test/elixir/kernel/impl_test.exs b/lib/elixir/test/elixir/kernel/impl_test.exs index 2ad09fac2f..651dc96cdc 100644 --- a/lib/elixir/test/elixir/kernel/impl_test.exs +++ b/lib/elixir/test/elixir/kernel/impl_test.exs @@ -202,28 +202,41 @@ defmodule Kernel.ImplTest do end test "warns for @impl true with callback name not in behaviour" do - assert capture_err(fn -> - Code.eval_string(""" - defmodule Kernel.ImplTest.ImplAttributes do - @behaviour Kernel.ImplTest.Behaviour - @impl true - def bar(), do: :ok - end - """) - end) =~ + message = + capture_err(fn -> + Code.eval_string(""" + defmodule Kernel.ImplTest.ImplAttributes do + @behaviour Kernel.ImplTest.Behaviour + @impl true + def bar(), do: :ok + end + """) + end) + + assert message =~ "got \"@impl true\" for function bar/0 but no behaviour specifies such callback" + + assert message =~ "The known callbacks are" + assert message =~ "* Kernel.ImplTest.Behaviour.foo/0 (function)" end test "warns for @impl true with macro callback name not in behaviour" do - assert capture_err(fn -> - Code.eval_string(""" - defmodule Kernel.ImplTest.ImplAttributes do - @behaviour Kernel.ImplTest.MacroBehaviour - @impl true - defmacro foo(), do: :ok - end - """) - end) =~ "got \"@impl true\" for macro foo/0 but no behaviour specifies such callback" + message = + capture_err(fn -> + Code.eval_string(""" + defmodule Kernel.ImplTest.ImplAttributes do + @behaviour Kernel.ImplTest.MacroBehaviour + @impl true + defmacro foo(), do: :ok + end + """) + end) + + assert message =~ + "got \"@impl true\" for macro foo/0 but no behaviour specifies such callback" + + assert message =~ "The known callbacks are" + assert message =~ "* Kernel.ImplTest.MacroBehaviour.bar/0 (macro)" end test "warns for @impl true with callback kind not in behaviour" do From da3e093a1d9965e1c5e6ccb1da2de60141bb2449 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 16 Oct 2023 14:20:23 +0200 Subject: [PATCH 0057/1886] Improve titles to reduce ambiguity --- .../pages/cheatsheets/enum-cheat.cheatmd | 20 +++++++++---------- lib/elixir/pages/mix-and-otp/agents.md | 4 ++-- .../pages/mix-and-otp/dynamic-supervisor.md | 2 +- .../pages/mix-and-otp/erlang-term-storage.md | 2 +- lib/elixir/pages/mix-and-otp/genservers.md | 2 +- .../mix-and-otp/supervisor-and-application.md | 2 +- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/lib/elixir/pages/cheatsheets/enum-cheat.cheatmd b/lib/elixir/pages/cheatsheets/enum-cheat.cheatmd index a419c78dc9..1e6190b62a 100644 --- a/lib/elixir/pages/cheatsheets/enum-cheat.cheatmd +++ b/lib/elixir/pages/cheatsheets/enum-cheat.cheatmd @@ -307,7 +307,7 @@ iex> cart |> Enum.map(& &1.count) |> Enum.product() ## Sorting {: .col-2} -### [`sort(enum, sorter \\\\ :asc)`](`Enum.sort/2`) +### [`sort(enum, sorter \\ :asc)`](`Enum.sort/2`) ```elixir iex> cart |> Enum.map(& &1.fruit) |> Enum.sort() @@ -318,7 +318,7 @@ iex> cart |> Enum.map(& &1.fruit) |> Enum.sort(:desc) When sorting structs, use `Enum.sort/2` with a module as sorter. -### [`sort_by(enum, mapper, sorter \\\\ :asc)`](`Enum.sort_by/2`) +### [`sort_by(enum, mapper, sorter \\ :asc)`](`Enum.sort_by/2`) ```elixir iex> Enum.sort_by(cart, & &1.count) @@ -515,7 +515,7 @@ iex> Enum.uniq_by(cart, &String.last(&1.fruit)) ## Indexing {: .col-2} -### [`at(enum, index, default \\\\ nil)`](`Enum.at/2`) +### [`at(enum, index, default \\ nil)`](`Enum.at/2`) ```elixir iex> Enum.at(cart, 0) @@ -573,7 +573,7 @@ iex> Enum.with_index(cart, fn item, index -> ## Finding {: .col-2} -### [`find(enum, default \\\\ nil, fun)`](`Enum.find/2`) +### [`find(enum, default \\ nil, fun)`](`Enum.find/2`) ```elixir iex> Enum.find(cart, &(&1.fruit =~ "o")) @@ -593,7 +593,7 @@ iex> Enum.find_index(cart, &(&1.fruit =~ "y")) nil ``` -### [`find_value(enum, default \\\\ nil, fun)`](`Enum.find_value/2`) +### [`find_value(enum, default \\ nil, fun)`](`Enum.find_value/2`) ```elixir iex> Enum.find_value(cart, fn item -> @@ -639,28 +639,28 @@ iex> Enum.group_by(cart, &String.last(&1.fruit), & &1.fruit) ## Joining & interspersing {: .col-2} -### [`join(enum, joiner \\\\ "")`](`Enum.join/2`) +### [`join(enum, joiner \\ "")`](`Enum.join/2`) ```elixir iex> Enum.join(["apple", "banana", "orange"], ", ") "apple, banana, orange" ``` -### [`map_join(enum, joiner \\\\ "", mapper)`](`Enum.map_join/3`) +### [`map_join(enum, joiner \\ "", mapper)`](`Enum.map_join/3`) ```elixir iex> Enum.map_join(cart, ", ", & &1.fruit) "apple, banana, orange" ``` -### [`intersperse(enum, separator \\\\ "")`](`Enum.intersperse/2`) +### [`intersperse(enum, separator \\ "")`](`Enum.intersperse/2`) ```elixir iex> Enum.intersperse(["apple", "banana", "orange"], ", ") ["apple", ", ", "banana", ", ", "orange"] ``` -### [`map_intersperse(enum, separator \\\\ "", mapper)`](`Enum.map_intersperse/3`) +### [`map_intersperse(enum, separator \\ "", mapper)`](`Enum.map_intersperse/3`) ```elixir iex> Enum.map_intersperse(cart, ", ", & &1.fruit) @@ -940,7 +940,7 @@ iex> Enum.chunk_every(cart, 2) ] ``` -### [`chunk_every(enum, count, step, leftover \\\\ [])`](`Enum.chunk_every/2`) +### [`chunk_every(enum, count, step, leftover \\ [])`](`Enum.chunk_every/2`) ```elixir iex> Enum.chunk_every(cart, 2, 2, [:elements, :to_complete]) diff --git a/lib/elixir/pages/mix-and-otp/agents.md b/lib/elixir/pages/mix-and-otp/agents.md index 7d4e44964f..935064a492 100644 --- a/lib/elixir/pages/mix-and-otp/agents.md +++ b/lib/elixir/pages/mix-and-otp/agents.md @@ -1,4 +1,4 @@ -# Agent +# Simple state management with agents In this guide, we will learn how to keep and share state between multiple entities. If you have previous programming experience, you may think of globally shared variables, but the model we will learn here is quite different. The next chapters will generalize the concepts introduced here. @@ -21,7 +21,7 @@ We will explore most of these abstractions in this guide. Keep in mind that they Here, we will use agents, and create a module named `KV.Bucket`, responsible for storing our key-value entries in a way that allows them to be read and modified by other processes. -## Agents +## Agents 101 `Agent`s are simple wrappers around state. If all you want from a process is to keep state, agents are a great fit. Let's start a `iex` session inside the project with: diff --git a/lib/elixir/pages/mix-and-otp/dynamic-supervisor.md b/lib/elixir/pages/mix-and-otp/dynamic-supervisor.md index ef20b24a7a..e353133372 100644 --- a/lib/elixir/pages/mix-and-otp/dynamic-supervisor.md +++ b/lib/elixir/pages/mix-and-otp/dynamic-supervisor.md @@ -1,4 +1,4 @@ -# DynamicSupervisor +# Supervising dynamic children We have now successfully defined our supervisor which is automatically started (and stopped) as part of our application lifecycle. diff --git a/lib/elixir/pages/mix-and-otp/erlang-term-storage.md b/lib/elixir/pages/mix-and-otp/erlang-term-storage.md index 7fe26f8658..d5f4efb74b 100644 --- a/lib/elixir/pages/mix-and-otp/erlang-term-storage.md +++ b/lib/elixir/pages/mix-and-otp/erlang-term-storage.md @@ -1,4 +1,4 @@ -# ETS +# Speeding up with ETS Every time we need to look up a bucket, we need to send a message to the registry. In case our registry is being accessed concurrently by multiple processes, the registry may become a bottleneck! diff --git a/lib/elixir/pages/mix-and-otp/genservers.md b/lib/elixir/pages/mix-and-otp/genservers.md index a4451b915e..51be7cbe5b 100644 --- a/lib/elixir/pages/mix-and-otp/genservers.md +++ b/lib/elixir/pages/mix-and-otp/genservers.md @@ -1,4 +1,4 @@ -# GenServer +# Client-server communication with GenServer In the [previous chapter](../agents.md), we used agents to represent our buckets. In the [introduction to mix](../introduction-to-mix.md), we specified we would like to name each bucket so we can do the following: diff --git a/lib/elixir/pages/mix-and-otp/supervisor-and-application.md b/lib/elixir/pages/mix-and-otp/supervisor-and-application.md index b71f2221bb..fd61a97cba 100644 --- a/lib/elixir/pages/mix-and-otp/supervisor-and-application.md +++ b/lib/elixir/pages/mix-and-otp/supervisor-and-application.md @@ -1,4 +1,4 @@ -# Supervisor and application +# Supervision trees and applications In the previous chapter about `GenServer`, we implemented `KV.Registry` to manage buckets. At some point, we started monitoring buckets so we were able to take action whenever a `KV.Bucket` crashed. Although the change was relatively small, it introduced a question which is frequently asked by Elixir developers: what happens when something fails? From ddb2434357234a78a69e85e016149ac353cd7fb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 17 Oct 2023 10:03:28 +0200 Subject: [PATCH 0058/1886] Emit warning if True/False/Nil are used also outside of alias --- lib/elixir/src/elixir_expand.erl | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/elixir/src/elixir_expand.erl b/lib/elixir/src/elixir_expand.erl index 2c4420cccf..0eb61e28c5 100644 --- a/lib/elixir/src/elixir_expand.erl +++ b/lib/elixir/src/elixir_expand.erl @@ -68,10 +68,7 @@ expand({alias, Meta, [Ref, Opts]}, S, E) -> assert_no_match_or_guard_scope(Meta, "alias", S, E), {ERef, SR, ER} = expand_without_aliases_report(Ref, S, E), {EOpts, ST, ET} = expand_opts(Meta, alias, [as, warn], no_alias_opts(Opts), SR, ER), - case lists:member(ERef, ['Elixir.True', 'Elixir.False', 'Elixir.Nil']) of - true -> elixir_errors:file_warn(Meta, E, ?MODULE, {commonly_mistaken_alias, Ref}); - false -> ok - end, + if is_atom(ERef) -> {ERef, ST, expand_alias(Meta, true, ERef, EOpts, ET)}; @@ -1036,6 +1033,12 @@ expand_without_aliases_report(Other, S, E) -> expand_aliases({'__aliases__', Meta, _} = Alias, S, E, Report) -> case elixir_aliases:expand_or_concat(Alias, E) of Receiver when is_atom(Receiver) -> + if + Receiver =:= 'Elixir.True'; Receiver =:= 'Elixir.False'; Receiver =:= 'Elixir.Nil' -> + elixir_errors:file_warn(Meta, E, ?MODULE, {commonly_mistaken_alias, Receiver}); + true -> + ok + end, Report andalso elixir_env:trace({alias_reference, Meta, Receiver}, E), {Receiver, S, E}; From ec4dafae0c71f9d3e5e2f721b99b672bee3a370e Mon Sep 17 00:00:00 2001 From: Jean Klingler Date: Tue, 17 Oct 2023 17:53:10 +0900 Subject: [PATCH 0059/1886] Fix overriding with Elixir. prefixed defmodule (#13011) Closes #12456. --- lib/elixir/lib/kernel.ex | 15 +++++----- lib/elixir/test/elixir/kernel/alias_test.exs | 31 ++++++++++++++++++++ 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/lib/elixir/lib/kernel.ex b/lib/elixir/lib/kernel.ex index 2c9ab81c7c..f55b3d827d 100644 --- a/lib/elixir/lib/kernel.ex +++ b/lib/elixir/lib/kernel.ex @@ -4889,14 +4889,13 @@ defmodule Kernel do expanded = expand_module_alias(alias, env) {expanded, with_alias} = - case is_atom(expanded) do - true -> + case alias_defmodule(alias, expanded, env) do + {full, old, new} -> # Expand the module considering the current environment/nesting - {full, old, new} = alias_defmodule(alias, expanded, env) meta = [defined: full, context: env.module] ++ alias_meta(alias) {full, {:alias, meta, [old, [as: new, warn: false]]}} - false -> + nil -> {expanded, nil} end @@ -4955,13 +4954,13 @@ defmodule Kernel do defp expand_module_alias(other, env), do: Macro.expand(other, env) + defp alias_defmodule(_raw, module, _env) when not is_atom(module), do: nil + # defmodule Elixir.Alias - defp alias_defmodule({:__aliases__, _, [:"Elixir", _ | _]}, module, _env), - do: {module, module, nil} + defp alias_defmodule({:__aliases__, _, [:"Elixir", _ | _]}, _module, _env), do: nil # defmodule Alias in root - defp alias_defmodule({:__aliases__, _, _}, module, %{module: nil}), - do: {module, module, nil} + defp alias_defmodule({:__aliases__, _, _}, _module, %{module: nil}), do: nil # defmodule Alias nested defp alias_defmodule({:__aliases__, _, [h | t]}, _module, env) when is_atom(h) do diff --git a/lib/elixir/test/elixir/kernel/alias_test.exs b/lib/elixir/test/elixir/kernel/alias_test.exs index e91b31d157..a1e3d0f5cb 100644 --- a/lib/elixir/test/elixir/kernel/alias_test.exs +++ b/lib/elixir/test/elixir/kernel/alias_test.exs @@ -147,3 +147,34 @@ defmodule Macro.AliasTest.User do assert is_map(struct(Macro.AliasTest.User.Second, []).baz) end end + +defmodule Kernel.AliasNestingEnvTest do + use ExUnit.Case, async: true + + alias Another.AliasEnv, warn: false + + def aliases_before, do: __ENV__.aliases + + defmodule Elixir.AliasEnv do + def aliases_nested, do: __ENV__.aliases + end + + def aliases_after, do: __ENV__.aliases + + test "keeps env after overriding nested Elixir module of the same name" do + assert aliases_before() == [ + {Elixir.Nested, Kernel.AliasTest.Nested}, + {Elixir.AliasEnv, Another.AliasEnv} + ] + + assert Elixir.AliasEnv.aliases_nested() == [ + {Elixir.Nested, Kernel.AliasTest.Nested}, + {Elixir.AliasEnv, Another.AliasEnv} + ] + + assert aliases_after() == [ + {Elixir.Nested, Kernel.AliasTest.Nested}, + {Elixir.AliasEnv, Another.AliasEnv} + ] + end +end From 69255ecbc8737e2bcd8c0266248236dd671a590d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 17 Oct 2023 11:09:21 +0200 Subject: [PATCH 0060/1886] Do not leak alias from Elixir root and simplify defmodule implementation --- lib/elixir/lib/kernel.ex | 25 ++++++++------- lib/elixir/src/elixir_expand.erl | 43 +++++++++++++------------- lib/elixir/test/elixir/module_test.exs | 10 ++++++ 3 files changed, 45 insertions(+), 33 deletions(-) diff --git a/lib/elixir/lib/kernel.ex b/lib/elixir/lib/kernel.ex index f55b3d827d..49ea98d19b 100644 --- a/lib/elixir/lib/kernel.ex +++ b/lib/elixir/lib/kernel.ex @@ -4889,13 +4889,14 @@ defmodule Kernel do expanded = expand_module_alias(alias, env) {expanded, with_alias} = - case alias_defmodule(alias, expanded, env) do - {full, old, new} -> + case is_atom(expanded) do + true -> + {full, old, opts} = alias_defmodule(alias, expanded, env) # Expand the module considering the current environment/nesting - meta = [defined: full, context: env.module] ++ alias_meta(alias) - {full, {:alias, meta, [old, [as: new, warn: false]]}} + meta = [defined: full] ++ alias_meta(alias) + {full, {:require, meta, [old, opts]}} - nil -> + false -> {expanded, nil} end @@ -4954,28 +4955,28 @@ defmodule Kernel do defp expand_module_alias(other, env), do: Macro.expand(other, env) - defp alias_defmodule(_raw, module, _env) when not is_atom(module), do: nil - # defmodule Elixir.Alias - defp alias_defmodule({:__aliases__, _, [:"Elixir", _ | _]}, _module, _env), do: nil + defp alias_defmodule({:__aliases__, _, [:"Elixir", _ | _]}, module, _env), + do: {module, module, []} # defmodule Alias in root - defp alias_defmodule({:__aliases__, _, _}, _module, %{module: nil}), do: nil + defp alias_defmodule({:__aliases__, _, _}, module, %{module: nil}), do: {module, module, []} # defmodule Alias nested defp alias_defmodule({:__aliases__, _, [h | t]}, _module, env) when is_atom(h) do module = :elixir_aliases.concat([env.module, h]) alias = String.to_atom("Elixir." <> Atom.to_string(h)) + opts = [as: alias, warn: false] case t do - [] -> {module, module, alias} - _ -> {String.to_atom(Enum.join([module | t], ".")), module, alias} + [] -> {module, module, opts} + _ -> {String.to_atom(Enum.join([module | t], ".")), module, opts} end end # defmodule _ defp alias_defmodule(_raw, module, _env) do - {module, module, nil} + {module, module, []} end defp module_var({name, kind}, meta) when is_atom(kind), do: {name, meta, kind} diff --git a/lib/elixir/src/elixir_expand.erl b/lib/elixir/src/elixir_expand.erl index 0eb61e28c5..de71ccf377 100644 --- a/lib/elixir/src/elixir_expand.erl +++ b/lib/elixir/src/elixir_expand.erl @@ -84,11 +84,19 @@ expand({require, Meta, [Ref, Opts]}, S, E) -> {ERef, SR, ER} = expand_without_aliases_report(Ref, S, E), {EOpts, ST, ET} = expand_opts(Meta, require, [as, warn], no_alias_opts(Opts), SR, ER), - if - is_atom(ERef) -> + %% Add the alias to context_modules if defined is set. + %% This is used by defmodule in order to store the defined + %% module in context modules. + case lists:keyfind(defined, 1, Meta) of + {defined, Mod} when is_atom(Mod) -> + EA = ET#{context_modules := [Mod | ?key(ET, context_modules)]}, + {ERef, ST, expand_alias(Meta, false, ERef, EOpts, EA)}; + + false when is_atom(ERef) -> elixir_aliases:ensure_loaded(Meta, ERef, ET), {ERef, ST, expand_require(Meta, ERef, EOpts, ET)}; - true -> + + false -> file_error(Meta, E, ?MODULE, {expected_compile_time_module, require, Ref}) end; @@ -982,29 +990,22 @@ expand_require(Meta, Ref, Opts, E) -> RE = E#{requires := ordsets:add_element(Ref, ?key(E, requires))}, expand_alias(Meta, false, Ref, Opts, RE). -expand_alias(Meta, IncludeByDefault, Ref, Opts, #{context_modules := Context} = E) -> - New = expand_as(lists:keyfind(as, 1, Opts), Meta, IncludeByDefault, Ref, E), +expand_alias(Meta, IncludeByDefault, Ref, Opts, E) -> + case expand_as(lists:keyfind(as, 1, Opts), Meta, IncludeByDefault, Ref, E) of + {ok, New} -> + {Aliases, MacroAliases} = elixir_aliases:store(Meta, New, Ref, Opts, E), + E#{aliases := Aliases, macro_aliases := MacroAliases}; - %% Add the alias to context_modules if defined is set. - %% This is used by defmodule in order to store the defined - %% module in context modules. - NewContext = - case lists:keyfind(defined, 1, Meta) of - {defined, Mod} when is_atom(Mod) -> [Mod | Context]; - false -> Context - end, - - {Aliases, MacroAliases} = elixir_aliases:store(Meta, New, Ref, Opts, E), - E#{aliases := Aliases, macro_aliases := MacroAliases, context_modules := NewContext}. + error -> + E + end. -expand_as({as, nil}, _Meta, _IncludeByDefault, Ref, _E) -> - Ref; expand_as({as, Atom}, Meta, _IncludeByDefault, _Ref, E) when is_atom(Atom), not is_boolean(Atom) -> case atom_to_list(Atom) of "Elixir." ++ ([FirstLetter | _] = Rest) when FirstLetter >= $A, FirstLetter =< $Z -> case string:tokens(Rest, ".") of [_] -> - Atom; + {ok, Atom}; _ -> file_error(Meta, E, ?MODULE, {invalid_alias_for_as, nested_alias, Atom}) end; @@ -1015,10 +1016,10 @@ expand_as(false, Meta, IncludeByDefault, Ref, E) -> if IncludeByDefault -> case elixir_aliases:last(Ref) of - {ok, NewRef} -> NewRef; + {ok, NewRef} -> {ok, NewRef}; error -> file_error(Meta, E, ?MODULE, {invalid_alias_module, Ref}) end; - true -> Ref + true -> error end; expand_as({as, Other}, Meta, _IncludeByDefault, _Ref, E) -> file_error(Meta, E, ?MODULE, {invalid_alias_for_as, not_alias, Other}). diff --git a/lib/elixir/test/elixir/module_test.exs b/lib/elixir/test/elixir/module_test.exs index 0938410f21..c6a7a43b03 100644 --- a/lib/elixir/test/elixir/module_test.exs +++ b/lib/elixir/test/elixir/module_test.exs @@ -334,6 +334,16 @@ defmodule ModuleTest do assert Elixir.ModuleTest.NonAtomAlias.hello() == :world end + test "does not leak alias from Elixir root alias" do + defmodule Elixir.ModuleTest.ElixirRootAlias do + def hello, do: :world + end + + refute __ENV__.aliases[Elixir.ModuleTest] + refute __ENV__.aliases[Elixir.ElixirRootAlias] + assert Elixir.ModuleTest.ElixirRootAlias.hello() == :world + end + test "does not warn on captured underscored vars" do _unused = 123 From ddf00f779c8a78365bec0f6ad544b99722244049 Mon Sep 17 00:00:00 2001 From: Adam Millerchip Date: Wed, 18 Oct 2023 22:38:17 +0900 Subject: [PATCH 0061/1886] Speed up _ interspersing by using bitstrings (#13019) --- lib/elixir/lib/code/formatter.ex | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/lib/elixir/lib/code/formatter.ex b/lib/elixir/lib/code/formatter.ex index 8a9dcebdce..063e6d4d30 100644 --- a/lib/elixir/lib/code/formatter.ex +++ b/lib/elixir/lib/code/formatter.ex @@ -1622,25 +1622,30 @@ defmodule Code.Formatter do end defp insert_underscores(digits) do + byte_size = byte_size(digits) + cond do digits =~ "_" -> digits - byte_size(digits) >= 6 -> - digits - |> String.to_charlist() - |> Enum.reverse() - |> Enum.chunk_every(3) - |> Enum.intersperse(~c"_") - |> List.flatten() - |> Enum.reverse() - |> List.to_string() + byte_size >= 6 -> + offset = rem(byte_size, 3) + {prefix, rest} = String.split_at(digits, offset) + do_insert_underscores(prefix, rest) true -> digits end end + defp do_insert_underscores(acc, ""), do: acc + + defp do_insert_underscores("", <>), + do: do_insert_underscores(next, rest) + + defp do_insert_underscores(acc, <>), + do: do_insert_underscores(<>, rest) + defp escape_heredoc(string, escape) do string = String.replace(string, escape, "\\" <> escape) heredoc_to_algebra(["" | String.split(string, "\n")]) From 5fa100b9972aca5dd0c807bcbe08f7ccf015a2fa Mon Sep 17 00:00:00 2001 From: Gary Rennie Date: Wed, 18 Oct 2023 15:18:35 +0100 Subject: [PATCH 0062/1886] Mention capture_log with a level in the ExUnit config (#13020) --- lib/ex_unit/lib/ex_unit.ex | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/ex_unit/lib/ex_unit.ex b/lib/ex_unit/lib/ex_unit.ex index ef80c4b2eb..7a0ad68d7d 100644 --- a/lib/ex_unit/lib/ex_unit.ex +++ b/lib/ex_unit/lib/ex_unit.ex @@ -231,7 +231,10 @@ defmodule ExUnit do * `:capture_log` - if ExUnit should default to keeping track of log messages and print them on test failure. Can be overridden for individual tests via - `@tag capture_log: false`. Defaults to `false`; + `@tag capture_log: false`. This can also be configured to a specific level + with `capture_log: [level: LEVEL]`, for example: + `capture_log: [level: :emergency]` to prevent any output from test failures. + Defaults to `false`; * `:colors` - a keyword list of color options to be used by some formatters: * `:enabled` - boolean option to enable colors, defaults to `IO.ANSI.enabled?/0`; From f2d25227214404e82e8021686cdd4521580ba81e Mon Sep 17 00:00:00 2001 From: Josh Bones Date: Thu, 19 Oct 2023 20:22:52 +0200 Subject: [PATCH 0063/1886] Fix typo in code-smells/non-assertive truthiness documentation (#13021) --- lib/elixir/pages/anti-patterns/code-anti-patterns.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/pages/anti-patterns/code-anti-patterns.md b/lib/elixir/pages/anti-patterns/code-anti-patterns.md index d87a39b9cc..b29858f8d9 100644 --- a/lib/elixir/pages/anti-patterns/code-anti-patterns.md +++ b/lib/elixir/pages/anti-patterns/code-anti-patterns.md @@ -594,7 +594,7 @@ Given both operands of `&&/2` are booleans, the code is more generic than necess To remove this anti-pattern, we can replace `&&/2`, `||/2`, and `!/1` by `and/2`, `or/2`, and `not/1` respectively. These operators assert at least their first argument is a boolean: ```elixir -if is_binary(name) or is_integer(age) do +if is_binary(name) and is_integer(age) do # ... else # ... From 2f9061e8e0c800cb505036bed9e001bd8a098112 Mon Sep 17 00:00:00 2001 From: samanera <141537607+samanera@users.noreply.github.com> Date: Thu, 19 Oct 2023 23:25:12 -0500 Subject: [PATCH 0064/1886] Fix typos in elixir/pages/ (#13022) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `is an a float` → `is a float` `valuesare` → `values are` `specificatio` → `specification` --- lib/elixir/pages/getting-started/basic-types.md | 4 ++-- lib/elixir/pages/mix-and-otp/task-and-gen-tcp.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/elixir/pages/getting-started/basic-types.md b/lib/elixir/pages/getting-started/basic-types.md index ca4bd2f71d..140192e214 100644 --- a/lib/elixir/pages/getting-started/basic-types.md +++ b/lib/elixir/pages/getting-started/basic-types.md @@ -79,7 +79,7 @@ iex> is_integer(2.0) false ``` -You can also use `is_float` or `is_number` to check, respectively, if an argument is an a float, or either an integer or float. +You can also use `is_float` or `is_number` to check, respectively, if an argument is a float, or either an integer or float. ## Identifying functions and documentation @@ -143,7 +143,7 @@ iex> true or raise("This error will never be raised") true ``` -Elixir also provides the concept of `nil`, to indicate the absence of a value, and a set of logical operators that also manipulate `nil`: `||/2`, `&&/2`, and `!/1`. For these operators, `false` and `nil` are considered "falsy", all other valuesare considered "truthy": +Elixir also provides the concept of `nil`, to indicate the absence of a value, and a set of logical operators that also manipulate `nil`: `||/2`, `&&/2`, and `!/1`. For these operators, `false` and `nil` are considered "falsy", all other values are considered "truthy": ```elixir # or diff --git a/lib/elixir/pages/mix-and-otp/task-and-gen-tcp.md b/lib/elixir/pages/mix-and-otp/task-and-gen-tcp.md index 34b5cbe9a5..9e32c1e428 100644 --- a/lib/elixir/pages/mix-and-otp/task-and-gen-tcp.md +++ b/lib/elixir/pages/mix-and-otp/task-and-gen-tcp.md @@ -284,7 +284,7 @@ In this case, the answer is yes: if the acceptor crashes, there is no need to cr However, there is still one concern left, which are the restart strategies. Tasks, by default, have the `:restart` value set to `:temporary`, which means they are not restarted. This is an excellent default for the connections started via the `Task.Supervisor`, as it makes no sense to restart a failed connection, but it is a bad choice for the acceptor. If the acceptor crashes, we want to bring the acceptor up and running again. -Let's fix this. We know that for a child of shape `{Task, fun}`, Elixir will invoke `Task.child_spec(fun)` to retrieve the underlying child specification. Therefore, one might imagine that to change the `{Task, fun}` specificatio to have a `:restart` of `:permanent`, we would need to change the `Task` module. However, that's impossible to do, as the `Task` module is defined as part of Elixir's standard library (and even if it was possible, it is unlikely it would be a good idea). +Let's fix this. We know that for a child of shape `{Task, fun}`, Elixir will invoke `Task.child_spec(fun)` to retrieve the underlying child specification. Therefore, one might imagine that to change the `{Task, fun}` specification to have a `:restart` of `:permanent`, we would need to change the `Task` module. However, that's impossible to do, as the `Task` module is defined as part of Elixir's standard library (and even if it was possible, it is unlikely it would be a good idea). Luckily, this can be done by using `Supervisor.child_spec/2`, which allows us to configure a child specification with new values. Let's rewrite `start/2` in `KVServer.Application` once more: ```elixir From d332f262e7d2938be2657e0872c408eb01ab8d52 Mon Sep 17 00:00:00 2001 From: Dino Date: Fri, 20 Oct 2023 06:01:38 +0100 Subject: [PATCH 0065/1886] Fix IEx doc printing for headings with newline characters (#13018) Update `IEx.introspection.print_doc/5` to trasverse the list of headings and split each heading into multiple headings, in case the heading has any newline characters (`\n`). This fixes an issue where using the `h` macro with some functions ended up with a broken output for the function signature, as the padding done by `IO.ANSI.Docs.print_headings/2` does not take into consideration the fact the the headings can contain newline characters, and thus end up in multiple lines. --- lib/elixir/lib/io/ansi/docs.ex | 4 ++++ lib/elixir/test/elixir/io/ansi/docs_test.exs | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/lib/elixir/lib/io/ansi/docs.ex b/lib/elixir/lib/io/ansi/docs.ex index e0e72a1f48..d0be03e380 100644 --- a/lib/elixir/lib/io/ansi/docs.ex +++ b/lib/elixir/lib/io/ansi/docs.ex @@ -50,6 +50,10 @@ defmodule IO.ANSI.Docs do """ @spec print_headings([String.t()], keyword) :: :ok def print_headings(headings, options \\ []) do + # It's possible for some of the headings to contain newline characters (`\n`), so in order to prevent it from + # breaking the output from `print_headings/2`, as `print_headings/2` tries to pad the whole heading, we first split + # any heading containgin newline characters into multiple headings, that way each one is padded on its own. + headings = Enum.flat_map(headings, fn heading -> String.split(heading, "\n") end) options = Keyword.merge(default_options(), options) newline_after_block(options) width = options[:width] diff --git a/lib/elixir/test/elixir/io/ansi/docs_test.exs b/lib/elixir/test/elixir/io/ansi/docs_test.exs index cce77a81fd..9a6ac082d3 100644 --- a/lib/elixir/test/elixir/io/ansi/docs_test.exs +++ b/lib/elixir/test/elixir/io/ansi/docs_test.exs @@ -33,6 +33,14 @@ defmodule IO.ANSI.DocsTest do assert String.contains?(result, " foo ") assert String.contains?(result, " bar ") end + + test "is correctly formatted when newline character is present" do + result = format_headings(["foo\nbar"]) + assert :binary.matches(result, "\e[0m\n\e[7m\e[33m") |> length == 2 + assert ["\e[0m", foo_line, bar_line, "\e[0m"] = String.split(result, "\n") + assert Regex.match?(~r/\e\[7m\e\[33m +foo +\e\[0m/, foo_line) + assert Regex.match?(~r/\e\[7m\e\[33m +bar +\e\[0m/, bar_line) + end end describe "metadata" do From 4b85aa2cec132e9f0cfd23ef5f48a9ca4f792a16 Mon Sep 17 00:00:00 2001 From: Artem Solomatin Date: Sat, 21 Oct 2023 06:56:23 +0300 Subject: [PATCH 0066/1886] Add AST link to quote-and-unquote guide (#13026) --- lib/elixir/pages/meta-programming/quote-and-unquote.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/pages/meta-programming/quote-and-unquote.md b/lib/elixir/pages/meta-programming/quote-and-unquote.md index 6690b08024..f91dd8dcbb 100644 --- a/lib/elixir/pages/meta-programming/quote-and-unquote.md +++ b/lib/elixir/pages/meta-programming/quote-and-unquote.md @@ -40,7 +40,7 @@ iex> quote do: x {:x, [], Elixir} ``` -When quoting more complex expressions, we can see that the code is represented in such tuples, which are often nested inside each other in a structure resembling a tree. Many languages would call such representations an *Abstract Syntax Tree* (AST). Elixir calls them *quoted expressions*: +When quoting more complex expressions, we can see that the code is represented in such tuples, which are often nested inside each other in a structure resembling a tree. Many languages would call such representations an [*Abstract Syntax Tree*](https://en.wikipedia.org/wiki/Abstract_syntax_tree) (AST). Elixir calls them *quoted expressions*: ```elixir iex> quote do: sum(1, 2 + 3, 4) From 92d46d0069906f8ed0ccc709e40e21e2acac68c1 Mon Sep 17 00:00:00 2001 From: bo0tzz Date: Sat, 21 Oct 2023 22:43:22 +0200 Subject: [PATCH 0067/1886] Fix typos in anti-patterns documentation (#13027) --- lib/elixir/pages/anti-patterns/code-anti-patterns.md | 8 ++++---- lib/elixir/pages/anti-patterns/design-anti-patterns.md | 8 ++++---- lib/elixir/pages/anti-patterns/process-anti-patterns.md | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/elixir/pages/anti-patterns/code-anti-patterns.md b/lib/elixir/pages/anti-patterns/code-anti-patterns.md index b29858f8d9..39deca2d93 100644 --- a/lib/elixir/pages/anti-patterns/code-anti-patterns.md +++ b/lib/elixir/pages/anti-patterns/code-anti-patterns.md @@ -110,7 +110,7 @@ def get_customer(customer_id) do end ``` -Both `http_customer_to_struct(customer_id, body)` and `http_error(status, body)` above contains the previous branches refactored into private functions. +Both `http_customer_to_struct(customer_id, body)` and `http_error(status, body)` above contain the previous branches refactored into private functions. It is worth noting that this refactoring is trivial to perform in Elixir because clauses cannot define variables or otherwise affect their parent scope. Therefore, extracting any clause or branch to a private function is a matter of gathering all variables used in that branch and passing them as arguments to the new function. @@ -118,7 +118,7 @@ It is worth noting that this refactoring is trivial to perform in Elixir because #### Problem -This anti-pattern refers to `with` statements that flatten all its error clauses into a single complex `else` block. This situation is harmful to the code readability and maintainability because difficult to know from which clause the error value came. +This anti-pattern refers to `with` statements that flatten all its error clauses into a single complex `else` block. This situation is harmful to the code readability and maintainability because it's difficult to know from which clause the error value came. #### Example @@ -136,7 +136,7 @@ def open_decoded_file(path) do end ``` -In the code above, it is unclear how each pattern on the left side of `<-` relates to their error at the end. The more patterns in a `with`, the less clear the code gets, and the more likely unrelated failures will overlap each other. +In the code above, it is unclear how each pattern on the left side of `<-` relates to their error at the end. The more patterns in a `with`, the less clear the code gets, and the more likely it is that unrelated failures will overlap each other. #### Refactoring @@ -302,7 +302,7 @@ However, keep in mind using a module attribute or defining the atoms in the modu #### Problem -In a functional language like Elixir, functions tend to explicitly receive all inputs and return all relevant outputs, instead of relying on mutations or side-effects. As functions grow in complexity, the amount of arguments (parameters) they need to work with may grow, to a point the function's interface becomes confusing and prone to errors during use. +In a functional language like Elixir, functions tend to explicitly receive all inputs and return all relevant outputs, instead of relying on mutations or side-effects. As functions grow in complexity, the amount of arguments (parameters) they need to work with may grow, to a point where the function's interface becomes confusing and prone to errors during use. #### Example diff --git a/lib/elixir/pages/anti-patterns/design-anti-patterns.md b/lib/elixir/pages/anti-patterns/design-anti-patterns.md index c60d4df712..1e533a06a9 100644 --- a/lib/elixir/pages/anti-patterns/design-anti-patterns.md +++ b/lib/elixir/pages/anti-patterns/design-anti-patterns.md @@ -7,7 +7,7 @@ play within a codebase. #### Problem -This anti-pattern refers to functions that receive options (typically as a *keyword list*) parameters that drastically change their return type. Because options are optional and sometimes set dynamically, if they also change the return type, it may be hard to understand what the function actually returns. +This anti-pattern refers to functions that receive options (typically as a *keyword list* parameter) that drastically change their return type. Because options are optional and sometimes set dynamically, if they also change the return type, it may be hard to understand what the function actually returns. #### Example @@ -170,7 +170,7 @@ end This is only possible because the `File` module provides APIs for reading files with tuples as results (`File.read/1`), as well as a version that raises an exception (`File.read!/1`). The bang (exclamation point) is effectively part of [Elixir's naming conventions](naming-conventions.html#trailing-bang-foo). -Library authors are encouraged to follow the same practices. In practice, the bang variant is implemented on top of the non-raising version of the code. For example, `File.read/1` is implemented as: +Library authors are encouraged to follow the same practices. In practice, the bang variant is implemented on top of the non-raising version of the code. For example, `File.read!/1` is implemented as: ```elixir def read!(path) do @@ -184,7 +184,7 @@ def read!(path) do end ``` -A common practice followed by the community is to make the non-raising version to return `{:ok, result}` or `{:error, Exception.t}`. For example, an HTTP client may return `{:ok, %HTTP.Response{}}` on success cases and a `{:error, %HTTP.Error{}}` for failures, where `HTTP.Error` is [implemented as an exception](`Kernel.defexception/1`). This makes it convenient for anyone to raise an exception by simply calling `Kernel.raise/1`. +A common practice followed by the community is to make the non-raising version return `{:ok, result}` or `{:error, Exception.t}`. For example, an HTTP client may return `{:ok, %HTTP.Response{}}` on success cases and `{:error, %HTTP.Error{}}` for failures, where `HTTP.Error` is [implemented as an exception](`Kernel.defexception/1`). This makes it convenient for anyone to raise an exception by simply calling `Kernel.raise/1`. ## Feature envy @@ -280,7 +280,7 @@ end #### Problem -This anti-pattern refers to a function that does not validate its parameters and propagates it to other functions, which can produce internal unexpected behavior. When an error is raised inside a function due to an invalid parameter value, it can be confusing for developers and make it harder to locate and fix the error. +This anti-pattern refers to a function that does not validate its parameters and propagates them to other functions, which can produce internal unexpected behavior. When an error is raised inside a function due to an invalid parameter value, it can be confusing for developers and make it harder to locate and fix the error. #### Example diff --git a/lib/elixir/pages/anti-patterns/process-anti-patterns.md b/lib/elixir/pages/anti-patterns/process-anti-patterns.md index 03a5dddcd1..5d83d8f32f 100644 --- a/lib/elixir/pages/anti-patterns/process-anti-patterns.md +++ b/lib/elixir/pages/anti-patterns/process-anti-patterns.md @@ -10,7 +10,7 @@ This anti-pattern refers to code that is unnecessarily organized by processes. A #### Example -An example of this anti-pattern, as shown below, is a module that implements arithmetic operations (like `add` and `subtract`) by means of a `GenSever` process. If the number of calls to this single process grows, this code organization can compromise the system performance, therefore becoming a bottleneck. +An example of this anti-pattern, as shown below, is a module that implements arithmetic operations (like `add` and `subtract`) by means of a `GenServer` process. If the number of calls to this single process grows, this code organization can compromise the system performance, therefore becoming a bottleneck. ```elixir defmodule Calculator do From e94ff76527699aef77d7ada7f362f4e7cb01df4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 23 Oct 2023 10:59:58 +0200 Subject: [PATCH 0068/1886] Avoid traversing the file system twice on parallel checker Closes #13024. --- lib/elixir/lib/module/parallel_checker.ex | 49 +++++++++++++---------- lib/mix/test/mix/tasks/xref_test.exs | 17 -------- 2 files changed, 28 insertions(+), 38 deletions(-) diff --git a/lib/elixir/lib/module/parallel_checker.ex b/lib/elixir/lib/module/parallel_checker.ex index 27f2ee00a6..fa16a310b3 100644 --- a/lib/elixir/lib/module/parallel_checker.ex +++ b/lib/elixir/lib/module/parallel_checker.ex @@ -356,29 +356,36 @@ defmodule Module.ParallelChecker do defp cache_module({server, ets}, module) do if lock(server, module) do - cache_from_chunk(ets, module) || cache_from_info(ets, module) - unlock(server, module) - end - end + object_code = :code.get_object_code(module) - defp cache_from_chunk(ets, module) do - with {^module, binary, _filename} <- :code.get_object_code(module), - {:ok, {^module, [{~c"ExCk", chunk}]}} <- :beam_lib.chunks(binary, [~c"ExCk"]), - {:elixir_checker_v1, contents} <- :erlang.binary_to_term(chunk) do - cache_chunk(ets, module, contents.exports) - true - else - _ -> false - end - end + # The chunk has more information, so that's our preference + with {^module, binary, _filename} <- object_code, + {:ok, {^module, [{~c"ExCk", chunk}]}} <- :beam_lib.chunks(binary, [~c"ExCk"]), + {:elixir_checker_v1, contents} <- :erlang.binary_to_term(chunk) do + cache_chunk(ets, module, contents.exports) + else + _ -> + # Otherwise, if the module is loaded, use its info + case :erlang.module_loaded(module) do + true -> + {mode, exports} = info_exports(module) + deprecated = info_deprecated(module) + cache_info(ets, module, exports, deprecated, mode) + + false -> + # Or load exports from chunk + with {^module, binary, _filename} <- object_code, + {:ok, {^module, [exports: exports]}} <- :beam_lib.chunks(binary, [:exports]) do + exports = Map.new(Enum.map(exports, &{&1, :def})) + cache_info(ets, module, exports, %{}, :erlang) + else + _ -> + :ets.insert(ets, {{:cached, module}, false}) + end + end + end - defp cache_from_info(ets, module) do - if Code.ensure_loaded?(module) do - {mode, exports} = info_exports(module) - deprecated = info_deprecated(module) - cache_info(ets, module, exports, deprecated, mode) - else - :ets.insert(ets, {{:cached, module}, false}) + unlock(server, module) end end diff --git a/lib/mix/test/mix/tasks/xref_test.exs b/lib/mix/test/mix/tasks/xref_test.exs index fa6b0de9ea..aa1d2b1ccb 100644 --- a/lib/mix/test/mix/tasks/xref_test.exs +++ b/lib/mix/test/mix/tasks/xref_test.exs @@ -69,23 +69,6 @@ defmodule Mix.Tasks.XrefTest do assert_all_calls(files, output) end - test "returns empty on cover compiled modules" do - files = %{ - "lib/a.ex" => """ - defmodule A do - def a, do: A.a() - end - """ - } - - assert_all_calls(files, [], fn -> - :cover.start() - :cover.compile_beam_directory(to_charlist(Mix.Project.compile_path())) - end) - after - :cover.stop() - end - defp assert_all_calls(files, expected, after_compile \\ fn -> :ok end) do in_fixture("no_mixfile", fn -> generate_files(files) From 09946ee77677cbac02a9ef670f88bf38f48f7eaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 23 Oct 2023 20:12:18 +0200 Subject: [PATCH 0069/1886] Include column information on definition metadata Closes #13029. --- lib/elixir/src/elixir_def.erl | 9 +++++++-- lib/elixir/test/elixir/module_test.exs | 6 ++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/lib/elixir/src/elixir_def.erl b/lib/elixir/src/elixir_def.erl index 8d01880351..a642a64353 100644 --- a/lib/elixir/src/elixir_def.erl +++ b/lib/elixir/src/elixir_def.erl @@ -148,11 +148,16 @@ store_definition(Kind, HasNoUnquote, Call, Body, #{line := Line} = E) -> %% extract meta information like file and context. {_, Meta, _} = Call, - Context = case lists:keyfind(context, 1, Meta) of - {context, _} = ContextPair -> [ContextPair]; + Column = case lists:keyfind(column, 1, Meta) of + {column, _} = ColumnPair -> [ColumnPair]; _ -> [] end, + Context = case lists:keyfind(context, 1, Meta) of + {context, _} = ContextPair -> [ContextPair | Column]; + _ -> Column + end, + Generated = case lists:keyfind(generated, 1, Meta) of {generated, true} -> ?generated(Context); _ -> Context diff --git a/lib/elixir/test/elixir/module_test.exs b/lib/elixir/test/elixir/module_test.exs index c6a7a43b03..1c0f31e608 100644 --- a/lib/elixir/test/elixir/module_test.exs +++ b/lib/elixir/test/elixir/module_test.exs @@ -523,12 +523,14 @@ defmodule ModuleTest do in_module do def foo(a, b), do: a + b - assert {:v1, :def, _, + assert {:v1, :def, def_meta, [ - {_, [{:a, _, nil}, {:b, _, nil}], [], + {clause_meta, [{:a, _, nil}, {:b, _, nil}], [], {{:., _, [:erlang, :+]}, _, [{:a, _, nil}, {:b, _, nil}]}} ]} = Module.get_definition(__MODULE__, {:foo, 2}) + assert [line: _, column: _] = Keyword.take(def_meta, [:line, :column]) + assert [line: _, column: _] = Keyword.take(clause_meta, [:line, :column]) assert {:v1, :def, _, []} = Module.get_definition(__MODULE__, {:foo, 2}, skip_clauses: true) assert Module.delete_definition(__MODULE__, {:foo, 2}) From c35d002dc2454e8840459cfe2733c7cb3e7a4302 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 24 Oct 2023 00:36:24 +0200 Subject: [PATCH 0070/1886] Fix column information in tests --- lib/elixir/src/elixir_def.erl | 24 +++++------ lib/elixir/test/elixir/kernel/errors_test.exs | 18 ++++---- .../test/elixir/kernel/warning_test.exs | 42 +++++++++---------- 3 files changed, 40 insertions(+), 44 deletions(-) diff --git a/lib/elixir/src/elixir_def.erl b/lib/elixir/src/elixir_def.erl index a642a64353..99e9794ba4 100644 --- a/lib/elixir/src/elixir_def.erl +++ b/lib/elixir/src/elixir_def.erl @@ -138,29 +138,25 @@ store_definition(Kind, Key, Pos) -> store_definition(Kind, HasNoUnquote, Call, Body, #{line := Line} = E) -> {NameAndArgs, Guards} = elixir_utils:extract_guards(Call), - {Name, Args} = case NameAndArgs of - {N, _, A} when is_atom(N), is_atom(A) -> {N, []}; - {N, _, A} when is_atom(N), is_list(A) -> {N, A}; + {Name, Meta, Args} = case NameAndArgs of + {N, M, A} when is_atom(N), is_atom(A) -> {N, M, []}; + {N, M, A} when is_atom(N), is_list(A) -> {N, M, A}; _ -> elixir_errors:file_error([{line, Line}], E, ?MODULE, {invalid_def, Kind, NameAndArgs}) end, - %% Now that we have verified the call format, - %% extract meta information like file and context. - {_, Meta, _} = Call, - - Column = case lists:keyfind(column, 1, Meta) of - {column, _} = ColumnPair -> [ColumnPair]; + Context = case lists:keyfind(context, 1, Meta) of + {context, _} = ContextPair -> [ContextPair]; _ -> [] end, - Context = case lists:keyfind(context, 1, Meta) of - {context, _} = ContextPair -> [ContextPair | Column]; - _ -> Column + Column = case lists:keyfind(column, 1, Meta) of + {column, _} = ColumnPair -> [ColumnPair | Context]; + _ -> Context end, Generated = case lists:keyfind(generated, 1, Meta) of - {generated, true} -> ?generated(Context); - _ -> Context + {generated, true} = GeneratedPair -> [GeneratedPair | Column]; + _ -> Column end, CheckClauses = if diff --git a/lib/elixir/test/elixir/kernel/errors_test.exs b/lib/elixir/test/elixir/kernel/errors_test.exs index 8efde8486c..ac8a3ad93e 100644 --- a/lib/elixir/test/elixir/kernel/errors_test.exs +++ b/lib/elixir/test/elixir/kernel/errors_test.exs @@ -146,7 +146,7 @@ defmodule Kernel.ErrorsTest do test "function without definition" do assert_compile_error( - ["nofile:2: ", "implementation not provided for predefined def foo/0"], + ["nofile:2:7: ", "implementation not provided for predefined def foo/0"], ~c""" defmodule Kernel.ErrorsTest.FunctionWithoutDefition do def foo @@ -174,7 +174,7 @@ defmodule Kernel.ErrorsTest do test "guard without definition" do assert_compile_error( - ["nofile:2: ", "implementation not provided for predefined defmacro foo/1"], + ["nofile:2:12: ", "implementation not provided for predefined defmacro foo/1"], ~c""" defmodule Kernel.ErrorsTest.GuardWithoutDefition do defguard foo(bar) @@ -505,7 +505,7 @@ defmodule Kernel.ErrorsTest do test "function local conflict" do assert_compile_error( - ["nofile:3: ", "imported Kernel.&&/2 conflicts with local function"], + ["nofile:3:9: ", "imported Kernel.&&/2 conflicts with local function"], ~c""" defmodule Kernel.ErrorsTest.FunctionLocalConflict do def other, do: 1 && 2 @@ -611,7 +611,7 @@ defmodule Kernel.ErrorsTest do test "function definition with alias" do assert_compile_error( [ - "nofile:2\n", + "nofile:2:7\n", "function names should start with lowercase characters or underscore, invalid name Bar" ], ~c""" @@ -670,7 +670,7 @@ defmodule Kernel.ErrorsTest do test "def defmacro clause change" do assert_compile_error( - ["nofile:3\n", "defmacro foo/1 already defined as def in nofile:2"], + ["nofile:3:12\n", "defmacro foo/1 already defined as def in nofile:2"], ~c""" defmodule Kernel.ErrorsTest.DefDefmacroClauseChange do def foo(1), do: 1 @@ -692,7 +692,7 @@ defmodule Kernel.ErrorsTest do test "internal function overridden" do assert_compile_error( - ["nofile:2\n", "cannot define def __info__/1 as it is automatically defined by Elixir"], + ["nofile:2:7\n", "cannot define def __info__/1 as it is automatically defined by Elixir"], ~c""" defmodule Kernel.ErrorsTest.InternalFunctionOverridden do def __info__(_), do: [] @@ -858,13 +858,13 @@ defmodule Kernel.ErrorsTest do end test "function head with guard" do - assert_compile_error(["nofile:2: ", "missing :do option in \"def\""], ~c""" + assert_compile_error(["nofile:2:7: ", "missing :do option in \"def\""], ~c""" defmodule Kernel.ErrorsTest.BodyessFunctionWithGuard do def foo(n) when is_number(n) end """) - assert_compile_error(["nofile:2: ", "missing :do option in \"def\""], ~c""" + assert_compile_error(["nofile:2:7: ", "missing :do option in \"def\""], ~c""" defmodule Kernel.ErrorsTest.BodyessFunctionWithGuard do def foo(n) when is_number(n), true end @@ -873,7 +873,7 @@ defmodule Kernel.ErrorsTest do test "invalid args for function head" do assert_compile_error( - ["nofile:2: ", "only variables and \\\\ are allowed as arguments in function head."], + ["nofile:2:7: ", "only variables and \\\\ are allowed as arguments in function head."], ~c""" defmodule Kernel.ErrorsTest.InvalidArgsForBodylessClause do def foo(nil) diff --git a/lib/elixir/test/elixir/kernel/warning_test.exs b/lib/elixir/test/elixir/kernel/warning_test.exs index 7cba3fabd6..d91cf99a10 100644 --- a/lib/elixir/test/elixir/kernel/warning_test.exs +++ b/lib/elixir/test/elixir/kernel/warning_test.exs @@ -601,7 +601,7 @@ defmodule Kernel.WarningTest do test "unused function" do assert_warn_eval( - ["nofile:2: ", "function hello/0 is unused\n"], + ["nofile:2:8: ", "function hello/0 is unused\n"], """ defmodule Sample1 do defp hello, do: nil @@ -610,7 +610,7 @@ defmodule Kernel.WarningTest do ) assert_warn_eval( - ["nofile:2: ", "function hello/1 is unused\n"], + ["nofile:2:8: ", "function hello/1 is unused\n"], """ defmodule Sample2 do defp hello(0), do: hello(1) @@ -620,7 +620,7 @@ defmodule Kernel.WarningTest do ) assert_warn_eval( - ["nofile:4: ", "function c/2 is unused\n"], + ["nofile:4:8: ", "function c/2 is unused\n"], ~S""" defmodule Sample3 do def a, do: nil @@ -632,7 +632,7 @@ defmodule Kernel.WarningTest do ) assert_warn_eval( - ["nofile:3: ", "function b/2 is unused\n"], + ["nofile:3:8: ", "function b/2 is unused\n"], ~S""" defmodule Sample4 do def a, do: nil @@ -643,7 +643,7 @@ defmodule Kernel.WarningTest do ) assert_warn_eval( - ["nofile:3: ", "function b/0 is unused\n"], + ["nofile:3:8: ", "function b/0 is unused\n"], ~S""" defmodule Sample5 do def a, do: nil @@ -658,9 +658,9 @@ defmodule Kernel.WarningTest do test "unused cyclic functions" do assert_warn_eval( [ - "nofile:2: ", + "nofile:2:8: ", "function a/0 is unused\n", - "nofile:3: ", + "nofile:3:8: ", "function b/0 is unused\n" ], """ @@ -676,7 +676,7 @@ defmodule Kernel.WarningTest do test "unused macro" do assert_warn_eval( - ["nofile:2: ", "macro hello/0 is unused"], + ["nofile:2:13: ", "macro hello/0 is unused"], """ defmodule Sample do defmacrop hello, do: nil @@ -685,7 +685,7 @@ defmodule Kernel.WarningTest do ) assert_warn_eval( - ["nofile:2: ", "macro hello/0 is unused\n"], + ["nofile:2:13: ", "macro hello/0 is unused\n"], ~S""" defmodule Sample2 do defmacrop hello do @@ -715,7 +715,7 @@ defmodule Kernel.WarningTest do test "unused default args" do assert_warn_eval( - ["nofile:3: ", "default values for the optional arguments in b/3 are never used"], + ["nofile:3:8: ", "default values for the optional arguments in b/3 are never used"], ~S""" defmodule Sample1 do def a, do: b(1, 2, 3) @@ -726,7 +726,7 @@ defmodule Kernel.WarningTest do assert_warn_eval( [ - "nofile:3: ", + "nofile:3:8: ", "the default value for the last optional argument in b/3 is never used" ], ~S""" @@ -739,7 +739,7 @@ defmodule Kernel.WarningTest do assert_warn_eval( [ - "nofile:3: ", + "nofile:3:8: ", "the default values for the last 2 optional arguments in b/4 are never used" ], ~S""" @@ -758,7 +758,7 @@ defmodule Kernel.WarningTest do """) == "" assert_warn_eval( - ["nofile:3: ", "the default value for the last optional argument in b/3 is never used"], + ["nofile:3:8: ", "the default value for the last optional argument in b/3 is never used"], ~S""" defmodule Sample5 do def a, do: b(1, 2) @@ -992,7 +992,7 @@ defmodule Kernel.WarningTest do test "late function heads" do assert_warn_eval( [ - "nofile:4\n", + "nofile:4:7\n", "function head for def add/2 must come at the top of its direct implementation" ], """ @@ -1117,7 +1117,7 @@ defmodule Kernel.WarningTest do ) assert_warn_eval( - ["nofile:3\n", message], + ["nofile:3:7\n", message], ~S""" defmodule Sample2 do def hello(_arg) @@ -1145,7 +1145,7 @@ defmodule Kernel.WarningTest do test "unused with local with overridable" do assert_warn_eval( - ["nofile:3: ", "function world/0 is unused"], + ["nofile:3:8: ", "function world/0 is unused"], """ defmodule Sample do def hello, do: world() @@ -1516,7 +1516,7 @@ defmodule Kernel.WarningTest do test "ungrouped def name" do assert_warn_eval( [ - "nofile:4\n", + "nofile:4:7\n", "clauses with the same name should be grouped together, \"def foo/2\" was previously defined (nofile:2)" ], """ @@ -1534,7 +1534,7 @@ defmodule Kernel.WarningTest do test "ungrouped def name and arity" do assert_warn_eval( [ - "nofile:4\n", + "nofile:4:7\n", "clauses with the same name and arity (number of arguments) should be grouped together, \"def foo/2\" was previously defined (nofile:2)" ], """ @@ -2006,7 +2006,7 @@ defmodule Kernel.WarningTest do test "catch comes before rescue in def" do assert_warn_eval( - ["nofile:2\n", ~s("catch" should always come after "rescue" in def)], + ["nofile:2:7\n", ~s("catch" should always come after "rescue" in def)], """ defmodule Sample do def foo do @@ -2052,7 +2052,7 @@ defmodule Kernel.WarningTest do test "unused private guard" do assert_warn_eval( - ["nofile:2: ", "macro foo/2 is unused\n"], + ["nofile:2:13: ", "macro foo/2 is unused\n"], """ defmodule Sample do defguardp foo(bar, baz) when bar + baz @@ -2173,7 +2173,7 @@ defmodule Kernel.WarningTest do test "def warns if only clause is else" do assert_warn_compile( - ["nofile:2\n", "\"else\" shouldn't be used as the only clause in \"def\""], + ["nofile:2:7\n", "\"else\" shouldn't be used as the only clause in \"def\""], """ defmodule Sample do def foo do From 9581ba791a9f0c7a8d0773d38a28a8ee35356121 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 24 Oct 2023 00:50:58 +0200 Subject: [PATCH 0071/1886] Annotate function definition and not when guard --- lib/elixir/src/elixir_quote.erl | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/lib/elixir/src/elixir_quote.erl b/lib/elixir/src/elixir_quote.erl index 730dd0e252..62d6b805f7 100644 --- a/lib/elixir/src/elixir_quote.erl +++ b/lib/elixir/src/elixir_quote.erl @@ -576,12 +576,18 @@ keynew(Key, Meta, Value) -> %% expressions, so we need to clean up the forms to %% allow them to get a new counter on the next expansion. -annotate({Def, Meta, [{H, M, A} | T]}, Context) when ?defs(Def) -> - {Def, Meta, [{H, keystore(context, M, Context), A} | T]}; -annotate({{'.', _, [_, Def]} = Target, Meta, [{H, M, A} | T]}, Context) when ?defs(Def) -> - {Target, Meta, [{H, keystore(context, M, Context), A} | T]}; - +annotate({Def, Meta, [H | T]}, Context) when ?defs(Def) -> + {Def, Meta, [annotate_def(H, Context) | T]}; +annotate({{'.', _, [_, Def]} = Target, Meta, [H | T]}, Context) when ?defs(Def) -> + {Target, Meta, [annotate_def(H, Context) | T]}; annotate({Lexical, Meta, [_ | _] = Args}, Context) when ?lexical(Lexical) -> NewMeta = keystore(context, keydelete(counter, Meta), Context), {Lexical, NewMeta, Args}; -annotate(Tree, _Context) -> Tree. \ No newline at end of file +annotate(Tree, _Context) -> Tree. + +annotate_def({'when', Meta, [Left, Right]}, Context) -> + {'when', Meta, [annotate_def(Left, Context), Right]}; +annotate_def({Fun, Meta, Args}, Context) -> + {Fun, keystore(context, Meta, Context), Args}; +annotate_def(Other, _Context) -> + Other. \ No newline at end of file From af1e2b89f480bb22e916b575a5c511d64cf71e3b Mon Sep 17 00:00:00 2001 From: teiko <111567918+teiko42@users.noreply.github.com> Date: Tue, 24 Oct 2023 16:12:29 +0200 Subject: [PATCH 0072/1886] Fix typos in DateTime.add/4 docs (#13031) --- lib/elixir/lib/calendar/datetime.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/elixir/lib/calendar/datetime.ex b/lib/elixir/lib/calendar/datetime.ex index 2d70e3d1d8..ce15683ac3 100644 --- a/lib/elixir/lib/calendar/datetime.ex +++ b/lib/elixir/lib/calendar/datetime.ex @@ -1557,10 +1557,10 @@ defmodule DateTime do `t:System.time_unit/0`. It defaults to `:second`. Negative values will move backwards in time. - This function always consider the unit to be computed according + This function always considers the unit to be computed according to the `Calendar.ISO`. - This function uses relies on a contiguous representation of time, + This function relies on a contiguous representation of time, ignoring the wall time and timezone changes. For example, if you add one day when there are summer time/daylight saving time changes, it will also change the time forward or backward by one hour, From af9c0aaeb6044a23e568d7adeb9317097f7ff078 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aziz=20K=C3=B6ksal?= Date: Tue, 24 Oct 2023 20:11:48 +0200 Subject: [PATCH 0073/1886] Change the word "duplicated" to "duplicate" (#13008) --- lib/elixir/lib/enum.ex | 8 ++++---- lib/elixir/lib/kernel.ex | 2 +- lib/elixir/lib/map.ex | 2 +- lib/elixir/lib/option_parser.ex | 2 +- lib/elixir/lib/supervisor/spec.ex | 2 +- lib/elixir/src/elixir_clauses.erl | 2 +- .../test/elixir/kernel/expansion_test.exs | 18 +++++++++--------- 7 files changed, 18 insertions(+), 18 deletions(-) diff --git a/lib/elixir/lib/enum.ex b/lib/elixir/lib/enum.ex index eeb6737c59..fb489b7aae 100644 --- a/lib/elixir/lib/enum.ex +++ b/lib/elixir/lib/enum.ex @@ -815,11 +815,11 @@ defmodule Enum do @doc """ Enumerates the `enumerable`, returning a list where all consecutive - duplicated elements are collapsed to a single element. + duplicate elements are collapsed to a single element. Elements are compared using `===/2`. - If you want to remove all duplicated elements, regardless of order, + If you want to remove all duplicate elements, regardless of order, see `uniq/1`. ## Examples @@ -848,7 +848,7 @@ defmodule Enum do @doc """ Enumerates the `enumerable`, returning a list where all consecutive - duplicated elements are collapsed to a single element. + duplicate elements are collapsed to a single element. The function `fun` maps every element to a term which is used to determine if two elements are duplicates. @@ -3725,7 +3725,7 @@ defmodule Enum do def to_list(enumerable), do: reverse(enumerable) |> :lists.reverse() @doc """ - Enumerates the `enumerable`, removing all duplicated elements. + Enumerates the `enumerable`, removing all duplicate elements. ## Examples diff --git a/lib/elixir/lib/kernel.ex b/lib/elixir/lib/kernel.ex index 49ea98d19b..7b37d18163 100644 --- a/lib/elixir/lib/kernel.ex +++ b/lib/elixir/lib/kernel.ex @@ -2364,7 +2364,7 @@ defmodule Kernel do Keys in the `Enumerable` that don't exist in the struct are automatically discarded. Note that keys must be atoms, as only atoms are allowed when - defining a struct. If keys in the `Enumerable` are duplicated, the last + defining a struct. If there are duplicate keys in the `Enumerable`, the last entry will be taken (same behaviour as `Map.new/1`). This function is useful for dynamically creating and updating structs, as diff --git a/lib/elixir/lib/map.ex b/lib/elixir/lib/map.ex index e23c8a2d4a..8082b40168 100644 --- a/lib/elixir/lib/map.ex +++ b/lib/elixir/lib/map.ex @@ -14,7 +14,7 @@ defmodule Map do in the example above has a different order than the map that was created). Maps do not impose any restriction on the key type: anything can be a key in a - map. As a key-value structure, maps do not allow duplicated keys. Keys are + map. As a key-value structure, maps do not allow duplicate keys. Keys are compared using the exact-equality operator (`===/2`). If colliding keys are defined in a map literal, the last one prevails. diff --git a/lib/elixir/lib/option_parser.ex b/lib/elixir/lib/option_parser.ex index 32bd7acddb..b1006f8217 100644 --- a/lib/elixir/lib/option_parser.ex +++ b/lib/elixir/lib/option_parser.ex @@ -134,7 +134,7 @@ defmodule OptionParser do Switches can be specified with modifiers, which change how they behave. The following modifiers are supported: - * `:keep` - keeps duplicated elements instead of overriding them; + * `:keep` - keeps duplicate elements instead of overriding them; works with all types except `:count`. Specifying `switch_name: :keep` assumes the type of `:switch_name` will be `:string`. diff --git a/lib/elixir/lib/supervisor/spec.ex b/lib/elixir/lib/supervisor/spec.ex index 61c54d2b2f..f2a55a1570 100644 --- a/lib/elixir/lib/supervisor/spec.ex +++ b/lib/elixir/lib/supervisor/spec.ex @@ -194,7 +194,7 @@ defmodule Supervisor.Spec do defp assert_unique_ids([id | rest]) do if id in rest do raise ArgumentError, - "duplicated ID #{inspect(id)} found in the supervisor specification, " <> + "duplicate ID #{inspect(id)} found in the supervisor specification, " <> "please explicitly pass the :id option when defining this worker/supervisor" else assert_unique_ids(rest) diff --git a/lib/elixir/src/elixir_clauses.erl b/lib/elixir/src/elixir_clauses.erl index ae261d3ce1..179e2d7eb1 100644 --- a/lib/elixir/src/elixir_clauses.erl +++ b/lib/elixir/src/elixir_clauses.erl @@ -382,7 +382,7 @@ format_error({bad_or_missing_clauses, Kind}) -> io_lib:format("expected -> clauses in \"~ts\"", [Kind]); format_error({duplicated_clauses, Kind, Key}) -> - io_lib:format("duplicated :~ts clauses given for \"~ts\"", [Key, Kind]); + io_lib:format("duplicate :~ts clauses given for \"~ts\"", [Key, Kind]); format_error({unexpected_option, Kind, Option}) -> io_lib:format("unexpected option ~ts in \"~ts\"", ['Elixir.Macro':to_string(Option), Kind]); diff --git a/lib/elixir/test/elixir/kernel/expansion_test.exs b/lib/elixir/test/elixir/kernel/expansion_test.exs index eb14aa602c..6ee156d51e 100644 --- a/lib/elixir/test/elixir/kernel/expansion_test.exs +++ b/lib/elixir/test/elixir/kernel/expansion_test.exs @@ -1403,7 +1403,7 @@ defmodule Kernel.ExpansionTest do expand(quote(do: cond([]))) end) - assert_compile_error(~r"duplicated :do clauses given for \"cond\"", fn -> + assert_compile_error(~r"duplicate :do clauses given for \"cond\"", fn -> expand(quote(do: cond(do: (x -> x), do: (y -> y)))) end) end @@ -1575,7 +1575,7 @@ defmodule Kernel.ExpansionTest do expand(quote(do: case(e, []))) end) - assert_compile_error(~r"duplicated :do clauses given for \"case\"", fn -> + assert_compile_error(~r"duplicate :do clauses given for \"case\"", fn -> expand(quote(do: case(e, do: (x -> x), do: (y -> y)))) end) end @@ -1775,11 +1775,11 @@ defmodule Kernel.ExpansionTest do expand(quote(do: receive([]))) end) - assert_compile_error(~r"duplicated :do clauses given for \"receive\"", fn -> + assert_compile_error(~r"duplicate :do clauses given for \"receive\"", fn -> expand(quote(do: receive(do: (x -> x), do: (y -> y)))) end) - assert_compile_error(~r"duplicated :after clauses given for \"receive\"", fn -> + assert_compile_error(~r"duplicate :after clauses given for \"receive\"", fn -> code = quote do receive do @@ -2012,11 +2012,11 @@ defmodule Kernel.ExpansionTest do end test "expects at most one clause" do - assert_compile_error(~r"duplicated :do clauses given for \"try\"", fn -> + assert_compile_error(~r"duplicate :do clauses given for \"try\"", fn -> expand(quote(do: try(do: e, do: f))) end) - assert_compile_error(~r"duplicated :rescue clauses given for \"try\"", fn -> + assert_compile_error(~r"duplicate :rescue clauses given for \"try\"", fn -> code = quote do try do @@ -2031,7 +2031,7 @@ defmodule Kernel.ExpansionTest do expand(code) end) - assert_compile_error(~r"duplicated :after clauses given for \"try\"", fn -> + assert_compile_error(~r"duplicate :after clauses given for \"try\"", fn -> code = quote do try do @@ -2046,7 +2046,7 @@ defmodule Kernel.ExpansionTest do expand(code) end) - assert_compile_error(~r"duplicated :else clauses given for \"try\"", fn -> + assert_compile_error(~r"duplicate :else clauses given for \"try\"", fn -> code = quote do try do @@ -2061,7 +2061,7 @@ defmodule Kernel.ExpansionTest do expand(code) end) - assert_compile_error(~r"duplicated :catch clauses given for \"try\"", fn -> + assert_compile_error(~r"duplicate :catch clauses given for \"try\"", fn -> code = quote do try do From 0a7a41cc2c064b69b1698ce8b6146d165e399a2b Mon Sep 17 00:00:00 2001 From: Gilbert Bishop-White Date: Tue, 24 Oct 2023 19:34:51 +0100 Subject: [PATCH 0074/1886] Add example of inserting into Map that already has key (#13013) --- lib/elixir/lib/enum.ex | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/elixir/lib/enum.ex b/lib/elixir/lib/enum.ex index fb489b7aae..e34d665ca4 100644 --- a/lib/elixir/lib/enum.ex +++ b/lib/elixir/lib/enum.ex @@ -1493,6 +1493,9 @@ defmodule Enum do iex> Enum.into([a: 1, a: 2], %{}) %{a: 2} + iex> Enum.into([a: 2], %{a: 1, b: 3}) + %{a: 2, b: 3} + """ @spec into(Enumerable.t(), Collectable.t()) :: Collectable.t() def into(enumerable, collectable) From cbf4b8c85d05b0c59941b7fcb1d2d49f68c31a27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Samson?= Date: Wed, 25 Oct 2023 23:49:21 +0200 Subject: [PATCH 0075/1886] add missing :expr to surround_context typespec (#13034) --- lib/elixir/lib/code/fragment.ex | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/elixir/lib/code/fragment.ex b/lib/elixir/lib/code/fragment.ex index 89404f8d21..0b379489e6 100644 --- a/lib/elixir/lib/code/fragment.ex +++ b/lib/elixir/lib/code/fragment.ex @@ -597,7 +597,8 @@ defmodule Code.Fragment do | {:dot, inside_dot, charlist} | {:module_attribute, charlist} | {:unquoted_atom, charlist} - | {:var, charlist}, + | {:var, charlist} + | :expr, inside_alias: {:local_or_var, charlist} | {:module_attribute, charlist}, From 08f315016fbd2c4722c4a0695eb6b08574dcfecf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 25 Oct 2023 23:53:18 +0200 Subject: [PATCH 0076/1886] Add @nifs attributes --- lib/elixir/lib/module.ex | 23 ++++++- lib/elixir/src/elixir_module.erl | 66 +++++++++++-------- lib/elixir/test/elixir/kernel/errors_test.exs | 27 +++++++- 3 files changed, 85 insertions(+), 31 deletions(-) diff --git a/lib/elixir/lib/module.ex b/lib/elixir/lib/module.ex index 3df0a1402e..1ca92dd6b9 100644 --- a/lib/elixir/lib/module.ex +++ b/lib/elixir/lib/module.ex @@ -323,6 +323,12 @@ defmodule Module do Once this module is compiled, this information becomes available via the `Code.fetch_docs/1` function. + ### `@nifs` (since v1.16.0) + + A list of functions and their arities which will be overridden + by a native implementation (NIF). See the Erlang documentation + for more information: https://www.erlang.org/doc/man/erl_nif + ### `@on_definition` A hook that will be invoked when each function or macro in the current @@ -2209,6 +2215,16 @@ defmodule Module do end end + defp preprocess_attribute(:nifs, value) do + unless function_arity_list?(value) do + raise ArgumentError, + "@nifs is a built-in module attribute for specifying a list " <> + "of functions and their arities that are NIFs, got: #{inspect(value)}" + end + + value + end + defp preprocess_attribute(:dialyzer, value) do # From https://github.com/erlang/otp/blob/master/lib/stdlib/src/erl_lint.erl :lists.foreach( @@ -2229,12 +2245,17 @@ defmodule Module do defp valid_dialyzer_attribute?({key, fun_arities}) when is_atom(key) do (key == :nowarn_function or valid_dialyzer_attribute?(key)) and + function_arity_list?(List.wrap(fun_arities)) + end + + defp function_arity_list?(fun_arities) do + is_list(fun_arities) and :lists.all( fn {fun, arity} when is_atom(fun) and is_integer(arity) -> true _ -> false end, - List.wrap(fun_arities) + fun_arities ) end diff --git a/lib/elixir/src/elixir_module.erl b/lib/elixir/src/elixir_module.erl index ed87b3f520..1cee92e4a1 100644 --- a/lib/elixir/src/elixir_module.erl +++ b/lib/elixir/src/elixir_module.erl @@ -140,6 +140,9 @@ compile(Line, Module, ModuleAsCharlist, Block, Vars, Prune, E) -> DialyzerAttribute = lists:keyfind(dialyzer, 1, Attributes), validate_dialyzer_attribute(DialyzerAttribute, AllDefinitions, Line, E), + NifsAttribute = lists:keyfind(nifs, 1, Attributes), + validate_nifs_attribute(NifsAttribute, AllDefinitions, Line, E), + Unreachable = elixir_locals:warn_unused_local(Module, AllDefinitions, NewPrivate, E), elixir_locals:ensure_no_undefined_local(Module, AllDefinitions, E), elixir_locals:ensure_no_import_conflict(Module, AllDefinitions, E), @@ -229,8 +232,8 @@ validate_compile_opt({inline, Inlines}, Defs, Unreachable, Line, E) -> []; {ok, FilteredInlines} -> [{inline, FilteredInlines}]; - {error, Def} -> - elixir_errors:module_error([{line, Line}], E, ?MODULE, {bad_inline, Def}), + {error, Reason} -> + elixir_errors:module_error([{line, Line}], E, ?MODULE, Reason), [] end; validate_compile_opt(Opt, Defs, Unreachable, Line, E) when is_list(Opt) -> @@ -240,7 +243,10 @@ validate_compile_opt(Opt, _Defs, _Unreachable, _Line, _E) -> validate_inlines([Inline | Inlines], Defs, Unreachable, Acc) -> case lists:keyfind(Inline, 1, Defs) of - false -> {error, Inline}; + false -> + {error, {undefined_function, {compile, inline}, Inline}}; + {_Def, Type, _Meta, _Clauses} when Type == defmacro; Type == defmacrop -> + {error, {bad_macro, {compile, inline}, Inline}}; _ -> case lists:member(Inline, Unreachable) of true -> validate_inlines(Inlines, Defs, Unreachable, Acc); @@ -252,27 +258,36 @@ validate_inlines([], _Defs, _Unreachable, Acc) -> {ok, Acc}. validate_on_load_attribute({on_load, Def}, Defs, Private, Line, E) -> case lists:keyfind(Def, 1, Defs) of false -> - elixir_errors:module_error([{line, Line}], E, ?MODULE, {undefined_on_load, Def}), + elixir_errors:module_error([{line, Line}], E, ?MODULE, {undefined_function, on_load, Def}), + Private; + {_Def, Type, _Meta, _Clauses} when Type == defmacro; Type == defmacrop -> + elixir_errors:module_error([{line, Line}], E, ?MODULE, {bad_macro, on_load, Def}), Private; - {_, Kind, _, _} when Kind == def; Kind == defp -> - lists:keydelete(Def, 1, Private); - {_, WrongKind, _, _} -> - elixir_errors:module_error([{line, Line}], E, ?MODULE, {wrong_kind_on_load, Def, WrongKind}), - Private + _ -> + lists:keydelete(Def, 1, Private) end; validate_on_load_attribute(false, _Defs, Private, _Line, _E) -> Private. validate_dialyzer_attribute({dialyzer, Dialyzer}, Defs, Line, E) -> - [case lists:keyfind(Fun, 1, Defs) of + [validate_definition({dialyzer, Key}, Fun, Defs, Line, E) + || {Key, Funs} <- lists:flatten([Dialyzer]), Fun <- lists:flatten([Funs])]; +validate_dialyzer_attribute(false, _Defs, _Line, _E) -> + ok. + +validate_nifs_attribute({nifs, Funs}, Defs, Line, E) -> + [validate_definition(nifs, Fun, Defs, Line, E) || Fun <- lists:flatten([Funs])]; +validate_nifs_attribute(false, _Defs, _Line, _E) -> + ok. + +validate_definition(Key, Fun, Defs, Line, E) -> + case lists:keyfind(Fun, 1, Defs) of false -> - elixir_errors:module_error([{line, Line}], E, ?MODULE, {bad_dialyzer_no_def, Key, Fun}); + elixir_errors:module_error([{line, Line}], E, ?MODULE, {undefined_function, Key, Fun}); {Fun, Type, _Meta, _Clauses} when Type == defmacro; Type == defmacrop -> - elixir_errors:module_error([{line, Line}], E, ?MODULE, {bad_dialyzer_no_macro, Key, Fun}); + elixir_errors:module_error([{line, Line}], E, ?MODULE, {bad_macro, Key, Fun}); _ -> ok - end || {Key, Funs} <- lists:flatten([Dialyzer]), Fun <- lists:flatten([Funs])]; -validate_dialyzer_attribute(false, _Defs, _Line, _E) -> - ok. + end. defines_behaviour(DataBag) -> ets:member(DataBag, {accumulate, callback}) orelse ets:member(DataBag, {accumulate, macrocallback}). @@ -374,7 +389,7 @@ build(Module, Line, File, E) -> {?counter_attr, 0} ]), - Persisted = [behaviour, on_load, external_resource, dialyzer, vsn], + Persisted = [behaviour, dialyzer, external_resource, on_load, vsn, nifs], ets:insert(DataBag, [{persisted_attributes, Attr} || Attr <- Persisted]), OnDefinition = @@ -580,17 +595,14 @@ format_error({module_reserved, Module}) -> format_error({module_in_definition, Module, File, Line}) -> io_lib:format("cannot define module ~ts because it is currently being defined in ~ts:~B", [elixir_aliases:inspect(Module), elixir_utils:relative_to_cwd(File), Line]); -format_error({bad_inline, {Name, Arity}}) -> - io_lib:format("inlined function ~ts/~B undefined", [Name, Arity]); -format_error({bad_dialyzer_no_def, Key, {Name, Arity}}) -> - io_lib:format("undefined function ~ts/~B given to @dialyzer :~ts", [Name, Arity, Key]); -format_error({bad_dialyzer_no_macro, Key, {Name, Arity}}) -> - io_lib:format("macro ~ts/~B given to @dialyzer :~ts (@dialyzer only supports function annotations)", [Name, Arity, Key]); -format_error({undefined_on_load, {Name, Arity}}) -> - io_lib:format("undefined function ~ts/~B given to @on_load", [Name, Arity]); -format_error({wrong_kind_on_load, {Name, Arity}, WrongKind}) -> - io_lib:format("expected @on_load function ~ts/~B to be a function, got \"~ts\"", - [Name, Arity, WrongKind]); +format_error({undefined_function, {Attr, Key}, {Name, Arity}}) -> + io_lib:format("undefined function ~ts/~B given to @~ts :~ts", [Name, Arity, Attr, Key]); +format_error({undefined_function, Attr, {Name, Arity}}) -> + io_lib:format("undefined function ~ts/~B given to @~ts", [Name, Arity, Attr]); +format_error({bad_macro, {Attr, Key}, {Name, Arity}}) -> + io_lib:format("macro ~ts/~B given to @~ts :~ts (only functions are supported)", [Name, Arity, Attr, Key]); +format_error({bad_macro, Attr, {Name, Arity}}) -> + io_lib:format("macro ~ts/~B given to @~ts (only functions are supported)", [Name, Arity, Attr]); format_error({parse_transform, Module}) -> io_lib:format("@compile {:parse_transform, ~ts} is deprecated. Elixir will no longer support " "Erlang-based transforms in future versions", [elixir_aliases:inspect(Module)]). diff --git a/lib/elixir/test/elixir/kernel/errors_test.exs b/lib/elixir/test/elixir/kernel/errors_test.exs index ac8a3ad93e..4a54be931e 100644 --- a/lib/elixir/test/elixir/kernel/errors_test.exs +++ b/lib/elixir/test/elixir/kernel/errors_test.exs @@ -771,11 +771,32 @@ defmodule Kernel.ErrorsTest do ~c"Module.eval_quoted Record, quote(do: 1), [], file: __ENV__.file" end - test "@compile inline with undefined function" do + test "invalid @compile inline" do assert_compile_error( - ["nofile:1: ", "inlined function foo/1 undefined"], + ["nofile:1: ", "undefined function foo/1 given to @compile :inline"], ~c"defmodule Test do @compile {:inline, foo: 1} end" ) + + assert_compile_error( + ["nofile:1: ", "undefined function foo/1 given to @compile :inline"], + ~c"defmodule Test do @compile {:inline, foo: 1}; defmacro foo(_) end" + ) + end + + test "invalid @nifs attribute" do + assert_compile_error( + ["nofile:1: ", "undefined function foo/1 given to @nifs"], + ~c"defmodule Test do @nifs [foo: 1] end" + ) + + assert_compile_error( + ["nofile:1: ", "undefined function foo/1 given to @nifs"], + ~c"defmodule Test do @nifs [foo: 1]; defmacro foo(_) end" + ) + + assert_eval_raise ArgumentError, + ["@nifs is a built-in module attribute"], + ~c"defmodule Test do @nifs :not_an_option end" end test "invalid @dialyzer options" do @@ -825,7 +846,7 @@ defmodule Kernel.ErrorsTest do test "wrong kind for @on_load attribute" do assert_compile_error( - ["nofile:1: ", "expected @on_load function foo/0 to be a function, got \"defmacro\""], + ["nofile:1: ", "macro foo/0 given to @on_load"], ~c""" defmodule PrivateOnLoadFunction do @on_load :foo From 3781a4dc3ab7b1d3c23be42f2550dc60ee46117e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Samson?= Date: Thu, 26 Oct 2023 00:19:29 +0200 Subject: [PATCH 0077/1886] Fix crash in surround_context (#13033) --- lib/elixir/lib/code/fragment.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/lib/code/fragment.ex b/lib/elixir/lib/code/fragment.ex index 0b379489e6..ace8d46766 100644 --- a/lib/elixir/lib/code/fragment.ex +++ b/lib/elixir/lib/code/fragment.ex @@ -489,7 +489,7 @@ defmodule Code.Fragment do cond do Code.Identifier.unary_op(op) == :error and Code.Identifier.binary_op(op) == :error -> - :none + {:none, 0} match?([?. | rest] when rest == [] or hd(rest) != ?., rest) -> dot(tl(rest), dot_count + 1, acc) From 08cc3015b516422b5a8ed46323d26140c6b584ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 26 Oct 2023 00:29:50 +0200 Subject: [PATCH 0078/1886] Add a test to stab as not an operator --- lib/elixir/test/elixir/code_fragment_test.exs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/elixir/test/elixir/code_fragment_test.exs b/lib/elixir/test/elixir/code_fragment_test.exs index 328973b323..41ad84ef40 100644 --- a/lib/elixir/test/elixir/code_fragment_test.exs +++ b/lib/elixir/test/elixir/code_fragment_test.exs @@ -1050,6 +1050,9 @@ defmodule CodeFragmentTest do begin: {1, 5}, end: {1, 6} } + + # invalid + assert CF.surround_context("->", {1, 2}) == :none end test "sigil" do From 62759e4bac5548a64f3d8d76043fd4b6872e8531 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 26 Oct 2023 00:30:42 +0200 Subject: [PATCH 0079/1886] Reorder clauses --- lib/elixir/lib/module.ex | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/elixir/lib/module.ex b/lib/elixir/lib/module.ex index 1ca92dd6b9..3e23e85286 100644 --- a/lib/elixir/lib/module.ex +++ b/lib/elixir/lib/module.ex @@ -2243,11 +2243,6 @@ defmodule Module do value end - defp valid_dialyzer_attribute?({key, fun_arities}) when is_atom(key) do - (key == :nowarn_function or valid_dialyzer_attribute?(key)) and - function_arity_list?(List.wrap(fun_arities)) - end - defp function_arity_list?(fun_arities) do is_list(fun_arities) and :lists.all( @@ -2259,6 +2254,11 @@ defmodule Module do ) end + defp valid_dialyzer_attribute?({key, fun_arities}) when is_atom(key) do + (key == :nowarn_function or valid_dialyzer_attribute?(key)) and + function_arity_list?(List.wrap(fun_arities)) + end + defp valid_dialyzer_attribute?(attr) do :lists.member( attr, From 7cfe015564412617b64f2f10397dec07f71634a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 26 Oct 2023 00:34:53 +0200 Subject: [PATCH 0080/1886] Update CHANGELOG --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c4af4b21c5..c0df6048ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -78,7 +78,9 @@ Finally, we have started enriching our documentation with [Mermaid.js](https://m * [Code.Fragment] Handle anonymous calls in fragments * [Kernel] Suggest module names based on suffix and casing errors when the module does not exist in `UndefinedFunctionError` * [Kernel.ParallelCompiler] Introduce `Kernel.ParallelCompiler.pmap/2` to compile multiple additional entries in parallel + * [Kernel.SpecialForms] Warn if `True`/`False`/`Nil` are used as aliases and there is no such alias * [Macro] Add `Macro.compile_apply/4` + * [Module] Add support for `@nifs` annotation from Erlang/OTP 25 * [String] Update to Unicode 15.1.0 #### Mix @@ -89,10 +91,12 @@ Finally, we have started enriching our documentation with [Mermaid.js](https://m #### Elixir + * [Code.Fragment] Fix crash in `Code.Fragment.surround_context/2` when matching on `->` * [IO] Raise when using `IO.binwrite/2` on terminated device (mirroring `IO.write/2`) * [Kernel] Do not expand aliases recursively (the alias stored in Macro.Env is already expanded) * [Kernel] Ensure `dbg` module is a compile-time dependency * [Kernel] Warn when a private function or macro uses `unquote/1` and the function/macro itself is unused + * [Kernel] Do not define an alias for nested modules starting with `Elixir.` in their definition * [Kernel.ParallelCompiler] Consider a module has been defined in `@after_compile` callbacks to avoid deadlocks * [Path] Ensure `Path.relative_to/2` returns a relative path when the given argument does not share a common prefix with `cwd` From 35619fd892d085462b85ea14d4f2718fd499bc9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 26 Oct 2023 09:25:22 +0200 Subject: [PATCH 0081/1886] Clarify "known keys" and struct usage --- lib/elixir/pages/anti-patterns/code-anti-patterns.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/elixir/pages/anti-patterns/code-anti-patterns.md b/lib/elixir/pages/anti-patterns/code-anti-patterns.md index 39deca2d93..eba9c2740f 100644 --- a/lib/elixir/pages/anti-patterns/code-anti-patterns.md +++ b/lib/elixir/pages/anti-patterns/code-anti-patterns.md @@ -375,7 +375,7 @@ There are few known exceptions to this anti-pattern: #### Problem -In Elixir, it is possible to access values from `Map`s, which are key-value data structures, either statically or dynamically. When the keys are known upfront, they must be accessed using the `map.key` notation, which asserts the key exists. If `map[:key]` is used and the informed key does not exist, `nil` is returned. This return can be confusing and does not allow developers to conclude whether the key is non-existent in the map or just has a bound `nil` value. In this way, this anti-pattern may cause bugs in the code. +In Elixir, it is possible to access values from `Map`s, which are key-value data structures, either statically or dynamically. When a key is expected to exist in the map, it must be accessed using the `map.key` notation, which asserts the key exists. If `map[:key]` is used and the informed key does not exist, `nil` is returned. This return can be confusing and does not allow developers to conclude whether the key is non-existent in the map or just has a bound `nil` value. In this way, this anti-pattern may cause bugs in the code. #### Example @@ -407,7 +407,7 @@ As can be seen in the example above, even when the key `:z` does not exist in th #### Refactoring -To remove this anti-pattern, whenever accessing a known key of `Atom` type, replace the dynamic `map[:key]` syntax by the static `map.key` notation. This way, when a non-existent key is accessed, Elixir raises an error immediately, allowing developers to find bugs faster. The next code illustrates the refactoring of `plot/1`, removing this anti-pattern: +To remove this anti-pattern, whenever accessing an existing key of `Atom` type in the map, replace the dynamic `map[:key]` syntax by the static `map.key` notation. This way, when a non-existent key is accessed, Elixir raises an error immediately, allowing developers to find bugs faster. The next code illustrates the refactoring of `plot/1`, removing this anti-pattern: ```elixir defmodule Graphics do @@ -457,7 +457,7 @@ iex> Graphics.plot(point_3d) {5, 6, nil} ``` -Another alternative is to use structs. By default, structs only support static access to its fields, promoting cleaner patterns: +Another alternative is to use structs. By default, structs only support static access to its fields: ```elixir defmodule Point.2D do @@ -477,6 +477,8 @@ iex> point[:x] # <= by default, struct does not support dynamic access ** (UndefinedFunctionError) ... (Point does not implement the Access behaviour) ``` +Generally speaking, structs are useful when sharing data structures across modules, at the cost of adding a compile time dependency between these modules. If module `A` uses a struct defined in module `B`, `A` must be recompiled if the fields in the struct `B` change. + #### Additional remarks This anti-pattern was formerly known as [Accessing non-existent map/struct fields](https://github.com/lucasvegi/Elixir-Code-Smells#accessing-non-existent-mapstruct-fields). From 784f6eda93f0c9ccb9ecff95c54a62c92e3cec63 Mon Sep 17 00:00:00 2001 From: Serge Aleynikov Date: Thu, 26 Oct 2023 03:58:49 -0400 Subject: [PATCH 0082/1886] Document the `prune_code_paths` option for the compiler (#13035) --- lib/mix/lib/mix/tasks/compile.ex | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/mix/lib/mix/tasks/compile.ex b/lib/mix/lib/mix/tasks/compile.ex index 4e605c2e23..1763716f05 100644 --- a/lib/mix/lib/mix/tasks/compile.ex +++ b/lib/mix/lib/mix/tasks/compile.ex @@ -35,6 +35,13 @@ defmodule Mix.Tasks.Compile do this has undesirable side-effects (such as skipping some compiler checks) and should be avoided. + * `:prune_code_paths` - prune code paths before compilation. When true + (default), this prunes code paths of applications that are not listed + in the project file with dependencies. When false, this keeps the + entirety of Erlang/OTP available on the project starts, including + the paths set by the code loader from the `ERL_LIBS` environment as + well as explicitely listed by providing `-pa` and `-pz` options + to Erlang. ## Compilers To see documentation for each specific compiler, you must From 9a3c732ddecc6268b295a1a3cf7b1e8e6ff3fc3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 26 Oct 2023 13:34:37 +0200 Subject: [PATCH 0083/1886] Also exclusive optional applications to Mix load path --- lib/mix/lib/mix.ex | 48 ++++++++++++++++++++++++++++++---------------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/lib/mix/lib/mix.ex b/lib/mix/lib/mix.ex index c0036831bb..1812d8a9b6 100644 --- a/lib/mix/lib/mix.ex +++ b/lib/mix/lib/mix.ex @@ -602,28 +602,42 @@ defmodule Mix do """ @doc since: "1.15.0" def ensure_application!(app) when is_atom(app) do - ensure_application!(app, Mix.State.builtin_apps(), []) + ensure_application!(app, Mix.State.builtin_apps(), [], %{}) :ok end - defp ensure_application!(app, builtin_apps, optional) do - case builtin_apps do - %{^app => path} -> - Code.prepend_path(path, cache: true) - Application.load(app) - optional = List.wrap(Application.spec(app, :optional_applications)) - - Application.spec(app, :applications) - |> List.wrap() - |> Enum.each(&ensure_application!(&1, builtin_apps, optional)) + defp ensure_application!(app, builtin_apps, optional, seen) do + case seen do + %{^app => _} -> + seen %{} -> - unless app in optional do - Mix.raise( - "The application \"#{app}\" could not be found. This may happen if your " <> - "Operating System broke Erlang into multiple packages and may be fixed " <> - "by installing the missing \"erlang-dev\" and \"erlang-#{app}\" packages" - ) + seen = Map.put(seen, app, true) + + case builtin_apps do + %{^app => path} -> + Code.prepend_path(path, cache: true) + Application.load(app) + + required = List.wrap(Application.spec(app, :applications)) + optional = List.wrap(Application.spec(app, :optional_applications)) + + Enum.reduce( + required ++ optional, + seen, + &ensure_application!(&1, builtin_apps, optional, &2) + ) + + %{} -> + unless app in optional do + Mix.raise( + "The application \"#{app}\" could not be found. This may happen if your " <> + "Operating System broke Erlang into multiple packages and may be fixed " <> + "by installing the missing \"erlang-dev\" and \"erlang-#{app}\" packages" + ) + end + + seen end end end From e88377f5178db49cf1014ab77694e56c09395e10 Mon Sep 17 00:00:00 2001 From: Philip Munksgaard Date: Thu, 26 Oct 2023 14:45:01 +0200 Subject: [PATCH 0084/1886] Add missing dialyzer attributes (#13037) Taken from this list: https://www.erlang.org/doc/man/dialyzer.html#type-warn_option Fixes #13036 --- lib/elixir/lib/module.ex | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/elixir/lib/module.ex b/lib/elixir/lib/module.ex index 3e23e85286..2184a34569 100644 --- a/lib/elixir/lib/module.ex +++ b/lib/elixir/lib/module.ex @@ -2266,7 +2266,9 @@ defmodule Module do [:no_match, :no_opaque, :no_fail_call, :no_contracts] ++ [:no_behaviours, :no_undefined_callbacks, :unmatched_returns] ++ [:error_handling, :race_conditions, :no_missing_calls] ++ - [:specdiffs, :overspecs, :underspecs, :unknown, :no_underspecs] + [:specdiffs, :overspecs, :underspecs, :unknown, :no_underspecs] ++ + [:extra_return, :no_extra_return, :no_missing_return] ++ + [:missing_return, :no_unknown] ) end From b86b8ba47ef364790b1f6c3cf984b6541957b89f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 26 Oct 2023 18:33:13 +0200 Subject: [PATCH 0085/1886] Clarify primitive obsession --- lib/elixir/pages/anti-patterns/design-anti-patterns.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/elixir/pages/anti-patterns/design-anti-patterns.md b/lib/elixir/pages/anti-patterns/design-anti-patterns.md index 1e533a06a9..8a1b13d562 100644 --- a/lib/elixir/pages/anti-patterns/design-anti-patterns.md +++ b/lib/elixir/pages/anti-patterns/design-anti-patterns.md @@ -242,7 +242,7 @@ This refactoring is only possible when you own both modules. If the module you a #### Problem -This anti-pattern happens when Elixir basic types (for example, *integer*, *float*, and *string*) are abusively used in function parameters and code variables, rather than creating specific composite data types (for example, *tuples* and *structs*) that can better represent a domain. +This anti-pattern happens when Elixir basic types (for example, *integer*, *float*, and *string*) are abusively used in function parameters and code variables, rather than creating specific composite data types (for example, *tuples*, *maps*, and *structs*) that can better represent a domain. #### Example @@ -260,7 +260,7 @@ Another example of this anti-pattern is using floating numbers to model money an #### Refactoring -We can create an `Address` struct to remove this anti-pattern, better representing this domain through a composite type. Additionally, we can modify the `process_address/1` function to accept a parameter of type `Address` instead of a *string*. With this modification, we can extract each field of this composite type individually when needed. +Possible solutions to this anti-pattern is to use maps or structs to model our address. The example below creates an `Address` struct, better representing this domain through a composite type. Additionally, we can modify the `process_address/1` function to accept a parameter of type `Address` instead of a *string*. With this modification, we can extract each field of this composite type individually when needed. ```elixir defmodule Address do From dc79914182e00c230c49b9524ece7aaa0b51b9c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 26 Oct 2023 23:19:21 +0200 Subject: [PATCH 0086/1886] Add :limit option to Task.yield_many/2 --- lib/elixir/lib/task.ex | 28 +++++++++++++++++-------- lib/elixir/test/elixir/task_test.exs | 31 ++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 9 deletions(-) diff --git a/lib/elixir/lib/task.ex b/lib/elixir/lib/task.ex index 5688bdd53d..2dd53da2a6 100644 --- a/lib/elixir/lib/task.ex +++ b/lib/elixir/lib/task.ex @@ -1109,7 +1109,8 @@ defmodule Task do * `{:ok, term}` if the task has successfully reported its result back in the given time interval * `{:exit, reason}` if the task has died - * `nil` if the task keeps running past the timeout + * `nil` if the task keeps running, either because a limit + has been reached or past the timeout Check `yield/2` for more information. @@ -1162,6 +1163,10 @@ defmodule Task do The second argument is either a timeout or options, which defaults to this: + * `:limit` - the maximum amount of tasks to wait for. + If the limit is reached before the timeout, this function + returns immediately without triggering the `:on_timeout` behaviour + * `:timeout` - the maximum amount of time (in milliseconds or `:infinity`) each task is allowed to execute for. Defaults to `5000`. @@ -1173,7 +1178,11 @@ defmodule Task do * `:kill_task` - the task that timed out is killed. """ @spec yield_many([t], timeout) :: [{t, {:ok, term} | {:exit, term} | nil}] - @spec yield_many([t], timeout: timeout, on_timeout: :nothing | :ignore | :kill_task) :: + @spec yield_many([t], + limit: pos_integer(), + timeout: timeout, + on_timeout: :nothing | :ignore | :kill_task + ) :: [{t, {:ok, term} | {:exit, term} | nil}] def yield_many(tasks, opts \\ []) @@ -1182,9 +1191,6 @@ defmodule Task do end def yield_many(tasks, opts) when is_list(opts) do - on_timeout = Keyword.get(opts, :on_timeout, :nothing) - timeout = Keyword.get(opts, :timeout, 5_000) - refs = Map.new(tasks, fn %Task{ref: ref, owner: owner} = task -> if owner != self() do @@ -1194,6 +1200,9 @@ defmodule Task do {ref, nil} end) + on_timeout = Keyword.get(opts, :on_timeout, :nothing) + timeout = Keyword.get(opts, :timeout, 5_000) + limit = Keyword.get(opts, :limit, map_size(refs)) timeout_ref = make_ref() timer_ref = @@ -1202,16 +1211,17 @@ defmodule Task do end try do - yield_many(map_size(refs), refs, timeout_ref, timer_ref) + yield_many(limit, refs, timeout_ref, timer_ref) catch {:noconnection, reason} -> exit({reason, {__MODULE__, :yield_many, [tasks, timeout]}}) else - refs -> + {timed_out?, refs} -> for task <- tasks do value = with nil <- Map.fetch!(refs, task.ref) do case on_timeout do + _ when not timed_out? -> nil :nothing -> nil :kill_task -> shutdown(task, :brutal_kill) :ignore -> ignore(task) @@ -1226,7 +1236,7 @@ defmodule Task do defp yield_many(0, refs, timeout_ref, timer_ref) do timer_ref && Process.cancel_timer(timer_ref) receive do: (^timeout_ref -> :ok), after: (0 -> :ok) - refs + {false, refs} end defp yield_many(limit, refs, timeout_ref, timer_ref) do @@ -1243,7 +1253,7 @@ defmodule Task do end ^timeout_ref -> - refs + {true, refs} end end diff --git a/lib/elixir/test/elixir/task_test.exs b/lib/elixir/test/elixir/task_test.exs index 703b7d8904..4c7d3215ec 100644 --- a/lib/elixir/test/elixir/task_test.exs +++ b/lib/elixir/test/elixir/task_test.exs @@ -601,6 +601,37 @@ defmodule TaskTest do [{task1, {:ok, :result}}, {task2, nil}, {task3, {:exit, :normal}}] end + test "returns results from multiple tasks with limit" do + task1 = %Task{ref: make_ref(), owner: self(), pid: nil, mfa: {__MODULE__, :test, 1}} + task2 = %Task{ref: make_ref(), owner: self(), pid: nil, mfa: {__MODULE__, :test, 1}} + task3 = %Task{ref: make_ref(), owner: self(), pid: nil, mfa: {__MODULE__, :test, 1}} + + send(self(), {task1.ref, :result}) + send(self(), {:DOWN, task3.ref, :process, self(), :normal}) + + assert Task.yield_many([task1, task2, task3], limit: 1, timeout: :infinity) == + [{task1, {:ok, :result}}, {task2, nil}, {task3, nil}] + + assert Task.yield_many([task2, task3], limit: 1, timeout: :infinity) == + [{task2, nil}, {task3, {:exit, :normal}}] + end + + test "returns results from multiple tasks with limit and on timeout" do + Process.flag(:trap_exit, true) + task1 = Task.async(fn -> Process.sleep(:infinity) end) + task2 = Task.async(fn -> :done end) + + assert Task.yield_many([task1, task2], timeout: :infinity, on_timeout: :kill_task, limit: 1) == + [{task1, nil}, {task2, {:ok, :done}}] + + assert Process.alive?(task1.pid) + + assert Task.yield_many([task1], timeout: 0, on_timeout: :kill_task, limit: 1) == + [{task1, nil}] + + refute Process.alive?(task1.pid) + end + test "returns results on infinity timeout" do task1 = %Task{ref: make_ref(), owner: self(), pid: nil, mfa: {__MODULE__, :test, 1}} task2 = %Task{ref: make_ref(), owner: self(), pid: nil, mfa: {__MODULE__, :test, 1}} From 1b1bbf1ad96a964975bbfb9d078499f690c581ae Mon Sep 17 00:00:00 2001 From: Serge Aleynikov Date: Fri, 27 Oct 2023 02:17:51 -0400 Subject: [PATCH 0087/1886] Fix typos in documentation (#13039) --- lib/mix/lib/mix/tasks/compile.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/mix/lib/mix/tasks/compile.ex b/lib/mix/lib/mix/tasks/compile.ex index 1763716f05..ed55c7b246 100644 --- a/lib/mix/lib/mix/tasks/compile.ex +++ b/lib/mix/lib/mix/tasks/compile.ex @@ -38,7 +38,7 @@ defmodule Mix.Tasks.Compile do * `:prune_code_paths` - prune code paths before compilation. When true (default), this prunes code paths of applications that are not listed in the project file with dependencies. When false, this keeps the - entirety of Erlang/OTP available on the project starts, including + entirety of Erlang/OTP available when the project starts, including the paths set by the code loader from the `ERL_LIBS` environment as well as explicitely listed by providing `-pa` and `-pz` options to Erlang. @@ -70,7 +70,7 @@ defmodule Mix.Tasks.Compile do if a library still successfully compiles without optional dependencies (which is the default case with dependencies) * `--no-prune-code-paths` - do not prune code paths before compilation, this keeps - the entirety of Erlang/OTP available on the project starts + the entirety of Erlang/OTP available when the project starts * `--no-protocol-consolidation` - skips protocol consolidation * `--no-validate-compile-env` - does not validate the application compile environment * `--return-errors` - returns error status and diagnostics instead of exiting on error From 0e1a5d3cfe9c6251ea5986796642d4f1dff33c0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 27 Oct 2023 09:02:41 +0200 Subject: [PATCH 0088/1886] More improvements to anti-pattern --- lib/elixir/pages/anti-patterns/code-anti-patterns.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/elixir/pages/anti-patterns/code-anti-patterns.md b/lib/elixir/pages/anti-patterns/code-anti-patterns.md index eba9c2740f..07004922b2 100644 --- a/lib/elixir/pages/anti-patterns/code-anti-patterns.md +++ b/lib/elixir/pages/anti-patterns/code-anti-patterns.md @@ -375,7 +375,9 @@ There are few known exceptions to this anti-pattern: #### Problem -In Elixir, it is possible to access values from `Map`s, which are key-value data structures, either statically or dynamically. When a key is expected to exist in the map, it must be accessed using the `map.key` notation, which asserts the key exists. If `map[:key]` is used and the informed key does not exist, `nil` is returned. This return can be confusing and does not allow developers to conclude whether the key is non-existent in the map or just has a bound `nil` value. In this way, this anti-pattern may cause bugs in the code. +In Elixir, it is possible to access values from `Map`s, which are key-value data structures, either statically or dynamically. When a key is expected to exist in the map, it must be accessed using the `map.key` notation, which asserts the key exists. If the key does not exist, an exception is raised (and in some situations also compiler warnings), allowing developers to catch bugs early on. + +`map[:key]` must be used with optional keys. This way, if the informed key does not exist, `nil` is returned. When used with required keys, this return can be confusing and allow `nil` values to pass through the system, while `map.key` would raise upfront. In this way, this anti-pattern may cause bugs in the code. #### Example @@ -432,7 +434,9 @@ iex> Graphics.plot(point_3d) {5, 6, nil} ``` -As shown below, another alternative to refactor this anti-pattern is to use pattern matching: +Overall, the usage of `map.key` and `map[:key]` encode important information about your data structure, allowing developers to be clear about their intent. See both `Map` and `Access` module documentation for more information and examples. + +Another alternative to refactor this anti-pattern is to use pattern matching: ```elixir defmodule Graphics do From 8923da00e3f4f0080b7c655ef5099f6ddffad601 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 27 Oct 2023 11:06:07 +0200 Subject: [PATCH 0089/1886] Ensure duplicate modules are recompiled, closes #13040 --- lib/mix/lib/mix/compilers/elixir.ex | 63 +++++++++++-------- .../test/mix/tasks/compile.elixir_test.exs | 30 ++++++++- 2 files changed, 67 insertions(+), 26 deletions(-) diff --git a/lib/mix/lib/mix/compilers/elixir.ex b/lib/mix/lib/mix/compilers/elixir.ex index 4668ef57e4..19b0cfea58 100644 --- a/lib/mix/lib/mix/compilers/elixir.ex +++ b/lib/mix/lib/mix/compilers/elixir.ex @@ -173,7 +173,7 @@ defmodule Mix.Compilers.Elixir do compiler_loop(stale, stale_modules, dest, timestamp, opts, state) else {:ok, info, state} -> - {modules, _exports, sources, pending_modules, _pending_exports} = state + {modules, _exports, sources, pending_modules, _stale_exports} = state previous_warnings = if Keyword.get(opts, :all_warnings, true), @@ -531,7 +531,7 @@ defmodule Mix.Compilers.Elixir do defp remove_stale_entry(entry, acc, sources, stale_exports, compile_path) do module(module: module, sources: source_files, export: export) = entry - {rest, exports, changed, stale} = acc + {rest, exports, changed, stale_modules} = acc {compile_references, export_references, runtime_references} = Enum.reduce(source_files, {[], [], []}, fn file, {compile_acc, export_acc, runtime_acc} -> @@ -548,20 +548,20 @@ defmodule Mix.Compilers.Elixir do # If I changed in disk or have a compile time reference to # something stale or have a reference to an old export, # I need to be recompiled. - has_any_key?(changed, source_files) or has_any_key?(stale, compile_references) or + has_any_key?(changed, source_files) or has_any_key?(stale_modules, compile_references) or has_any_key?(stale_exports, export_references) -> remove_and_purge(beam_path(compile_path, module), module) changed = Enum.reduce(source_files, changed, &Map.put(&2, &1, true)) - {rest, Map.put(exports, module, export), changed, Map.put(stale, module, true)} + {rest, Map.put(exports, module, export), changed, Map.put(stale_modules, module, true)} # If I have a runtime references to something stale, # I am stale too. - has_any_key?(stale, runtime_references) -> - {[entry | rest], exports, changed, Map.put(stale, module, true)} + has_any_key?(stale_modules, runtime_references) -> + {[entry | rest], exports, changed, Map.put(stale_modules, module, true)} # Otherwise, we don't store it anywhere true -> - {[entry | rest], exports, changed, stale} + {[entry | rest], exports, changed, stale_modules} end end @@ -1032,11 +1032,29 @@ defmodule Mix.Compilers.Elixir do end end - defp each_cycle(stale_modules, compile_path, timestamp, state) do - {modules, _exports, sources, pending_modules, pending_exports} = state + defp each_cycle(runtime_modules, compile_path, timestamp, state) do + {modules, _exports, sources, pending_modules, stale_exports} = state + + stale_modules = + modules + |> Enum.map(&module(&1, :module)) + |> Map.from_keys(true) + + # Because a module may be accidentally overridden in another file, + # we need to mark any pending module that has been defined as changed. + {pending_modules, changed} = + Enum.reduce(pending_modules, {[], []}, fn module, {pending_acc, changed_acc} -> + if is_map_key(stale_modules, module(module, :module)) do + {pending_acc, module(module, :sources) ++ changed_acc} + else + {[module | pending_acc], changed_acc} + end + end) + # We don't need to pass stale_modules here because + # runtime dependencies have already been marked as stale. {pending_modules, exports, changed} = - update_stale_entries(pending_modules, sources, [], %{}, pending_exports, compile_path) + update_stale_entries(pending_modules, sources, changed, %{}, stale_exports, compile_path) # For each changed file, mark it as changed. # If compilation fails mid-cycle, they will be picked next time around. @@ -1045,18 +1063,13 @@ defmodule Mix.Compilers.Elixir do end if changed == [] do - stale_modules = - modules - |> Enum.map(&module(&1, :module)) - |> Map.from_keys(true) - |> Map.merge(stale_modules) - - {_, runtime_modules, sources} = fixpoint_runtime_modules(sources, stale_modules) + {_, runtime_modules, sources} = + fixpoint_runtime_modules(sources, Map.merge(stale_modules, runtime_modules)) runtime_paths = Enum.map(runtime_modules, &{&1, Path.join(compile_path, Atom.to_string(&1) <> ".beam")}) - state = {modules, exports, sources, pending_modules, pending_exports} + state = {modules, exports, sources, pending_modules, stale_exports} {{:runtime, runtime_paths, []}, state} else Mix.Utils.compiling_n(length(changed), :ex) @@ -1085,7 +1098,7 @@ defmodule Mix.Compilers.Elixir do defp each_file(file, references, verbose, state, cwd) do {compile_references, export_references, runtime_references, compile_env} = references - {modules, exports, sources, pending_modules, pending_exports} = state + {modules, exports, sources, pending_modules, stale_exports} = state file = Path.relative_to(file, cwd) @@ -1114,22 +1127,22 @@ defmodule Mix.Compilers.Elixir do compile_env: compile_env ) - {modules, exports, [source | sources], pending_modules, pending_exports} + {modules, exports, [source | sources], pending_modules, stale_exports} end defp each_module(file, module, kind, external, new_export, state, timestamp, cwd) do - {modules, exports, sources, pending_modules, pending_exports} = state + {modules, exports, sources, pending_modules, stale_exports} = state file = Path.relative_to(file, cwd) external = process_external_resources(external, cwd) old_export = Map.get(exports, module) - pending_exports = + stale_exports = if old_export && old_export != new_export do - pending_exports + stale_exports else - Map.delete(pending_exports, module) + Map.delete(stale_exports, module) end {module_sources, existing_module?} = @@ -1163,7 +1176,7 @@ defmodule Mix.Compilers.Elixir do ) modules = prepend_or_merge(modules, module, module(:module), module, existing_module?) - {modules, exports, [source | sources], pending_modules, pending_exports} + {modules, exports, [source | sources], pending_modules, stale_exports} end defp prepend_or_merge(collection, key, pos, value, true) do diff --git a/lib/mix/test/mix/tasks/compile.elixir_test.exs b/lib/mix/test/mix/tasks/compile.elixir_test.exs index 038fd8039f..3b6c077981 100644 --- a/lib/mix/test/mix/tasks/compile.elixir_test.exs +++ b/lib/mix/test/mix/tasks/compile.elixir_test.exs @@ -931,10 +931,11 @@ defmodule Mix.Tasks.Compile.ElixirTest do assert File.regular?("_build/dev/lib/sample/ebin/Elixir.A.beam") assert File.regular?("_build/dev/lib/sample/ebin/Elixir.B.beam") + # Compile directly so it does not point to a .beam file Code.put_compiler_option(:ignore_module_conflict, true) Code.compile_file("lib/b.ex") - force_recompilation("lib/a.ex") + force_recompilation("lib/a.ex") Mix.Tasks.Compile.Elixir.run(["--verbose"]) assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]} end) @@ -963,6 +964,33 @@ defmodule Mix.Tasks.Compile.ElixirTest do end) end + test "compiles dependent changed on conflict" do + in_fixture("no_mixfile", fn -> + Mix.Project.push(MixTest.Case.Sample) + + assert Mix.Tasks.Compile.Elixir.run(["--verbose"]) == {:ok, []} + assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]} + assert_received {:mix_shell, :info, ["Compiled lib/b.ex"]} + purge([A, B]) + + capture_io(:stderr, fn -> + File.write!("lib/a.ex", "defmodule B, do: :not_ok") + assert {:ok, [_, _]} = Mix.Tasks.Compile.Elixir.run(["--verbose"]) + assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]} + assert_received {:mix_shell, :info, ["Compiled lib/b.ex"]} + purge([A, B]) + end) + + capture_io(:stderr, fn -> + File.write!("lib/a.ex", "defmodule A, do: :ok") + assert {:ok, []} = Mix.Tasks.Compile.Elixir.run(["--verbose"]) + assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]} + assert_received {:mix_shell, :info, ["Compiled lib/b.ex"]} + purge([A, B]) + end) + end) + end + test "compiles dependent changed external resources" do in_fixture("no_mixfile", fn -> Mix.Project.push(MixTest.Case.Sample) From b5704cc7d074d61d64dcae8aa68e525ac36fd469 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 27 Oct 2023 12:59:30 +0200 Subject: [PATCH 0090/1886] Add MIX_PROFILE --- lib/mix/lib/mix.ex | 3 +++ lib/mix/lib/mix/cli.ex | 4 ++++ lib/mix/lib/mix/task.ex | 23 +++++++++++++++-------- lib/mix/lib/mix/tasks/profile.eprof.ex | 5 ++++- 4 files changed, 26 insertions(+), 9 deletions(-) diff --git a/lib/mix/lib/mix.ex b/lib/mix/lib/mix.ex index 1812d8a9b6..bf17b11565 100644 --- a/lib/mix/lib/mix.ex +++ b/lib/mix/lib/mix.ex @@ -356,6 +356,9 @@ defmodule Mix do * `MIX_PATH` - appends extra code paths + * `MIX_PROFILE` - a list of comma-separated Mix tasks to profile the time spent on + functions by the process running the task + * `MIX_QUIET` - does not print information messages to the terminal * `MIX_REBAR3` - path to rebar3 command that overrides the one Mix installs diff --git a/lib/mix/lib/mix/cli.ex b/lib/mix/lib/mix/cli.ex index 6847645420..10b4eb2f41 100644 --- a/lib/mix/lib/mix/cli.ex +++ b/lib/mix/lib/mix/cli.ex @@ -10,6 +10,10 @@ defmodule Mix.CLI do if env_variable_activated?("MIX_QUIET"), do: Mix.shell(Mix.Shell.Quiet) if env_variable_activated?("MIX_DEBUG"), do: Mix.debug(true) + if profile = System.get_env("MIX_PROFILE", "") do + Mix.State.put(:profile, String.split(profile, ",")) + end + case check_for_shortcuts(args) do :help -> Mix.shell().info("Mix is a build tool for Elixir") diff --git a/lib/mix/lib/mix/task.ex b/lib/mix/lib/mix/task.ex index 8b901436be..b5a4b81c5b 100644 --- a/lib/mix/lib/mix/task.ex +++ b/lib/mix/lib/mix/task.ex @@ -492,14 +492,21 @@ defmodule Mix.Task do end defp with_debug(task, args, proj, fun) do - if Mix.debug?() do - shell = Mix.shell() - shell.info(["-> Running mix ", task_to_string(task, args), project_to_string(proj)]) - {time, res} = :timer.tc(fun) - shell.info(["<- Ran mix ", task, " in ", Integer.to_string(div(time, 1000)), "ms"]) - res - else - fun.() + cond do + Mix.debug?() -> + shell = Mix.shell() + shell.info(["-> Running mix ", task_to_string(task, args), project_to_string(proj)]) + {time, res} = :timer.tc(fun) + shell.info(["<- Ran mix ", task, " in ", Integer.to_string(div(time, 1000)), "ms"]) + res + + task in Mix.State.get(:profile, []) -> + shell = Mix.shell() + shell.info(["-> Profiling mix ", task_to_string(task, args), project_to_string(proj)]) + Mix.Tasks.Profile.Eprof.profile(fun, warmup: false, set_on_spawn: false) + + true -> + fun.() end end diff --git a/lib/mix/lib/mix/tasks/profile.eprof.ex b/lib/mix/lib/mix/tasks/profile.eprof.ex index 36cc259840..792b74e13e 100644 --- a/lib/mix/lib/mix/tasks/profile.eprof.ex +++ b/lib/mix/lib/mix/tasks/profile.eprof.ex @@ -181,6 +181,8 @@ defmodule Mix.Tasks.Profile.Eprof do * `:calls` - filters out any results with a call count lower than this * `:time` - filters out any results that took lower than specified (in µs) * `:sort` - sort the results by `:time` or `:calls` (default: `:time`) + * `:warmup` - if the code should be warmed up before profiling (default: `true`) + * `:set_on_spawn` - if newly spawned processes should be measured (default: `true`) """ @spec profile((-> result), keyword()) :: result when result: any() @@ -198,7 +200,8 @@ defmodule Mix.Tasks.Profile.Eprof do end :eprof.start() - {:ok, return_value} = :eprof.profile([], fun, Keyword.get(opts, :matching, {:_, :_, :_})) + matching = Keyword.get(opts, :matching, {:_, :_, :_}) + {:ok, return_value} = :eprof.profile([], fun, matching, Keyword.take(opts, [:set_on_spawn])) results = Enum.map(:eprof.dump(), fn {pid, call_results} -> From 663477314577e093b0f81d1aacfc549299f88512 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 27 Oct 2023 14:08:13 +0200 Subject: [PATCH 0091/1886] Optimize mix compile.elixir when changing one file between thousands --- lib/mix/lib/mix/compilers/elixir.ex | 251 +++++++++--------- lib/mix/lib/mix/compilers/test.ex | 68 +++-- lib/mix/lib/mix/tasks/profile.eprof.ex | 3 +- lib/mix/lib/mix/tasks/xref.ex | 52 ++-- .../test/mix/tasks/compile.elixir_test.exs | 2 +- 5 files changed, 184 insertions(+), 192 deletions(-) diff --git a/lib/mix/lib/mix/compilers/elixir.ex b/lib/mix/lib/mix/compilers/elixir.ex index 19b0cfea58..7f7bbb3298 100644 --- a/lib/mix/lib/mix/compilers/elixir.ex +++ b/lib/mix/lib/mix/compilers/elixir.ex @@ -1,15 +1,14 @@ defmodule Mix.Compilers.Elixir do @moduledoc false - @manifest_vsn 21 + @manifest_vsn 22 @checkpoint_vsn 2 import Record - defrecord :module, [:module, :kind, :sources, :export, :recompile?, :timestamp] + defrecord :module, [:kind, :sources, :export, :recompile?, :timestamp] defrecord :source, - source: nil, size: 0, digest: nil, compile_references: [], @@ -112,7 +111,7 @@ defmodule Mix.Compilers.Elixir do compile_env_apps = compile_env_apps ++ config_apps compile_env_stale = - for source(compile_env: compile_env, modules: modules) <- all_sources, + for {_, source(compile_env: compile_env, modules: modules)} <- all_sources, Enum.any?(compile_env_apps, &List.keymember?(compile_env, &1, 0)), module <- modules, do: module @@ -128,7 +127,7 @@ defmodule Mix.Compilers.Elixir do {stale_modules, stale_exports, all_local_exports} = stale_local_deps(local_deps, manifest, stale, modified, all_local_exports) - prev_paths = for source(source: source) <- all_sources, do: source + prev_paths = Map.keys(all_sources) removed = prev_paths -- all_paths {sources, removed_modules} = remove_removed_sources(all_sources, removed) @@ -169,11 +168,11 @@ defmodule Mix.Compilers.Elixir do previous_opts = set_compiler_opts(opts) try do - state = {[], exports, sources, modules, removed_modules} + state = {%{}, exports, sources, [], modules, removed_modules} compiler_loop(stale, stale_modules, dest, timestamp, opts, state) else {:ok, info, state} -> - {modules, _exports, sources, pending_modules, _stale_exports} = state + {modules, _exports, sources, _changed, pending_modules, _stale_exports} = state previous_warnings = if Keyword.get(opts, :all_warnings, true), @@ -184,7 +183,7 @@ defmodule Mix.Compilers.Elixir do write_manifest( manifest, - modules ++ pending_modules, + Map.merge(modules, pending_modules), sources, all_local_exports, new_parents, @@ -200,7 +199,7 @@ defmodule Mix.Compilers.Elixir do {:error, errors, %{runtime_warnings: r_warnings, compile_warnings: c_warnings}, state} -> # In case of errors, we show all previous warnings and all new ones. - {_, _, sources, _, _} = state + {_, _, sources, _, _, _} = state errors = Enum.map(errors, &diagnostic/1) warnings = Enum.map(r_warnings ++ c_warnings, &diagnostic/1) all_warnings = Keyword.get(opts, :all_warnings, errors == []) @@ -273,7 +272,7 @@ defmodule Mix.Compilers.Elixir do def clean(manifest, compile_path) do {modules, _} = read_manifest(manifest) - Enum.each(modules, fn module(module: module) -> + Enum.each(modules, fn {module, _} -> File.rm(beam_path(compile_path, module)) end) end @@ -284,7 +283,7 @@ defmodule Mix.Compilers.Elixir do def protocols_and_impls(manifest, compile_path) do {modules, _} = read_manifest(manifest) - for module(module: module, kind: kind) <- modules, + for {module, module(kind: kind)} <- modules, match?(:protocol, kind) or match?({:impl, _}, kind), do: {module, kind, beam_path(compile_path, module)} end @@ -305,7 +304,7 @@ defmodule Mix.Compilers.Elixir do defp compiler_info_from_force(manifest, all_paths, all_modules, dest) do # A config, path dependency or manifest has changed, let's just compile everything - for module(module: module) <- all_modules, + for {module, _} <- all_modules, do: remove_and_purge(beam_path(dest, module), module) sources_stats = @@ -318,7 +317,7 @@ defmodule Mix.Compilers.Elixir do # would have an outdated manifest. File.rm(manifest) - {[], %{}, all_paths, sources_stats} + {%{}, %{}, all_paths, sources_stats} end # If any .beam file is missing, the first one will the first to miss, @@ -339,7 +338,7 @@ defmodule Mix.Compilers.Elixir do dest ) do {modules_to_recompile, modules_to_mix_check} = - for module(module: module, recompile?: recompile?) <- all_modules, reduce: {[], []} do + for {module, module(recompile?: recompile?)} <- all_modules, reduce: {[], []} do {modules_to_recompile, modules_to_mix_check} -> cond do Map.has_key?(stale_modules, module) -> @@ -387,7 +386,7 @@ defmodule Mix.Compilers.Elixir do # Sources that have changed on disk or # any modules associated with them need to be recompiled changed = - for source(source: source, external: external, size: size, digest: digest, modules: modules) <- + for {source, source(external: external, size: size, digest: digest, modules: modules)} <- all_sources, {last_mtime, last_size} = Map.fetch!(sources_stats, source), # If the user does a change, compilation fails, and then they revert @@ -433,7 +432,7 @@ defmodule Mix.Compilers.Elixir do end defp mtimes_and_sizes(sources) do - Enum.reduce(sources, %{}, fn source(source: source, external: external), map -> + Enum.reduce(sources, %{}, fn {source, source(external: external)}, map -> map = Map.put_new_lazy(map, source, fn -> Mix.Utils.last_modified_and_size(source) end) Enum.reduce(external, map, fn {file, _}, map -> @@ -467,7 +466,7 @@ defmodule Mix.Compilers.Elixir do defp put_compile_env(sources) do all_compile_env = - Enum.reduce(sources, :ordsets.new(), fn source(compile_env: compile_env), acc -> + Enum.reduce(sources, :ordsets.new(), fn {_, source(compile_env: compile_env)}, acc -> :ordsets.union(compile_env, acc) end) @@ -478,8 +477,7 @@ defmodule Mix.Compilers.Elixir do defp remove_removed_sources(sources, removed) do Enum.reduce(removed, {sources, %{}}, fn file, {acc_sources, acc_modules} -> - {source(modules: modules), acc_sources} = List.keytake(acc_sources, file, source(:source)) - + {source(modules: modules), acc_sources} = Map.pop(acc_sources, file) acc_modules = Enum.reduce(modules, acc_modules, &Map.put(&2, &1, true)) {acc_sources, acc_modules} end) @@ -492,14 +490,14 @@ defmodule Mix.Compilers.Elixir do Enum.reduce(stale, {sources, removed_modules}, fn file, {acc_sources, acc_modules} -> %{^file => {_, size}} = sources_stats - {modules, acc_sources} = - case List.keytake(acc_sources, file, source(:source)) do - {source(modules: modules), acc_sources} -> {modules, acc_sources} - nil -> {[], acc_sources} + modules = + case acc_sources do + %{^file => source(modules: modules)} -> modules + %{} -> [] end acc_modules = Enum.reduce(modules, acc_modules, &Map.put(&2, &1, true)) - {[source(source: file, size: size) | acc_sources], acc_modules} + {Map.put(acc_sources, file, source(size: size)), acc_modules} end) end @@ -520,7 +518,7 @@ defmodule Mix.Compilers.Elixir do defp remove_stale_entries(modules, exports, old_changed, old_stale, reducer) do {pending_modules, exports, new_changed, new_stale} = - Enum.reduce(modules, {[], exports, old_changed, old_stale}, reducer) + Enum.reduce(modules, {modules, exports, old_changed, old_stale}, reducer) if map_size(new_stale) > map_size(old_stale) or map_size(new_changed) > map_size(old_changed) do remove_stale_entries(pending_modules, exports, new_changed, new_stale, reducer) @@ -530,8 +528,8 @@ defmodule Mix.Compilers.Elixir do end defp remove_stale_entry(entry, acc, sources, stale_exports, compile_path) do - module(module: module, sources: source_files, export: export) = entry - {rest, exports, changed, stale_modules} = acc + {module, module(sources: source_files, export: export)} = entry + {pending_modules, exports, changed, stale_modules} = acc {compile_references, export_references, runtime_references} = Enum.reduce(source_files, {[], [], []}, fn file, {compile_acc, export_acc, runtime_acc} -> @@ -539,7 +537,7 @@ defmodule Mix.Compilers.Elixir do compile_references: compile_refs, export_references: export_refs, runtime_references: runtime_refs - ) = List.keyfind(sources, file, source(:source)) + ) = Map.fetch!(sources, file) {compile_acc ++ compile_refs, export_acc ++ export_refs, runtime_acc ++ runtime_refs} end) @@ -552,16 +550,18 @@ defmodule Mix.Compilers.Elixir do has_any_key?(stale_exports, export_references) -> remove_and_purge(beam_path(compile_path, module), module) changed = Enum.reduce(source_files, changed, &Map.put(&2, &1, true)) - {rest, Map.put(exports, module, export), changed, Map.put(stale_modules, module, true)} + + {Map.delete(pending_modules, module), Map.put(exports, module, export), changed, + Map.put(stale_modules, module, true)} # If I have a runtime references to something stale, # I am stale too. has_any_key?(stale_modules, runtime_references) -> - {[entry | rest], exports, changed, Map.put(stale_modules, module, true)} + {pending_modules, exports, changed, Map.put(stale_modules, module, true)} # Otherwise, we don't store it anywhere true -> - {[entry | rest], exports, changed, stale_modules} + {pending_modules, exports, changed, stale_modules} end end @@ -584,7 +584,7 @@ defmodule Mix.Compilers.Elixir do {manifest_modules, manifest_sources} = read_manifest(manifest) dep_modules = - for module(module: module, timestamp: timestamp) <- manifest_modules, + for {module, module(timestamp: timestamp)} <- manifest_modules, timestamp > modified, do: module @@ -638,7 +638,7 @@ defmodule Mix.Compilers.Elixir do end defp fixpoint_runtime_modules(sources, modules) when modules != %{} do - fixpoint_runtime_modules(sources, modules, false, [], [], []) + fixpoint_runtime_modules(Map.to_list(sources), modules, false, [], [], sources) end defp fixpoint_runtime_modules(sources, modules) do @@ -646,31 +646,34 @@ defmodule Mix.Compilers.Elixir do end defp fixpoint_runtime_modules( - [source | sources], + [{source_path, source_entry} = pair | sources], modules, new?, pending_sources, acc_modules, acc_sources ) do - source(export_references: export_refs, runtime_references: runtime_refs) = source + source(export_references: export_refs, runtime_references: runtime_refs) = source_entry if has_any_key?(modules, export_refs) or has_any_key?(modules, runtime_refs) do - new_modules = Enum.reject(source(source, :modules), &Map.has_key?(modules, &1)) + new_modules = Enum.reject(source(source_entry, :modules), &Map.has_key?(modules, &1)) modules = Enum.reduce(new_modules, modules, &Map.put(&2, &1, true)) new? = new? or new_modules != [] acc_modules = new_modules ++ acc_modules - acc_sources = [source(source, runtime_warnings: []) | acc_sources] + + acc_sources = + Map.replace!(acc_sources, source_path, source(source_entry, runtime_warnings: [])) + fixpoint_runtime_modules(sources, modules, new?, pending_sources, acc_modules, acc_sources) else - pending_sources = [source | pending_sources] + pending_sources = [pair | pending_sources] fixpoint_runtime_modules(sources, modules, new?, pending_sources, acc_modules, acc_sources) end end defp fixpoint_runtime_modules([], modules, new?, pending_sources, acc_modules, acc_sources) when new? == false or pending_sources == [], - do: {modules, acc_modules, acc_sources ++ pending_sources} + do: {modules, acc_modules, acc_sources} defp fixpoint_runtime_modules([], modules, true, pending_sources, acc_modules, acc_sources), do: fixpoint_runtime_modules(pending_sources, modules, false, [], acc_modules, acc_sources) @@ -714,11 +717,11 @@ defmodule Mix.Compilers.Elixir do end defp previous_warnings(sources, print?) do - for source( - source: source, - compile_warnings: compile_warnings, - runtime_warnings: runtime_warnings - ) <- sources, + for {source, + source( + compile_warnings: compile_warnings, + runtime_warnings: runtime_warnings + )} <- sources, file = Path.absname(source), {position, message, span} <- compile_warnings ++ runtime_warnings do diagnostic = %Mix.Task.Compiler.Diagnostic{ @@ -740,21 +743,27 @@ defmodule Mix.Compilers.Elixir do end end + defp apply_warnings(sources, %{runtime_warnings: [], compile_warnings: []}) do + sources + end + defp apply_warnings(sources, %{runtime_warnings: r_warnings, compile_warnings: c_warnings}) do runtime_group = Enum.group_by(r_warnings, & &1.file, &{&1.position, &1.message, &1.span}) compile_group = Enum.group_by(c_warnings, & &1.file, &{&1.position, &1.message, &1.span}) - for source( - source: source_path, - runtime_warnings: runtime_warnings, - compile_warnings: compile_warnings - ) = s <- sources do + for {source_path, source_entry} <- sources, into: %{} do key = Path.absname(source_path) - source(s, - runtime_warnings: Map.get(runtime_group, key, runtime_warnings), - compile_warnings: Map.get(compile_group, key, compile_warnings) - ) + source( + runtime_warnings: runtime_warnings, + compile_warnings: compile_warnings + ) = source_entry + + {source_path, + source(source_entry, + runtime_warnings: Map.get(runtime_group, key, runtime_warnings), + compile_warnings: Map.get(compile_group, key, compile_warnings) + )} end end @@ -829,7 +838,7 @@ defmodule Mix.Compilers.Elixir do ## Manifest handling - @default_manifest {[], [], %{}, [], nil, nil} + @default_manifest {%{}, %{}, %{}, [], nil, nil} # Similar to read_manifest, but for internal consumption and with data migration support. defp parse_manifest(manifest, compile_path) do @@ -842,7 +851,8 @@ defmodule Mix.Compilers.Elixir do {@manifest_vsn, modules, sources, local_exports, parent, cache_key, deps_config} -> {modules, sources, local_exports, parent, cache_key, deps_config} - # {vsn, modules, sources, ...} v5-v16 + # {vsn, %{module => record}, sources, ...} v22-? + # {vsn, [module_record], sources, ...} v5-v21 manifest when is_tuple(manifest) and is_integer(elem(manifest, 0)) -> purge_old_manifest(compile_path, elem(manifest, 1)) @@ -857,8 +867,18 @@ defmodule Mix.Compilers.Elixir do defp purge_old_manifest(compile_path, data) do try do - for module <- data, elem(module, 0) == :module do - module = elem(module, 1) + # If data is a list, we have an old manifest and + # we convert it to the same format as maps. + data = + if is_list(data) do + for entry <- data, elem(entry, 0) == :module do + {elem(entry, 1), entry} + end + else + data + end + + for {module, _} <- data do File.rm(beam_path(compile_path, module)) :code.purge(module) :code.delete(module) @@ -873,33 +893,34 @@ defmodule Mix.Compilers.Elixir do @default_manifest end - defp write_manifest(manifest, [], [], _exports, _parents, _cache_key, _deps_config, _timestamp) do - File.rm(manifest) - :ok - end - defp write_manifest( manifest, - modules, - sources, + %{} = modules, + %{} = sources, exports, parents, cache_key, deps_config, timestamp ) do - File.mkdir_p!(Path.dirname(manifest)) - - term = {@manifest_vsn, modules, sources, exports, parents, cache_key, deps_config} - manifest_data = :erlang.term_to_binary(term, [:compressed]) - File.write!(manifest, manifest_data) - File.touch!(manifest, timestamp) - delete_checkpoint(manifest) + if modules == %{} and sources == %{} do + File.rm(manifest) + else + File.mkdir_p!(Path.dirname(manifest)) + + term = {@manifest_vsn, modules, sources, exports, parents, cache_key, deps_config} + manifest_data = :erlang.term_to_binary(term, [:compressed]) + File.write!(manifest, manifest_data) + File.touch!(manifest, timestamp) + delete_checkpoint(manifest) + + # Since Elixir is a dependency itself, we need to touch the lock + # so the current Elixir version, used to compile the files above, + # is properly stored. + Mix.Dep.ElixirSCM.update() + end - # Since Elixir is a dependency itself, we need to touch the lock - # so the current Elixir version, used to compile the files above, - # is properly stored. - Mix.Dep.ElixirSCM.update() + :ok end defp beam_path(compile_path, module) do @@ -1033,26 +1054,8 @@ defmodule Mix.Compilers.Elixir do end defp each_cycle(runtime_modules, compile_path, timestamp, state) do - {modules, _exports, sources, pending_modules, stale_exports} = state - - stale_modules = - modules - |> Enum.map(&module(&1, :module)) - |> Map.from_keys(true) - - # Because a module may be accidentally overridden in another file, - # we need to mark any pending module that has been defined as changed. - {pending_modules, changed} = - Enum.reduce(pending_modules, {[], []}, fn module, {pending_acc, changed_acc} -> - if is_map_key(stale_modules, module(module, :module)) do - {pending_acc, module(module, :sources) ++ changed_acc} - else - {[module | pending_acc], changed_acc} - end - end) + {modules, _exports, sources, changed, pending_modules, stale_exports} = state - # We don't need to pass stale_modules here because - # runtime dependencies have already been marked as stale. {pending_modules, exports, changed} = update_stale_entries(pending_modules, sources, changed, %{}, stale_exports, compile_path) @@ -1063,13 +1066,16 @@ defmodule Mix.Compilers.Elixir do end if changed == [] do + # We merge runtime_modules (which is a map of %{module => true}) into + # a map of modules (which is a map of %{module => record}). This is fine + # since fixpoint_runtime_modules only cares about map keys. {_, runtime_modules, sources} = - fixpoint_runtime_modules(sources, Map.merge(stale_modules, runtime_modules)) + fixpoint_runtime_modules(sources, Map.merge(modules, runtime_modules)) runtime_paths = Enum.map(runtime_modules, &{&1, Path.join(compile_path, Atom.to_string(&1) <> ".beam")}) - state = {modules, exports, sources, pending_modules, stale_exports} + state = {modules, exports, sources, [], pending_modules, stale_exports} {{:runtime, runtime_paths, []}, state} else Mix.Utils.compiling_n(length(changed), :ex) @@ -1081,24 +1087,22 @@ defmodule Mix.Compilers.Elixir do # remove the pending exports as we notice they have not gone stale. {sources, removed_modules} = Enum.reduce(changed, {sources, %{}}, fn file, {acc_sources, acc_modules} -> - {source(size: size, digest: digest, modules: modules), acc_sources} = - List.keytake(acc_sources, file, source(:source)) - + source(size: size, digest: digest, modules: modules) = Map.fetch!(acc_sources, file) acc_modules = Enum.reduce(modules, acc_modules, &Map.put(&2, &1, true)) # Define empty records for the sources that needs # to be recompiled (but were not changed on disk) - {[source(source: file, size: size, digest: digest) | acc_sources], acc_modules} + {Map.replace!(acc_sources, file, source(size: size, digest: digest)), acc_modules} end) - state = {modules, exports, sources, pending_modules, removed_modules} + state = {modules, exports, sources, [], pending_modules, removed_modules} {{:compile, changed, []}, state} end end defp each_file(file, references, verbose, state, cwd) do {compile_references, export_references, runtime_references, compile_env} = references - {modules, exports, sources, pending_modules, stale_exports} = state + {modules, exports, sources, changed, pending_modules, stale_exports} = state file = Path.relative_to(file, cwd) @@ -1106,12 +1110,10 @@ defmodule Mix.Compilers.Elixir do Mix.shell().info("Compiled #{file}") end - {source, sources} = List.keytake(sources, file, source(:source)) - compile_references = Enum.reject(compile_references, &match?("elixir_" <> _, Atom.to_string(&1))) - source(modules: source_modules) = source + source(modules: source_modules) = source = Map.fetch!(sources, file) compile_references = compile_references -- source_modules export_references = export_references -- source_modules runtime_references = runtime_references -- source_modules @@ -1127,15 +1129,15 @@ defmodule Mix.Compilers.Elixir do compile_env: compile_env ) - {modules, exports, [source | sources], pending_modules, stale_exports} + sources = Map.replace!(sources, file, source) + {modules, exports, sources, changed, pending_modules, stale_exports} end defp each_module(file, module, kind, external, new_export, state, timestamp, cwd) do - {modules, exports, sources, pending_modules, stale_exports} = state + {modules, exports, sources, changed, pending_modules, stale_exports} = state file = Path.relative_to(file, cwd) external = process_external_resources(external, cwd) - old_export = Map.get(exports, module) stale_exports = @@ -1145,14 +1147,14 @@ defmodule Mix.Compilers.Elixir do Map.delete(stale_exports, module) end - {module_sources, existing_module?} = - case List.keyfind(modules, module, module(:module)) do - module(sources: old_sources) -> {[file | List.delete(old_sources, file)], true} - nil -> {[file], false} + module_sources = + case modules do + %{^module => module(sources: old_sources)} -> [file | List.delete(old_sources, file)] + %{} -> [file] end - {source, sources} = - List.keytake(sources, file, source(:source)) || + source = + Map.get(sources, file) || Mix.raise( "Could not find source for #{inspect(file)}. Make sure the :elixirc_paths configuration " <> "is a list of relative paths to the current project or absolute paths to external directories" @@ -1165,9 +1167,8 @@ defmodule Mix.Compilers.Elixir do modules: [module | source(source, :modules)] ) - module = + entry = module( - module: module, kind: kind, sources: module_sources, export: new_export, @@ -1175,16 +1176,18 @@ defmodule Mix.Compilers.Elixir do recompile?: function_exported?(module, :__mix_recompile__?, 0) ) - modules = prepend_or_merge(modules, module, module(:module), module, existing_module?) - {modules, exports, [source | sources], pending_modules, stale_exports} - end + modules = Map.put(modules, module, entry) + sources = Map.replace!(sources, file, source) - defp prepend_or_merge(collection, key, pos, value, true) do - List.keystore(collection, key, pos, value) - end + # In case the module defined is pending, this is a source conflict. + # So we need to compile all duplicates. + changed = + case pending_modules do + %{^module => module(sources: sources)} -> sources ++ changed + %{} -> changed + end - defp prepend_or_merge(collection, _key, _pos, value, false) do - [value | collection] + {modules, exports, sources, changed, pending_modules, stale_exports} end defp detect_kind(module) do diff --git a/lib/mix/lib/mix/compilers/test.ex b/lib/mix/lib/mix/compilers/test.ex index 1e38f9a573..ded0a2c793 100644 --- a/lib/mix/lib/mix/compilers/test.ex +++ b/lib/mix/lib/mix/compilers/test.ex @@ -6,7 +6,6 @@ defmodule Mix.Compilers.Test do import Record defrecordp :source, - source: nil, compile_references: [], runtime_references: [], external: [] @@ -14,7 +13,7 @@ defmodule Mix.Compilers.Test do # Necessary to avoid warnings during bootstrap @compile {:no_warn_undefined, ExUnit} @stale_manifest "compile.test_stale" - @manifest_vsn 1 + @manifest_vsn 2 @doc """ Requires and runs test files. @@ -107,10 +106,10 @@ defmodule Mix.Compilers.Test do defp set_up_stale(matched_test_files, test_paths, opts) do manifest = manifest() modified = Mix.Utils.last_modified(manifest) - all_sources = read_manifest() + test_sources = read_manifest() removed = - for source(source: source) <- all_sources, source not in matched_test_files, do: source + for {source, _} <- test_sources, source not in matched_test_files, do: source test_helpers = Enum.map(test_paths, &Path.join(&1, "test_helper.exs")) sources = [Mix.Project.config_mtime(), Mix.Project.project_file() | test_helpers] @@ -121,17 +120,17 @@ defmodule Mix.Compilers.Test do # Let's just require everything matched_test_files else - sources_mtimes = mtimes(all_sources) + sources_mtimes = mtimes(test_sources) # Otherwise let's start with the new sources # Plus the sources that have changed in disk for( source <- matched_test_files, - not List.keymember?(all_sources, source, source(:source)), + not is_map_key(test_sources, source), do: source ) ++ for( - source(source: source, external: external) <- all_sources, + {source, source(external: external)} <- test_sources, times = Enum.map([source | external], &Map.fetch!(sources_mtimes, &1)), Mix.Utils.stale?(times, [modified]), do: source @@ -139,7 +138,7 @@ defmodule Mix.Compilers.Test do end stale = MapSet.new(changed -- removed) - sources = update_stale_sources(all_sources, removed, changed) + sources = update_stale_sources(test_sources, removed, changed) test_files_to_run = sources @@ -181,7 +180,7 @@ defmodule Mix.Compilers.Test do ## Setup helpers defp mtimes(sources) do - Enum.reduce(sources, %{}, fn source(source: source, external: external), map -> + Enum.reduce(sources, %{}, fn {source, source(external: external)}, map -> Enum.reduce([source | external], map, fn file, map -> Map.put_new_lazy(map, file, fn -> Mix.Utils.last_modified(file) end) end) @@ -189,11 +188,8 @@ defmodule Mix.Compilers.Test do end defp update_stale_sources(sources, removed, changed) do - sources = Enum.reject(sources, fn source(source: source) -> source in removed end) - - sources = - Enum.reduce(changed, sources, &List.keystore(&2, &1, source(:source), source(source: &1))) - + sources = Map.drop(sources, removed) + sources = Enum.reduce(changed, sources, &Map.put(&2, &1, source())) sources end @@ -206,20 +202,20 @@ defmodule Mix.Compilers.Test do [@manifest_vsn | sources] = manifest() |> File.read!() |> :erlang.binary_to_term() sources rescue - _ -> [] + _ -> %{} end end - defp write_manifest([]) do + defp write_manifest(test_sources) when test_sources == %{} do File.rm(manifest()) :ok end - defp write_manifest(sources) do + defp write_manifest(test_sources = %{}) do manifest = manifest() File.mkdir_p!(Path.dirname(manifest)) - manifest_data = :erlang.term_to_binary([@manifest_vsn | sources], [:compressed]) + manifest_data = :erlang.term_to_binary([@manifest_vsn | test_sources], [:compressed]) File.write!(manifest, manifest_data) end @@ -234,7 +230,7 @@ defmodule Mix.Compilers.Test do {elixir_modules, elixir_sources} = CE.read_manifest(elixir_manifest) stale_modules = - for CE.module(module: module) <- elixir_modules, + for {module, _} <- elixir_modules, beam = Path.join(compile_path, Atom.to_string(module) <> ".beam"), Mix.Utils.stale?([beam], [test_manifest]), do: module, @@ -242,9 +238,8 @@ defmodule Mix.Compilers.Test do stale_modules = find_all_dependent_on(stale_modules, elixir_modules, elixir_sources) - for module <- stale_modules, - source(source: source, runtime_references: r, compile_references: c) <- test_sources, - module in r or module in c, + for {source, source(runtime_references: r, compile_references: c)} <- test_sources, + Enum.any?(r, &(&1 in stale_modules)) or Enum.any?(c, &(&1 in stale_modules)), do: source, into: MapSet.new() else @@ -252,30 +247,30 @@ defmodule Mix.Compilers.Test do end end - defp find_all_dependent_on(modules, all_modules, sources, resolved \\ MapSet.new()) do + defp find_all_dependent_on(modules, elixir_modules, elixir_sources, resolved \\ MapSet.new()) do new_modules = for module <- modules, module not in resolved, - dependent_module <- dependent_modules(module, all_modules, sources), + dependent_module <- dependent_modules(module, elixir_modules, elixir_sources), do: dependent_module, into: modules if MapSet.size(new_modules) == MapSet.size(modules) do new_modules else - find_all_dependent_on(new_modules, all_modules, sources, modules) + find_all_dependent_on(new_modules, elixir_modules, elixir_sources, modules) end end defp dependent_modules(module, modules, sources) do - for CE.source( - source: source, - runtime_references: r, - compile_references: c, - export_references: e - ) <- sources, + for {source, + CE.source( + runtime_references: r, + compile_references: c, + export_references: e + )} <- sources, module in r or module in c or module in e, - CE.module(sources: sources, module: dependent_module) <- modules, + {dependent_module, CE.module(sources: sources)} <- modules, source in sources, do: dependent_module end @@ -288,8 +283,8 @@ defmodule Mix.Compilers.Test do if external != [] do Agent.update(pid, fn sources -> file = Path.relative_to(file, cwd) - {source, sources} = List.keytake(sources, file, source(:source)) - [source(source, external: external ++ source(source, :external)) | sources] + source(external: current_external) = source = Map.fetch!(sources, file) + Map.put(sources, file, source(source, external: external ++ current_external)) end) end @@ -299,19 +294,18 @@ defmodule Mix.Compilers.Test do defp each_file(pid, cwd, file, lexical) do Agent.update(pid, fn sources -> file = Path.relative_to(file, cwd) - {source, sources} = List.keytake(sources, file, source(:source)) {compile_references, export_references, runtime_references, _compile_env} = Kernel.LexicalTracker.references(lexical) source = source( - source, + Map.fetch!(sources, file), compile_references: compile_references ++ export_references, runtime_references: runtime_references ) - [source | sources] + Map.put(sources, file, source) end) end diff --git a/lib/mix/lib/mix/tasks/profile.eprof.ex b/lib/mix/lib/mix/tasks/profile.eprof.ex index 792b74e13e..f9ffdf9dca 100644 --- a/lib/mix/lib/mix/tasks/profile.eprof.ex +++ b/lib/mix/lib/mix/tasks/profile.eprof.ex @@ -201,7 +201,8 @@ defmodule Mix.Tasks.Profile.Eprof do :eprof.start() matching = Keyword.get(opts, :matching, {:_, :_, :_}) - {:ok, return_value} = :eprof.profile([], fun, matching, Keyword.take(opts, [:set_on_spawn])) + set_on_spawn = Keyword.get(opts, :set_on_spawn, true) + {:ok, return_value} = :eprof.profile([], fun, matching, set_on_spawn: set_on_spawn) results = Enum.map(:eprof.dump(), fn {pid, call_results} -> diff --git a/lib/mix/lib/mix/tasks/xref.ex b/lib/mix/lib/mix/tasks/xref.ex index bf55ebb47d..ae7b67260e 100644 --- a/lib/mix/lib/mix/tasks/xref.ex +++ b/lib/mix/lib/mix/tasks/xref.ex @@ -2,7 +2,7 @@ defmodule Mix.Tasks.Xref do use Mix.Task import Mix.Compilers.Elixir, - only: [read_manifest: 1, source: 0, source: 1, source: 2, module: 1] + only: [read_manifest: 1, source: 1, source: 2, module: 1] @shortdoc "Prints cross reference information" @manifest "compile.elixir" @@ -363,7 +363,7 @@ defmodule Mix.Tasks.Xref do ] def calls(opts \\ []) do for manifest <- manifests(opts), - source(source: source, modules: modules) <- read_manifest(manifest) |> elem(1), + {source, source(modules: modules)} <- read_manifest(manifest) |> elem(1), module <- modules, call <- collect_calls(source, module), do: call @@ -469,9 +469,10 @@ defmodule Mix.Tasks.Xref do module = parse_module(module) file_callers = - for source <- sources(opts), - reference = reference(module, source), - do: {source(source, :source), reference} + for manifest <- manifests(opts), + {source_path, source_entry} <- read_manifest(manifest) |> elem(1), + reference = reference(module, source_entry), + do: {source_path, reference} for {file, type} <- Enum.sort(file_callers) do Mix.shell().info([file, " (", type, ")"]) @@ -719,32 +720,31 @@ defmodule Mix.Tasks.Xref do end defp file_references(filter, opts) do - module_sources = + module_sources_list = for manifest_path <- manifests(opts), {manifest_modules, manifest_sources} = read_manifest(manifest_path), - module(module: module, sources: sources) <- manifest_modules, - source <- sources, - source = Enum.find(manifest_sources, &match?(source(source: ^source), &1)), - do: {module, source} + {module, module(sources: sources)} <- manifest_modules, + source_path <- sources, + source_entry = manifest_sources[source_path], + do: {module, {source_path, source_entry}} - all_modules = MapSet.new(module_sources, &elem(&1, 0)) + module_sources = Map.new(module_sources_list) - Map.new(module_sources, fn {current, source} -> + Map.new(module_sources_list, fn {current, {file, source_entry}} -> source( runtime_references: runtime, export_references: exports, - compile_references: compile, - source: file - ) = source + compile_references: compile + ) = source_entry compile_references = - modules_to_nodes(compile, :compile, current, source, module_sources, all_modules, filter) + modules_to_nodes(compile, :compile, current, file, module_sources, filter) export_references = - modules_to_nodes(exports, :export, current, source, module_sources, all_modules, filter) + modules_to_nodes(exports, :export, current, file, module_sources, filter) runtime_references = - modules_to_nodes(runtime, nil, current, source, module_sources, all_modules, filter) + modules_to_nodes(runtime, nil, current, file, module_sources, filter) references = runtime_references @@ -756,16 +756,16 @@ defmodule Mix.Tasks.Xref do end) end - defp modules_to_nodes(_, label, _, _, _, _, filter) when filter != :all and label != filter do + defp modules_to_nodes(_, label, _, _, _, filter) when filter != :all and label != filter do %{} end - defp modules_to_nodes(modules, label, current, source, module_sources, all_modules, _filter) do + defp modules_to_nodes(modules, label, current, file, module_sources, _filter) do for module <- modules, module != current, - module in all_modules, - module_sources[module] != source, - do: {source(module_sources[module], :source), label}, + {source_path, _source_entry} <- [module_sources[module]], + file != source_path, + do: {source_path, label}, into: %{} end @@ -1048,12 +1048,6 @@ defmodule Mix.Tasks.Xref do ## Helpers - defp sources(opts) do - for manifest <- manifests(opts), - source() = source <- read_manifest(manifest) |> elem(1), - do: source - end - defp apps(opts) do siblings = if opts[:include_siblings] do diff --git a/lib/mix/test/mix/tasks/compile.elixir_test.exs b/lib/mix/test/mix/tasks/compile.elixir_test.exs index 3b6c077981..9666cbf5db 100644 --- a/lib/mix/test/mix/tasks/compile.elixir_test.exs +++ b/lib/mix/test/mix/tasks/compile.elixir_test.exs @@ -975,7 +975,7 @@ defmodule Mix.Tasks.Compile.ElixirTest do capture_io(:stderr, fn -> File.write!("lib/a.ex", "defmodule B, do: :not_ok") - assert {:ok, [_, _]} = Mix.Tasks.Compile.Elixir.run(["--verbose"]) + assert {:ok, [_ | _]} = Mix.Tasks.Compile.Elixir.run(["--verbose"]) assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]} assert_received {:mix_shell, :info, ["Compiled lib/b.ex"]} purge([A, B]) From 0bb98d431bcfee9691792dd22b16ec30eabe034b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 27 Oct 2023 18:39:33 +0200 Subject: [PATCH 0092/1886] Upadte CHANGELOG --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c0df6048ed..d5e1d9b99e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -81,10 +81,14 @@ Finally, we have started enriching our documentation with [Mermaid.js](https://m * [Kernel.SpecialForms] Warn if `True`/`False`/`Nil` are used as aliases and there is no such alias * [Macro] Add `Macro.compile_apply/4` * [Module] Add support for `@nifs` annotation from Erlang/OTP 25 + * [Module] Add support for missing `@dialyzer` configuration * [String] Update to Unicode 15.1.0 + * [Task] Add `:limit` option to `Task.yield_many/2` #### Mix + * [mix] Add `MIX_PROFILE` to profile a list of comma separated tasks + * [mix compile.elixir] Optimize scenario where there are thousands of files in `lib/` and one of them is changed * [mix test] Allow testing multiple file:line at once, such as `mix test test/foo_test.exs:13 test/bar_test.exs:27` ### 2. Bug fixes @@ -104,6 +108,10 @@ Finally, we have started enriching our documentation with [Mermaid.js](https://m * [ExUnit] Raise on incorrectly dedented doctests +#### Mix + + * [Mix] Ensure files with duplicate modules are recompiled whenever any of the files change + ### 3. Soft deprecations (no warnings emitted) #### Elixir From 86eebc5b3f4efc6d5dc4d3debc1caa8b5f0ebb7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sat, 28 Oct 2023 09:16:37 +0200 Subject: [PATCH 0093/1886] Do not provide default value for profiling --- lib/mix/lib/mix/cli.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mix/lib/mix/cli.ex b/lib/mix/lib/mix/cli.ex index 10b4eb2f41..5880178ca2 100644 --- a/lib/mix/lib/mix/cli.ex +++ b/lib/mix/lib/mix/cli.ex @@ -10,7 +10,7 @@ defmodule Mix.CLI do if env_variable_activated?("MIX_QUIET"), do: Mix.shell(Mix.Shell.Quiet) if env_variable_activated?("MIX_DEBUG"), do: Mix.debug(true) - if profile = System.get_env("MIX_PROFILE", "") do + if profile = System.get_env("MIX_PROFILE") do Mix.State.put(:profile, String.split(profile, ",")) end From 0dc4d7d9f82cad350de209626926f0586d116ed0 Mon Sep 17 00:00:00 2001 From: Artem Solomatin Date: Sat, 28 Oct 2023 14:53:48 +0300 Subject: [PATCH 0094/1886] Add usage example for Keyword.from_keys (#13041) --- lib/elixir/lib/keyword.ex | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/elixir/lib/keyword.ex b/lib/elixir/lib/keyword.ex index 7b59a4451f..7fa369b258 100644 --- a/lib/elixir/lib/keyword.ex +++ b/lib/elixir/lib/keyword.ex @@ -112,6 +112,8 @@ defmodule Keyword do iex> Keyword.from_keys([:foo, :bar, :baz], :atom) [foo: :atom, bar: :atom, baz: :atom] + iex> Keyword.from_keys([], :atom) + [] """ @doc since: "1.14.0" From 331e565c64c45a4f3e4254ea2f6fb1bbcae38f0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sun, 29 Oct 2023 08:31:24 +0100 Subject: [PATCH 0095/1886] Add a link to Erlang application specification, closes #13042 --- lib/elixir/lib/application.ex | 5 ++++- lib/mix/lib/mix/tasks/compile.app.ex | 6 ++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/elixir/lib/application.ex b/lib/elixir/lib/application.ex index d3bab40966..3a2c856f22 100644 --- a/lib/elixir/lib/application.ex +++ b/lib/elixir/lib/application.ex @@ -200,7 +200,7 @@ defmodule Application do In the sections above, we have configured an application in the `application/0` section of the `mix.exs` file. Ultimately, Mix will use this configuration to create an [*application resource - file*](https://www.erlang.org/doc/man/application.html), which is a file called + file*](https://www.erlang.org/doc/man/app), which is a file called `APP_NAME.app`. For example, the application resource file of the OTP application `ex_unit` is called `ex_unit.app`. @@ -467,6 +467,9 @@ defmodule Application do * #{Enum.map_join(@application_keys, "\n * ", &"`#{inspect(&1)}`")} + For a description of all fields, see [Erlang's application + specification](https://www.erlang.org/doc/man/app). + Note the environment is not returned as it can be accessed via `fetch_env/2`. Returns `nil` if the application is not loaded. """ diff --git a/lib/mix/lib/mix/tasks/compile.app.ex b/lib/mix/lib/mix/tasks/compile.app.ex index 59f80a82ab..4aaaecf6cc 100644 --- a/lib/mix/lib/mix/tasks/compile.app.ex +++ b/lib/mix/lib/mix/tasks/compile.app.ex @@ -78,8 +78,10 @@ defmodule Mix.Tasks.Compile.App do technically valid in any resource file, but it is only effective for applications with a callback module. Defaults to `:infinity`. - Besides the options above, `.app` files also expect other options like - `:modules` and `:vsn`, but these are automatically added by Mix. + Besides the options above, `.app` files also expect other options + like `:modules` and `:vsn`, but these are automatically added by Mix. + The complete list can be found on [Erlang's application + specification](https://www.erlang.org/doc/man/app). ## Command line options From a32ce8b252976ce4f28830576ee58fa6f42bed81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sun, 29 Oct 2023 08:41:15 +0100 Subject: [PATCH 0096/1886] Provide more rationale on do-end --- lib/elixir/pages/getting-started/keywords-and-maps.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/pages/getting-started/keywords-and-maps.md b/lib/elixir/pages/getting-started/keywords-and-maps.md index e36c773dcd..e99b38399f 100644 --- a/lib/elixir/pages/getting-started/keywords-and-maps.md +++ b/lib/elixir/pages/getting-started/keywords-and-maps.md @@ -127,7 +127,7 @@ iex> if true, do: "This will be seen", else: "This won't" Pay close attention to both syntaxes. In the keyword list format, we separate each key-value pair with commas, and each key is followed by `:`. In the `do`-blocks, we get rid of the colons, the commas, and separate each keyword by a newline. They are useful exactly because they remove the verbosity when writing blocks of code. Most of the time, you will use the block syntax, but it is good to know they are equivalent. -Note that only a handful of keyword lists can be converted to blocks: `do`, `else`, `catch`, `rescue`, and `after`. Those are all the keywords used by Elixir control-flow constructs. We have already learned some of them and we will learn others in the future. +This plays an important role in the language as it allows Elixir syntax to stay small but still expressive. We only need few data structures to represent the language, a topic we will come back to when talking about [optional syntax](optional-syntax.md) and go in-depth when discussing [meta-programming](../quote-and-unquote.md). With this out of the way, let's talk about maps. From bb1f1af1136238f1a378f0939d1a4232ed096ff7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sun, 29 Oct 2023 08:51:48 +0100 Subject: [PATCH 0097/1886] Trim trailing whitespace on heredocs with \r\n --- lib/elixir/lib/code/formatter.ex | 1 + lib/elixir/test/elixir/code_formatter/literals_test.exs | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/lib/elixir/lib/code/formatter.ex b/lib/elixir/lib/code/formatter.ex index 063e6d4d30..eb1d0dd773 100644 --- a/lib/elixir/lib/code/formatter.ex +++ b/lib/elixir/lib/code/formatter.ex @@ -1683,6 +1683,7 @@ defmodule Code.Formatter do end defp heredoc_line(["", _ | _]), do: nest(line(), :reset) + defp heredoc_line(["\r", _ | _]), do: nest(line(), :reset) defp heredoc_line(_), do: line() defp args_to_algebra_with_comments(args, meta, skip_parens?, last_arg_mode, join, state, fun) do diff --git a/lib/elixir/test/elixir/code_formatter/literals_test.exs b/lib/elixir/test/elixir/code_formatter/literals_test.exs index f735e8315b..ec614b6949 100644 --- a/lib/elixir/test/elixir/code_formatter/literals_test.exs +++ b/lib/elixir/test/elixir/code_formatter/literals_test.exs @@ -384,6 +384,14 @@ defmodule Code.Formatter.LiteralsTest do """ ''' end + + test "with new lines" do + assert_format ~s|foo do\n """\n foo\n \n bar\n """\nend|, + ~s|foo do\n """\n foo\n\n bar\n """\nend| + + assert_format ~s|foo do\r\n """\r\n foo\r\n \r\n bar\r\n """\r\nend|, + ~s|foo do\n """\n foo\r\n\r\n bar\r\n """\nend| + end end describe "charlist heredocs" do From 5949460d638d17899dc6acf94484f901722ad74c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sun, 29 Oct 2023 12:33:01 +0100 Subject: [PATCH 0098/1886] Start v1.17-dev --- CHANGELOG.md | 132 +----------------- SECURITY.md | 6 +- VERSION | 2 +- bin/elixir | 2 +- bin/elixir.bat | 2 +- .../compatibility-and-deprecations.md | 6 +- 6 files changed, 11 insertions(+), 139 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d5e1d9b99e..a01b1a86cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,143 +1,15 @@ # Changelog for Elixir v1.16 -## Code snippets in diagnostics - -Elixir v1.15 introduced a new compiler diagnostic format and the ability to print multiple error diagnostics per compilation (in addition to multiple warnings). - -With Elixir v1.16, we also include code snippets in exceptions and diagnostics raised by the compiler. For example, a syntax error now includes a pointer to where the error happened: - -``` -** (SyntaxError) invalid syntax found on lib/my_app.ex:1:17: - error: syntax error before: '*' - │ - 1 │ [1, 2, 3, 4, 5, *] - │ ^ - │ - └─ lib/my_app.ex:1:17 -``` - -For mismatched delimiters, it now shows both delimiters: - -``` -** (MismatchedDelimiterError) mismatched delimiter found on lib/my_app.ex:1:18: - error: unexpected token: ) - │ - 1 │ [1, 2, 3, 4, 5, 6) - │ │ └ mismatched closing delimiter (expected "]") - │ └ unclosed delimiter - │ - └─ lib/my_app.ex:1:18 -``` - -Errors and warnings diagnostics also include code snippets. When possible, we will show precise spans, such as on undefined variables: - -``` - error: undefined variable "unknown_var" - │ -5 │ a - unknown_var - │ ^^^^^^^^^^^ - │ - └─ lib/sample.ex:5:9: Sample.foo/1 -``` - -Otherwise the whole line is underlined: - -``` -error: function names should start with lowercase characters or underscore, invalid name CamelCase - │ -3 │ def CamelCase do - │ ^^^^^^^^^^^^^^^^ - │ - └─ lib/sample.ex:3 -``` - -A huge thank you to Vinícius Muller for working on the new diagnostics. - -## Revamped documentation - -Elixir's Getting Started guided has been made part of the Elixir repository and incorporated into ExDoc. This was an opportunity to revisit and unify all official guides and references. - -We have also incorporated and extended the work on [Understanding Code Smells in Elixir Functional Language](https://github.com/lucasvegi/Elixir-Code-Smells/blob/main/etc/2023-emse-code-smells-elixir.pdf), by Lucas Vegi and Marco Tulio Valente, from [ASERG/DCC/UFMG](http://aserg.labsoft.dcc.ufmg.br/), into the official document in the form of anti-patterns. The anti-patterns are divided into four categories: code-related, design-related, process-related, and meta-programming. Our goal is to give all developers with both positive and negative examples of Elixir code, with context and examples on how to improve their codebases. - -Another [ExDoc](https://github.com/elixir-lang/ex_doc) feature we have incorporated in this release is the addition of cheatsheets, starting with [a cheatsheet for the Enum module](https://hexdocs.pm/elixir/main/enum-cheat.html). If you would like to contribute future cheatsheets to Elixir itself, feel free to start a discussion with an issue. - -Finally, we have started enriching our documentation with [Mermaid.js](https://mermaid.js.org/) diagrams. You can find examples in the [GenServer](https://hexdocs.pm/elixir/main/GenServer.html) and [Supervisor](https://hexdocs.pm/elixir/main/Supervisor.html) docs. - -## v1.16.0-dev +## v1.17.0-dev ### 1. Enhancements -#### EEx - - * [EEx] Include relative file information in diagnostics - -#### Elixir - - * [Code] Automatically include columns in parsing options - * [Code] Introduce `MismatchedDelimiterError` for handling mismatched delimiter exceptions - * [Code.Fragment] Handle anonymous calls in fragments - * [Kernel] Suggest module names based on suffix and casing errors when the module does not exist in `UndefinedFunctionError` - * [Kernel.ParallelCompiler] Introduce `Kernel.ParallelCompiler.pmap/2` to compile multiple additional entries in parallel - * [Kernel.SpecialForms] Warn if `True`/`False`/`Nil` are used as aliases and there is no such alias - * [Macro] Add `Macro.compile_apply/4` - * [Module] Add support for `@nifs` annotation from Erlang/OTP 25 - * [Module] Add support for missing `@dialyzer` configuration - * [String] Update to Unicode 15.1.0 - * [Task] Add `:limit` option to `Task.yield_many/2` - -#### Mix - - * [mix] Add `MIX_PROFILE` to profile a list of comma separated tasks - * [mix compile.elixir] Optimize scenario where there are thousands of files in `lib/` and one of them is changed - * [mix test] Allow testing multiple file:line at once, such as `mix test test/foo_test.exs:13 test/bar_test.exs:27` - ### 2. Bug fixes -#### Elixir - - * [Code.Fragment] Fix crash in `Code.Fragment.surround_context/2` when matching on `->` - * [IO] Raise when using `IO.binwrite/2` on terminated device (mirroring `IO.write/2`) - * [Kernel] Do not expand aliases recursively (the alias stored in Macro.Env is already expanded) - * [Kernel] Ensure `dbg` module is a compile-time dependency - * [Kernel] Warn when a private function or macro uses `unquote/1` and the function/macro itself is unused - * [Kernel] Do not define an alias for nested modules starting with `Elixir.` in their definition - * [Kernel.ParallelCompiler] Consider a module has been defined in `@after_compile` callbacks to avoid deadlocks - * [Path] Ensure `Path.relative_to/2` returns a relative path when the given argument does not share a common prefix with `cwd` - -#### ExUnit - - * [ExUnit] Raise on incorrectly dedented doctests - -#### Mix - - * [Mix] Ensure files with duplicate modules are recompiled whenever any of the files change - ### 3. Soft deprecations (no warnings emitted) -#### Elixir - - * [File] Deprecate `File.stream!(file, options, line_or_bytes)` in favor of keeping the options as last argument, as in `File.stream!(file, line_or_bytes, options)` - * [Kernel.ParallelCompiler] Deprecate `Kernel.ParallelCompiler.async/1` in favor of `Kernel.ParallelCompiler.pmap/2` - * [Path] Deprecate `Path.safe_relative_to/2` in favor of `Path.safe_relative/2` - ### 4. Hard deprecations -#### Elixir - - * [Date] Deprecate inferring a range with negative step, call `Date.range/3` with a negative step instead - * [Enum] Deprecate passing a range with negative step on `Enum.slice/2`, give `first..last//1` instead - * [Kernel] `~R/.../` is deprecated in favor of `~r/.../`. This is because `~R/.../` still allowed escape codes, which did not fit the definition of uppercase sigils - * [String] Deprecate passing a range with negative step on `String.slice/2`, give `first..last//1` instead - -#### ExUnit - - * [ExUnit.Formatter] Deprecate `format_time/2`, use `format_times/1` instead - -#### Mix - - * [mix compile.leex] Require `:leex` to be added as a compiler to run the `leex` compiler - * [mix compile.yecc] Require `:yecc` to be added as a compiler to run the `yecc` compiler - ## v1.15 -The CHANGELOG for v1.15 releases can be found [in the v1.15 branch](https://github.com/elixir-lang/elixir/blob/v1.15/CHANGELOG.md). +The CHANGELOG for v1.16 releases can be found [in the v1.16 branch](https://github.com/elixir-lang/elixir/blob/v1.16/CHANGELOG.md). diff --git a/SECURITY.md b/SECURITY.md index 69252d3ad7..a5ec4489d6 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -6,12 +6,12 @@ Elixir applies bug fixes only to the latest minor branch. Security patches are a Elixir version | Support :------------- | :----------------------------- -1.16 | Development -1.15 | Bug fixes and security patches +1.17 | Development +1.16 | Bug fixes and security patches +1.15 | Security patches only 1.14 | Security patches only 1.13 | Security patches only 1.12 | Security patches only -1.11 | Security patches only ## Announcements diff --git a/VERSION b/VERSION index 1f0d2f3351..ee8855caa4 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.16.0-dev +1.17.0-dev diff --git a/bin/elixir b/bin/elixir index cb0a84650b..a30bf064dc 100755 --- a/bin/elixir +++ b/bin/elixir @@ -1,7 +1,7 @@ #!/bin/sh set -e -ELIXIR_VERSION=1.16.0-dev +ELIXIR_VERSION=1.17.0-dev if [ $# -eq 0 ] || { [ $# -eq 1 ] && { [ "$1" = "--help" ] || [ "$1" = "-h" ]; }; }; then cat <&2 diff --git a/bin/elixir.bat b/bin/elixir.bat index a222cbcc65..fddbeb4135 100644 --- a/bin/elixir.bat +++ b/bin/elixir.bat @@ -1,6 +1,6 @@ @if defined ELIXIR_CLI_ECHO (@echo on) else (@echo off) -set ELIXIR_VERSION=1.16.0-dev +set ELIXIR_VERSION=1.17.0-dev setlocal enabledelayedexpansion if ""%1""=="""" if ""%2""=="""" goto documentation diff --git a/lib/elixir/pages/references/compatibility-and-deprecations.md b/lib/elixir/pages/references/compatibility-and-deprecations.md index 9159ae1620..d25ce7ad6f 100644 --- a/lib/elixir/pages/references/compatibility-and-deprecations.md +++ b/lib/elixir/pages/references/compatibility-and-deprecations.md @@ -8,12 +8,12 @@ Elixir applies bug fixes only to the latest minor branch. Security patches are a Elixir version | Support :------------- | :----------------------------- -1.16 | Development -1.15 | Bug fixes and security patches +1.17 | Development +1.16 | Bug fixes and security patches +1.15 | Security patches only 1.14 | Security patches only 1.13 | Security patches only 1.12 | Security patches only -1.11 | 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). From fe8b31eef8244e278c377e70068b1c5e582e906f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sun, 29 Oct 2023 12:52:21 +0100 Subject: [PATCH 0099/1886] Introduce deprecations scheduled for v1.17 --- lib/elixir/lib/enum.ex | 10 +- lib/elixir/lib/io.ex | 24 ++- lib/elixir/lib/kernel.ex | 47 ++++-- lib/elixir/lib/macro.ex | 1 - lib/elixir/lib/range.ex | 12 +- lib/elixir/test/elixir/enum_test.exs | 154 +++++++++--------- lib/elixir/test/elixir/io_test.exs | 24 +-- lib/elixir/test/elixir/kernel/with_test.exs | 2 +- lib/elixir/test/elixir/kernel_test.exs | 24 +-- lib/elixir/test/elixir/range_test.exs | 10 +- lib/elixir/test/elixir/string_io_test.exs | 4 +- lib/elixir/test/elixir/string_test.exs | 2 +- .../test/elixir/task/supervisor_test.exs | 4 +- lib/elixir/test/elixir/task_test.exs | 12 +- lib/ex_unit/lib/ex_unit/case.ex | 4 +- lib/ex_unit/test/ex_unit/register_test.exs | 30 +++- lib/mix/lib/mix/tasks/format.ex | 4 +- lib/mix/lib/mix/tasks/test.ex | 2 +- 18 files changed, 196 insertions(+), 174 deletions(-) diff --git a/lib/elixir/lib/enum.ex b/lib/elixir/lib/enum.ex index e34d665ca4..90fb70a8fa 100644 --- a/lib/elixir/lib/enum.ex +++ b/lib/elixir/lib/enum.ex @@ -2733,7 +2733,7 @@ defmodule Enum do end # Normalize negative input ranges like Enum.slice/2 - def slide(enumerable, first..last, insertion_index) + def slide(enumerable, first..last//_, insertion_index) when first < 0 or last < 0 or insertion_index < 0 do count = Enum.count(enumerable) normalized_first = if first >= 0, do: first, else: Kernel.max(first + count, 0) @@ -2750,23 +2750,23 @@ defmodule Enum do end end - def slide(enumerable, insertion_index.._, insertion_index) do + def slide(enumerable, insertion_index.._//_, insertion_index) do Enum.to_list(enumerable) end - def slide(_, first..last, insertion_index) + def slide(_, first..last//_, insertion_index) when insertion_index > first and insertion_index <= last do raise ArgumentError, "insertion index for slide must be outside the range being moved " <> "(tried to insert #{first}..#{last} at #{insertion_index})" end - def slide(enumerable, first..last, _insertion_index) when first > last do + def slide(enumerable, first..last//_, _insertion_index) when first > last do Enum.to_list(enumerable) end # Guarantees at this point: step size == 1 and first <= last and (insertion_index < first or insertion_index > last) - def slide(enumerable, first..last, insertion_index) do + def slide(enumerable, first..last//_, insertion_index) do impl = if is_list(enumerable), do: &slide_list_start/4, else: &slide_any/4 cond do diff --git a/lib/elixir/lib/io.ex b/lib/elixir/lib/io.ex index d5d0788b6d..5cf21c2089 100644 --- a/lib/elixir/lib/io.ex +++ b/lib/elixir/lib/io.ex @@ -128,8 +128,15 @@ defmodule IO do @doc """ Reads from the IO `device`. - The `device` is iterated by the given number of characters, line by line if - `:line` is given, or until `:eof`. + The `device` is iterated as specified by the `line_or_chars` argument: + + * if `line_or_chars` is an integer, it represents a number of bytes. The device is + iterated by that number of bytes. + + * if `line_or_chars` is `:line`, the device is iterated line by line. + + * if `line_or_chars` is `:eof` (since v1.13), the device is iterated until `:eof`. + If the device is already at the end, it returns `:eof` itself. It returns: @@ -145,8 +152,10 @@ defmodule IO do @spec read(device, :eof | :line | non_neg_integer) :: chardata | nodata def read(device \\ :stdio, line_or_chars) - # TODO: Deprecate me on v1.17 + # TODO: Remove me on v2.0 def read(device, :all) do + IO.warn("IO.read(device, :all) is deprecated, use IO.read(device, :eof) instead") + with :eof <- read(device, :eof) do with [_ | _] = opts <- :io.getopts(device), false <- Keyword.get(opts, :binary, true) do @@ -179,10 +188,8 @@ defmodule IO do * if `line_or_chars` is `:line`, the device is iterated line by line. - * if `line_or_chars` is `:eof`, the device is iterated until `:eof`. `line_or_chars` - can only be `:eof` since Elixir 1.13.0. `:eof` replaces the deprecated `:all`, - with the difference that `:all` returns `""` on end of file, while `:eof` returns - `:eof` itself. + * if `line_or_chars` is `:eof` (since v1.13), the device is iterated until `:eof`. + If the device is already at the end, it returns `:eof` itself. It returns: @@ -200,8 +207,9 @@ defmodule IO do @spec binread(device, :eof | :line | non_neg_integer) :: iodata | nodata def binread(device \\ :stdio, line_or_chars) - # TODO: Deprecate me on v1.17 + # TODO: Remove me on v2.0 def binread(device, :all) do + IO.warn("IO.binread(device, :all) is deprecated, use IO.binread(device, :eof) instead") with :eof <- binread(device, :eof), do: "" end diff --git a/lib/elixir/lib/kernel.ex b/lib/elixir/lib/kernel.ex index 7b37d18163..52983d9b45 100644 --- a/lib/elixir/lib/kernel.ex +++ b/lib/elixir/lib/kernel.ex @@ -3951,24 +3951,36 @@ defmodule Kernel do first = Macro.expand(first, __CALLER__) last = Macro.expand(last, __CALLER__) validate_range!(first, last) - range(__CALLER__.context, first, last) + stepless_range(__CALLER__.context, first, last, __CALLER__) false -> - range(__CALLER__.context, first, last) + stepless_range(__CALLER__.context, first, last, __CALLER__) end end - defp range(_context, first, last) when is_integer(first) and is_integer(last) do - # TODO: Deprecate inferring a range with a step of -1 on Elixir v1.17 - step = if first <= last, do: 1, else: -1 + defp stepless_range(_context, first, last, caller) + when is_integer(first) and is_integer(last) do + step = + if first <= last do + 1 + else + # TODO: Always use 1 as an step in Elixir v2.0 + IO.warn( + "#{first}..#{last} has a default step of -1, please write #{first}..#{last}//-1 instead", + Macro.Env.stacktrace(caller) + ) + + -1 + end + {:%{}, [], [__struct__: Elixir.Range, first: first, last: last, step: step]} end - defp range(nil, first, last) do + defp stepless_range(nil, first, last, _caller) do quote(do: Elixir.Range.new(unquote(first), unquote(last))) end - defp range(:guard, first, last) do + defp stepless_range(:guard, first, last, caller) do # We need to compute the step using guards. We don't have conditionals, # but we can emulate them using a map access. step = @@ -3979,12 +3991,27 @@ defmodule Kernel do ) end - # TODO: Deprecate me inside guard when sides are not integers on Elixir v1.17 + # TODO: Always assume step 1 in Elixir v2.0 + range = "#{Macro.to_string(first)}..#{Macro.to_string(last)}" + + IO.warn( + "#{range} inside guards requires an explicit step, please write #{range}//1 or #{range}//-1 instead", + Macro.Env.stacktrace(caller) + ) + {:%{}, [], [__struct__: Elixir.Range, first: first, last: last, step: step]} end - defp range(:match, first, last) do - # TODO: Deprecate me inside match in all occasions (including literals) on Elixir v1.17 + defp stepless_range(:match, first, last, caller) do + # TODO: Make me an error in Elixir v2.0 + range = "#{Macro.to_string(first)}..#{Macro.to_string(last)}" + + IO.warn( + "#{range} inside match is deprecated, " <> + "you must always match on the step: #{range}//var or #{range}//_ if you want to ignore it", + Macro.Env.stacktrace(caller) + ) + {:%{}, [], [__struct__: Elixir.Range, first: first, last: last]} end diff --git a/lib/elixir/lib/macro.ex b/lib/elixir/lib/macro.ex index 844ed55201..5d3ab5bc55 100644 --- a/lib/elixir/lib/macro.ex +++ b/lib/elixir/lib/macro.ex @@ -1130,7 +1130,6 @@ defmodule Macro do """ @spec to_string(t()) :: String.t() - # TODO: Allow line_length to be configurable on v1.17 def to_string(tree) do doc = Inspect.Algebra.format(Code.quoted_to_algebra(tree), 98) IO.iodata_to_binary(doc) diff --git a/lib/elixir/lib/range.ex b/lib/elixir/lib/range.ex index ad86e609df..c59a28a93a 100644 --- a/lib/elixir/lib/range.ex +++ b/lib/elixir/lib/range.ex @@ -184,7 +184,7 @@ defmodule Range do @spec new(limit, limit) :: t def new(first, last) when is_integer(first) and is_integer(last) do - # TODO: Deprecate inferring a range with a step of -1 on Elixir v1.17 + # TODO: Deprecate inferring a range with a step of -1 on Elixir v1.18 step = if first <= last, do: 1, else: -1 %Range{first: first, last: last, step: step} end @@ -231,8 +231,6 @@ defmodule Range do iex> Range.size(1..10//-1) 0 - iex> Range.size(10..1) - 10 iex> Range.size(10..1//-1) 10 iex> Range.size(10..1//-2) @@ -428,7 +426,7 @@ defmodule Range do iex> Range.disjoint?(1..5, 6..9) true - iex> Range.disjoint?(5..1, 6..9) + iex> Range.disjoint?(5..1//-1, 6..9) true iex> Range.disjoint?(1..5, 5..9) false @@ -508,8 +506,10 @@ defmodule Range do @doc false @deprecated "Pattern match on first..last//step instead" - def range?(term) - def range?(first..last) when is_integer(first) and is_integer(last), do: true + def range?(%{__struct__: Range, first: first, last: last}) + when is_integer(first) and is_integer(last), + do: true + def range?(_), do: false end diff --git a/lib/elixir/test/elixir/enum_test.exs b/lib/elixir/test/elixir/enum_test.exs index ffb9c0bf8f..ebb4fd3188 100644 --- a/lib/elixir/test/elixir/enum_test.exs +++ b/lib/elixir/test/elixir/enum_test.exs @@ -851,7 +851,7 @@ defmodule EnumTest do end test "handles negative indices" do - make_negative_range = fn first..last, length -> + make_negative_range = fn first..last//1, length -> (first - length)..(last - length)//1 end @@ -890,7 +890,7 @@ defmodule EnumTest do end test "raises an error when the step is not exactly 1" do - slide_ranges_that_should_fail = [2..10//2, 8..-1, 10..2//-1, 10..4//-2, -1..-8//-1] + slide_ranges_that_should_fail = [2..10//2, 8..-1//-1, 10..2//-1, 10..4//-2, -1..-8//-1] for zero_to_20 <- [0..20, Enum.to_list(0..20)], range_that_should_fail <- slide_ranges_that_should_fail do @@ -1295,8 +1295,8 @@ defmodule EnumTest do assert Enum.sum([-3, -2, -1, 0, 1, 2, 3]) == 0 assert Enum.sum(42..42) == 42 assert Enum.sum(11..17) == 98 - assert Enum.sum(17..11) == 98 - assert Enum.sum(11..-17) == Enum.sum(-17..11) + assert Enum.sum(17..11//-1) == 98 + assert Enum.sum(11..-17//-1) == Enum.sum(-17..11) assert_raise ArithmeticError, fn -> Enum.sum([{}]) @@ -1313,7 +1313,7 @@ defmodule EnumTest do assert Enum.product([1, 2, 3, 4, 5]) == 120 assert Enum.product([1, -2, 3, 4, 5]) == -120 assert Enum.product(1..5) == 120 - assert Enum.product(11..-17) == Enum.product(-17..11) + assert Enum.product(11..-17//-1) == Enum.product(-17..11) assert_raise ArithmeticError, fn -> Enum.product([{}]) @@ -1579,7 +1579,7 @@ defmodule EnumTest.Range do test "all?/2" do assert Enum.all?(0..1) - assert Enum.all?(1..0) + assert Enum.all?(1..0//-1) refute Enum.all?(0..5, fn x -> rem(x, 2) == 0 end) assert Enum.all?(0..1, fn x -> x < 2 end) @@ -1589,7 +1589,7 @@ defmodule EnumTest.Range do end test "any?/2" do - assert Enum.any?(1..0) + assert Enum.any?(1..0//-1) refute Enum.any?(0..5, &(&1 > 10)) assert Enum.any?(0..5, &(&1 > 3)) @@ -1691,7 +1691,7 @@ defmodule EnumTest.Range do assert Enum.drop(1..3, -1) == [1, 2] assert Enum.drop(1..3, -2) == [1] assert Enum.drop(1..3, -4) == [] - assert Enum.drop(1..0, 3) == [] + assert Enum.drop(1..0//-1, 3) == [] assert Enum.drop(1..9//2, 2) == [5, 7, 9] assert Enum.drop(1..9//2, -2) == [1, 3, 5] @@ -1720,12 +1720,12 @@ defmodule EnumTest.Range do assert Enum.drop_while(0..6, fn x -> x <= 3 end) == [4, 5, 6] assert Enum.drop_while(0..6, fn _ -> false end) == [0, 1, 2, 3, 4, 5, 6] assert Enum.drop_while(0..3, fn x -> x <= 3 end) == [] - assert Enum.drop_while(1..0, fn _ -> nil end) == [1, 0] + assert Enum.drop_while(1..0//-1, fn _ -> nil end) == [1, 0] end test "each/2" do try do - assert Enum.each(1..0, fn x -> x end) == :ok + assert Enum.each(1..0//-1, fn x -> x end) == :ok assert Enum.each(1..3, fn x -> Process.put(:enum_test_each, x * 2) end) == :ok assert Process.get(:enum_test_each) == 6 after @@ -1733,7 +1733,7 @@ defmodule EnumTest.Range do end try do - assert Enum.each(-1..-3, fn x -> Process.put(:enum_test_each, x * 2) end) == :ok + assert Enum.each(-1..-3//-1, fn x -> Process.put(:enum_test_each, x * 2) end) == :ok assert Process.get(:enum_test_each) == -6 after Process.delete(:enum_test_each) @@ -1741,7 +1741,7 @@ defmodule EnumTest.Range do end test "empty?/1" do - refute Enum.empty?(1..0) + refute Enum.empty?(1..0//-1) refute Enum.empty?(1..2) refute Enum.empty?(1..2//2) assert Enum.empty?(1..2//-2) @@ -1762,17 +1762,17 @@ defmodule EnumTest.Range do assert Enum.fetch(-10..20, -32) == :error # descending order - assert Enum.fetch(20..-10, 4) == {:ok, 16} - assert Enum.fetch(20..-10, -4) == {:ok, -7} + assert Enum.fetch(20..-10//-1, 4) == {:ok, 16} + assert Enum.fetch(20..-10//-1, -4) == {:ok, -7} # descending order, first - assert Enum.fetch(20..-10, 0) == {:ok, 20} - assert Enum.fetch(20..-10, -31) == {:ok, 20} + assert Enum.fetch(20..-10//-1, 0) == {:ok, 20} + assert Enum.fetch(20..-10//-1, -31) == {:ok, 20} # descending order, last - assert Enum.fetch(20..-10, -1) == {:ok, -10} - assert Enum.fetch(20..-10, 30) == {:ok, -10} + assert Enum.fetch(20..-10//-1, -1) == {:ok, -10} + assert Enum.fetch(20..-10//-1, 30) == {:ok, -10} # descending order, out of bound - assert Enum.fetch(20..-10, 31) == :error - assert Enum.fetch(20..-10, -32) == :error + assert Enum.fetch(20..-10//-1, 31) == :error + assert Enum.fetch(20..-10//-1, -32) == :error # edge cases assert Enum.fetch(42..42, 0) == {:ok, 42} @@ -1791,15 +1791,15 @@ defmodule EnumTest.Range do assert Enum.fetch!(2..6, 4) == 6 assert Enum.fetch!(2..6, -1) == 6 assert Enum.fetch!(2..6, -2) == 5 - assert Enum.fetch!(-2..-6, 0) == -2 - assert Enum.fetch!(-2..-6, 4) == -6 + assert Enum.fetch!(-2..-6//-1, 0) == -2 + assert Enum.fetch!(-2..-6//-1, 4) == -6 assert_raise Enum.OutOfBoundsError, fn -> Enum.fetch!(2..6, 8) end assert_raise Enum.OutOfBoundsError, fn -> - Enum.fetch!(-2..-6, 8) + Enum.fetch!(-2..-6//-1, 8) end assert_raise Enum.OutOfBoundsError, fn -> @@ -1849,7 +1849,7 @@ defmodule EnumTest.Range do end test "intersperse/2" do - assert Enum.intersperse(1..0, true) == [1, true, 0] + assert Enum.intersperse(1..0//-1, true) == [1, true, 0] assert Enum.intersperse(1..3, false) == [1, false, 2, false, 3] end @@ -1863,14 +1863,14 @@ defmodule EnumTest.Range do end test "join/2" do - assert Enum.join(1..0, " = ") == "1 = 0" + assert Enum.join(1..0//-1, " = ") == "1 = 0" assert Enum.join(1..3, " = ") == "1 = 2 = 3" assert Enum.join(1..3) == "123" end test "map/2" do assert Enum.map(1..3, fn x -> x * 2 end) == [2, 4, 6] - assert Enum.map(-1..-3, fn x -> x * 2 end) == [-2, -4, -6] + assert Enum.map(-1..-3//-1, fn x -> x * 2 end) == [-2, -4, -6] assert Enum.map(1..10//2, fn x -> x * 2 end) == [2, 6, 10, 14, 18] assert Enum.map(3..1//-2, fn x -> x * 2 end) == [6, 2] assert Enum.map(0..1//-1, fn x -> x * 2 end) == [] @@ -1879,7 +1879,7 @@ defmodule EnumTest.Range do test "map_every/3" do assert Enum.map_every(1..10, 2, fn x -> x * 2 end) == [2, 2, 6, 4, 10, 6, 14, 8, 18, 10] - assert Enum.map_every(-1..-10, 2, fn x -> x * 2 end) == + assert Enum.map_every(-1..-10//-1, 2, fn x -> x * 2 end) == [-2, -2, -6, -4, -10, -6, -14, -8, -18, -10] assert Enum.map_every(1..2, 2, fn x -> x * 2 end) == [2, 2] @@ -1896,20 +1896,20 @@ defmodule EnumTest.Range do end test "map_join/3" do - assert Enum.map_join(1..0, " = ", &(&1 * 2)) == "2 = 0" + assert Enum.map_join(1..0//-1, " = ", &(&1 * 2)) == "2 = 0" assert Enum.map_join(1..3, " = ", &(&1 * 2)) == "2 = 4 = 6" assert Enum.map_join(1..3, &(&1 * 2)) == "246" end test "map_reduce/3" do - assert Enum.map_reduce(1..0, 1, fn x, acc -> {x * 2, x + acc} end) == {[2, 0], 2} + assert Enum.map_reduce(1..0//-1, 1, fn x, acc -> {x * 2, x + acc} end) == {[2, 0], 2} assert Enum.map_reduce(1..3, 1, fn x, acc -> {x * 2, x + acc} end) == {[2, 4, 6], 7} end test "max/1" do assert Enum.max(1..1) == 1 assert Enum.max(1..3) == 3 - assert Enum.max(3..1) == 3 + assert Enum.max(3..1//-1) == 3 assert Enum.max(1..9//2) == 9 assert Enum.max(1..10//2) == 9 @@ -1967,7 +1967,7 @@ defmodule EnumTest.Range do test "min_max/1" do assert Enum.min_max(1..1) == {1, 1} assert Enum.min_max(1..3) == {1, 3} - assert Enum.min_max(3..1) == {1, 3} + assert Enum.min_max(3..1//-1) == {1, 3} assert Enum.min_max(1..9//2) == {1, 9} assert Enum.min_max(1..10//2) == {1, 9} @@ -1999,7 +1999,7 @@ defmodule EnumTest.Range do :rand.seed(:exsss, seed1) assert Enum.random(1..2) == 1 assert Enum.random(1..3) == 1 - assert Enum.random(3..1) == 2 + assert Enum.random(3..1//-1) == 2 :rand.seed(:exsss, seed2) assert Enum.random(1..2) == 1 @@ -2016,7 +2016,7 @@ defmodule EnumTest.Range do end test "reduce/3" do - assert Enum.reduce(1..0, 1, fn x, acc -> x + acc end) == 2 + assert Enum.reduce(1..0//-1, 1, fn x, acc -> x + acc end) == 2 assert Enum.reduce(1..3, 1, fn x, acc -> x + acc end) == 7 assert Enum.reduce(1..10//2, 1, fn x, acc -> x + acc end) == 26 assert Enum.reduce(0..1//-1, 1, fn x, acc -> x + acc end) == 1 @@ -2120,22 +2120,22 @@ defmodule EnumTest.Range do assert Enum.slice(1..5, -4..-1//4) == [2] assert Enum.slice(1..5, -4..-1//5) == [2] - assert Enum.slice(5..1, 0..0) == [5] - assert Enum.slice(5..1, 0..1) == [5, 4] - assert Enum.slice(5..1, 0..2) == [5, 4, 3] - assert Enum.slice(5..1, 1..2) == [4, 3] - assert Enum.slice(5..1, 1..0//1) == [] - assert Enum.slice(5..1, 2..5) == [3, 2, 1] - assert Enum.slice(5..1, 2..6) == [3, 2, 1] - assert Enum.slice(5..1, 4..4) == [1] - assert Enum.slice(5..1, 5..5) == [] - assert Enum.slice(5..1, 6..5//1) == [] - assert Enum.slice(5..1, 6..0//1) == [] - assert Enum.slice(5..1, -6..0) == [5] - assert Enum.slice(5..1, -6..5) == [5, 4, 3, 2, 1] - assert Enum.slice(5..1, -6..-1) == [5, 4, 3, 2, 1] - assert Enum.slice(5..1, -5..-1) == [5, 4, 3, 2, 1] - assert Enum.slice(5..1, -5..-3) == [5, 4, 3] + assert Enum.slice(5..1//-1, 0..0) == [5] + assert Enum.slice(5..1//-1, 0..1) == [5, 4] + assert Enum.slice(5..1//-1, 0..2) == [5, 4, 3] + assert Enum.slice(5..1//-1, 1..2) == [4, 3] + assert Enum.slice(5..1//-1, 1..0//1) == [] + assert Enum.slice(5..1//-1, 2..5) == [3, 2, 1] + assert Enum.slice(5..1//-1, 2..6) == [3, 2, 1] + assert Enum.slice(5..1//-1, 4..4) == [1] + assert Enum.slice(5..1//-1, 5..5) == [] + assert Enum.slice(5..1//-1, 6..5//1) == [] + assert Enum.slice(5..1//-1, 6..0//1) == [] + assert Enum.slice(5..1//-1, -6..0) == [5] + assert Enum.slice(5..1//-1, -6..5) == [5, 4, 3, 2, 1] + assert Enum.slice(5..1//-1, -6..-1) == [5, 4, 3, 2, 1] + assert Enum.slice(5..1//-1, -5..-1) == [5, 4, 3, 2, 1] + assert Enum.slice(5..1//-1, -5..-3) == [5, 4, 3] assert Enum.slice(1..10//2, 0..0) == [1] assert Enum.slice(1..10//2, 0..1) == [1, 3] @@ -2189,19 +2189,19 @@ defmodule EnumTest.Range do Enum.slice(1..5, 0, 0.99) end - assert Enum.slice(5..1, 0, 0) == [] - assert Enum.slice(5..1, 0, 1) == [5] - assert Enum.slice(5..1, 0, 2) == [5, 4] - assert Enum.slice(5..1, 1, 2) == [4, 3] - assert Enum.slice(5..1, 1, 0) == [] - assert Enum.slice(5..1, 2, 3) == [3, 2, 1] - assert Enum.slice(5..1, 2, 6) == [3, 2, 1] - assert Enum.slice(5..1, 4, 4) == [1] - assert Enum.slice(5..1, 5, 5) == [] - assert Enum.slice(5..1, 6, 5) == [] - assert Enum.slice(5..1, 6, 0) == [] - assert Enum.slice(5..1, -6, 0) == [] - assert Enum.slice(5..1, -6, 5) == [5, 4, 3, 2, 1] + assert Enum.slice(5..1//-1, 0, 0) == [] + assert Enum.slice(5..1//-1, 0, 1) == [5] + assert Enum.slice(5..1//-1, 0, 2) == [5, 4] + assert Enum.slice(5..1//-1, 1, 2) == [4, 3] + assert Enum.slice(5..1//-1, 1, 0) == [] + assert Enum.slice(5..1//-1, 2, 3) == [3, 2, 1] + assert Enum.slice(5..1//-1, 2, 6) == [3, 2, 1] + assert Enum.slice(5..1//-1, 4, 4) == [1] + assert Enum.slice(5..1//-1, 5, 5) == [] + assert Enum.slice(5..1//-1, 6, 5) == [] + assert Enum.slice(5..1//-1, 6, 0) == [] + assert Enum.slice(5..1//-1, -6, 0) == [] + assert Enum.slice(5..1//-1, -6, 5) == [5, 4, 3, 2, 1] assert Enum.slice(1..10//2, 0, 0) == [] assert Enum.slice(1..10//2, 0, 1) == [1] @@ -2220,24 +2220,24 @@ defmodule EnumTest.Range do end test "sort/1" do - assert Enum.sort(3..1) == [1, 2, 3] - assert Enum.sort(2..1) == [1, 2] + assert Enum.sort(3..1//-1) == [1, 2, 3] + assert Enum.sort(2..1//-1) == [1, 2] assert Enum.sort(1..1) == [1] end test "sort/2" do - assert Enum.sort(3..1, &(&1 > &2)) == [3, 2, 1] - assert Enum.sort(2..1, &(&1 > &2)) == [2, 1] + assert Enum.sort(3..1//-1, &(&1 > &2)) == [3, 2, 1] + assert Enum.sort(2..1//-1, &(&1 > &2)) == [2, 1] assert Enum.sort(1..1, &(&1 > &2)) == [1] - assert Enum.sort(3..1, :asc) == [1, 2, 3] - assert Enum.sort(3..1, :desc) == [3, 2, 1] + assert Enum.sort(3..1//-1, :asc) == [1, 2, 3] + assert Enum.sort(3..1//-1, :desc) == [3, 2, 1] end test "sort_by/2" do - assert Enum.sort_by(3..1, & &1) == [1, 2, 3] - assert Enum.sort_by(3..1, & &1, :asc) == [1, 2, 3] - assert Enum.sort_by(3..1, & &1, :desc) == [3, 2, 1] + assert Enum.sort_by(3..1//-1, & &1) == [1, 2, 3] + assert Enum.sort_by(3..1//-1, & &1, :asc) == [1, 2, 3] + assert Enum.sort_by(3..1//-1, & &1, :desc) == [3, 2, 1] end test "split/2" do @@ -2250,7 +2250,7 @@ defmodule EnumTest.Range do assert Enum.split(1..3, -2) == {[1], [2, 3]} assert Enum.split(1..3, -3) == {[], [1, 2, 3]} assert Enum.split(1..3, -10) == {[], [1, 2, 3]} - assert Enum.split(1..0, 3) == {[1, 0], []} + assert Enum.split(1..0//-1, 3) == {[1, 0], []} end test "split_while/2" do @@ -2261,7 +2261,7 @@ defmodule EnumTest.Range do assert Enum.split_while(1..3, fn x -> x > 3 end) == {[], [1, 2, 3]} assert Enum.split_while(1..3, fn x -> x < 3 end) == {[1, 2], [3]} assert Enum.split_while(1..3, fn x -> x end) == {[1, 2, 3], []} - assert Enum.split_while(1..0, fn _ -> true end) == {[1, 0], []} + assert Enum.split_while(1..0//-1, fn _ -> true end) == {[1, 0], []} end test "sum/1" do @@ -2270,8 +2270,8 @@ defmodule EnumTest.Range do assert Enum.sum(1..3) == 6 assert Enum.sum(0..100) == 5050 assert Enum.sum(10..100) == 5005 - assert Enum.sum(100..10) == 5005 - assert Enum.sum(-10..-20) == -165 + assert Enum.sum(100..10//-1) == 5005 + assert Enum.sum(-10..-20//-1) == -165 assert Enum.sum(-10..2) == -52 assert Enum.sum(0..1//-1) == 0 @@ -2289,7 +2289,7 @@ defmodule EnumTest.Range do assert Enum.take(1..3, -1) == [3] assert Enum.take(1..3, -2) == [2, 3] assert Enum.take(1..3, -4) == [1, 2, 3] - assert Enum.take(1..0, 3) == [1, 0] + assert Enum.take(1..0//-1, 3) == [1, 0] assert Enum.take(1..0//1, -3) == [] end @@ -2324,7 +2324,7 @@ defmodule EnumTest.Range do :rand.seed(:exsss, seed1) assert Enum.take_random(1..3, 4) == [3, 1, 2] :rand.seed(:exsss, seed1) - assert Enum.take_random(3..1, 1) == [1] + assert Enum.take_random(3..1//-1, 1) == [1] :rand.seed(:exsss, seed2) assert Enum.take_random(1..3, 1) == [1] :rand.seed(:exsss, seed2) diff --git a/lib/elixir/test/elixir/io_test.exs b/lib/elixir/test/elixir/io_test.exs index 3dbcd18023..6786a318f8 100644 --- a/lib/elixir/test/elixir/io_test.exs +++ b/lib/elixir/test/elixir/io_test.exs @@ -21,17 +21,17 @@ defmodule IOTest do test "read all charlist" do {:ok, file} = File.open(Path.expand(~c"fixtures/multiline_file.txt", __DIR__), [:charlist]) - assert ~c"this is the first line\nthis is the second line\n" == IO.read(file, :all) + assert ~c"this is the first line\nthis is the second line\n" == IO.read(file, :eof) assert File.close(file) == :ok end test "read empty file" do {:ok, file} = File.open(Path.expand(~c"fixtures/cp_mode", __DIR__), []) - assert IO.read(file, :all) == "" + assert IO.read(file, :eof) == :eof assert File.close(file) == :ok {:ok, file} = File.open(Path.expand(~c"fixtures/cp_mode", __DIR__), [:charlist]) - assert IO.read(file, :all) == ~c"" + assert IO.read(file, :eof) == :eof assert File.close(file) == :ok end @@ -41,9 +41,9 @@ defmodule IOTest do assert File.close(file) == :ok end - test "binread all" do + test "binread eof" do {:ok, file} = File.open(Path.expand(~c"fixtures/file.bin", __DIR__)) - assert "LF\nCR\rCRLF\r\nLFCR\n\r" == IO.binread(file, :all) + assert "LF\nCR\rCRLF\r\nLFCR\n\r" == IO.binread(file, :eof) assert File.close(file) == :ok end @@ -96,13 +96,6 @@ defmodule IOTest do assert File.close(file) == :ok end - test "read with all" do - {:ok, file} = File.open(Path.expand(~c"fixtures/file.txt", __DIR__)) - assert "FOO\n" == IO.read(file, :all) - assert "" == IO.read(file, :all) - assert File.close(file) == :ok - end - test "read with eof" do {:ok, file} = File.open(Path.expand(~c"fixtures/file.txt", __DIR__)) assert "FOO\n" == IO.read(file, :eof) @@ -131,13 +124,6 @@ defmodule IOTest do assert File.close(file) == :ok end - test "binread with all" do - {:ok, file} = File.open(Path.expand(~c"fixtures/utf8.txt", __DIR__)) - assert "Русский\n日\n" == IO.binread(file, :all) - assert "" == IO.binread(file, :all) - assert File.close(file) == :ok - end - test "binread with eof" do {:ok, file} = File.open(Path.expand(~c"fixtures/utf8.txt", __DIR__)) assert "Русский\n日\n" == IO.binread(file, :eof) diff --git a/lib/elixir/test/elixir/kernel/with_test.exs b/lib/elixir/test/elixir/kernel/with_test.exs index d2e202af45..0ad008186b 100644 --- a/lib/elixir/test/elixir/kernel/with_test.exs +++ b/lib/elixir/test/elixir/kernel/with_test.exs @@ -9,7 +9,7 @@ defmodule Kernel.WithTest do end test "matching with" do - assert with(_..42 <- 1..42, do: :ok) == :ok + assert with(_..42//_ <- 1..42, do: :ok) == :ok assert with({:ok, res} <- error(), do: res) == :error assert with({:ok, _} = res <- ok(42), do: elem(res, 1)) == 42 end diff --git a/lib/elixir/test/elixir/kernel_test.exs b/lib/elixir/test/elixir/kernel_test.exs index c75fa2062b..220702fb8d 100644 --- a/lib/elixir/test/elixir/kernel_test.exs +++ b/lib/elixir/test/elixir/kernel_test.exs @@ -534,28 +534,6 @@ defmodule KernelTest do assert fun_in(17.0) == :none end - def dynamic_in(x, y, z) when x in y..z, do: true - def dynamic_in(_x, _y, _z), do: false - - test "in dynamic range function guard" do - assert dynamic_in(1, 1, 3) - assert dynamic_in(2, 1, 3) - assert dynamic_in(3, 1, 3) - - assert dynamic_in(1, 3, 1) - assert dynamic_in(2, 3, 1) - assert dynamic_in(3, 3, 1) - - refute dynamic_in(0, 1, 3) - refute dynamic_in(4, 1, 3) - refute dynamic_in(0, 3, 1) - refute dynamic_in(4, 3, 1) - - refute dynamic_in(2, 1.0, 3) - refute dynamic_in(2, 1, 3.0) - refute dynamic_in(2.0, 1, 3) - end - def dynamic_step_in(x, y, z, w) when x in y..z//w, do: true def dynamic_step_in(_x, _y, _z, _w), do: false @@ -597,7 +575,7 @@ defmodule KernelTest do assert case_in(1, 1..3) == true assert case_in(2, 1..3) == true assert case_in(3, 1..3) == true - assert case_in(-3, -1..-3) == true + assert case_in(-3, -1..-3//-1) == true end def map_dot(map) when map.field, do: true diff --git a/lib/elixir/test/elixir/range_test.exs b/lib/elixir/test/elixir/range_test.exs index a1de723ac8..8f0b5485b6 100644 --- a/lib/elixir/test/elixir/range_test.exs +++ b/lib/elixir/test/elixir/range_test.exs @@ -5,8 +5,8 @@ defmodule RangeTest do doctest Range - defp reverse(first..last) do - last..first + defp reverse(first..last//step) do + last..first//-step end defp assert_disjoint(r1, r2) do @@ -39,7 +39,7 @@ defmodule RangeTest do assert (1..3).first == 1 assert (1..3).last == 3 assert (1..3).step == 1 - assert (3..1).step == -1 + assert (3..1//-1).step == -1 assert (1..3//2).step == 2 end @@ -47,7 +47,7 @@ defmodule RangeTest do assert inspect(1..3) == "1..3" assert inspect(1..3//2) == "1..3//2" - assert inspect(3..1) == "3..1//-1" + assert inspect(3..1//-1) == "3..1//-1" assert inspect(3..1//1) == "3..1//1" end @@ -59,7 +59,7 @@ defmodule RangeTest do test "in guard equality" do case {1, 1..1} do - {n, range} when range == n..n -> true + {n, range} when range == n..n//1 -> true end end diff --git a/lib/elixir/test/elixir/string_io_test.exs b/lib/elixir/test/elixir/string_io_test.exs index 479f27aca5..03c070aab9 100644 --- a/lib/elixir/test/elixir/string_io_test.exs +++ b/lib/elixir/test/elixir/string_io_test.exs @@ -332,14 +332,14 @@ defmodule StringIOTest do {:ok, pid} = StringIO.open("abcdefg") result = get_until(pid, :unicode, "", GetUntilCallbacks, :up_to_3_bytes) assert result == "abc" - assert IO.read(pid, :all) == "defg" + assert IO.read(pid, :eof) == "defg" end test "get_until with up_to_3_bytes_discard_rest" do {:ok, pid} = StringIO.open("abcdefg") result = get_until(pid, :unicode, "", GetUntilCallbacks, :up_to_3_bytes_discard_rest) assert result == "abc" - assert IO.read(pid, :all) == "" + assert IO.read(pid, :eof) == :eof end test "get_until with until_eof" do diff --git a/lib/elixir/test/elixir/string_test.exs b/lib/elixir/test/elixir/string_test.exs index d644796b3a..830ed1fc01 100644 --- a/lib/elixir/test/elixir/string_test.exs +++ b/lib/elixir/test/elixir/string_test.exs @@ -781,7 +781,7 @@ defmodule StringTest do end assert ExUnit.CaptureIO.capture_io(:stderr, fn -> - assert String.slice("elixir", 0..-2) == "elixi" + assert String.slice("elixir", 0..-2//-1) == "elixi" end) =~ "negative steps are not supported in String.slice/2, pass 0..-2//1 instead" end diff --git a/lib/elixir/test/elixir/task/supervisor_test.exs b/lib/elixir/test/elixir/task/supervisor_test.exs index 754efdd0e4..3656f573c9 100644 --- a/lib/elixir/test/elixir/task/supervisor_test.exs +++ b/lib/elixir/test/elixir/task/supervisor_test.exs @@ -383,7 +383,7 @@ defmodule Task.SupervisorTest do Process.flag(:trap_exit, true) assert supervisor - |> Task.Supervisor.async_stream(4..1, &sleep/1, @opts) + |> Task.Supervisor.async_stream(4..1//-1, &sleep/1, @opts) |> Enum.to_list() == [ok: 4, ok: 3, ok: 2, ok: 1] end @@ -504,7 +504,7 @@ defmodule Task.SupervisorTest do test "streams an enumerable with slowest first", %{supervisor: supervisor} do assert supervisor - |> Task.Supervisor.async_stream_nolink(4..1, &sleep/1, @opts) + |> Task.Supervisor.async_stream_nolink(4..1//-1, &sleep/1, @opts) |> Enum.to_list() == [ok: 4, ok: 3, ok: 2, ok: 1] end diff --git a/lib/elixir/test/elixir/task_test.exs b/lib/elixir/test/elixir/task_test.exs index 4c7d3215ec..f5ab6f55b8 100644 --- a/lib/elixir/test/elixir/task_test.exs +++ b/lib/elixir/test/elixir/task_test.exs @@ -842,13 +842,13 @@ defmodule TaskTest do test "streams an enumerable with ordered: false" do opts = [max_concurrency: 1, ordered: false] - assert 4..1 + assert 4..1//-1 |> Task.async_stream(&sleep(&1 * 100), opts) |> Enum.to_list() == [ok: 400, ok: 300, ok: 200, ok: 100] opts = [max_concurrency: 4, ordered: false] - assert 4..1 + assert 4..1//-1 |> Task.async_stream(&sleep(&1 * 100), opts) |> Enum.to_list() == [ok: 100, ok: 200, ok: 300, ok: 400] end @@ -943,7 +943,7 @@ defmodule TaskTest do test "streams an enumerable with slowest first" do Process.flag(:trap_exit, true) - assert 4..1 + assert 4..1//-1 |> Task.async_stream(&sleep/1, @opts) |> Enum.to_list() == [ok: 4, ok: 3, ok: 2, ok: 1] end @@ -986,7 +986,7 @@ defmodule TaskTest do end test "is zippable with slowest first" do - task = 4..1 |> Task.async_stream(&sleep/1, @opts) |> Stream.map(&elem(&1, 1)) + task = 4..1//-1 |> Task.async_stream(&sleep/1, @opts) |> Stream.map(&elem(&1, 1)) assert Enum.zip(task, task) == [{4, 4}, {3, 3}, {2, 2}, {1, 1}] end @@ -1007,7 +1007,7 @@ defmodule TaskTest do end test "with inner halt and slowest first" do - assert 8..1 + assert 8..1//-1 |> Stream.take(4) |> Task.async_stream(&sleep/1, @opts) |> Enum.to_list() == [ok: 8, ok: 7, ok: 6, ok: 5] @@ -1028,7 +1028,7 @@ defmodule TaskTest do end test "with outer halt and slowest first" do - assert 8..1 + assert 8..1//-1 |> Task.async_stream(&sleep/1, @opts) |> Enum.take(4) == [ok: 8, ok: 7, ok: 6, ok: 5] end diff --git a/lib/ex_unit/lib/ex_unit/case.ex b/lib/ex_unit/lib/ex_unit/case.ex index 7cbeefa828..579a6ee16c 100644 --- a/lib/ex_unit/lib/ex_unit/case.ex +++ b/lib/ex_unit/lib/ex_unit/case.ex @@ -614,8 +614,8 @@ defmodule ExUnit.Case do This function is deprecated in favor of `register_test/6` which performs better under tight loops by avoiding `__ENV__`. """ - # TODO: Deprecate on Elixir v1.17 - @doc deprecated: "Use register_test/6 instead" + # TODO: Remove me Elixir v2.0 + @deprecated "Use register_test/6 instead" @doc since: "1.3.0" def register_test(%{module: mod, file: file, line: line}, test_type, name, tags) do register_test(mod, file, line, test_type, name, tags) diff --git a/lib/ex_unit/test/ex_unit/register_test.exs b/lib/ex_unit/test/ex_unit/register_test.exs index 57cba7ea41..8bd5a92325 100644 --- a/lib/ex_unit/test/ex_unit/register_test.exs +++ b/lib/ex_unit/test/ex_unit/register_test.exs @@ -15,7 +15,15 @@ defmodule ExUnit.RegisterTest do defmodule SingularTestTypeCase do use ExUnit.Case - :"property is true" = ExUnit.Case.register_test(__ENV__, :property, "is true", []) + :"property is true" = + ExUnit.Case.register_test( + __ENV__.module, + __ENV__.file, + __ENV__.line, + :property, + "is true", + [] + ) def unquote(:"property is true")(_) do assert succeed() @@ -43,13 +51,29 @@ defmodule ExUnit.RegisterTest do defmodule PluralTestTypeCase do use ExUnit.Case - :"property is true" = ExUnit.Case.register_test(__ENV__, :property, "is true", []) + :"property is true" = + ExUnit.Case.register_test( + __ENV__.module, + __ENV__.file, + __ENV__.line, + :property, + "is true", + [] + ) def unquote(:"property is true")(_) do assert succeed() end - :"property is also true" = ExUnit.Case.register_test(__ENV__, :property, "is also true", []) + :"property is also true" = + ExUnit.Case.register_test( + __ENV__.module, + __ENV__.file, + __ENV__.line, + :property, + "is also true", + [] + ) def unquote(:"property is also true")(_) do assert succeed() diff --git a/lib/mix/lib/mix/tasks/format.ex b/lib/mix/lib/mix/tasks/format.ex index 1e24fed462..fb2a65044a 100644 --- a/lib/mix/lib/mix/tasks/format.ex +++ b/lib/mix/lib/mix/tasks/format.ex @@ -314,8 +314,8 @@ defmodule Mix.Tasks.Format do @doc """ Returns formatter options to be used for the given file. """ - # TODO: Deprecate on Elixir v1.17 - @doc deprecated: "Use formatter_for_file/2 instead" + # TODO: Remove me Elixir v1.17 + @deprecated "Use formatter_for_file/2 instead" def formatter_opts_for_file(file, opts \\ []) do {_, formatter_opts} = formatter_for_file(file, opts) formatter_opts diff --git a/lib/mix/lib/mix/tasks/test.ex b/lib/mix/lib/mix/tasks/test.ex index 0e5fbb3f0b..8780929d39 100644 --- a/lib/mix/lib/mix/tasks/test.ex +++ b/lib/mix/lib/mix/tasks/test.ex @@ -766,7 +766,7 @@ defmodule Mix.Tasks.Test do partition = System.get_env("MIX_TEST_PARTITION") case partition && Integer.parse(partition) do - {partition, ""} when partition in 1..total -> + {partition, ""} when partition in 1..total//1 -> partition = partition - 1 # We sort the files because Path.wildcard does not guarantee From 14f3a1e3720fe9d643813325bf8b594026e394b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sun, 29 Oct 2023 13:30:03 +0100 Subject: [PATCH 0100/1886] Deprecate single quotes as charlists --- lib/eex/lib/eex.ex | 4 +- lib/elixir/lib/atom.ex | 2 +- lib/elixir/lib/code/fragment.ex | 4 +- lib/elixir/lib/enum.ex | 2 +- lib/elixir/lib/float.ex | 2 +- lib/elixir/lib/integer.ex | 14 ++-- lib/elixir/lib/io/ansi.ex | 2 +- lib/elixir/lib/regex.ex | 2 +- lib/elixir/src/elixir_tokenizer.erl | 10 ++- lib/elixir/test/elixir/exception_test.exs | 5 -- lib/elixir/test/erlang/string_test.erl | 81 ++++--------------- .../test/mix/tasks/compile.elixir_test.exs | 2 +- lib/mix/test/mix/tasks/release_test.exs | 2 +- 13 files changed, 38 insertions(+), 94 deletions(-) diff --git a/lib/eex/lib/eex.ex b/lib/eex/lib/eex.ex index 2e50ae1f84..3ed3363988 100644 --- a/lib/eex/lib/eex.ex +++ b/lib/eex/lib/eex.ex @@ -317,8 +317,8 @@ defmodule EEx do ## Examples - iex> EEx.tokenize('foo', line: 1, column: 1) - {:ok, [{:text, 'foo', %{column: 1, line: 1}}, {:eof, %{column: 4, line: 1}}]} + iex> EEx.tokenize(~c"foo", line: 1, column: 1) + {:ok, [{:text, ~c"foo", %{column: 1, line: 1}}, {:eof, %{column: 4, line: 1}}]} ## Result diff --git a/lib/elixir/lib/atom.ex b/lib/elixir/lib/atom.ex index 39418cc390..685ddf6aad 100644 --- a/lib/elixir/lib/atom.ex +++ b/lib/elixir/lib/atom.ex @@ -67,7 +67,7 @@ defmodule Atom do ## Examples iex> Atom.to_charlist(:"An atom") - 'An atom' + ~c"An atom" """ @spec to_charlist(atom) :: charlist diff --git a/lib/elixir/lib/code/fragment.ex b/lib/elixir/lib/code/fragment.ex index ace8d46766..e104a1a4be 100644 --- a/lib/elixir/lib/code/fragment.ex +++ b/lib/elixir/lib/code/fragment.ex @@ -31,7 +31,7 @@ defmodule Code.Fragment do :expr iex> Code.Fragment.cursor_context("hello_wor") - {:local_or_var, 'hello_wor'} + {:local_or_var, ~c"hello_wor"} ## Return values @@ -547,7 +547,7 @@ defmodule Code.Fragment do ## Examples iex> Code.Fragment.surround_context("foo", {1, 1}) - %{begin: {1, 1}, context: {:local_or_var, 'foo'}, end: {1, 4}} + %{begin: {1, 1}, context: {:local_or_var, ~c"foo"}, end: {1, 4}} ## Differences to `cursor_context/2` diff --git a/lib/elixir/lib/enum.ex b/lib/elixir/lib/enum.ex index 90fb70a8fa..9ded32a922 100644 --- a/lib/elixir/lib/enum.ex +++ b/lib/elixir/lib/enum.ex @@ -3590,7 +3590,7 @@ defmodule Enum do iex> Enum.take_random(1..10, 2) [3, 1] iex> Enum.take_random(?a..?z, 5) - 'mikel' + ~c"mikel" """ @spec take_random(t, non_neg_integer) :: list diff --git a/lib/elixir/lib/float.ex b/lib/elixir/lib/float.ex index 49862329e6..22973c0223 100644 --- a/lib/elixir/lib/float.ex +++ b/lib/elixir/lib/float.ex @@ -571,7 +571,7 @@ defmodule Float do ## Examples iex> Float.to_charlist(7.0) - '7.0' + ~c"7.0" """ @spec to_charlist(float) :: charlist diff --git a/lib/elixir/lib/integer.ex b/lib/elixir/lib/integer.ex index 67f900a986..cd608cd4f0 100644 --- a/lib/elixir/lib/integer.ex +++ b/lib/elixir/lib/integer.ex @@ -362,25 +362,25 @@ defmodule Integer do ## Examples iex> Integer.to_charlist(123) - '123' + ~c"123" iex> Integer.to_charlist(+456) - '456' + ~c"456" iex> Integer.to_charlist(-789) - '-789' + ~c"-789" iex> Integer.to_charlist(0123) - '123' + ~c"123" iex> Integer.to_charlist(100, 16) - '64' + ~c"64" iex> Integer.to_charlist(-100, 16) - '-64' + ~c"-64" iex> Integer.to_charlist(882_681_651, 36) - 'ELIXIR' + ~c"ELIXIR" """ @spec to_charlist(integer, 2..36) :: charlist diff --git a/lib/elixir/lib/io/ansi.ex b/lib/elixir/lib/io/ansi.ex index 957a5b22ba..2932418c8f 100644 --- a/lib/elixir/lib/io/ansi.ex +++ b/lib/elixir/lib/io/ansi.ex @@ -299,7 +299,7 @@ defmodule IO.ANSI do ## Examples - iex> IO.ANSI.format_fragment([:bright, 'Word'], true) + iex> IO.ANSI.format_fragment([:bright, ~c"Word"], true) [[[[[[] | "\e[1m"], 87], 111], 114], 100] """ diff --git a/lib/elixir/lib/regex.ex b/lib/elixir/lib/regex.ex index 21dcbf59da..b89d18f761 100644 --- a/lib/elixir/lib/regex.ex +++ b/lib/elixir/lib/regex.ex @@ -213,7 +213,7 @@ defmodule Regex do {:ok, ~r/foo/} iex> Regex.compile("*foo") - {:error, {'nothing to repeat', 0}} + {:error, {~c"nothing to repeat", 0}} iex> Regex.compile("foo", "i") {:ok, ~r/foo/i} diff --git a/lib/elixir/src/elixir_tokenizer.erl b/lib/elixir/src/elixir_tokenizer.erl index 6db3de8c6e..72531b15be 100644 --- a/lib/elixir/src/elixir_tokenizer.erl +++ b/lib/elixir/src/elixir_tokenizer.erl @@ -247,18 +247,20 @@ tokenize([$?, Char | T], Line, Column, Scope, Tokens) -> tokenize("\"\"\"" ++ T, Line, Column, Scope, Tokens) -> handle_heredocs(T, Line, Column, $", Scope, Tokens); -%% TODO: Deprecate single-quoted in Elixir v1.17 +%% TODO: Remove me in Elixir v2.0 tokenize("'''" ++ T, Line, Column, Scope, Tokens) -> - handle_heredocs(T, Line, Column, $', Scope, Tokens); + NewScope = prepend_warning(Line, Column, "single-quoted string represent charlists. Use ~c''' if you indeed want a charlist or use \"\"\" instead", Scope), + handle_heredocs(T, Line, Column, $', NewScope, Tokens); % Strings tokenize([$" | T], Line, Column, Scope, Tokens) -> handle_strings(T, Line, Column + 1, $", Scope, Tokens); -%% TODO: Deprecate single-quoted in Elixir v1.17 +%% TODO: Remove me in Elixir v2.0 tokenize([$' | T], Line, Column, Scope, Tokens) -> - handle_strings(T, Line, Column + 1, $', Scope, Tokens); + NewScope = prepend_warning(Line, Column, "single-quoted strings represent charlists. Use ~c\"\" if you indeed want a charlist or use \"\" instead", Scope), + handle_strings(T, Line, Column + 1, $', NewScope, Tokens); % Operator atoms diff --git a/lib/elixir/test/elixir/exception_test.exs b/lib/elixir/test/elixir/exception_test.exs index 6084499cf5..79f7753907 100644 --- a/lib/elixir/test/elixir/exception_test.exs +++ b/lib/elixir/test/elixir/exception_test.exs @@ -687,11 +687,6 @@ defmodule ExceptionTest do assert blame_message(nil, & &1.foo) == "key :foo not found in: nil\n\nIf you are using the dot syntax, " <> "such as map.field, make sure the left-hand side of the dot is a map" - - # we use `Code.eval_string/1` to escape the formatter and warnings - assert blame_message("nil.foo", &Code.eval_string/1) == - "key :foo not found in: nil\n\nIf you are using the dot syntax, " <> - "such as map.field, make sure the left-hand side of the dot is a map" end test "annotates undefined function clause error with nil hints" do diff --git a/lib/elixir/test/erlang/string_test.erl b/lib/elixir/test/erlang/string_test.erl index c0b2ed2e79..47e65e09a2 100644 --- a/lib/elixir/test/erlang/string_test.erl +++ b/lib/elixir/test/erlang/string_test.erl @@ -80,49 +80,46 @@ extract_interpolations_with_invalid_expression_inside_interpolation_test() -> %% Bin strings -empty_bin_string_test() -> +empty_test() -> {<<"">>, _} = eval("\"\""). -simple_bin_string_test() -> - {<<"foo">>, _} = eval("\"foo\""). - -bin_string_with_double_quotes_test() -> +string_with_double_quotes_test() -> {<<"f\"o\"o">>, _} = eval("\"f\\\"o\\\"o\""). -bin_string_with_newline_test() -> +string_with_newline_test() -> {<<"f\no">>, _} = eval("\"f\no\""). -bin_string_with_slash_test() -> +string_with_slash_test() -> {<<"f\\o">>, _} = eval("\"f\\\\o\""). -bin_string_with_bell_character_test() -> +string_with_bell_character_test() -> {<<"f\ao">>, _} = eval("\"f\ao\""). -bin_string_with_interpolation_test() -> +string_with_interpolation_test() -> {<<"foo">>, _} = eval("\"f#{\"o\"}o\""). -bin_string_with_another_string_inside_string_inside_interpolation_test() -> +string_with_another_string_inside_string_inside_interpolation_test() -> {<<"fbaro">>, _} = eval("\"f#{\"b#{\"a\"}r\"}o\""). -bin_string_with_another_string_with_curly_inside_interpolation_test() -> +string_with_another_string_with_curly_inside_interpolation_test() -> {<<"fb}ro">>, _} = eval("\"f#{\"b}r\"}o\""). -bin_string_with_atom_with_separator_inside_interpolation_test() -> +string_with_atom_with_separator_inside_interpolation_test() -> {<<"f}o">>, _} = eval("\"f#{\"}\"}o\""). -bin_string_with_lower_case_hex_interpolation_test() -> +string_with_lower_case_hex_interpolation_test() -> {<<"jklmno">>, _} = eval("\"\\x6a\\x6b\\x6c\\x6d\\x6e\\x6f\""). -bin_string_with_upper_case_hex_interpolation_test() -> +string_with_upper_case_hex_interpolation_test() -> {<<"jklmno">>, _} = eval("\"\\x6A\\x6B\\x6C\\x6D\\x6E\\x6F\""). -bin_string_without_interpolation_and_escaped_test() -> +string_without_interpolation_and_escaped_test() -> {<<"f#o">>, _} = eval("\"f\\#o\""). -bin_string_with_escaped_interpolation_test() -> +string_with_escaped_interpolation_test() -> {<<"f#{'o}o">>, _} = eval("\"f\\#{'o}o\""). -bin_string_with_the_end_of_line_slash_test() -> +string_with_the_end_of_line_slash_test() -> {<<"fo">>, _} = eval("\"f\\\no\""), {<<"fo">>, _} = eval("\"f\\\r\no\""). @@ -133,57 +130,7 @@ invalid_string_interpolation_test() -> unterminated_string_interpolation_test() -> ?assertError(#{'__struct__' := 'Elixir.TokenMissingError'}, eval("\"foo")). -%% List strings - -empty_list_string_test() -> - {[], _} = eval("\'\'"). - -simple_list_string_test() -> - {"foo", _} = eval("'foo'"). - -list_string_with_double_quotes_test() -> - {"f'o'o", _} = eval("'f\\'o\\'o'"). - -list_string_with_newline_test() -> - {"f\no", _} = eval("'f\no'"). - -list_string_with_slash_test() -> - {"f\\o", _} = eval("'f\\\\o'"). - -list_string_with_bell_character_test() -> - {"f\ao", _} = eval("'f\ao'"). - -list_string_with_interpolation_test() -> - {"foo", _} = eval("'f#{\"o\"}o'"). - -list_string_with_another_string_with_curly_inside_interpolation_test() -> - {"fb}ro", _} = eval("'f#{\"b}r\"}o'"). - -list_string_with_atom_with_separator_inside_interpolation_test() -> - {"f}o", _} = eval("'f#{\"}\"}o'"). - -list_string_with_lower_case_hex_interpolation_test() -> - {"JKLMNO", _} = eval("'\\x4a\\x4b\\x4c\\x4d\\x4e\\x4f'"). - -list_string_with_upper_case_hex_interpolation_test() -> - {"JKLMNO", _} = eval("'\\x4A\\x4B\\x4C\\x4D\\x4E\\x4F'"). - -list_string_without_interpolation_and_escaped_test() -> - {"f#o", _} = eval("'f\\#o'"). - -list_string_with_escaped_interpolation_test() -> - {"f#{\"o}o", _} = eval("'f\\#{\"o}o'"). - -list_string_with_the_end_of_line_slash_test() -> - {"fo", _} = eval("'f\\\no'"), - {"fo", _} = eval("'f\\\r\no'"). - char_test() -> {99, []} = eval("?1 + ?2"), {10, []} = eval("?\\n"), {40, []} = eval("?("). - -%% Binaries - -bitstr_with_int_test() -> - {<<"fdo">>, _} = eval("<< \"f\", 50+50, \"o\" >>"). diff --git a/lib/mix/test/mix/tasks/compile.elixir_test.exs b/lib/mix/test/mix/tasks/compile.elixir_test.exs index 9666cbf5db..ceac247373 100644 --- a/lib/mix/test/mix/tasks/compile.elixir_test.exs +++ b/lib/mix/test/mix/tasks/compile.elixir_test.exs @@ -1471,7 +1471,7 @@ defmodule Mix.Tasks.Compile.ElixirTest do Mix.Project.push(MixTest.Case.Sample) File.write!("lib/a.ex", """ - IO.warn "warning", [{nil, nil, 0, file: 'lib/foo.txt', line: 3}] + IO.warn "warning", [{nil, nil, 0, file: ~c"lib/foo.txt", line: 3}] """) capture_io(:stderr, fn -> diff --git a/lib/mix/test/mix/tasks/release_test.exs b/lib/mix/test/mix/tasks/release_test.exs index a4c553561b..5adaa70b56 100644 --- a/lib/mix/test/mix/tasks/release_test.exs +++ b/lib/mix/test/mix/tasks/release_test.exs @@ -392,7 +392,7 @@ defmodule Mix.Tasks.ReleaseTest do config_target: config_target(), mode: :code.get_mode() - config :release_test, :encoding, {:runtime, :_μ, :"£", "£", '£'} + config :release_test, :encoding, {:runtime, :_μ, :"£", "£", ~c"£"} """) root = Path.absname("_build/dev/rel/runtime_config") From de3d1b472fc9b2775d50d9d392b5e57923b60725 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sun, 29 Oct 2023 13:39:39 +0100 Subject: [PATCH 0101/1886] Update CHANGELOG --- CHANGELOG.md | 11 +++++++++++ .../references/compatibility-and-deprecations.md | 7 ++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a01b1a86cb..d8551be980 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,17 @@ ### 4. Hard deprecations +#### Elixir + + * [IO] Passing `:all` to `IO.read/2` and `IO.binread/2` is deprecated, pass `:eof` instead + * [Kernel] Single-quote charlists are deprecated, use `~c` instead + * [Range] `left..right` without explicit steps inside patterns and guards is deprecated, write `left..right//step` instead + * [Range] Decreasing ranges, such as `10..1` without an explicit step is deprecated, write `10..1//-1` instead + +#### ExUnit + + * [ExUnit.Case] `register_test/4` is deprecated in favor of `register_test/6` for performance reasons + ## v1.15 The CHANGELOG for v1.16 releases can be found [in the v1.16 branch](https://github.com/elixir-lang/elixir/blob/v1.16/CHANGELOG.md). diff --git a/lib/elixir/pages/references/compatibility-and-deprecations.md b/lib/elixir/pages/references/compatibility-and-deprecations.md index d25ce7ad6f..c60a3607dc 100644 --- a/lib/elixir/pages/references/compatibility-and-deprecations.md +++ b/lib/elixir/pages/references/compatibility-and-deprecations.md @@ -80,6 +80,10 @@ The first column is the version the feature was hard deprecated. The second colu Version | Deprecated feature | Replaced by (available since) :-------| :-------------------------------------------------- | :--------------------------------------------------------------- +[v1.17] | Single-quoted charlists (`'foo'`) | `~c"foo"` (v1.0) +[v1.17] | `left..right` in patterns and guards | `left..right//step` (v1.11) +[v1.17] | `ExUnit.Case.register_test/4` | `register_test/6` (v1.10) +[v1.17] | `:all` in `IO.read/2` and `IO.binread/2` | `:eof` (v1.13) [v1.16] | `~R/.../` | `~r/.../` (v1.0) [v1.16] | Ranges with negative steps in `Enum.slice/2` | Explicit steps in ranges (v1.11) [v1.16] | Ranges with negative steps in `String.slice/2` | Explicit steps in ranges (v1.11) @@ -207,4 +211,5 @@ Version | Deprecated feature | Replaced by (ava [v1.13]: https://github.com/elixir-lang/elixir/blob/v1.13/CHANGELOG.md#4-hard-deprecations [v1.14]: https://github.com/elixir-lang/elixir/blob/v1.14/CHANGELOG.md#4-hard-deprecations [v1.15]: https://github.com/elixir-lang/elixir/blob/v1.15/CHANGELOG.md#4-hard-deprecations -[v1.16]: https://github.com/elixir-lang/elixir/blob/main/CHANGELOG.md#4-hard-deprecations +[v1.16]: https://github.com/elixir-lang/elixir/blob/v1.16/CHANGELOG.md#4-hard-deprecations +[v1.17]: https://github.com/elixir-lang/elixir/blob/main/CHANGELOG.md#4-hard-deprecations From ee17ecf8c6476afa0a60e6b4b7f8746fb4be7ab0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 30 Oct 2023 08:11:18 +0100 Subject: [PATCH 0102/1886] Clarify scope of anti-patterns --- lib/elixir/pages/anti-patterns/what-anti-patterns.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/elixir/pages/anti-patterns/what-anti-patterns.md b/lib/elixir/pages/anti-patterns/what-anti-patterns.md index 41c7cef509..b33e69f7df 100644 --- a/lib/elixir/pages/anti-patterns/what-anti-patterns.md +++ b/lib/elixir/pages/anti-patterns/what-anti-patterns.md @@ -4,9 +4,11 @@ Anti-patterns describe common mistakes or indicators of potential problems in co They are also known as "code smells". The goal of these guides is to document known anti-patterns found in Elixir software -and teach developers how to identify and correct them. If an existing piece of code -matches an anti-pattern, it does not mean your code must be rewritten. However, you -should take its potential pitfalls and alternatives into consideration. +and teach developers how to identify them and their limitations. If an existing piece +of code matches an anti-pattern, it does not mean your code must be rewritten. +No codebase is free of anti-patterns and one should not aim to remove all +anti-patterns of a codebase. The goal is to promote a discussion of potential +pitfalls and provide alternatives into consideration. The anti-patterns in these guides are broken into 4 main categories: From f16aed50d624ff9f10da6407f2602fea32ee148e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 30 Oct 2023 09:18:18 +0100 Subject: [PATCH 0103/1886] Describe pattern matching as simpler --- lib/elixir/pages/anti-patterns/code-anti-patterns.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/elixir/pages/anti-patterns/code-anti-patterns.md b/lib/elixir/pages/anti-patterns/code-anti-patterns.md index 07004922b2..093b1a60b0 100644 --- a/lib/elixir/pages/anti-patterns/code-anti-patterns.md +++ b/lib/elixir/pages/anti-patterns/code-anti-patterns.md @@ -436,7 +436,7 @@ iex> Graphics.plot(point_3d) Overall, the usage of `map.key` and `map[:key]` encode important information about your data structure, allowing developers to be clear about their intent. See both `Map` and `Access` module documentation for more information and examples. -Another alternative to refactor this anti-pattern is to use pattern matching: +An even simpler alternative to refactor this anti-pattern is to use pattern matching: ```elixir defmodule Graphics do @@ -461,6 +461,8 @@ iex> Graphics.plot(point_3d) {5, 6, nil} ``` +Pattern-matching is specially useful when matching over multiple keys at once and also when you want to match and assert on the values of a map. + Another alternative is to use structs. By default, structs only support static access to its fields: ```elixir From ad7c73244d9890229ed58c899cc680076fc39a5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 30 Oct 2023 15:12:28 +0100 Subject: [PATCH 0104/1886] Clarify best practices and update anti-patterns list --- .../pages/anti-patterns/code-anti-patterns.md | 72 +------------------ .../pages/anti-patterns/what-anti-patterns.md | 2 +- .../alias-require-and-import.md | 2 +- 3 files changed, 4 insertions(+), 72 deletions(-) diff --git a/lib/elixir/pages/anti-patterns/code-anti-patterns.md b/lib/elixir/pages/anti-patterns/code-anti-patterns.md index 093b1a60b0..fa5bd56d63 100644 --- a/lib/elixir/pages/anti-patterns/code-anti-patterns.md +++ b/lib/elixir/pages/anti-patterns/code-anti-patterns.md @@ -34,7 +34,8 @@ You could refactor the code above like this: @five_min_in_seconds 60 * 5 defp unix_five_min_from_now do - unix_now = DateTime.to_unix(DateTime.utc_now(), :second) + now = DateTime.utc_now() + unix_now = DateTime.to_unix(now, :second) unix_now + @five_min_in_seconds end ``` @@ -45,75 +46,6 @@ We removed the unnecessary comments. We also added a `@five_min_in_seconds` modu Elixir makes a clear distinction between **documentation** and code comments. The language has built-in first-class support for documentation through `@doc`, `@moduledoc`, and more. See the ["Writing documentation"](../getting-started/writing-documentation.md) guide for more information. -## Complex branching - -#### Problem - -When a function assumes the responsibility of handling multiple errors alone, it can increase its cyclomatic complexity (metric of control-flow) and become incomprehensible. This situation can configure a specific instance of "Long function", a traditional anti-pattern, but has implications of its own. Under these circumstances, this function could get very confusing, difficult to maintain and test, and therefore bug-proneness. - -#### Example - -An example of this anti-pattern is when a function uses the `case` control-flow structure or other similar constructs (for example, `cond` or `receive`) to handle variations of a return type. This practice can make the function more complex, long, and difficult to understand, as shown next. - -```elixir -def get_customer(customer_id) do - case SomeHTTPClient.get("/customers/#{customer_id}") do - {:ok, %{status: 200, body: body}} -> - case Jason.decode(body) do - {:ok, decoded} -> - %{ - "first_name" => first_name, - "last_name" => last_name, - "company" => company - } = decoded - - customer = - %Customer{ - id: customer_id, - name: "#{first_name} #{last_name}", - company: company - } - - {:ok, customer} - - {:error, _} -> - {:error, "invalid response body"} - end - - {:error, %{status: status, body: body}} -> - case Jason.decode(body) do - %{"error" => message} when is_binary(message) -> - {:error, message} - - %{} -> - {:error, "invalid response with status #{status}"} - end - end -end -``` - -The code above is complex because the `case` clauses are long and often have their own branching logic in them. With the clauses spread out, it is hard to understand what each clause does individually and it is hard to see all of the different scenarios your code pattern matches on. - -#### Refactoring - -As shown below, in this situation, instead of concentrating all error handling within the same function, creating complex branches, it is better to delegate each branch to a different private function. In this way, the code will be cleaner and more readable. - -```elixir -def get_customer(customer_id) do - case SomeHTTPClient.get("/customers/#{customer_id}") do - {:ok, %{status: 200, body: body}} -> - http_customer_to_struct(customer_id, body) - - {:error, %{status: status, body: body}} -> - http_error(status, body) - end -end -``` - -Both `http_customer_to_struct(customer_id, body)` and `http_error(status, body)` above contain the previous branches refactored into private functions. - -It is worth noting that this refactoring is trivial to perform in Elixir because clauses cannot define variables or otherwise affect their parent scope. Therefore, extracting any clause or branch to a private function is a matter of gathering all variables used in that branch and passing them as arguments to the new function. - ## Complex `else` clauses in `with` #### Problem diff --git a/lib/elixir/pages/anti-patterns/what-anti-patterns.md b/lib/elixir/pages/anti-patterns/what-anti-patterns.md index b33e69f7df..1cb4e72eb6 100644 --- a/lib/elixir/pages/anti-patterns/what-anti-patterns.md +++ b/lib/elixir/pages/anti-patterns/what-anti-patterns.md @@ -38,4 +38,4 @@ Each anti-pattern is documented using the following structure: The initial catalog of anti-patterns was proposed by Lucas Vegi and Marco Tulio Valente, from [ASERG/DCC/UFMG](http://aserg.labsoft.dcc.ufmg.br/). For more info, see [Understanding Code Smells in Elixir Functional Language](https://github.com/lucasvegi/Elixir-Code-Smells/blob/main/etc/2023-emse-code-smells-elixir.pdf) and [the associated code repository](https://github.com/lucasvegi/Elixir-Code-Smells). -Additionally, the Security Working Group of the [Erlang Ecosystem Foundation](https://erlef.github.io/security-wg/) publishes [documents with security resources and best-practices of both Erland and Elixir, including detailed guides for web applications](https://erlef.github.io/security-wg/). +Additionally, the Security Working Group of the [Erlang Ecosystem Foundation](https://erlef.github.io/security-wg/) publishes [documents with security resources and best-practices of both Erlang and Elixir, including detailed guides for web applications](https://erlef.github.io/security-wg/). diff --git a/lib/elixir/pages/getting-started/alias-require-and-import.md b/lib/elixir/pages/getting-started/alias-require-and-import.md index c3be241883..2e45feef2e 100644 --- a/lib/elixir/pages/getting-started/alias-require-and-import.md +++ b/lib/elixir/pages/getting-started/alias-require-and-import.md @@ -112,7 +112,7 @@ end In the example above, the imported `List.duplicate/2` is only visible within that specific function. `duplicate/2` won't be available in any other function in that module (or any other module for that matter). -Note that `import`s are generally discouraged in the language. When working on your own code, prefer `alias` to `import`. +While `import`s can be a useful for frameworks and libraries to build abstractions, developers should generally prefer `alias` to `import` on their own codebases, as aliases make the origin of the function being invoked clearer. ## use From cd3af7d5d2f0bad06009dfd4dcefffd3a0fe96b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 30 Oct 2023 15:50:36 +0100 Subject: [PATCH 0105/1886] Improve examples and docs --- lib/elixir/lib/registry.ex | 14 ++-- .../pages/anti-patterns/code-anti-patterns.md | 2 +- .../anti-patterns/design-anti-patterns.md | 82 +++++-------------- 3 files changed, 30 insertions(+), 68 deletions(-) diff --git a/lib/elixir/lib/registry.ex b/lib/elixir/lib/registry.ex index cde2af610a..4177e35bae 100644 --- a/lib/elixir/lib/registry.ex +++ b/lib/elixir/lib/registry.ex @@ -27,8 +27,8 @@ defmodule Registry do `Registry.start_link/1`, it can be used to register and access named processes using the `{:via, Registry, {registry, key}}` tuple: - {:ok, _} = Registry.start_link(keys: :unique, name: Registry.ViaTest) - name = {:via, Registry, {Registry.ViaTest, "agent"}} + {:ok, _} = Registry.start_link(keys: :unique, name: MyApp.Registry) + name = {:via, Registry, {MyApp.Registry, "agent"}} {:ok, _} = Agent.start_link(fn -> 0 end, name: name) Agent.get(name, & &1) #=> 0 @@ -39,22 +39,22 @@ defmodule Registry do In the previous example, we were not interested in associating a value to the process: - Registry.lookup(Registry.ViaTest, "agent") + Registry.lookup(MyApp.Registry, "agent") #=> [{self(), nil}] However, in some cases it may be desired to associate a value to the process using the alternate `{:via, Registry, {registry, key, value}}` tuple: - {:ok, _} = Registry.start_link(keys: :unique, name: Registry.ViaTest) - name = {:via, Registry, {Registry.ViaTest, "agent", :hello}} + {:ok, _} = Registry.start_link(keys: :unique, name: MyApp.Registry) + name = {:via, Registry, {MyApp.Registry, "agent", :hello}} {:ok, agent_pid} = Agent.start_link(fn -> 0 end, name: name) - Registry.lookup(Registry.ViaTest, "agent") + Registry.lookup(MyApp.Registry, "agent") #=> [{agent_pid, :hello}] To this point, we have been starting `Registry` using `start_link/1`. Typically the registry is started as part of a supervision tree though: - {Registry, keys: :unique, name: Registry.ViaTest} + {Registry, keys: :unique, name: MyApp.Registry} Only registries with unique keys can be used in `:via`. If the name is already taken, the case-specific `start_link` function (`Agent.start_link/2` diff --git a/lib/elixir/pages/anti-patterns/code-anti-patterns.md b/lib/elixir/pages/anti-patterns/code-anti-patterns.md index fa5bd56d63..6ae84a3db5 100644 --- a/lib/elixir/pages/anti-patterns/code-anti-patterns.md +++ b/lib/elixir/pages/anti-patterns/code-anti-patterns.md @@ -299,7 +299,7 @@ There are few known exceptions to this anti-pattern: * [Protocol implementations](`Kernel.defimpl/2`) are, by design, defined under the protocol namespace - * [Custom Mix tasks](`Mix.Task`) are always defined under the `Mix.Tasks` namespace, such as `Mix.Tasks.PlugAuth` + * In some scenarios, the namespace owner may allow exceptions to this rule. For example, in Elixir itself, you defined [custom Mix tasks](`Mix.Task`) by placing them under the `Mix.Tasks` namespace, such as `Mix.Tasks.PlugAuth` * If you are the maintainer for both `plug` and `plug_auth`, then you may allow `plug_auth` to define modules with the `Plug` namespace, such as `Plug.Auth`. However, you are responsible for avoiding or managing any conflicts that may arise in the future diff --git a/lib/elixir/pages/anti-patterns/design-anti-patterns.md b/lib/elixir/pages/anti-patterns/design-anti-patterns.md index 8a1b13d562..7ec9f99214 100644 --- a/lib/elixir/pages/anti-patterns/design-anti-patterns.md +++ b/lib/elixir/pages/anti-patterns/design-anti-patterns.md @@ -126,7 +126,7 @@ Remember booleans are internally represented as atoms. Therefore there is no per #### Problem -This anti-pattern refers to code that uses exceptions for control flow. Exception handling itself does not represent an anti-pattern, but developers must prefer to use `case` and pattern matching to change the flow of their code, instead of `try/rescue`. In turn, library authors should provide developers with APIs to handle errors without relying on exception handling. When developers have no freedom to decide if an error is exceptional or not, this is considered an anti-pattern. +This anti-pattern refers to code that uses `Exception`s for control flow. Exception handling itself does not represent an anti-pattern, but developers must prefer to use `case` and pattern matching to change the flow of their code, instead of `try/rescue`. In turn, library authors should provide developers with APIs to handle errors without relying on exception handling. When developers have no freedom to decide if an error is exceptional or not, this is considered an anti-pattern. #### Example @@ -186,63 +186,11 @@ end A common practice followed by the community is to make the non-raising version return `{:ok, result}` or `{:error, Exception.t}`. For example, an HTTP client may return `{:ok, %HTTP.Response{}}` on success cases and `{:error, %HTTP.Error{}}` for failures, where `HTTP.Error` is [implemented as an exception](`Kernel.defexception/1`). This makes it convenient for anyone to raise an exception by simply calling `Kernel.raise/1`. -## Feature envy - -#### Problem - -This anti-pattern occurs when a function accesses more data or calls more functions from another module than from its own. The presence of this anti-pattern can make a module less cohesive and increase code coupling. - -#### Example - -In the following code, all the data used in the `calculate_total_item/1` function of the module `Order` comes from the `OrderItem` module. This increases coupling and decreases code cohesion unnecessarily. - -```elixir -defmodule Order do - # Some functions... - - def calculate_total_item(id) do - item = OrderItem.find_item(id) - total = (item.price + item.taxes) * item.amount - - if discount = OrderItem.find_discount(item) do - total - total * discount - else - total - end - end -end -``` - -#### Refactoring - -To remove this anti-pattern we can move `calculate_total_item/1` to `OrderItem`, decreasing coupling: - -```elixir -defmodule OrderItem do - def find_item(id) - def find_discount(item) - - def calculate_total_item(id) do # <= function moved from Order! - item = find_item(id) - total = (item.price + item.taxes) * item.amount - discount = find_discount(item) - - unless is_nil(discount) do - total - total * discount - else - total - end - end -end -``` - -This refactoring is only possible when you own both modules. If the module you are invoking belongs to another application, then it is not possible to add new functions to it, and your only option is to define an additional module that augments the third-party module. - ## Primitive obsession #### Problem -This anti-pattern happens when Elixir basic types (for example, *integer*, *float*, and *string*) are abusively used in function parameters and code variables, rather than creating specific composite data types (for example, *tuples*, *maps*, and *structs*) that can better represent a domain. +This anti-pattern happens when Elixir basic types (for example, *integer*, *float*, and *string*) are excessively used to carry structured information, rather than creating specific composite data types (for example, *tuples*, *maps*, and *structs*) that can better represent a domain. #### Example @@ -250,17 +198,23 @@ An example of this anti-pattern is the use of a single *string* to represent an ```elixir defmodule MyApp do - def process_address(address) when is_binary(address) do - # Do something with address... + def extract_postal_code(address) when is_binary(address) do + # Extract postal code with address... + end + + def fill_in_country(address) when is_binary(address) do + # Fill in missing country... end end ``` +While you may receive the `address` as a string from a database, web request, or a third-party, if you find yourself frequently manipulating or extracting information from the string, it is a good indicator you should convert the address into structured data: + Another example of this anti-pattern is using floating numbers to model money and currency, when [richer data structures should be preferred](https://hexdocs.pm/ex_money/). #### Refactoring -Possible solutions to this anti-pattern is to use maps or structs to model our address. The example below creates an `Address` struct, better representing this domain through a composite type. Additionally, we can modify the `process_address/1` function to accept a parameter of type `Address` instead of a *string*. With this modification, we can extract each field of this composite type individually when needed. +Possible solutions to this anti-pattern is to use maps or structs to model our address. The example below creates an `Address` struct, better representing this domain through a composite type. Additionally, we introduce a `parse/1` function, that converts the string into an `Address`, which will simplify the logic of remainng functions. With this modification, we can extract each field of this composite type individually when needed. ```elixir defmodule Address do @@ -270,8 +224,16 @@ end ```elixir defmodule MyApp do - def process_address(%Address{} = address) do - # Do something with address... + def parse(address) when is_binary(address) do + # Returns %Address{} + end + + def extract_postal_code(%Address{} = address) do + # Extract postal code with address... + end + + def fill_in_country(%Address{} = address) do + # Fill in missing country... end end ``` @@ -342,7 +304,7 @@ Using multi-clause functions in Elixir, to group functions of the same name, is A frequent example of this usage of multi-clause functions is when developers mix unrelated business logic into the same function definition. Such functions often have generic names or too broad specifications, making it difficult for maintainers and users of said functions to maintain and understand them. -Some developers may use documentation mechanisms such as `@doc` annotations to compensate for poor code readability, however the documentation itself may end-up full of conditionals to describe how the function behaves for each different argument combination. +Some developers may use documentation mechanisms such as `@doc` annotations to compensate for poor code readability, however the documentation itself may end-up full of conditionals to describe how the function behaves for each different argument combination. This is a good indicator that the clauses are ultimately unrelated. ```elixir @doc """ From eb20bdd7167d40503d6cd664f83a1e28106939c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?George=20Guimar=C3=A3es?= Date: Mon, 30 Oct 2023 13:48:47 -0300 Subject: [PATCH 0106/1886] Fix some typos (#13043) --- lib/elixir/lib/calendar/datetime.ex | 2 +- lib/elixir/lib/kernel.ex | 2 +- lib/elixir/lib/kernel/utils.ex | 2 +- lib/elixir/lib/macro.ex | 2 +- lib/elixir/src/elixir_errors.erl | 2 +- lib/elixir/src/elixir_expand.erl | 2 +- lib/elixir/test/elixir/module_test.exs | 2 +- lib/iex/test/iex/autocomplete_test.exs | 4 ++-- lib/mix/lib/mix.ex | 2 +- lib/mix/lib/mix/tasks/compile.ex | 2 +- 10 files changed, 11 insertions(+), 11 deletions(-) diff --git a/lib/elixir/lib/calendar/datetime.ex b/lib/elixir/lib/calendar/datetime.ex index ce15683ac3..396f4984e5 100644 --- a/lib/elixir/lib/calendar/datetime.ex +++ b/lib/elixir/lib/calendar/datetime.ex @@ -90,7 +90,7 @@ defmodule DateTime do hour that does not exist. Then, when they move the clock back, there is a certain hour that will happen twice. So if you want to schedule a meeting when this shift back happens, you would need to - explicitly say which occurence of 2:30 AM you mean: the one in + explicitly say which occurrence of 2:30 AM you mean: the one in "Summer Time", which occurs before the shift, or the one in "Standard Time", which occurs after it. Applications that are date and time sensitive need to take these scenarios into account diff --git a/lib/elixir/lib/kernel.ex b/lib/elixir/lib/kernel.ex index 52983d9b45..0ca281d3e5 100644 --- a/lib/elixir/lib/kernel.ex +++ b/lib/elixir/lib/kernel.ex @@ -6060,7 +6060,7 @@ defmodule Kernel do iex> ~S(\o/) "\\o/" - However, if you want to re-use the sigil character itself on + However, if you want to reuse the sigil character itself on the string, you need to escape it: iex> ~S((\)) diff --git a/lib/elixir/lib/kernel/utils.ex b/lib/elixir/lib/kernel/utils.ex index 23235f1170..8af029279e 100644 --- a/lib/elixir/lib/kernel/utils.ex +++ b/lib/elixir/lib/kernel/utils.ex @@ -289,7 +289,7 @@ defmodule Kernel.Utils do macro. Secondly, if the expression is being used outside of a guard, we want to unquote - `value`, but only once, and then re-use the unquoted form throughout the expression. + `value`, but only once, and then reuse the unquoted form throughout the expression. This helper does exactly that: takes the AST for an expression and a list of variable references it should be aware of, and rewrites it into a new expression diff --git a/lib/elixir/lib/macro.ex b/lib/elixir/lib/macro.ex index 5d3ab5bc55..c2dda7fa3c 100644 --- a/lib/elixir/lib/macro.ex +++ b/lib/elixir/lib/macro.ex @@ -1709,7 +1709,7 @@ defmodule Macro do @doc """ Applies a `mod`, `function`, and `args` at compile-time in `caller`. - This is used when you want to programatically invoke a macro at + This is used when you want to programmatically invoke a macro at compile-time. """ @doc since: "1.16.0" diff --git a/lib/elixir/src/elixir_errors.erl b/lib/elixir/src/elixir_errors.erl index a7969afb1d..1cb3d92bed 100644 --- a/lib/elixir/src/elixir_errors.erl +++ b/lib/elixir/src/elixir_errors.erl @@ -133,7 +133,7 @@ extract_column({_, C}) -> C; extract_column(_) -> nil. %% Format snippets -%% "Snippet" here refers to the source code line where the diagnostic/error occured +%% "Snippet" here refers to the source code line where the diagnostic/error occurred format_snippet(_Position, nil, Message, nil, Severity, _Stacktrace, _Span) -> Formatted = [prefix(Severity), " ", Message], diff --git a/lib/elixir/src/elixir_expand.erl b/lib/elixir/src/elixir_expand.erl index de71ccf377..c6399ee613 100644 --- a/lib/elixir/src/elixir_expand.erl +++ b/lib/elixir/src/elixir_expand.erl @@ -1336,7 +1336,7 @@ format_error({undefined_function, Name, Args}) -> io_lib:format("undefined function ~ts/~B (there is no such import)", [Name, length(Args)]); format_error({unpinned_bitsize_var, Name, Kind}) -> io_lib:format("the variable \"~ts\"~ts is accessed inside size(...) of a bitstring " - "but it was defined outside of the match. You must preceed it with the " + "but it was defined outside of the match. You must precede it with the " "pin operator", [Name, context_info(Kind)]); format_error({underscored_var_repeat, Name, Kind}) -> io_lib:format("the underscored variable \"~ts\"~ts appears more than once in a " diff --git a/lib/elixir/test/elixir/module_test.exs b/lib/elixir/test/elixir/module_test.exs index 1c0f31e608..42ab7e2506 100644 --- a/lib/elixir/test/elixir/module_test.exs +++ b/lib/elixir/test/elixir/module_test.exs @@ -567,7 +567,7 @@ defmodule ModuleTest do end describe "get_attribute/3" do - test "returns a list when the attribute is marked as `accummulate: true`" do + test "returns a list when the attribute is marked as `accumulate: true`" do in_module do Module.register_attribute(__MODULE__, :value, accumulate: true) Module.put_attribute(__MODULE__, :value, 1) diff --git a/lib/iex/test/iex/autocomplete_test.exs b/lib/iex/test/iex/autocomplete_test.exs index b3ef186bde..2720a43327 100644 --- a/lib/iex/test/iex/autocomplete_test.exs +++ b/lib/iex/test/iex/autocomplete_test.exs @@ -415,7 +415,7 @@ defmodule IEx.AutocompleteTest do assert {:yes, ~c"ry: ", []} = expand(~c"%URI{path: \"foo\", que") assert {:no, [], []} = expand(~c"%URI{path: \"foo\", unkno") - assert {:no, [], []} = expand(~c"%Unkown{path: \"foo\", unkno") + assert {:no, [], []} = expand(~c"%Unknown{path: \"foo\", unkno") end test "completion for struct keys in update syntax" do @@ -429,7 +429,7 @@ defmodule IEx.AutocompleteTest do assert {:yes, ~c"ry: ", []} = expand(~c"%URI{var | path: \"foo\", que") assert {:no, [], []} = expand(~c"%URI{var | path: \"foo\", unkno") - assert {:no, [], []} = expand(~c"%Unkown{var | path: \"foo\", unkno") + assert {:no, [], []} = expand(~c"%Unknown{var | path: \"foo\", unkno") end test "completion for map keys in update syntax" do diff --git a/lib/mix/lib/mix.ex b/lib/mix/lib/mix.ex index bf17b11565..576cfb5320 100644 --- a/lib/mix/lib/mix.ex +++ b/lib/mix/lib/mix.ex @@ -297,7 +297,7 @@ defmodule Mix do are several tasks depending on `mix compile`, the code will be compiled only once. - Similary, `mix format` can only be invoked once. So if you have an alias + Similarly, `mix format` can only be invoked once. So if you have an alias that attempts to invoke `mix format` multiple times, it won't work unless it is explicitly reenabled using `Mix.Task.reenable/1`: diff --git a/lib/mix/lib/mix/tasks/compile.ex b/lib/mix/lib/mix/tasks/compile.ex index ed55c7b246..c7f90c45ec 100644 --- a/lib/mix/lib/mix/tasks/compile.ex +++ b/lib/mix/lib/mix/tasks/compile.ex @@ -40,7 +40,7 @@ defmodule Mix.Tasks.Compile do in the project file with dependencies. When false, this keeps the entirety of Erlang/OTP available when the project starts, including the paths set by the code loader from the `ERL_LIBS` environment as - well as explicitely listed by providing `-pa` and `-pz` options + well as explicitly listed by providing `-pa` and `-pz` options to Erlang. ## Compilers From d9be48d04cb845e2a51df64fea036dbae319f10c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 30 Oct 2023 20:10:43 +0100 Subject: [PATCH 0107/1886] Streamline unrelated introduction --- lib/elixir/pages/anti-patterns/design-anti-patterns.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/elixir/pages/anti-patterns/design-anti-patterns.md b/lib/elixir/pages/anti-patterns/design-anti-patterns.md index 7ec9f99214..9afada4bea 100644 --- a/lib/elixir/pages/anti-patterns/design-anti-patterns.md +++ b/lib/elixir/pages/anti-patterns/design-anti-patterns.md @@ -298,11 +298,11 @@ The following arguments were given to MyLibrary.foo/1: #### Problem -Using multi-clause functions in Elixir, to group functions of the same name, is not an anti-pattern in itself. However, due to the great flexibility provided by this programming feature, some developers may abuse the number of guard clauses and pattern matches to group *unrelated* functionality. +Using multi-clause functions in Elixir, to group functions of the same name, is a powerful Elixir feature. However, some developers may abuse this feature to group *unrelated* functionality, which configures an anti-pattern. #### Example -A frequent example of this usage of multi-clause functions is when developers mix unrelated business logic into the same function definition. Such functions often have generic names or too broad specifications, making it difficult for maintainers and users of said functions to maintain and understand them. +A frequent example of this usage of multi-clause functions is when developers mix unrelated business logic into the same function definition. Such functions often have generic names or too broad specifications, making it difficult for other developers to understand and maintain them. Some developers may use documentation mechanisms such as `@doc` annotations to compensate for poor code readability, however the documentation itself may end-up full of conditionals to describe how the function behaves for each different argument combination. This is a good indicator that the clauses are ultimately unrelated. From 2f43c8cc47fa5c7ecf03a32012d05190133b1c41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 30 Oct 2023 14:49:57 +0100 Subject: [PATCH 0108/1886] Warn on map.field() and mod.function --- lib/elixir/lib/application.ex | 5 +- lib/elixir/lib/inspect.ex | 2 +- lib/elixir/lib/io.ex | 2 +- lib/elixir/lib/supervisor.ex | 4 +- lib/elixir/lib/system.ex | 6 ++- lib/elixir/src/elixir_erl_pass.erl | 52 ++++++++++++++----- lib/ex_unit/lib/ex_unit/cli_formatter.ex | 2 +- lib/iex/lib/iex/autocomplete.ex | 2 +- lib/mix/lib/mix/app_loader.ex | 2 +- lib/mix/lib/mix/dep.ex | 2 +- lib/mix/lib/mix/dep/fetcher.ex | 2 +- lib/mix/lib/mix/dep/loader.ex | 2 +- lib/mix/lib/mix/project.ex | 2 +- lib/mix/lib/mix/tasks/compile.ex | 2 +- lib/mix/lib/mix/tasks/compile.protocols.ex | 2 +- lib/mix/lib/mix/tasks/deps.clean.ex | 2 +- lib/mix/lib/mix/tasks/deps.compile.ex | 8 +-- lib/mix/lib/mix/tasks/deps.loadpaths.ex | 2 +- .../test/mix/tasks/compile.elixir_test.exs | 18 +++---- lib/mix/test/mix/tasks/deps.git_test.exs | 6 +-- 20 files changed, 79 insertions(+), 46 deletions(-) diff --git a/lib/elixir/lib/application.ex b/lib/elixir/lib/application.ex index 3a2c856f22..11f1af388d 100644 --- a/lib/elixir/lib/application.ex +++ b/lib/elixir/lib/application.ex @@ -850,7 +850,10 @@ defmodule Application do # TODO: Remove this deprecation warning on 2.0+ and allow list lookups as in compile_env. defp maybe_warn_on_app_env_key(app, key) do - message = "passing non-atom as application env key is deprecated, got: #{inspect(key)}" + message = fn -> + "passing non-atom as application env key is deprecated, got: #{inspect(key)}" + end + IO.warn_once({Application, :key, app, key}, message, _stacktrace_drop_levels = 2) end diff --git a/lib/elixir/lib/inspect.ex b/lib/elixir/lib/inspect.ex index d5753b8d84..725cc6e76d 100644 --- a/lib/elixir/lib/inspect.ex +++ b/lib/elixir/lib/inspect.ex @@ -449,7 +449,7 @@ defimpl Inspect, for: Function do match?(@elixir_compiler ++ _, Atom.to_charlist(mod)) -> if function_exported?(mod, :__RELATIVE__, 0) do - "#Function<#{uniq(fun_info)} in file:#{mod.__RELATIVE__}>" + "#Function<#{uniq(fun_info)} in file:#{mod.__RELATIVE__()}>" else default_inspect(mod, fun_info) end diff --git a/lib/elixir/lib/io.ex b/lib/elixir/lib/io.ex index 5cf21c2089..41f77cfdbc 100644 --- a/lib/elixir/lib/io.ex +++ b/lib/elixir/lib/io.ex @@ -376,7 +376,7 @@ defmodule IO do stacktrace = Enum.drop(stacktrace, stacktrace_drop_levels) if :elixir_config.warn(key, stacktrace) do - warn(message, stacktrace) + warn(message.(), stacktrace) else :ok end diff --git a/lib/elixir/lib/supervisor.ex b/lib/elixir/lib/supervisor.ex index e56527fa99..f8962a0080 100644 --- a/lib/elixir/lib/supervisor.ex +++ b/lib/elixir/lib/supervisor.ex @@ -998,7 +998,9 @@ defmodule Supervisor do def start_child(supervisor, args) when is_list(args) do IO.warn_once( {__MODULE__, :start_child}, - "Supervisor.start_child/2 with a list of args is deprecated, please use DynamicSupervisor instead", + fn -> + "Supervisor.start_child/2 with a list of args is deprecated, please use DynamicSupervisor instead" + end, _stacktrace_drop_levels = 2 ) diff --git a/lib/elixir/lib/system.ex b/lib/elixir/lib/system.ex index 912b17b77a..7f67495e31 100644 --- a/lib/elixir/lib/system.ex +++ b/lib/elixir/lib/system.ex @@ -1422,8 +1422,10 @@ defmodule System do defp warn(unit, replacement_unit) do IO.warn_once( {__MODULE__, unit}, - "deprecated time unit: #{inspect(unit)}. A time unit should be " <> - ":second, :millisecond, :microsecond, :nanosecond, or a positive integer", + fn -> + "deprecated time unit: #{inspect(unit)}. A time unit should be " <> + ":second, :millisecond, :microsecond, :nanosecond, or a positive integer" + end, _stacktrace_drop_levels = 4 ) diff --git a/lib/elixir/src/elixir_erl_pass.erl b/lib/elixir/src/elixir_erl_pass.erl index a46d0ec1a5..341b1d4d8e 100644 --- a/lib/elixir/src/elixir_erl_pass.erl +++ b/lib/elixir/src/elixir_erl_pass.erl @@ -1,6 +1,6 @@ %% Translate Elixir quoted expressions to Erlang Abstract Format. -module(elixir_erl_pass). --export([translate/3, translate_args/3]). +-export([translate/3, translate_args/3, no_parens_remote/2, parens_map_field/2]). -include("elixir.hrl"). %% = @@ -231,7 +231,9 @@ translate({{'.', _, [Left, Right]}, Meta, []}, _Ann, S) TRight = {atom, Ann, Right}, Generated = erl_anno:set_generated(true, Ann), - {Var, SV} = elixir_erl_var:build('_', SL), + {InnerVar, SI} = elixir_erl_var:build('_', SL), + TInnerVar = {var, Generated, InnerVar}, + {Var, SV} = elixir_erl_var:build('_', SI), TVar = {var, Generated, Var}, case proplists:get_value(no_parens, Meta, false) of @@ -242,26 +244,22 @@ translate({{'.', _, [Left, Right]}, Meta, []}, _Ann, S) [{map, Ann, [{map_field_exact, Ann, TRight, TVar}]}], [], [TVar]}, - {clause, Generated, - [TVar], - [[ - ?remote(Generated, erlang, is_atom, [TVar]), - {op, Generated, '=/=', TVar, {atom, Generated, nil}}, - {op, Generated, '=/=', TVar, {atom, Generated, true}}, - {op, Generated, '=/=', TVar, {atom, Generated, false}} - ]], - [{call, Generated, {remote, Generated, TVar, TRight}, []}]}, {clause, Generated, [TVar], [], - [?remote(Ann, erlang, error, [TError])]} + [{'case', Generated, ?remote(Generated, elixir_erl_pass, no_parens_remote, [TVar, TRight]), [ + {clause, Generated, + [{tuple, Generated, [{atom, Generated, ok}, TInnerVar]}], [], [TInnerVar]}, + {clause, Generated, + [{var, Generated, '_'}], [], [?remote(Ann, erlang, error, [TError])]} + ]}]} ]}, SV}; false -> {{'case', Generated, TLeft, [ {clause, Generated, [{map, Ann, [{map_field_exact, Ann, TRight, TVar}]}], [], - [TVar]}, + [?remote(Generated, elixir_erl_pass, parens_map_field, [TRight, TVar])]}, {clause, Generated, [TVar], [], @@ -640,3 +638,31 @@ generate_struct_name_guard([{map_field_exact, Ann, {atom, _, '__struct__'} = Key {lists:reverse(Acc, [{map_field_exact, Ann, Key, Match} | Rest]), S2}; generate_struct_name_guard([Field | Rest], Acc, S) -> generate_struct_name_guard(Rest, [Field | Acc], S). + +%% TODO: Make this a runtime error on Elixir v2.0 +no_parens_remote(nil, _Fun) -> error; +no_parens_remote(false, _Fun) -> error; +no_parens_remote(true, _Fun) -> error; +no_parens_remote(Atom, Fun) when is_atom(Atom) -> + Message = fun() -> + io_lib:format( + "using map.field notation (without parens) to invoke function ~ts.~ts() is deprecated, " + "you must add parens instead: remote.function()", + [elixir_aliases:inspect(Atom), Fun] + ) + end, + 'Elixir.IO':warn_once(?MODULE, Message, 3), + {ok, apply(Atom, Fun, [])}; +no_parens_remote(_Other, _Fun) -> + error. + +parens_map_field(Key, Value) -> + Message = fun() -> + io_lib:format( + "using module.function() notation (with parentheses) to fetch map field ~ts is deprecated, " + "you must remove the parentheses: map.field", + [elixir_aliases:inspect(Key)] + ) + end, + 'Elixir.IO':warn_once(?MODULE, Message, 3), + Value. diff --git a/lib/ex_unit/lib/ex_unit/cli_formatter.ex b/lib/ex_unit/lib/ex_unit/cli_formatter.ex index 0cbc65e2a3..472a3bbea9 100644 --- a/lib/ex_unit/lib/ex_unit/cli_formatter.ex +++ b/lib/ex_unit/lib/ex_unit/cli_formatter.ex @@ -125,7 +125,7 @@ defmodule ExUnit.CLIFormatter do end def handle_cast({:module_started, %ExUnit.TestModule{name: name, file: file}}, config) do - if config.trace() do + if config.trace do IO.puts("\n#{inspect(name)} [#{Path.relative_to_cwd(file)}]") end diff --git a/lib/iex/lib/iex/autocomplete.ex b/lib/iex/lib/iex/autocomplete.ex index 3ea8062416..efa46b9773 100644 --- a/lib/iex/lib/iex/autocomplete.ex +++ b/lib/iex/lib/iex/autocomplete.ex @@ -338,7 +338,7 @@ defmodule IEx.Autocomplete do container_context_map_fields(pairs, map, hint) {:struct, alias, pairs} when context == :expr -> - map = Map.from_struct(alias.__struct__) + map = Map.from_struct(alias.__struct__()) container_context_map_fields(pairs, map, hint) :bitstring_modifier -> diff --git a/lib/mix/lib/mix/app_loader.ex b/lib/mix/lib/mix/app_loader.ex index 363e2f3cea..2392c528d5 100644 --- a/lib/mix/lib/mix/app_loader.ex +++ b/lib/mix/lib/mix/app_loader.ex @@ -38,7 +38,7 @@ defmodule Mix.AppLoader do else List.first( for %{app: app, scm: scm, opts: opts} <- Mix.Dep.cached(), - not scm.fetchable?, + not scm.fetchable?(), Mix.Utils.last_modified(Path.join([opts[:build], "ebin", "#{app}.app"])) > modified do manifest diff --git a/lib/mix/lib/mix/dep.ex b/lib/mix/lib/mix/dep.ex index 402fe60da0..2208910fd5 100644 --- a/lib/mix/lib/mix/dep.ex +++ b/lib/mix/lib/mix/dep.ex @@ -257,7 +257,7 @@ defmodule Mix.Dep do # If the dependency is not fetchable, then it is never compiled # from scratch and therefore it needs the parent configuration # files to know when to recompile. - config = [inherit_parent_config_files: not scm.fetchable?] ++ config + config = [inherit_parent_config_files: not scm.fetchable?()] ++ config env = opts[:env] || :prod old_env = Mix.env() diff --git a/lib/mix/lib/mix/dep/fetcher.ex b/lib/mix/lib/mix/dep/fetcher.ex index 8b44480355..2f3a3b3bbb 100644 --- a/lib/mix/lib/mix/dep/fetcher.ex +++ b/lib/mix/lib/mix/dep/fetcher.ex @@ -50,7 +50,7 @@ defmodule Mix.Dep.Fetcher do cond do # Dependencies that cannot be fetched are always compiled afterwards - not scm.fetchable? -> + not scm.fetchable?() -> {dep, [app | acc], lock} # If the dependency is not available or we have a lock mismatch diff --git a/lib/mix/lib/mix/dep/loader.ex b/lib/mix/lib/mix/dep/loader.ex index 49d7f6c1ef..f9b532c9fa 100644 --- a/lib/mix/lib/mix/dep/loader.ex +++ b/lib/mix/lib/mix/dep/loader.ex @@ -403,7 +403,7 @@ defmodule Mix.Dep.Loader do end defp recently_fetched?(%Mix.Dep{opts: opts, scm: scm}) do - scm.fetchable? && + scm.fetchable?() && Mix.Utils.stale?( join_stale(opts, :dest, ".fetch"), join_stale(opts, :build, ".mix/compile.fetch") diff --git a/lib/mix/lib/mix/project.ex b/lib/mix/lib/mix/project.ex index 84d7e0ded0..9c11f2f4ad 100644 --- a/lib/mix/lib/mix/project.ex +++ b/lib/mix/lib/mix/project.ex @@ -935,5 +935,5 @@ defmodule Mix.Project do @private_config [:build_scm, :deps_app_path, :deps_build_path] defp get_project_config(nil), do: [] - defp get_project_config(atom), do: atom.project |> Keyword.drop(@private_config) + defp get_project_config(atom), do: atom.project() |> Keyword.drop(@private_config) end diff --git a/lib/mix/lib/mix/tasks/compile.ex b/lib/mix/lib/mix/tasks/compile.ex index c7f90c45ec..5ed640909c 100644 --- a/lib/mix/lib/mix/tasks/compile.ex +++ b/lib/mix/lib/mix/tasks/compile.ex @@ -235,7 +235,7 @@ defmodule Mix.Tasks.Compile do module = Mix.Task.get("compile.#{compiler}") if module && function_exported?(module, :manifests, 0) do - module.manifests + module.manifests() else [] end diff --git a/lib/mix/lib/mix/tasks/compile.protocols.ex b/lib/mix/lib/mix/tasks/compile.protocols.ex index 16bc00d03b..62a34bc739 100644 --- a/lib/mix/lib/mix/tasks/compile.protocols.ex +++ b/lib/mix/lib/mix/tasks/compile.protocols.ex @@ -91,7 +91,7 @@ defmodule Mix.Tasks.Compile.Protocols do end defp protocols_and_impls(config) do - deps = for %{scm: scm, opts: opts} <- Mix.Dep.cached(), not scm.fetchable?, do: opts[:build] + deps = for %{scm: scm, opts: opts} <- Mix.Dep.cached(), not scm.fetchable?(), do: opts[:build] paths = if Mix.Project.umbrella?(config) do diff --git a/lib/mix/lib/mix/tasks/deps.clean.ex b/lib/mix/lib/mix/tasks/deps.clean.ex index 1fbdb41e74..f09cde8ad0 100644 --- a/lib/mix/lib/mix/tasks/deps.clean.ex +++ b/lib/mix/lib/mix/tasks/deps.clean.ex @@ -102,7 +102,7 @@ defmodule Mix.Tasks.Deps.Clean do defp do_clean(apps, deps, build_path, deps_path, build_only?) do shell = Mix.shell() - local = for %{scm: scm, app: app} <- deps, not scm.fetchable?, do: Atom.to_string(app) + local = for %{scm: scm, app: app} <- deps, not scm.fetchable?(), do: Atom.to_string(app) Enum.each(apps, fn app -> shell.info("* Cleaning #{app}") diff --git a/lib/mix/lib/mix/tasks/deps.compile.ex b/lib/mix/lib/mix/tasks/deps.compile.ex index 84ba8a3c8b..dd0ce4ec4d 100644 --- a/lib/mix/lib/mix/tasks/deps.compile.ex +++ b/lib/mix/lib/mix/tasks/deps.compile.ex @@ -121,7 +121,7 @@ defmodule Mix.Tasks.Deps.Compile do end defp touch_fetchable(scm, path) do - if scm.fetchable? do + if scm.fetchable?() do path = Path.join(path, ".mix") File.mkdir_p!(path) File.touch!(Path.join(path, "compile.fetch")) @@ -132,7 +132,7 @@ defmodule Mix.Tasks.Deps.Compile do end defp check_unavailable!(app, scm, {:unavailable, path}) do - if scm.fetchable? do + if scm.fetchable?() do Mix.raise( "Cannot compile dependency #{inspect(app)} because " <> "it isn't available, run \"mix deps.get\" first" @@ -339,13 +339,13 @@ defmodule Mix.Tasks.Deps.Compile do defp filter_available_and_local_deps(deps) do Enum.filter(deps, fn dep -> - Mix.Dep.available?(dep) or not dep.scm.fetchable? + Mix.Dep.available?(dep) or not dep.scm.fetchable?() end) end defp reject_local_deps(deps, options) do if options[:skip_local_deps] do - Enum.filter(deps, fn %{scm: scm} -> scm.fetchable? end) + Enum.filter(deps, fn %{scm: scm} -> scm.fetchable?() end) else deps end diff --git a/lib/mix/lib/mix/tasks/deps.loadpaths.ex b/lib/mix/lib/mix/tasks/deps.loadpaths.ex index f2756b1a95..cbb4bef6ec 100644 --- a/lib/mix/lib/mix/tasks/deps.loadpaths.ex +++ b/lib/mix/lib/mix/tasks/deps.loadpaths.ex @@ -123,7 +123,7 @@ defmodule Mix.Tasks.Deps.Loadpaths do # Every local dependency (i.e. that are not fetchable) # are automatically recompiled if they are ok. defp local?(dep) do - not dep.scm.fetchable? + not dep.scm.fetchable?() end defp show_not_ok!([]) do diff --git a/lib/mix/test/mix/tasks/compile.elixir_test.exs b/lib/mix/test/mix/tasks/compile.elixir_test.exs index ceac247373..1bb509fbdd 100644 --- a/lib/mix/test/mix/tasks/compile.elixir_test.exs +++ b/lib/mix/test/mix/tasks/compile.elixir_test.exs @@ -779,7 +779,7 @@ defmodule Mix.Tasks.Compile.ElixirTest do assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]} assert_received {:mix_shell, :info, ["Compiled lib/b.ex"]} - Mix.shell().flush + Mix.shell().flush() purge([A, B]) same_length_content = "lib/a.ex" |> File.read!() |> String.replace("A", "Z") @@ -812,7 +812,7 @@ defmodule Mix.Tasks.Compile.ElixirTest do assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]} assert_received {:mix_shell, :info, ["Compiled lib/b.ex"]} - Mix.shell().flush + Mix.shell().flush() purge([A, B]) future = {{2038, 1, 1}, {0, 0, 0}} @@ -843,7 +843,7 @@ defmodule Mix.Tasks.Compile.ElixirTest do assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]} assert_received {:mix_shell, :info, ["Compiled lib/b.ex"]} - Mix.shell().flush + Mix.shell().flush() purge([A, B]) # Compile with error @@ -882,7 +882,7 @@ defmodule Mix.Tasks.Compile.ElixirTest do assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]} assert_received {:mix_shell, :info, ["Compiled lib/b.ex"]} - Mix.shell().flush + Mix.shell().flush() purge([A, B]) File.write!("lib/a.ex", File.read!("lib/a.ex") <> "\n") @@ -903,7 +903,7 @@ defmodule Mix.Tasks.Compile.ElixirTest do assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]} assert_received {:mix_shell, :info, ["Compiled lib/b.ex"]} - Mix.shell().flush + Mix.shell().flush() purge([A, B]) force_recompilation("lib/b.ex") @@ -952,7 +952,7 @@ defmodule Mix.Tasks.Compile.ElixirTest do assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]} assert_received {:mix_shell, :info, ["Compiled lib/b.ex"]} - Mix.shell().flush + Mix.shell().flush() purge([A, B]) File.rm("lib/b.ex") @@ -1012,7 +1012,7 @@ defmodule Mix.Tasks.Compile.ElixirTest do # Compiles with missing external resources assert Mix.Tasks.Compile.Elixir.run(["--verbose"]) == {:ok, []} assert Mix.Tasks.Compile.Elixir.run(["--verbose"]) == {:noop, []} - Mix.shell().flush + Mix.shell().flush() purge([A, B]) # Update local existing resource timestamp is not enough @@ -1028,7 +1028,7 @@ defmodule Mix.Tasks.Compile.ElixirTest do # Does not update on old existing resource File.touch!("lib/a.eex", @old_time) assert Mix.Tasks.Compile.Elixir.run(["--verbose"]) == {:noop, []} - Mix.shell().flush + Mix.shell().flush() purge([A, B]) # Create external resource @@ -1211,7 +1211,7 @@ defmodule Mix.Tasks.Compile.ElixirTest do assert_received {:mix_shell, :info, ["Compiled lib/b.ex"]} refute function_exported?(A, :one, 0) - Mix.shell().flush + Mix.shell().flush() purge([A]) File.rm("lib/b.ex") diff --git a/lib/mix/test/mix/tasks/deps.git_test.exs b/lib/mix/test/mix/tasks/deps.git_test.exs index 92a8a4b95a..8983a6abb7 100644 --- a/lib/mix/test/mix/tasks/deps.git_test.exs +++ b/lib/mix/test/mix/tasks/deps.git_test.exs @@ -252,7 +252,7 @@ defmodule Mix.Tasks.DepsGitTest do assert_raise Mix.Error, fn -> Mix.Tasks.Deps.Loadpaths.run([]) end # Flush the errors we got, move to a clean slate - Mix.shell().flush + Mix.shell().flush() Mix.Task.clear() # Calling get should update the dependency @@ -281,7 +281,7 @@ defmodule Mix.Tasks.DepsGitTest do refute File.exists?("deps/git_repo/lib/git_repo.ex") # Flush the errors we got, move to a clean slate - Mix.shell().flush + Mix.shell().flush() Mix.Task.clear() Process.delete(:git_repo_opts) Mix.Project.pop() @@ -309,7 +309,7 @@ defmodule Mix.Tasks.DepsGitTest do assert File.exists?("deps/git_repo/lib/git_repo.ex") # Flush the errors we got, move to a clean slate - Mix.shell().flush + Mix.shell().flush() Mix.Task.clear() Process.put(:git_repo_opts, sparse: "sparse_dir") Mix.Project.pop() From e525aa0f49a1824164de6de5657a65fa462a1944 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 30 Oct 2023 23:39:43 +0100 Subject: [PATCH 0109/1886] Fix getting started links --- lib/elixir/lib/gen_server.ex | 2 +- lib/elixir/lib/process.ex | 2 +- lib/elixir/pages/references/syntax-reference.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/elixir/lib/gen_server.ex b/lib/elixir/lib/gen_server.ex index 79549a9c73..cf581bd522 100644 --- a/lib/elixir/lib/gen_server.ex +++ b/lib/elixir/lib/gen_server.ex @@ -475,7 +475,7 @@ defmodule GenServer do guide provides a tutorial-like introduction. The documentation and links in Erlang can also provide extra insight. - * [GenServer - Elixir's Getting Started Guide](https://elixir-lang.org/getting-started/mix-otp/genserver.html) + * [GenServer - Elixir's Getting Started Guide](genservers.md) * [`:gen_server` module documentation](`:gen_server`) * [gen_server Behaviour - OTP Design Principles](https://www.erlang.org/doc/design_principles/gen_server_concepts.html) * [Clients and Servers - Learn You Some Erlang for Great Good!](http://learnyousomeerlang.com/clients-and-servers) diff --git a/lib/elixir/lib/process.ex b/lib/elixir/lib/process.ex index e1968538ac..abb69426a8 100644 --- a/lib/elixir/lib/process.ex +++ b/lib/elixir/lib/process.ex @@ -504,7 +504,7 @@ defmodule Process do If the process is already dead when calling `Process.monitor/1`, a `:DOWN` message is delivered immediately. - See ["The need for monitoring"](https://elixir-lang.org/getting-started/mix-otp/genserver.html#the-need-for-monitoring) + See ["The need for monitoring"](genservers.md#the-need-for-monitoring) for an example. See `:erlang.monitor/2` for more information. Inlined by the compiler. diff --git a/lib/elixir/pages/references/syntax-reference.md b/lib/elixir/pages/references/syntax-reference.md index 5564ceb6af..857fe11070 100644 --- a/lib/elixir/pages/references/syntax-reference.md +++ b/lib/elixir/pages/references/syntax-reference.md @@ -395,7 +395,7 @@ end All of the constructs above are part of Elixir's syntax and have their own representation as part of the Elixir AST. This section will discuss the remaining constructs that are alternative representations of the constructs above. In other words, the constructs below can be represented in more than one way in your Elixir code and retain AST equivalence. We call this "Optional Syntax". -For a lightweight introduction to Elixir's Optional Syntax, [see this document](https://elixir-lang.org/getting-started/optional-syntax.html). Below we continue with a more complete reference. +For a lightweight introduction to Elixir's Optional Syntax, [see this document](optional-syntax.md). Below we continue with a more complete reference. ### Integers in other bases and Unicode code points From 7a570abe4935a5724b827f9023899876264d49cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 30 Oct 2023 23:54:38 +0100 Subject: [PATCH 0110/1886] Add missing module to dialyzer suite --- lib/elixir/test/elixir/kernel/dialyzer_test.exs | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/elixir/test/elixir/kernel/dialyzer_test.exs b/lib/elixir/test/elixir/kernel/dialyzer_test.exs index 6c2bef0ff4..889ec4e4ef 100644 --- a/lib/elixir/test/elixir/kernel/dialyzer_test.exs +++ b/lib/elixir/test/elixir/kernel/dialyzer_test.exs @@ -25,6 +25,7 @@ defmodule Kernel.DialyzerTest do mods = [ :elixir, :elixir_env, + :elixir_erl_pass, ArgumentError, Atom, Code, From f437e8695b14cd02b0407e9d71f111863b988ed7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 30 Oct 2023 23:55:36 +0100 Subject: [PATCH 0111/1886] Require Erlang/OTP 25+ (#13045) --- .github/workflows/builds.hex.pm.yml | 8 +-- .github/workflows/ci.yml | 14 ++--- .github/workflows/release.yml | 6 +- Makefile | 4 +- .../compatibility-and-deprecations.md | 1 + lib/elixir/src/elixir.erl | 15 +---- lib/elixir/test/elixir/code_test.exs | 56 +++++++++---------- lib/elixir/test/elixir/exception_test.exs | 16 +++--- lib/iex/lib/iex/evaluator.ex | 38 +++---------- 9 files changed, 58 insertions(+), 100 deletions(-) diff --git a/.github/workflows/builds.hex.pm.yml b/.github/workflows/builds.hex.pm.yml index 7cfb1d790d..bdfcb7ba9d 100644 --- a/.github/workflows/builds.hex.pm.yml +++ b/.github/workflows/builds.hex.pm.yml @@ -22,13 +22,11 @@ jobs: max-parallel: 1 matrix: include: - - otp: 24 - otp_version: '24.3' - upload_generic_version: upload_generic_version - otp: 25 - otp_version: '25.3' + otp_version: "25.3" + upload_generic_version: upload_generic_version - otp: 26 - otp_version: '26.0' + otp_version: "26.0" build_docs: build_docs runs-on: ubuntu-22.04 steps: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index afe1f6b4b5..00abece1d0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,10 +3,10 @@ name: CI on: push: paths-ignore: - - 'lib/**/*.md' + - "lib/**/*.md" pull_request: paths-ignore: - - 'lib/**/*.md' + - "lib/**/*.md" env: ELIXIR_ASSERT_TIMEOUT: 2000 @@ -24,12 +24,10 @@ jobs: fail-fast: false matrix: include: - - otp_version: '26.0' + - otp_version: "26.0" otp_latest: true - - otp_version: '25.3' - - otp_version: '25.0' - - otp_version: '24.3' - - otp_version: '24.0' + - otp_version: "25.3" + - otp_version: "25.0" - otp_version: master development: true - otp_version: maint @@ -77,7 +75,7 @@ jobs: name: Windows Server 2019, Erlang/OTP ${{ matrix.otp_version }} strategy: matrix: - otp_version: ['24', '25', '26'] + otp_version: ["25", "26"] runs-on: windows-2019 steps: - name: Configure Git diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8723e4752b..fec4e33c61 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -33,12 +33,10 @@ jobs: fail-fast: true matrix: include: - - otp: 24 - otp_version: '24.3' - otp: 25 - otp_version: '25.3' + otp_version: "25.3" - otp: 26 - otp_version: '26.0' + otp_version: "26.0" build_docs: build_docs runs-on: ubuntu-22.04 steps: diff --git a/Makefile b/Makefile index 1dd0b18fdc..7548d62a8a 100644 --- a/Makefile +++ b/Makefile @@ -27,9 +27,9 @@ SOURCE_DATE_EPOCH_FILE = $(SOURCE_DATE_EPOCH_PATH)/SOURCE_DATE_EPOCH #==> Functions define CHECK_ERLANG_RELEASE - erl -noshell -eval '{V,_} = string:to_integer(erlang:system_info(otp_release)), io:fwrite("~s", [is_integer(V) and (V >= 24)])' -s erlang halt | grep -q '^true'; \ + erl -noshell -eval '{V,_} = string:to_integer(erlang:system_info(otp_release)), io:fwrite("~s", [is_integer(V) and (V >= 25)])' -s erlang halt | grep -q '^true'; \ if [ $$? != 0 ]; then \ - echo "At least Erlang/OTP 24.0 is required to build Elixir"; \ + echo "At least Erlang/OTP 25.0 is required to build Elixir"; \ exit 1; \ fi endef diff --git a/lib/elixir/pages/references/compatibility-and-deprecations.md b/lib/elixir/pages/references/compatibility-and-deprecations.md index c60a3607dc..7658658caa 100644 --- a/lib/elixir/pages/references/compatibility-and-deprecations.md +++ b/lib/elixir/pages/references/compatibility-and-deprecations.md @@ -43,6 +43,7 @@ Erlang/OTP versioning is independent from the versioning of Elixir. Erlang relea Elixir version | Supported Erlang/OTP versions :------------- | :------------------------------- +1.16 | 24 - 26 1.15 | 24 - 26 1.14 | 23 - 25 1.13 | 22 - 24 (and Erlang/OTP 25 from v1.13.4) diff --git a/lib/elixir/src/elixir.erl b/lib/elixir/src/elixir.erl index a7a1e7451e..66b1cc7076 100644 --- a/lib/elixir/src/elixir.erl +++ b/lib/elixir/src/elixir.erl @@ -139,10 +139,10 @@ preload_common_modules() -> parse_otp_release() -> %% Whenever we change this check, we should also change Makefile. case string:to_integer(erlang:system_info(otp_release)) of - {Num, _} when Num >= 24 -> + {Num, _} when Num >= 25 -> Num; _ -> - io:format(standard_error, "ERROR! Unsupported Erlang/OTP version, expected Erlang/OTP 24+~n", []), + io:format(standard_error, "ERROR! Unsupported Erlang/OTP version, expected Erlang/OTP 25+~n", []), erlang:halt(1) end. @@ -361,7 +361,7 @@ eval_forms(Tree, Binding, OrigE, Opts) -> %% We use remote names so eval works across Elixir versions. LocalHandler = {value, fun ?MODULE:eval_local_handler/2}, - ExternalHandler = eval_external_handler(), + ExternalHandler = {value, fun ?MODULE:eval_external_handler/3}, {value, Value, NewBinding} = try @@ -394,11 +394,6 @@ eval_local_handler(FunName, Args) -> Exception = 'Elixir.UndefinedFunctionError':exception(Opts), erlang:raise(error, Exception, Stack). -%% TODO: Remove conditional once we require Erlang/OTP 25+. --if(?OTP_RELEASE >= 25). -eval_external_handler() -> - {value, fun ?MODULE:eval_external_handler/3}. - eval_external_handler(Ann, FunOrModFun, Args) -> try case FunOrModFun of @@ -461,10 +456,6 @@ drop_common([_ | T1], T2, ToDrop) -> drop_common(T1, T2, ToDrop); drop_common([], [{?MODULE, _, _, _} | T2], _ToDrop) -> T2; drop_common([], [_ | T2], true) -> T2; drop_common([], T2, _) -> T2. --else. -eval_external_handler() -> none. -eval_external_handler(_Ann, _FunOrModFun, _Args) -> error(unused). --endif. %% Converts a quoted expression to Erlang abstract format diff --git a/lib/elixir/test/elixir/code_test.exs b/lib/elixir/test/elixir/code_test.exs index b9be631cb7..8cef777253 100644 --- a/lib/elixir/test/elixir/code_test.exs +++ b/lib/elixir/test/elixir/code_test.exs @@ -181,25 +181,23 @@ defmodule CodeTest do end end - if System.otp_release() >= "25" do - test "includes eval file in stacktrace" do - try do - Code.eval_string("<>", [a: :a, b: :b], file: "myfile") - rescue - _ -> - assert Exception.format_stacktrace(__STACKTRACE__) =~ "myfile:1" - end + test "includes eval file in stacktrace" do + try do + Code.eval_string("<>", [a: :a, b: :b], file: "myfile") + rescue + _ -> + assert Exception.format_stacktrace(__STACKTRACE__) =~ "myfile:1" + end - try do - Code.eval_string( - "Enum.map([a: :a, b: :b], fn {a, b} -> <> end)", - [], - file: "myfile" - ) - rescue - _ -> - assert Exception.format_stacktrace(__STACKTRACE__) =~ "myfile:1" - end + try do + Code.eval_string( + "Enum.map([a: :a, b: :b], fn {a, b} -> <> end)", + [], + file: "myfile" + ) + rescue + _ -> + assert Exception.format_stacktrace(__STACKTRACE__) =~ "myfile:1" end end @@ -338,19 +336,17 @@ defmodule CodeTest do {[{{:x, :foo}, 2}], [x: :foo]} end - if :erlang.system_info(:otp_release) >= ~c"25" do - test "undefined function" do - env = Code.env_for_eval(__ENV__) - quoted = quote do: foo() + test "undefined function" do + env = Code.env_for_eval(__ENV__) + quoted = quote do: foo() - assert_exception( - UndefinedFunctionError, - ["** (UndefinedFunctionError) function foo/0 is undefined (there is no such import)"], - fn -> - Code.eval_quoted_with_env(quoted, [], env) - end - ) - end + assert_exception( + UndefinedFunctionError, + ["** (UndefinedFunctionError) function foo/0 is undefined (there is no such import)"], + fn -> + Code.eval_quoted_with_env(quoted, [], env) + end + ) end defmodule Tracer do diff --git a/lib/elixir/test/elixir/exception_test.exs b/lib/elixir/test/elixir/exception_test.exs index 79f7753907..7160c67b74 100644 --- a/lib/elixir/test/elixir/exception_test.exs +++ b/lib/elixir/test/elixir/exception_test.exs @@ -996,17 +996,15 @@ defmodule ExceptionTest do end end - if System.otp_release() >= "25" do - describe "binary constructor error info" do - defp concat(a, b), do: a <> b + describe "binary constructor error info" do + defp concat(a, b), do: a <> b - test "on binary concatenation" do - assert message(123, &concat(&1, "bar")) == - "construction of binary failed: segment 1 of type 'binary': expected a binary but got: 123" + test "on binary concatenation" do + assert message(123, &concat(&1, "bar")) == + "construction of binary failed: segment 1 of type 'binary': expected a binary but got: 123" - assert message(~D[0001-02-03], &concat(&1, "bar")) == - "construction of binary failed: segment 1 of type 'binary': expected a binary but got: ~D[0001-02-03]" - end + assert message(~D[0001-02-03], &concat(&1, "bar")) == + "construction of binary failed: segment 1 of type 'binary': expected a binary but got: ~D[0001-02-03]" end end diff --git a/lib/iex/lib/iex/evaluator.ex b/lib/iex/lib/iex/evaluator.ex index 5887625ec7..0ffab18843 100644 --- a/lib/iex/lib/iex/evaluator.ex +++ b/lib/iex/lib/iex/evaluator.ex @@ -432,36 +432,14 @@ defmodule IEx.Evaluator do end end - if System.otp_release() >= "25" do - defp prune_stacktrace(stack) do - stack - |> Enum.reverse() - |> Enum.drop_while(&(elem(&1, 0) != :elixir_eval)) - |> Enum.reverse() - |> case do - [] -> stack - stack -> stack - end - end - else - @elixir_internals [:elixir, :elixir_expand, :elixir_compiler, :elixir_module] ++ - [:elixir_clauses, :elixir_lexical, :elixir_def, :elixir_map] ++ - [:elixir_erl, :elixir_erl_clauses, :elixir_erl_pass] ++ - [Kernel.ErrorHandler, Module.ParallelChecker] - - defp prune_stacktrace(stacktrace) do - # The order in which each drop_while is listed is important. - # For example, the user may call Code.eval_string/2 in IEx - # and if there is an error we should not remove erl_eval - # and eval_bits information from the user stacktrace. - stacktrace - |> Enum.reverse() - |> Enum.drop_while(&(elem(&1, 0) == :proc_lib)) - |> Enum.drop_while(&(elem(&1, 0) == __MODULE__)) - |> Enum.drop_while(&(elem(&1, 0) in [Code, Module.ParallelChecker, :elixir])) - |> Enum.drop_while(&(elem(&1, 0) in [:erl_eval, :eval_bits])) - |> Enum.reverse() - |> Enum.reject(&(elem(&1, 0) in @elixir_internals)) + defp prune_stacktrace(stack) do + stack + |> Enum.reverse() + |> Enum.drop_while(&(elem(&1, 0) != :elixir_eval)) + |> Enum.reverse() + |> case do + [] -> stack + stack -> stack end end end From e3abea87fd1160070061ce0ff75166af0ba37db5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 31 Oct 2023 07:49:22 +0100 Subject: [PATCH 0112/1886] Improve complex extraction example --- lib/elixir/pages/anti-patterns/code-anti-patterns.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/elixir/pages/anti-patterns/code-anti-patterns.md b/lib/elixir/pages/anti-patterns/code-anti-patterns.md index 6ae84a3db5..c41dc17e44 100644 --- a/lib/elixir/pages/anti-patterns/code-anti-patterns.md +++ b/lib/elixir/pages/anti-patterns/code-anti-patterns.md @@ -101,11 +101,11 @@ end #### Problem -When we use multi-clause functions, it is possible to extract values in the clauses for further usage and for pattern matching/guard checking. This extraction itself does not represent an anti-pattern, but when you have too many clauses or too many arguments, it becomes hard to know which extracted parts are used for pattern/guards and what is used only inside the function body. This anti-pattern is related to [Unrelated multi-clause function](design-anti-patterns.md#unrelated-multi-clause-function), but with implications of its own. It impairs the code readability in a different way. +When we use multi-clause functions, it is possible to extract values in the clauses for further usage and for pattern matching/guard checking. This extraction itself does not represent an anti-pattern, but when you have *extractions made across several clauses and several arguments of the same function*, it becomes hard to know which extracted parts are used for pattern/guards and what is used only inside the function body. This anti-pattern is related to [Unrelated multi-clause function](design-anti-patterns.md#unrelated-multi-clause-function), but with implications of its own. It impairs the code readability in a different way. #### Example -The multi-clause function `drive/1` is extracting fields of an `%User{}` struct for usage in the clause expression (`age`) and for usage in the function body (`name`). Ideally, a function should not mix pattern matching extractions for usage in its guard expressions and also in its body. +The multi-clause function `drive/1` is extracting fields of an `%User{}` struct for usage in the clause expression (`age`) and for usage in the function body (`name`): ```elixir def drive(%User{name: name, age: age}) when age >= 18 do @@ -117,7 +117,7 @@ def drive(%User{name: name, age: age}) when age < 18 do end ``` -While the example is small and looks like a clear code, try to imagine a situation where `drive/1` was more complex, having many more clauses, arguments, and extractions. +While the example above is small and does not configure an anti-pattern, it is an example of mixed extraction and pattern matching. A situation where `drive/1` was more complex, having many more clauses, arguments, and extractions, would make it hard to know at a glance which variables are used for pattern/guards and which ones are not. #### Refactoring From 7ff58d75f5ce088ab8f70d7e4e50f3cc154b3875 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 31 Oct 2023 08:02:15 +0100 Subject: [PATCH 0113/1886] Fix warnings and messages on Erlang/OTP 25 --- lib/elixir/lib/kernel/cli.ex | 3 +- lib/elixir/test/elixir/inspect_test.exs | 53 +++++++------------------ lib/ex_unit/lib/ex_unit/diff.ex | 2 +- lib/logger/lib/logger/translator.ex | 3 +- 4 files changed, 17 insertions(+), 44 deletions(-) diff --git a/lib/elixir/lib/kernel/cli.ex b/lib/elixir/lib/kernel/cli.ex index 1a4955ffb8..17cce1a1f6 100644 --- a/lib/elixir/lib/kernel/cli.ex +++ b/lib/elixir/lib/kernel/cli.ex @@ -462,8 +462,7 @@ defmodule Kernel.CLI do defp process_command({:compile, patterns}, config) do # If ensuring the dir returns an error no files will be found. - # TODO: Use :filelib.ensure_path/1 once we require Erlang/OTP 25+ - _ = :filelib.ensure_dir(:filename.join(config.output, ".")) + _ = :filelib.ensure_path(config.output) case filter_multiple_patterns(patterns) do {:ok, []} -> diff --git a/lib/elixir/test/elixir/inspect_test.exs b/lib/elixir/test/elixir/inspect_test.exs index 2338c78b40..9bbfe3d55d 100644 --- a/lib/elixir/test/elixir/inspect_test.exs +++ b/lib/elixir/test/elixir/inspect_test.exs @@ -1,29 +1,5 @@ Code.require_file("test_helper.exs", __DIR__) -# This is to temporarily test some inconsistencies in -# the error ArgumentError messages. -# Remove MyArgumentError and replace the calls to: -# - MyArgumentError with ArgumentError -# - MyArgumentError.culprit() with Atom.to_string("Foo") -# in Erlang/OTP 25 -defmodule MyArgumentError do - defexception message: "argument error" - - @impl true - def message(_) do - """ - errors were found at the given arguments: - - * 1st argument: not an atom - """ - end - - def culprit() do - raise = fn -> raise(MyArgumentError) end - raise.() - end -end - defmodule Inspect.AtomTest do use ExUnit.Case, async: true @@ -467,20 +443,20 @@ defmodule Inspect.MapTest do defstruct @enforce_keys defimpl Inspect do - def inspect(%Failing{name: _name}, _) do - MyArgumentError.culprit() + def inspect(%Failing{name: name}, _) do + Atom.to_string(name) end end end test "safely inspect bad implementation" do - assert_raise MyArgumentError, ~r/errors were found at the given arguments:/, fn -> - raise(MyArgumentError) + assert_raise ArgumentError, ~r/argument error/, fn -> + raise(ArgumentError) end message = ~s''' #Inspect.Error< - got MyArgumentError with message: + got ArgumentError with message: """ errors were found at the given arguments: @@ -499,7 +475,7 @@ defmodule Inspect.MapTest do test "safely inspect bad implementation disables colors" do message = ~s''' #Inspect.Error< - got MyArgumentError with message: + got ArgumentError with message: """ errors were found at the given arguments: @@ -520,7 +496,7 @@ defmodule Inspect.MapTest do test "unsafely inspect bad implementation" do exception_message = ~s''' - got MyArgumentError with message: + got ArgumentError with message: """ errors were found at the given arguments: @@ -538,10 +514,7 @@ defmodule Inspect.MapTest do rescue exception in Inspect.Error -> assert Exception.message(exception) =~ exception_message - assert [{MyArgumentError, fun_name, 0, [{:file, _}, {:line, _} | _]} | _] = __STACKTRACE__ - - assert fun_name in [:"-culprit/0-fun-0-", :culprit] - assert Exception.message(exception) =~ exception_message + assert [{:erlang, :atom_to_binary, [_], [_ | _]} | _] = __STACKTRACE__ else _ -> flunk("expected failure") end @@ -552,7 +525,7 @@ defmodule Inspect.MapTest do # called by another exception (Protocol.UndefinedError in this case) exception_message = ~s''' protocol Enumerable not implemented for #Inspect.Error< - got MyArgumentError with message: + got ArgumentError with message: """ errors were found at the given arguments: @@ -597,9 +570,11 @@ defmodule Inspect.MapTest do end test "Exception.message/1 with bad implementation" do + failing = %Failing{name: "Foo"} + message = ~s''' #Inspect.Error< - got MyArgumentError with message: + got ArgumentError with message: """ errors were found at the given arguments: @@ -614,7 +589,7 @@ defmodule Inspect.MapTest do {my_argument_error, stacktrace} = try do - MyArgumentError.culprit() + Atom.to_string(failing.name) rescue e -> {e, __STACKTRACE__} @@ -629,7 +604,7 @@ defmodule Inspect.MapTest do ) ) - assert inspect(%Failing{name: "Foo"}, custom_options: [sort_maps: true]) =~ message + assert inspect(failing, custom_options: [sort_maps: true]) =~ message assert inspected =~ message end diff --git a/lib/ex_unit/lib/ex_unit/diff.ex b/lib/ex_unit/lib/ex_unit/diff.ex index 051fd250cc..06548cfed0 100644 --- a/lib/ex_unit/lib/ex_unit/diff.ex +++ b/lib/ex_unit/lib/ex_unit/diff.ex @@ -749,7 +749,7 @@ defmodule ExUnit.Diff do defp load_struct(struct) do if is_atom(struct) and struct != nil and Code.ensure_loaded?(struct) and function_exported?(struct, :__struct__, 0) do - struct.__struct__ + struct.__struct__() end end diff --git a/lib/logger/lib/logger/translator.ex b/lib/logger/lib/logger/translator.ex index 51446bea50..8521cb281d 100644 --- a/lib/logger/lib/logger/translator.ex +++ b/lib/logger/lib/logger/translator.ex @@ -183,8 +183,7 @@ defmodule Logger.Translator do end def translate(_min_level, :debug, :report, {:logger, [formatter_error: formatter] ++ data}) do - # TODO: Remove :catched once we require Erlang/OTP 25+ - case data[:caught] || data[:catched] do + case data[:caught] do {:throw, {:error, good, bad}, stacktrace} -> message = "bad return value from Logger formatter #{inspect(formatter)}, " <> From bc8463f5b2508b5c67820d2e10d12df28fea5b51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 31 Oct 2023 08:05:40 +0100 Subject: [PATCH 0114/1886] Remove more test suite warnings --- lib/elixir/lib/calendar.ex | 2 +- lib/elixir/test/erlang/tokenizer_test.erl | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/lib/elixir/lib/calendar.ex b/lib/elixir/lib/calendar.ex index b85eae4fe7..8bab6e2794 100644 --- a/lib/elixir/lib/calendar.ex +++ b/lib/elixir/lib/calendar.ex @@ -705,7 +705,7 @@ defmodule Calendar do # Hour using a 12-hour clock defp format_modifiers("I" <> rest, width, pad, datetime, format_options, acc) do - result = (rem(datetime.hour() + 23, 12) + 1) |> Integer.to_string() |> pad_leading(width, pad) + result = (rem(datetime.hour + 23, 12) + 1) |> Integer.to_string() |> pad_leading(width, pad) parse(rest, datetime, format_options, [result | acc]) end diff --git a/lib/elixir/test/erlang/tokenizer_test.erl b/lib/elixir/test/erlang/tokenizer_test.erl index b3df6a3c65..f03904e021 100644 --- a/lib/elixir/test/erlang/tokenizer_test.erl +++ b/lib/elixir/test/erlang/tokenizer_test.erl @@ -159,16 +159,14 @@ aliases_test() -> string_test() -> [{bin_string, {1, 1, nil}, [<<"foo">>]}] = tokenize("\"foo\""), - [{bin_string, {1, 1, nil}, [<<"f\"">>]}] = tokenize("\"f\\\"\""), - [{list_string, {1, 1, nil}, [<<"foo">>]}] = tokenize("'foo'"). + [{bin_string, {1, 1, nil}, [<<"f\"">>]}] = tokenize("\"f\\\"\""). heredoc_test() -> [{bin_heredoc, {1, 1, nil}, 0, [<<"heredoc\n">>]}] = tokenize("\"\"\"\nheredoc\n\"\"\""), [{bin_heredoc, {1, 1, nil}, 1, [<<"heredoc\n">>]}, {';', {3, 5, 0}}] = tokenize("\"\"\"\n heredoc\n \"\"\";"). empty_string_test() -> - [{bin_string, {1, 1, nil}, [<<>>]}] = tokenize("\"\""), - [{list_string, {1, 1, nil}, [<<>>]}] = tokenize("''"). + [{bin_string, {1, 1, nil}, [<<>>]}] = tokenize("\"\""). concat_test() -> [{identifier, {1, 1, _}, x}, From cda4b9452aed97fab0dcaf951438389b57d15a66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 31 Oct 2023 08:52:05 +0100 Subject: [PATCH 0115/1886] Describe them as potential anti-patterns --- .../pages/anti-patterns/code-anti-patterns.md | 4 +++- .../pages/anti-patterns/design-anti-patterns.md | 2 +- .../pages/anti-patterns/macro-anti-patterns.md | 2 +- .../pages/anti-patterns/process-anti-patterns.md | 2 +- .../pages/anti-patterns/what-anti-patterns.md | 14 ++++++++------ 5 files changed, 14 insertions(+), 10 deletions(-) diff --git a/lib/elixir/pages/anti-patterns/code-anti-patterns.md b/lib/elixir/pages/anti-patterns/code-anti-patterns.md index c41dc17e44..22479a3a80 100644 --- a/lib/elixir/pages/anti-patterns/code-anti-patterns.md +++ b/lib/elixir/pages/anti-patterns/code-anti-patterns.md @@ -1,6 +1,6 @@ # Code-related anti-patterns -This document outlines anti-patterns related to your code and particular Elixir idioms and features. +This document outlines potential anti-patterns related to your code and particular Elixir idioms and features. ## Comments @@ -295,6 +295,8 @@ defmodule PlugAuth do end ``` +#### Additional remarks + There are few known exceptions to this anti-pattern: * [Protocol implementations](`Kernel.defimpl/2`) are, by design, defined under the protocol namespace diff --git a/lib/elixir/pages/anti-patterns/design-anti-patterns.md b/lib/elixir/pages/anti-patterns/design-anti-patterns.md index 9afada4bea..f1eb627988 100644 --- a/lib/elixir/pages/anti-patterns/design-anti-patterns.md +++ b/lib/elixir/pages/anti-patterns/design-anti-patterns.md @@ -1,6 +1,6 @@ # Design-related anti-patterns -This document outlines anti-patterns related to your modules, functions, and the role they +This document outlines potential anti-patterns related to your modules, functions, and the role they play within a codebase. ## Alternative return types diff --git a/lib/elixir/pages/anti-patterns/macro-anti-patterns.md b/lib/elixir/pages/anti-patterns/macro-anti-patterns.md index 1784c7318e..26a0c60302 100644 --- a/lib/elixir/pages/anti-patterns/macro-anti-patterns.md +++ b/lib/elixir/pages/anti-patterns/macro-anti-patterns.md @@ -1,6 +1,6 @@ # Meta-programming anti-patterns -This document outlines anti-patterns related to meta-programming. +This document outlines potential anti-patterns related to meta-programming. ## Large code generation by macros diff --git a/lib/elixir/pages/anti-patterns/process-anti-patterns.md b/lib/elixir/pages/anti-patterns/process-anti-patterns.md index 5d83d8f32f..7a7378279b 100644 --- a/lib/elixir/pages/anti-patterns/process-anti-patterns.md +++ b/lib/elixir/pages/anti-patterns/process-anti-patterns.md @@ -1,6 +1,6 @@ # Process-related anti-patterns -This document outlines anti-patterns related to processes and process-based abstractions. +This document outlines potential anti-patterns related to processes and process-based abstractions. ## Code organization by process diff --git a/lib/elixir/pages/anti-patterns/what-anti-patterns.md b/lib/elixir/pages/anti-patterns/what-anti-patterns.md index 1cb4e72eb6..5977eb5464 100644 --- a/lib/elixir/pages/anti-patterns/what-anti-patterns.md +++ b/lib/elixir/pages/anti-patterns/what-anti-patterns.md @@ -1,14 +1,14 @@ # What are anti-patterns? -Anti-patterns describe common mistakes or indicators of potential problems in code. +Anti-patterns describe common mistakes or indicators of problems in code. They are also known as "code smells". -The goal of these guides is to document known anti-patterns found in Elixir software -and teach developers how to identify them and their limitations. If an existing piece +The goal of these guides is to document potential anti-patterns found in Elixir software +and teach developers how to identify them and their pitfalls. If an existing piece of code matches an anti-pattern, it does not mean your code must be rewritten. -No codebase is free of anti-patterns and one should not aim to remove all -anti-patterns of a codebase. The goal is to promote a discussion of potential -pitfalls and provide alternatives into consideration. +Sometimes, even if a snippet matches a potential anti-pattern and its limitations, +it may be the best approach to the problem at hand. No codebase is free of anti-patterns +and one should not aim to remove all of them. The anti-patterns in these guides are broken into 4 main categories: @@ -36,6 +36,8 @@ Each anti-pattern is documented using the following structure: * **Refactoring:** Ways to change your code to improve its qualities. Examples of refactored code are presented to illustrate these changes. +An additional section with "Additional Remarks" may be provided. Those may include known scenarios where the anti-pattern does not apply. + The initial catalog of anti-patterns was proposed by Lucas Vegi and Marco Tulio Valente, from [ASERG/DCC/UFMG](http://aserg.labsoft.dcc.ufmg.br/). For more info, see [Understanding Code Smells in Elixir Functional Language](https://github.com/lucasvegi/Elixir-Code-Smells/blob/main/etc/2023-emse-code-smells-elixir.pdf) and [the associated code repository](https://github.com/lucasvegi/Elixir-Code-Smells). Additionally, the Security Working Group of the [Erlang Ecosystem Foundation](https://erlef.github.io/security-wg/) publishes [documents with security resources and best-practices of both Erlang and Elixir, including detailed guides for web applications](https://erlef.github.io/security-wg/). From 2b5c2a1e54a026aa0d6af5b2f34d8201b74cb0e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?George=20Guimar=C3=A3es?= Date: Tue, 31 Oct 2023 05:27:57 -0300 Subject: [PATCH 0116/1886] Also lint README.md using markdownlint (#13044) --- .github/workflows/ci-markdown.yml | 1 + .markdownlint.jsonc | 1 + README.md | 26 +++++++++++++------------- 3 files changed, 15 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci-markdown.yml b/.github/workflows/ci-markdown.yml index de96eeb629..4d6b966196 100644 --- a/.github/workflows/ci-markdown.yml +++ b/.github/workflows/ci-markdown.yml @@ -31,3 +31,4 @@ jobs: with: globs: | lib/elixir/pages/**/*.md + README.md diff --git a/.markdownlint.jsonc b/.markdownlint.jsonc index 97b74672ff..3ab6858ffd 100644 --- a/.markdownlint.jsonc +++ b/.markdownlint.jsonc @@ -22,6 +22,7 @@ // Allowed HTML inline elements. "MD033": { "allowed_elements": [ + "h1", "a", "br", "img", diff --git a/README.md b/README.md index 1cb38e0827..8ccc427b3c 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -Elixir -Elixir +

Elixir +Elixir

[![CI](https://github.com/elixir-lang/elixir/workflows/CI/badge.svg?branch=main)](https://github.com/elixir-lang/elixir/actions?query=branch%3Amain+workflow%3ACI) @@ -12,7 +12,7 @@ For more about Elixir, installation and documentation, ## Policies New releases are announced in the [announcement mailing list][8]. -You can subscribe by sending an email to elixir-lang-ann+subscribe@googlegroups.com +You can subscribe by sending an email to and replying to the confirmation email. All security releases [will be tagged with `[security]`][10]. For more @@ -25,7 +25,7 @@ All interactions in our official communication channels follow our For reporting bugs, [visit our issue tracker][2] and follow the steps for reporting a new issue. **Please disclose security vulnerabilities -privately at elixir-security@googlegroups.com**. +privately at **. ## Issues tracker management @@ -107,17 +107,17 @@ We welcome everyone to contribute to Elixir. To do so, there are a few things you need to know about the code. First, Elixir code is divided in applications inside the `lib` folder: -* `elixir` - Elixir's kernel and standard library + * `elixir` - Elixir's kernel and standard library -* `eex` - EEx is the template engine that allows you to embed Elixir + * `eex` - EEx is the template engine that allows you to embed Elixir -* `ex_unit` - ExUnit is a simple test framework that ships with Elixir + * `ex_unit` - ExUnit is a simple test framework that ships with Elixir -* `iex` - IEx stands for Interactive Elixir: Elixir's interactive shell + * `iex` - IEx stands for Interactive Elixir: Elixir's interactive shell -* `logger` - Logger is the built-in logger + * `logger` - Logger is the built-in logger -* `mix` - Mix is Elixir's build tool + * `mix` - Mix is Elixir's build tool You can run all tests in the root directory with `make test` and you can also run tests for a specific framework `make test_#{APPLICATION}`, for example, @@ -166,9 +166,9 @@ With tests running and passing, you are ready to contribute to Elixir and We have saved some excellent pull requests we have received in the past in case you are looking for some examples: -* [Implement Enum.member? - Pull request](https://github.com/elixir-lang/elixir/pull/992) -* [Add String.valid? - Pull request](https://github.com/elixir-lang/elixir/pull/1058) -* [Implement capture_io for ExUnit - Pull request](https://github.com/elixir-lang/elixir/pull/1059) + * [Implement Enum.member? - Pull request](https://github.com/elixir-lang/elixir/pull/992) + * [Add String.valid? - Pull request](https://github.com/elixir-lang/elixir/pull/1058) + * [Implement capture_io for ExUnit - Pull request](https://github.com/elixir-lang/elixir/pull/1059) ### Reviewing changes From e5d5b435ff3637a8bda460377ec116be2c93e14c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 31 Oct 2023 11:59:51 +0100 Subject: [PATCH 0117/1886] Handle warnings from unquote functions --- lib/elixir/lib/module/parallel_checker.ex | 2 +- .../test/elixir/kernel/diagnostics_test.exs | 2 +- .../elixir/module/types/integration_test.exs | 23 +++++++++++++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/lib/elixir/lib/module/parallel_checker.ex b/lib/elixir/lib/module/parallel_checker.ex index fa16a310b3..1dab57db3d 100644 --- a/lib/elixir/lib/module/parallel_checker.ex +++ b/lib/elixir/lib/module/parallel_checker.ex @@ -334,7 +334,7 @@ defmodule Module.ParallelChecker do defp position_to_tuple(position) do case position[:column] do - nil -> position[:line] + nil -> position[:line] || 0 col -> {position[:line], col} end end diff --git a/lib/elixir/test/elixir/kernel/diagnostics_test.exs b/lib/elixir/test/elixir/kernel/diagnostics_test.exs index c2cf1a98aa..6e30f0f10b 100644 --- a/lib/elixir/test/elixir/kernel/diagnostics_test.exs +++ b/lib/elixir/test/elixir/kernel/diagnostics_test.exs @@ -153,7 +153,7 @@ defmodule Kernel.DiagnosticsTest do """ end - test "trim inbetween lines if too many" do + test "trim in between lines if too many" do output = capture_raise( """ diff --git a/lib/elixir/test/elixir/module/types/integration_test.exs b/lib/elixir/test/elixir/module/types/integration_test.exs index d38a6ce617..ee8cdca18b 100644 --- a/lib/elixir/test/elixir/module/types/integration_test.exs +++ b/lib/elixir/test/elixir/module/types/integration_test.exs @@ -695,6 +695,29 @@ defmodule Module.Types.IntegrationTest do assert_warnings(files, warnings) end + + test "reports unquote functions" do + files = %{ + "a.ex" => """ + defmodule A do + @deprecated "oops" + def a, do: :ok + end + """, + "b.ex" => """ + defmodule B do + def b, do: unquote(&A.a/0) + end + """ + } + + warnings = [ + "A.a/0 is deprecated. oops", + "b.ex: B.b/0" + ] + + assert_warnings(files, warnings) + end end defp assert_warnings(files, expected) when is_binary(expected) do From d1f493cff58eef21c9e08884340c5005663b58a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?George=20Guimar=C3=A3es?= Date: Tue, 31 Oct 2023 10:01:32 -0300 Subject: [PATCH 0118/1886] Update README.md with new prefers-color-scheme media feature. The old GitHub method using #gh-dark-mode-only and #gh-light-mode-only is deprecated and will stop working --- .markdownlint.jsonc | 2 ++ README.md | 8 ++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.markdownlint.jsonc b/.markdownlint.jsonc index 3ab6858ffd..4f0816602d 100644 --- a/.markdownlint.jsonc +++ b/.markdownlint.jsonc @@ -26,6 +26,8 @@ "a", "br", "img", + "picture", + "source", "noscript", "p", "script" diff --git a/README.md b/README.md index 8ccc427b3c..de23421e07 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ -

Elixir -Elixir

+

+ + + Elixir logo + +

[![CI](https://github.com/elixir-lang/elixir/workflows/CI/badge.svg?branch=main)](https://github.com/elixir-lang/elixir/actions?query=branch%3Amain+workflow%3ACI) From e0ce98a0cfcbb20cebc02289da40328a5a3c279d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 31 Oct 2023 16:19:59 +0100 Subject: [PATCH 0119/1886] Add missing parens --- lib/mix/lib/mix/tasks/test.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mix/lib/mix/tasks/test.ex b/lib/mix/lib/mix/tasks/test.ex index 8780929d39..d49b9dca28 100644 --- a/lib/mix/lib/mix/tasks/test.ex +++ b/lib/mix/lib/mix/tasks/test.ex @@ -538,7 +538,7 @@ defmodule Mix.Tasks.Test do # Start the app and configure ExUnit with command line options # before requiring test_helper.exs so that the configuration is # available in test_helper.exs - Mix.shell().print_app + Mix.shell().print_app() app_start_args = if opts[:slowest], do: ["--preload-modules" | args], else: args Mix.Task.run("app.start", app_start_args) From c0ac2426358c0e93c4b3521e6062f0ab8a4b6b87 Mon Sep 17 00:00:00 2001 From: Jean Klingler Date: Wed, 1 Nov 2023 00:28:57 +0900 Subject: [PATCH 0120/1886] Remove calling_self clause in GenServer (#13047) --- lib/elixir/lib/gen_server.ex | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/elixir/lib/gen_server.ex b/lib/elixir/lib/gen_server.ex index cf581bd522..fcef177baa 100644 --- a/lib/elixir/lib/gen_server.ex +++ b/lib/elixir/lib/gen_server.ex @@ -1106,10 +1106,6 @@ defmodule GenServer do nil -> exit({:noproc, {__MODULE__, :call, [server, request, timeout]}}) - # TODO: remove this clause when we require Erlang/OTP 25+ - pid when pid == self() -> - exit({:calling_self, {__MODULE__, :call, [server, request, timeout]}}) - pid -> try do :gen.call(pid, :"$gen_call", request, timeout) From ebbe71f802a2bb2717a56423c1ceacfe19db58e1 Mon Sep 17 00:00:00 2001 From: Jean Klingler Date: Wed, 1 Nov 2023 00:43:30 +0900 Subject: [PATCH 0121/1886] Use :erlang.float_to_binary/2 in Float.to_string/1 (#13046) --- lib/elixir/lib/float.ex | 4 ++-- lib/elixir/lib/inspect.ex | 2 +- lib/elixir/lib/list/chars.ex | 2 +- lib/elixir/lib/string/chars.ex | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/elixir/lib/float.ex b/lib/elixir/lib/float.ex index 22973c0223..87aebdba11 100644 --- a/lib/elixir/lib/float.ex +++ b/lib/elixir/lib/float.ex @@ -576,7 +576,7 @@ defmodule Float do """ @spec to_charlist(float) :: charlist def to_charlist(float) when is_float(float) do - :io_lib_format.fwrite_g(float) + :erlang.float_to_list(float, [:short]) end @doc """ @@ -603,7 +603,7 @@ defmodule Float do """ @spec to_string(float) :: String.t() def to_string(float) when is_float(float) do - IO.iodata_to_binary(:io_lib_format.fwrite_g(float)) + :erlang.float_to_binary(float, [:short]) end @doc false diff --git a/lib/elixir/lib/inspect.ex b/lib/elixir/lib/inspect.ex index 725cc6e76d..d901f18916 100644 --- a/lib/elixir/lib/inspect.ex +++ b/lib/elixir/lib/inspect.ex @@ -387,7 +387,7 @@ defimpl Inspect, for: Float do if abs >= 1.0 and abs < 1.0e16 and trunc(float) == float do [Integer.to_string(trunc(float)), ?., ?0] else - :io_lib_format.fwrite_g(float) + :erlang.float_to_list(float, [:short]) end color(IO.iodata_to_binary(formatted), :number, opts) diff --git a/lib/elixir/lib/list/chars.ex b/lib/elixir/lib/list/chars.ex index 2ffa8cad60..eab1ab9a94 100644 --- a/lib/elixir/lib/list/chars.ex +++ b/lib/elixir/lib/list/chars.ex @@ -58,6 +58,6 @@ end defimpl List.Chars, for: Float do def to_charlist(term) do - :io_lib_format.fwrite_g(term) + :erlang.float_to_list(term, [:short]) end end diff --git a/lib/elixir/lib/string/chars.ex b/lib/elixir/lib/string/chars.ex index af6eabf45d..272ed227c6 100644 --- a/lib/elixir/lib/string/chars.ex +++ b/lib/elixir/lib/string/chars.ex @@ -57,6 +57,6 @@ end defimpl String.Chars, for: Float do def to_string(term) do - IO.iodata_to_binary(:io_lib_format.fwrite_g(term)) + :erlang.float_to_binary(term, [:short]) end end From e88979dfddfc650eecf655589ec778cb6058269b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 31 Oct 2023 20:57:09 +0100 Subject: [PATCH 0122/1886] Add more examples to unrelated clauses --- .../anti-patterns/design-anti-patterns.md | 71 ++++++++++++------- 1 file changed, 44 insertions(+), 27 deletions(-) diff --git a/lib/elixir/pages/anti-patterns/design-anti-patterns.md b/lib/elixir/pages/anti-patterns/design-anti-patterns.md index f1eb627988..9fe89c6dea 100644 --- a/lib/elixir/pages/anti-patterns/design-anti-patterns.md +++ b/lib/elixir/pages/anti-patterns/design-anti-patterns.md @@ -302,7 +302,7 @@ Using multi-clause functions in Elixir, to group functions of the same name, is #### Example -A frequent example of this usage of multi-clause functions is when developers mix unrelated business logic into the same function definition. Such functions often have generic names or too broad specifications, making it difficult for other developers to understand and maintain them. +A frequent example of this usage of multi-clause functions is when developers mix unrelated business logic into the same function definition, in a way the behaviour of each clause is completely distinct from the other ones. Such functions often have too broad specifications, making it difficult for other developers to understand and maintain them. Some developers may use documentation mechanisms such as `@doc` annotations to compensate for poor code readability, however the documentation itself may end-up full of conditionals to describe how the function behaves for each different argument combination. This is a good indicator that the clauses are ultimately unrelated. @@ -310,65 +310,82 @@ Some developers may use documentation mechanisms such as `@doc` annotations to c @doc """ Updates a struct. -If given a "sharp" product (metal or glass with empty count), -it will... - -If given a blunt product, it will... +If given a product, it will... If given an animal, it will... """ -def update(%Product{count: nil, material: material}) - when material in ["metal", "glass"] do +def update(%Product{count: count, material: material}) do # ... end -def update(%Product{count: count, material: material}) - when count > 0 and material not in ["metal", "glass"] do - # ... -end - -def update(%Animal{count: 1, skin: skin}) - when skin in ["fur", "hairy"] do +def update(%Animal{count: count, skin: skin}) do # ... end ``` +If updating an animal is completely different from updating a product and requires a different set of rules, it may be worth splitting those over different functions or even different modules. + #### Refactoring -As shown below, a possible solution to this anti-pattern is to break the business rules that are mixed up in a single unrelated multi-clause function in several different simple functions. More precise names make the scope of the function clear. Each function can have a specific `@doc`, describing its behavior and parameters received. While this refactoring sounds simple, it can have a lot of impact on the function's current users, so be careful! +As shown below, a possible solution to this anti-pattern is to break the business rules that are mixed up in a single unrelated multi-clause function in simple functions. Each function can have a specific name and `@doc`, describing its behavior and parameters received. While this refactoring sounds simple, it can impact the function's current users, so be careful! ```elixir @doc """ -Updates a "sharp" product. +Updates a product. It will... """ -def update_sharp_product(%Product{count: nil, material: material}) - when material in ["metal", "glass"] do +def update_product(%Product{count: count, material: material}) do # ... end @doc """ -Updates a "blunt" product. +Updates an animal. It will... """ -def update_blunt_product(%Product{count: count, material: material}) - when count > 0 and material not in ["metal", "glass"] do +def update_animal(%Animal{count: count, skin: skin}) do # ... end +``` -@doc """ -Updates an animal. +These functions may still be implemented with multiple clauses, as long as the clauses group related funtionality. For example, `update_product` could be in practice implemented as follows: -It will... -""" -def update_animal(%Animal{count: 1, skin: skin}) - when skin in ["fur", "hairy"] do +```elixir +def update_product(%Product{count: 0}) do + # ... +end + +def update_product(%Product{material: material}) + when material in ["metal", "glass"] do + # ... +end + +def update_product(%Product{material: material}) + when material not in ["metal", "glass"] do # ... end ``` +You can see this pattern in practice within Elixir itself. The `+/2` operator can add `Integer`s and `Float`s together, but not `String`s, which instead use the `<>/2` operator. In this sense, it is reasonable to handle integers and floats in the same operation, but strings are unrelated enough to deserve their own function. + +You will also find examples in Elixir of functions that work with any struct, such as `struct/2`: + +```elixir +iex> struct(URI.parse("/foo/bar"), path: "/bar/baz") +%URI{ + scheme: nil, + userinfo: nil, + host: nil, + port: nil, + path: "/bar/baz", + query: nil, + fragment: nil +} +``` + +The difference here is that the `struct/2` function behaves precisely the same for any struct given, therefore there is no question of how the function handles different inputs. If the behaviour is clear and consistent for all inputs, then the anti-pattern does not take place. + ## Using application configuration for libraries #### Problem From 13f79ef0f839e0481d791d10e0ba540ba11520ac Mon Sep 17 00:00:00 2001 From: Artem Solomatin Date: Wed, 1 Nov 2023 00:14:04 +0300 Subject: [PATCH 0123/1886] Add spec for Mix.installed?() (#13049) --- lib/mix/lib/mix.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/mix/lib/mix.ex b/lib/mix/lib/mix.ex index 576cfb5320..d46cd7f113 100644 --- a/lib/mix/lib/mix.ex +++ b/lib/mix/lib/mix.ex @@ -985,6 +985,7 @@ defmodule Mix do Returns whether `Mix.install/2` was called in the current node. """ @doc since: "1.13.0" + @spec installed?() :: boolean() def installed? do Mix.State.get(:installed) != nil end From cc5a823f5c29c8ea0b80b95d36096572cde4b96f Mon Sep 17 00:00:00 2001 From: Wojtek Mach Date: Tue, 31 Oct 2023 22:37:05 +0100 Subject: [PATCH 0124/1886] Mix.install is no longer considered experimental --- lib/mix/lib/mix.ex | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/mix/lib/mix.ex b/lib/mix/lib/mix.ex index d46cd7f113..96cc982042 100644 --- a/lib/mix/lib/mix.ex +++ b/lib/mix/lib/mix.ex @@ -661,9 +661,6 @@ defmodule Mix do This function can only be called outside of a Mix project and only with the same dependencies in the given VM. - **Note:** this feature is currently experimental and it may change - in future releases. - ## Options * `:force` - if `true`, runs with empty install cache. This is useful when you want From b43b2e9f077962db6e4c1345340b0acdb9e12b92 Mon Sep 17 00:00:00 2001 From: Steven C Date: Wed, 1 Nov 2023 16:14:16 +0800 Subject: [PATCH 0125/1886] Fix typo around Enum.slide/3 in the Enum cheatsheet (#13053) --- lib/elixir/pages/cheatsheets/enum-cheat.cheatmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/pages/cheatsheets/enum-cheat.cheatmd b/lib/elixir/pages/cheatsheets/enum-cheat.cheatmd index 1e6190b62a..deedabad05 100644 --- a/lib/elixir/pages/cheatsheets/enum-cheat.cheatmd +++ b/lib/elixir/pages/cheatsheets/enum-cheat.cheatmd @@ -707,7 +707,7 @@ fruits = ["apple", "banana", "grape", "orange", "pear"] iex> Enum.slide(fruits, 2, 0) ["grape", "apple", "banana", "orange", "pear"] iex> Enum.slide(fruits, 2, 4) -["apple", "banana", "orange", "pear", "grape", ] +["apple", "banana", "orange", "pear", "grape"] iex> Enum.slide(fruits, 1..3, 0) ["banana", "grape", "orange", "apple", "pear"] iex> Enum.slide(fruits, 1..3, 4) From 4cc9ed53c59295e75d6834cd87095f4354f78717 Mon Sep 17 00:00:00 2001 From: Udo <379915+optikfluffel@users.noreply.github.com> Date: Wed, 1 Nov 2023 09:35:00 +0100 Subject: [PATCH 0126/1886] Fix typo in design-anti-patterns.md (#13056) --- lib/elixir/pages/anti-patterns/design-anti-patterns.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/pages/anti-patterns/design-anti-patterns.md b/lib/elixir/pages/anti-patterns/design-anti-patterns.md index 9fe89c6dea..98e1ad366f 100644 --- a/lib/elixir/pages/anti-patterns/design-anti-patterns.md +++ b/lib/elixir/pages/anti-patterns/design-anti-patterns.md @@ -349,7 +349,7 @@ def update_animal(%Animal{count: count, skin: skin}) do end ``` -These functions may still be implemented with multiple clauses, as long as the clauses group related funtionality. For example, `update_product` could be in practice implemented as follows: +These functions may still be implemented with multiple clauses, as long as the clauses group related functionality. For example, `update_product` could be in practice implemented as follows: ```elixir def update_product(%Product{count: 0}) do From d94d76721c79d24e0f54be88c447dab3a21d6802 Mon Sep 17 00:00:00 2001 From: Jean Klingler Date: Wed, 1 Nov 2023 18:02:18 +0900 Subject: [PATCH 0127/1886] Fix Enum.slide/3 example in cheatsheet (#13054) --- lib/elixir/pages/cheatsheets/enum-cheat.cheatmd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/pages/cheatsheets/enum-cheat.cheatmd b/lib/elixir/pages/cheatsheets/enum-cheat.cheatmd index deedabad05..94b071d63c 100644 --- a/lib/elixir/pages/cheatsheets/enum-cheat.cheatmd +++ b/lib/elixir/pages/cheatsheets/enum-cheat.cheatmd @@ -711,7 +711,7 @@ iex> Enum.slide(fruits, 2, 4) iex> Enum.slide(fruits, 1..3, 0) ["banana", "grape", "orange", "apple", "pear"] iex> Enum.slide(fruits, 1..3, 4) -["banana", "pear", "grape", "orange", "apple"] +["apple", "pear", "banana", "grape", "orange"] ``` ## Reversing From b2e68903a9d83adf367f3a1e7d630754c446e8e2 Mon Sep 17 00:00:00 2001 From: Artem Solomatin Date: Wed, 1 Nov 2023 12:22:39 +0300 Subject: [PATCH 0128/1886] Add spec for IEx.Pry.whereami (#13051) --- lib/iex/lib/iex/pry.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/iex/lib/iex/pry.ex b/lib/iex/lib/iex/pry.ex index 82842605f7..ce858e48ba 100644 --- a/lib/iex/lib/iex/pry.ex +++ b/lib/iex/lib/iex/pry.ex @@ -116,6 +116,7 @@ defmodule IEx.Pry do The actual line is especially formatted in bold. """ + @spec whereami(String.t(), non_neg_integer(), pos_integer()) :: {:ok, IO.chardata()} | :error def whereami(file, line, radius) when is_binary(file) and is_integer(line) and is_integer(radius) and radius > 0 do with true <- File.regular?(file), From f5a61d15b7e8f453cbc7b7796615fdd37d9b3e05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 1 Nov 2023 10:34:52 +0100 Subject: [PATCH 0129/1886] Update docs --- lib/elixir/lib/gen_server.ex | 11 ----------- lib/elixir/lib/module.ex | 13 +++++++++++-- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/lib/elixir/lib/gen_server.ex b/lib/elixir/lib/gen_server.ex index fcef177baa..8102d5d478 100644 --- a/lib/elixir/lib/gen_server.ex +++ b/lib/elixir/lib/gen_server.ex @@ -8,17 +8,6 @@ defmodule GenServer do will have a standard set of interface functions and include functionality for tracing and error reporting. It will also fit into a supervision tree. - ```mermaid - graph TD - GenServer - GenServer -. reply -.-> A - GenServer -. reply -.-> B - GenServer -. reply -.-> C - A(Client #1) -- request --> GenServer - B(Client #2) -- request --> GenServer - C(Client #3) -- request --> GenServer - ``` - ## Example The GenServer behaviour abstracts the common client-server interaction. diff --git a/lib/elixir/lib/module.ex b/lib/elixir/lib/module.ex index 2184a34569..087c04f1a0 100644 --- a/lib/elixir/lib/module.ex +++ b/lib/elixir/lib/module.ex @@ -326,8 +326,17 @@ defmodule Module do ### `@nifs` (since v1.16.0) A list of functions and their arities which will be overridden - by a native implementation (NIF). See the Erlang documentation - for more information: https://www.erlang.org/doc/man/erl_nif + by a native implementation (NIF). + + defmodule MyLibrary.MyModule do + @nifs [foo: 1, bar: 2] + + def foo(arg1), do: :erlang.nif_error(:not_loaded) + def bar(arg1, arg2), do: :erlang.nif_error(:not_loaded) + end + + See the Erlang documentation for more information: + https://www.erlang.org/doc/man/erl_nif ### `@on_definition` From 49e94729d75c56ce5c04add15db947efcf035ec2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 1 Nov 2023 11:17:44 +0100 Subject: [PATCH 0130/1886] Bring behaviour section from website --- lib/elixir/pages/references/typespecs.md | 143 +++++++++++++++++------ 1 file changed, 107 insertions(+), 36 deletions(-) diff --git a/lib/elixir/pages/references/typespecs.md b/lib/elixir/pages/references/typespecs.md index d0281000a1..163219d88d 100644 --- a/lib/elixir/pages/references/typespecs.md +++ b/lib/elixir/pages/references/typespecs.md @@ -17,7 +17,7 @@ Type specifications (most often referred to as *typespecs*) are defined in diffe * `@callback` * `@macrocallback` -In addition, you can use `@typedoc` to describe a custom `@type` definition. +In addition, you can use `@typedoc` to document a custom `@type` definition. See the "User-defined types" and "Defining a specification" sub-sections below for more information on defining types and typespecs. @@ -250,61 +250,128 @@ Behaviours in Elixir (and Erlang) are a way to separate and abstract the generic A behaviour module defines a set of functions and macros (referred to as *callbacks*) that callback modules implementing that behaviour must export. This "interface" identifies the specific part of the component. For example, the `GenServer` behaviour and functions abstract away all the message-passing (sending and receiving) and error reporting that a "server" process will likely want to implement from the specific parts such as the actions that this server process has to perform. -To define a behaviour module, it's enough to define one or more callbacks in that module. To define callbacks, the `@callback` and `@macrocallback` module attributes can be used (for function callbacks and macro callbacks respectively). +Say we want to implement a bunch of parsers, each parsing structured data: for example, a JSON parser and a MessagePack parser. Each of these two parsers will *behave* the same way: both will provide a `parse/1` function and an `extensions/0` function. The `parse/1` function will return an Elixir representation of the structured data, while the `extensions/0` function will return a list of file extensions that can be used for each type of data (e.g., `.json` for JSON files). - defmodule MyBehaviour do - @callback my_fun(arg :: any) :: any - @macrocallback my_macro(arg :: any) :: Macro.t - end +We can create a `Parser` behaviour: + +```elixir +defmodule Parser do + @doc """ + Parses a string. + """ + @callback parse(String.t) :: {:ok, term} | {:error, atom} + + @doc """ + Lists all supported file extensions. + """ + @callback extensions() :: [String.t] +end +``` As seen in the example above, defining a callback is a matter of defining a specification for that callback, made of: - * the callback name (`my_fun` or `my_macro` in the example) - * the arguments that the callback must accept (`arg :: any` in the example) + * the callback name (`parse` or `extensions` in the example) + * the arguments that the callback must accept (`String.t`) * the *expected* type of the callback return value -### Optional callbacks +Modules adopting the `Parser` behaviour will have to implement all the functions defined with the `@callback` attribute. As you can see, `@callback` expects a function name but also a function specification like the ones used with the `@spec` attribute we saw above. -Optional callbacks are callbacks that callback modules may implement if they want to, but are not required to. Usually, behaviour modules know if they should call those callbacks based on configuration, or they check if the callbacks are defined with `function_exported?/3` or `macro_exported?/3`. +### Implementing behaviours -Optional callbacks can be defined through the `@optional_callbacks` module attribute, which has to be a keyword list with function or macro name as key and arity as value. For example: +Implementing a behaviour is straightforward: - defmodule MyBehaviour do - @callback vital_fun() :: any - @callback non_vital_fun() :: any - @macrocallback non_vital_macro(arg :: any) :: Macro.t - @optional_callbacks non_vital_fun: 0, non_vital_macro: 1 - end +```elixir +defmodule JSONParser do + @behaviour Parser -One example of optional callback in Elixir's standard library is `c:GenServer.format_status/2`. + @impl Parser + def parse(str), do: {:ok, "some json " <> str} # ... parse JSON -### Implementing behaviours + @impl Parser + def extensions, do: [".json"] +end +``` -To specify that a module implements a given behaviour, the `@behaviour` attribute must be used: +```elixir +defmodule CSVParser do + @behaviour Parser - defmodule MyBehaviour do - @callback my_fun(arg :: any) :: any - end + @impl Parser + def parse(str), do: {:ok, "some csv " <> str} # ... parse CSV - defmodule MyCallbackModule do - @behaviour MyBehaviour - def my_fun(arg), do: arg - end + @impl Parser + def extensions, do: [".csv"] +end +``` -If a callback module that implements a given behaviour doesn't export all the functions and macros defined by that behaviour, the user will be notified through warnings during the compilation process (no errors will happen). +If a module adopting a given behaviour doesn't implement one of the callbacks required by that behaviour, a compile-time warning will be generated. -You can also use the `@impl` attribute before a function to denote that particular function is implementation a behaviour: +Furthermore, with `@impl` you can also make sure that you are implementing the **correct** callbacks from the given behaviour in an explicit manner. For example, the following parser implements both `parse` and `extensions`. However, thanks to a typo, `BADParser` is implementing `parse/0` instead of `parse/1`. - defmodule MyCallbackModule do - @behaviour MyBehaviour +```elixir +defmodule BADParser do + @behaviour Parser - @impl true - def my_fun(arg), do: arg - end + @impl Parser + def parse, do: {:ok, "something bad"} + + @impl Parser + def extensions, do: ["bad"] +end +``` + +This code generates a warning letting you know that you are mistakenly implementing `parse/0` instead of `parse/1`. +You can read more about `@impl` in the [module documentation](Module.html#module-impl). + +### Using behaviours + +Behaviours are useful because you can pass modules around as arguments and you can then *call back* to any of the functions specified in the behaviour. For example, we can have a function that receives a filename, several parsers, and parses the file based on its extension: + +```elixir +@spec parse_path(Path.t(), [module()]) :: {:ok, term} | {:error, atom} +def parse_path(filename, parsers) do + with {:ok, ext} <- parse_extension(filename), + {:ok, parser} <- find_parser(ext, parsers), + {:ok, contents} <- File.read(filename) do + parser.parse(contents) + end +end + +defp parse_extension(filename) do + if ext = Path.extname(filename) do + {:ok, ext} + else + {:error, :no_extension} + end +end + +defp find_parser(ext, parsers) do + if parser = Enum.find(parsers, fn parser -> ext in parser.extensions() end) do + {:ok, parser} + else + {:error, :no_matching_parser} + end +end +``` + +You could also invoke any parser directly: `CSVParser.parse(...)`. + +Note you don't need to define a behaviour in order to dynamically dispatch on a module, but those features often go hand in hand. -You can also use `@impl MyBehaviour` to make clearer from which behaviour the callbacks comes from, providing even more context for future readers of your code. +### Optional callbacks + +Optional callbacks are callbacks that callback modules may implement if they want to, but are not required to. Usually, behaviour modules know if they should call those callbacks based on configuration, or they check if the callbacks are defined with `function_exported?/3` or `macro_exported?/3`. -Elixir's standard library contains a few frequently used behaviours such as `GenServer`, `Supervisor`, and `Application`. +Optional callbacks can be defined through the `@optional_callbacks` module attribute, which has to be a keyword list with function or macro name as key and arity as value. For example: + + defmodule MyBehaviour do + @callback vital_fun() :: any + @callback non_vital_fun() :: any + @macrocallback non_vital_macro(arg :: any) :: Macro.t + @optional_callbacks non_vital_fun: 0, non_vital_macro: 1 + end + +One example of optional callback in Elixir's standard library is `c:GenServer.format_status/2`. ### Inspecting behaviours @@ -319,6 +386,10 @@ For example, for the `MyBehaviour` module defined in "Optional callbacks" above: When using `iex`, the `IEx.Helpers.b/1` helper is also available. +## Pitfalls + +There are some known pitfalls when using typespecs, they are documented next. + ## The `string()` type Elixir discourages the use of the `string()` type. The `string()` type refers to Erlang strings, which are known as "charlists" in Elixir. They do not refer to Elixir strings, which are UTF-8 encoded binaries. To avoid confusion, if you attempt to use the type `string()`, Elixir will emit a warning. You should use `charlist()`, `nonempty_charlist()`, `binary()` or `String.t()` accordingly, or any of the several literal representations for these types. From 20c5a18afb0e94eea167a18b3109a11623af906f Mon Sep 17 00:00:00 2001 From: Jean Klingler Date: Wed, 1 Nov 2023 19:42:19 +0900 Subject: [PATCH 0131/1886] Always use system certificates (#13052) --- lib/mix/lib/mix/utils.ex | 24 +++++++----------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/lib/mix/lib/mix/utils.ex b/lib/mix/lib/mix/utils.ex index 7ec4311f3a..e28a80245a 100644 --- a/lib/mix/lib/mix/utils.ex +++ b/lib/mix/lib/mix/utils.ex @@ -655,23 +655,13 @@ defmodule Mix.Utils do headers = [{~c"user-agent", ~c"Mix/#{System.version()}"}] request = {:binary.bin_to_list(path), headers} - # Use the system certificates if available, otherwise skip peer verification - # TODO: Always use system certificates when OTP >= 25.1 is required - ssl_options = - if Code.ensure_loaded?(:httpc) and function_exported?(:httpc, :ssl_verify_host_options, 1) do - try do - apply(:httpc, :ssl_verify_host_options, [true]) - rescue - _ -> - Mix.shell().error( - "warning: failed to load system certificates. SSL peer verification will be skipped but downloads are still verified with a checksum" - ) - - [verify: :verify_none] - end - else - [verify: :verify_none] - end + # Use the system certificates + # TODO: use `ssl_options = :httpc.ssl_verify_host_options(true)` on Erlang/OTP 26+ + ssl_options = [ + verify: :verify_peer, + cacerts: :public_key.cacerts_get(), + customize_hostname_check: [match_fun: :public_key.pkix_verify_hostname_match_fun(:https)] + ] # We are using relaxed: true because some servers is returning a Location # header with relative paths, which does not follow the spec. This would From 4c0380e2bd5ff1677a5a0e56cfe0e2e353a4e4d1 Mon Sep 17 00:00:00 2001 From: Panagiotis Nezis Date: Wed, 1 Nov 2023 13:27:21 +0200 Subject: [PATCH 0132/1886] Additional remarks for application config anti-pattern for Mix tasks (#13057) --- .../anti-patterns/design-anti-patterns.md | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/lib/elixir/pages/anti-patterns/design-anti-patterns.md b/lib/elixir/pages/anti-patterns/design-anti-patterns.md index 98e1ad366f..178615edd4 100644 --- a/lib/elixir/pages/anti-patterns/design-anti-patterns.md +++ b/lib/elixir/pages/anti-patterns/design-anti-patterns.md @@ -444,3 +444,35 @@ iex> DashSplitter.split("Lucas-Francisco-da-Matta-Vegi", [parts: 5]) iex> DashSplitter.split("Lucas-Francisco-da-Matta-Vegi") #<= default config is used! ["Lucas", "Francisco-da-Matta-Vegi"] ``` + +#### Additional Remarks + +For Mix tasks and related tools, it may be necessary to provide per-project configuration. For example, imagine you have a `:linter` project, which supports setting the output file and the verbosity level. You may choose to configure it through application environment: + +```elixir +config :linter, + output_file: "/path/to/output.json", + verbosity: 3 +``` + +However, `Mix` allows tasks to read per-project configuration via `Mix.Project.config/0`. In this case, you can configure the `:linter` directly in the `mix.exs` file: + +```elixir +def project do + [ + app: :my_app, + version: "1.0.0", + linter: [ + output_file: "/path/to/output.json", + verbosity: 3 + ], + ... + ] +end +``` + +Additonally, if a Mix task is available, you can also accept these options as command line arguments (see `OptionParser`): + +```bash +mix linter --output-file /path/to/output.json --verbosity 3 +``` From 8d7975bc0b2142e014f1ca61dc02baf513a8b7a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 1 Nov 2023 12:42:41 +0100 Subject: [PATCH 0133/1886] s/parens/parentheses --- lib/elixir/src/elixir_erl_pass.erl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/elixir/src/elixir_erl_pass.erl b/lib/elixir/src/elixir_erl_pass.erl index 341b1d4d8e..575004934e 100644 --- a/lib/elixir/src/elixir_erl_pass.erl +++ b/lib/elixir/src/elixir_erl_pass.erl @@ -646,8 +646,8 @@ no_parens_remote(true, _Fun) -> error; no_parens_remote(Atom, Fun) when is_atom(Atom) -> Message = fun() -> io_lib:format( - "using map.field notation (without parens) to invoke function ~ts.~ts() is deprecated, " - "you must add parens instead: remote.function()", + "using map.field notation (without parentheses) to invoke function ~ts.~ts() is deprecated, " + "you must add parentheses instead: remote.function()", [elixir_aliases:inspect(Atom), Fun] ) end, From 497ef3462fa43f79e01e61e45a8b6c6c26932d37 Mon Sep 17 00:00:00 2001 From: rktjmp Date: Thu, 2 Nov 2023 01:33:01 +1100 Subject: [PATCH 0134/1886] Restore GenServer introduction mermaid graph (#13058) Restores graph removed in f5a61d1, with correct request -> reply arrow ordering. --- lib/elixir/lib/gen_server.ex | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/elixir/lib/gen_server.ex b/lib/elixir/lib/gen_server.ex index 8102d5d478..8bb29f8be9 100644 --- a/lib/elixir/lib/gen_server.ex +++ b/lib/elixir/lib/gen_server.ex @@ -8,6 +8,13 @@ defmodule GenServer do will have a standard set of interface functions and include functionality for tracing and error reporting. It will also fit into a supervision tree. + ```mermaid + graph BT + C(Client #3) ~~~ B(Client #2) ~~~ A(Client #1) + A & B & C -->|request| GenServer + GenServer -.->|reply| A & B & C + ``` + ## Example The GenServer behaviour abstracts the common client-server interaction. From 151025f68a17fb2e2fa4b4577f411f60211c2afb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 1 Nov 2023 16:11:09 +0100 Subject: [PATCH 0135/1886] Fix typo on docs --- lib/elixir/lib/inspect/algebra.ex | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/lib/elixir/lib/inspect/algebra.ex b/lib/elixir/lib/inspect/algebra.ex index cfd790f77f..5e213b3779 100644 --- a/lib/elixir/lib/inspect/algebra.ex +++ b/lib/elixir/lib/inspect/algebra.ex @@ -143,9 +143,8 @@ defmodule Inspect.Opts do function as this must be controlled by applications. Libraries should instead define their own structs with custom inspect implementations. If a library must change the default inspect - function, then it is best to define to ask users of your library - to explicitly call `default_inspect_fun/1` with your function of - choice. + function, then it is best to ask users of your library to explicitly + call `default_inspect_fun/1` with your function of choice. The default is `Inspect.inspect/2`. From 9e681c7a96874f26a0f2554ede039539db443ae5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 1 Nov 2023 16:33:17 +0100 Subject: [PATCH 0136/1886] Use explicit/implicit vs manual/automatic --- lib/elixir/lib/supervisor.ex | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/elixir/lib/supervisor.ex b/lib/elixir/lib/supervisor.ex index f8962a0080..0307673567 100644 --- a/lib/elixir/lib/supervisor.ex +++ b/lib/elixir/lib/supervisor.ex @@ -413,10 +413,10 @@ defmodule Supervisor do The difference between the two approaches is that a module-based supervisor gives you more direct control over how the supervisor is initialized. Instead of calling `Supervisor.start_link/2` with - a list of child specifications that are automatically initialized, we manually - initialize the children by calling `Supervisor.init/2` inside its - `c:init/1` callback. `Supervisor.init/2` accepts the same `:strategy`, - `:max_restarts`, and `:max_seconds` options as `start_link/2`. + a list of child specifications that are implicitly initialized for us, + we must explicitly initialize the children by calling `Supervisor.init/2` + inside its `c:init/1` callback. `Supervisor.init/2` accepts the same + `:strategy`, `:max_restarts`, and `:max_seconds` options as `start_link/2`. > #### `use Supervisor` {: .info} > From 0089eae8d9677c94494d1f73b490a0b6ddddfefc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Samson?= Date: Thu, 2 Nov 2023 08:11:31 +0100 Subject: [PATCH 0137/1886] Lazily evaluate File.cwd! in Path.expand and Path.absname (#13061) do not crash with File.Error with already absolute paths if File.cwd returns error or nil --- lib/elixir/lib/path.ex | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/lib/elixir/lib/path.ex b/lib/elixir/lib/path.ex index 472ea988f4..0955e72c50 100644 --- a/lib/elixir/lib/path.ex +++ b/lib/elixir/lib/path.ex @@ -44,7 +44,7 @@ defmodule Path do """ @spec absname(t) :: binary def absname(path) do - absname(path, File.cwd!()) + absname(path, &File.cwd!/0) end @doc """ @@ -71,13 +71,27 @@ defmodule Path do case type(path) do :relative -> + relative_to = + if is_function(relative_to, 0) do + relative_to.() + else + relative_to + end + absname_join([relative_to, path]) :absolute -> absname_join([path]) :volumerelative -> - relative_to = IO.chardata_to_string(relative_to) + relative_to = + if is_function(relative_to, 0) do + relative_to.() + else + relative_to + end + |> IO.chardata_to_string() + absname_vr(split(path), split(relative_to), relative_to) end end @@ -163,7 +177,7 @@ defmodule Path do """ @spec expand(t) :: binary def expand(path) do - expand_dot(absname(expand_home(path), File.cwd!())) + expand_dot(absname(expand_home(path), &File.cwd!/0)) end @doc """ @@ -192,7 +206,7 @@ defmodule Path do """ @spec expand(t, t) :: binary def expand(path, relative_to) do - expand_dot(absname(absname(expand_home(path), expand_home(relative_to)), File.cwd!())) + expand_dot(absname(absname(expand_home(path), expand_home(relative_to)), &File.cwd!/0)) end @doc """ From 1d5f79c1a3d95bc4ce4be4b7b3f1ef918306dd3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 2 Nov 2023 08:29:12 +0100 Subject: [PATCH 0138/1886] Do not use Erlang/OTP 26.1 on CI (#13062) It has a bug when looking up mismatched module names. --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 00abece1d0..a85f03eb81 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -75,7 +75,7 @@ jobs: name: Windows Server 2019, Erlang/OTP ${{ matrix.otp_version }} strategy: matrix: - otp_version: ["25", "26"] + otp_version: ["25.3", "26.0"] runs-on: windows-2019 steps: - name: Configure Git From 23747559ee2f9f9deaeb41b9c76414ae570ae021 Mon Sep 17 00:00:00 2001 From: Panagiotis Nezis Date: Thu, 2 Nov 2023 09:57:35 +0200 Subject: [PATCH 0139/1886] Support --sparse in archive.install and escript.install (#13059) --- lib/mix/lib/mix/local/installer.ex | 24 +++++++++++--- lib/mix/lib/mix/tasks/archive.install.ex | 4 +++ lib/mix/lib/mix/tasks/escript.install.ex | 4 +++ lib/mix/test/mix/local/installer_test.exs | 40 +++++++++++++++++++---- 4 files changed, 61 insertions(+), 11 deletions(-) diff --git a/lib/mix/lib/mix/local/installer.ex b/lib/mix/lib/mix/local/installer.ex index ce23c02b94..ea39b6d78f 100644 --- a/lib/mix/lib/mix/local/installer.ex +++ b/lib/mix/lib/mix/local/installer.ex @@ -245,7 +245,7 @@ defmodule Mix.Local.Installer do end defp git_fetcher(url, git_config, opts) do - git_opts = git_config ++ [git: url, submodules: opts[:submodules]] + git_opts = git_config ++ [git: url, submodules: opts[:submodules], sparse: opts[:sparse]] app_name = if opts[:app] do @@ -347,9 +347,7 @@ defmodule Mix.Local.Installer do File.cwd!() end) - package_name = elem(dep_spec, 0) - package_name_string = Atom.to_string(package_name) - package_path = Path.join([tmp_path, "deps", package_name_string]) + {package_name, package_path} = package_name_path(dep_spec, tmp_path) post_config = [ deps_path: Path.join(tmp_path, "deps"), @@ -368,6 +366,24 @@ defmodule Mix.Local.Installer do :code.delete(Mix.Local.Installer.Fetcher) end + defp package_name_path(dep_spec, tmp_path) do + package_name = elem(dep_spec, 0) + package_name_string = Atom.to_string(package_name) + package_path = Path.join([tmp_path, "deps", package_name_string, maybe_sparse_dir(dep_spec)]) + + {package_name, package_path} + end + + defp maybe_sparse_dir({_app, opts}) when is_list(opts) do + if opts[:git] do + opts[:sparse] || "" + else + "" + end + end + + defp maybe_sparse_dir(_dep_spec), do: "" + defp in_fetcher(_mix_exs) do Mix.Task.run("deps.get", ["--only", Atom.to_string(Mix.env())]) end diff --git a/lib/mix/lib/mix/tasks/archive.install.ex b/lib/mix/lib/mix/tasks/archive.install.ex index 19c47dfb17..39abd5dd51 100644 --- a/lib/mix/lib/mix/tasks/archive.install.ex +++ b/lib/mix/lib/mix/tasks/archive.install.ex @@ -47,6 +47,9 @@ defmodule Mix.Tasks.Archive.Install do * `--submodules` - fetches repository submodules before building archive from Git or GitHub + * `--sparse` - checkout a single directory inside the Git repository and use + it as the archive root directory + * `--app` - specifies a custom app name to be used for building the archive from Git, GitHub, or Hex @@ -63,6 +66,7 @@ defmodule Mix.Tasks.Archive.Install do force: :boolean, sha512: :string, submodules: :boolean, + sparse: :string, app: :string, organization: :string, repo: :string, diff --git a/lib/mix/lib/mix/tasks/escript.install.ex b/lib/mix/lib/mix/tasks/escript.install.ex index d06995a951..ba1b9a46ed 100644 --- a/lib/mix/lib/mix/tasks/escript.install.ex +++ b/lib/mix/lib/mix/tasks/escript.install.ex @@ -47,6 +47,9 @@ defmodule Mix.Tasks.Escript.Install do * `--submodules` - fetches repository submodules before building escript from Git or GitHub + * `--sparse` - checkout a single directory inside the Git repository and use + it as the escript project directory + * `--app` - specifies a custom app name to be used for building the escript from Git, GitHub, or Hex @@ -66,6 +69,7 @@ defmodule Mix.Tasks.Escript.Install do force: :boolean, sha512: :string, submodules: :boolean, + sparse: :string, app: :string, organization: :string, repo: :string, diff --git a/lib/mix/test/mix/local/installer_test.exs b/lib/mix/test/mix/local/installer_test.exs index 5c5fdcae0c..bf10c4d137 100644 --- a/lib/mix/test/mix/local/installer_test.exs +++ b/lib/mix/test/mix/local/installer_test.exs @@ -30,50 +30,76 @@ defmodule Mix.Local.InstallerTest do test "parse_args Git" do args = ["git", "https://example.com/user/repo.git"] - opts = [git: "https://example.com/user/repo.git", submodules: nil] + opts = [git: "https://example.com/user/repo.git", submodules: nil, sparse: nil] assert Mix.Local.Installer.parse_args(args, []) == {:fetcher, {:"new package", opts}} end test "parse_args Git branch" do args = ["git", "https://example.com/user/repo.git", "branch", "not_main"] - opts = [branch: "not_main", git: "https://example.com/user/repo.git", submodules: nil] + + opts = [ + branch: "not_main", + git: "https://example.com/user/repo.git", + submodules: nil, + sparse: nil + ] assert Mix.Local.Installer.parse_args(args, []) == {:fetcher, {:"new package", opts}} end test "parse_args Git ref" do args = ["git", "https://example.com/user/repo.git", "ref", "not_main"] - opts = [ref: "not_main", git: "https://example.com/user/repo.git", submodules: nil] + + opts = [ + ref: "not_main", + git: "https://example.com/user/repo.git", + submodules: nil, + sparse: nil + ] assert Mix.Local.Installer.parse_args(args, []) == {:fetcher, {:"new package", opts}} end test "parse_args Git tag" do args = ["git", "https://example.com/user/repo.git", "tag", "not_main"] - opts = [tag: "not_main", git: "https://example.com/user/repo.git", submodules: nil] + + opts = [ + tag: "not_main", + git: "https://example.com/user/repo.git", + submodules: nil, + sparse: nil + ] assert Mix.Local.Installer.parse_args(args, []) == {:fetcher, {:"new package", opts}} end test "parse_args Git submodules" do args = ["git", "https://example.com/user/repo.git"] - opts = [git: "https://example.com/user/repo.git", submodules: true] + opts = [git: "https://example.com/user/repo.git", submodules: true, sparse: nil] assert Mix.Local.Installer.parse_args(args, submodules: true) == {:fetcher, {:"new package", opts}} end + test "parse_args Git sparse" do + args = ["git", "https://example.com/user/repo.git"] + opts = [git: "https://example.com/user/repo.git", submodules: nil, sparse: "foo"] + + assert Mix.Local.Installer.parse_args(args, sparse: "foo") == + {:fetcher, {:"new package", opts}} + end + test "parse_args Git app" do args = ["git", "https://example.com/user/repo.git"] - opts = [git: "https://example.com/user/repo.git", submodules: nil] + opts = [git: "https://example.com/user/repo.git", submodules: nil, sparse: nil] assert Mix.Local.Installer.parse_args(args, app: "my_app") == {:fetcher, {:my_app, opts}} end test "parse_args GitHub" do args = ["github", "user/repo"] - opts = [git: "https://github.com/user/repo.git", submodules: nil] + opts = [git: "https://github.com/user/repo.git", submodules: nil, sparse: nil] assert Mix.Local.Installer.parse_args(args, []) == {:fetcher, {:"new package", opts}} end From dcced959438985852752cbe47f01602a506df72d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 2 Nov 2023 10:19:11 +0100 Subject: [PATCH 0140/1886] Warn if both :applications and :extra_applications are used --- lib/mix/lib/mix/tasks/compile.app.ex | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/mix/lib/mix/tasks/compile.app.ex b/lib/mix/lib/mix/tasks/compile.app.ex index 4aaaecf6cc..f54b3a6f33 100644 --- a/lib/mix/lib/mix/tasks/compile.app.ex +++ b/lib/mix/lib/mix/tasks/compile.app.ex @@ -344,6 +344,13 @@ defmodule Mix.Tasks.Compile.App do defp handle_extra_applications(properties, config) do {extra, properties} = Keyword.pop(properties, :extra_applications, []) + if extra != [] and Keyword.has_key?(properties, :applications) do + Mix.shell().error( + "both :extra_applications and :applications was found in your mix.exs. " <> + "You most likely want to remove the :applications key, as all applications are derived from your dependencies" + ) + end + {all, optional} = project_apps(properties, config, extra, fn -> apps_from_runtime_prod_deps(properties, config) From a346c4f42841b5bd91c22ba8e9707e12978d4fd7 Mon Sep 17 00:00:00 2001 From: Marco Milanesi Date: Thu, 2 Nov 2023 12:45:18 +0100 Subject: [PATCH 0141/1886] Add metadata examples to logger documentation (#13064) --- lib/logger/lib/logger.ex | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/logger/lib/logger.ex b/lib/logger/lib/logger.ex index e6e8968337..fca6e5766b 100644 --- a/lib/logger/lib/logger.ex +++ b/lib/logger/lib/logger.ex @@ -942,6 +942,7 @@ defmodule Logger do for level <- @levels do report = [something: :reported, this: level] + metadata = [user_id: 42, request_id: "xU32kFa"] extra = if translation = translations[level] do @@ -973,6 +974,13 @@ defmodule Logger do # as map Logger.#{level}(#{inspect(Map.new(report))}) + Report message with metadata (maps or keywords): + + # as a keyword list + Logger.#{level}("this is a #{level} message", #{inspect(metadata)}) + + # as map + Logger.#{level}("this is a #{level} message", #{inspect(Map.new(metadata))}) """ # Only macros generated for the "new" Erlang levels are available since 1.11.0. Other From f22a0b5b7afc79800e8dc417475524ff85de91fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20=C5=81=C4=99picki?= Date: Thu, 2 Nov 2023 13:08:28 +0100 Subject: [PATCH 0142/1886] Fix Path.absname/2 spec (#13065) --- lib/elixir/lib/path.ex | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/elixir/lib/path.ex b/lib/elixir/lib/path.ex index 0955e72c50..0f18487a10 100644 --- a/lib/elixir/lib/path.ex +++ b/lib/elixir/lib/path.ex @@ -51,10 +51,10 @@ defmodule Path do Builds a path from `relative_to` to `path`. If `path` is already an absolute path, `relative_to` is ignored. See also - `relative_to/3`. + `relative_to/3`. `relative_to` is either a path or an anonymous function, + which is invoked only when necessary, that returns a path. - Unlike `expand/2`, no attempt is made to - resolve `..`, `.` or `~`. + Unlike `expand/2`, no attempt is made to resolve `..`, `.` or `~`. ## Examples @@ -65,7 +65,7 @@ defmodule Path do "bar/../x" """ - @spec absname(t, t) :: binary + @spec absname(t, t | (-> t)) :: binary def absname(path, relative_to) do path = IO.chardata_to_string(path) From 0bb70366553f31e7cf0fcf80e46ae041611b1056 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonatan=20K=C5=82osko?= Date: Thu, 2 Nov 2023 17:57:49 +0100 Subject: [PATCH 0143/1886] Add offset option to File.stream! (#13063) --- lib/elixir/lib/file.ex | 3 ++ lib/elixir/lib/file/stream.ex | 46 +++++++++++++++++++-- lib/elixir/test/elixir/file/stream_test.exs | 30 ++++++++++++++ 3 files changed, 75 insertions(+), 4 deletions(-) diff --git a/lib/elixir/lib/file.ex b/lib/elixir/lib/file.ex index f9f928146f..b65c51478c 100644 --- a/lib/elixir/lib/file.ex +++ b/lib/elixir/lib/file.ex @@ -133,6 +133,7 @@ defmodule File do @type stream_mode :: encoding_mode() + | read_offset_mode() | :append | :compressed | :delayed_write @@ -140,6 +141,8 @@ defmodule File do | {:read_ahead, pos_integer | false} | {:delayed_write, non_neg_integer, non_neg_integer} + @type read_offset_mode :: {:read_offset, non_neg_integer()} + @type erlang_time :: {{year :: non_neg_integer(), month :: 1..12, day :: 1..31}, {hour :: 0..23, minute :: 0..59, second :: 0..59}} diff --git a/lib/elixir/lib/file/stream.ex b/lib/elixir/lib/file/stream.ex index cd23f2b0b5..4633ca1f1c 100644 --- a/lib/elixir/lib/file/stream.ex +++ b/lib/elixir/lib/file/stream.ex @@ -18,6 +18,12 @@ defmodule File.Stream do @doc false def __build__(path, line_or_bytes, modes) do + with {:read_offset, offset} <- :lists.keyfind(:read_offset, 1, modes), + false <- is_integer(offset) and offset >= 0 do + raise ArgumentError, + "expected :read_offset to be a non-negative integer, got: #{inspect(offset)}" + end + raw = :lists.keyfind(:encoding, 1, modes) == false modes = @@ -88,7 +94,7 @@ defmodule File.Stream do start_fun = fn -> case File.Stream.__open__(stream, read_modes(modes)) do {:ok, device} -> - if :trim_bom in modes, do: trim_bom(device, raw) |> elem(0), else: device + skip_bom_and_offset(device, raw, modes) {:error, reason} -> raise File.Error, reason: reason, action: "stream", path: stream.path @@ -104,9 +110,14 @@ defmodule File.Stream do Stream.resource(start_fun, next_fun, &:file.close/1).(acc, fun) end - def count(%{modes: modes, line_or_bytes: :line, path: path} = stream) do + def count(%{modes: modes, line_or_bytes: :line, path: path, raw: raw} = stream) do pattern = :binary.compile_pattern("\n") - counter = &count_lines(&1, path, pattern, read_function(stream), 0) + + counter = fn device -> + device = skip_bom_and_offset(device, raw, modes) + count_lines(device, path, pattern, read_function(stream), 0) + end + {:ok, open!(stream, modes, counter)} end @@ -116,8 +127,11 @@ defmodule File.Stream do {:error, __MODULE__} {:ok, %{size: size}} -> + bom_offset = count_raw_bom(stream, modes) + offset = get_read_offset(modes) + size = max(size - bom_offset - offset, 0) remainder = if rem(size, bytes) == 0, do: 0, else: 1 - {:ok, div(size, bytes) + remainder - count_raw_bom(stream, modes)} + {:ok, div(size, bytes) + remainder} {:error, reason} -> raise File.Error, reason: reason, action: "stream", path: path @@ -158,6 +172,23 @@ defmodule File.Stream do end end + defp skip_bom_and_offset(device, raw, modes) do + device = + if :trim_bom in modes do + device |> trim_bom(raw) |> elem(0) + else + device + end + + offset = get_read_offset(modes) + + if offset > 0 do + {:ok, _} = :file.position(device, {:cur, offset}) + end + + device + end + defp trim_bom(device, true) do bom_length = device |> IO.binread(4) |> bom_length() {:ok, new_pos} = :file.position(device, bom_length) @@ -183,6 +214,13 @@ defmodule File.Stream do defp bom_length(<<254, 255, 0, 0, _rest::binary>>), do: 4 defp bom_length(_binary), do: 0 + def get_read_offset(modes) do + case :lists.keyfind(:read_offset, 1, modes) do + {:read_offset, offset} -> offset + false -> 0 + end + end + defp read_modes(modes) do for mode <- modes, mode not in [:write, :append, :trim_bom], do: mode end diff --git a/lib/elixir/test/elixir/file/stream_test.exs b/lib/elixir/test/elixir/file/stream_test.exs index 2aa7703066..e61f7b5d43 100644 --- a/lib/elixir/test/elixir/file/stream_test.exs +++ b/lib/elixir/test/elixir/file/stream_test.exs @@ -141,6 +141,35 @@ defmodule File.StreamTest do end end + test "supports byte offset" do + src = fixture_path("file.txt") + + assert @node + |> stream!(src, [{:read_offset, 0}]) + |> Enum.take(1) == ["FOO\n"] + + assert @node + |> stream!(src, [{:read_offset, 1}]) + |> Enum.take(1) == ["OO\n"] + + assert @node + |> stream!(src, [{:read_offset, 4}]) + |> Enum.take(1) == [] + + assert @node |> stream!(src, 1, [{:read_offset, 1}]) |> Enum.count() == 3 + assert @node |> stream!(src, 1, [{:read_offset, 4}]) |> Enum.count() == 0 + end + + test "applies offset after trimming BOM" do + src = fixture_path("utf8_bom.txt") + + assert @node + |> stream!(src, [:trim_bom, {:read_offset, 4}]) + |> Enum.take(1) == ["сский\n"] + + assert @node |> stream!(src, 1, [:trim_bom, {:read_offset, 4}]) |> Enum.count() == 15 + end + test "keeps BOM when raw" do src = fixture_path("utf8_bom.txt") @@ -169,6 +198,7 @@ defmodule File.StreamTest do assert @node |> stream!(src, [:trim_bom]) |> Enum.count() == 2 assert @node |> stream!(src, 1, [:trim_bom]) |> Enum.count() == 19 + assert @node |> stream!(src, 2, [:trim_bom]) |> Enum.count() == 10 end test "keeps BOM with utf8 encoding" do From eedfbec34fda6b985dd1b761a94af6ef60e1394d Mon Sep 17 00:00:00 2001 From: Cameron Duley Date: Thu, 2 Nov 2023 14:29:11 -0400 Subject: [PATCH 0144/1886] Add `String.replace_invalid/2` (#13067) --- lib/elixir/lib/string.ex | 105 +++++++++++++++++++++++++ lib/elixir/test/elixir/string_test.exs | 27 +++++++ 2 files changed, 132 insertions(+) diff --git a/lib/elixir/lib/string.ex b/lib/elixir/lib/string.ex index 29f0166916..562e7b8ca2 100644 --- a/lib/elixir/lib/string.ex +++ b/lib/elixir/lib/string.ex @@ -1871,6 +1871,111 @@ defmodule String do end end + @doc ~S""" + Returns a new string created by replacing all invalid bytes with `replacement` (`"�"` by default). + + ## Examples + + iex> String.replace_invalid("asd" <> <<0xFF::8>>) + "asd�" + + iex> String.replace_invalid("nem rán bề bề") + "nem rán bề bề" + + iex> String.replace_invalid("nem rán b" <> <<225, 187>> <> " bề") + "nem rán b� bề" + + iex> String.replace_invalid("nem rán b" <> <<225, 187>> <> " bề", "ERROR!") + "nem rán bERROR! bề" + """ + def replace_invalid(string, replacement \\ "�") + when is_binary(string) and is_binary(replacement) do + do_replace_invalid(string, replacement, <<>>) + end + + # Valid ASCII (for better average speed) + defp do_replace_invalid(<>, rep, acc) + when ascii in 0..127 and n_lead != 0b10 do + do_replace_invalid(<>, rep, <>) + end + + # Valid UTF-8 + defp do_replace_invalid(<>, rep, acc) do + do_replace_invalid(rest, rep, <>) + end + + # 2/3 truncated sequence + defp do_replace_invalid(<<0b1110::4, i::4, 0b10::2, ii::6>>, rep, acc) do + <> = <> + <> + end + + defp do_replace_invalid(<<0b1110::4, i::4, 0b10::2, ii::6, n_lead::2, rest::bits>>, rep, acc) + when n_lead != 0b10 do + <> = <> + + do_replace_invalid( + <>, + rep, + <> + ) + end + + # 2/4 + defp do_replace_invalid(<<0b11110::5, i::3, 0b10::2, ii::6>>, rep, acc) do + <> = <> + <> + end + + defp do_replace_invalid(<<0b11110::5, i::3, 0b10::2, ii::6, n_lead::2, rest::bits>>, rep, acc) + when n_lead != 0b10 do + <> = <> + + do_replace_invalid( + <>, + rep, + <> + ) + end + + # 3/4 + defp do_replace_invalid(<<0b11110::5, i::3, 0b10::2, ii::6, 0b10::2, iii::6>>, rep, acc) do + <> = <> + <> + end + + defp do_replace_invalid( + <<0b11110::5, i::3, 0b10::2, ii::6, 0b10::2, iii::6, n_lead::2, rest::bits>>, + rep, + acc + ) + when n_lead != 0b10 do + <> = <> + + do_replace_invalid( + <>, + rep, + <> + ) + end + + # any other invalid bytes + defp do_replace_invalid(<<_, rest::bits>>, rep, acc), + do: do_replace_invalid(rest, rep, <>) + + defp do_replace_invalid(<<>>, _, acc), do: acc + + # bounds-checking truncated code points for overlong encodings + defp replace_invalid_ii_of_iii(tcp, rep) when tcp >= 32 and tcp <= 863, do: rep + defp replace_invalid_ii_of_iii(tcp, rep) when tcp >= 896 and tcp <= 1023, do: rep + defp replace_invalid_ii_of_iii(_, rep), do: rep <> rep + + defp replace_invalid_ii_of_iiii(tcp, rep) when tcp >= 16 and tcp <= 271, do: rep + defp replace_invalid_ii_of_iiii(_, rep), do: rep <> rep + + defp replace_invalid_iii_of_iiii(tcp, rep) when tcp >= 1024 and tcp <= 17407, do: rep + defp replace_invalid_iii_of_iiii(_, rep), do: rep <> rep <> rep + @doc ~S""" Splits the string into chunks of characters that share a common trait. diff --git a/lib/elixir/test/elixir/string_test.exs b/lib/elixir/test/elixir/string_test.exs index 830ed1fc01..f808c8d602 100644 --- a/lib/elixir/test/elixir/string_test.exs +++ b/lib/elixir/test/elixir/string_test.exs @@ -826,6 +826,33 @@ defmodule StringTest do refute String.valid?("asdasdasd" <> <<0xFFFF::16>>, :fast_ascii) end + test "replace_invalid" do + assert String.replace_invalid("") === "" + assert String.replace_invalid(<<0xFF>>) === "�" + assert String.replace_invalid(<<0xFF, 0xFF, 0xFF>>) === "���" + + # Valid ASCII + assert String.replace_invalid("hello") === "hello" + + # Valid UTF-8 + assert String.replace_invalid("こんにちは") === "こんにちは" + + # 2/3 byte truncated "ề" + assert String.replace_invalid(<<225, 187>>) === "�" + assert String.replace_invalid("nem rán b" <> <<225, 187>> <> " bề") === "nem rán b� bề" + + # 2/4 byte truncated "😔" + assert String.replace_invalid(<<240, 159>>) === "�" + assert String.replace_invalid("It's so over " <> <<240, 159>>) === "It's so over �" + + # 3/4 byte truncated "😃" + assert String.replace_invalid(<<240, 159, 152>>) === "�" + assert String.replace_invalid("We're so back " <> <<240, 159, 152>>) === "We're so back �" + + # 3 byte overlong "e" + assert String.replace_invalid(<<0b11100000, 0b10000001, 0b10100101>>) === "���" + end + test "chunk/2 with :valid trait" do assert String.chunk("", :valid) == [] From bf50de0b566c57b748ac63cd29020da1e0d185ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 2 Nov 2023 19:31:58 +0100 Subject: [PATCH 0145/1886] Docs to new options and functions --- lib/elixir/lib/file.ex | 6 ++++-- lib/elixir/lib/string.ex | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/elixir/lib/file.ex b/lib/elixir/lib/file.ex index b65c51478c..d302d97141 100644 --- a/lib/elixir/lib/file.ex +++ b/lib/elixir/lib/file.ex @@ -1751,13 +1751,15 @@ defmodule File do One may also consider passing the `:delayed_write` option if the stream is meant to be written to under a tight loop. - ## Byte order marks + ## Byte order marks an dread offset If you pass `:trim_bom` in the modes parameter, the stream will trim UTF-8, UTF-16 and UTF-32 byte order marks when reading from file. Note that this function does not try to discover the file encoding - based on BOM. + based on BOM. From Elixir v1.16.0, you may also pass a `:read_offset` + that is skipped whenever enumerating the stream (if both `:read_offset` + and `:trim_bom` are given, the offset is skipped after the BOM). ## Examples diff --git a/lib/elixir/lib/string.ex b/lib/elixir/lib/string.ex index 562e7b8ca2..5bfadfd562 100644 --- a/lib/elixir/lib/string.ex +++ b/lib/elixir/lib/string.ex @@ -1888,6 +1888,7 @@ defmodule String do iex> String.replace_invalid("nem rán b" <> <<225, 187>> <> " bề", "ERROR!") "nem rán bERROR! bề" """ + @doc since: "1.16.0" def replace_invalid(string, replacement \\ "�") when is_binary(string) and is_binary(replacement) do do_replace_invalid(string, replacement, <<>>) From 4c8a8ca3811af76852c0804046fc03f8fe8f4268 Mon Sep 17 00:00:00 2001 From: Rich Morin Date: Thu, 2 Nov 2023 13:04:21 -0700 Subject: [PATCH 0146/1886] Fix typo (#13068) --- lib/elixir/pages/anti-patterns/code-anti-patterns.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/pages/anti-patterns/code-anti-patterns.md b/lib/elixir/pages/anti-patterns/code-anti-patterns.md index 22479a3a80..2d0147f5a6 100644 --- a/lib/elixir/pages/anti-patterns/code-anti-patterns.md +++ b/lib/elixir/pages/anti-patterns/code-anti-patterns.md @@ -220,7 +220,7 @@ defmodule MyRequestHandler do end ``` -All valid statuses all defined as atoms within the same module, and that's enough. If you want to be explicit, you could also have a function that lists them: +All valid statuses are defined as atoms within the same module, and that's enough. If you want to be explicit, you could also have a function that lists them: ```elixir def valid_statuses do From 7ea97ad1ed5eacbb3dc29068987c41046e41911c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20=C5=81=C4=99picki?= Date: Fri, 3 Nov 2023 08:08:07 +0100 Subject: [PATCH 0147/1886] Fix typo: an dread -> and read (#13069) --- lib/elixir/lib/file.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/lib/file.ex b/lib/elixir/lib/file.ex index d302d97141..ecf4b8550d 100644 --- a/lib/elixir/lib/file.ex +++ b/lib/elixir/lib/file.ex @@ -1751,7 +1751,7 @@ defmodule File do One may also consider passing the `:delayed_write` option if the stream is meant to be written to under a tight loop. - ## Byte order marks an dread offset + ## Byte order marks and read offset If you pass `:trim_bom` in the modes parameter, the stream will trim UTF-8, UTF-16 and UTF-32 byte order marks when reading from file. From 576a347b177daca847d2149580599cf367f7b390 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 3 Nov 2023 08:06:59 +0100 Subject: [PATCH 0148/1886] Improve error messages on KeyError and ArgumentError --- lib/elixir/lib/exception.ex | 94 +++++++++++------------ lib/elixir/test/elixir/exception_test.exs | 17 +--- 2 files changed, 48 insertions(+), 63 deletions(-) diff --git a/lib/elixir/lib/exception.ex b/lib/elixir/lib/exception.ex index 68bb06bc6a..ddb682ffe6 100644 --- a/lib/elixir/lib/exception.ex +++ b/lib/elixir/lib/exception.ex @@ -839,44 +839,6 @@ defmodule ArgumentError do """ defexception message: "argument error" - - @impl true - def blame( - exception, - [{:erlang, :apply, [module, function, args], _} | _] = stacktrace - ) do - message = - cond do - not proper_list?(args) -> - "you attempted to apply a function named #{inspect(function)} on module #{inspect(module)} " <> - "with arguments #{inspect(args)}. Arguments (the third argument of apply) must always be a proper list" - - # Note that args may be an empty list even if they were supplied - not is_atom(module) and is_atom(function) and args == [] -> - "you attempted to apply a function named #{inspect(function)} on #{inspect(module)}. " <> - "If you are using Kernel.apply/3, make sure the module is an atom. " <> - "If you are using the dot syntax, such as module.function(), " <> - "make sure the left-hand side of the dot is a module atom" - - not is_atom(module) -> - "you attempted to apply a function on #{inspect(module)}. " <> - "Modules (the first argument of apply) must always be an atom" - - not is_atom(function) -> - "you attempted to apply a function named #{inspect(function)} on module #{inspect(module)}. " <> - "However, #{inspect(function)} is not a valid function name. Function names (the second argument " <> - "of apply) must always be an atom" - end - - {%{exception | message: message}, stacktrace} - end - - def blame(exception, stacktrace) do - {exception, stacktrace} - end - - defp proper_list?(list) when length(list) >= 0, do: true - defp proper_list?(_), do: false end defmodule ArithmeticError do @@ -1998,10 +1960,17 @@ defmodule KeyError do defp message(key, term) do message = "key #{inspect(key)} not found" - if term != nil do - message <> " in: #{inspect(term, pretty: true, limit: :infinity)}" - else - message + cond do + term == nil -> + message + + is_atom(term) and is_atom(key) -> + message <> + " in: #{inspect(term)} (if instead you want to invoke #{inspect(term)}.#{key}(), " <> + "make sure to add parentheses after the function name)" + + true -> + message <> " in: #{inspect(term, pretty: true, limit: :infinity)}" end end @@ -2010,11 +1979,6 @@ defmodule KeyError do {exception, stacktrace} end - def blame(exception = %{term: nil}, stacktrace) do - message = message(exception.key, exception.term) - {%{exception | message: message}, stacktrace} - end - def blame(exception, stacktrace) do %{term: term, key: key} = exception message = message(key, term) @@ -2231,9 +2195,39 @@ defmodule ErlangError do @doc false def normalize(:badarg, stacktrace) do - case error_info(:badarg, stacktrace, "errors were found at the given arguments") do - {:ok, reason, details} -> %ArgumentError{message: reason <> details} - :error -> %ArgumentError{} + case stacktrace do + [{:erlang, :apply, [module, function, args], _} | _] when not is_atom(module) -> + message = + cond do + is_map(module) and is_atom(function) and is_map_key(module, function) -> + "you attempted to apply a function named #{inspect(function)} on a map/struct. " <> + "If you are using Kernel.apply/3, make sure the module is an atom. " <> + if is_function(module[function]) do + "If you are trying to invoke an anonymous function in a map/struct, " <> + "add a dot between the function name and the parenthesis: map.#{function}.()" + else + "If you are using the dot syntax, ensure there are no parentheses " <> + "after the field name, such as map.#{function}" + end + + is_atom(function) and args == [] -> + "you attempted to apply a function named #{inspect(function)} on #{inspect(module)}. " <> + "If you are using Kernel.apply/3, make sure the module is an atom. " <> + "If you are using the dot syntax, such as module.function(), " <> + "make sure the left-hand side of the dot is an atom representing a module" + + true -> + "you attempted to apply a function on #{inspect(module)}. " <> + "Modules (the first argument of apply) must always be an atom" + end + + %ArgumentError{message: message} + + _ -> + case error_info(:badarg, stacktrace, "errors were found at the given arguments") do + {:ok, reason, details} -> %ArgumentError{message: reason <> details} + :error -> %ArgumentError{} + end end end diff --git a/lib/elixir/test/elixir/exception_test.exs b/lib/elixir/test/elixir/exception_test.exs index 7160c67b74..5ba0dc5d69 100644 --- a/lib/elixir/test/elixir/exception_test.exs +++ b/lib/elixir/test/elixir/exception_test.exs @@ -534,24 +534,15 @@ defmodule ExceptionTest do assert blame_message([], & &1.foo()) == "you attempted to apply a function named :foo on []. If you are using Kernel.apply/3, make sure " <> "the module is an atom. If you are using the dot syntax, such as " <> - "module.function(), make sure the left-hand side of the dot is a module atom" + "module.function(), make sure the left-hand side of the dot is an atom representing a module" assert blame_message([], &apply(&1, :foo, [])) == "you attempted to apply a function named :foo on []. If you are using Kernel.apply/3, make sure " <> "the module is an atom. If you are using the dot syntax, such as " <> - "module.function(), make sure the left-hand side of the dot is a module atom" + "module.function(), make sure the left-hand side of the dot is an atom representing a module" - assert blame_message([], &apply(Kernel, &1, [1, 2])) == - "you attempted to apply a function named [] on module Kernel. However, [] is not a valid function name. " <> - "Function names (the second argument of apply) must always be an atom" - - assert blame_message(123, &apply(Kernel, :+, &1)) == - "you attempted to apply a function named :+ on module Kernel with arguments 123. " <> - "Arguments (the third argument of apply) must always be a proper list" - - assert blame_message(123, &apply(Kernel, :+, [&1 | 456])) == - "you attempted to apply a function named :+ on module Kernel with arguments [123 | 456]. " <> - "Arguments (the third argument of apply) must always be a proper list" + assert blame_message([], &apply(&1, :foo, [1, 2])) == + "you attempted to apply a function on []. Modules (the first argument of apply) must always be an atom" end test "annotates function clause errors" do From 0731f429c474fa5d964d3feb2f210368062ec3c4 Mon Sep 17 00:00:00 2001 From: Jean Klingler Date: Fri, 3 Nov 2023 16:28:53 +0900 Subject: [PATCH 0149/1886] Add doctest for absname with lazy path (#13070) --- lib/elixir/lib/path.ex | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/elixir/lib/path.ex b/lib/elixir/lib/path.ex index 0f18487a10..680ff77c87 100644 --- a/lib/elixir/lib/path.ex +++ b/lib/elixir/lib/path.ex @@ -64,6 +64,9 @@ defmodule Path do iex> Path.absname("../x", "bar") "bar/../x" + iex> Path.absname("foo", fn -> "lazy" end) + "lazy/foo" + """ @spec absname(t, t | (-> t)) :: binary def absname(path, relative_to) do From a34cd281c58f6d94dbbae55ad98e78b57293b938 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Samson?= Date: Fri, 3 Nov 2023 11:53:30 +0100 Subject: [PATCH 0150/1886] Elixir 1.14.5 supports Erlang/OTP 26 (#13071) --- lib/elixir/pages/references/compatibility-and-deprecations.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/pages/references/compatibility-and-deprecations.md b/lib/elixir/pages/references/compatibility-and-deprecations.md index 7658658caa..9f6e4ee51d 100644 --- a/lib/elixir/pages/references/compatibility-and-deprecations.md +++ b/lib/elixir/pages/references/compatibility-and-deprecations.md @@ -45,7 +45,7 @@ Elixir version | Supported Erlang/OTP versions :------------- | :------------------------------- 1.16 | 24 - 26 1.15 | 24 - 26 -1.14 | 23 - 25 +1.14 | 23 - 25 (and Erlang/OTP 26 from v1.14.5) 1.13 | 22 - 24 (and Erlang/OTP 25 from v1.13.4) 1.12 | 22 - 24 1.11 | 21 - 23 (and Erlang/OTP 24 from v1.11.4) From 8e9cbfcd8c219f9d3558158f1ebee5ec4fadd762 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 3 Nov 2023 21:19:16 +0100 Subject: [PATCH 0151/1886] Implement Algorithm L for Reservoir Sampling in Enum This optimizes Enum.random/1 and Enum.take_random/2 to be 6.3x times faster and use 2.7x less memory. --- lib/elixir/lib/enum.ex | 159 ++++++++++++++------------- lib/elixir/test/elixir/enum_test.exs | 39 +++---- 2 files changed, 102 insertions(+), 96 deletions(-) diff --git a/lib/elixir/lib/enum.ex b/lib/elixir/lib/enum.ex index 9ded32a922..a2efb8dffa 100644 --- a/lib/elixir/lib/enum.ex +++ b/lib/elixir/lib/enum.ex @@ -2362,12 +2362,6 @@ defmodule Enum do the random value. Check its documentation for setting a different random algorithm or a different seed. - The implementation is based on the - [reservoir sampling](https://en.wikipedia.org/wiki/Reservoir_sampling#Relation_to_Fisher-Yates_shuffle) - algorithm. - It assumes that the sample being returned can fit into memory; - the input `enumerable` doesn't have to, as it is traversed just once. - If a range is passed into the function, this function will pick a random value between the range limits, without traversing the whole range (thus executing in constant time and constant memory). @@ -2386,6 +2380,12 @@ defmodule Enum do iex> Enum.random(1..1_000) 309 + ## Implementation + + The random functions in this module implement reservoir sampling, + which allows them to sample infinite collections. In particular, + we implement Algorithm L, as described in by Kim-Hung Li in + "Reservoir-Sampling Algorithms of Time Complexity O(n(1+log(N/n)))". """ @spec random(t) :: element def random(enumerable) @@ -2902,11 +2902,11 @@ defmodule Enum do the default from Erlang/OTP 22: # Although not necessary, let's seed the random algorithm - iex> :rand.seed(:exsss, {1, 2, 3}) - iex> Enum.shuffle([1, 2, 3]) - [3, 2, 1] + iex> :rand.seed(:exsss, {11, 22, 33}) iex> Enum.shuffle([1, 2, 3]) [2, 1, 3] + iex> Enum.shuffle([1, 2, 3]) + [2, 3, 1] """ @spec shuffle(t) :: list @@ -2916,9 +2916,12 @@ defmodule Enum do [{:rand.uniform(), x} | acc] end) - shuffle_unwrap(:lists.keysort(1, randomized), []) + shuffle_unwrap(:lists.keysort(1, randomized)) end + defp shuffle_unwrap([{_, h} | rest]), do: [h | shuffle_unwrap(rest)] + defp shuffle_unwrap([]), do: [] + @doc """ Returns a subset list of the given `enumerable` by `index_range`. @@ -3588,100 +3591,114 @@ defmodule Enum do # Although not necessary, let's seed the random algorithm iex> :rand.seed(:exsss, {1, 2, 3}) iex> Enum.take_random(1..10, 2) - [3, 1] + [6, 1] iex> Enum.take_random(?a..?z, 5) - ~c"mikel" + ~c"bkzmt" """ @spec take_random(t, non_neg_integer) :: list def take_random(enumerable, count) def take_random(_enumerable, 0), do: [] - def take_random([], _), do: [] - def take_random([h | t], 1), do: take_random_list_one(t, h, 1) def take_random(enumerable, 1) do enumerable - |> reduce([], fn - x, [current | index] -> - if :rand.uniform(index + 1) == 1 do - [x | index + 1] - else - [current | index + 1] - end + |> reduce({0, 0, 1.0, nil}, fn + elem, {idx, idx, w, _current} -> + {jdx, w} = take_jdx_w(idx, w, 1) + {idx + 1, jdx, w, elem} - x, [] -> - [x | 1] + _elem, {idx, jdx, w, current} -> + {idx + 1, jdx, w, current} end) |> case do - [] -> [] - [current | _index] -> [current] + {0, 0, 1.0, nil} -> [] + {_idx, _jdx, _w, current} -> [current] end end - def take_random(enumerable, count) when is_integer(count) and count in 0..128 do + def take_random(enumerable, count) when count in 0..128 do sample = Tuple.duplicate(nil, count) - reducer = fn elem, {idx, sample} -> - jdx = random_index(idx) + reducer = fn + elem, {idx, jdx, w, sample} when idx < count -> + rand = take_index(idx) + sample = sample |> put_elem(idx, elem(sample, rand)) |> put_elem(rand, elem) - cond do - idx < count -> - value = elem(sample, jdx) - {idx + 1, put_elem(sample, idx, value) |> put_elem(jdx, elem)} + if idx == jdx do + {jdx, w} = take_jdx_w(idx, w, count) + {idx + 1, jdx, w, sample} + else + {idx + 1, jdx, w, sample} + end - jdx < count -> - {idx + 1, put_elem(sample, jdx, elem)} + elem, {idx, idx, w, sample} -> + pos = :rand.uniform(count) - 1 + {jdx, w} = take_jdx_w(idx, w, count) + {idx + 1, jdx, w, put_elem(sample, pos, elem)} - true -> - {idx + 1, sample} - end + _elem, {idx, jdx, w, sample} -> + {idx + 1, jdx, w, sample} end - {size, sample} = reduce(enumerable, {0, sample}, reducer) - sample |> Tuple.to_list() |> take(Kernel.min(count, size)) + {size, _, _, sample} = reduce(enumerable, {0, count - 1, 1.0, sample}, reducer) + + if count < size do + Tuple.to_list(sample) + else + take_tupled(sample, size, []) + end end def take_random(enumerable, count) when is_integer(count) and count >= 0 do - reducer = fn elem, {idx, sample} -> - jdx = random_index(idx) - - cond do - idx < count -> - value = Map.get(sample, jdx) - {idx + 1, Map.put(sample, idx, value) |> Map.put(jdx, elem)} + reducer = fn + elem, {idx, jdx, w, sample} when idx < count -> + rand = take_index(idx) + sample = sample |> Map.put(idx, Map.get(sample, rand)) |> Map.put(rand, elem) + + if idx == jdx do + {jdx, w} = take_jdx_w(idx, w, count) + {idx + 1, jdx, w, sample} + else + {idx + 1, jdx, w, sample} + end - jdx < count -> - {idx + 1, Map.put(sample, jdx, elem)} + elem, {idx, idx, w, sample} -> + pos = :rand.uniform(count) - 1 + {jdx, w} = take_jdx_w(idx, w, count) + {idx + 1, jdx, w, %{sample | pos => elem}} - true -> - {idx + 1, sample} - end + _elem, {idx, jdx, w, sample} -> + {idx + 1, jdx, w, sample} end - {size, sample} = reduce(enumerable, {0, %{}}, reducer) - take_random(sample, Kernel.min(count, size), []) + {size, _, _, sample} = reduce(enumerable, {0, count - 1, 1.0, %{}}, reducer) + take_mapped(sample, Kernel.min(count, size), []) end - defp take_random(_sample, 0, acc), do: acc - - defp take_random(sample, position, acc) do - position = position - 1 - take_random(sample, position, [Map.get(sample, position) | acc]) + @compile {:inline, take_jdx_w: 3, take_index: 1} + defp take_jdx_w(idx, w, count) do + w = w * :math.exp(:math.log(:rand.uniform()) / count) + jdx = idx + floor(:math.log(:rand.uniform()) / :math.log(1 - w)) + 1 + {jdx, w} end - defp take_random_list_one([h | t], current, index) do - if :rand.uniform(index + 1) == 1 do - take_random_list_one(t, h, index + 1) - else - take_random_list_one(t, current, index + 1) - end + defp take_index(0), do: 0 + defp take_index(idx), do: :rand.uniform(idx + 1) - 1 + + defp take_tupled(_sample, 0, acc), do: acc + + defp take_tupled(sample, position, acc) do + position = position - 1 + take_tupled(sample, position, [elem(sample, position) | acc]) end - defp take_random_list_one([], current, _), do: [current] + defp take_mapped(_sample, 0, acc), do: acc - defp random_index(0), do: 0 - defp random_index(idx), do: :rand.uniform(idx + 1) - 1 + defp take_mapped(sample, position, acc) do + position = position - 1 + take_mapped(sample, position, [Map.fetch!(sample, position) | acc]) + end @doc """ Takes the elements from the beginning of the `enumerable` while `fun` returns @@ -4439,14 +4456,6 @@ defmodule Enum do [acc | scan_list(rest, acc, fun)] end - ## shuffle - - defp shuffle_unwrap([{_, h} | enumerable], t) do - shuffle_unwrap(enumerable, [h | t]) - end - - defp shuffle_unwrap([], t), do: t - ## slice defp slice_forward(enumerable, start, amount, step) when start < 0 do diff --git a/lib/elixir/test/elixir/enum_test.exs b/lib/elixir/test/elixir/enum_test.exs index ebb4fd3188..eee7d772cd 100644 --- a/lib/elixir/test/elixir/enum_test.exs +++ b/lib/elixir/test/elixir/enum_test.exs @@ -1008,7 +1008,7 @@ defmodule EnumTest do test "shuffle/1" do # set a fixed seed so the test can be deterministic :rand.seed(:exsss, {1374, 347_975, 449_264}) - assert Enum.shuffle([1, 2, 3, 4, 5]) == [1, 3, 4, 5, 2] + assert Enum.shuffle([1, 2, 3, 4, 5]) == [2, 5, 4, 3, 1] end test "slice/2" do @@ -1377,16 +1377,16 @@ defmodule EnumTest do seed1 = {1406, 407_414, 139_258} seed2 = {1406, 421_106, 567_597} :rand.seed(:exsss, seed1) - assert Enum.take_random([1, 2, 3], 1) == [3] - assert Enum.take_random([1, 2, 3], 2) == [3, 2] + assert Enum.take_random([1, 2, 3], 1) == [2] + assert Enum.take_random([1, 2, 3], 2) == [2, 3] assert Enum.take_random([1, 2, 3], 3) == [3, 1, 2] - assert Enum.take_random([1, 2, 3], 4) == [1, 3, 2] + assert Enum.take_random([1, 2, 3], 4) == [2, 3, 1] :rand.seed(:exsss, seed2) assert Enum.take_random([1, 2, 3], 1) == [1] assert Enum.take_random([1, 2, 3], 2) == [3, 1] - assert Enum.take_random([1, 2, 3], 3) == [3, 1, 2] - assert Enum.take_random([1, 2, 3], 4) == [2, 1, 3] - assert Enum.take_random([1, 2, 3], 129) == [2, 3, 1] + assert Enum.take_random([1, 2, 3], 3) == [2, 3, 1] + assert Enum.take_random([1, 2, 3], 4) == [3, 2, 1] + assert Enum.take_random([1, 2, 3], 129) == [2, 1, 3] # assert that every item in the sample comes from the input list list = for _ <- 1..100, do: make_ref() @@ -2071,8 +2071,8 @@ defmodule EnumTest.Range do test "shuffle/1" do # set a fixed seed so the test can be deterministic :rand.seed(:exsss, {1374, 347_975, 449_264}) - assert Enum.shuffle(1..5) == [1, 3, 4, 5, 2] - assert Enum.shuffle(1..10//2) == [3, 9, 7, 1, 5] + assert Enum.shuffle(1..5) == [2, 5, 4, 3, 1] + assert Enum.shuffle(1..10//2) == [5, 1, 7, 9, 3] end test "slice/2" do @@ -2316,7 +2316,7 @@ defmodule EnumTest.Range do seed1 = {1406, 407_414, 139_258} seed2 = {1406, 421_106, 567_597} :rand.seed(:exsss, seed1) - assert Enum.take_random(1..3, 1) == [3] + assert Enum.take_random(1..3, 1) == [2] :rand.seed(:exsss, seed1) assert Enum.take_random(1..3, 2) == [3, 1] :rand.seed(:exsss, seed1) @@ -2324,22 +2324,19 @@ defmodule EnumTest.Range do :rand.seed(:exsss, seed1) assert Enum.take_random(1..3, 4) == [3, 1, 2] :rand.seed(:exsss, seed1) - assert Enum.take_random(3..1//-1, 1) == [1] + assert Enum.take_random(1..3, 5) == [3, 1, 2] + :rand.seed(:exsss, seed1) + assert Enum.take_random(3..1//-1, 1) == [2] :rand.seed(:exsss, seed2) assert Enum.take_random(1..3, 1) == [1] :rand.seed(:exsss, seed2) - assert Enum.take_random(1..3, 2) == [1, 3] + assert Enum.take_random(1..3, 2) == [3, 2] :rand.seed(:exsss, seed2) assert Enum.take_random(1..3, 3) == [1, 3, 2] :rand.seed(:exsss, seed2) assert Enum.take_random(1..3, 4) == [1, 3, 2] - - # make sure optimizations don't change fixed seeded tests - :rand.seed(:exsss, {101, 102, 103}) - one = Enum.take_random(1..100, 1) - :rand.seed(:exsss, {101, 102, 103}) - two = Enum.take_random(1..100, 2) - assert hd(one) == hd(two) + :rand.seed(:exsss, seed2) + assert Enum.take_random(1..3, 5) == [1, 3, 2] end test "take_while/2" do @@ -2425,7 +2422,7 @@ defmodule EnumTest.Map do seed1 = {1406, 407_414, 139_258} seed2 = {1406, 421_106, 567_597} :rand.seed(:exsss, seed1) - assert Enum.take_random(map, 1) == [x3] + assert Enum.take_random(map, 1) == [x2] :rand.seed(:exsss, seed1) assert Enum.take_random(map, 2) == [x3, x1] :rand.seed(:exsss, seed1) @@ -2435,7 +2432,7 @@ defmodule EnumTest.Map do :rand.seed(:exsss, seed2) assert Enum.take_random(map, 1) == [x1] :rand.seed(:exsss, seed2) - assert Enum.take_random(map, 2) == [x1, x3] + assert Enum.take_random(map, 2) == [x3, x2] :rand.seed(:exsss, seed2) assert Enum.take_random(map, 3) == [x1, x3, x2] :rand.seed(:exsss, seed2) From 113dba376c4b9aa6b21f3feecccf28f736fb416f Mon Sep 17 00:00:00 2001 From: Ioannis Kyriazis Date: Fri, 3 Nov 2023 18:23:45 -0400 Subject: [PATCH 0152/1886] is -> us (#13072) --- lib/elixir/pages/getting-started/io-and-the-file-system.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/pages/getting-started/io-and-the-file-system.md b/lib/elixir/pages/getting-started/io-and-the-file-system.md index a3168b2536..57a4760633 100644 --- a/lib/elixir/pages/getting-started/io-and-the-file-system.md +++ b/lib/elixir/pages/getting-started/io-and-the-file-system.md @@ -129,7 +129,7 @@ iex> IO.write(pid, "hello") After `IO.write/2`, we can see the request sent by the `IO` module printed out (a four-elements tuple). Soon after that, we see that it fails since the `IO` module expected some kind of result, which we did not supply. -By modeling IO devices with processes, the Erlang VM allows is to even read and write to files across nodes. Neat! +By modeling IO devices with processes, the Erlang VM allows us to even read and write to files across nodes. Neat! ## `iodata` and `chardata` From 076d1e2056d8cc0771287d154a61fc1bc2acd2f6 Mon Sep 17 00:00:00 2001 From: Tony Dang Date: Sat, 4 Nov 2023 00:59:00 -0700 Subject: [PATCH 0153/1886] Fix typo in "Getting Started - Enumerables and Streams" docs (#13073) --- lib/elixir/pages/getting-started/enumerable-and-streams.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/pages/getting-started/enumerable-and-streams.md b/lib/elixir/pages/getting-started/enumerable-and-streams.md index 7dc7d98e98..bc4f81516f 100644 --- a/lib/elixir/pages/getting-started/enumerable-and-streams.md +++ b/lib/elixir/pages/getting-started/enumerable-and-streams.md @@ -119,6 +119,6 @@ iex> Enum.take(stream, 10) The example above will fetch the first 10 lines of the file you have selected. This means streams can be very useful for handling large files or even slow resources like network resources. -The `Enum` and `Stream` modules provide a wide-range functions but know all of them at heart. Familiarize yourself with `Enum.map/2`, `Enum.reduce/3` and other functions with either `map` or `reduce` in their names, and you will naturally build an intuition around the most important use cases. You may also focus on the `Enum` module first and only move to `Stream` for the particular scenarios where laziness is required, to either deal with slow resources or large, possibly infinite, collections. +The `Enum` and `Stream` modules provide a wide range of functions, but you don't have to know all of them by heart. Familiarize yourself with `Enum.map/2`, `Enum.reduce/3` and other functions with either `map` or `reduce` in their names, and you will naturally build an intuition around the most important use cases. You may also focus on the `Enum` module first and only move to `Stream` for the particular scenarios where laziness is required, to either deal with slow resources or large, possibly infinite, collections. Next, we'll look at a feature central to Elixir, Processes, which allows us to write concurrent, parallel and distributed programs in an easy and understandable way. From 8d0cbaa2127888c82321997c1f71a23db9029a55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Samson?= Date: Sun, 5 Nov 2023 20:15:15 +0100 Subject: [PATCH 0154/1886] Fix crashes when :beam_lib.info(beam) returns error (#13075) --- lib/elixir/lib/code.ex | 3 ++- lib/elixir/lib/code/typespec.ex | 3 ++- lib/iex/lib/iex/helpers.ex | 3 ++- lib/iex/lib/iex/info.ex | 2 +- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/elixir/lib/code.ex b/lib/elixir/lib/code.ex index 09099667a5..a700b71b67 100644 --- a/lib/elixir/lib/code.ex +++ b/lib/elixir/lib/code.ex @@ -2057,7 +2057,8 @@ defmodule Code do defp get_beam_and_path(module) do with {^module, beam, filename} <- :code.get_object_code(module), - {:ok, ^module} <- beam |> :beam_lib.info() |> Keyword.fetch(:module) do + info_pairs when is_list(info_pairs) <- :beam_lib.info(beam), + {:ok, ^module} <- Keyword.fetch(info_pairs, :module) do {beam, filename} else _ -> :error diff --git a/lib/elixir/lib/code/typespec.ex b/lib/elixir/lib/code/typespec.ex index 7cda6db78b..cd38949bc7 100644 --- a/lib/elixir/lib/code/typespec.ex +++ b/lib/elixir/lib/code/typespec.ex @@ -175,7 +175,8 @@ defmodule Code.Typespec do defp get_module_and_beam(module) when is_atom(module) do with {^module, beam, _filename} <- :code.get_object_code(module), - {:ok, ^module} <- beam |> :beam_lib.info() |> Keyword.fetch(:module) do + info_pairs when is_list(info_pairs) <- :beam_lib.info(beam), + {:ok, ^module} <- Keyword.fetch(info_pairs, :module) do {module, beam} else _ -> :error diff --git a/lib/iex/lib/iex/helpers.ex b/lib/iex/lib/iex/helpers.ex index f693113eb1..fa49873562 100644 --- a/lib/iex/lib/iex/helpers.ex +++ b/lib/iex/lib/iex/helpers.ex @@ -1476,7 +1476,8 @@ defmodule IEx.Helpers do defp get_beam_and_path(module) do with {^module, beam, filename} <- :code.get_object_code(module), - {:ok, ^module} <- beam |> :beam_lib.info() |> Keyword.fetch(:module) do + info_pairs when is_list(info_pairs) <- :beam_lib.info(beam), + {:ok, ^module} <- Keyword.fetch(info_pairs, :module) do {beam, filename} else _ -> :error diff --git a/lib/iex/lib/iex/info.ex b/lib/iex/lib/iex/info.ex index 2878f53807..b1b9c0fae8 100644 --- a/lib/iex/lib/iex/info.ex +++ b/lib/iex/lib/iex/info.ex @@ -89,7 +89,7 @@ defimpl IEx.Info, for: Atom do {^atom, beam, _path} -> info = :beam_lib.info(beam) - Keyword.fetch(info, :module) == {:ok, atom} + is_list(info) and Keyword.fetch(info, :module) == {:ok, atom} end end From 4b9ec7bdb1e6f61b2b7f076442e98e070abc56e9 Mon Sep 17 00:00:00 2001 From: rktjmp Date: Mon, 6 Nov 2023 06:27:48 +1100 Subject: [PATCH 0155/1886] Clarify receive docs for unmatched messages (#13076) Update the docs to reinforce that we only operate on "matching messages" and explicitly call out that unmatched messages remain in the mailbox. --- lib/elixir/lib/kernel/special_forms.ex | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/elixir/lib/kernel/special_forms.ex b/lib/elixir/lib/kernel/special_forms.ex index 9b06165734..b9f1e1d746 100644 --- a/lib/elixir/lib/kernel/special_forms.ex +++ b/lib/elixir/lib/kernel/special_forms.ex @@ -2300,11 +2300,13 @@ defmodule Kernel.SpecialForms do defmacro try(args), do: error!([args]) @doc """ - Checks if there is a message matching the given clauses - in the current process mailbox. + Checks if there is a message matching any of the given clauses in the current + process mailbox. - In case there is no such message, the current process hangs - until a message arrives or waits until a given timeout value. + If there is no matching message, the current process waits until a matching + message arrives or until after a given timeout value. + + Any new and existing messages that do not match will remain in the mailbox. ## Examples @@ -2317,8 +2319,8 @@ defmodule Kernel.SpecialForms do IO.puts(:stderr, "Unexpected message received") end - An optional `after` clause can be given in case the message was not - received after the given timeout period, specified in milliseconds: + An optional `after` clause can be given in case no matching message is + received during the given timeout period, specified in milliseconds: receive do {:selector, number, name} when is_integer(number) -> From 298bf7e7e5dc206119aeaf2c75c4722beb613b9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Erik=20Andr=C3=A9=20Jakobsen?= Date: Mon, 6 Nov 2023 13:27:14 +0100 Subject: [PATCH 0156/1886] [docs] Clean up sigils intro (#13077) --- lib/elixir/pages/getting-started/sigils.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/pages/getting-started/sigils.md b/lib/elixir/pages/getting-started/sigils.md index 7897f277a4..9da5509a42 100644 --- a/lib/elixir/pages/getting-started/sigils.md +++ b/lib/elixir/pages/getting-started/sigils.md @@ -2,7 +2,7 @@ Elixir provides double-quoted strings as well as a concept called charlists, which are defined using the `~c"hello world"` sigil syntax. In this chapter, we will learn more about sigils and how to define our own. -One of Elixir's goals is extensibility: developers should be able to extend the language to fit any particular domain. Sigils provide the foundation for extending the language with custom textual representations. Sigils start with the tilde (`~`) character which is followed by a one lower-case letter or several upper-case ones and then a delimiter. Optionally, modifiers can be added after the final delimiter. +One of Elixir's goals is extensibility: developers should be able to extend the language to fit any particular domain. Sigils provide the foundation for extending the language with custom textual representations. Sigils start with the tilde (`~`) character which is followed by either a single lower-case letter or one or more upper-case letters, and then a delimiter. Optional modifiers are added after the final delimiter. ## Regular expressions From 8ae45c1c6bb89200f881eb396e19bf44c7f067fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 6 Nov 2023 13:28:49 +0100 Subject: [PATCH 0157/1886] Fix case clause error on tokenizer --- lib/elixir/src/elixir_tokenizer.erl | 2 +- lib/elixir/test/elixir/kernel/parser_test.exs | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/elixir/src/elixir_tokenizer.erl b/lib/elixir/src/elixir_tokenizer.erl index 72531b15be..e9cb79f226 100644 --- a/lib/elixir/src/elixir_tokenizer.erl +++ b/lib/elixir/src/elixir_tokenizer.erl @@ -796,7 +796,7 @@ handle_strings(T, Line, Column, H, Scope, Tokens) -> Token = {kw_identifier, {Line, Column - 1, nil}, Atom}, tokenize(Rest, NewLine, NewColumn + 1, NewScope, [Token | Tokens]); {error, Reason} -> - {error, Reason, Rest, Tokens} + error(Reason, Rest, NewScope, Tokens) end; {ok, Unescaped} -> diff --git a/lib/elixir/test/elixir/kernel/parser_test.exs b/lib/elixir/test/elixir/kernel/parser_test.exs index cc9e053157..149498ec2e 100644 --- a/lib/elixir/test/elixir/kernel/parser_test.exs +++ b/lib/elixir/test/elixir/kernel/parser_test.exs @@ -1136,7 +1136,12 @@ defmodule Kernel.ParserTest do assert_syntax_error( ["atom length must be less than system limit: "], - ~s[:"#{atom}"] + ~s{:"#{atom}"} + ) + + assert_syntax_error( + ["atom length must be less than system limit: "], + ~s{["#{atom}": 123]} ) end end From ce854d9ad7d3bb225fc03763d657cbb6f49c8dc0 Mon Sep 17 00:00:00 2001 From: Lucas Francisco da Matta Vegi Date: Mon, 6 Nov 2023 11:37:48 -0300 Subject: [PATCH 0158/1886] Additional remarks for maintaining research history (#13078) Similar to what we had already done with other anti-patterns that changed names --- lib/elixir/pages/anti-patterns/design-anti-patterns.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/elixir/pages/anti-patterns/design-anti-patterns.md b/lib/elixir/pages/anti-patterns/design-anti-patterns.md index 178615edd4..2db2cce45f 100644 --- a/lib/elixir/pages/anti-patterns/design-anti-patterns.md +++ b/lib/elixir/pages/anti-patterns/design-anti-patterns.md @@ -186,6 +186,10 @@ end A common practice followed by the community is to make the non-raising version return `{:ok, result}` or `{:error, Exception.t}`. For example, an HTTP client may return `{:ok, %HTTP.Response{}}` on success cases and `{:error, %HTTP.Error{}}` for failures, where `HTTP.Error` is [implemented as an exception](`Kernel.defexception/1`). This makes it convenient for anyone to raise an exception by simply calling `Kernel.raise/1`. +#### Additional remarks + +This anti-pattern was formerly known as [Using exceptions for control-flow](https://github.com/lucasvegi/Elixir-Code-Smells#using-exceptions-for-control-flow). + ## Primitive obsession #### Problem @@ -294,6 +298,10 @@ The following arguments were given to MyLibrary.foo/1: my_library.ex:2: MyLibrary.foo/1 ``` +#### Additional remarks + +This anti-pattern was formerly known as [Working with invalid data](https://github.com/lucasvegi/Elixir-Code-Smells#working-with-invalid-data). + ## Unrelated multi-clause function #### Problem From a8ba1d74ccef99d74813eb75103f0ceab16ce733 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 6 Nov 2023 15:51:37 +0100 Subject: [PATCH 0159/1886] Restore code paths in archive.install/escript.install Closes #13079. --- lib/mix/lib/mix/local/installer.ex | 79 +++++++++++++----------------- 1 file changed, 34 insertions(+), 45 deletions(-) diff --git a/lib/mix/lib/mix/local/installer.ex b/lib/mix/lib/mix/local/installer.ex index ea39b6d78f..12fbd41e21 100644 --- a/lib/mix/lib/mix/local/installer.ex +++ b/lib/mix/lib/mix/local/installer.ex @@ -319,8 +319,15 @@ defmodule Mix.Local.Installer do """ @spec fetch(tuple, (atom -> any), (atom -> any)) :: any def fetch(dep_spec, in_fetcher \\ &in_fetcher/1, in_package) do - with_tmp_dir(fn tmp_path -> + tmp_path = tmp_path() + previous_env = Mix.env() + deps_path = System.get_env("MIX_DEPS_PATH") + code_path = :code.get_path() + + try do File.mkdir_p!(tmp_path) + System.delete_env("MIX_DEPS_PATH") + Mix.env(:prod) File.write!(Path.join(tmp_path, "mix.exs"), """ defmodule Mix.Local.Installer.MixProject do @@ -336,34 +343,36 @@ defmodule Mix.Local.Installer do end """) - with_reset_prod_env(fn -> - Mix.ProjectStack.on_clean_slate(fn -> - tmp_path = - Mix.Project.in_project(:mix_local_installer, tmp_path, [], fn mix_exs -> - in_fetcher.(mix_exs) + Mix.ProjectStack.on_clean_slate(fn -> + tmp_path = + Mix.Project.in_project(:mix_local_installer, tmp_path, [], fn mix_exs -> + in_fetcher.(mix_exs) - # The tmp_dir may have symlinks in it, so we properly resolve - # the directory before customizing deps_path and lockfile. - File.cwd!() - end) + # The tmp_dir may have symlinks in it, so we properly resolve + # the directory before customizing deps_path and lockfile. + File.cwd!() + end) - {package_name, package_path} = package_name_path(dep_spec, tmp_path) + {package_name, package_path} = package_name_path(dep_spec, tmp_path) - post_config = [ - deps_path: Path.join(tmp_path, "deps"), - lockfile: Path.join(tmp_path, "mix.lock") - ] + post_config = [ + deps_path: Path.join(tmp_path, "deps"), + lockfile: Path.join(tmp_path, "mix.lock") + ] - Mix.Project.in_project(package_name, package_path, post_config, fn mix_exs -> - in_fetcher.(mix_exs) - in_package.(mix_exs) - end) + Mix.Project.in_project(package_name, package_path, post_config, fn mix_exs -> + in_fetcher.(mix_exs) + in_package.(mix_exs) end) end) - end) - after - :code.purge(Mix.Local.Installer.Fetcher) - :code.delete(Mix.Local.Installer.Fetcher) + after + File.rm_rf(tmp_path) + Mix.env(previous_env) + deps_path && System.put_env("MIX_DEPS_PATH", deps_path) + :code.set_path(code_path) + :code.purge(Mix.Local.Installer.Fetcher) + :code.delete(Mix.Local.Installer.Fetcher) + end end defp package_name_path(dep_spec, tmp_path) do @@ -388,28 +397,8 @@ defmodule Mix.Local.Installer do Mix.Task.run("deps.get", ["--only", Atom.to_string(Mix.env())]) end - defp with_tmp_dir(fun) do + defp tmp_path do unique = :crypto.strong_rand_bytes(4) |> Base.url_encode64(padding: false) - tmp_path = Path.join(System.tmp_dir!(), "mix-local-installer-fetcher-" <> unique) - - try do - fun.(tmp_path) - after - File.rm_rf(tmp_path) - end - end - - defp with_reset_prod_env(fun) do - previous_env = Mix.env() - deps_path = System.get_env("MIX_DEPS_PATH") - - try do - System.delete_env("MIX_DEPS_PATH") - Mix.env(:prod) - fun.() - after - Mix.env(previous_env) - deps_path && System.put_env("MIX_DEPS_PATH", deps_path) - end + Path.join(System.tmp_dir!(), "mix-local-installer-fetcher-" <> unique) end end From c4fa754dde2d70dca188c470b3836a438ae5d94a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 6 Nov 2023 16:30:46 +0100 Subject: [PATCH 0160/1886] Add :emit_warnings to Code.string_to_quoted --- lib/elixir/lib/code.ex | 8 +++----- lib/elixir/lib/code/fragment.ex | 2 +- lib/elixir/src/elixir.erl | 6 +++--- lib/elixir/src/elixir.hrl | 1 - lib/elixir/src/elixir_tokenizer.erl | 5 +---- lib/mix/lib/mix/dep/lock.ex | 2 +- 6 files changed, 9 insertions(+), 15 deletions(-) diff --git a/lib/elixir/lib/code.ex b/lib/elixir/lib/code.ex index a700b71b67..2d6a0d02f1 100644 --- a/lib/elixir/lib/code.ex +++ b/lib/elixir/lib/code.ex @@ -962,10 +962,9 @@ defmodule Code do to_quoted_opts = [ unescape: false, - warn_on_unnecessary_quotes: false, literal_encoder: &{:ok, {:__block__, &2, [&1]}}, token_metadata: true, - warnings: false + emit_warnings: false ] ++ opts {forms, comments} = string_to_quoted_with_comments!(string, to_quoted_opts) @@ -1126,9 +1125,8 @@ defmodule Code do atoms but `:existing_atoms_only` is still used for dynamic atoms, such as atoms with interpolations. - * `:warn_on_unnecessary_quotes` - when `false`, does not warn - when atoms, keywords or calls have unnecessary quotes on - them. Defaults to `true`. + * `:emit_warnings` (since v1.16.0) - when `false`, does not emit + tokenizing/parsing related warnings. Defaults to `true`. ## `Macro.to_string/2` diff --git a/lib/elixir/lib/code/fragment.ex b/lib/elixir/lib/code/fragment.ex index e104a1a4be..1e27c9c14d 100644 --- a/lib/elixir/lib/code/fragment.ex +++ b/lib/elixir/lib/code/fragment.ex @@ -1107,6 +1107,6 @@ defmodule Code.Fragment do opts = Keyword.take(opts, [:file, :line, :column, :columns, :token_metadata, :literal_encoder]) - Code.string_to_quoted(fragment, [cursor_completion: true, warnings: false] ++ opts) + Code.string_to_quoted(fragment, [cursor_completion: true, emit_warnings: false] ++ opts) end end diff --git a/lib/elixir/src/elixir.erl b/lib/elixir/src/elixir.erl index 66b1cc7076..3884a859da 100644 --- a/lib/elixir/src/elixir.erl +++ b/lib/elixir/src/elixir.erl @@ -477,7 +477,7 @@ string_to_tokens(String, StartLine, StartColumn, File, Opts) when is_integer(Sta {ok, _Line, _Column, [], Tokens} -> {ok, Tokens}; {ok, _Line, _Column, Warnings, Tokens} -> - (lists:keyfind(warnings, 1, Opts) /= {warnings, false}) andalso + (lists:keyfind(emit_warnings, 1, Opts) /= {emit_warnings, false}) andalso [elixir_errors:erl_warn(L, File, M) || {L, M} <- lists:reverse(Warnings)], {ok, Tokens}; {error, {Location, {ErrorPrefix, ErrorSuffix}, Token}, _Rest, _Warnings, _SoFar} -> @@ -535,8 +535,8 @@ to_binary(Atom) when is_atom(Atom) -> atom_to_binary(Atom). handle_parsing_opts(File, Opts) -> WarningFile = - case lists:keyfind(warnings, 1, Opts) of - {warnings, false} -> nil; + case lists:keyfind(emit_warnings, 1, Opts) of + {emit_warnings, false} -> nil; _ -> File end, LiteralEncoder = diff --git a/lib/elixir/src/elixir.hrl b/lib/elixir/src/elixir.hrl index 3199ea2ebe..e5d12c9338 100644 --- a/lib/elixir/src/elixir.hrl +++ b/lib/elixir/src/elixir.hrl @@ -36,6 +36,5 @@ ascii_identifiers_only=true, indentation=0, mismatch_hints=[], - warn_on_unnecessary_quotes=true, warnings=[] }). diff --git a/lib/elixir/src/elixir_tokenizer.erl b/lib/elixir/src/elixir_tokenizer.erl index e9cb79f226..c186b9a6bb 100644 --- a/lib/elixir/src/elixir_tokenizer.erl +++ b/lib/elixir/src/elixir_tokenizer.erl @@ -123,8 +123,6 @@ tokenize(String, Line, Column, Opts) -> Acc#elixir_tokenizer{preserve_comments=PreserveComments}; ({unescape, Unescape}, Acc) when is_boolean(Unescape) -> Acc#elixir_tokenizer{unescape=Unescape}; - ({warn_on_unnecessary_quotes, Unnecessary}, Acc) when is_boolean(Unnecessary) -> - Acc#elixir_tokenizer{warn_on_unnecessary_quotes=Unnecessary}; (_, Acc) -> Acc end, #elixir_tokenizer{identifier_tokenizer=IdentifierTokenizer}, Opts), @@ -966,12 +964,11 @@ eol(_Line, _Column, [{eol, {Line, Column, Count}} | Tokens]) -> eol(Line, Column, Tokens) -> [{eol, {Line, Column, 1}} | Tokens]. -is_unnecessary_quote([Part], #elixir_tokenizer{warn_on_unnecessary_quotes=true} = Scope) when is_list(Part) -> +is_unnecessary_quote([Part], Scope) when is_list(Part) -> case (Scope#elixir_tokenizer.identifier_tokenizer):tokenize(Part) of {identifier, _, [], _, true, Special} -> not lists:member(at, Special); _ -> false end; - is_unnecessary_quote(_Parts, _Scope) -> false. diff --git a/lib/mix/lib/mix/dep/lock.ex b/lib/mix/lib/mix/dep/lock.ex index c104e26f34..248831caf0 100644 --- a/lib/mix/lib/mix/dep/lock.ex +++ b/lib/mix/lib/mix/dep/lock.ex @@ -11,7 +11,7 @@ defmodule Mix.Dep.Lock do """ @spec read(Path.t()) :: map() def read(lockfile \\ lockfile()) do - opts = [file: lockfile, warn_on_unnecessary_quotes: false] + opts = [file: lockfile, emit_warnings: false] with {:ok, contents} <- File.read(lockfile), assert_no_merge_conflicts_in_lockfile(lockfile, contents), From 3904af04c55668249e3833a3918523e91a04cf4a Mon Sep 17 00:00:00 2001 From: Jacob Swanner Date: Tue, 7 Nov 2023 00:59:24 -0800 Subject: [PATCH 0161/1886] Fix Enum cheatsheet for drop/2 and take/2 with negative index (#13080) --- lib/elixir/pages/cheatsheets/enum-cheat.cheatmd | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/elixir/pages/cheatsheets/enum-cheat.cheatmd b/lib/elixir/pages/cheatsheets/enum-cheat.cheatmd index 94b071d63c..73255de6c1 100644 --- a/lib/elixir/pages/cheatsheets/enum-cheat.cheatmd +++ b/lib/elixir/pages/cheatsheets/enum-cheat.cheatmd @@ -820,7 +820,10 @@ Negative indexes count from the back: ```elixir iex> Enum.drop(cart, -1) -[%{fruit: "orange", count: 6}] +[ + %{fruit: "apple", count: 3}, + %{fruit: "banana", count: 1} +] ``` ### [`drop_every(enum, nth)`](`Enum.drop_every/2`) @@ -851,10 +854,7 @@ Negative indexes count from the back: ```elixir iex> Enum.take(cart, -1) -[ - %{fruit: "apple", count: 3}, - %{fruit: "banana", count: 1} -] +[%{fruit: "orange", count: 6}] ``` ### [`take_every(enum, nth)`](`Enum.take_every/2`) From 6c6e1477d124107acc446eca9ffa65f4cbb46cd3 Mon Sep 17 00:00:00 2001 From: Andrea Leopardi Date: Tue, 7 Nov 2023 11:16:52 +0100 Subject: [PATCH 0162/1886] Use Markdown in titles in the Mix docs (#13081) --- lib/mix/lib/mix.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/mix/lib/mix.ex b/lib/mix/lib/mix.ex index 96cc982042..8532964475 100644 --- a/lib/mix/lib/mix.ex +++ b/lib/mix/lib/mix.ex @@ -3,7 +3,7 @@ defmodule Mix do Mix is a build tool that provides tasks for creating, compiling, and testing Elixir projects, managing its dependencies, and more. - ## Mix.Project + ## `Mix.Project` The foundation of Mix is a project. A project can be defined by using `Mix.Project` in a module, usually placed in a file named `mix.exs`: @@ -36,7 +36,7 @@ defmodule Mix do The best way to get started with your first project is by calling `mix new my_project` from the command line. - ## Mix.Task + ## `Mix.Task` Tasks are what make Mix extensible. From 200ea042034c3979c0e64e0e5d454edefe2a5652 Mon Sep 17 00:00:00 2001 From: Andrea Leopardi Date: Tue, 7 Nov 2023 11:24:49 +0100 Subject: [PATCH 0163/1886] Improve docs for Mix.Task (#13082) --- lib/mix/lib/mix/task.ex | 47 ++++++++++++++++++++++++++++------------- 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/lib/mix/lib/mix/task.ex b/lib/mix/lib/mix/task.ex index b5a4b81c5b..fb3b1a2448 100644 --- a/lib/mix/lib/mix/task.ex +++ b/lib/mix/lib/mix/task.ex @@ -2,11 +2,17 @@ defmodule Mix.Task do @moduledoc """ Provides conveniences for creating, loading, and manipulating Mix tasks. - A Mix task can be defined by `use Mix.Task` in a module whose name - begins with `Mix.Tasks.` and which defines the `run/1` function. + To create a new Mix task, you'll need to: + + 1. Create a module whose name begins with `Mix.Tasks.` (for example, + `Mix.Tasks.MyTask`). + 2. Call `use Mix.Task` in that module. + 3. Implement the `Mix.Task` behaviour in that module (that is, + implement the `c:run/1` callback). + Typically, task modules live inside the `lib/mix/tasks/` directory, and their file names use dot separators instead of underscores - (e.g. `deps.clean.ex`) - although ultimately the file name is not + (for example, `deps.clean.ex`) - although ultimately the file name is not relevant. For example: @@ -71,20 +77,20 @@ defmodule Mix.Task do A task will typically depend on one of the following tasks: - * "loadpaths" - this ensures dependencies are available - and compiled. If you are publishing a task as part of - a library to be used by others, and your task does not - need to interact with the user code in any way, this is - the recommended requirement + * ["loadpaths"](`Mix.Tasks.Loadpaths`) - this ensures + dependencies are available and compiled. If you are publishing + a task as part of a library to be used by others, and your + task does not need to interact with the user code in any way, + this is the recommended requirement - * "app.config" - additionally compiles and loads the runtime - configuration for the current project. If you are creating - a task to be used within your application or as part of a - library, which must invoke or interact with the user code, - this is the minimum recommended requirement + * ["app.config"](`Mix.Tasks.App.Config`) - additionally compiles + and load the runtime configuration for the current project. If + you are creating a task to be used within your application or + as part of a library, which must invoke or interact with the + user code, this is the minimum recommended requirement - * "app.start" - additionally starts the supervision tree of - the current project and its dependencies + * ["app.start"](`Mix.Tasks.App.Start`) - additionally starts the + supervision tree of the current project and its dependencies ### `@recursive` @@ -105,7 +111,18 @@ defmodule Mix.Task do shown is the `@moduledoc` of the task's module. """ + @typedoc """ + The name of a task. + + For example, `"deps.clean"` or `:"deps.clean"`. + """ @type task_name :: String.t() | atom + + @typedoc """ + The module that implements a Mix task. + + For example, `Mix.Tasks.MyTask`. + """ @type task_module :: atom @doc """ From cdcf4a2f97224f7e700c68a9ac59ecf4b891753e Mon Sep 17 00:00:00 2001 From: Juan Barrios <03juan@users.noreply.github.com> Date: Tue, 7 Nov 2023 21:10:50 +0200 Subject: [PATCH 0164/1886] Update float.ex description of ceil/2 and floor/2 (#13084) --- lib/elixir/lib/float.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/elixir/lib/float.ex b/lib/elixir/lib/float.ex index 87aebdba11..2c6c42eee0 100644 --- a/lib/elixir/lib/float.ex +++ b/lib/elixir/lib/float.ex @@ -198,7 +198,7 @@ defmodule Float do defp add_dot(acc, false), do: acc <> ".0" @doc """ - Rounds a float to the largest number less than or equal to `num`. + Rounds a float to the largest float less than or equal to `number`. `floor/2` also accepts a precision to round a floating-point value down to an arbitrary number of fractional digits (between 0 and 15). @@ -246,7 +246,7 @@ defmodule Float do end @doc """ - Rounds a float to the smallest integer greater than or equal to `num`. + Rounds a float to the smallest float greater than or equal to `number`. `ceil/2` also accepts a precision to round a floating-point value down to an arbitrary number of fractional digits (between 0 and 15). From 80a4f8a77d194f59e4a1335ad1e87641556ebcaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 7 Nov 2023 21:29:58 +0100 Subject: [PATCH 0165/1886] Cache mix format --- lib/mix/lib/mix/tasks/format.ex | 60 +++++++++++++++++++++----- lib/mix/test/mix/tasks/format_test.exs | 12 ++++++ 2 files changed, 62 insertions(+), 10 deletions(-) diff --git a/lib/mix/lib/mix/tasks/format.ex b/lib/mix/lib/mix/tasks/format.ex index fb2a65044a..e07d740703 100644 --- a/lib/mix/lib/mix/tasks/format.ex +++ b/lib/mix/lib/mix/tasks/format.ex @@ -53,6 +53,9 @@ defmodule Mix.Tasks.Format do ## Task-specific options + * `--force` - force formatting to happen on all files, instead of + relying on cache. + * `--check-formatted` - checks that the file is already formatted. This is useful in pre-commit hooks and CI scripts if you want to reject contributions with unformatted code. If the check fails, @@ -180,10 +183,12 @@ defmodule Mix.Tasks.Format do no_exit: :boolean, dot_formatter: :string, dry_run: :boolean, - stdin_filename: :string + stdin_filename: :string, + force: :boolean ] - @manifest "cached_dot_formatter" + @manifest_timestamp "format_timestamp" + @manifest_dot_formatter "cached_dot_formatter" @manifest_vsn 2 @newline "\n" @@ -216,9 +221,9 @@ defmodule Mix.Tasks.Format do @callback format(String.t(), Keyword.t()) :: String.t() @impl true - def run(args) do + def run(all_args) do cwd = File.cwd!() - {opts, args} = OptionParser.parse!(args, strict: @switches) + {opts, args} = OptionParser.parse!(all_args, strict: @switches) {dot_formatter, formatter_opts} = eval_dot_formatter(cwd, opts) if opts[:check_equivalent] do @@ -233,14 +238,49 @@ defmodule Mix.Tasks.Format do eval_deps_and_subdirectories(cwd, dot_formatter, formatter_opts, [dot_formatter]) formatter_opts_and_subs = load_plugins(formatter_opts_and_subs) + files = expand_args(args, cwd, dot_formatter, formatter_opts_and_subs, opts) - args - |> expand_args(cwd, dot_formatter, formatter_opts_and_subs, opts) - |> Task.async_stream(&format_file(&1, opts), ordered: false, timeout: :infinity) - |> Enum.reduce({[], []}, &collect_status/2) - |> check!(opts) + maybe_cache_timestamps(all_args, files, fn files -> + files + |> Task.async_stream(&format_file(&1, opts), ordered: false, timeout: :infinity) + |> Enum.reduce({[], []}, &collect_status/2) + |> check!(opts) + end) end + defp maybe_cache_timestamps([], files, fun) do + if Mix.Project.get() do + # We fetch the time from before we read files so any future + # change to files are still picked up by the formatter + timestamp = System.os_time(:second) + dir = Mix.Project.manifest_path() + manifest_timestamp = Path.join(dir, @manifest_timestamp) + manifest_dot_formatter = Path.join(dir, @manifest_dot_formatter) + last_modified = Mix.Utils.last_modified(manifest_timestamp) + sources = [Mix.Project.config_mtime(), manifest_dot_formatter, ".formatter.exs"] + + files = + if Mix.Utils.stale?(sources, [last_modified]) do + files + else + Enum.filter(files, fn {file, _opts} -> + Mix.Utils.last_modified(file) > last_modified + end) + end + + try do + fun.(files) + after + File.mkdir_p!(dir) + File.touch!(manifest_timestamp, timestamp) + end + else + fun.(files) + end + end + + defp maybe_cache_timestamps([_ | _], files, fun), do: fun.(files) + defp load_plugins({formatter_opts, subs}) do plugins = Keyword.get(formatter_opts, :plugins, []) @@ -353,7 +393,7 @@ defmodule Mix.Tasks.Format do if deps == [] and subs == [] do {{formatter_opts, []}, sources} else - manifest = Path.join(Mix.Project.manifest_path(), @manifest) + manifest = Path.join(Mix.Project.manifest_path(), @manifest_dot_formatter) {{locals_without_parens, subdirectories}, sources} = maybe_cache_in_manifest(dot_formatter, manifest, fn -> diff --git a/lib/mix/test/mix/tasks/format_test.exs b/lib/mix/test/mix/tasks/format_test.exs index a5d43fa295..dcfe2d767f 100644 --- a/lib/mix/test/mix/tasks/format_test.exs +++ b/lib/mix/test/mix/tasks/format_test.exs @@ -724,6 +724,12 @@ defmodule Mix.Tasks.FormatTest do """) File.touch!("lib/sub/.formatter.exs", {{2038, 1, 1}, {0, 0, 0}}) + + File.touch!( + "_build/dev/lib/format_with_deps/.mix/format_timestamp", + {{2010, 1, 1}, {0, 0, 0}} + ) + Mix.Tasks.Format.run([]) assert File.read!("lib/sub/a.ex") == """ @@ -744,6 +750,12 @@ defmodule Mix.Tasks.FormatTest do """) File.touch!("lib/extra/.formatter.exs", {{2038, 1, 1}, {0, 0, 0}}) + + File.touch!( + "_build/dev/lib/format_with_deps/.mix/format_timestamp", + {{2010, 1, 1}, {0, 0, 0}} + ) + Mix.Tasks.Format.run([]) {_, formatter_opts} = Mix.Tasks.Format.formatter_for_file("lib/extra/a.ex") From df777df054c843bb911af2af45fc7c6185935598 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 7 Nov 2023 21:42:22 +0100 Subject: [PATCH 0166/1886] Improve coverage on mix format cache --- lib/mix/test/mix/tasks/format_test.exs | 27 ++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/lib/mix/test/mix/tasks/format_test.exs b/lib/mix/test/mix/tasks/format_test.exs index dcfe2d767f..476b40340d 100644 --- a/lib/mix/test/mix/tasks/format_test.exs +++ b/lib/mix/test/mix/tasks/format_test.exs @@ -183,6 +183,9 @@ defmodule Mix.Tasks.FormatTest do test "uses inputs and configuration from .formatter.exs", context do in_tmp(context.test, fn -> + # We need a project in order to enable caching + Mix.Project.push(__MODULE__.FormatWithDepsApp) + File.write!(".formatter.exs", """ [ inputs: ["a.ex"], @@ -199,6 +202,17 @@ defmodule Mix.Tasks.FormatTest do assert File.read!("a.ex") == """ foo bar(baz) """ + + File.write!(".formatter.exs", """ + [inputs: ["a.ex"]] + """) + + ensure_touched(".formatter.exs", "_build/dev/lib/format_with_deps/.mix/format_timestamp") + Mix.Tasks.Format.run([]) + + assert File.read!("a.ex") == """ + foo(bar(baz)) + """ end) end @@ -677,6 +691,7 @@ defmodule Mix.Tasks.FormatTest do test "reads exported configuration from dependencies and subdirectories", context do in_tmp(context.test, fn -> Mix.Project.push(__MODULE__.FormatWithDepsApp) + format_timestamp = "_build/dev/lib/format_with_deps/.mix/format_timestamp" File.mkdir_p!("deps/my_dep/") @@ -724,11 +739,7 @@ defmodule Mix.Tasks.FormatTest do """) File.touch!("lib/sub/.formatter.exs", {{2038, 1, 1}, {0, 0, 0}}) - - File.touch!( - "_build/dev/lib/format_with_deps/.mix/format_timestamp", - {{2010, 1, 1}, {0, 0, 0}} - ) + File.touch!(format_timestamp, {{2010, 1, 1}, {0, 0, 0}}) Mix.Tasks.Format.run([]) @@ -750,11 +761,7 @@ defmodule Mix.Tasks.FormatTest do """) File.touch!("lib/extra/.formatter.exs", {{2038, 1, 1}, {0, 0, 0}}) - - File.touch!( - "_build/dev/lib/format_with_deps/.mix/format_timestamp", - {{2010, 1, 1}, {0, 0, 0}} - ) + File.touch!(format_timestamp, {{2010, 1, 1}, {0, 0, 0}}) Mix.Tasks.Format.run([]) From bd51ca7bd84c5f5255c07353e0c477417dd89189 Mon Sep 17 00:00:00 2001 From: Minh Dao <43783196+minhqdao@users.noreply.github.com> Date: Tue, 7 Nov 2023 22:51:35 +0100 Subject: [PATCH 0167/1886] Fix typo in getting-started guide (#13085) Co-authored-by: minhqdao --- lib/elixir/pages/getting-started/basic-types.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/pages/getting-started/basic-types.md b/lib/elixir/pages/getting-started/basic-types.md index 140192e214..6b26fba71c 100644 --- a/lib/elixir/pages/getting-started/basic-types.md +++ b/lib/elixir/pages/getting-started/basic-types.md @@ -83,7 +83,7 @@ You can also use `is_float` or `is_number` to check, respectively, if an argumen ## Identifying functions and documentation -Before we move on to the next data type, let's talk about how Elixir identity functions. +Before we move on to the next data type, let's talk about how Elixir identifies functions. Functions in Elixir are identified by both their name and their arity. The arity of a function describes the number of arguments that the function takes. From this point on we will use both the function name and its arity to describe functions throughout the documentation. `trunc/1` identifies the function which is named `trunc` and takes `1` argument, whereas `trunc/2` identifies a different (nonexistent) function with the same name but with an arity of `2`. From e92def337110078ed89f4f2dd0a12ba9fb708a46 Mon Sep 17 00:00:00 2001 From: Cameron Duley Date: Thu, 9 Nov 2023 15:13:03 -0500 Subject: [PATCH 0168/1886] Fix `String.replace_invalid/2` perf regressions (#13090) --- lib/elixir/lib/string.ex | 112 +++++++++++++++++++-------------------- 1 file changed, 54 insertions(+), 58 deletions(-) diff --git a/lib/elixir/lib/string.ex b/lib/elixir/lib/string.ex index 5bfadfd562..fe47e33dd7 100644 --- a/lib/elixir/lib/string.ex +++ b/lib/elixir/lib/string.ex @@ -1871,6 +1871,22 @@ defmodule String do end end + defguardp replace_invalid_ii_of_iii(i, ii) + when (896 <= Bitwise.bor(Bitwise.bsl(i, 6), ii) and + Bitwise.bor(Bitwise.bsl(i, 6), ii) <= 1023) or + (32 <= Bitwise.bor(Bitwise.bsl(i, 6), ii) and + Bitwise.bor(Bitwise.bsl(i, 6), ii) <= 863) + + defguardp replace_invalid_ii_of_iv(i, ii) + when 16 <= Bitwise.bor(Bitwise.bsl(i, 6), ii) and + Bitwise.bor(Bitwise.bsl(i, 6), ii) <= 271 + + defguardp replace_invalid_iii_of_iv(i, ii, iii) + when 1024 <= Bitwise.bor(Bitwise.bor(Bitwise.bsl(i, 12), Bitwise.bsl(ii, 6)), iii) and + Bitwise.bor(Bitwise.bor(Bitwise.bsl(i, 12), Bitwise.bsl(ii, 6)), iii) <= 17407 + + defguardp replace_invalid_is_next(next) when Bitwise.bsr(next, 6) !== 0b10 + @doc ~S""" Returns a new string created by replacing all invalid bytes with `replacement` (`"�"` by default). @@ -1889,94 +1905,74 @@ defmodule String do "nem rán bERROR! bề" """ @doc since: "1.16.0" - def replace_invalid(string, replacement \\ "�") - when is_binary(string) and is_binary(replacement) do - do_replace_invalid(string, replacement, <<>>) + def replace_invalid(bytes, replacement \\ "�") + when is_binary(bytes) and is_binary(replacement) do + do_replace_invalid(bytes, replacement, <<>>) end # Valid ASCII (for better average speed) - defp do_replace_invalid(<>, rep, acc) - when ascii in 0..127 and n_lead != 0b10 do - do_replace_invalid(<>, rep, <>) + defp do_replace_invalid(<> = rest, rep, acc) + when ascii in 0..127 and replace_invalid_is_next(next) do + <<_::8, rest::bytes>> = rest + do_replace_invalid(rest, rep, acc <> <>) end # Valid UTF-8 - defp do_replace_invalid(<>, rep, acc) do - do_replace_invalid(rest, rep, <>) + defp do_replace_invalid(<>, rep, acc) do + do_replace_invalid(rest, rep, acc <> <>) end # 2/3 truncated sequence - defp do_replace_invalid(<<0b1110::4, i::4, 0b10::2, ii::6>>, rep, acc) do - <> = <> - <> + defp do_replace_invalid(<<0b1110::4, i::4, 0b10::2, ii::6>>, rep, acc) + when replace_invalid_ii_of_iii(i, ii) do + acc <> rep end - defp do_replace_invalid(<<0b1110::4, i::4, 0b10::2, ii::6, n_lead::2, rest::bits>>, rep, acc) - when n_lead != 0b10 do - <> = <> - - do_replace_invalid( - <>, - rep, - <> - ) + defp do_replace_invalid(<<0b1110::4, i::4, 0b10::2, ii::6, next::8, _::bytes>> = rest, rep, acc) + when replace_invalid_ii_of_iii(i, ii) and replace_invalid_is_next(next) do + <<_::16, rest::bytes>> = rest + do_replace_invalid(rest, rep, acc <> rep) end # 2/4 - defp do_replace_invalid(<<0b11110::5, i::3, 0b10::2, ii::6>>, rep, acc) do - <> = <> - <> + defp do_replace_invalid(<<0b11110::5, i::3, 0b10::2, ii::6>>, rep, acc) + when replace_invalid_ii_of_iv(i, ii) do + acc <> rep end - defp do_replace_invalid(<<0b11110::5, i::3, 0b10::2, ii::6, n_lead::2, rest::bits>>, rep, acc) - when n_lead != 0b10 do - <> = <> - - do_replace_invalid( - <>, - rep, - <> - ) + defp do_replace_invalid( + <<0b11110::5, i::3, 0b10::2, ii::6, next::8, _::bytes>> = rest, + rep, + acc + ) + when replace_invalid_ii_of_iv(i, ii) and replace_invalid_is_next(next) do + <<_::16, rest::bytes>> = rest + do_replace_invalid(rest, rep, acc <> rep) end # 3/4 - defp do_replace_invalid(<<0b11110::5, i::3, 0b10::2, ii::6, 0b10::2, iii::6>>, rep, acc) do - <> = <> - <> + defp do_replace_invalid(<<0b11110::5, i::3, 0b10::2, ii::6, 0b10::2, iii::6>>, rep, acc) + when replace_invalid_iii_of_iv(i, ii, iii) do + acc <> rep end defp do_replace_invalid( - <<0b11110::5, i::3, 0b10::2, ii::6, 0b10::2, iii::6, n_lead::2, rest::bits>>, + <<0b11110::5, i::3, 0b10::2, ii::6, 0b10::2, iii::6, next::8, _::bytes>> = rest, rep, acc ) - when n_lead != 0b10 do - <> = <> - - do_replace_invalid( - <>, - rep, - <> - ) + when replace_invalid_iii_of_iv(i, ii, iii) and replace_invalid_is_next(next) do + <<_::24, rest::bytes>> = rest + do_replace_invalid(rest, rep, acc <> rep) end - # any other invalid bytes - defp do_replace_invalid(<<_, rest::bits>>, rep, acc), - do: do_replace_invalid(rest, rep, <>) + # Everything else + defp do_replace_invalid(<<_, rest::bytes>>, rep, acc), + do: do_replace_invalid(rest, rep, acc <> rep) + # Final defp do_replace_invalid(<<>>, _, acc), do: acc - # bounds-checking truncated code points for overlong encodings - defp replace_invalid_ii_of_iii(tcp, rep) when tcp >= 32 and tcp <= 863, do: rep - defp replace_invalid_ii_of_iii(tcp, rep) when tcp >= 896 and tcp <= 1023, do: rep - defp replace_invalid_ii_of_iii(_, rep), do: rep <> rep - - defp replace_invalid_ii_of_iiii(tcp, rep) when tcp >= 16 and tcp <= 271, do: rep - defp replace_invalid_ii_of_iiii(_, rep), do: rep <> rep - - defp replace_invalid_iii_of_iiii(tcp, rep) when tcp >= 1024 and tcp <= 17407, do: rep - defp replace_invalid_iii_of_iiii(_, rep), do: rep <> rep <> rep - @doc ~S""" Splits the string into chunks of characters that share a common trait. From 49820a3bfe783eae25cfbe191ab5217b57191ebe Mon Sep 17 00:00:00 2001 From: Cameron Duley Date: Thu, 9 Nov 2023 18:59:05 -0500 Subject: [PATCH 0169/1886] Use `in/2` in `String.replace_invalid/2` guards (#13093) --- lib/elixir/lib/string.ex | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/lib/elixir/lib/string.ex b/lib/elixir/lib/string.ex index fe47e33dd7..1966c53340 100644 --- a/lib/elixir/lib/string.ex +++ b/lib/elixir/lib/string.ex @@ -1872,18 +1872,14 @@ defmodule String do end defguardp replace_invalid_ii_of_iii(i, ii) - when (896 <= Bitwise.bor(Bitwise.bsl(i, 6), ii) and - Bitwise.bor(Bitwise.bsl(i, 6), ii) <= 1023) or - (32 <= Bitwise.bor(Bitwise.bsl(i, 6), ii) and - Bitwise.bor(Bitwise.bsl(i, 6), ii) <= 863) + when Bitwise.bor(Bitwise.bsl(i, 6), ii) in 32..863 or + Bitwise.bor(Bitwise.bsl(i, 6), ii) in 896..1023 defguardp replace_invalid_ii_of_iv(i, ii) - when 16 <= Bitwise.bor(Bitwise.bsl(i, 6), ii) and - Bitwise.bor(Bitwise.bsl(i, 6), ii) <= 271 + when Bitwise.bor(Bitwise.bsl(i, 6), ii) in 16..271 defguardp replace_invalid_iii_of_iv(i, ii, iii) - when 1024 <= Bitwise.bor(Bitwise.bor(Bitwise.bsl(i, 12), Bitwise.bsl(ii, 6)), iii) and - Bitwise.bor(Bitwise.bor(Bitwise.bsl(i, 12), Bitwise.bsl(ii, 6)), iii) <= 17407 + when Bitwise.bor(Bitwise.bor(Bitwise.bsl(i, 12), Bitwise.bsl(ii, 6)), iii) in 1024..17407 defguardp replace_invalid_is_next(next) when Bitwise.bsr(next, 6) !== 0b10 From 1b6fb26dfb761b1c3ec0d4f9af2156db2d0f2372 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 10 Nov 2023 12:36:04 +0100 Subject: [PATCH 0170/1886] Let's not deprecate ...foo as the API may be useful for the type system in the future. This reverts commit f97d8585e8c7c1c6d578d927c0cda47eb9ef79b4. --- lib/elixir/src/elixir_parser.yrl | 5 ----- 1 file changed, 5 deletions(-) diff --git a/lib/elixir/src/elixir_parser.yrl b/lib/elixir/src/elixir_parser.yrl index de678104fd..ce6afd3217 100644 --- a/lib/elixir/src/elixir_parser.yrl +++ b/lib/elixir/src/elixir_parser.yrl @@ -910,11 +910,6 @@ build_identifier({'.', Meta, _} = Dot, Args) -> build_identifier({op_identifier, Location, Identifier}, [Arg]) -> {Identifier, [{ambiguous_op, nil} | meta_from_location(Location)], [Arg]}; -%% TODO: Either remove ... or make it an operator on v2.0 -build_identifier({_, {Line, Column, _} = Location, '...'}, Args) when is_list(Args) -> - warn({Line, Column}, "... is no longer supported as a function call and it must receive no arguments"), - {'...', meta_from_location(Location), Args}; - build_identifier({_, Location, Identifier}, Args) -> {Identifier, meta_from_location(Location), Args}. From 665a459a5938608fa970c6e5c45eb8e81fdc83c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 10 Nov 2023 12:53:34 +0100 Subject: [PATCH 0171/1886] Handle nil values in IO.warn --- lib/elixir/src/elixir_errors.erl | 6 +- .../test/elixir/kernel/diagnostics_test.exs | 100 ++++-------------- 2 files changed, 23 insertions(+), 83 deletions(-) diff --git a/lib/elixir/src/elixir_errors.erl b/lib/elixir/src/elixir_errors.erl index 1cb3d92bed..2455d69ae5 100644 --- a/lib/elixir/src/elixir_errors.erl +++ b/lib/elixir/src/elixir_errors.erl @@ -57,8 +57,7 @@ get_snippet(File, Position) -> LineNumber = extract_line(Position), get_file_line(File, LineNumber). -get_file_line(_, 0) -> nil; -get_file_line(File, LineNumber) -> +get_file_line(File, LineNumber) when is_integer(LineNumber), LineNumber > 0 -> case file:open(File, [read, binary]) of {ok, IoDevice} -> Line = traverse_file_line(IoDevice, LineNumber), @@ -66,7 +65,8 @@ get_file_line(File, LineNumber) -> Line; {error, _} -> nil - end. + end; +get_file_line(_, _) -> nil. traverse_file_line(IoDevice, 1) -> case file:read_line(IoDevice) of diff --git a/lib/elixir/test/elixir/kernel/diagnostics_test.exs b/lib/elixir/test/elixir/kernel/diagnostics_test.exs index 6e30f0f10b..a34dbb4e1d 100644 --- a/lib/elixir/test/elixir/kernel/diagnostics_test.exs +++ b/lib/elixir/test/elixir/kernel/diagnostics_test.exs @@ -544,101 +544,41 @@ defmodule Kernel.DiagnosticsTest do end @tag :tmp_dir - test "long message (file)", %{tmp_dir: tmp_dir} do - path = make_relative_tmp(tmp_dir, "long-warning.ex") + test "IO.warn file+line+column", %{tmp_dir: tmp_dir} do + path = make_relative_tmp(tmp_dir, "io-warn-file-line-column.ex") source = """ - defmodule Sample do - @file "#{path}" - - def atom_case do - v = "bc" - - case v do - _ when is_atom(v) -> :ok - _ -> :fail - end - end - end + IO.warn("oops\\nmulti\\nline", file: __ENV__.file, line: __ENV__.line, column: 4) """ File.write!(path, source) expected = """ - warning: incompatible types: - - binary() !~ atom() - - in expression: - - # #{path}:8 - is_atom(v) - - where "v" was given the type binary() in: - - # #{path}:5 - v = "bc" - - where "v" was given the type atom() in: - - # #{path}:8 - is_atom(v) - - Conflict found at + warning: oops + multi + line │ - 8 │ _ when is_atom(v) -> :ok - │ ~ + 1 │ IO.warn("oops\\nmulti\\nline", file: __ENV__.file, line: __ENV__.line, column: 4) + │ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ │ - └─ #{path}:8:14: Sample.atom_case/0 + └─ tmp\ """ - assert capture_eval(source) =~ expected - after - purge(Sample) + assert capture_io(:stderr, fn -> Code.eval_file(path) end) =~ expected end - test "long message (nofile)" do - source = """ - defmodule Sample do - def atom_case do - v = "bc" - - case v do - _ when is_atom(v) -> :ok - _ -> :fail - end - end - end - """ - - expected = """ - warning: incompatible types: - - binary() !~ atom() - - in expression: - - # nofile:6 - is_atom(v) + test "IO.warn with missing data" do + assert capture_eval(""" + IO.warn("oops-bad", file: #{inspect(__ENV__.file)}, line: 3, column: nil) + """) =~ "warning: oops-bad" - where "v" was given the type binary() in: + assert capture_eval(""" + IO.warn("oops-bad", file: #{inspect(__ENV__.file)}, line: nil) + """) =~ "oops-bad" - # nofile:3 - v = "bc" - - where "v" was given the type atom() in: - - # nofile:6 - is_atom(v) - - Conflict found at - └─ nofile:6:14: Sample.atom_case/0 - - """ - - assert capture_eval(source) =~ expected - after - purge(Sample) + assert capture_eval(""" + IO.warn("oops-bad", file: nil) + """) =~ "oops-bad" end @tag :tmp_dir From 9eb86db1c019b4ce5c7298ed94924c39f723f146 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Jan=20Niemier?= Date: Fri, 10 Nov 2023 20:37:39 +0100 Subject: [PATCH 0172/1886] fix: correct `Enum.join/2` spec (#13094) --- lib/elixir/lib/enum.ex | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/elixir/lib/enum.ex b/lib/elixir/lib/enum.ex index a2efb8dffa..1a2dfe52b3 100644 --- a/lib/elixir/lib/enum.ex +++ b/lib/elixir/lib/enum.ex @@ -1636,8 +1636,8 @@ defmodule Enum do If `joiner` is not passed at all, it defaults to an empty string. - All elements in the `enumerable` must be convertible to a string, - otherwise an error is raised. + All elements in the `enumerable` must be convertible to a string + or be a binary, otherwise an error is raised. ## Examples @@ -1651,7 +1651,7 @@ defmodule Enum do "ab cdefg h i" """ - @spec join(t, String.t()) :: String.t() + @spec join(t, binary()) :: binary() def join(enumerable, joiner \\ "") def join(enumerable, "") do From 98f412c54b1701354e45ab777c345e5ece96d7f0 Mon Sep 17 00:00:00 2001 From: Panagiotis Nezis Date: Fri, 10 Nov 2023 21:45:08 +0200 Subject: [PATCH 0173/1886] Pretty print alias definition in mix help (#13066) --- lib/mix/lib/mix/tasks/help.ex | 50 +++++++++++++++- lib/mix/test/mix/tasks/help_test.exs | 88 +++++++++++++++++++++------- 2 files changed, 114 insertions(+), 24 deletions(-) diff --git a/lib/mix/lib/mix/tasks/help.ex b/lib/mix/lib/mix/tasks/help.ex index b2770513be..9c2122b3c0 100644 --- a/lib/mix/lib/mix/tasks/help.ex +++ b/lib/mix/lib/mix/tasks/help.ex @@ -14,6 +14,7 @@ defmodule Mix.Tasks.Help do $ mix help --search PATTERN - prints all tasks and aliases that contain PATTERN in the name $ mix help --names - prints all task names and aliases (useful for autocompleting) + $ mix help --aliases - prints all aliases ## Colors @@ -69,6 +70,16 @@ defmodule Mix.Tasks.Help do end end + def run(["--aliases"]) do + loadpaths!() + + aliases = load_aliases() + + {docs, max} = build_doc_list([], aliases) + + display_doc_list(docs, max) + end + def run(["--search", pattern]) do loadpaths!() @@ -194,13 +205,35 @@ defmodule Mix.Tasks.Help do end defp build_alias_doc_list(aliases) do - Enum.reduce(aliases, {[], 0}, fn {alias_name, _task_name}, {docs, max} -> - doc = "Alias defined in mix.exs" + Enum.reduce(aliases, {[], 0}, fn {alias_name, task}, {docs, max} -> + doc = alias_doc(task) task = "mix " <> alias_name {[{task, doc} | docs], max(byte_size(task), max)} end) end + defp alias_doc(task) do + "Alias for " <> format_alias_doc(task) + end + + defp format_alias_doc(task), do: Enum.map_join(List.wrap(task), ", ", &format_alias_task/1) + + defp format_alias_task(task) when is_binary(task), do: task + + defp format_alias_task(task) when is_function(task) do + info = Function.info(task) + name = Atom.to_string(info[:name]) + + cond do + info[:type] == :remote -> inspect(task) + info[:type] == :local and String.contains?(name, "/") -> "a function" + true -> "&#{name}/#{info[:arity]}" + end + end + + # for invalid aliases + defp format_alias_task(task), do: inspect(task) + defp verbose_doc(task) do aliases = load_aliases() @@ -221,7 +254,18 @@ defmodule Mix.Tasks.Help do end defp alias_doc(task_name, note) do - {"Alias for " <> inspect(task_name), "mix.exs", note} + alias_doc = """ + Alias for + + #{format_alias(task_name)} + """ + + {alias_doc, "mix.exs", note} + end + + defp format_alias(task) do + inspect(task, pretty: true, width: 0) + |> String.replace("\n", "\n ") end defp task_doc(task) do diff --git a/lib/mix/test/mix/tasks/help_test.exs b/lib/mix/test/mix/tasks/help_test.exs index d5ade575af..759ee34adf 100644 --- a/lib/mix/test/mix/tasks/help_test.exs +++ b/lib/mix/test/mix/tasks/help_test.exs @@ -29,20 +29,6 @@ defmodule Mix.Tasks.HelpTest do end end - test "help lists all aliases", context do - in_tmp(context.test, fn -> - Mix.Project.push(Aliases) - - Mix.Tasks.Help.run([]) - - assert_received {:mix_shell, :info, ["mix h" <> message]} - assert message =~ ~r/# Alias defined in mix.exs/ - - assert_received {:mix_shell, :info, ["mix c" <> message]} - assert message =~ ~r/# Alias defined in mix.exs/ - end) - end - test "help --names", context do in_tmp(context.test, fn -> Mix.Project.push(Aliases) @@ -63,11 +49,50 @@ defmodule Mix.Tasks.HelpTest do aliases: [ h: "hello", p: &inspect/1, + foo: &foo/1, + bar: fn _ -> :ok end, help: ["help", "hello"], - "nested.h": [&Mix.shell().info(inspect(&1)), "h foo bar"] + "nested.h": [&Mix.shell().info(inspect(&1)), "h foo bar"], + other: [ + "format --check-formatted", + fn _ -> :ok end, + &foo/1, + "help" + ] ] ] end + + defp foo(_), do: :ok + end + + test "help lists all aliases", context do + in_tmp(context.test, fn -> + Mix.Project.push(ComplexAliases) + + Mix.Tasks.Help.run([]) + + assert_received {:mix_shell, :info, ["mix h" <> message]} + assert message =~ ~r/# Alias for hello/ + + assert_received {:mix_shell, :info, ["mix p" <> message]} + assert message =~ ~r/# Alias for &inspect\/1/ + + assert_received {:mix_shell, :info, ["mix foo" <> message]} + assert message =~ ~r/# Alias for &foo\/1/ + + assert_received {:mix_shell, :info, ["mix bar" <> message]} + assert message =~ ~r/# Alias for a function/ + + assert_received {:mix_shell, :info, ["mix help" <> message]} + assert message =~ ~r/# Alias for help, hello/ + + assert_received {:mix_shell, :info, ["mix nested.h" <> message]} + assert message =~ ~r/# Alias for a function, h foo bar/ + + assert_received {:mix_shell, :info, ["mix other" <> message]} + assert message =~ ~r/# Alias for format --check-formatted, a function, &foo\/1, help/ + end) end test "help ALIAS", context do @@ -80,7 +105,8 @@ defmodule Mix.Tasks.HelpTest do end) assert output =~ "mix h\n\n" - assert output =~ "Alias for \"hello\"\n" + assert output =~ "Alias for\n\n" + assert output =~ " \"hello\"\n" assert output =~ ~r/^Location: mix.exs/m output = @@ -89,7 +115,8 @@ defmodule Mix.Tasks.HelpTest do end) assert output =~ "mix p\n\n" - assert output =~ "Alias for &Kernel.inspect/1\n" + assert output =~ "Alias for\n\n" + assert output =~ " &Kernel.inspect/1\n" assert output =~ ~r/^Location: mix.exs/m output = @@ -98,7 +125,9 @@ defmodule Mix.Tasks.HelpTest do end) assert output =~ "mix help\n\n" - assert output =~ "Alias for [\"help\", \"hello\"]\n" + assert output =~ "Alias for\n\n" + assert output =~ " [\"help\",\n" + assert output =~ " \"hello\"]\n" assert output =~ ~r/^Location: mix.exs/m output = @@ -107,7 +136,8 @@ defmodule Mix.Tasks.HelpTest do end) assert output =~ "mix nested.h\n\n" - assert output =~ ~r/Alias for \[#Function/ + assert output =~ "Alias for\n\n" + assert output =~ ~r/ \[#Function/ assert output =~ ~r/^Location: mix.exs/m end) end @@ -149,7 +179,8 @@ defmodule Mix.Tasks.HelpTest do end) assert output =~ "mix compile\n\n" - assert output =~ "Alias for \"compile\"\n" + assert output =~ "Alias for\n\n" + assert output =~ "\"compile\"\n" assert output =~ ~r/^Location: mix.exs/m assert output =~ @@ -172,7 +203,7 @@ defmodule Mix.Tasks.HelpTest do Mix.Tasks.Help.run(["--search", "h"]) assert_received {:mix_shell, :info, ["mix h" <> message]} - assert message =~ ~r/# Alias defined in mix.exs/ + assert message =~ ~r/# Alias for hello/ end) end @@ -193,6 +224,21 @@ defmodule Mix.Tasks.HelpTest do end) end + test "help --aliases", context do + in_tmp(context.test, fn -> + Mix.Project.push(Aliases) + + Mix.Tasks.Help.run(["--aliases"]) + assert_received {:mix_shell, :info, ["mix h" <> message]} + assert message =~ ~r/# Alias for hello/ + + assert_received {:mix_shell, :info, ["mix c" <> message]} + assert message =~ ~r/# Alias for compile/ + + refute_received {:mix_shell, :info, ["mix deps" <> _]} + end) + end + test "bad arguments" do message = "Unexpected arguments, expected \"mix help\" or \"mix help TASK\"" From 91e1bfeb3cb98b6000d9ec9587e6e16a916e70ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sat, 11 Nov 2023 08:59:55 +0100 Subject: [PATCH 0174/1886] Fix link, closes #13095 --- lib/elixir/pages/mix-and-otp/agents.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/pages/mix-and-otp/agents.md b/lib/elixir/pages/mix-and-otp/agents.md index 935064a492..5c9026eebb 100644 --- a/lib/elixir/pages/mix-and-otp/agents.md +++ b/lib/elixir/pages/mix-and-otp/agents.md @@ -124,7 +124,7 @@ The first step in our implementation is to call `use Agent`. Most of the functio Then we define a `start_link/1` function, which will effectively start the agent. It is a convention to define a `start_link/1` function that always accepts a list of options. We don't plan on using any options right now, but we might later on. We then proceed to call `Agent.start_link/1`, which receives an anonymous function that returns the Agent's initial state. -We are keeping a map inside the agent to store our keys and values. Getting and putting values on the map is done with the Agent API and the capture operator `&`, introduced in [the Getting Started guide](../getting-started/modules-and-functions.md#function-capturing). The agent passes its state to the anonymous function via the `&1` argument when `Agent.get/2` and `Agent.update/2` are called. +We are keeping a map inside the agent to store our keys and values. Getting and putting values on the map is done with the Agent API and the capture operator `&`, introduced in [the Getting Started guide](../getting-started/anonymous-functions.md#the-capture-operator). The agent passes its state to the anonymous function via the `&1` argument when `Agent.get/2` and `Agent.update/2` are called. Now that the `KV.Bucket` module has been defined, our test should pass! You can try it yourself by running: `mix test`. From 09b417770221b48b2145db2ecae665f84b6c9f93 Mon Sep 17 00:00:00 2001 From: Christopher Keele Date: Sat, 11 Nov 2023 02:20:01 -0600 Subject: [PATCH 0175/1886] Produce better error messages for non-binary mix git deps refspecs. (#13088) When using git dependencies, a branch/ref/tag specifier is passed verbatim to `System.cmd/3`. This can lead to intimidating error messages when they are not provided as a binary (for instance, an atom like `tag: :stable`): ``` ** (ArgumentError) all arguments for System.cmd/3 must be binaries (elixir 1.15.6) lib/system.ex:1083: System.cmd/3 (mix 1.15.6) lib/mix/scm/git.ex:287: Mix.SCM.Git.git!/2 ``` This PR adds a check during git opts verification time to provide better feedback. --- lib/mix/lib/mix/scm/git.ex | 26 ++++++++++++++++---------- lib/mix/test/mix/scm/git_test.exs | 16 +++++++++++++++- 2 files changed, 31 insertions(+), 11 deletions(-) diff --git a/lib/mix/lib/mix/scm/git.ex b/lib/mix/lib/mix/scm/git.ex index 7b46434443..a5471bca18 100644 --- a/lib/mix/lib/mix/scm/git.ex +++ b/lib/mix/lib/mix/scm/git.ex @@ -201,18 +201,24 @@ defmodule Mix.SCM.Git do ## Helpers defp validate_git_options(opts) do - err = - "You should specify only one of branch, ref or tag, and only once. " <> - "Error on Git dependency: #{redact_uri(opts[:git])}" + case Keyword.take(opts, [:branch, :ref, :tag]) do + [] -> + opts - validate_single_uniq(opts, [:branch, :ref, :tag], err) - end + [{_refspec, value}] when is_binary(value) -> + opts - defp validate_single_uniq(opts, take, error) do - case Keyword.take(opts, take) do - [] -> opts - [_] -> opts - _ -> Mix.raise(error) + [{refspec, value}] -> + Mix.raise( + "A dependency's #{refspec} must be a string, got: #{inspect(value)}. " <> + "Error on Git dependency: #{redact_uri(opts[:git])}" + ) + + _ -> + Mix.raise( + "You should specify only one of branch, ref or tag, and only once. " <> + "Error on Git dependency: #{redact_uri(opts[:git])}" + ) end end diff --git a/lib/mix/test/mix/scm/git_test.exs b/lib/mix/test/mix/scm/git_test.exs index 96634bc355..2442951054 100644 --- a/lib/mix/test/mix/scm/git_test.exs +++ b/lib/mix/test/mix/scm/git_test.exs @@ -50,7 +50,7 @@ defmodule Mix.SCM.GitTest do end end - test "raises about conflicting Git checkout options" do + test "raises about conflicting Git refspec options" do assert_raise Mix.Error, ~r/You should specify only one of branch, ref or tag/, fn -> Mix.SCM.Git.accepts_options(nil, git: "/repo", branch: "main", tag: "0.1.0") end @@ -60,6 +60,20 @@ defmodule Mix.SCM.GitTest do end end + test "raises about non-binary Git refspec options" do + assert_raise Mix.Error, ~r/A dependency's branch must be a string/, fn -> + Mix.SCM.Git.accepts_options(nil, git: "/repo", branch: :main) + end + + assert_raise Mix.Error, ~r/A dependency's tag must be a string/, fn -> + Mix.SCM.Git.accepts_options(nil, git: "/repo", tag: :stable) + end + + assert_raise Mix.Error, ~r/A dependency's ref must be a string/, fn -> + Mix.SCM.Git.accepts_options(nil, git: "/repo", ref: :abcdef0123456789) + end + end + defp lock(opts \\ []) do [lock: {:git, "/repo", "abcdef0123456789", opts}] end From 9e6695f653ee8d962cfe4f28827cf8c00c9944ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sat, 11 Nov 2023 21:55:18 +0100 Subject: [PATCH 0176/1886] Fix GenServer cheatsheet link Closes #13098. --- lib/elixir/pages/mix-and-otp/genservers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/pages/mix-and-otp/genservers.md b/lib/elixir/pages/mix-and-otp/genservers.md index 51be7cbe5b..74da0577c3 100644 --- a/lib/elixir/pages/mix-and-otp/genservers.md +++ b/lib/elixir/pages/mix-and-otp/genservers.md @@ -320,7 +320,7 @@ So far we have used three callbacks: `handle_call/3`, `handle_cast/2` and `handl Since any message, including the ones sent via `send/2`, go to `handle_info/2`, there is a chance that unexpected messages will arrive to the server. Therefore, if we don't define the catch-all clause, those messages could cause our registry to crash, because no clause would match. We don't need to worry about such cases for `handle_call/3` and `handle_cast/2` though. Calls and casts are only done via the `GenServer` API, so an unknown message is quite likely a developer mistake. -To help developers remember the differences between call, cast and info, the supported return values and more, we have a tiny [GenServer cheat sheet](/downloads/cheatsheets/gen-server.pdf). +To help developers remember the differences between call, cast and info, the supported return values and more, we have a tiny [GenServer cheat sheet](https://elixir-lang.org/downloads/cheatsheets/gen-server.pdf). ## Monitors or links? From dbde3ba78ef47ae48aa86af91be554ebc2f13af3 Mon Sep 17 00:00:00 2001 From: Artem Solomatin Date: Sun, 12 Nov 2023 12:07:12 +0300 Subject: [PATCH 0177/1886] Fix links references (#13099) --- lib/elixir/pages/getting-started/keywords-and-maps.md | 2 +- lib/elixir/pages/mix-and-otp/distributed-tasks.md | 2 +- lib/elixir/pages/mix-and-otp/genservers.md | 2 +- lib/elixir/pages/references/syntax-reference.md | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/elixir/pages/getting-started/keywords-and-maps.md b/lib/elixir/pages/getting-started/keywords-and-maps.md index e99b38399f..11b1c01cbf 100644 --- a/lib/elixir/pages/getting-started/keywords-and-maps.md +++ b/lib/elixir/pages/getting-started/keywords-and-maps.md @@ -127,7 +127,7 @@ iex> if true, do: "This will be seen", else: "This won't" Pay close attention to both syntaxes. In the keyword list format, we separate each key-value pair with commas, and each key is followed by `:`. In the `do`-blocks, we get rid of the colons, the commas, and separate each keyword by a newline. They are useful exactly because they remove the verbosity when writing blocks of code. Most of the time, you will use the block syntax, but it is good to know they are equivalent. -This plays an important role in the language as it allows Elixir syntax to stay small but still expressive. We only need few data structures to represent the language, a topic we will come back to when talking about [optional syntax](optional-syntax.md) and go in-depth when discussing [meta-programming](../quote-and-unquote.md). +This plays an important role in the language as it allows Elixir syntax to stay small but still expressive. We only need few data structures to represent the language, a topic we will come back to when talking about [optional syntax](optional-syntax.md) and go in-depth when discussing [meta-programming](../meta-programming/quote-and-unquote.md). With this out of the way, let's talk about maps. diff --git a/lib/elixir/pages/mix-and-otp/distributed-tasks.md b/lib/elixir/pages/mix-and-otp/distributed-tasks.md index d2a274f3c5..608fd3c2dd 100644 --- a/lib/elixir/pages/mix-and-otp/distributed-tasks.md +++ b/lib/elixir/pages/mix-and-otp/distributed-tasks.md @@ -90,7 +90,7 @@ There are three better alternatives to `Node.spawn_link/2` that we could use in 2. We could have a server running on the other node and send requests to that node via the `GenServer` API. For example, you can call a server on a remote node by using `GenServer.call({name, node}, arg)` or passing the remote process PID as the first argument -3. We could use [tasks](`Task`), which we have learned about in [a previous chapter](../getting-started/mix-otp/task-and-gen-tcp.md), as they can be spawned on both local and remote nodes +3. We could use [tasks](`Task`), which we have learned about in [a previous chapter](task-and-gen-tcp.md), as they can be spawned on both local and remote nodes The options above have different properties. The GenServer would serialize your requests on a single server, while tasks are effectively running asynchronously on the remote node, with the only serialization point being the spawning done by the supervisor. diff --git a/lib/elixir/pages/mix-and-otp/genservers.md b/lib/elixir/pages/mix-and-otp/genservers.md index 74da0577c3..734c810b7c 100644 --- a/lib/elixir/pages/mix-and-otp/genservers.md +++ b/lib/elixir/pages/mix-and-otp/genservers.md @@ -1,6 +1,6 @@ # Client-server communication with GenServer -In the [previous chapter](../agents.md), we used agents to represent our buckets. In the [introduction to mix](../introduction-to-mix.md), we specified we would like to name each bucket so we can do the following: +In the [previous chapter](agents.md), we used agents to represent our buckets. In the [introduction to mix](introduction-to-mix.md), we specified we would like to name each bucket so we can do the following: ```elixir CREATE shopping diff --git a/lib/elixir/pages/references/syntax-reference.md b/lib/elixir/pages/references/syntax-reference.md index 857fe11070..f9ec122012 100644 --- a/lib/elixir/pages/references/syntax-reference.md +++ b/lib/elixir/pages/references/syntax-reference.md @@ -395,7 +395,7 @@ end All of the constructs above are part of Elixir's syntax and have their own representation as part of the Elixir AST. This section will discuss the remaining constructs that are alternative representations of the constructs above. In other words, the constructs below can be represented in more than one way in your Elixir code and retain AST equivalence. We call this "Optional Syntax". -For a lightweight introduction to Elixir's Optional Syntax, [see this document](optional-syntax.md). Below we continue with a more complete reference. +For a lightweight introduction to Elixir's Optional Syntax, [see this document](../getting-started/optional-syntax.md). Below we continue with a more complete reference. ### Integers in other bases and Unicode code points From 51d23cbba8199936101bda9d57b341105a9efc14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 13 Nov 2023 11:53:44 +0100 Subject: [PATCH 0178/1886] Do not escape \ in uppercase sigils, closes #8989 --- lib/elixir/src/elixir_interpolation.erl | 4 ++-- lib/elixir/test/elixir/code_formatter/literals_test.exs | 4 ++-- lib/elixir/test/elixir/kernel/sigils_test.exs | 1 + 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/elixir/src/elixir_interpolation.erl b/lib/elixir/src/elixir_interpolation.erl index c63370dfe3..e0035559a8 100644 --- a/lib/elixir/src/elixir_interpolation.erl +++ b/lib/elixir/src/elixir_interpolation.erl @@ -60,8 +60,8 @@ extract([$#, ${ | Rest], Buffer, Output, Line, Column, Scope, true, Last) -> {error, {string, Line, Column, "missing interpolation terminator: \"}\"", []}} end; -extract([$\\ | Rest], Buffer, Output, Line, Column, Scope, Interpol, Last) -> - extract_char(Rest, [$\\ | Buffer], Output, Line, Column + 1, Scope, Interpol, Last); +extract([$\\ | Rest], Buffer, Output, Line, Column, Scope, true, Last) -> + extract_char(Rest, [$\\ | Buffer], Output, Line, Column + 1, Scope, true, Last); %% Catch all clause diff --git a/lib/elixir/test/elixir/code_formatter/literals_test.exs b/lib/elixir/test/elixir/code_formatter/literals_test.exs index ec614b6949..4a6325d3be 100644 --- a/lib/elixir/test/elixir/code_formatter/literals_test.exs +++ b/lib/elixir/test/elixir/code_formatter/literals_test.exs @@ -85,8 +85,8 @@ defmodule Code.Formatter.LiteralsTest do end test "without escapes" do - assert_same ~S[:foo] - assert_same ~S[:\\] + assert_same ~s[:foo] + assert_same ~s[:\\\\] end test "with escapes" do diff --git a/lib/elixir/test/elixir/kernel/sigils_test.exs b/lib/elixir/test/elixir/kernel/sigils_test.exs index f81cb0bc41..834db25ed6 100644 --- a/lib/elixir/test/elixir/kernel/sigils_test.exs +++ b/lib/elixir/test/elixir/kernel/sigils_test.exs @@ -29,6 +29,7 @@ defmodule Kernel.SigilsTest do assert ~S(f\no) == "f\\no" assert ~S(foo\)) == "foo)" assert ~S[foo\]] == "foo]" + assert ~S[foo\\]] == "foo\\]" end test "sigil S newline" do From dfba5db6c075d55f4025b9c3d2086e0ba7a108a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 13 Nov 2023 12:28:39 +0100 Subject: [PATCH 0179/1886] Consider start line in MismatchedDelimiterError --- lib/elixir/lib/exception.ex | 37 ++++- lib/elixir/src/elixir_errors.erl | 5 +- .../test/elixir/kernel/diagnostics_test.exs | 155 ++++++++++++++---- 3 files changed, 156 insertions(+), 41 deletions(-) diff --git a/lib/elixir/lib/exception.ex b/lib/elixir/lib/exception.ex index ddb682ffe6..4cd0e2b64a 100644 --- a/lib/elixir/lib/exception.ex +++ b/lib/elixir/lib/exception.ex @@ -916,6 +916,7 @@ defmodule MismatchedDelimiterError do :file, :line, :column, + :line_offset, :end_line, :end_column, :opening_delimiter, @@ -930,6 +931,7 @@ defmodule MismatchedDelimiterError do column: start_column, end_line: end_line, end_column: end_column, + line_offset: line_offset, description: description, opening_delimiter: opening_delimiter, closing_delimiter: _closing_delimiter, @@ -941,13 +943,24 @@ defmodule MismatchedDelimiterError do lines = String.split(snippet, "\n") expected_delimiter = :elixir_tokenizer.terminator(opening_delimiter) - snippet = format_snippet(start_pos, end_pos, description, file, lines, expected_delimiter) + snippet = + format_snippet( + start_pos, + end_pos, + line_offset, + description, + file, + lines, + expected_delimiter + ) + format_message(file, end_line, end_column, snippet) end defp format_snippet( {start_line, _start_column} = start_pos, {end_line, end_column} = end_pos, + line_offset, description, file, lines, @@ -960,12 +973,21 @@ defmodule MismatchedDelimiterError do relevant_lines = if end_line - start_line < @max_lines_shown do - line_range(lines, start_pos, end_pos, padding, max_digits, expected_delimiter) + line_range( + lines, + start_pos, + end_pos, + line_offset, + padding, + max_digits, + expected_delimiter + ) else trimmed_inbetween_lines( lines, start_pos, end_pos, + line_offset, padding, max_digits, expected_delimiter @@ -984,6 +1006,7 @@ defmodule MismatchedDelimiterError do defp format_snippet( {start_line, start_column}, {end_line, end_column}, + line_offset, description, file, lines, @@ -994,7 +1017,7 @@ defmodule MismatchedDelimiterError do general_padding = max(2, max_digits) + 1 padding = n_spaces(general_padding) - line = Enum.fetch!(lines, end_line - 1) + line = Enum.fetch!(lines, end_line - 1 - line_offset) formatted_line = [line_padding(end_line, max_digits), to_string(end_line), " │ ", line] mismatched_closing_line = @@ -1042,14 +1065,15 @@ defmodule MismatchedDelimiterError do lines, {start_line, start_column}, {end_line, end_column}, + line_offset, padding, max_digits, expected_delimiter ) do start_padding = line_padding(start_line, max_digits) end_padding = line_padding(end_line, max_digits) - first_line = Enum.fetch!(lines, start_line - 1) - last_line = Enum.fetch!(lines, end_line - 1) + first_line = Enum.fetch!(lines, start_line - 1 - line_offset) + last_line = Enum.fetch!(lines, end_line - 1 - line_offset) """ #{start_padding}#{start_line} │ #{first_line} @@ -1064,6 +1088,7 @@ defmodule MismatchedDelimiterError do lines, {start_line, start_column}, {end_line, end_column}, + line_offset, padding, max_digits, expected_delimiter @@ -1072,7 +1097,7 @@ defmodule MismatchedDelimiterError do end_line = end_line - 1 lines - |> Enum.slice(start_line..end_line) + |> Enum.slice((start_line - line_offset)..(end_line - line_offset)) |> Enum.zip_with(start_line..end_line, fn line, line_number -> line_number = line_number + 1 start_line = start_line + 1 diff --git a/lib/elixir/src/elixir_errors.erl b/lib/elixir/src/elixir_errors.erl index 2455d69ae5..c4382af4b3 100644 --- a/lib/elixir/src/elixir_errors.erl +++ b/lib/elixir/src/elixir_errors.erl @@ -404,9 +404,10 @@ parse_erl_term(Term) -> Parsed. raise_mismatched_delimiter(Location, File, Input, Message) -> - {InputString, _, _} = Input, + {InputString, StartLine, _} = Input, InputBinary = elixir_utils:characters_to_binary(InputString), - raise('Elixir.MismatchedDelimiterError', Message, [{file, File}, {snippet, InputBinary} | Location]). + KV = [{file, File}, {line_offset, StartLine - 1}, {snippet, InputBinary} | Location], + raise('Elixir.MismatchedDelimiterError', Message, KV). raise_reserved(Location, File, Input, Keyword) -> raise_snippet(Location, File, Input, 'Elixir.SyntaxError', diff --git a/lib/elixir/test/elixir/kernel/diagnostics_test.exs b/lib/elixir/test/elixir/kernel/diagnostics_test.exs index a34dbb4e1d..07b1f1a603 100644 --- a/lib/elixir/test/elixir/kernel/diagnostics_test.exs +++ b/lib/elixir/test/elixir/kernel/diagnostics_test.exs @@ -53,6 +53,28 @@ defmodule Kernel.DiagnosticsTest do """ end + test "same line with offset" do + output = + capture_raise( + """ + [1, 2, 3, 4, 5, 6) + """, + MismatchedDelimiterError, + line: 3 + ) + + assert output == """ + ** (MismatchedDelimiterError) mismatched delimiter found on nofile:3:18: + error: unexpected token: ) + │ + 3 │ [1, 2, 3, 4, 5, 6) + │ │ └ mismatched closing delimiter (expected "]") + │ └ unclosed delimiter + │ + └─ nofile:3:18\ + """ + end + test "two-line span" do output = capture_raise( @@ -76,6 +98,30 @@ defmodule Kernel.DiagnosticsTest do """ end + test "two-line span with offset" do + output = + capture_raise( + """ + [a, b, c + d, f, g} + """, + MismatchedDelimiterError, + line: 3 + ) + + assert output == """ + ** (MismatchedDelimiterError) mismatched delimiter found on nofile:4:9: + error: unexpected token: } + │ + 3 │ [a, b, c + │ └ unclosed delimiter + 4 │ d, f, g} + │ └ mismatched closing delimiter (expected "]") + │ + └─ nofile:4:9\ + """ + end + test "many-line span" do output = capture_raise( @@ -103,7 +149,9 @@ defmodule Kernel.DiagnosticsTest do │ └─ nofile:5:5\ """ + end + test "many-line span with offset" do output = capture_raise( """ @@ -111,20 +159,21 @@ defmodule Kernel.DiagnosticsTest do IO.inspect(2 + 2) + 2 ) """, - MismatchedDelimiterError + MismatchedDelimiterError, + line: 3 ) assert output == """ - ** (MismatchedDelimiterError) mismatched delimiter found on nofile:3:1: + ** (MismatchedDelimiterError) mismatched delimiter found on nofile:5:1: error: unexpected token: ) │ - 1 │ fn always_forget_end -> + 3 │ fn always_forget_end -> │ └ unclosed delimiter - 2 │ IO.inspect(2 + 2) + 2 - 3 │ ) + 4 │ IO.inspect(2 + 2) + 2 + 5 │ ) │ └ mismatched closing delimiter (expected "end") │ - └─ nofile:3:1\ + └─ nofile:5:1\ """ end @@ -290,16 +339,6 @@ defmodule Kernel.DiagnosticsTest do describe "compile-time exceptions" do test "SyntaxError (snippet)" do - expected = """ - ** (SyntaxError) invalid syntax found on nofile:1:17: - error: syntax error before: '*' - │ - 1 │ [1, 2, 3, 4, 5, *] - │ ^ - │ - └─ nofile:1:17\ - """ - output = capture_raise( """ @@ -308,20 +347,39 @@ defmodule Kernel.DiagnosticsTest do SyntaxError ) - assert output == expected + assert output == """ + ** (SyntaxError) invalid syntax found on nofile:1:17: + error: syntax error before: '*' + │ + 1 │ [1, 2, 3, 4, 5, *] + │ ^ + │ + └─ nofile:1:17\ + """ end - test "TokenMissingError (snippet)" do - expected = """ - ** (TokenMissingError) token missing on nofile:1:4: - error: syntax error: expression is incomplete - │ - 1 │ 1 + - │ ^ - │ - └─ nofile:1:4\ - """ + test "SyntaxError (snippet) with offset" do + output = + capture_raise( + """ + [1, 2, 3, 4, 5, *] + """, + SyntaxError, + line: 3 + ) + + assert output == """ + ** (SyntaxError) invalid syntax found on nofile:3:17: + error: syntax error before: '*' + │ + 3 │ [1, 2, 3, 4, 5, *] + │ ^ + │ + └─ nofile:3:17\ + """ + end + test "TokenMissingError (snippet)" do output = capture_raise( """ @@ -330,7 +388,36 @@ defmodule Kernel.DiagnosticsTest do TokenMissingError ) - assert output == expected + assert output == """ + ** (TokenMissingError) token missing on nofile:1:4: + error: syntax error: expression is incomplete + │ + 1 │ 1 + + │ ^ + │ + └─ nofile:1:4\ + """ + end + + test "TokenMissingError (snippet) with offset" do + output = + capture_raise( + """ + 1 + + """, + TokenMissingError, + line: 3 + ) + + assert output == """ + ** (TokenMissingError) token missing on nofile:3:4: + error: syntax error: expression is incomplete + │ + 3 │ 1 + + │ ^ + │ + └─ nofile:3:4\ + """ end test "TokenMissingError (no snippet)" do @@ -419,7 +506,7 @@ defmodule Kernel.DiagnosticsTest do 1 - """, TokenMissingError, - fake_stacktrace + stacktrace: fake_stacktrace ) assert output == expected @@ -448,7 +535,7 @@ defmodule Kernel.DiagnosticsTest do 1 - """, TokenMissingError, - fake_stacktrace + stacktrace: fake_stacktrace ) assert output == expected @@ -1104,14 +1191,16 @@ defmodule Kernel.DiagnosticsTest do end) end - defp capture_raise(source, exception, mock_stacktrace \\ []) do + defp capture_raise(source, exception, opts \\ []) do + {stacktrace, opts} = Keyword.pop(opts, :stacktrace, []) + e = assert_raise exception, fn -> - ast = Code.string_to_quoted!(source, columns: true) + ast = Code.string_to_quoted!(source, [columns: true] ++ opts) Code.eval_quoted(ast) end - Exception.format(:error, e, mock_stacktrace) + Exception.format(:error, e, stacktrace) end defp purge(module) when is_atom(module) do From 65ffb5db92c398840fa3579430b43a609d61769e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 14 Nov 2023 00:51:12 +0100 Subject: [PATCH 0180/1886] Handle error in Macro.to_string/1, closes #13102 --- lib/elixir/lib/code/normalizer.ex | 6 ++++-- lib/elixir/test/elixir/code_normalizer/quoted_ast_test.exs | 6 ++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/lib/elixir/lib/code/normalizer.ex b/lib/elixir/lib/code/normalizer.ex index 03c8a70676..5a1c3c2fe6 100644 --- a/lib/elixir/lib/code/normalizer.ex +++ b/lib/elixir/lib/code/normalizer.ex @@ -349,13 +349,15 @@ defmodule Code.Normalizer do meta end + last = List.last(args) + cond do - Keyword.has_key?(meta, :do) or match?([{{:__block__, _, [:do]}, _} | _], List.last(args)) -> + Keyword.has_key?(meta, :do) or match?([{{:__block__, _, [:do]}, _} | _], last) -> # def foo do :ok end # def foo, do: :ok normalize_kw_blocks(form, meta, args, state) - match?([{:do, _} | _], List.last(args)) -> + match?([{:do, _} | _], last) and Keyword.keyword?(last) -> # Non normalized kw blocks line = state.parent_meta[:line] meta = meta ++ [do: [line: line], end: [line: line]] diff --git a/lib/elixir/test/elixir/code_normalizer/quoted_ast_test.exs b/lib/elixir/test/elixir/code_normalizer/quoted_ast_test.exs index 6cc2fce843..f70cd98f52 100644 --- a/lib/elixir/test/elixir/code_normalizer/quoted_ast_test.exs +++ b/lib/elixir/test/elixir/code_normalizer/quoted_ast_test.exs @@ -229,6 +229,12 @@ defmodule Code.Normalizer.QuotedASTTest do test "invalid block" do assert quoted_to_string({:__block__, [], {:bar, [], []}}) == "{:__block__, [], {:bar, [], []}}" + + assert quoted_to_string({:foo, [], [{:do, :ok}, :not_keyword]}) == + "foo({:do, :ok}, :not_keyword)" + + assert quoted_to_string({:foo, [], [[{:do, :ok}, :not_keyword]]}) == + "foo([{:do, :ok}, :not_keyword])" end test "not in" do From a48c2d5dd2452b72aac4827baf77829e1701fe2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 14 Nov 2023 00:51:34 +0100 Subject: [PATCH 0181/1886] Remove deprecated code and add TODOs --- lib/elixir/lib/code/formatter.ex | 2 ++ lib/elixir/lib/code/normalizer.ex | 1 + .../code_normalizer/quoted_ast_test.exs | 22 ------------------- 3 files changed, 3 insertions(+), 22 deletions(-) diff --git a/lib/elixir/lib/code/formatter.ex b/lib/elixir/lib/code/formatter.ex index eb1d0dd773..1ba43c1077 100644 --- a/lib/elixir/lib/code/formatter.ex +++ b/lib/elixir/lib/code/formatter.ex @@ -289,6 +289,7 @@ defmodule Code.Formatter do end end + # TODO: Remove this clause on Elixir v2.0 once single-quoted charlists are removed defp quoted_to_algebra( {{:., _, [List, :to_charlist]}, meta, [entries]} = quoted, context, @@ -2224,6 +2225,7 @@ defmodule Code.Formatter do (not interpolated?(entries) and eol_or_comments?(meta, state)) end + # TODO: Remove this clause on Elixir v2.0 once single-quoted charlists are removed defp next_break_fits?({{:., _, [List, :to_charlist]}, meta, [[_ | _]]}, _state) do meta[:delimiter] == ~s['''] end diff --git a/lib/elixir/lib/code/normalizer.ex b/lib/elixir/lib/code/normalizer.ex index 5a1c3c2fe6..9715c940a9 100644 --- a/lib/elixir/lib/code/normalizer.ex +++ b/lib/elixir/lib/code/normalizer.ex @@ -96,6 +96,7 @@ defmodule Code.Normalizer do end # Charlists with interpolations + # TODO: Remove this clause on Elixir v2.0 once single-quoted charlists are removed defp do_normalize({{:., dot_meta, [List, :to_charlist]}, call_meta, [parts]} = quoted, state) do if list_interpolated?(parts) do parts = diff --git a/lib/elixir/test/elixir/code_normalizer/quoted_ast_test.exs b/lib/elixir/test/elixir/code_normalizer/quoted_ast_test.exs index f70cd98f52..7e0b894971 100644 --- a/lib/elixir/test/elixir/code_normalizer/quoted_ast_test.exs +++ b/lib/elixir/test/elixir/code_normalizer/quoted_ast_test.exs @@ -688,28 +688,6 @@ defmodule Code.Normalizer.QuotedASTTest do assert quoted_to_string(~c"\x00\x01\x10") == ~S/[0, 1, 16]/ end - test "charlists with interpolations" do - # using string_to_quoted to avoid the formatter fixing the charlists - - assert Code.string_to_quoted!(~S/'one #{2} three'/) |> quoted_to_string(escape: false) == - ~S/~c"one #{2} three"/ - - assert Code.string_to_quoted!(~S/'one #{2} three'/) |> quoted_to_string() == - ~S/~c"one #{2} three"/ - - assert Code.string_to_quoted!(~S/'one\n\'#{2}\'\nthree'/) |> quoted_to_string(escape: false) == - ~s[~c"one\n'\#{2}'\nthree"] - - assert Code.string_to_quoted!(~S/'one\n\'#{2}\'\nthree'/) |> quoted_to_string() == - ~S[~c"one\n'#{2}'\nthree"] - - assert Code.string_to_quoted!(~S/'one\n"#{2}"\nthree'/) |> quoted_to_string(escape: false) == - ~s[~c"one\n\\"\#{2}\\"\nthree"] - - assert Code.string_to_quoted!(~S/'one\n"#{2}"\nthree'/) |> quoted_to_string() == - ~S[~c"one\n\"#{2}\"\nthree"] - end - test "atoms" do assert quoted_to_string(quote(do: :"a\nb\tc"), escape: false) == ~s/:"a\nb\tc"/ assert quoted_to_string(quote(do: :"a\nb\tc")) == ~S/:"a\nb\tc"/ From 8b7ce389de102fed51d042b4b234347c77d76aa3 Mon Sep 17 00:00:00 2001 From: Wojtek Mach Date: Tue, 14 Nov 2023 09:58:36 +0100 Subject: [PATCH 0182/1886] Preserve column when translating typespecs (#13101) --- lib/elixir/lib/code/typespec.ex | 10 ++- lib/elixir/lib/kernel/typespec.ex | 92 +++++++++++++----------- lib/elixir/test/elixir/protocol_test.exs | 8 ++- lib/elixir/test/elixir/typespec_test.exs | 20 ++++-- 4 files changed, 79 insertions(+), 51 deletions(-) diff --git a/lib/elixir/lib/code/typespec.ex b/lib/elixir/lib/code/typespec.ex index cd38949bc7..017b2f05dc 100644 --- a/lib/elixir/lib/code/typespec.ex +++ b/lib/elixir/lib/code/typespec.ex @@ -420,5 +420,13 @@ defmodule Code.Typespec do :error end - defp meta(anno), do: [line: :erl_anno.line(anno)] + defp meta(anno) do + case :erl_anno.location(anno) do + {line, column} -> + [line: line, column: column] + + line when is_integer(line) -> + [line: line] + end + end end diff --git a/lib/elixir/lib/kernel/typespec.ex b/lib/elixir/lib/kernel/typespec.ex index 2e5f88baf2..d95c34fc45 100644 --- a/lib/elixir/lib/kernel/typespec.ex +++ b/lib/elixir/lib/kernel/typespec.ex @@ -385,17 +385,17 @@ defmodule Kernel.Typespec do compile_error(caller, error) end - line = line(meta) + location = location(meta) vars = Keyword.keys(guard) {args, state} = :lists.mapfoldl(&typespec(&1, vars, caller, &2), state, args) {return, state} = typespec(return, vars, caller, state) - spec = {:type, line, :fun, [{:type, line, :product, args}, return]} + spec = {:type, location, :fun, [{:type, location, :product, args}, return]} {spec, state} = case guard_to_constraints(guard, vars, meta, caller, state) do {[], state} -> {spec, state} - {constraints, state} -> {{:type, line, :bounded_fun, [spec, constraints]}, state} + {constraints, state} -> {{:type, location, :bounded_fun, [spec, constraints]}, state} end ensure_no_unused_local_vars!(caller, state.local_vars) @@ -437,7 +437,7 @@ defmodule Kernel.Typespec do defp ensure_not_default(_), do: :ok defp guard_to_constraints(guard, vars, meta, caller, state) do - line = line(meta) + location = location(meta) fun = fn {_name, {:var, _, context}}, {constraints, state} when is_atom(context) -> @@ -445,9 +445,9 @@ defmodule Kernel.Typespec do {name, type}, {constraints, state} -> {spec, state} = typespec(type, vars, caller, state) - constraint = [{:atom, line, :is_subtype}, [{:var, line, name}, spec]] + constraint = [{:atom, location, :is_subtype}, [{:var, location, name}, spec]] state = update_local_vars(state, name) - {[{:type, line, :constraint, constraint} | constraints], state} + {[{:type, location, :constraint, constraint} | constraints], state} end {constraints, state} = :lists.foldl(fun, {[], state}, guard) @@ -456,21 +456,27 @@ defmodule Kernel.Typespec do ## To typespec conversion - defp line(meta) do - Keyword.get(meta, :line, 0) + defp location(meta) do + line = Keyword.get(meta, :line, 0) + + if column = Keyword.get(meta, :column) do + {line, column} + else + line + end end # Handle unions defp typespec({:|, meta, [_, _]} = exprs, vars, caller, state) do exprs = collect_union(exprs) {union, state} = :lists.mapfoldl(&typespec(&1, vars, caller, &2), state, exprs) - {{:type, line(meta), :union, union}, state} + {{:type, location(meta), :union, union}, state} end # Handle binaries defp typespec({:<<>>, meta, []}, _, _, state) do - line = line(meta) - {{:type, line, :binary, [{:integer, line, 0}, {:integer, line, 0}]}, state} + location = location(meta) + {{:type, location, :binary, [{:integer, location, 0}, {:integer, location, 0}]}, state} end defp typespec( @@ -480,14 +486,18 @@ defmodule Kernel.Typespec do state ) when is_atom(ctx1) and is_atom(ctx2) and unit in 1..256 do - line = line(meta) - {{:type, line, :binary, [{:integer, line, 0}, {:integer, line(unit_meta), unit}]}, state} + location = location(meta) + + {{:type, location, :binary, [{:integer, location, 0}, {:integer, location(unit_meta), unit}]}, + state} end defp typespec({:<<>>, meta, [{:"::", size_meta, [{:_, _, ctx}, size]}]}, _, _, state) when is_atom(ctx) and is_integer(size) and size >= 0 do - line = line(meta) - {{:type, line, :binary, [{:integer, line(size_meta), size}, {:integer, line, 0}]}, state} + location = location(meta) + + {{:type, location, :binary, [{:integer, location(size_meta), size}, {:integer, location, 0}]}, + state} end defp typespec( @@ -505,8 +515,8 @@ defmodule Kernel.Typespec do ) when is_atom(ctx1) and is_atom(ctx2) and is_atom(ctx3) and is_integer(size) and size >= 0 and unit in 1..256 do - args = [{:integer, line(size_meta), size}, {:integer, line(unit_meta), unit}] - {{:type, line(meta), :binary, args}, state} + args = [{:integer, location(size_meta), size}, {:integer, location(unit_meta), unit}] + {{:type, location(meta), :binary, args}, state} end defp typespec({:<<>>, _meta, _args}, _vars, caller, _state) do @@ -519,7 +529,7 @@ defmodule Kernel.Typespec do ## Handle maps and structs defp typespec({:map, meta, args}, _vars, _caller, state) when args == [] or is_atom(args) do - {{:type, line(meta), :map, :any}, state} + {{:type, location(meta), :map, :any}, state} end defp typespec({:%{}, meta, fields} = map, vars, caller, state) do @@ -527,17 +537,17 @@ defmodule Kernel.Typespec do {{:required, meta2, [k]}, v}, state -> {arg1, state} = typespec(k, vars, caller, state) {arg2, state} = typespec(v, vars, caller, state) - {{:type, line(meta2), :map_field_exact, [arg1, arg2]}, state} + {{:type, location(meta2), :map_field_exact, [arg1, arg2]}, state} {{:optional, meta2, [k]}, v}, state -> {arg1, state} = typespec(k, vars, caller, state) {arg2, state} = typespec(v, vars, caller, state) - {{:type, line(meta2), :map_field_assoc, [arg1, arg2]}, state} + {{:type, location(meta2), :map_field_assoc, [arg1, arg2]}, state} {k, v}, state -> {arg1, state} = typespec(k, vars, caller, state) {arg2, state} = typespec(v, vars, caller, state) - {{:type, line(meta), :map_field_exact, [arg1, arg2]}, state} + {{:type, location(meta), :map_field_exact, [arg1, arg2]}, state} {:|, _, [_, _]}, _state -> error = @@ -551,7 +561,7 @@ defmodule Kernel.Typespec do end {fields, state} = :lists.mapfoldl(fun, state, fields) - {{:type, line(meta), :map, fields}, state} + {{:type, location(meta), :map, fields}, state} end defp typespec({:%, _, [name, {:%{}, meta, fields}]} = node, vars, caller, state) do @@ -644,7 +654,7 @@ defmodule Kernel.Typespec do {right, state} = typespec(right, vars, caller, state) :ok = validate_range(left, right, caller) - {{:type, line(meta), :range, [left, right]}, state} + {{:type, location(meta), :range, [left, right]}, state} end # Handle special forms @@ -668,7 +678,7 @@ defmodule Kernel.Typespec do pair -> pair end - {{:type, line(meta), :fun, fun_args}, state} + {{:type, location(meta), :fun, fun_args}, state} end # Handle type operator @@ -691,10 +701,10 @@ defmodule Kernel.Typespec do # This may be generating an invalid typespec but we need to generate it # to avoid breaking existing code that was valid but only broke Dialyzer {right, state} = typespec(expr, vars, caller, state) - {{:ann_type, line(meta), [{:var, line(var_meta), var_name}, right]}, state} + {{:ann_type, location(meta), [{:var, location(var_meta), var_name}, right]}, state} {right, state} -> - {{:ann_type, line(meta), [{:var, line(var_meta), var_name}, right]}, state} + {{:ann_type, location(meta), [{:var, location(var_meta), var_name}, right]}, state} end end @@ -723,13 +733,13 @@ defmodule Kernel.Typespec do {left, state} = typespec(left, vars, caller, state) state = %{state | undefined_type_error_enabled?: true} {right, state} = typespec(right, vars, caller, state) - {{:ann_type, line(meta), [left, right]}, state} + {{:ann_type, location(meta), [left, right]}, state} end # Handle unary ops defp typespec({op, meta, [integer]}, _, _, state) when op in [:+, :-] and is_integer(integer) do - line = line(meta) - {{:op, line, op, {:integer, line, integer}}, state} + location = location(meta) + {{:op, location, op, {:integer, location, integer}}, state} end # Handle remote calls in the form of @module_attribute.type. @@ -778,12 +788,12 @@ defmodule Kernel.Typespec do # Handle tuples defp typespec({:tuple, meta, []}, _vars, _caller, state) do - {{:type, line(meta), :tuple, :any}, state} + {{:type, location(meta), :tuple, :any}, state} end defp typespec({:{}, meta, t}, vars, caller, state) when is_list(t) do {args, state} = :lists.mapfoldl(&typespec(&1, vars, caller, &2), state, t) - {{:type, line(meta), :tuple, args}, state} + {{:type, location(meta), :tuple, args}, state} end defp typespec({left, right}, vars, caller, state) do @@ -799,7 +809,7 @@ defmodule Kernel.Typespec do defp typespec({name, meta, atom}, vars, caller, state) when is_atom(atom) do if :lists.member(name, vars) do state = update_local_vars(state, name) - {{:var, line(meta), name}, state} + {{:var, location(meta), name}, state} else typespec({name, meta, []}, vars, caller, state) end @@ -814,7 +824,7 @@ defmodule Kernel.Typespec do IO.warn(warning, caller) {args, state} = :lists.mapfoldl(&typespec(&1, vars, caller, &2), state, args) - {{:type, line(meta), :string, args}, state} + {{:type, location(meta), :string, args}, state} end defp typespec({:nonempty_string, meta, args}, vars, caller, state) do @@ -825,7 +835,7 @@ defmodule Kernel.Typespec do IO.warn(warning, caller) {args, state} = :lists.mapfoldl(&typespec(&1, vars, caller, &2), state, args) - {{:type, line(meta), :nonempty_string, args}, state} + {{:type, location(meta), :nonempty_string, args}, state} end defp typespec({type, _meta, []}, vars, caller, state) when type in [:charlist, :char_list] do @@ -855,7 +865,7 @@ defmodule Kernel.Typespec do defp typespec({:fun, meta, args}, vars, caller, state) do {args, state} = :lists.mapfoldl(&typespec(&1, vars, caller, &2), state, args) - {{:type, line(meta), :fun, args}, state} + {{:type, location(meta), :fun, args}, state} end defp typespec({:..., _meta, _args}, _vars, caller, _state) do @@ -872,7 +882,7 @@ defmodule Kernel.Typespec do case :erl_internal.is_type(name, arity) do true -> - {{:type, line(meta), name, args}, state} + {{:type, location(meta), name, args}, state} false -> if state.undefined_type_error_enabled? and @@ -890,7 +900,7 @@ defmodule Kernel.Typespec do %{state | used_type_pairs: [{name, arity} | state.used_type_pairs]} end - {{:user_type, line(meta), name, args}, state} + {{:user_type, location(meta), name, args}, state} end end @@ -963,7 +973,7 @@ defmodule Kernel.Typespec do defp remote_type({remote, meta, name, args}, vars, caller, state) do {args, state} = :lists.mapfoldl(&typespec(&1, vars, caller, &2), state, args) - {{:remote_type, line(meta), [remote, name, args]}, state} + {{:remote_type, location(meta), [remote, name, args]}, state} end defp collect_union({:|, _, [a, b]}), do: [a | collect_union(b)] @@ -996,16 +1006,16 @@ defmodule Kernel.Typespec do end defp fn_args(meta, [{:..., _, _}], _vars, _caller, state) do - {{:type, line(meta), :any}, state} + {{:type, location(meta), :any}, state} end defp fn_args(meta, args, vars, caller, state) do {args, state} = :lists.mapfoldl(&typespec(&1, vars, caller, &2), state, args) - {{:type, line(meta), :product, args}, state} + {{:type, location(meta), :product, args}, state} end defp variable({name, meta, args}) when is_atom(name) and is_atom(args) do - {:var, line(meta), name} + {:var, location(meta), name} end defp variable(expr), do: expr diff --git a/lib/elixir/test/elixir/protocol_test.exs b/lib/elixir/test/elixir/protocol_test.exs index e3754578a8..9931917bf1 100644 --- a/lib/elixir/test/elixir/protocol_test.exs +++ b/lib/elixir/test/elixir/protocol_test.exs @@ -154,8 +154,12 @@ defmodule ProtocolTest do end test "protocol defines callbacks" do - assert [{:type, 13, :fun, args}] = get_callbacks(@sample_binary, :ok, 1) - assert args == [{:type, 13, :product, [{:user_type, 13, :t, []}]}, {:type, 13, :boolean, []}] + assert [{:type, {13, 19}, :fun, args}] = get_callbacks(@sample_binary, :ok, 1) + + assert args == [ + {:type, {13, 19}, :product, [{:user_type, {13, 16}, :t, []}]}, + {:type, {13, 22}, :boolean, []} + ] assert [{:type, 23, :fun, args}] = get_callbacks(@with_any_binary, :ok, 1) assert args == [{:type, 23, :product, [{:user_type, 23, :t, []}]}, {:type, 23, :term, []}] diff --git a/lib/elixir/test/elixir/typespec_test.exs b/lib/elixir/test/elixir/typespec_test.exs index f08079b42a..347670981e 100644 --- a/lib/elixir/test/elixir/typespec_test.exs +++ b/lib/elixir/test/elixir/typespec_test.exs @@ -1582,23 +1582,29 @@ defmodule TypespecTest do """) [type: type] = types(:typespec_test_mod) - line = 5 assert Code.Typespec.type_to_quoted(type) == - {:"::", [], [{:t, [], [{:x, [line: line], nil}]}, [{:x, [line: line], nil}]]} + {:"::", [], + [ + {:t, [], [{:x, meta(5, 9), nil}]}, + [{:x, meta(5, 20), nil}] + ]} [{{:f, 1}, [spec]}] = specs(:typespec_test_mod) - line = 7 assert Code.Typespec.spec_to_quoted(:f, spec) == - {:when, [line: line], + {:when, meta(7, 8), [ - {:"::", [line: line], - [{:f, [line: line], [{:x, [line: line], nil}]}, {:x, [line: line], nil}]}, - [x: {:var, [line: line], nil}] + {:"::", meta(7, 8), + [{:f, meta(7, 8), [{:x, meta(7, 9), nil}]}, {:x, meta(7, 15), nil}]}, + [x: {:var, meta(7, 8), nil}] ]} end + defp meta(line, column) do + [line: line, column: column] + end + defp erlc(context, module, code) do dir = context.tmp_dir From a6b21f5e7d446731922e9191d218ef771818f6af Mon Sep 17 00:00:00 2001 From: dawe Date: Wed, 15 Nov 2023 00:02:27 +0100 Subject: [PATCH 0183/1886] Improve wording of @doc for expand (#13105) --- lib/iex/lib/iex/autocomplete.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/iex/lib/iex/autocomplete.ex b/lib/iex/lib/iex/autocomplete.ex index efa46b9773..084738fef9 100644 --- a/lib/iex/lib/iex/autocomplete.ex +++ b/lib/iex/lib/iex/autocomplete.ex @@ -38,7 +38,7 @@ defmodule IEx.Autocomplete do @doc """ The expansion logic. - Some of the expansion has to be use the current shell + Some of the expansion has to use the current shell environment, which is found via the broker. """ def expand(code, shell \\ IEx.Broker.shell()) do From 856da799a30dbf5e24f347316dff7bdd76507015 Mon Sep 17 00:00:00 2001 From: Jean Klingler Date: Wed, 15 Nov 2023 20:50:10 +0900 Subject: [PATCH 0184/1886] Auto infer size of matched variable in bitstrings (#13106) --- lib/elixir/lib/kernel.ex | 3 -- lib/elixir/src/elixir_bitstring.erl | 21 ++++++++--- lib/elixir/test/elixir/kernel/binary_test.exs | 36 +++++++++++-------- 3 files changed, 38 insertions(+), 22 deletions(-) diff --git a/lib/elixir/lib/kernel.ex b/lib/elixir/lib/kernel.ex index 0ca281d3e5..18e0249871 100644 --- a/lib/elixir/lib/kernel.ex +++ b/lib/elixir/lib/kernel.ex @@ -2064,9 +2064,6 @@ defmodule Kernel do {var, _, nil} when is_atom(var) -> invalid_concat_left_argument_error(Atom.to_string(var)) - {:^, _, [{var, _, nil}]} when is_atom(var) -> - invalid_concat_left_argument_error("^#{Atom.to_string(var)}") - _ -> expanded_arg end diff --git a/lib/elixir/src/elixir_bitstring.erl b/lib/elixir/src/elixir_bitstring.erl index 96d4cd92c4..1d85535d06 100644 --- a/lib/elixir/src/elixir_bitstring.erl +++ b/lib/elixir/src/elixir_bitstring.erl @@ -35,7 +35,12 @@ expand(BitstrMeta, Fun, [{'::', Meta, [Left, Right]} | T], Acc, S, E, Alignment, MatchOrRequireSize = RequireSize or is_match_size(T, EL), EType = expr_type(ELeft), - {ERight, EAlignment, SS, ES} = expand_specs(EType, Meta, Right, SL, OriginalS, EL, MatchOrRequireSize), + ExpectSize = case ELeft of + {'^', _, [{_, _, _}]} -> {infer, ELeft}; + _ when MatchOrRequireSize -> required; + _ -> optional + end, + {ERight, EAlignment, SS, ES} = expand_specs(EType, Meta, Right, SL, OriginalS, EL, ExpectSize), EAcc = concat_or_prepend_bitstring(Meta, ELeft, ERight, Acc, ES, MatchOrRequireSize), expand(BitstrMeta, Fun, T, EAcc, {SS, OriginalS}, ES, alignment(Alignment, EAlignment), RequireSize); @@ -147,7 +152,7 @@ expand_expr(Meta, Component, Fun, S, E) -> %% Expands and normalizes types of a bitstring. -expand_specs(ExprType, Meta, Info, S, OriginalS, E, RequireSize) -> +expand_specs(ExprType, Meta, Info, S, OriginalS, E, ExpectSize) -> Default = #{size => default, unit => default, @@ -158,11 +163,17 @@ expand_specs(ExprType, Meta, Info, S, OriginalS, E, RequireSize) -> expand_each_spec(Meta, unpack_specs(Info, []), Default, S, OriginalS, E), MergedType = type(Meta, ExprType, Type, E), - validate_size_required(Meta, RequireSize, ExprType, MergedType, Size, ES), + validate_size_required(Meta, ExpectSize, ExprType, MergedType, Size, ES), SizeAndUnit = size_and_unit(Meta, ExprType, Size, Unit, ES), Alignment = compute_alignment(MergedType, Size, Unit), - [H | T] = build_spec(Meta, Size, Unit, MergedType, Endianness, Sign, SizeAndUnit, ES), + MaybeInferredSize = case {ExpectSize, MergedType, SizeAndUnit} of + {{infer, PinnedVar}, binary, []} -> [{size, Meta, [{{'.', Meta, [erlang, byte_size]}, Meta, [PinnedVar]}]}]; + {{infer, PinnedVar}, bitstring, []} -> [{size, Meta, [{{'.', Meta, [erlang, bit_size]}, Meta, [PinnedVar]}]}]; + _ -> SizeAndUnit + end, + + [H | T] = build_spec(Meta, Size, Unit, MergedType, Endianness, Sign, MaybeInferredSize, ES), {lists:foldl(fun(I, Acc) -> {'-', Meta, [Acc, I]} end, H, T), Alignment, SS, ES}. type(_, default, default, _) -> @@ -276,7 +287,7 @@ validate_spec_arg(Meta, unit, Value, _S, _OriginalS, E) when not is_integer(Valu validate_spec_arg(_Meta, _Key, _Value, _S, _OriginalS, _E) -> ok. -validate_size_required(Meta, true, default, Type, default, E) when Type == binary; Type == bitstring -> +validate_size_required(Meta, required, default, Type, default, E) when Type == binary; Type == bitstring -> function_error(Meta, E, ?MODULE, unsized_binary), ok; validate_size_required(_, _, _, _, _, _) -> diff --git a/lib/elixir/test/elixir/kernel/binary_test.exs b/lib/elixir/test/elixir/kernel/binary_test.exs index 7f2eb9a35e..fd26ad3aee 100644 --- a/lib/elixir/test/elixir/kernel/binary_test.exs +++ b/lib/elixir/test/elixir/kernel/binary_test.exs @@ -128,20 +128,6 @@ defmodule Kernel.BinaryTest do assert_raise ArgumentError, message, fn -> Code.eval_string(~s["a" <> b <> "c" = "abc"]) end - - assert_raise ArgumentError, message, fn -> - Code.eval_string(~s[ - a = "a" - ^a <> "b" = "ab" - ]) - end - - assert_raise ArgumentError, message, fn -> - Code.eval_string(~s[ - b = "b" - "a" <> ^b <> "c" = "abc" - ]) - end end test "hex" do @@ -269,6 +255,28 @@ defmodule Kernel.BinaryTest do assert <<1::size((^foo).bar)>> = <<1::5>> end + test "automatic size computation of matched bitsyntax variable" do + var = "foo" + <<^var::binary, rest::binary>> = "foobar" + assert rest == "bar" + + <<^var::bytes, rest::bytes>> = "foobar" + assert rest == "bar" + + ^var <> rest = "foobar" + assert rest == "bar" + + var = <<0, 1>> + <<^var::bitstring, rest::bitstring>> = <<0, 1, 2, 3>> + assert rest == <<2, 3>> + + <<^var::bits, rest::bits>> = <<0, 1, 2, 3>> + assert rest == <<2, 3>> + + ^var <> rest = <<0, 1, 2, 3>> + assert rest == <<2, 3>> + end + defmacro signed_16 do quote do big - signed - integer - unit(16) From e58e267586375b4525723b079647bb0803a24242 Mon Sep 17 00:00:00 2001 From: Jean Klingler Date: Wed, 15 Nov 2023 21:35:25 +0900 Subject: [PATCH 0185/1886] Formatter keeps quotes in atom keys (#13108) --- lib/elixir/lib/code/formatter.ex | 9 +++++---- .../test/elixir/code_formatter/containers_test.exs | 1 + 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/elixir/lib/code/formatter.ex b/lib/elixir/lib/code/formatter.ex index 1ba43c1077..e8fd2a1a69 100644 --- a/lib/elixir/lib/code/formatter.ex +++ b/lib/elixir/lib/code/formatter.ex @@ -521,14 +521,15 @@ defmodule Code.Formatter do {string(~S{"..//":}), state} {:__block__, _, [atom]} when is_atom(atom) -> - key = + iodata = if Macro.classify_atom(atom) in [:identifier, :unquoted] do - IO.iodata_to_binary([Atom.to_string(atom), ?:]) + [Atom.to_string(atom), ?:] else - IO.iodata_to_binary([?", Atom.to_string(atom), ?", ?:]) + [?", atom |> Atom.to_string() |> String.replace("\"", "\\\""), ?", ?:] end - {string(key) |> color(:atom, state.inspect_opts), state} + {iodata |> IO.iodata_to_binary() |> string() |> color(:atom, state.inspect_opts), + state} {{:., _, [:erlang, :binary_to_atom]}, _, [{:<<>>, _, entries}, :utf8]} -> interpolation_to_algebra(entries, @double_quote, state, "\"", "\":") diff --git a/lib/elixir/test/elixir/code_formatter/containers_test.exs b/lib/elixir/test/elixir/code_formatter/containers_test.exs index 9f3b3bc5d5..8e7b5e430d 100644 --- a/lib/elixir/test/elixir/code_formatter/containers_test.exs +++ b/lib/elixir/test/elixir/code_formatter/containers_test.exs @@ -203,6 +203,7 @@ defmodule Code.Formatter.ContainersTest do assert_same ~S(["\w": 1, "\\w": 2]) assert_same ~S(["Elixir.Foo": 1, "Elixir.Bar": 2]) assert_format ~S(["Foo": 1, "Bar": 2]), ~S([Foo: 1, Bar: 2]) + assert_same ~S(["with \"scare quotes\"": 1]) end test "with operators keyword lists" do From 47fcb5f902ad01565e088848efe93a672577645d Mon Sep 17 00:00:00 2001 From: Stevo-S <4288648+Stevo-S@users.noreply.github.com> Date: Thu, 16 Nov 2023 00:41:45 +0300 Subject: [PATCH 0186/1886] Fix a broken link to the "Guards" section (#13112) of the "Patterns and Guards" references page. The link is from the "Case, Cond and If" getting-started page. --- lib/elixir/pages/getting-started/case-cond-and-if.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/pages/getting-started/case-cond-and-if.md b/lib/elixir/pages/getting-started/case-cond-and-if.md index 431ff2786e..a140665326 100644 --- a/lib/elixir/pages/getting-started/case-cond-and-if.md +++ b/lib/elixir/pages/getting-started/case-cond-and-if.md @@ -65,7 +65,7 @@ iex> case :ok do ** (CaseClauseError) no case clause matching: :ok ``` -The documentation for the `Kernel` module lists all available guards in its sidebar. You can also consult the complete [Patterns and Guards](../references/patterns-and-guards.html#guards) reference for in-depth documentation. +The documentation for the `Kernel` module lists all available guards in its sidebar. You can also consult the complete [Patterns and Guards](../references/patterns-and-guards.md#guards) reference for in-depth documentation. ## cond From ac7d47e32b1341c428c3d399fa67fae41ae43f53 Mon Sep 17 00:00:00 2001 From: Artem Solomatin Date: Thu, 16 Nov 2023 02:05:31 +0300 Subject: [PATCH 0187/1886] Few text improvements in Code (#13113) --- lib/elixir/lib/code.ex | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/elixir/lib/code.ex b/lib/elixir/lib/code.ex index 2d6a0d02f1..67df43483e 100644 --- a/lib/elixir/lib/code.ex +++ b/lib/elixir/lib/code.ex @@ -1100,7 +1100,7 @@ defmodule Code do For example, `"null byte\\t\\x00"` will be kept as is instead of being converted to a bitstring literal. Note if you set this option to false, the resulting AST is no longer valid, but it can be useful to analyze/transform - source code, typically in in combination with `quoted_to_algebra/2`. + source code, typically in combination with `quoted_to_algebra/2`. Defaults to `true`. * `:existing_atoms_only` - when `true`, raises an error @@ -1213,7 +1213,7 @@ defmodule Code do Comments are maps with the following fields: - * `:line` - The line number the source code + * `:line` - The line number of the source code * `:text` - The full text of the comment, including the leading `#` @@ -1693,7 +1693,7 @@ defmodule Code do Returns a list of tuples where the first element is the module name and the second one is its bytecode (as a binary). A `file` can be - given as second argument which will be used for reporting warnings + given as a second argument which will be used for reporting warnings and errors. **Warning**: `string` can be any Elixir code and code can be executed with From 0352aba8c750fb7a4e406816493e89edadf7150d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 16 Nov 2023 09:50:32 +0100 Subject: [PATCH 0188/1886] Improve capture_log docs --- lib/ex_unit/lib/ex_unit.ex | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/ex_unit/lib/ex_unit.ex b/lib/ex_unit/lib/ex_unit.ex index 7a0ad68d7d..72a7a467a7 100644 --- a/lib/ex_unit/lib/ex_unit.ex +++ b/lib/ex_unit/lib/ex_unit.ex @@ -232,9 +232,10 @@ defmodule ExUnit do * `:capture_log` - if ExUnit should default to keeping track of log messages and print them on test failure. Can be overridden for individual tests via `@tag capture_log: false`. This can also be configured to a specific level - with `capture_log: [level: LEVEL]`, for example: - `capture_log: [level: :emergency]` to prevent any output from test failures. - Defaults to `false`; + with `capture_log: [level: LEVEL]`, to capture all logs but only keep those + above `LEVEL`. Note that `on_exit` and `setup_all` callbacks may still log, + as they run outside of the testing process. To silent those, you can use + `ExUnit.CaptureLog.capture_log/2` or consider disabling logging altogether. * `:colors` - a keyword list of color options to be used by some formatters: * `:enabled` - boolean option to enable colors, defaults to `IO.ANSI.enabled?/0`; From 9415b3779eb3a481ef67487b3c53367c9558d12f Mon Sep 17 00:00:00 2001 From: Wojtek Mach Date: Thu, 16 Nov 2023 13:59:21 +0100 Subject: [PATCH 0189/1886] Update `Mix.Config` mentions (#13115) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ExDoc main emitted these warnings on Elixir main: ``` warning: documentation references module "Mix.Config" but it is hidden │ 49 │ `Mix.Config`, which was specific to Mix and has been deprecated. │ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ │ └─ lib/elixir/lib/config.ex:49: Config (module) warning: documentation references module "Mix.Config" but it is hidden │ 51 │ You can leverage `Config` instead of `Mix.Config` in three steps. The first │ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ │ └─ lib/elixir/lib/config.ex:51: Config (module) ``` --- lib/elixir/lib/config.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/elixir/lib/config.ex b/lib/elixir/lib/config.ex index cc39e43baa..abde56cb1d 100644 --- a/lib/elixir/lib/config.ex +++ b/lib/elixir/lib/config.ex @@ -46,9 +46,9 @@ defmodule Config do ## Migrating from `use Mix.Config` The `Config` module in Elixir was introduced in v1.9 as a replacement to - `Mix.Config`, which was specific to Mix and has been deprecated. + `use Mix.Config`, which was specific to Mix and has been deprecated. - You can leverage `Config` instead of `Mix.Config` in three steps. The first + You can leverage `Config` instead of `use Mix.Config` in three steps. The first step is to replace `use Mix.Config` at the top of your config files by `import Config`. From df66d88a9bedadb38afc0e831c501bf6f3df502e Mon Sep 17 00:00:00 2001 From: Wojtek Mach Date: Thu, 16 Nov 2023 14:00:20 +0100 Subject: [PATCH 0190/1886] Update Mix.Task.preferred_cli_env/1 docs (#13114) --- lib/mix/lib/mix/task.ex | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/mix/lib/mix/task.ex b/lib/mix/lib/mix/task.ex index fb3b1a2448..19440f297c 100644 --- a/lib/mix/lib/mix/task.ex +++ b/lib/mix/lib/mix/task.ex @@ -256,6 +256,9 @@ defmodule Mix.Task do Mix.ProjectStack.recursing() != nil end + @doc """ + Available for backwards compatibility. + """ @deprecated "Configure the environment in your mix.exs" defdelegate preferred_cli_env(task), to: Mix.CLI From da671deba400be8835aec409aedd1a8dabb207b1 Mon Sep 17 00:00:00 2001 From: Artem Solomatin Date: Sat, 18 Nov 2023 17:17:36 +0300 Subject: [PATCH 0191/1886] Update Time.add message about units (#13121) --- lib/elixir/lib/calendar/time.ex | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/elixir/lib/calendar/time.ex b/lib/elixir/lib/calendar/time.ex index 31b5a91ac0..04c9990613 100644 --- a/lib/elixir/lib/calendar/time.ex +++ b/lib/elixir/lib/calendar/time.ex @@ -515,6 +515,12 @@ defmodule Time do def add(%{calendar: calendar, microsecond: {_, precision}} = time, amount_to_add, unit) when is_integer(amount_to_add) do + if not is_integer(unit) and + unit not in ~w(second millisecond microsecond nanosecond)a do + raise ArgumentError, + "unsupported time unit. Expected :hour, :minute, :second, :millisecond, :microsecond, :nanosecond, or a positive integer, got #{inspect(unit)}" + end + amount_to_add = System.convert_time_unit(amount_to_add, unit, :microsecond) total = time_to_microseconds(time) + amount_to_add parts = Integer.mod(total, @parts_per_day) From b89d8559b27d01e1c3c82547f26ce52d91a0229b Mon Sep 17 00:00:00 2001 From: Travis Vander Hoop Date: Sat, 18 Nov 2023 13:08:19 -0700 Subject: [PATCH 0192/1886] Update CHANGELOG.md (#13124) Update version headings in changelog --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8551be980..af9adf7d46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -# Changelog for Elixir v1.16 +# Changelog for Elixir v1.17 ## v1.17.0-dev @@ -21,6 +21,6 @@ * [ExUnit.Case] `register_test/4` is deprecated in favor of `register_test/6` for performance reasons -## v1.15 +## v1.16 The CHANGELOG for v1.16 releases can be found [in the v1.16 branch](https://github.com/elixir-lang/elixir/blob/v1.16/CHANGELOG.md). From 15e17c1feb6f12ad8e045382bb760db07570ec43 Mon Sep 17 00:00:00 2001 From: Juan Barrios <03juan@users.noreply.github.com> Date: Sat, 18 Nov 2023 22:37:18 +0200 Subject: [PATCH 0193/1886] Raise in Time.add/3 for non-positive integer (#13122) --- lib/elixir/lib/calendar/time.ex | 5 +++-- lib/elixir/test/elixir/calendar/time_test.exs | 10 ++++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/lib/elixir/lib/calendar/time.ex b/lib/elixir/lib/calendar/time.ex index 04c9990613..d7a16307a3 100644 --- a/lib/elixir/lib/calendar/time.ex +++ b/lib/elixir/lib/calendar/time.ex @@ -515,10 +515,11 @@ defmodule Time do def add(%{calendar: calendar, microsecond: {_, precision}} = time, amount_to_add, unit) when is_integer(amount_to_add) do - if not is_integer(unit) and + if (is_integer(unit) and unit < 1) or unit not in ~w(second millisecond microsecond nanosecond)a do raise ArgumentError, - "unsupported time unit. Expected :hour, :minute, :second, :millisecond, :microsecond, :nanosecond, or a positive integer, got #{inspect(unit)}" + "unsupported time unit. Expected :hour, :minute, :second, :millisecond, " <> + ":microsecond, :nanosecond, or a positive integer, got #{inspect(unit)}" end amount_to_add = System.convert_time_unit(amount_to_add, unit, :microsecond) diff --git a/lib/elixir/test/elixir/calendar/time_test.exs b/lib/elixir/test/elixir/calendar/time_test.exs index 262ad2232f..41a36aae71 100644 --- a/lib/elixir/test/elixir/calendar/time_test.exs +++ b/lib/elixir/test/elixir/calendar/time_test.exs @@ -90,4 +90,14 @@ defmodule TimeTest do assert Time.truncate(~T[01:01:01.123456], :second) == ~T[01:01:01] end + + test "add/3" do + time = ~T[00:00:00.0] + + assert Time.add(time, 1, :hour) == ~T[01:00:00.0] + + assert_raise ArgumentError, ~r/Expected :hour, :minute, :second/, fn -> + Time.add(time, 1, 0) + end + end end From 00aa2ee091bed99998603fed4193a1309250ce3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20=C5=81=C4=99picki?= Date: Sun, 19 Nov 2023 09:37:22 +0100 Subject: [PATCH 0194/1886] Fix Time.add/3 for integer unit (#13125) --- lib/elixir/lib/calendar/time.ex | 8 ++++++-- lib/elixir/test/elixir/calendar/time_test.exs | 2 ++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/lib/elixir/lib/calendar/time.ex b/lib/elixir/lib/calendar/time.ex index d7a16307a3..43ef7a6e2d 100644 --- a/lib/elixir/lib/calendar/time.ex +++ b/lib/elixir/lib/calendar/time.ex @@ -515,8 +515,12 @@ defmodule Time do def add(%{calendar: calendar, microsecond: {_, precision}} = time, amount_to_add, unit) when is_integer(amount_to_add) do - if (is_integer(unit) and unit < 1) or - unit not in ~w(second millisecond microsecond nanosecond)a do + valid? = + if is_integer(unit), + do: unit > 0, + else: unit in ~w(second millisecond microsecond nanosecond)a + + unless valid? do raise ArgumentError, "unsupported time unit. Expected :hour, :minute, :second, :millisecond, " <> ":microsecond, :nanosecond, or a positive integer, got #{inspect(unit)}" diff --git a/lib/elixir/test/elixir/calendar/time_test.exs b/lib/elixir/test/elixir/calendar/time_test.exs index 41a36aae71..125ff55e6b 100644 --- a/lib/elixir/test/elixir/calendar/time_test.exs +++ b/lib/elixir/test/elixir/calendar/time_test.exs @@ -96,6 +96,8 @@ defmodule TimeTest do assert Time.add(time, 1, :hour) == ~T[01:00:00.0] + assert Time.add(time, 1, 10) == ~T[00:00:00.100000] + assert_raise ArgumentError, ~r/Expected :hour, :minute, :second/, fn -> Time.add(time, 1, 0) end From 82a03f6b045f18fbfe9738fefca0cfe43838731a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20M=C3=BCller?= Date: Mon, 20 Nov 2023 04:00:46 -0300 Subject: [PATCH 0195/1886] Typo fix (#13126) --- lib/elixir/lib/macro.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/lib/macro.ex b/lib/elixir/lib/macro.ex index c2dda7fa3c..d8e4f39ef3 100644 --- a/lib/elixir/lib/macro.ex +++ b/lib/elixir/lib/macro.ex @@ -367,7 +367,7 @@ defmodule Macro do Note the arguments are not unique. If you later on want to access the same variables, you can invoke this function with the same inputs. Use `generate_unique_arguments/2` to - generate a unique arguments that can't be overridden. + generate unique arguments that can't be overridden. ## Examples From 84283f7dc7946cbc5cee453a59e70086c6673ba3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 20 Nov 2023 21:19:40 +0800 Subject: [PATCH 0196/1886] Do not assume there is a $HOME, closes #13127 --- lib/iex/lib/iex/evaluator.ex | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/iex/lib/iex/evaluator.ex b/lib/iex/lib/iex/evaluator.ex index 0ffab18843..ac1fbd78a7 100644 --- a/lib/iex/lib/iex/evaluator.ex +++ b/lib/iex/lib/iex/evaluator.ex @@ -253,9 +253,10 @@ defmodule IEx.Evaluator do if path do [path] else - Enum.map([".", System.get_env("IEX_HOME", "~")], fn dir -> - dir |> Path.join(".iex.exs") |> Path.expand() - end) + # Do not assume there is a $HOME + for dir <- [".", System.get_env("IEX_HOME") || System.user_home()], + dir != nil, + do: dir |> Path.join(".iex.exs") |> Path.expand() end path = Enum.find(candidates, &File.regular?/1) From a5ac8b2b2ca5093aebfa68db857e464297d2f72c Mon Sep 17 00:00:00 2001 From: Damir Vandic Date: Tue, 21 Nov 2023 09:26:04 +0100 Subject: [PATCH 0197/1886] Add additional docs to Kernel.if/2 (#13120) --- lib/elixir/lib/kernel.ex | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/elixir/lib/kernel.ex b/lib/elixir/lib/kernel.ex index 18e0249871..3383a74ad2 100644 --- a/lib/elixir/lib/kernel.ex +++ b/lib/elixir/lib/kernel.ex @@ -3777,7 +3777,9 @@ defmodule Kernel do Provides an `if/2` macro. This macro expects the first argument to be a condition and the second - argument to be a keyword list. + argument to be a keyword list. Similar to `case/2`, any assignment in + the condition will be available on both clauses, as well as after the + `if` expression. ## One-liner examples From e258ccc27992d5e575f161f6701d99bf2904ab17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 22 Nov 2023 08:54:39 +0800 Subject: [PATCH 0198/1886] Remove warning on non-ambiguous nullary remote call --- lib/elixir/src/elixir_expand.erl | 18 ++++++------------ lib/elixir/test/elixir/kernel/warning_test.exs | 12 ------------ 2 files changed, 6 insertions(+), 24 deletions(-) diff --git a/lib/elixir/src/elixir_expand.erl b/lib/elixir/src/elixir_expand.erl index c6399ee613..16e5a54d61 100644 --- a/lib/elixir/src/elixir_expand.erl +++ b/lib/elixir/src/elixir_expand.erl @@ -402,16 +402,12 @@ expand({Atom, Meta, Args}, S, E) when is_atom(Atom), is_list(Meta), is_list(Args %% Remote calls -expand({{'.', DotMeta, [Left, Right]}, Meta, Args} = Expr, S, E) +expand({{'.', DotMeta, [Left, Right]}, Meta, Args}, S, E) when (is_tuple(Left) orelse is_atom(Left)), is_atom(Right), is_list(Meta), is_list(Args) -> {ELeft, SL, EL} = expand(Left, elixir_env:prepare_write(S), E), - NoParens = lists:keyfind(no_parens, 1, Meta), - - (is_atom(ELeft) and (Args =:= []) and (NoParens =:= {no_parens, true})) andalso - elixir_errors:file_warn(Meta, E, ?MODULE, {remote_nullary_no_parens, Expr}), elixir_dispatch:dispatch_require(Meta, ELeft, Right, Args, S, EL, fun(AR, AF, AA) -> - expand_remote(AR, DotMeta, AF, Meta, NoParens, AA, S, SL, EL) + expand_remote(AR, DotMeta, AF, Meta, AA, S, SL, EL) end); %% Anonymous calls @@ -858,12 +854,13 @@ expand_local(Meta, Name, Args, _S, #{function := nil} = E) -> %% Remote -expand_remote(Receiver, DotMeta, Right, Meta, NoParens, Args, S, SL, #{context := Context} = E) when is_atom(Receiver) or is_tuple(Receiver) -> +expand_remote(Receiver, DotMeta, Right, Meta, Args, S, SL, #{context := Context} = E) + when is_atom(Receiver) or is_tuple(Receiver) -> assert_no_clauses(Right, Meta, Args, E), if Context =:= guard, is_tuple(Receiver) -> - (NoParens /= {no_parens, true}) andalso + (lists:keyfind(no_parens, 1, Meta) /= {no_parens, true}) andalso function_error(Meta, E, ?MODULE, {parens_map_lookup, Receiver, Right, guard_context(S)}), {{{'.', DotMeta, [Receiver, Right]}, Meta, []}, SL, E}; @@ -881,7 +878,7 @@ expand_remote(Receiver, DotMeta, Right, Meta, NoParens, Args, S, SL, #{context : file_error(Meta, E, elixir_rewrite, Error) end end; -expand_remote(Receiver, DotMeta, Right, Meta, _NoParens, Args, _, _, E) -> +expand_remote(Receiver, DotMeta, Right, Meta, Args, _, _, E) -> Call = {{'.', DotMeta, [Receiver, Right]}, Meta, Args}, file_error(Meta, E, ?MODULE, {invalid_call, Call}). @@ -1171,9 +1168,6 @@ assert_no_underscore_clause_in_cond(_Other, _E) -> guard_context(#elixir_ex{prematch={_, _, {bitsize, _}}}) -> "bitstring size specifier"; guard_context(_) -> "guards". -format_error({remote_nullary_no_parens, Expr}) -> - String = 'Elixir.String':replace_suffix('Elixir.Macro':to_string(Expr), <<"()">>, <<>>), - io_lib:format("parentheses are required for function calls with no arguments, got: ~ts", [String]); format_error(invalid_match_on_zero_float) -> "pattern matching on 0.0 is equivalent to matching only on +0.0 from Erlang/OTP 27+. Instead you must match on +0.0 or -0.0"; format_error({useless_literal, Term}) -> diff --git a/lib/elixir/test/elixir/kernel/warning_test.exs b/lib/elixir/test/elixir/kernel/warning_test.exs index d91cf99a10..518da3c4c8 100644 --- a/lib/elixir/test/elixir/kernel/warning_test.exs +++ b/lib/elixir/test/elixir/kernel/warning_test.exs @@ -1175,18 +1175,6 @@ defmodule Kernel.WarningTest do purge(Sample) end - test "parens on nullary remote call" do - assert_warn_eval( - [ - "nofile:1:8", - "parentheses are required for function calls with no arguments, got: System.version" - ], - "System.version" - ) - after - purge(Sample) - end - test "parens with module attribute" do assert_warn_eval( [ From 1d0fc3ad252046f902909e44365e0445b2cbeaba Mon Sep 17 00:00:00 2001 From: Artem Solomatin Date: Wed, 22 Nov 2023 03:55:25 +0300 Subject: [PATCH 0199/1886] Small text improvement for operators reference (#13131) --- lib/elixir/pages/references/operators.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/pages/references/operators.md b/lib/elixir/pages/references/operators.md index 41642f0a9f..3cf97835bd 100644 --- a/lib/elixir/pages/references/operators.md +++ b/lib/elixir/pages/references/operators.md @@ -132,7 +132,7 @@ The following is a table of all the operators that Elixir is capable of parsing, * `+++` * `---` -The following operators are used by the `Bitwise` module when imported: [`&&&`](`Bitwise.&&&/2`), [`<<<`](`Bitwise.<<>>`](`Bitwise.>>>/2`), and [`|||`](`Bitwise.|||/2`). See the documentation for `Bitwise` for more information. +The following operators are used by the `Bitwise` module when imported: [`&&&`](`Bitwise.&&&/2`), [`<<<`](`Bitwise.<<>>`](`Bitwise.>>>/2`), and [`|||`](`Bitwise.|||/2`). See the `Bitwise` documentation for more information. Note that the Elixir community generally discourages custom operators. They can be hard to read and even more to understand, as they don't have a descriptive name like functions do. That said, some specific cases or custom domain specific languages (DSLs) may justify these practices. From dfbb61b45b3915b3ea1fbcf07355bfda81e6932a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 22 Nov 2023 09:23:01 +0800 Subject: [PATCH 0200/1886] Improve Logger docs, closes #13119 --- lib/logger/lib/logger.ex | 56 +++++++++++++++------------------------- 1 file changed, 21 insertions(+), 35 deletions(-) diff --git a/lib/logger/lib/logger.ex b/lib/logger/lib/logger.ex index fca6e5766b..d12cdee288 100644 --- a/lib/logger/lib/logger.ex +++ b/lib/logger/lib/logger.ex @@ -13,16 +13,16 @@ defmodule Logger do * Supports both message-based and structural logging. + * Integrate with Erlang's [`:logger'](`:logger`) and + support custom filters and handlers. + * Formats and truncates messages on the client - to avoid clogging `Logger` backends. + to avoid clogging `Logger` handlers. * Alternates between sync and async modes to remain performant when required but also apply back-pressure when under stress. - * Support for custom filters and handlers as provided by - Erlang's `:logger`. - * Allows overriding the logging level for a specific module, application or process. @@ -65,7 +65,7 @@ defmodule Logger do For example, `:info` takes precedence over `:debug`. If your log level is set to `:info`, then all `:info`, `:notice` and above will - be passed to backends. If your log level is set to `:alert`, only + be passed to handlers. If your log level is set to `:alert`, only `:alert` and `:emergency` will be printed. ## Message @@ -126,8 +126,8 @@ defmodule Logger do * `:crash_reason` - a two-element tuple with the throw/error/exit reason as first argument and the stacktrace as second. A throw will always be `{:nocatch, term}`. An error is always an `Exception` struct. All other - entries are exits. The console backend ignores this metadata by default - but it can be useful to other backends, such as the ones that report + entries are exits. The default formatter ignores this metadata by default + but it can be useful to certain handlers, such as the ones that report errors to third-party services There are two special metadata keys, `:module` and `:function`, which @@ -275,8 +275,8 @@ defmodule Logger do Remember that if you want to purge log calls from a dependency, the dependency must be recompiled. - For example, to configure the `:backends` and purge all calls that happen - at compile time with level lower than `:info` in a `config/config.exs` file: + For example, to purge all calls that happen at compile time with level + lower than `:info` in a `config/config.exs` file: config :logger, compile_time_purge_matching: [ @@ -300,7 +300,7 @@ defmodule Logger do * `:level` - the logging level. Attempting to log any message with severity less than the configured level will simply - cause the message to be ignored. Keep in mind that each backend + cause the message to be ignored. Keep in mind that each handler may have its specific level, too. In addition to levels mentioned above it also supports 2 "meta-levels": @@ -397,7 +397,7 @@ defmodule Logger do Prior to Elixir v1.15, custom logging could be achieved with Logger backends. The main API for writing Logger backends have been moved to the [`:logger_backends`](https://github.com/elixir-lang/logger_backends) - project. However, the backends are still part of Elixir for backwards + project. However, the backends API are still part of Elixir for backwards compatibility. Important remarks: @@ -428,9 +428,12 @@ defmodule Logger do Backends, you can still set `backends: [Logger.Backends.Console]` and place the configuration under `config :logger, Logger.Backends.Console`. Although consider using the [`:logger_backends`](https://github.com/elixir-lang/logger_backends) - project in such case, as `Logger.Backends.Console` itself will be deprecated + project in such cases, as `Logger.Backends.Console` itself will be deprecated in future releases + * `Logger.Backends` only receive `:debug`, `:info`, `:warning`, and `:error` + messages. `:notice` maps to `:info`. `:warn` amps to `:warnings`. + All others map to `:error` """ @type level :: @@ -933,38 +936,21 @@ defmodule Logger do defp add_elixir_domain(metadata), do: Map.put(metadata, :domain, [:elixir]) - translations = %{ - emergency: :error, - alert: :error, - critical: :error, - notice: :info - } - for level <- @levels do report = [something: :reported, this: level] metadata = [user_id: 42, request_id: "xU32kFa"] - - extra = - if translation = translations[level] do - """ - - - This is reported as \"#{translation}\" in Elixir's - logger backends for backwards compatibility reasons. - - """ - end + article = if level in [:info, :error, :alert, :emergency], do: "an", else: "a" @doc """ - Logs a #{level} message. + Logs #{article} #{level} message. - Returns `:ok`.#{extra} + Returns `:ok`. ## Examples Logging a message (string or iodata): - Logger.#{level}("this is a #{level} message") + Logger.#{level}("this is #{article} #{level} message") Report message (maps or keywords): @@ -977,10 +963,10 @@ defmodule Logger do Report message with metadata (maps or keywords): # as a keyword list - Logger.#{level}("this is a #{level} message", #{inspect(metadata)}) + Logger.#{level}("this is #{article} #{level} message", #{inspect(metadata)}) # as map - Logger.#{level}("this is a #{level} message", #{inspect(Map.new(metadata))}) + Logger.#{level}("this is #{article} #{level} message", #{inspect(Map.new(metadata))}) """ # Only macros generated for the "new" Erlang levels are available since 1.11.0. Other From 07af739c30213fba5d58f7de372ef4959c74d5f3 Mon Sep 17 00:00:00 2001 From: Andrea Leopardi Date: Wed, 22 Nov 2023 10:52:33 +0100 Subject: [PATCH 0201/1886] Add some specs and types to ExUnit.Formatter (#13130) --- lib/ex_unit/lib/ex_unit/formatter.ex | 36 ++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/lib/ex_unit/lib/ex_unit/formatter.ex b/lib/ex_unit/lib/ex_unit/formatter.ex index ed639cea4b..b3e73fd460 100644 --- a/lib/ex_unit/lib/ex_unit/formatter.ex +++ b/lib/ex_unit/lib/ex_unit/formatter.ex @@ -67,6 +67,20 @@ defmodule ExUnit.Formatter do load: pos_integer | nil } + @typedoc """ + A function that this module calls to format various things. + """ + @typedoc since: "1.16.0" + @type formatter_callback :: (atom, term -> term) + + @typedoc """ + Width for formatting. + + For example, see `format_assertion_diff/4`. + """ + @typedoc since: "1.16.0" + @type width :: non_neg_integer | :infinity + import Exception, only: [format_stacktrace_entry: 1, format_file_line: 3] alias ExUnit.Diff @@ -155,6 +169,14 @@ defmodule ExUnit.Formatter do @doc """ Receives a test and formats its failure. """ + @spec format_test_failure( + ExUnit.Test.t(), + [failure], + non_neg_integer, + width, + formatter_callback + ) :: String.t() + when failure: {atom, term, Exception.stacktrace()} def format_test_failure(test, failures, counter, width, formatter) do %ExUnit.Test{name: name, module: module, tags: tags} = test @@ -177,6 +199,14 @@ defmodule ExUnit.Formatter do @doc """ Receives a test module and formats its failure. """ + @spec format_test_all_failure( + ExUnit.TestModule.t(), + [failure], + non_neg_integer, + width, + formatter_callback + ) :: String.t() + when failure: {atom, term, Exception.stacktrace()} def format_test_all_failure(test_module, failures, counter, width, formatter) do name = test_module.name @@ -280,6 +310,12 @@ defmodule ExUnit.Formatter do for formatted content, the width (may be `:infinity`), and the formatter callback function. """ + @spec format_assertion_diff( + %ExUnit.AssertionError{}, + non_neg_integer, + width, + formatter_callback + ) :: keyword def format_assertion_diff(assert_error, padding_size, width, formatter) def format_assertion_diff(%ExUnit.AssertionError{context: {:mailbox, _pins, []}}, _, _, _) do From 141c3f74136383a529f37b6758b65ff9ded275bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Samson?= Date: Wed, 22 Nov 2023 17:45:54 +0100 Subject: [PATCH 0202/1886] Properly escape `\` in Path.wildcard docs (#13137) --- lib/elixir/lib/path.ex | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/elixir/lib/path.ex b/lib/elixir/lib/path.ex index 680ff77c87..4cf3445cc9 100644 --- a/lib/elixir/lib/path.ex +++ b/lib/elixir/lib/path.ex @@ -762,9 +762,9 @@ defmodule Path do You may call `Path.expand/1` to normalize the path before invoking this function. - A character preceded by \ loses its special meaning. - Note that \ must be written as \\ in a string literal. - For example, "\\?*" will match any filename starting with ?. + A character preceded by `\\` loses its special meaning. + Note that `\\` must be written as `\\\\` in a string literal. + For example, `"\\\\?*"` will match any filename starting with `?.`. By default, the patterns `*` and `?` do not match files starting with a dot `.`. See the `:match_dot` option in the "Options" section From 68b03eb9ee0271584da77a9310af44b469a9822c Mon Sep 17 00:00:00 2001 From: Andrea Leopardi Date: Thu, 23 Nov 2023 04:23:27 +0100 Subject: [PATCH 0203/1886] Add callback docs to ExUnit.Formatter (#13135) --- lib/ex_unit/lib/ex_unit/formatter.ex | 117 +++++++++++++++++++++++++-- 1 file changed, 112 insertions(+), 5 deletions(-) diff --git a/lib/ex_unit/lib/ex_unit/formatter.ex b/lib/ex_unit/lib/ex_unit/formatter.ex index b3e73fd460..ab25f21d18 100644 --- a/lib/ex_unit/lib/ex_unit/formatter.ex +++ b/lib/ex_unit/lib/ex_unit/formatter.ex @@ -67,11 +67,88 @@ defmodule ExUnit.Formatter do load: pos_integer | nil } + @typedoc """ + Key passed to a formatter callback to format a diff. + + See `t:formatter_callback/0`. + """ + @typedoc since: "1.16.0" + @type formatter_callback_diff_key :: + :diff_delete + | :diff_delete_whitespace + | :diff_insert + | :diff_insert_whitespace + + @typedoc """ + Key passed to a formatter callback to format information. + + See `t:formatter_callback/0`. + """ + @typedoc since: "1.16.0" + @type formatter_callback_info_key :: + :extra_info + | :error_info + | :test_module_info + | :test_info + | :location_info + | :stacktrace_info + | :blame_diff + @typedoc """ A function that this module calls to format various things. + + You can pass this functions to various functions in this module, and use it + to customize the formatting of the output. For example, ExUnit's CLI formatter + uses this callback to colorize output. + + ## Keys + + The possible keys are: + + * `:diff_enabled?` - whether diffing is enabled. It receives a boolean + indicating whether diffing is enabled by default and returns a boolean + indicating whether diffing should be enabled for the current test. + + * `:diff_delete` and `:diff_delete_whitespace` - Should format a diff deletion, + with or without whitespace respectively. + + * `:diff_insert` and `:diff_insert_whitespace` - Should format a diff insertion, + with or without whitespace respectively. + + * `:extra_info` - Should format extra information, such as the `"code: "` label + that precedes code to show. + + * `:error_info` - Should format error information. + + * `:error_info` - Should format error information. + + * `:test_module_info` - Should format test module information. The message returned + when this key is passed precedes messages such as `"failure on setup_all callback [...]"`. + + * `:test_info` - Should format test information. + + * `:location_info` - Should format test location information. + + * `:stacktrace_info` - Should format stacktrace information. + + * `:blame_diff` - Should format a string of code. + + ## Examples + + For example, to format errors as *red strings* and everything else as is, you could define + a formatter callback function like this: + + formatter_callback = fn + :error_info, msg -> [:red, msg, :reset] |> IO.ANSI.format() |> IO.iodata_to_binary() + _key, value -> value + end + """ @typedoc since: "1.16.0" - @type formatter_callback :: (atom, term -> term) + @type formatter_callback :: + (:diff_enabled?, boolean -> boolean) + | (formatter_callback_diff_key, Inspect.Algebra.t() -> Inspect.Algebra.t()) + | (formatter_callback_info_key, String.t() -> String.t()) @typedoc """ Width for formatting. @@ -166,11 +243,20 @@ defmodule ExUnit.Formatter do end end - @doc """ - Receives a test and formats its failure. + @doc ~S""" + Receives a test and formats its failures. + + ## Examples + + iex> failure = {:error, catch_error(raise "oops"), _stacktrace = []} + iex> formatter_cb = fn _key, value -> value end + iex> test = %ExUnit.Test{name: :"it works", module: MyTest, tags: %{file: "file.ex", line: 7}} + iex> format_test_failure(test, [failure], 1, 80, formatter_cb) + " 1) it works (MyTest)\n file.ex:7\n ** (RuntimeError) oops\n" + """ @spec format_test_failure( - ExUnit.Test.t(), + test, [failure], non_neg_integer, width, @@ -196,8 +282,17 @@ defmodule ExUnit.Formatter do format_test_all_failure(test_case, failures, counter, width, formatter) end - @doc """ + @doc ~S""" Receives a test module and formats its failure. + + ## Examples + + iex> failure = {:error, catch_error(raise "oops"), _stacktrace = []} + iex> formatter_cb = fn _key, value -> value end + iex> test_module = %ExUnit.TestModule{name: Hello} + iex> format_test_all_failure(test_module, [failure], 1, 80, formatter_cb) + " 1) Hello: failure on setup_all callback, all tests have been invalidated\n ** (RuntimeError) oops\n" + """ @spec format_test_all_failure( ExUnit.TestModule.t(), @@ -309,6 +404,18 @@ defmodule ExUnit.Formatter do It expects the assertion error, the `padding_size` for formatted content, the width (may be `:infinity`), and the formatter callback function. + + ## Examples + + iex> error = assert_raise ExUnit.AssertionError, fn -> assert [1, 2] == [1, 3] end + iex> formatter_cb = fn + ...> :diff_enabled?, _ -> true + ...> _key, value -> value + ...> end + iex> keyword = format_assertion_diff(error, 5, 80, formatter_cb) + iex> for {key, val} <- keyword, do: {key, IO.iodata_to_binary(val)} + [left: "[1, 2]", right: "[1, 3]"] + """ @spec format_assertion_diff( %ExUnit.AssertionError{}, From 1a65b04c922e0cb5654bced10ac97143d21f6513 Mon Sep 17 00:00:00 2001 From: Andrea Leopardi Date: Thu, 23 Nov 2023 04:23:54 +0100 Subject: [PATCH 0204/1886] Add t/0 types for some ExUnit exceptions (#13134) --- lib/ex_unit/lib/ex_unit/assertions.ex | 19 +++++++++++++++++++ lib/ex_unit/lib/ex_unit/formatter.ex | 2 +- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/lib/ex_unit/lib/ex_unit/assertions.ex b/lib/ex_unit/lib/ex_unit/assertions.ex index 9b4bd54d4e..6ba920372e 100644 --- a/lib/ex_unit/lib/ex_unit/assertions.ex +++ b/lib/ex_unit/lib/ex_unit/assertions.ex @@ -1,10 +1,23 @@ defmodule ExUnit.AssertionError do @moduledoc """ Raised to signal an assertion error. + + This is used by macros such as `ExUnit.Assertions.assert/1`. """ @no_value :ex_unit_no_meaningful_value + @typedoc since: "1.16.0" + @type t :: %__MODULE__{ + left: any, + right: any, + message: any, + expr: any, + args: any, + doctest: any, + context: any + } + defexception left: @no_value, right: @no_value, message: @no_value, @@ -16,6 +29,7 @@ defmodule ExUnit.AssertionError do @doc """ Indicates no meaningful value for a field. """ + @spec no_value :: atom def no_value do @no_value end @@ -31,6 +45,11 @@ defmodule ExUnit.MultiError do Raised to signal multiple errors happened in a test case. """ + @typedoc since: "1.16.0" + @type t :: %__MODULE__{ + errors: [{Exception.kind(), any, Exception.stacktrace()}] + } + defexception errors: [] @impl true diff --git a/lib/ex_unit/lib/ex_unit/formatter.ex b/lib/ex_unit/lib/ex_unit/formatter.ex index ab25f21d18..da41f6c3ef 100644 --- a/lib/ex_unit/lib/ex_unit/formatter.ex +++ b/lib/ex_unit/lib/ex_unit/formatter.ex @@ -418,7 +418,7 @@ defmodule ExUnit.Formatter do """ @spec format_assertion_diff( - %ExUnit.AssertionError{}, + ExUnit.AssertionError.t(), non_neg_integer, width, formatter_callback From f547a752fc8c780e732af7223c95539d7faaf171 Mon Sep 17 00:00:00 2001 From: Andrea Leopardi Date: Thu, 23 Nov 2023 12:08:39 +0100 Subject: [PATCH 0205/1886] Fix typo in Logger docs --- lib/logger/lib/logger.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/logger/lib/logger.ex b/lib/logger/lib/logger.ex index d12cdee288..c59df5ca43 100644 --- a/lib/logger/lib/logger.ex +++ b/lib/logger/lib/logger.ex @@ -13,7 +13,7 @@ defmodule Logger do * Supports both message-based and structural logging. - * Integrate with Erlang's [`:logger'](`:logger`) and + * Integrate with Erlang's [`:logger`](`:logger`) and support custom filters and handlers. * Formats and truncates messages on the client From 705794a7239d04ec5d35030f9e877f35533e102a Mon Sep 17 00:00:00 2001 From: Andrea Leopardi Date: Thu, 23 Nov 2023 12:16:15 +0100 Subject: [PATCH 0206/1886] Add t/0 types to remaining ExUnit exceptions (#13139) --- lib/ex_unit/lib/ex_unit.ex | 10 ++++++++++ lib/ex_unit/lib/ex_unit/case.ex | 15 +++++++++++++++ lib/ex_unit/lib/ex_unit/doc_test.ex | 6 ++++++ 3 files changed, 31 insertions(+) diff --git a/lib/ex_unit/lib/ex_unit.ex b/lib/ex_unit/lib/ex_unit.ex index 72a7a467a7..dd5fa35dbc 100644 --- a/lib/ex_unit/lib/ex_unit.ex +++ b/lib/ex_unit/lib/ex_unit.ex @@ -148,6 +148,16 @@ defmodule ExUnit do end defmodule TimeoutError do + @moduledoc """ + Exception raised when a test times out. + """ + + @typedoc since: "1.16.0" + @type t :: %__MODULE__{ + timeout: non_neg_integer, + type: String.t() + } + defexception [:timeout, :type] @impl true diff --git a/lib/ex_unit/lib/ex_unit/case.ex b/lib/ex_unit/lib/ex_unit/case.ex index 579a6ee16c..05976748de 100644 --- a/lib/ex_unit/lib/ex_unit/case.ex +++ b/lib/ex_unit/lib/ex_unit/case.ex @@ -1,8 +1,23 @@ defmodule ExUnit.DuplicateTestError do + @moduledoc """ + Exception raised to indicate two or more tests with the same name. + """ + + @typedoc since: "1.16.0" + @type t :: %__MODULE__{message: String.t()} + defexception [:message] end defmodule ExUnit.DuplicateDescribeError do + @moduledoc """ + Exception raised to indicate two or more `describe` blocks with + the same name. + """ + + @typedoc since: "1.16.0" + @type t :: %__MODULE__{message: String.t()} + defexception [:message] end diff --git a/lib/ex_unit/lib/ex_unit/doc_test.ex b/lib/ex_unit/lib/ex_unit/doc_test.ex index 8224d4fe10..7d99db9339 100644 --- a/lib/ex_unit/lib/ex_unit/doc_test.ex +++ b/lib/ex_unit/lib/ex_unit/doc_test.ex @@ -152,6 +152,12 @@ defmodule ExUnit.DocTest do @opaque_type_regex ~r/#[\w\.]+ Date: Thu, 23 Nov 2023 13:15:51 +0100 Subject: [PATCH 0207/1886] Add missing @spec to some Logger functions (#13140) --- lib/logger/lib/logger/formatter.ex | 1 + lib/logger/lib/logger/translator.ex | 3 +++ 2 files changed, 4 insertions(+) diff --git a/lib/logger/lib/logger/formatter.ex b/lib/logger/lib/logger/formatter.ex index 10a9515e4f..999436a638 100644 --- a/lib/logger/lib/logger/formatter.ex +++ b/lib/logger/lib/logger/formatter.ex @@ -134,6 +134,7 @@ defmodule Logger.Formatter do The color of the message can also be configured per message via the `:ansi_color` metadata. """ + @spec new(keyword) :: formatter when formatter: term def new(options \\ []) do template = compile(options[:format]) colors = colors(options[:colors] || []) diff --git a/lib/logger/lib/logger/translator.ex b/lib/logger/lib/logger/translator.ex index 8521cb281d..ab8df0ebac 100644 --- a/lib/logger/lib/logger/translator.ex +++ b/lib/logger/lib/logger/translator.ex @@ -40,6 +40,9 @@ defmodule Logger.Translator do @doc """ Built-in translation function. + + This function is an implementation of the `c:translate/4` callback. + For arguments and return value of this function, see that callback. """ def translate(min_level, level, kind, message) From 341519cb6af347dad542464708c75f7fa8060801 Mon Sep 17 00:00:00 2001 From: Samuel Chase Date: Thu, 23 Nov 2023 19:37:36 +0530 Subject: [PATCH 0208/1886] Fix typo in design-anti-patterns.md (#13141) --- lib/elixir/pages/anti-patterns/design-anti-patterns.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/pages/anti-patterns/design-anti-patterns.md b/lib/elixir/pages/anti-patterns/design-anti-patterns.md index 2db2cce45f..009a970b7e 100644 --- a/lib/elixir/pages/anti-patterns/design-anti-patterns.md +++ b/lib/elixir/pages/anti-patterns/design-anti-patterns.md @@ -218,7 +218,7 @@ Another example of this anti-pattern is using floating numbers to model money an #### Refactoring -Possible solutions to this anti-pattern is to use maps or structs to model our address. The example below creates an `Address` struct, better representing this domain through a composite type. Additionally, we introduce a `parse/1` function, that converts the string into an `Address`, which will simplify the logic of remainng functions. With this modification, we can extract each field of this composite type individually when needed. +Possible solutions to this anti-pattern is to use maps or structs to model our address. The example below creates an `Address` struct, better representing this domain through a composite type. Additionally, we introduce a `parse/1` function, that converts the string into an `Address`, which will simplify the logic of remaining functions. With this modification, we can extract each field of this composite type individually when needed. ```elixir defmodule Address do From 1ef9eaf9001d0173504f4ca352c6fbf84384ec9e Mon Sep 17 00:00:00 2001 From: Zeke Dou <59962222+c4710n@users.noreply.github.com> Date: Thu, 23 Nov 2023 08:08:31 -0600 Subject: [PATCH 0209/1886] Add Logger.levels/0 (#13136) Co-authored-by: c4710n --- lib/logger/lib/logger.ex | 7 +++++++ lib/logger/test/logger_test.exs | 5 +++++ 2 files changed, 12 insertions(+) diff --git a/lib/logger/lib/logger.ex b/lib/logger/lib/logger.ex index c59df5ca43..fff2c4dc8d 100644 --- a/lib/logger/lib/logger.ex +++ b/lib/logger/lib/logger.ex @@ -445,6 +445,13 @@ defmodule Logger do @levels [:error, :info, :debug] ++ @new_erlang_levels @metadata :logger_level + @doc ~S""" + Returns all the available levels. + """ + @doc since: "1.16.0" + @spec levels() :: [level(), ...] + def levels(), do: @levels + @doc ~S""" Returns the default formatter used by Logger. diff --git a/lib/logger/test/logger_test.exs b/lib/logger/test/logger_test.exs index d9ee5eb838..2d26722711 100644 --- a/lib/logger/test/logger_test.exs +++ b/lib/logger/test/logger_test.exs @@ -16,6 +16,11 @@ defmodule LoggerTest do msg("module=LoggerTest #{text}") end + test "levels/0" do + assert [_ | _] = Logger.levels() + assert :info in Logger.levels() + end + test "level/0" do assert Logger.level() == :debug From 9daef619419a505391094d19ebb578b449dea8d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 23 Nov 2023 22:33:23 +0800 Subject: [PATCH 0210/1886] Fix prying functions with only literals, closes #13133 --- lib/iex/lib/iex/pry.ex | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/iex/lib/iex/pry.ex b/lib/iex/lib/iex/pry.ex index ce858e48ba..8e9236c173 100644 --- a/lib/iex/lib/iex/pry.ex +++ b/lib/iex/lib/iex/pry.ex @@ -520,7 +520,7 @@ defmodule IEx.Pry do next_binding = binding(expr, binding) {min_line, max_line} = line_range(expr, line) - if force? or (min_line > line and min_line != :infinity) do + if force? or min_line > line do pry_var = next_var(version) pry_binding = Map.to_list(binding) pry_opts = [line: min_line] ++ opts @@ -546,7 +546,9 @@ defmodule IEx.Pry do end defp line_range(ast, line) do - {_, min_max} = + # We want min_line to start from infinity because + # if it starts from line it will always just return line. + {_, {min, max}} = Macro.prewalk(ast, {:infinity, line}, fn {_, meta, _} = ast, {min_line, max_line} when is_list(meta) -> line = meta[:line] @@ -561,7 +563,7 @@ defmodule IEx.Pry do {ast, acc} end) - min_max + if min == :infinity, do: {line, max}, else: {min, max} end defp binding(ast, binding) do From 7d04c40fc09696f0180baef6b088da87234afd0c Mon Sep 17 00:00:00 2001 From: Wojtek Mach Date: Fri, 24 Nov 2023 16:56:21 +0100 Subject: [PATCH 0211/1886] Update `Application.ensure_all_started/2` docs (#13144) --- lib/elixir/lib/application.ex | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/elixir/lib/application.ex b/lib/elixir/lib/application.ex index 11f1af388d..5aacb9c19b 100644 --- a/lib/elixir/lib/application.ex +++ b/lib/elixir/lib/application.ex @@ -892,11 +892,12 @@ defmodule Application do ## Options - * `:type` - if the application should be started in `:permanent`, - `:temporary`, or `:transient`. See `t:restart_type/1` for more information. + * `:type` - if the application should be started `:temporary` (default), + `:permanent`, or `:transient`. See `t:restart_type/1` for more information. * `:mode` - (since v1.15.0) if the applications should be started serially - or concurrently. This option requires Erlang/OTP 26+. + (`:serial`, default) or concurrently (`:concurrent`). This option requires + Erlang/OTP 26+. """ @spec ensure_all_started(app | [app], type: restart_type(), mode: :serial | :concurrent) :: From 4cac7affbab9ac89da46c3b23cbf60c1805b7dd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sat, 25 Nov 2023 10:34:38 +0800 Subject: [PATCH 0212/1886] Include both priv and include in releases, closes #13145 --- lib/mix/lib/mix/release.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mix/lib/mix/release.ex b/lib/mix/lib/mix/release.ex index be395713e6..ce3060a51f 100644 --- a/lib/mix/lib/mix/release.ex +++ b/lib/mix/lib/mix/release.ex @@ -68,7 +68,7 @@ defmodule Mix.Release do @safe_modes [:permanent, :temporary, :transient] @unsafe_modes [:load, :none] @additional_chunks ~w(Attr)c - @copy_app_dirs ["priv"] + @copy_app_dirs ["priv", "include"] @doc false @spec from_config!(atom, keyword, keyword) :: t From ba2e56954daad0ae67b98908f5ed8c6d62b72d83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sat, 25 Nov 2023 12:36:41 +0800 Subject: [PATCH 0213/1886] Slightly clarify why guards --- lib/elixir/pages/references/patterns-and-guards.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/pages/references/patterns-and-guards.md b/lib/elixir/pages/references/patterns-and-guards.md index dd53d1b5da..0a509333a1 100644 --- a/lib/elixir/pages/references/patterns-and-guards.md +++ b/lib/elixir/pages/references/patterns-and-guards.md @@ -270,7 +270,7 @@ Suffix matches (`hello <> " world"`) are not valid patterns. Guards are a way to augment pattern matching with more complex checks. They are allowed in a predefined set of constructs where pattern matching is allowed, such as function definitions, case clauses, and others. -Not all expressions are allowed in guard clauses, but only a handful of them. This is a deliberate choice. This way, Elixir (and Erlang) can make sure that nothing bad happens while executing guards and no mutations happen anywhere. It also allows the compiler to optimize the code related to guards efficiently. +Not all expressions are allowed in guard clauses, but only a handful of them. This is a deliberate choice. This way, Elixir (through Erlang) ensures that all guards are predictable (no mutations or other side-effects) and they can be optimized and performed efficiently. ### List of allowed functions and operators From dde2a7c7822dce546c008d8729279c451017593a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 28 Nov 2023 02:01:44 +0100 Subject: [PATCH 0214/1886] Bump DavidAnson/markdownlint-cli2-action from 13.0.0 to 14.0.0 (#13148) Bumps [DavidAnson/markdownlint-cli2-action](https://github.com/davidanson/markdownlint-cli2-action) from 13.0.0 to 14.0.0. - [Release notes](https://github.com/davidanson/markdownlint-cli2-action/releases) - [Commits](https://github.com/davidanson/markdownlint-cli2-action/compare/v13.0.0...v14.0.0) --- updated-dependencies: - dependency-name: DavidAnson/markdownlint-cli2-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci-markdown.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-markdown.yml b/.github/workflows/ci-markdown.yml index 4d6b966196..c53358c514 100644 --- a/.github/workflows/ci-markdown.yml +++ b/.github/workflows/ci-markdown.yml @@ -27,7 +27,7 @@ jobs: fetch-depth: 10 - name: Run markdownlint - uses: DavidAnson/markdownlint-cli2-action@v13.0.0 + uses: DavidAnson/markdownlint-cli2-action@v14.0.0 with: globs: | lib/elixir/pages/**/*.md From 5b1521b2e47e74107feaba8079739975517d6489 Mon Sep 17 00:00:00 2001 From: Andrea Leopardi Date: Tue, 28 Nov 2023 16:29:08 +0100 Subject: [PATCH 0215/1886] Small doc improvements to "mix compile.app" --- lib/mix/lib/mix/tasks/compile.app.ex | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/mix/lib/mix/tasks/compile.app.ex b/lib/mix/lib/mix/tasks/compile.app.ex index f54b3a6f33..162fec230f 100644 --- a/lib/mix/lib/mix/tasks/compile.app.ex +++ b/lib/mix/lib/mix/tasks/compile.app.ex @@ -4,14 +4,14 @@ defmodule Mix.Tasks.Compile.App do @recursive true @moduledoc """ - Writes an .app file. + Writes a `.app` file. - An `.app` file is a file containing Erlang terms that defines + A `.app` file is a file containing Erlang terms that defines your application. Mix automatically generates this file based on your `mix.exs` configuration. In order to generate the `.app` file, Mix expects your project - to have both `:app` and `:version` keys. Furthermore, you can + to have both the `:app` and `:version` keys. Furthermore, you can configure the generated application by defining an `application/0` function in your `mix.exs` that returns a keyword list. From d348606e10cc1585e750b2f067b09ee88aa02335 Mon Sep 17 00:00:00 2001 From: Andrea Leopardi Date: Tue, 28 Nov 2023 16:29:19 +0100 Subject: [PATCH 0216/1886] Small doc improvements to "mix loadconfig" --- lib/mix/lib/mix/tasks/loadconfig.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/mix/lib/mix/tasks/loadconfig.ex b/lib/mix/lib/mix/tasks/loadconfig.ex index e21595698e..537213e351 100644 --- a/lib/mix/lib/mix/tasks/loadconfig.ex +++ b/lib/mix/lib/mix/tasks/loadconfig.ex @@ -11,8 +11,8 @@ defmodule Mix.Tasks.Loadconfig do Any configuration file loaded with `loadconfig` is treated as a compile-time configuration. - Note that "config/config.exs" is always loaded automatically - by the Mix CLI when it boots. "config/runtime.exs" is loaded + `config/config.exs` is **always loaded automatically** + by the Mix CLI when it boots. `config/runtime.exs` is loaded automatically by `mix app.config` before starting the current application. Therefore there is no need to load those config files directly. From 2e0a1fe186fa47c7c8d22775060641ae2db24b0a Mon Sep 17 00:00:00 2001 From: Logan Hasson Date: Thu, 30 Nov 2023 18:22:22 -0500 Subject: [PATCH 0217/1886] Update arity of String.split (#13151) `String.split/2` is mentioned, but `String.split/1` is actually used in the example. --- lib/elixir/pages/getting-started/lists-and-tuples.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/pages/getting-started/lists-and-tuples.md b/lib/elixir/pages/getting-started/lists-and-tuples.md index ef4ea6b4b1..341361f0a1 100644 --- a/lib/elixir/pages/getting-started/lists-and-tuples.md +++ b/lib/elixir/pages/getting-started/lists-and-tuples.md @@ -150,7 +150,7 @@ iex> String.split("hello beautiful world") ["hello", "beautiful", "world"] ``` -The `String.split/2` function breaks a string into a list of strings on every whitespace character. Since the amount of elements returned depends on the input, we use a list. +The `String.split/1` function breaks a string into a list of strings on every whitespace character. Since the amount of elements returned depends on the input, we use a list. On the other hand, `String.split_at/2` splits a string in two parts at a given position. Since it always returns two entries, regardless of the input size, it returns tuples: From e9522566193925b0bfa5a05192e569c5a6b1df21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dmitry=20Slutsky=20=E2=80=AE=20=E2=80=AE?= Date: Sun, 3 Dec 2023 23:55:38 +0100 Subject: [PATCH 0218/1886] Add info that Regex.scan/3 is only matching non-overlapping matches (#13153) With corresponding doctests. --- lib/elixir/lib/regex.ex | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/elixir/lib/regex.ex b/lib/elixir/lib/regex.ex index b89d18f761..0094298b45 100644 --- a/lib/elixir/lib/regex.ex +++ b/lib/elixir/lib/regex.ex @@ -471,8 +471,7 @@ defmodule Regex do end @doc ~S""" - Same as `run/3`, but scans the target several times collecting all - matches of the regular expression. + Same as `run/3` but returns all non-overlapping matches of the regular expression. A list of lists is returned, where each entry in the primary list represents a match and each entry in the secondary list represents the captured contents. @@ -497,6 +496,12 @@ defmodule Regex do iex> Regex.scan(~r/e/, "abcd") [] + iex> Regex.scan(~r/ab|bc|cd/, "abcd") + [["ab"], ["cd"]] + + iex> Regex.scan(~r/ab|bc|cd/, "abbccd") + [["ab"], ["bc"], ["cd"]] + iex> Regex.scan(~r/\p{Sc}/u, "$, £, and €") [["$"], ["£"], ["€"]] From 41690a378cf28a5c21ad40968a10a6c1a48b3768 Mon Sep 17 00:00:00 2001 From: Rodolfo Carvalho Date: Mon, 4 Dec 2023 12:14:01 +0100 Subject: [PATCH 0219/1886] Add :depth option to git deps (#13128) This allows for faster clones that transfer less data over the network and take less space in disk, for cases when the full history is not needed. --- lib/mix/lib/mix/scm/git.ex | 85 ++++++++-- lib/mix/lib/mix/tasks/deps.ex | 4 + lib/mix/test/mix/scm/git_test.exs | 2 +- lib/mix/test/mix/tasks/deps.git_test.exs | 201 +++++++++++++++++++++++ lib/mix/test/test_helper.exs | 8 +- 5 files changed, 282 insertions(+), 18 deletions(-) diff --git a/lib/mix/lib/mix/scm/git.ex b/lib/mix/lib/mix/scm/git.ex index a5471bca18..ea8f1d7eeb 100644 --- a/lib/mix/lib/mix/scm/git.ex +++ b/lib/mix/lib/mix/scm/git.ex @@ -125,14 +125,18 @@ defmodule Mix.SCM.Git do sparse_toggle(opts) update_origin(opts[:git]) + rev = get_lock_rev(opts[:lock], opts) || get_opts_rev(opts) + # Fetch external data ["--git-dir=.git", "fetch", "--force", "--quiet"] |> Kernel.++(progress_switch(git_version())) |> Kernel.++(tags_switch(opts[:tag])) + |> Kernel.++(depth_switch(opts[:depth])) + |> Kernel.++(if rev, do: ["origin", rev], else: []) |> git!() # Migrate the Git repo - rev = get_lock_rev(opts[:lock], opts) || get_opts_rev(opts) || default_branch() + rev = rev || default_branch() git!(["--git-dir=.git", "checkout", "--quiet", rev]) if opts[:submodules] do @@ -164,7 +168,7 @@ defmodule Mix.SCM.Git do defp sparse_toggle(opts) do cond do sparse = opts[:sparse] -> - sparse_check(git_version()) + check_sparse_support(git_version()) git!(["--git-dir=.git", "config", "core.sparsecheckout", "true"]) File.mkdir_p!(".git/info") File.write!(".git/info/sparse-checkout", sparse) @@ -180,27 +184,50 @@ defmodule Mix.SCM.Git do end end - defp sparse_check(version) do - unless {1, 7, 4} <= version do - version = version |> Tuple.to_list() |> Enum.join(".") + @min_git_version_sparse {1, 7, 4} + @min_git_version_depth {1, 5, 0} + @min_git_version_progress {1, 7, 1} + + defp check_sparse_support(version) do + ensure_feature_compatibility(version, @min_git_version_sparse, "sparse checkout") + end + + defp check_depth_support(version) do + ensure_feature_compatibility(version, @min_git_version_depth, "depth (shallow clone)") + end + defp ensure_feature_compatibility(version, required_version, feature) do + unless required_version <= version do Mix.raise( - "Git >= 1.7.4 is required to use sparse checkout. " <> - "You are running version #{version}" + "Git >= #{format_version(required_version)} is required to use #{feature}. " <> + "You are running version #{format_version(version)}" ) end end defp progress_switch(version) do - if {1, 7, 1} <= version, do: ["--progress"], else: [] + if @min_git_version_progress <= version, do: ["--progress"], else: [] end defp tags_switch(nil), do: [] defp tags_switch(_), do: ["--tags"] + defp depth_switch(nil), do: [] + + defp depth_switch(n) when is_integer(n) and n > 0 do + check_depth_support(git_version()) + ["--depth=#{n}"] + end + ## Helpers defp validate_git_options(opts) do + opts + |> validate_refspec() + |> validate_depth() + end + + defp validate_refspec(opts) do case Keyword.take(opts, [:branch, :ref, :tag]) do [] -> opts @@ -222,6 +249,22 @@ defmodule Mix.SCM.Git do end end + defp validate_depth(opts) do + case Keyword.take(opts, [:depth]) do + [] -> + opts + + [{:depth, depth}] when is_integer(depth) and depth > 0 -> + opts + + invalid_depth -> + Mix.raise( + "The depth must be a positive integer, and be specified only once, got: #{inspect(invalid_depth)}. " <> + "Error on Git dependency: #{redact_uri(opts[:git])}" + ) + end + end + defp get_lock(opts) do %{rev: rev} = get_rev_info() {:git, opts[:git], rev, get_lock_opts(opts)} @@ -238,7 +281,7 @@ defmodule Mix.SCM.Git do defp get_lock_rev(_, _), do: nil defp get_lock_opts(opts) do - lock_opts = Keyword.take(opts, [:branch, :ref, :tag, :sparse, :subdir]) + lock_opts = Keyword.take(opts, [:branch, :ref, :tag, :sparse, :subdir, :depth]) if opts[:submodules] do lock_opts ++ [submodules: true] @@ -248,11 +291,7 @@ defmodule Mix.SCM.Git do end defp get_opts_rev(opts) do - if branch = opts[:branch] do - "origin/#{branch}" - else - opts[:ref] || opts[:tag] - end + opts[:branch] || opts[:ref] || opts[:tag] end defp redact_uri(git) do @@ -282,6 +321,8 @@ defmodule Mix.SCM.Git do end defp default_branch() do + # Note: the `set-head -a` command requires the remote reference to be + # fetched first. git!(["--git-dir=.git", "remote", "set-head", "origin", "-a"]) "origin/HEAD" end @@ -328,9 +369,17 @@ defmodule Mix.SCM.Git do end end - # Also invoked by lib/mix/test/test_helper.exs + # Invoked by lib/mix/test/test_helper.exs @doc false - def git_version do + def unsupported_options do + git_version = git_version() + + [] + |> Kernel.++(if git_version < @min_git_version_sparse, do: [:sparse], else: []) + |> Kernel.++(if git_version < @min_git_version_depth, do: [:depth], else: []) + end + + defp git_version do case Mix.State.fetch(:git_version) do {:ok, version} -> version @@ -354,6 +403,10 @@ defmodule Mix.SCM.Git do |> List.to_tuple() end + defp format_version(version) do + version |> Tuple.to_list() |> Enum.join(".") + end + defp to_integer(string) do {int, _} = Integer.parse(string) int diff --git a/lib/mix/lib/mix/tasks/deps.ex b/lib/mix/lib/mix/tasks/deps.ex index ff51f089dc..d1acb08db5 100644 --- a/lib/mix/lib/mix/tasks/deps.ex +++ b/lib/mix/lib/mix/tasks/deps.ex @@ -120,6 +120,10 @@ defmodule Mix.Tasks.Deps do * `:subdir` - (since v1.13.0) search for the project in the given directory relative to the git checkout. This is similar to `:sparse` option but instead of a doing a sparse checkout it does a full checkout. + * `:depth` - (since v1.17.0) creates a shallow clone of the Git repository, + limiting the history to the specified number of commits. This can significantly + improve clone speed for large repositories when full history is not needed. + The value must be a positive integer, typically `1`. If your Git repository requires authentication, such as basic username:password HTTP authentication via URLs, it can be achieved via Git configuration, keeping diff --git a/lib/mix/test/mix/scm/git_test.exs b/lib/mix/test/mix/scm/git_test.exs index 2442951054..d572ba06f0 100644 --- a/lib/mix/test/mix/scm/git_test.exs +++ b/lib/mix/test/mix/scm/git_test.exs @@ -32,7 +32,7 @@ defmodule Mix.SCM.GitTest do "https://github.com/elixir-lang/some_dep.git - v1" assert Mix.SCM.Git.format(Keyword.put(opts, :branch, "b")) == - "https://github.com/elixir-lang/some_dep.git - origin/b" + "https://github.com/elixir-lang/some_dep.git - b" assert Mix.SCM.Git.format(Keyword.put(opts, :ref, "abcdef")) == "https://github.com/elixir-lang/some_dep.git - abcdef" diff --git a/lib/mix/test/mix/tasks/deps.git_test.exs b/lib/mix/test/mix/tasks/deps.git_test.exs index 8983a6abb7..a5338b2db3 100644 --- a/lib/mix/test/mix/tasks/deps.git_test.exs +++ b/lib/mix/test/mix/tasks/deps.git_test.exs @@ -478,6 +478,207 @@ defmodule Mix.Tasks.DepsGitTest do purge([GitRepo, GitRepo.MixProject]) end + describe "Git depth option" do + @describetag :git_depth + + test "gets and updates Git repos with depth option" do + Process.put(:git_repo_opts, depth: 1) + + in_fixture("no_mixfile", fn -> + Mix.Project.push(GitApp) + + Mix.Tasks.Deps.Get.run([]) + message = "* Getting git_repo (#{fixture_path("git_repo")})" + assert_received {:mix_shell, :info, [^message]} + assert_shallow("deps/git_repo", 1) + + # Expand depth + update_dep(depth: 2) + Mix.Tasks.Deps.Get.run([]) + assert_shallow("deps/git_repo", 2) + + # Reduce depth + update_dep(depth: 1) + Mix.Tasks.Deps.Get.run([]) + assert_shallow("deps/git_repo", 1) + end) + end + + test "with tag" do + Process.put(:git_repo_opts, depth: 1, tag: "with_module") + + in_fixture("no_mixfile", fn -> + Mix.Project.push(GitApp) + + Mix.Tasks.Deps.Get.run([]) + message = "* Getting git_repo (#{fixture_path("git_repo")} - with_module)" + assert_received {:mix_shell, :info, [^message]} + assert_shallow("deps/git_repo", 1) + end) + end + + test "with branch" do + Process.put(:git_repo_opts, depth: 1, branch: "main") + + in_fixture("no_mixfile", fn -> + Mix.Project.push(GitApp) + + Mix.Tasks.Deps.Get.run([]) + message = "* Getting git_repo (#{fixture_path("git_repo")} - main)" + assert_received {:mix_shell, :info, [^message]} + assert_shallow("deps/git_repo", 1) + end) + end + + test "with ref" do + [last, _ | _] = get_git_repo_revs("git_repo") + + Process.put(:git_repo_opts, depth: 1, ref: last) + + in_fixture("no_mixfile", fn -> + Mix.Project.push(GitApp) + + Mix.Tasks.Deps.Get.run([]) + message = "* Getting git_repo (#{fixture_path("git_repo")} - #{last})" + assert_received {:mix_shell, :info, [^message]} + assert_shallow("deps/git_repo", 1) + end) + end + + test "changing refspec updates retaining depth" do + [last, first | _] = get_git_repo_revs("git_repo") + + Process.put(:git_repo_opts, ref: first, depth: 1) + + in_fixture("no_mixfile", fn -> + Mix.Project.push(GitApp) + + Mix.Tasks.Deps.Get.run([]) + message = "* Getting git_repo (#{fixture_path("git_repo")} - #{first})" + assert_received {:mix_shell, :info, [^message]} + assert_shallow("deps/git_repo", 1) + assert File.read!("mix.lock") =~ first + + # Change refspec + update_dep(ref: last, depth: 1) + Mix.Tasks.Deps.Get.run([]) + assert_shallow("deps/git_repo", 1) + assert File.read!("mix.lock") =~ last + end) + end + + test "removing depth retains shallow repository" do + # For compatibility and simplicity, we follow Git's behavior and do not + # attempt to unshallow an existing repository. This should not be a + # problem, because all we guarantee is that the correct source code is + # available whenever mix.exs or mix.lock change. If one wanted to have a + # full clone, they can always run `deps.clean` and `deps.get` again. + Process.put(:git_repo_opts, depth: 1) + + in_fixture("no_mixfile", fn -> + Mix.Project.push(GitApp) + + Mix.Tasks.Deps.Get.run([]) + message = "* Getting git_repo (#{fixture_path("git_repo")})" + assert_received {:mix_shell, :info, [^message]} + assert_shallow("deps/git_repo", 1) + + # Remove depth + update_dep([]) + Mix.Tasks.Deps.Get.run([]) + refute File.read!("mix.lock") =~ "depth:" + assert File.exists?("deps/git_repo/.git/shallow") + + assert System.cmd("git", ~w[--git-dir=deps/git_repo/.git rev-list --count HEAD]) == + {"1\n", 0} + end) + end + + @tag :git_sparse + test "with sparse checkout" do + Process.put(:git_repo_opts, sparse: "sparse_dir", depth: 1) + + in_fixture("no_mixfile", fn -> + Mix.Project.push(GitApp) + + Mix.Tasks.Deps.Get.run([]) + message = "* Getting git_repo (#{fixture_path("git_repo")})" + assert_received {:mix_shell, :info, [^message]} + assert_shallow("deps/git_repo", 1) + + refute File.exists?("deps/git_repo/mix.exs") + assert File.exists?("deps/git_repo/sparse_dir/mix.exs") + assert File.read!("mix.lock") =~ "sparse: \"sparse_dir\"" + end) + end + + test "with subdir" do + Process.put(:git_repo_opts, subdir: "sparse_dir", depth: 1) + + in_fixture("no_mixfile", fn -> + Mix.Project.push(GitApp) + + Mix.Tasks.Deps.Get.run([]) + message = "* Getting git_repo (#{fixture_path("git_repo")})" + assert_received {:mix_shell, :info, [^message]} + assert_shallow("deps/git_repo", 1) + + assert File.exists?("deps/git_repo/mix.exs") + assert File.exists?("deps/git_repo/sparse_dir/mix.exs") + assert File.read!("mix.lock") =~ "subdir: \"sparse_dir\"" + end) + end + + test "does not affect submodules depth" do + # The expectation is that we can add an explicit option in the future, + # just like git-clone has `--shallow-submodules`. + Process.put(:git_repo_opts, submodules: true, depth: 1) + + in_fixture("no_mixfile", fn -> + Mix.Project.push(GitApp) + + Mix.Tasks.Deps.Get.run([]) + message = "* Getting git_repo (#{fixture_path("git_repo")})" + assert_received {:mix_shell, :info, [^message]} + assert_shallow("deps/git_repo", 1) + + assert File.read!("mix.lock") =~ "submodules: true" + # TODO: assert submodule is not shallow. This would likely require + # changes to the fixtures. Apparently, not even the submodules-specific + # tests check that the cloned repo contains submodules as expected. + end) + end + + defp update_dep(git_repo_opts) do + # Flush the errors we got, move to a clean slate + Mix.shell().flush() + Mix.Task.clear() + Process.put(:git_repo_opts, git_repo_opts) + Mix.Project.pop() + Mix.Project.push(GitApp) + end + + defp assert_shallow(repo_path, depth) do + assert File.read!("mix.lock") =~ "depth: #{depth}" + + # Check if the repository is a shallow clone + assert File.exists?(repo_path <> "/.git/shallow") + + # Check the number of commits in the current branch. + # + # We could consider all branches with `git rev-list --count --all`, as in + # practice there should be only a single branch. However, the test fixture + # sets up two branches, and that brings us to an interesting situation: + # instead of guaranteeing that the `:depth` option would keep the + # repository lean even after refspec changes, we only guarantee the number + # of commits in the current branch, perhaps leaving more objects around + # than strictly necessary. This allows us to keep the implementation + # simple, while still providing a reasonable guarantee. + assert System.cmd("git", ~w[--git-dir=#{repo_path}/.git rev-list --count HEAD]) == + {"#{depth}\n", 0} + end + end + defp refresh(post_config) do %{name: name, file: file} = Mix.Project.pop() Mix.ProjectStack.post_config(post_config) diff --git a/lib/mix/test/test_helper.exs b/lib/mix/test/test_helper.exs index 61733a02c5..c88194a04c 100644 --- a/lib/mix/test/test_helper.exs +++ b/lib/mix/test/test_helper.exs @@ -22,7 +22,13 @@ Application.put_env(:logger, :backends, []) os_exclude = if match?({:win32, _}, :os.type()), do: [unix: true], else: [windows: true] epmd_exclude = if match?({:win32, _}, :os.type()), do: [epmd: true], else: [] -git_exclude = if Mix.SCM.Git.git_version() <= {1, 7, 4}, do: [git_sparse: true], else: [] + +git_exclude = + Mix.SCM.Git.unsupported_options() + |> Enum.map(fn + :sparse -> {:git_sparse, true} + :depth -> {:git_depth, true} + end) {line_exclude, line_include} = if line = System.get_env("LINE"), do: {[:test], [line: line]}, else: {[], []} From 1d978bf73c8871fe61e4ca8dcf2fee8eaa52753f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20M=C3=BCller?= Date: Mon, 4 Dec 2023 08:46:15 -0300 Subject: [PATCH 0220/1886] Improve unclosed delimiter messages (#13123) --- lib/elixir/lib/exception.ex | 462 ++++++++++-------- lib/elixir/src/elixir_errors.erl | 10 +- lib/elixir/src/elixir_tokenizer.erl | 14 +- .../test/elixir/kernel/diagnostics_test.exs | 147 +++++- lib/elixir/test/elixir/kernel/errors_test.exs | 8 +- lib/elixir/test/elixir/kernel/parser_test.exs | 4 +- lib/ex_unit/test/ex_unit/doc_test_test.exs | 13 +- 7 files changed, 437 insertions(+), 221 deletions(-) diff --git a/lib/elixir/lib/exception.ex b/lib/elixir/lib/exception.ex index 4cd0e2b64a..04eafb6236 100644 --- a/lib/elixir/lib/exception.ex +++ b/lib/elixir/lib/exception.ex @@ -793,186 +793,25 @@ defmodule Exception do col -> format_file_line_column(Keyword.get(opts, :file), Keyword.get(opts, :line), col, " ") end end -end - -# Some exceptions implement "message/1" instead of "exception/1" mostly -# for bootstrap reasons. It is recommended for applications to implement -# "exception/1" instead of "message/1" as described in "defexception/1" -# docs. - -defmodule RuntimeError do - @moduledoc """ - An exception for a generic runtime error. - - This is the exception that `raise/1` raises when you pass it only a string as - a message: - - iex> raise "oops!" - ** (RuntimeError) oops! - - You should use this exceptions sparingly, since most of the time it might be - better to define your own exceptions specific to your application or library. - Sometimes, however, there are situations in which you don't expect a condition to - happen, but you want to give a meaningful error message if it does. In those cases, - `RuntimeError` can be a good choice. - - ## Fields - - `RuntimeError` exceptions have a single field, `:message` (a `t:String.t/0`), - which is public and can be accessed freely when reading or creating `RuntimeError` - exceptions. - """ - defexception message: "runtime error" -end - -defmodule ArgumentError do - @moduledoc """ - An exception raised when an argument to a function is invalid. - - You can raise this exception when you want to signal that an argument to - a function is invalid. - - `ArgumentError` exceptions have a single field, `:message` (a `t:String.t/0`), - which is public and can be accessed freely when reading or creating `ArgumentError` - exceptions. - """ - - defexception message: "argument error" -end - -defmodule ArithmeticError do - @moduledoc """ - An exception raised on invalid arithmetic operations. - - For example, this exception is raised if you divide by `0`: - - iex> 1 / 0 - ** (ArithmeticError) bad argument in arithmetic expression: 1 / 0 - - """ - - defexception message: "bad argument in arithmetic expression" - - @unary_ops [:+, :-] - @binary_ops [:+, :-, :*, :/] - @binary_funs [:div, :rem] - @bitwise_binary_funs [:band, :bor, :bxor, :bsl, :bsr] - - @impl true - def blame(%{message: message} = exception, [{:erlang, fun, args, _} | _] = stacktrace) do - message = - message <> - case {fun, args} do - {op, [a]} when op in @unary_ops -> - ": #{op}(#{inspect(a)})" - - {op, [a, b]} when op in @binary_ops -> - ": #{inspect(a)} #{op} #{inspect(b)}" - - {fun, [a, b]} when fun in @binary_funs -> - ": #{fun}(#{inspect(a)}, #{inspect(b)})" - - {fun, [a, b]} when fun in @bitwise_binary_funs -> - ": Bitwise.#{fun}(#{inspect(a)}, #{inspect(b)})" - - {:bnot, [a]} -> - ": Bitwise.bnot(#{inspect(a)})" - - _ -> - "" - end - - {%{exception | message: message}, stacktrace} - end - - def blame(exception, stacktrace) do - {exception, stacktrace} - end -end - -defmodule SystemLimitError do - @moduledoc """ - An exception raised when a system limit has been reached. - - For example, this can happen if you try to create an atom that is too large. - """ - - defexception message: "a system limit has been reached" -end - -defmodule MismatchedDelimiterError do - @moduledoc """ - An exception raised when a mismatched delimiter is found when parsing code. - - For example: - - `[1, 2, 3}` - - `fn a -> )` - """ - - @max_lines_shown 5 - - defexception [ - :file, - :line, - :column, - :line_offset, - :end_line, - :end_column, - :opening_delimiter, - :closing_delimiter, - :snippet, - description: "mismatched delimiter error" - ] - - @impl true - def message(%{ - line: start_line, - column: start_column, - end_line: end_line, - end_column: end_column, - line_offset: line_offset, - description: description, - opening_delimiter: opening_delimiter, - closing_delimiter: _closing_delimiter, - file: file, - snippet: snippet - }) do - start_pos = {start_line, start_column} - end_pos = {end_line, end_column} - lines = String.split(snippet, "\n") - expected_delimiter = :elixir_tokenizer.terminator(opening_delimiter) - - snippet = - format_snippet( - start_pos, - end_pos, + @doc false + def format_snippet( + {start_line, _start_column} = start_pos, + {end_line, end_column} = end_pos, line_offset, description, file, lines, - expected_delimiter + start_message, + end_message ) - - format_message(file, end_line, end_column, snippet) - end - - defp format_snippet( - {start_line, _start_column} = start_pos, - {end_line, end_column} = end_pos, - line_offset, - description, - file, - lines, - expected_delimiter - ) - when start_line < end_line do + when start_line < end_line do max_digits = digits(end_line) general_padding = max(2, max_digits) + 1 padding = n_spaces(general_padding) relevant_lines = - if end_line - start_line < @max_lines_shown do + if end_line - start_line < 5 do line_range( lines, start_pos, @@ -980,7 +819,8 @@ defmodule MismatchedDelimiterError do line_offset, padding, max_digits, - expected_delimiter + start_message, + end_message ) else trimmed_inbetween_lines( @@ -990,7 +830,8 @@ defmodule MismatchedDelimiterError do line_offset, padding, max_digits, - expected_delimiter + start_message, + end_message ) end @@ -1003,16 +844,17 @@ defmodule MismatchedDelimiterError do """ end - defp format_snippet( - {start_line, start_column}, - {end_line, end_column}, - line_offset, - description, - file, - lines, - expected_delimiter - ) - when start_line == end_line do + def format_snippet( + {start_line, start_column}, + {end_line, end_column}, + line_offset, + description, + file, + lines, + start_message, + end_message + ) + when start_line == end_line do max_digits = digits(end_line) general_padding = max(2, max_digits) + 1 padding = n_spaces(general_padding) @@ -1024,11 +866,11 @@ defmodule MismatchedDelimiterError do [ n_spaces(start_column - 1), red("│"), - mismatched_closing_delimiter(end_column - start_column, expected_delimiter) + format_end_message(end_column - start_column, end_message) ] unclosed_delimiter_line = - [padding, " │ ", unclosed_delimiter(start_column)] + [padding, " │ ", format_start_message(start_column, start_message)] below_line = [padding, " │ ", mismatched_closing_line, "\n", unclosed_delimiter_line] @@ -1068,7 +910,8 @@ defmodule MismatchedDelimiterError do line_offset, padding, max_digits, - expected_delimiter + start_message, + end_message ) do start_padding = line_padding(start_line, max_digits) end_padding = line_padding(end_line, max_digits) @@ -1077,10 +920,10 @@ defmodule MismatchedDelimiterError do """ #{start_padding}#{start_line} │ #{first_line} - #{padding}│ #{unclosed_delimiter(start_column)} + #{padding}│ #{format_start_message(start_column, start_message)} ... #{end_padding}#{end_line} │ #{last_line} - #{padding}│ #{mismatched_closing_delimiter(end_column, expected_delimiter)}\ + #{padding}│ #{format_end_message(end_column, end_message)}\ """ end @@ -1091,7 +934,8 @@ defmodule MismatchedDelimiterError do line_offset, padding, max_digits, - expected_delimiter + start_message, + end_message ) do start_line = start_line - 1 end_line = end_line - 1 @@ -1115,7 +959,7 @@ defmodule MismatchedDelimiterError do "\n", padding, " │ ", - unclosed_delimiter(start_column) + format_start_message(start_column, start_message) ] line_number == end_line -> @@ -1127,7 +971,7 @@ defmodule MismatchedDelimiterError do "\n", padding, " │ ", - mismatched_closing_delimiter(end_column, expected_delimiter) + format_end_message(end_column, end_message) ] true -> @@ -1137,14 +981,14 @@ defmodule MismatchedDelimiterError do |> Enum.intersperse("\n") end - defp mismatched_closing_delimiter(end_column, expected_closing_delimiter), + defp format_end_message(end_column, message), do: [ n_spaces(end_column - 1), - red(~s/└ mismatched closing delimiter (expected "#{expected_closing_delimiter}")/) + red(message) ] - defp unclosed_delimiter(start_column), - do: [n_spaces(start_column - 1), red("└ unclosed delimiter")] + defp format_start_message(start_column, message), + do: [n_spaces(start_column - 1), red(message)] defp pad_message(message, padding), do: String.replace(message, "\n", "\n #{padding}") @@ -1155,6 +999,171 @@ defmodule MismatchedDelimiterError do string end end +end + +# Some exceptions implement "message/1" instead of "exception/1" mostly +# for bootstrap reasons. It is recommended for applications to implement +# "exception/1" instead of "message/1" as described in "defexception/1" +# docs. + +defmodule RuntimeError do + @moduledoc """ + An exception for a generic runtime error. + + This is the exception that `raise/1` raises when you pass it only a string as + a message: + + iex> raise "oops!" + ** (RuntimeError) oops! + + You should use this exceptions sparingly, since most of the time it might be + better to define your own exceptions specific to your application or library. + Sometimes, however, there are situations in which you don't expect a condition to + happen, but you want to give a meaningful error message if it does. In those cases, + `RuntimeError` can be a good choice. + + ## Fields + + `RuntimeError` exceptions have a single field, `:message` (a `t:String.t/0`), + which is public and can be accessed freely when reading or creating `RuntimeError` + exceptions. + """ + + defexception message: "runtime error" +end + +defmodule ArgumentError do + @moduledoc """ + An exception raised when an argument to a function is invalid. + + You can raise this exception when you want to signal that an argument to + a function is invalid. + + `ArgumentError` exceptions have a single field, `:message` (a `t:String.t/0`), + which is public and can be accessed freely when reading or creating `ArgumentError` + exceptions. + """ + + defexception message: "argument error" +end + +defmodule ArithmeticError do + @moduledoc """ + An exception raised on invalid arithmetic operations. + + For example, this exception is raised if you divide by `0`: + + iex> 1 / 0 + ** (ArithmeticError) bad argument in arithmetic expression: 1 / 0 + + """ + + defexception message: "bad argument in arithmetic expression" + + @unary_ops [:+, :-] + @binary_ops [:+, :-, :*, :/] + @binary_funs [:div, :rem] + @bitwise_binary_funs [:band, :bor, :bxor, :bsl, :bsr] + + @impl true + def blame(%{message: message} = exception, [{:erlang, fun, args, _} | _] = stacktrace) do + message = + message <> + case {fun, args} do + {op, [a]} when op in @unary_ops -> + ": #{op}(#{inspect(a)})" + + {op, [a, b]} when op in @binary_ops -> + ": #{inspect(a)} #{op} #{inspect(b)}" + + {fun, [a, b]} when fun in @binary_funs -> + ": #{fun}(#{inspect(a)}, #{inspect(b)})" + + {fun, [a, b]} when fun in @bitwise_binary_funs -> + ": Bitwise.#{fun}(#{inspect(a)}, #{inspect(b)})" + + {:bnot, [a]} -> + ": Bitwise.bnot(#{inspect(a)})" + + _ -> + "" + end + + {%{exception | message: message}, stacktrace} + end + + def blame(exception, stacktrace) do + {exception, stacktrace} + end +end + +defmodule SystemLimitError do + @moduledoc """ + An exception raised when a system limit has been reached. + + For example, this can happen if you try to create an atom that is too large. + """ + + defexception message: "a system limit has been reached" +end + +defmodule MismatchedDelimiterError do + @moduledoc """ + An exception raised when a mismatched delimiter is found when parsing code. + + For example: + - `[1, 2, 3}` + - `fn a -> )` + """ + + defexception [ + :file, + :line, + :column, + :line_offset, + :end_line, + :end_column, + :opening_delimiter, + :closing_delimiter, + :snippet, + description: "mismatched delimiter error" + ] + + @impl true + def message(%{ + line: start_line, + column: start_column, + end_line: end_line, + end_column: end_column, + line_offset: line_offset, + description: description, + opening_delimiter: opening_delimiter, + closing_delimiter: _closing_delimiter, + file: file, + snippet: snippet + }) do + start_pos = {start_line, start_column} + end_pos = {end_line, end_column} + lines = String.split(snippet, "\n") + expected_delimiter = :elixir_tokenizer.terminator(opening_delimiter) + + start_message = "└ unclosed delimiter" + end_message = ~s/└ mismatched closing delimiter (expected "#{expected_delimiter}")/ + + snippet = + Exception.format_snippet( + start_pos, + end_pos, + line_offset, + description, + file, + lines, + start_message, + end_message + ) + + format_message(file, end_line, end_column, snippet) + end defp format_message(file, line, column, message) do location = Exception.format_file_line_column(Path.relative_to_cwd(file), line, column) @@ -1228,8 +1237,10 @@ defmodule TokenMissingError do defexception [ :file, :line, - :snippet, :column, + :end_line, + :line_offset, + :snippet, :opening_delimiter, description: "expression is incomplete" ] @@ -1239,14 +1250,51 @@ defmodule TokenMissingError do file: file, line: line, column: column, + end_line: end_line, + line_offset: line_offset, description: description, + opening_delimiter: opening_delimiter, snippet: snippet }) - when not is_nil(snippet) and not is_nil(column) do + when not is_nil(snippet) and not is_nil(column) and not is_nil(end_line) do + {lines, total_trimmed_lines} = handle_trailing_newlines(snippet) + end_line = end_line - total_trimmed_lines + + # For cases such as inside ExUnit doctests, our snippet is tiny, containing + # only the lines in the doctest, but the `line` and `end_line` we receive + # are still tied to the whole file. + # + # In these situations we use `line_offset` to treat `line` as 1 for + # operating on the snippet, while retaining the original line information. + should_use_line_offset? = is_nil(Enum.at(lines, end_line - 1)) + + end_column = + if should_use_line_offset? do + fetch_line_length(lines, end_line - line_offset - 1) + else + fetch_line_length(lines, end_line - 1) + end + + start_pos = {line, column} + end_pos = {end_line, end_column} + expected_delimiter = :elixir_tokenizer.terminator(opening_delimiter) + + start_message = ~s/└ unclosed delimiter/ + end_message = ~s/└ missing closing delimiter (expected "#{expected_delimiter}")/ + snippet = - :elixir_errors.format_snippet({line, column}, file, description, snippet, :error, [], nil) + Exception.format_snippet( + start_pos, + end_pos, + line_offset, + description, + file, + lines, + start_message, + end_message + ) - format_message(file, line, column, snippet) + format_message(file, end_line, end_column, snippet) end @impl true @@ -1254,13 +1302,27 @@ defmodule TokenMissingError do file: file, line: line, column: column, + snippet: snippet, description: description }) do snippet = - :elixir_errors.format_snippet({line, column}, file, description, nil, :error, [], nil) + :elixir_errors.format_snippet({line, column}, file, description, snippet, :error, [], nil) - padded = " " <> String.replace(snippet, "\n", "\n ") - format_message(file, line, column, padded) + format_message(file, line, column, snippet) + end + + defp handle_trailing_newlines(snippet) do + trimmed_snippet = String.trim_trailing(snippet, "\n") + total_trimmed_newlines = String.length(snippet) - String.length(trimmed_snippet) + lines = String.split(trimmed_snippet, "\n") + {lines, total_trimmed_newlines} + end + + defp fetch_line_length(lines, index) do + lines + |> Enum.fetch!(index) + |> String.length() + |> Kernel.+(1) end defp format_message(file, line, column, message) do diff --git a/lib/elixir/src/elixir_errors.erl b/lib/elixir/src/elixir_errors.erl index c4382af4b3..efc703949c 100644 --- a/lib/elixir/src/elixir_errors.erl +++ b/lib/elixir/src/elixir_errors.erl @@ -319,7 +319,10 @@ parse_error(Location, File, Error, <<>>, Input) -> <<"syntax error before: ">> -> <<"syntax error: expression is incomplete">>; _ -> <> end, - raise_snippet(Location, File, Input, 'Elixir.TokenMissingError', Message); + case lists:keytake(error_type, 1, Location) of + {value, {error_type, unclosed_delimiter}, Loc} -> raise_token_missing(Loc, File, Input, Message); + _ -> raise_snippet(Location, File, Input, 'Elixir.TokenMissingError', Message) + end; %% Show a nicer message for end of line parse_error(Location, File, <<"syntax error before: ">>, <<"eol">>, Input) -> @@ -409,6 +412,11 @@ raise_mismatched_delimiter(Location, File, Input, Message) -> KV = [{file, File}, {line_offset, StartLine - 1}, {snippet, InputBinary} | Location], raise('Elixir.MismatchedDelimiterError', Message, KV). +raise_token_missing(Location, File, Input, Message) -> + {InputString, StartLine, _} = Input, + InputBinary = elixir_utils:characters_to_binary(InputString), + raise('Elixir.TokenMissingError', Message, [{line_offset, StartLine - 1}, {file, File}, {snippet, InputBinary} | Location]). + raise_reserved(Location, File, Input, Keyword) -> raise_snippet(Location, File, Input, 'Elixir.SyntaxError', <<"syntax error before: ", Keyword/binary, ". \"", Keyword/binary, "\" is a " diff --git a/lib/elixir/src/elixir_tokenizer.erl b/lib/elixir/src/elixir_tokenizer.erl index c186b9a6bb..b63e09d27e 100644 --- a/lib/elixir/src/elixir_tokenizer.erl +++ b/lib/elixir/src/elixir_tokenizer.erl @@ -142,12 +142,18 @@ tokenize([], Line, Column, #elixir_tokenizer{cursor_completion=Cursor} = Scope, AccTokens = cursor_complete(Line, CursorColumn, CursorTerminators, CursorTokens), {ok, Line, Column, AllWarnings, AccTokens}; -tokenize([], EndLine, Column, #elixir_tokenizer{terminators=[{Start, {StartLine, _, _}, _} | _]} = Scope, Tokens) -> +tokenize([], EndLine, _, #elixir_tokenizer{terminators=[{Start, {StartLine, StartColumn, _}, _} | _]} = Scope, Tokens) -> End = terminator(Start), Hint = missing_terminator_hint(Start, End, Scope), - Message = "missing terminator: ~ts (for \"~ts\" starting at line ~B)", - Formatted = io_lib:format(Message, [End, Start, StartLine]), - Meta = [{opening_delimiter, Start} | ?LOC(EndLine, Column)], + Message = "missing terminator: ~ts", + Formatted = io_lib:format(Message, [End]), + Meta = [ + {error_type, unclosed_delimiter}, + {opening_delimiter, Start}, + {line, StartLine}, + {column, StartColumn}, + {end_line, EndLine} + ], error({Meta, [Formatted, Hint], []}, [], Scope, Tokens); tokenize([], Line, Column, #elixir_tokenizer{} = Scope, Tokens) -> diff --git a/lib/elixir/test/elixir/kernel/diagnostics_test.exs b/lib/elixir/test/elixir/kernel/diagnostics_test.exs index 07b1f1a603..184bb46a49 100644 --- a/lib/elixir/test/elixir/kernel/diagnostics_test.exs +++ b/lib/elixir/test/elixir/kernel/diagnostics_test.exs @@ -337,6 +337,140 @@ defmodule Kernel.DiagnosticsTest do end end + describe "token missing error" do + test "missing parens terminator" do + output = + capture_raise( + """ + my_numbers = [1, 2, 3, 4, 5, 6 + IO.inspect(my_numbers) + """, + TokenMissingError + ) + + assert output == """ + ** (TokenMissingError) token missing on nofile:2:23: + error: missing terminator: ] + │ + 1 │ my_numbers = [1, 2, 3, 4, 5, 6 + │ └ unclosed delimiter + 2 │ IO.inspect(my_numbers) + │ └ missing closing delimiter (expected "]") + │ + └─ nofile:2:23\ + """ + end + + test "shows in between lines if EOL is not far below" do + output = + capture_raise( + """ + my_numbers = [1, 2, 3, 4, 5, 6 + my_numbers + |> Enum.map(&(&1 + 1)) + |> Enum.map(&(&1 * &1)) + |> IO.inspect() + """, + TokenMissingError + ) + + assert output == """ + ** (TokenMissingError) token missing on nofile:5:16: + error: missing terminator: ] + │ + 1 │ my_numbers = [1, 2, 3, 4, 5, 6 + │ └ unclosed delimiter + 2 │ my_numbers + 3 │ |> Enum.map(&(&1 + 1)) + 4 │ |> Enum.map(&(&1 * &1)) + 5 │ |> IO.inspect() + │ └ missing closing delimiter (expected "]") + │ + └─ nofile:5:16\ + """ + end + + test "trims lines" do + output = + capture_raise( + """ + my_numbers = (1, 2, 3, 4, 5, 6 + + + + + + + IO.inspect(my_numbers) + """, + TokenMissingError + ) + + assert output == """ + ** (TokenMissingError) token missing on nofile:8:23: + error: missing terminator: ) + │ + 1 │ my_numbers = (1, 2, 3, 4, 5, 6 + │ └ unclosed delimiter + ... + 8 │ IO.inspect(my_numbers) + │ └ missing closing delimiter (expected ")") + │ + └─ nofile:8:23\ + """ + end + + test "shows the last non-empty line of a file" do + output = + capture_raise( + """ + my_numbers = {1, 2, 3, 4, 5, 6 + IO.inspect(my_numbers) + + + + + """, + TokenMissingError + ) + + assert output == """ + ** (TokenMissingError) token missing on nofile:2:23: + error: missing terminator: } + │ + 1 │ my_numbers = {1, 2, 3, 4, 5, 6 + │ └ unclosed delimiter + 2 │ IO.inspect(my_numbers) + │ └ missing closing delimiter (expected "}") + │ + └─ nofile:2:23\ + """ + end + + test "supports unicode" do + output = + capture_raise( + """ + my_emojis = [1, 2, 3, 4 # ⚗️ + IO.inspect(my_numbers) + """, + TokenMissingError + ) + + assert output == """ + ** (TokenMissingError) token missing on nofile:2:23: + error: missing terminator: ] + │ + 1 │ my_emojis = [1, 2, 3, 4 # ⚗️ + │ └ unclosed delimiter + 2 │ IO.inspect(my_numbers) + │ └ missing closing delimiter (expected "]") + │ + └─ nofile:2:23\ + """ + end + end + describe "compile-time exceptions" do test "SyntaxError (snippet)" do output = @@ -420,11 +554,16 @@ defmodule Kernel.DiagnosticsTest do """ end - test "TokenMissingError (no snippet)" do + test "TokenMissingError (unclosed delimiter)" do expected = """ - ** (TokenMissingError) token missing on nofile:2:1: - error: missing terminator: end (for "fn" starting at line 1) - └─ nofile:2:1\ + ** (TokenMissingError) token missing on nofile:1:5: + error: missing terminator: end + │ + 1 │ fn a + │ │ └ missing closing delimiter (expected "end") + │ └ unclosed delimiter + │ + └─ nofile:1:5\ """ output = diff --git a/lib/elixir/test/elixir/kernel/errors_test.exs b/lib/elixir/test/elixir/kernel/errors_test.exs index 4a54be931e..3335b5835a 100644 --- a/lib/elixir/test/elixir/kernel/errors_test.exs +++ b/lib/elixir/test/elixir/kernel/errors_test.exs @@ -473,7 +473,7 @@ defmodule Kernel.ErrorsTest do assert_eval_raise TokenMissingError, [ "nofile:1:5:", - ~r/missing terminator: end \(for "fn" starting at line 1\)/ + ~r/missing terminator: end/ ], ~c"fn 1" @@ -490,16 +490,16 @@ defmodule Kernel.ErrorsTest do assert_eval_raise TokenMissingError, [ "nofile:1:25:", - "missing terminator: end (for \"do\" starting at line 1)", + "missing terminator: end", "defmodule ShowSnippet do\n", - "^" + "└ unclosed delimiter" ], ~c"defmodule ShowSnippet do" end test "don't show snippet when error line is empty" do assert_eval_raise TokenMissingError, - ["nofile:3:1:", "missing terminator: end (for \"do\" starting at line 1)"], + ["nofile:1:25:", "missing terminator: end"], ~c"defmodule ShowSnippet do\n\n" end diff --git a/lib/elixir/test/elixir/kernel/parser_test.exs b/lib/elixir/test/elixir/kernel/parser_test.exs index 149498ec2e..e4ad788eae 100644 --- a/lib/elixir/test/elixir/kernel/parser_test.exs +++ b/lib/elixir/test/elixir/kernel/parser_test.exs @@ -494,7 +494,7 @@ defmodule Kernel.ParserTest do describe "token missing errors" do test "missing paren" do assert_token_missing( - ["nofile:1:9:", "missing terminator: ) (for \"(\" starting at line 1)"], + ["nofile:1:9:", "missing terminator: )"], ~c"case 1 (" ) end @@ -549,7 +549,7 @@ defmodule Kernel.ParserTest do test "missing end" do assert_token_missing( - ["nofile:1:9:", "missing terminator: end \(for \"do\" starting at line 1\)"], + ["nofile:1:9:", "missing terminator: end"], ~c"foo do 1" ) diff --git a/lib/ex_unit/test/ex_unit/doc_test_test.exs b/lib/ex_unit/test/ex_unit/doc_test_test.exs index 7319ddbb4d..21b3eec16b 100644 --- a/lib/ex_unit/test/ex_unit/doc_test_test.exs +++ b/lib/ex_unit/test/ex_unit/doc_test_test.exs @@ -790,15 +790,16 @@ defmodule ExUnit.DocTestTest do line = starting_line + 35 assert output =~ """ - 6) doctest ExUnit.DocTestTest.Invalid.misplaced_opaque_type/0 (6) (ExUnit.DocTestTest.InvalidCompiled) + 6) doctest ExUnit.DocTestTest.Invalid.misplaced_opaque_type/0 (6) (ExUnit.DocTestTest.InvalidCompiled) test/ex_unit/doc_test_test.exs:#{doctest_line} - Doctest did not compile, got: (TokenMissingError) token missing on test/ex_unit/doc_test_test.exs:#{line}:7: - error: missing terminator: } (for "{" starting at line #{line}) + Doctest did not compile, got: (TokenMissingError) token missing on test/ex_unit/doc_test_test.exs:#{line}:20: + error: missing terminator: } │ - #{line} │ {:ok, #Inspect<[]>} - │ ^ + 228 │ {:ok, #Inspect<[]>} + │ │ └ missing closing delimiter (expected "}") + │ └ unclosed delimiter │ - └─ test/ex_unit/doc_test_test.exs:#{line}:7 + └─ test/ex_unit/doc_test_test.exs:#{line}:20 If you are planning to assert on the result of an iex> expression which contains a value inspected as #Name<...>, please make sure the inspected value is placed at the beginning of the expression; otherwise Elixir will treat it as a comment due to the leading sign #. doctest: iex> {:ok, :oops} From 09da1ce57c2743efc8e7bf11624f585b4d762358 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 4 Dec 2023 22:07:32 +1000 Subject: [PATCH 0221/1886] Simplify offset handling in TokenMissingError --- lib/elixir/lib/exception.ex | 24 ++++-------------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/lib/elixir/lib/exception.ex b/lib/elixir/lib/exception.ex index 04eafb6236..3d91d1aed2 100644 --- a/lib/elixir/lib/exception.ex +++ b/lib/elixir/lib/exception.ex @@ -1260,20 +1260,11 @@ defmodule TokenMissingError do {lines, total_trimmed_lines} = handle_trailing_newlines(snippet) end_line = end_line - total_trimmed_lines - # For cases such as inside ExUnit doctests, our snippet is tiny, containing - # only the lines in the doctest, but the `line` and `end_line` we receive - # are still tied to the whole file. - # - # In these situations we use `line_offset` to treat `line` as 1 for - # operating on the snippet, while retaining the original line information. - should_use_line_offset? = is_nil(Enum.at(lines, end_line - 1)) - end_column = - if should_use_line_offset? do - fetch_line_length(lines, end_line - line_offset - 1) - else - fetch_line_length(lines, end_line - 1) - end + lines + |> Enum.fetch!(end_line - line_offset - 1) + |> String.length() + |> Kernel.+(1) start_pos = {line, column} end_pos = {end_line, end_column} @@ -1318,13 +1309,6 @@ defmodule TokenMissingError do {lines, total_trimmed_newlines} end - defp fetch_line_length(lines, index) do - lines - |> Enum.fetch!(index) - |> String.length() - |> Kernel.+(1) - end - defp format_message(file, line, column, message) do location = Exception.format_file_line_column(Path.relative_to_cwd(file), line, column) "token missing on " <> location <> "\n" <> message From b135a12aeb7454444c73156175a56fab294327f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 4 Dec 2023 22:27:47 +1000 Subject: [PATCH 0222/1886] Revert "Consider surround context until end whenever possible" This reverts commit a65dae971fcb44c5a845f7a8e0bbf14e3bfd2da4. Closes #13150. --- lib/elixir/lib/code/fragment.ex | 24 +--- lib/elixir/test/elixir/code_fragment_test.exs | 104 ++++++------------ 2 files changed, 40 insertions(+), 88 deletions(-) diff --git a/lib/elixir/lib/code/fragment.ex b/lib/elixir/lib/code/fragment.ex index 1e27c9c14d..d84cfb5b9c 100644 --- a/lib/elixir/lib/code/fragment.ex +++ b/lib/elixir/lib/code/fragment.ex @@ -636,7 +636,7 @@ defmodule Code.Fragment do {reversed_pre, post} = adjust_position(reversed_pre, post) case take_identifier(post, []) do - :none -> + {_, [], _} -> maybe_operator(reversed_pre, post, line, opts) {:identifier, reversed_post, rest} -> @@ -644,7 +644,7 @@ defmodule Code.Fragment do reversed = reversed_post ++ reversed_pre case codepoint_cursor_context(reversed, opts) do - {{:struct, acc}, offset} when acc != [] -> + {{:struct, acc}, offset} -> build_surround({:struct, acc}, reversed, line, offset) {{:alias, acc}, offset} -> @@ -749,27 +749,11 @@ defmodule Code.Fragment do do: take_identifier(t, [h | acc]) defp take_identifier(rest, acc) do - {stripped, _} = strip_spaces(rest, 0) - - with [?. | t] <- stripped, + with {[?. | t], _} <- strip_spaces(rest, 0), {[h | _], _} when h in ?A..?Z <- strip_spaces(t, 0) do take_alias(rest, acc) else - # Consider it an identifier if we are at the end of line - # or if we have spaces not followed by . (call) or / (arity) - _ when acc == [] and (rest == [] or (hd(rest) in @space and hd(stripped) not in ~c"/.")) -> - {:identifier, acc, rest} - - # If we are immediately followed by a container, we are still part of the identifier. - # We don't consider << as it _may_ be an operator. - _ when acc == [] and hd(stripped) in ~c"({[" -> - {:identifier, acc, rest} - - _ when acc == [] -> - :none - - _ -> - {:identifier, acc, rest} + _ -> {:identifier, acc, rest} end end diff --git a/lib/elixir/test/elixir/code_fragment_test.exs b/lib/elixir/test/elixir/code_fragment_test.exs index 41ad84ef40..5aa6800ae3 100644 --- a/lib/elixir/test/elixir/code_fragment_test.exs +++ b/lib/elixir/test/elixir/code_fragment_test.exs @@ -443,12 +443,11 @@ defmodule CodeFragmentTest do end test "column out of range" do - assert CF.surround_context("hello", {1, 20}) == - %{begin: {1, 1}, context: {:local_or_var, ~c"hello"}, end: {1, 6}} + assert CF.surround_context("hello", {1, 20}) == :none end test "local_or_var" do - for i <- 1..9 do + for i <- 1..8 do assert CF.surround_context("hello_wo", {1, i}) == %{ context: {:local_or_var, ~c"hello_wo"}, begin: {1, 1}, @@ -456,9 +455,9 @@ defmodule CodeFragmentTest do } end - assert CF.surround_context("hello_wo ", {1, 10}) == :none + assert CF.surround_context("hello_wo", {1, 9}) == :none - for i <- 2..10 do + for i <- 2..9 do assert CF.surround_context(" hello_wo", {1, i}) == %{ context: {:local_or_var, ~c"hello_wo"}, begin: {1, 2}, @@ -466,9 +465,9 @@ defmodule CodeFragmentTest do } end - assert CF.surround_context(" hello_wo ", {1, 11}) == :none + assert CF.surround_context(" hello_wo", {1, 10}) == :none - for i <- 1..7 do + for i <- 1..6 do assert CF.surround_context("hello!", {1, i}) == %{ context: {:local_or_var, ~c"hello!"}, begin: {1, 1}, @@ -476,9 +475,9 @@ defmodule CodeFragmentTest do } end - assert CF.surround_context("hello! ", {1, 8}) == :none + assert CF.surround_context("hello!", {1, 7}) == :none - for i <- 1..6 do + for i <- 1..5 do assert CF.surround_context("안녕_세상", {1, i}) == %{ context: {:local_or_var, ~c"안녕_세상"}, begin: {1, 1}, @@ -486,7 +485,7 @@ defmodule CodeFragmentTest do } end - assert CF.surround_context("안녕_세상 ", {1, 6}) == :none + assert CF.surround_context("안녕_세상", {1, 6}) == :none # Keywords are not local or var for keyword <- ~w(do end after catch else rescue fn true false nil)c do @@ -500,38 +499,8 @@ defmodule CodeFragmentTest do end end - test "local + operator" do - for i <- 1..8 do - assert CF.surround_context("hello_wo+", {1, i}) == %{ - context: {:local_or_var, ~c"hello_wo"}, - begin: {1, 1}, - end: {1, 9} - } - end - - assert CF.surround_context("hello_wo+", {1, 9}) == %{ - begin: {1, 9}, - context: {:operator, ~c"+"}, - end: {1, 10} - } - - for i <- 1..9 do - assert CF.surround_context("hello_wo +", {1, i}) == %{ - context: {:local_or_var, ~c"hello_wo"}, - begin: {1, 1}, - end: {1, 9} - } - end - - assert CF.surround_context("hello_wo +", {1, 10}) == %{ - begin: {1, 10}, - context: {:operator, ~c"+"}, - end: {1, 11} - } - end - test "local call" do - for i <- 1..9 do + for i <- 1..8 do assert CF.surround_context("hello_wo(", {1, i}) == %{ context: {:local_call, ~c"hello_wo"}, begin: {1, 1}, @@ -539,9 +508,9 @@ defmodule CodeFragmentTest do } end - assert CF.surround_context("hello_wo(", {1, 10}) == :none + assert CF.surround_context("hello_wo(", {1, 9}) == :none - for i <- 1..9 do + for i <- 1..8 do assert CF.surround_context("hello_wo (", {1, i}) == %{ context: {:local_call, ~c"hello_wo"}, begin: {1, 1}, @@ -549,10 +518,9 @@ defmodule CodeFragmentTest do } end - assert CF.surround_context("hello_wo (", {1, 10}) == :none - assert CF.surround_context("hello_wo (", {1, 11}) == :none + assert CF.surround_context("hello_wo (", {1, 9}) == :none - for i <- 1..7 do + for i <- 1..6 do assert CF.surround_context("hello!(", {1, i}) == %{ context: {:local_call, ~c"hello!"}, begin: {1, 1}, @@ -560,9 +528,9 @@ defmodule CodeFragmentTest do } end - assert CF.surround_context("hello!(", {1, 8}) == :none + assert CF.surround_context("hello!(", {1, 7}) == :none - for i <- 1..6 do + for i <- 1..5 do assert CF.surround_context("안녕_세상(", {1, i}) == %{ context: {:local_call, ~c"안녕_세상"}, begin: {1, 1}, @@ -570,7 +538,7 @@ defmodule CodeFragmentTest do } end - assert CF.surround_context("안녕_세상(", {1, 7}) == :none + assert CF.surround_context("안녕_세상(", {1, 6}) == :none end test "local arity" do @@ -698,7 +666,7 @@ defmodule CodeFragmentTest do end test "alias" do - for i <- 1..9 do + for i <- 1..8 do assert CF.surround_context("HelloWor", {1, i}) == %{ context: {:alias, ~c"HelloWor"}, begin: {1, 1}, @@ -706,9 +674,9 @@ defmodule CodeFragmentTest do } end - assert CF.surround_context("HelloWor ", {1, 10}) == :none + assert CF.surround_context("HelloWor", {1, 9}) == :none - for i <- 2..10 do + for i <- 2..9 do assert CF.surround_context(" HelloWor", {1, i}) == %{ context: {:alias, ~c"HelloWor"}, begin: {1, 2}, @@ -716,9 +684,9 @@ defmodule CodeFragmentTest do } end - assert CF.surround_context(" HelloWor ", {1, 11}) == :none + assert CF.surround_context(" HelloWor", {1, 10}) == :none - for i <- 1..10 do + for i <- 1..9 do assert CF.surround_context("Hello.Wor", {1, i}) == %{ context: {:alias, ~c"Hello.Wor"}, begin: {1, 1}, @@ -726,9 +694,9 @@ defmodule CodeFragmentTest do } end - assert CF.surround_context("Hello.Wor ", {1, 11}) == :none + assert CF.surround_context("Hello.Wor", {1, 10}) == :none - for i <- 1..12 do + for i <- 1..11 do assert CF.surround_context("Hello . Wor", {1, i}) == %{ context: {:alias, ~c"Hello.Wor"}, begin: {1, 1}, @@ -736,9 +704,9 @@ defmodule CodeFragmentTest do } end - assert CF.surround_context("Hello . Wor ", {1, 13}) == :none + assert CF.surround_context("Hello . Wor", {1, 12}) == :none - for i <- 1..16 do + for i <- 1..15 do assert CF.surround_context("Foo . Bar . Baz", {1, i}) == %{ context: {:alias, ~c"Foo.Bar.Baz"}, begin: {1, 1}, @@ -770,7 +738,7 @@ defmodule CodeFragmentTest do end: {1, 11} } - for i <- 1..15 do + for i <- 1..14 do assert CF.surround_context("__MODULE__.Foo", {1, i}) == %{ context: {:alias, {:local_or_var, ~c"__MODULE__"}, ~c"Foo"}, begin: {1, 1}, @@ -778,7 +746,7 @@ defmodule CodeFragmentTest do } end - for i <- 1..19 do + for i <- 1..18 do assert CF.surround_context("__MODULE__.Foo.Sub", {1, i}) == %{ context: {:alias, {:local_or_var, ~c"__MODULE__"}, ~c"Foo.Sub"}, begin: {1, 1}, @@ -830,7 +798,7 @@ defmodule CodeFragmentTest do end test "attribute submodules" do - for i <- 1..10 do + for i <- 1..9 do assert CF.surround_context("@some.Foo", {1, i}) == %{ context: {:alias, {:module_attribute, ~c"some"}, ~c"Foo"}, begin: {1, 1}, @@ -838,7 +806,7 @@ defmodule CodeFragmentTest do } end - for i <- 1..14 do + for i <- 1..13 do assert CF.surround_context("@some.Foo.Sub", {1, i}) == %{ context: {:alias, {:module_attribute, ~c"some"}, ~c"Foo.Sub"}, begin: {1, 1}, @@ -921,7 +889,7 @@ defmodule CodeFragmentTest do end: {1, 15} } - for i <- 2..10 do + for i <- 2..9 do assert CF.surround_context("%HelloWor", {1, i}) == %{ context: {:struct, ~c"HelloWor"}, begin: {1, 1}, @@ -929,7 +897,7 @@ defmodule CodeFragmentTest do } end - assert CF.surround_context("%HelloWor ", {1, 11}) == :none + assert CF.surround_context("%HelloWor", {1, 10}) == :none # With dot assert CF.surround_context("%Hello.Wor", {1, 1}) == %{ @@ -938,7 +906,7 @@ defmodule CodeFragmentTest do end: {1, 11} } - for i <- 2..11 do + for i <- 2..10 do assert CF.surround_context("%Hello.Wor", {1, i}) == %{ context: {:struct, ~c"Hello.Wor"}, begin: {1, 1}, @@ -946,7 +914,7 @@ defmodule CodeFragmentTest do } end - assert CF.surround_context("%Hello.Wor ", {1, 12}) == :none + assert CF.surround_context("%Hello.Wor", {1, 11}) == :none # With spaces assert CF.surround_context("% Hello . Wor", {1, 1}) == %{ @@ -955,7 +923,7 @@ defmodule CodeFragmentTest do end: {1, 14} } - for i <- 2..14 do + for i <- 2..13 do assert CF.surround_context("% Hello . Wor", {1, i}) == %{ context: {:struct, ~c"Hello.Wor"}, begin: {1, 1}, @@ -963,7 +931,7 @@ defmodule CodeFragmentTest do } end - assert CF.surround_context("% Hello . Wor ", {1, 15}) == :none + assert CF.surround_context("% Hello . Wor", {1, 14}) == :none end test "module attributes" do From 1bdcaa4af02cb7b6020007d61d9322f7e7dda5e1 Mon Sep 17 00:00:00 2001 From: Joe Yates Date: Wed, 6 Dec 2023 12:46:54 +0100 Subject: [PATCH 0223/1886] Enrich option parser docs for unknown switches (#13155) --- lib/elixir/lib/option_parser.ex | 57 +++++++++++++++++---------------- 1 file changed, 30 insertions(+), 27 deletions(-) diff --git a/lib/elixir/lib/option_parser.ex b/lib/elixir/lib/option_parser.ex index b1006f8217..5d62eb2bc7 100644 --- a/lib/elixir/lib/option_parser.ex +++ b/lib/elixir/lib/option_parser.ex @@ -152,44 +152,47 @@ defmodule OptionParser do ### Parsing unknown switches When the `:switches` option is given, `OptionParser` will attempt to parse - unknown switches: + unknown switches. + + Switches without an argument will be set to `true`: iex> OptionParser.parse(["--debug"], switches: [key: :string]) {[debug: true], [], []} Even though we haven't specified `--debug` in the list of switches, it is part - of the returned options. This would also work: + of the returned options. The same happens for switches followed by another switch: + + iex> OptionParser.parse(["--debug", "--ok"], switches: []) + {[debug: true, ok: true], [], []} + + Switches followed by a value will be assigned the value, as a string: iex> OptionParser.parse(["--debug", "value"], switches: [key: :string]) {[debug: "value"], [], []} - Switches followed by a value will be assigned the value, as a string. Switches - without an argument will be set automatically to `true`. Since we cannot assert - the type of the switch value, it is preferred to use the `:strict` option that - accepts only known switches and always verify their types. + Since we cannot assert the type of the switch value, it is preferred to use the + `:strict` option that accepts only known switches and always verify their types. If you do want to parse unknown switches, remember that Elixir converts switches - to atoms. Since atoms are not garbage-collected, OptionParser will only parse - switches that translate to atoms used by the runtime to avoid leaking atoms. - For instance, the code below will discard the `--option-parser-example` switch - because the `:option_parser_example` atom is never used anywhere: - - OptionParser.parse(["--option-parser-example"], switches: [debug: :boolean]) - # The :option_parser_example atom is not used anywhere below - - However, the code below would work as long as `:option_parser_example` atom is - used at some point later (or earlier) **in the same module**. For example: - - {opts, _, _} = OptionParser.parse(["--option-parser-example"], switches: [debug: :boolean]) - # ... then somewhere in the same module you access it ... - opts[:option_parser_example] - - In other words, Elixir will only parse options that are used by the runtime, - ignoring all others. If you would like to parse all switches, regardless if - they exist or not, you can force creation of atoms by passing - `allow_nonexistent_atoms: true` as option. Use this option with care. It is - only useful when you are building command-line applications that receive - dynamically-named arguments and must be avoided in long-running systems. + to atoms. Since atoms are not garbage-collected, to avoid creating new ones, + OptionParser by default only parses switches that translate to existing atoms. + The code below discards the `--option-parser-example` switch because the + `:option_parser_example` atom is never used anywhere: + + iex> OptionParser.parse(["--option-parser-example"], switches: []) + {[], [], []} + + If a switch corresponds to an existing Elixir atom, whether from your + code, a dependency or from Elixir itself, it will be accepted. However, + it is best to not rely on external code, and always define the atoms + you want to parse in the same module that calls `OptionParser` itself, + as direct arguments to the `:switches` or `:strict` options. + + If you would like to parse all switches, regardless if they exist or not, + you can force creation of atoms by passing `allow_nonexistent_atoms: true` + as option. Use this option with care. It is only useful when you are building + command-line applications that receive dynamically-named arguments and must + be avoided in long-running systems. ## Aliases From 9b7c897f1093f9f9aae384c7529dfc7ace4aec07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 6 Dec 2023 23:26:51 +1100 Subject: [PATCH 0224/1886] Clarify running mode of @after_verify --- lib/elixir/lib/module.ex | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/elixir/lib/module.ex b/lib/elixir/lib/module.ex index 087c04f1a0..41ba84bc0c 100644 --- a/lib/elixir/lib/module.ex +++ b/lib/elixir/lib/module.ex @@ -538,7 +538,9 @@ defmodule Module do undefined functions, deprecations, etc. A module is always verified after it is compiled. In Mix projects, a module is also verified when any of its runtime dependencies change. Therefore this is useful to perform verification - of the current module while avoiding compile-time dependencies. + of the current module while avoiding compile-time dependencies. Given the + callback is invoked under different scenarios, Elixir provides no guarantees + of when in the compilation cycle nor in which process the callback runs. Accepts a module or a `{module, function_name}` tuple. The function must take one argument: the module name. When just a module is provided, From 374edd8fb45c9bc4be47b85c1504a08770a3c0db Mon Sep 17 00:00:00 2001 From: Wojtek Mach Date: Wed, 6 Dec 2023 22:43:53 +0100 Subject: [PATCH 0225/1886] Update Windows installer to register in Add/Remove Programs (#13156) --- .../assets/{drop.ico => Elixir.ico} | Bin lib/elixir/scripts/windows_installer/build.sh | 38 +++++------------- .../scripts/windows_installer/installer.nsi | 12 +++++- 3 files changed, 22 insertions(+), 28 deletions(-) rename lib/elixir/scripts/windows_installer/assets/{drop.ico => Elixir.ico} (100%) diff --git a/lib/elixir/scripts/windows_installer/assets/drop.ico b/lib/elixir/scripts/windows_installer/assets/Elixir.ico similarity index 100% rename from lib/elixir/scripts/windows_installer/assets/drop.ico rename to lib/elixir/scripts/windows_installer/assets/Elixir.ico diff --git a/lib/elixir/scripts/windows_installer/build.sh b/lib/elixir/scripts/windows_installer/build.sh index e9b51e91f7..117fc51772 100755 --- a/lib/elixir/scripts/windows_installer/build.sh +++ b/lib/elixir/scripts/windows_installer/build.sh @@ -4,43 +4,27 @@ # With Elixir archive: # # ELIXIR_ZIP=Precompiled.zip OTP_VERSION=25.3.2.2 ./build.sh -# -# With Elixir version: -# -# ELIXIR_VERSION=1.14.5 OTP_VERSION=25.3.2.2 ./build.sh set -euo pipefail -OTP_VERSION="${OTP_VERSION:-26.0}" -otp_release=`echo "${OTP_VERSION}" | cut -d. -f1` - mkdir -p tmp +rm -rf tmp/elixir +unzip -d "tmp/elixir" "${ELIXIR_ZIP}" -ELIXIR_VERSION="${ELIXIR_VERSION:-}" -if [ -n "${ELIXIR_VERSION}" ]; then - ELIXIR_ZIP="tmp/elixir-${ELIXIR_VERSION}-otp-${otp_release}.zip" - if [ ! -f "${ELIXIR_ZIP}" ]; then - url="https://github.com/elixir-lang/elixir/releases/download/v${ELIXIR_VERSION}/elixir-otp-${otp_release}.zip" - echo "downloading ${url}" - curl --fail -L -o "${ELIXIR_ZIP}" "${url}" - fi - basename=elixir-${ELIXIR_VERSION}-otp-${otp_release} -else - basename=elixir-otp-${otp_release} -fi - -if [ ! -d "tmp/${basename}" ]; then - unzip -d "tmp/${basename}" "${ELIXIR_ZIP}" -fi +elixir_version=`cat tmp/elixir/VERSION` +otp_release=`erl -noshell -eval 'io:put_chars(erlang:system_info(otp_release)), halt().'` +otp_version=`erl -noshell -eval '{ok, Vsn} = file:read_file(code:root_dir() ++ "/releases/" ++ erlang:system_info(otp_release) ++ "/OTP_VERSION"), io:put_chars(Vsn), halt().'` +elixir_exe=elixir-otp-${otp_release}.exe # brew install makensis # apt install -y nsis # choco install -y nsis export PATH="/c/Program Files (x86)/NSIS:${PATH}" makensis \ - -X"OutFile tmp\\${basename}.exe" \ - -DOTP_VERSION=${OTP_VERSION} \ + -X"OutFile tmp\\${elixir_exe}" \ -DOTP_RELEASE="${otp_release}" \ - -DELIXIR_DIR=tmp\\${basename} \ + -DOTP_VERSION=${otp_version} \ + -DELIXIR_DIR=tmp\\elixir \ + -DELIXIR_VERSION=${elixir_version} \ installer.nsi -echo "Installer path: tmp/${basename}.exe" +echo "Installer path: tmp/${elixir_exe}" diff --git a/lib/elixir/scripts/windows_installer/installer.nsi b/lib/elixir/scripts/windows_installer/installer.nsi index a9e8495ea6..8df40aad15 100644 --- a/lib/elixir/scripts/windows_installer/installer.nsi +++ b/lib/elixir/scripts/windows_installer/installer.nsi @@ -6,7 +6,8 @@ Name "Elixir" ManifestDPIAware true Unicode True InstallDir "$PROGRAMFILES64\Elixir" -!define MUI_ICON "assets\drop.ico" +!define MUI_ICON "assets\Elixir.ico" +!define MUI_UNICON "assets\Elixir.ico" ; Install Page: Install Erlang/OTP @@ -191,7 +192,15 @@ FunctionEnd Section "Install Elixir" SectionElixir SetOutPath "$INSTDIR" File /r "${ELIXIR_DIR}\" + File "assets\Elixir.ico" File "update_system_path.erl" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Elixir" "DisplayName" "Elixir" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Elixir" "DisplayVersion" "${ELIXIR_VERSION}" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Elixir" "DisplayIcon" "$INSTDIR\Elixir.ico" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Elixir" "Publisher" "The Elixir Team" + WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Elixir" "UninstallString" '"$INSTDIR\Uninstall.exe"' + WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Elixir" "NoModify" 1 + WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Elixir" "NoRepair" 1 WriteUninstaller "Uninstall.exe" SectionEnd @@ -270,6 +279,7 @@ UninstPage custom un.FinishPageShow un.FinishPageLeave Section "Uninstall" RMDir /r "$INSTDIR" + DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Elixir" SectionEnd !insertmacro MUI_LANGUAGE "English" From cc9e986bde0ca0fcb2aaeb8383396712833ec738 Mon Sep 17 00:00:00 2001 From: Wojtek Mach Date: Thu, 7 Dec 2023 00:28:04 +0100 Subject: [PATCH 0226/1886] Update Windows installer to write Elixir install root to registry (#13157) We don't need this right now but it could be useful in the future, if anything to detect if Elixir was installed using this installer. Demo: iex> {:ok, r} = :win32reg.open([:read]) iex> :win32reg.change_key(r, ~c"\\hklm\\software\\wow6432node\\elixir\\elixir") iex> :win32reg.value(r, ~c"installroot") {:ok, ~c"C:\\Program Files\\Elixir"} --- lib/elixir/scripts/windows_installer/installer.nsi | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/elixir/scripts/windows_installer/installer.nsi b/lib/elixir/scripts/windows_installer/installer.nsi index 8df40aad15..d7d4c9bae9 100644 --- a/lib/elixir/scripts/windows_installer/installer.nsi +++ b/lib/elixir/scripts/windows_installer/installer.nsi @@ -202,6 +202,8 @@ Section "Install Elixir" SectionElixir WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Elixir" "NoModify" 1 WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Elixir" "NoRepair" 1 + WriteRegStr HKLM "Software\Elixir\Elixir" "InstallRoot" "$INSTDIR" + WriteUninstaller "Uninstall.exe" SectionEnd @@ -280,6 +282,8 @@ UninstPage custom un.FinishPageShow un.FinishPageLeave Section "Uninstall" RMDir /r "$INSTDIR" DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Elixir" + DeleteRegKey HKLM "Software\Elixir\Elixir" + DeleteRegKey /ifempty HKLM "Software\Elixir" SectionEnd !insertmacro MUI_LANGUAGE "English" From 6acc1740bc9b53796bcd9095f974fc315dad7b24 Mon Sep 17 00:00:00 2001 From: Michael Johnston Date: Fri, 8 Dec 2023 18:49:55 -0800 Subject: [PATCH 0227/1886] clarify treatment of whitespace in extended regexes (#13160) --- lib/elixir/lib/regex.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/lib/regex.ex b/lib/elixir/lib/regex.ex index 0094298b45..7b7f1b8332 100644 --- a/lib/elixir/lib/regex.ex +++ b/lib/elixir/lib/regex.ex @@ -84,7 +84,7 @@ defmodule Regex do each line; use `\A` and `\z` to match the end or beginning of the string * `:extended` (x) - whitespace characters are ignored except when escaped - and allow `#` to delimit comments + or within `[..]`, and allow `#` to delimit comments * `:firstline` (f) - forces the unanchored pattern to match before or at the first newline, though the matched text may continue over the newline From ed2bbe5b37e9b06d83b7a17629dc359d63dcc4a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sat, 9 Dec 2023 20:39:38 +1100 Subject: [PATCH 0228/1886] Pass original exception down to details in diagnostic, closes #13142 --- lib/elixir/lib/code.ex | 1 + lib/elixir/lib/kernel/parallel_compiler.ex | 6 +++++- lib/mix/lib/mix/compilers/elixir.ex | 21 ++++++++++++--------- lib/mix/lib/mix/task.compiler.ex | 2 +- lib/mix/test/mix/tasks/compile_test.exs | 3 ++- 5 files changed, 21 insertions(+), 12 deletions(-) diff --git a/lib/elixir/lib/code.ex b/lib/elixir/lib/code.ex index 67df43483e..9ab9c8cf59 100644 --- a/lib/elixir/lib/code.ex +++ b/lib/elixir/lib/code.ex @@ -204,6 +204,7 @@ defmodule Code do required(:position) => position, required(:stacktrace) => Exception.stacktrace(), required(:span) => {non_neg_integer, non_neg_integer} | nil, + optional(:exception) => Exception.t() | nil, optional(any()) => any() } diff --git a/lib/elixir/lib/kernel/parallel_compiler.ex b/lib/elixir/lib/kernel/parallel_compiler.ex index b86d8dc38f..ee5e3c7d3e 100644 --- a/lib/elixir/lib/kernel/parallel_compiler.ex +++ b/lib/elixir/lib/kernel/parallel_compiler.ex @@ -900,10 +900,14 @@ defmodule Kernel.ParallelCompiler do message: message, severity: :error, stacktrace: stack, - span: nil + span: nil, + exception: get_exception(reason) } end + defp get_exception(exception) when is_exception(exception), do: exception + defp get_exception(_reason), do: nil + defp get_line(_file, %{line: line, column: column}, _stack) when is_integer(line) and line > 0 and is_integer(column) and column >= 0 do {line, column} diff --git a/lib/mix/lib/mix/compilers/elixir.ex b/lib/mix/lib/mix/compilers/elixir.ex index 7f7bbb3298..22e2686aff 100644 --- a/lib/mix/lib/mix/compilers/elixir.ex +++ b/lib/mix/lib/mix/compilers/elixir.ex @@ -767,14 +767,16 @@ defmodule Mix.Compilers.Elixir do end end - defp diagnostic(%{ - file: file, - position: position, - message: message, - severity: severity, - stacktrace: stacktrace, - span: span - }) do + defp diagnostic( + %{ + file: file, + position: position, + message: message, + severity: severity, + stacktrace: stacktrace, + span: span + } = diagnostic + ) do %Mix.Task.Compiler.Diagnostic{ file: file, position: position, @@ -782,7 +784,8 @@ defmodule Mix.Compilers.Elixir do severity: severity, compiler_name: "Elixir", stacktrace: stacktrace, - span: span + span: span, + details: Map.get(diagnostic, :exception, nil) } end diff --git a/lib/mix/lib/mix/task.compiler.ex b/lib/mix/lib/mix/task.compiler.ex index be968b2ee2..9c80400f81 100644 --- a/lib/mix/lib/mix/task.compiler.ex +++ b/lib/mix/lib/mix/task.compiler.ex @@ -39,7 +39,7 @@ defmodule Mix.Task.Compiler do message: IO.chardata(), position: position, compiler_name: String.t(), - details: any, + details: Exception.t() | any, stacktrace: Exception.stacktrace(), span: {non_neg_integer, non_neg_integer} | nil } diff --git a/lib/mix/test/mix/tasks/compile_test.exs b/lib/mix/test/mix/tasks/compile_test.exs index ccc969257f..cb73f993a2 100644 --- a/lib/mix/test/mix/tasks/compile_test.exs +++ b/lib/mix/test/mix/tasks/compile_test.exs @@ -182,7 +182,8 @@ defmodule Mix.Tasks.CompileTest do severity: :error, position: {2, 20}, message: "** (SyntaxError) invalid syntax found on lib/a.ex:2:" <> _, - compiler_name: "Elixir" + compiler_name: "Elixir", + details: %SyntaxError{} } = diagnostic end) end) From aabe46536e021da88de39bc5c82c90d97b0604ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sat, 9 Dec 2023 20:49:12 +1100 Subject: [PATCH 0229/1886] Disable compiler optimizations only in module body --- lib/elixir/src/elixir_compiler.erl | 12 ++++++------ lib/elixir/src/elixir_module.erl | 4 +++- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/lib/elixir/src/elixir_compiler.erl b/lib/elixir/src/elixir_compiler.erl index 487d3b3fa5..980aa9e0e8 100644 --- a/lib/elixir/src/elixir_compiler.erl +++ b/lib/elixir/src/elixir_compiler.erl @@ -1,6 +1,6 @@ %% Elixir compiler front-end to the Erlang backend. -module(elixir_compiler). --export([string/3, quoted/3, bootstrap/0, file/2, compile/3]). +-export([string/3, quoted/3, bootstrap/0, file/2, compile/4]). -include("elixir.hrl"). string(Contents, File, Callback) -> @@ -36,22 +36,22 @@ maybe_fast_compile(Forms, Args, E) -> case (?key(E, module) == nil) andalso allows_fast_compilation(Forms) andalso (not elixir_config:is_bootstrap()) of true -> fast_compile(Forms, E); - false -> compile(Forms, Args, E) + false -> compile(Forms, Args, [], E) end, ok. -compile(Quoted, ArgsList, #{line := Line} = E) -> +compile(Quoted, ArgsList, CompilerOpts, #{line := Line} = E) -> Block = no_tail_optimize([{line, Line}], Quoted), {Expanded, SE, EE} = elixir_expand:expand(Block, elixir_env:env_to_ex(E), E), elixir_env:check_unused_vars(SE, EE), {Module, Fun, Purgeable} = - elixir_erl_compiler:spawn(fun() -> spawned_compile(Expanded, E) end), + elixir_erl_compiler:spawn(fun() -> spawned_compile(Expanded, CompilerOpts, E) end), Args = list_to_tuple(ArgsList), {dispatch(Module, Fun, Args, Purgeable), SE, EE}. -spawned_compile(ExExprs, #{line := Line, file := File} = E) -> +spawned_compile(ExExprs, CompilerOpts, #{line := Line, file := File} = E) -> {Vars, S} = elixir_erl_var:from_env(E), {ErlExprs, _} = elixir_erl_pass:translate(ExExprs, erl_anno:new(Line), S), @@ -59,7 +59,7 @@ spawned_compile(ExExprs, #{line := Line, file := File} = E) -> Fun = code_fun(?key(E, module)), Forms = code_mod(Fun, ErlExprs, Line, File, Module, Vars), - {Module, Binary} = elixir_erl_compiler:noenv_forms(Forms, File, [nowarn_nomatch, no_bool_opt, no_ssa_opt]), + {Module, Binary} = elixir_erl_compiler:noenv_forms(Forms, File, [nowarn_nomatch | CompilerOpts]), code:load_binary(Module, "", Binary), {Module, Fun, is_purgeable(Module, Binary)}. diff --git a/lib/elixir/src/elixir_module.erl b/lib/elixir/src/elixir_module.erl index 1cee92e4a1..637598d6f2 100644 --- a/lib/elixir/src/elixir_module.erl +++ b/lib/elixir/src/elixir_module.erl @@ -421,7 +421,9 @@ build(Module, Line, File, E) -> %% Handles module and callback evaluations. eval_form(Line, Module, DataBag, Block, Vars, Prune, E) -> - {Value, ExS, EE} = elixir_compiler:compile(Block, Vars, E), + %% Given Elixir modules can get very long to compile due to metaprogramming, + %% we disable expansions that take linear time to code size. + {Value, ExS, EE} = elixir_compiler:compile(Block, Vars, [no_bool_opt, no_ssa_opt], E), elixir_overridable:store_not_overridden(Module), EV = (elixir_env:reset_vars(EE))#{line := Line}, EC = eval_callbacks(Line, DataBag, before_compile, [EV], EV), From 4a7585ff6f7f78f5e8ac75ae59fa3037f9d21b0a Mon Sep 17 00:00:00 2001 From: Daven <77761194+fmterrorf@users.noreply.github.com> Date: Sun, 10 Dec 2023 01:58:10 -0600 Subject: [PATCH 0230/1886] Add warning when deps clean fails (#13161) --- lib/mix/lib/mix/tasks/deps.clean.ex | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/lib/mix/lib/mix/tasks/deps.clean.ex b/lib/mix/lib/mix/tasks/deps.clean.ex index f09cde8ad0..cc8313c115 100644 --- a/lib/mix/lib/mix/tasks/deps.clean.ex +++ b/lib/mix/lib/mix/tasks/deps.clean.ex @@ -99,6 +99,27 @@ defmodule Mix.Tasks.Deps.Clean do paths end + defp maybe_warn_failed_file_deletion(results, dependency) when is_list(results) do + messages = + Enum.flat_map(results, fn + {:error, reason, file} -> + ["\tfile: #{file}, reason: #{:file.format_error(reason)}"] + + _ -> + [] + end) + + with [_ | _] <- messages do + Mix.shell().error( + "warning: errors occurred while deleting files for dependency: #{dependency} \n" <> + Enum.join(messages, "\n") + ) + end + end + + defp maybe_warn_failed_file_deletion(result, dependency), + do: maybe_warn_failed_file_deletion([result], dependency) + defp do_clean(apps, deps, build_path, deps_path, build_only?) do shell = Mix.shell() @@ -112,7 +133,8 @@ defmodule Mix.Tasks.Deps.Clean do |> Path.join(to_string(app)) |> Path.wildcard() |> maybe_warn_for_invalid_path(app) - |> Enum.each(&File.rm_rf!/1) + |> Enum.map(&File.rm_rf/1) + |> maybe_warn_failed_file_deletion(app) # Remove everything from the source directory of dependencies. # Skip this step if --build option is specified or if @@ -122,7 +144,8 @@ defmodule Mix.Tasks.Deps.Clean do else deps_path |> Path.join(to_string(app)) - |> File.rm_rf!() + |> File.rm_rf() + |> maybe_warn_failed_file_deletion(app) end end) end From 9a5a83360f7100b7b6a56703b86fd41ffb56b3ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sun, 10 Dec 2023 09:03:41 +0100 Subject: [PATCH 0231/1886] Clean up failed deletion warning --- lib/mix/lib/mix/tasks/deps.clean.ex | 25 ++++++------------------- 1 file changed, 6 insertions(+), 19 deletions(-) diff --git a/lib/mix/lib/mix/tasks/deps.clean.ex b/lib/mix/lib/mix/tasks/deps.clean.ex index cc8313c115..c9edfa7d41 100644 --- a/lib/mix/lib/mix/tasks/deps.clean.ex +++ b/lib/mix/lib/mix/tasks/deps.clean.ex @@ -99,27 +99,15 @@ defmodule Mix.Tasks.Deps.Clean do paths end - defp maybe_warn_failed_file_deletion(results, dependency) when is_list(results) do - messages = - Enum.flat_map(results, fn - {:error, reason, file} -> - ["\tfile: #{file}, reason: #{:file.format_error(reason)}"] - - _ -> - [] - end) - - with [_ | _] <- messages do + defp maybe_warn_failed_file_deletion(result) do + with {:error, reason, file} <- result do Mix.shell().error( - "warning: errors occurred while deleting files for dependency: #{dependency} \n" <> - Enum.join(messages, "\n") + "warning: could not delete file #{Path.relative_to_cwd(file)}, " <> + "reason: #{:file.format_error(reason)}" ) end end - defp maybe_warn_failed_file_deletion(result, dependency), - do: maybe_warn_failed_file_deletion([result], dependency) - defp do_clean(apps, deps, build_path, deps_path, build_only?) do shell = Mix.shell() @@ -133,8 +121,7 @@ defmodule Mix.Tasks.Deps.Clean do |> Path.join(to_string(app)) |> Path.wildcard() |> maybe_warn_for_invalid_path(app) - |> Enum.map(&File.rm_rf/1) - |> maybe_warn_failed_file_deletion(app) + |> Enum.map(&(&1 |> File.rm_rf() |> maybe_warn_failed_file_deletion())) # Remove everything from the source directory of dependencies. # Skip this step if --build option is specified or if @@ -145,7 +132,7 @@ defmodule Mix.Tasks.Deps.Clean do deps_path |> Path.join(to_string(app)) |> File.rm_rf() - |> maybe_warn_failed_file_deletion(app) + |> maybe_warn_failed_file_deletion() end end) end From 7095b2c1b6e9a600b3a5e171fc3f1d44dfb4ae13 Mon Sep 17 00:00:00 2001 From: Andrea Leopardi Date: Sun, 10 Dec 2023 09:20:06 +0100 Subject: [PATCH 0232/1886] Add "available since" to for comprehension docs --- lib/elixir/lib/kernel/special_forms.ex | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/elixir/lib/kernel/special_forms.ex b/lib/elixir/lib/kernel/special_forms.ex index b9f1e1d746..b307cc5970 100644 --- a/lib/elixir/lib/kernel/special_forms.ex +++ b/lib/elixir/lib/kernel/special_forms.ex @@ -1494,6 +1494,8 @@ defmodule Kernel.SpecialForms do ## The `:reduce` option + *Available since Elixir v1.8*. + While the `:into` option allows us to customize the comprehension behaviour to a given data type, such as putting all of the values inside a map or inside a binary, it is not always enough. From fcacbfa40143b9877e0f79311e41e6505f81800e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 11 Dec 2023 07:28:35 +0100 Subject: [PATCH 0233/1886] Use Macro.Env to record attribute warnings Closes #13162. Closes #13164. --- lib/elixir/lib/module.ex | 3 +- .../test/elixir/kernel/warning_test.exs | 35 ++++++++++++++----- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/lib/elixir/lib/module.ex b/lib/elixir/lib/module.ex index 41ba84bc0c..6b0aca3a95 100644 --- a/lib/elixir/lib/module.ex +++ b/lib/elixir/lib/module.ex @@ -2135,8 +2135,7 @@ defmodule Module do end defp attribute_stack(module, line) do - file = String.to_charlist(Path.relative_to_cwd(:elixir_module.file(module))) - [{module, :__MODULE__, 0, file: file, line: line}] + struct!(Macro.Env, module: module, file: :elixir_module.file(module), line: line) end ## Helpers diff --git a/lib/elixir/test/elixir/kernel/warning_test.exs b/lib/elixir/test/elixir/kernel/warning_test.exs index 518da3c4c8..5196bd0b4e 100644 --- a/lib/elixir/test/elixir/kernel/warning_test.exs +++ b/lib/elixir/test/elixir/kernel/warning_test.exs @@ -1640,22 +1640,39 @@ defmodule Kernel.WarningTest do end test "reserved doc metadata keys" do - output = - capture_eval(""" - defmodule Sample do - @typedoc opaque: false - @type t :: binary + {output, diagnostics} = + Code.with_diagnostics([log: true], fn -> + capture_eval(""" + defmodule Sample do + @typedoc opaque: false + @type t :: binary - @doc defaults: 3, since: "1.2.3" - def foo(a), do: a - end - """) + @doc defaults: 3, since: "1.2.3" + def foo(a), do: a + end + """) + end) assert output =~ "ignoring reserved documentation metadata key: :opaque" assert output =~ "nofile:2: " assert output =~ "ignoring reserved documentation metadata key: :defaults" assert output =~ "nofile:5: " refute output =~ ":since" + + assert [ + %{ + message: "ignoring reserved documentation metadata key: :opaque", + position: 2, + file: "nofile", + severity: :warning + }, + %{ + message: "ignoring reserved documentation metadata key: :defaults", + position: 5, + file: "nofile", + severity: :warning + } + ] = diagnostics after purge(Sample) end From 8f64ca0287da4bc2223c99017c45d34b2c70b5a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 11 Dec 2023 11:13:24 +0100 Subject: [PATCH 0234/1886] Use Macro.Env in more warnings --- lib/elixir/lib/kernel.ex | 2 +- lib/elixir/lib/kernel/utils.ex | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/elixir/lib/kernel.ex b/lib/elixir/lib/kernel.ex index 3383a74ad2..5c359c3c17 100644 --- a/lib/elixir/lib/kernel.ex +++ b/lib/elixir/lib/kernel.ex @@ -3622,7 +3622,7 @@ defmodule Kernel do defp do_at([], meta, name, function?, env) do IO.warn( "the @#{name}() notation (with parentheses) is deprecated, please use @#{name} (without parentheses) instead", - Macro.Env.stacktrace(env) + env ) do_at(nil, meta, name, function?, env) diff --git a/lib/elixir/lib/kernel/utils.ex b/lib/elixir/lib/kernel/utils.ex index 8af029279e..2cdcd60130 100644 --- a/lib/elixir/lib/kernel/utils.ex +++ b/lib/elixir/lib/kernel/utils.ex @@ -36,14 +36,14 @@ defmodule Kernel.Utils do if is_list(funs) do IO.warn( "passing a list to Kernel.defdelegate/2 is deprecated, please define each delegate separately", - Macro.Env.stacktrace(env) + env ) end if Keyword.has_key?(opts, :append_first) do IO.warn( "Kernel.defdelegate/2 :append_first option is deprecated", - Macro.Env.stacktrace(env) + env ) end From 01fd433990683f5ccf4cbb176b17378150948b9a Mon Sep 17 00:00:00 2001 From: Andrea Leopardi Date: Tue, 12 Dec 2023 11:14:09 +0100 Subject: [PATCH 0235/1886] Improve docs for Application.put_all_env/2 --- lib/elixir/lib/application.ex | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/lib/elixir/lib/application.ex b/lib/elixir/lib/application.ex index 5aacb9c19b..1ba3e348bd 100644 --- a/lib/elixir/lib/application.ex +++ b/lib/elixir/lib/application.ex @@ -817,16 +817,29 @@ defmodule Application do end @doc """ - Puts the environment for multiple apps at the same time. + Puts the environment for multiple applications at the same time. The given config should not: * have the same application listed more than once * have the same key inside the same application listed more than once - If those conditions are not met, it will raise. + If those conditions are not met, this function will raise. + + This function receives the same options as `put_env/4`. Returns `:ok`. + + ## Examples + + Application.put_all_env( + my_app: [ + key: :value, + another_key: :another_value + ], + another_app: [ + key: :value + ] + ) - It receives the same options as `put_env/4`. Returns `:ok`. """ @doc since: "1.9.0" @spec put_all_env([{app, [{key, value}]}], timeout: timeout, persistent: boolean) :: :ok From 496706ee7baa856dbdf690dbd5d60a2fddfd6ed5 Mon Sep 17 00:00:00 2001 From: Wojtek Mach Date: Tue, 12 Dec 2023 14:31:12 +0100 Subject: [PATCH 0236/1886] elixir.bat: Quote file paths (#13172) --- bin/elixir.bat | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/bin/elixir.bat b/bin/elixir.bat index fddbeb4135..7974d006cc 100644 --- a/bin/elixir.bat +++ b/bin/elixir.bat @@ -140,16 +140,16 @@ if ""==!par:--remsh=! (set "parsElixir=!parsElixir! --remsh %~1" && shift && if ""==!par:--dot-iex=! (set "parsElixir=!parsElixir! --dot-iex %~1" && shift && goto startloop) if ""==!par:--dbg=! (set "parsElixir=!parsElixir! --dbg %~1" && shift && goto startloop) rem ******* ERLANG PARAMETERS ********************** -if ""==!par:--boot=! (set "parsErlang=!parsErlang! -boot %~1" && shift && goto startloop) -if ""==!par:--boot-var=! (set "parsErlang=!parsErlang! -boot_var %~1 %~2" && shift && shift && goto startloop) -if ""==!par:--cookie=! (set "parsErlang=!parsErlang! -setcookie %~1" && shift && goto startloop) +if ""==!par:--boot=! (set "parsErlang=!parsErlang! -boot "%~1"" && shift && goto startloop) +if ""==!par:--boot-var=! (set "parsErlang=!parsErlang! -boot_var "%~1" "%~2"" && shift && shift && goto startloop) +if ""==!par:--cookie=! (set "parsErlang=!parsErlang! -setcookie "%~1"" && shift && goto startloop) if ""==!par:--hidden=! (set "parsErlang=!parsErlang! -hidden" && goto startloop) -if ""==!par:--erl-config=! (set "parsErlang=!parsErlang! -config %~1" && shift && goto startloop) +if ""==!par:--erl-config=! (set "parsErlang=!parsErlang! -config "%~1"" && shift && goto startloop) if ""==!par:--logger-otp-reports=! (set "parsErlang=!parsErlang! -logger handle_otp_reports %1" && shift && goto startloop) if ""==!par:--logger-sasl-reports=! (set "parsErlang=!parsErlang! -logger handle_sasl_reports %1" && shift && goto startloop) -if ""==!par:--name=! (set "parsErlang=!parsErlang! -name %~1" && shift && goto startloop) -if ""==!par:--sname=! (set "parsErlang=!parsErlang! -sname %~1" && shift && goto startloop) -if ""==!par:--vm-args=! (set "parsErlang=!parsErlang! -args_file %~1" && shift && goto startloop) +if ""==!par:--name=! (set "parsErlang=!parsErlang! -name "%~1"" && shift && goto startloop) +if ""==!par:--sname=! (set "parsErlang=!parsErlang! -sname "%~1"" && shift && goto startloop) +if ""==!par:--vm-args=! (set "parsErlang=!parsErlang! -args_file "%~1"" && shift && goto startloop) if ""==!par:--erl=! (set "beforeExtra=!beforeExtra! %~1" && shift && goto startloop) if ""==!par:--pipe-to=! (echo --pipe-to : Option is not supported on Windows && goto end) set endLoop=1 From 0fdb0f8a62821e99e3a657ed66572fc83157da9e Mon Sep 17 00:00:00 2001 From: Tobias Pfeiffer Date: Tue, 12 Dec 2023 16:43:38 +0100 Subject: [PATCH 0237/1886] Mention dangers around `Task` and sending a lot of data along (#13173) The `Task` module is one of the coolest modules in elixir and is probably the first contact and experience of a lot of beginners with parallelism in elixir. I hence find it worthwhile to warn about the memory copying and its impacts here as it might easily lead to unwelcome results, so it's worth pointing out. --- lib/elixir/lib/task.ex | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/lib/elixir/lib/task.ex b/lib/elixir/lib/task.ex index 2dd53da2a6..1b221050fa 100644 --- a/lib/elixir/lib/task.ex +++ b/lib/elixir/lib/task.ex @@ -44,10 +44,38 @@ defmodule Task do means that, if the caller crashes, the task will crash too and vice-versa. This is on purpose: if the process meant to receive the result no longer exists, there is - no purpose in completing the computation. + no purpose in completing the computation. If this is not + desired, you will want to use supervised tasks, described + in a subsequent section. - If this is not desired, you will want to use supervised - tasks, described next. + ## Tasks are processes + + Tasks are processes and so data will need to be completely copied + to them. Take the following code as an example: + + large_data = fetch_large_data() + task = Task.async(fn -> do_some_work(large_data) end) + res = do_some_other_work() + res + Task.await(task) + + The code above copies over all of `large_data`, which can be + resource intensive depending on the size of the data. + There are two ways to address this. + + First, if you need to access only part of `large_data`, + consider extracting it before the task: + + large_data = fetch_large_data() + subset_data = large_data.some_field + task = Task.async(fn -> do_some_work(subset_data) end) + + Alternatively, if you can move the data loading altogether + to the task, it may be even better: + + task = Task.async(fn -> + large_data = fetch_large_data() + do_some_work(large_data) + end) ## Dynamically supervised tasks From 9e6974e93c79c00e0efd40fa9ab93faed33ee29a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 12 Dec 2023 16:47:47 +0100 Subject: [PATCH 0238/1886] Use strict option parsing on deps.clean, closes #13175 --- lib/mix/lib/mix/tasks/deps.clean.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mix/lib/mix/tasks/deps.clean.ex b/lib/mix/lib/mix/tasks/deps.clean.ex index c9edfa7d41..ef40731d33 100644 --- a/lib/mix/lib/mix/tasks/deps.clean.ex +++ b/lib/mix/lib/mix/tasks/deps.clean.ex @@ -27,7 +27,7 @@ defmodule Mix.Tasks.Deps.Clean do @impl true def run(args) do Mix.Project.get!() - {opts, apps, _} = OptionParser.parse(args, switches: @switches) + {opts, apps} = OptionParser.parse!(args, strict: @switches) build_path = Mix.Project.build_path() From ef2ccfc7db6d0378a0a8f89a252bdaca502b2dfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 13 Dec 2023 04:19:05 +0100 Subject: [PATCH 0239/1886] Update anonymous-functions.md --- lib/elixir/pages/getting-started/anonymous-functions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/pages/getting-started/anonymous-functions.md b/lib/elixir/pages/getting-started/anonymous-functions.md index a584b3d63a..14e2c61db8 100644 --- a/lib/elixir/pages/getting-started/anonymous-functions.md +++ b/lib/elixir/pages/getting-started/anonymous-functions.md @@ -17,7 +17,7 @@ true In the example above, we defined an anonymous function that receives two arguments, `a` and `b`, and returns the result of `a + b`. The arguments are always on the left-hand side of `->` and the code to be executed on the right-hand side. The anonymous function is stored in the variable `add`. -We can invoke anonymous functions by passing arguments to it. Note that a dot (`.`) between the variable and parentheses is required to invoke an anonymous function. The dot ensures there is no ambiguity between calling the anonymous function matched to a variable `add` and a named function `add/2`. We will write our own named functions when dealing with [Modules and Functions](modules-and-functions.md). For now, just remember that Elixir makes a clear distinction between anonymous functions and named functions. +We can invoke anonymous functions by passing arguments to it. Note that a dot (`.`) between the variable and parentheses is required to invoke an anonymous function. The dot makes it clear when you are calling an anonymous function, stored in the variable `add`, opposed to a function named `add/2`. For example, if you have an anonymous function stored in the variable `is_atom`, there is no ambiguity between `is_atom.(:foo)` and `is_atom(:foo)`. If both used the same `is_atom(:foo)` syntax, the only way to know the actual behaviour of `is_atom(:foo)` would be by scanning all code searching if an `is_atom` variable was defined thus far. This scanning hurts maintainability as it requires developers to track additional context in their head when reading and writing code. Anonymous functions in Elixir are also identified by the number of arguments they receive. We can check if a function is of any given arity by using `is_function/2`: From aa65d0625c3dcda76dd8b564ee93316c7a45261a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 13 Dec 2023 04:20:45 +0100 Subject: [PATCH 0240/1886] Update anonymous-functions.md --- lib/elixir/pages/getting-started/anonymous-functions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/pages/getting-started/anonymous-functions.md b/lib/elixir/pages/getting-started/anonymous-functions.md index 14e2c61db8..8c12cf242a 100644 --- a/lib/elixir/pages/getting-started/anonymous-functions.md +++ b/lib/elixir/pages/getting-started/anonymous-functions.md @@ -17,7 +17,7 @@ true In the example above, we defined an anonymous function that receives two arguments, `a` and `b`, and returns the result of `a + b`. The arguments are always on the left-hand side of `->` and the code to be executed on the right-hand side. The anonymous function is stored in the variable `add`. -We can invoke anonymous functions by passing arguments to it. Note that a dot (`.`) between the variable and parentheses is required to invoke an anonymous function. The dot makes it clear when you are calling an anonymous function, stored in the variable `add`, opposed to a function named `add/2`. For example, if you have an anonymous function stored in the variable `is_atom`, there is no ambiguity between `is_atom.(:foo)` and `is_atom(:foo)`. If both used the same `is_atom(:foo)` syntax, the only way to know the actual behaviour of `is_atom(:foo)` would be by scanning all code searching if an `is_atom` variable was defined thus far. This scanning hurts maintainability as it requires developers to track additional context in their head when reading and writing code. +We can invoke anonymous functions by passing arguments to it. Note that a dot (`.`) between the variable and parentheses is required to invoke an anonymous function. The dot makes it clear when you are calling an anonymous function, stored in the variable `add`, opposed to a function named `add/2`. For example, if you have an anonymous function stored in the variable `is_atom`, there is no ambiguity between `is_atom.(:foo)` and `is_atom(:foo)`. If both used the same `is_atom(:foo)` syntax, the only way to know the actual behaviour of `is_atom(:foo)` would be by scanning all code thus far for a possible definition of the `is_atom` variable. This scanning hurts maintainability as it requires developers to track additional context in their head when reading and writing code. Anonymous functions in Elixir are also identified by the number of arguments they receive. We can check if a function is of any given arity by using `is_function/2`: From f79e37daceb6e3b2357b37c76ccfd8bab72619ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 13 Dec 2023 11:56:41 +0100 Subject: [PATCH 0241/1886] Fix indentation in mix release --- lib/mix/lib/mix/tasks/release.ex | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/mix/lib/mix/tasks/release.ex b/lib/mix/lib/mix/tasks/release.ex index 1c08674824..e9d13d58f0 100644 --- a/lib/mix/lib/mix/tasks/release.ex +++ b/lib/mix/lib/mix/tasks/release.ex @@ -389,10 +389,10 @@ defmodule Mix.Tasks.Release do * `:strip_beams` - controls if BEAM files should have their debug information, documentation chunks, and other non-essential metadata removed. Defaults to `true`. May be set to `false` to disable stripping. Also accepts - `[keep: ["Docs", "Dbgi"]]` to keep certain chunks that are usually stripped. - You can also set the `:compress` option to true to enable individual - compression of BEAM files, although it is typically preferred to compress - the whole release instead. + `[keep: ["Docs", "Dbgi"]]` to keep certain chunks that are usually stripped. + You can also set the `:compress` option to true to enable individual + compression of BEAM files, although it is typically preferred to compress + the whole release instead. * `:cookie` - a string representing the Erlang Distribution cookie. If this option is not set, a random cookie is written to the `releases/COOKIE` file From 2c1ca00d6b0d91b762af25b3391d3046fd104eb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 13 Dec 2023 23:30:54 +0100 Subject: [PATCH 0242/1886] Improve docs and support column in IO.warn, closes #13179 --- lib/elixir/lib/io.ex | 34 ++++++++++++------- .../test/elixir/kernel/diagnostics_test.exs | 26 +++++++++++++- 2 files changed, 47 insertions(+), 13 deletions(-) diff --git a/lib/elixir/lib/io.ex b/lib/elixir/lib/io.ex index 41f77cfdbc..f0b26549a1 100644 --- a/lib/elixir/lib/io.ex +++ b/lib/elixir/lib/io.ex @@ -316,17 +316,20 @@ defmodule IO do entry from the compilation environment will be used * a keyword list with at least the `:file` option representing - a single stacktrace entry (since v1.14.0). The `:line`, `:module`, - `:function` options are also supported + a single stacktrace entry (since v1.14.0). The `:line`, `:column`, + `:module`, and `:function` options are also supported - This function also notifies the compiler a warning was printed - (in case --warnings-as-errors was enabled). It returns `:ok` - if it succeeds. + This function notifies the compiler a warning was printed + and emits a compiler diagnostic (`t:Code.diagnostic/1`). + The diagnostic will include precise file and location information + if a `Macro.Env` is given or those values have been passed as + keyword list, but not for stacktraces, as they are often imprecise. + + It returns `:ok` if it succeeds. ## Examples - stacktrace = [{MyApp, :main, 1, [file: 'my_app.ex', line: 4]}] - IO.warn("variable bar is unused", stacktrace) + IO.warn("variable bar is unused", module: MyApp, function: {:main, 1}, line: 4, file: "my_app.ex") #=> warning: variable bar is unused #=> my_app.ex:4: MyApp.main/1 @@ -345,15 +348,22 @@ defmodule IO do def warn(message, [{_, _} | _] = keyword) do if file = keyword[:file] do - warn( - message, - %{ + line = keyword[:line] + column = keyword[:column] + position = if line && column, do: {line, column}, else: line + message = to_chardata(message) + + stacktrace = + Macro.Env.stacktrace(%{ __ENV__ | module: keyword[:module], function: keyword[:function], - line: keyword[:line], + line: line, file: file - } + }) + + :elixir_errors.emit_diagnostic(:warning, position, file, message, stacktrace, + read_snippet: true ) else warn(message, []) diff --git a/lib/elixir/test/elixir/kernel/diagnostics_test.exs b/lib/elixir/test/elixir/kernel/diagnostics_test.exs index 184bb46a49..be5f0e042e 100644 --- a/lib/elixir/test/elixir/kernel/diagnostics_test.exs +++ b/lib/elixir/test/elixir/kernel/diagnostics_test.exs @@ -769,6 +769,30 @@ defmodule Kernel.DiagnosticsTest do purge(Sample) end + @tag :tmp_dir + test "IO.warn file+line", %{tmp_dir: tmp_dir} do + path = make_relative_tmp(tmp_dir, "io-warn-file-line.ex") + + source = """ + IO.warn("oops\\nmulti\\nline", file: __ENV__.file, line: __ENV__.line) + """ + + File.write!(path, source) + + expected = """ + warning: oops + multi + line + │ + 1 │ IO.warn("oops\\nmulti\\nline", file: __ENV__.file, line: __ENV__.line) + │ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + │ + └─ tmp\ + """ + + assert capture_io(:stderr, fn -> Code.eval_file(path) end) =~ expected + end + @tag :tmp_dir test "IO.warn file+line+column", %{tmp_dir: tmp_dir} do path = make_relative_tmp(tmp_dir, "io-warn-file-line-column.ex") @@ -785,7 +809,7 @@ defmodule Kernel.DiagnosticsTest do line │ 1 │ IO.warn("oops\\nmulti\\nline", file: __ENV__.file, line: __ENV__.line, column: 4) - │ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + │ ~ │ └─ tmp\ """ From 02b087615402fd0d899fbd8320512aa0f1b46a3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vin=C3=ADcius=20M=C3=BCller?= Date: Thu, 14 Dec 2023 05:24:43 -0300 Subject: [PATCH 0243/1886] Improve diagnostics for unclosed heredocs (#13182) --- lib/elixir/lib/exception.ex | 17 +++++++++--- lib/elixir/src/elixir_tokenizer.erl | 12 ++++++++- .../test/elixir/kernel/diagnostics_test.exs | 27 +++++++++++++++++++ lib/elixir/test/elixir/kernel/parser_test.exs | 2 +- 4 files changed, 52 insertions(+), 6 deletions(-) diff --git a/lib/elixir/lib/exception.ex b/lib/elixir/lib/exception.ex index 3d91d1aed2..699c3886cf 100644 --- a/lib/elixir/lib/exception.ex +++ b/lib/elixir/lib/exception.ex @@ -794,6 +794,15 @@ defmodule Exception do end end + @doc false + def format_expected_delimiter(opening_delimiter) do + terminator = :elixir_tokenizer.terminator(opening_delimiter) + + if terminator |> Atom.to_string() |> String.contains?(["\"", "'"]), + do: terminator, + else: ~s("#{terminator}") + end + @doc false def format_snippet( {start_line, _start_column} = start_pos, @@ -1145,10 +1154,10 @@ defmodule MismatchedDelimiterError do start_pos = {start_line, start_column} end_pos = {end_line, end_column} lines = String.split(snippet, "\n") - expected_delimiter = :elixir_tokenizer.terminator(opening_delimiter) + expected_delimiter = Exception.format_expected_delimiter(opening_delimiter) start_message = "└ unclosed delimiter" - end_message = ~s/└ mismatched closing delimiter (expected "#{expected_delimiter}")/ + end_message = ~s/└ mismatched closing delimiter (expected #{expected_delimiter})/ snippet = Exception.format_snippet( @@ -1268,10 +1277,10 @@ defmodule TokenMissingError do start_pos = {line, column} end_pos = {end_line, end_column} - expected_delimiter = :elixir_tokenizer.terminator(opening_delimiter) + expected_delimiter = Exception.format_expected_delimiter(opening_delimiter) start_message = ~s/└ unclosed delimiter/ - end_message = ~s/└ missing closing delimiter (expected "#{expected_delimiter}")/ + end_message = ~s/└ missing closing delimiter (expected #{expected_delimiter})/ snippet = Exception.format_snippet( diff --git a/lib/elixir/src/elixir_tokenizer.erl b/lib/elixir/src/elixir_tokenizer.erl index b63e09d27e..f4cf79f397 100644 --- a/lib/elixir/src/elixir_tokenizer.erl +++ b/lib/elixir/src/elixir_tokenizer.erl @@ -1032,7 +1032,16 @@ extract_heredoc_with_interpolation(Line, Column, Scope, Interpol, T, H) -> {ok, NewLine, NewColumn, tokens_to_binary(Parts2), Rest, NewScope}; {error, Reason} -> - {error, interpolation_format(Reason, " (for heredoc starting at line ~B)", [Line])} + {Position, Message, List} = interpolation_format(Reason, " (for heredoc starting at line ~B)", [Line]), + {line, EndLine} = lists:keyfind(line, 1, Position), + Meta = [ + {error_type, unclosed_delimiter}, + {opening_delimiter, '"""'}, + {line, Line}, + {column, Column}, + {end_line, EndLine} + ], + {error, {Meta, Message, List}} end; error -> @@ -1483,6 +1492,7 @@ terminator('do') -> 'end'; terminator('(') -> ')'; terminator('[') -> ']'; terminator('{') -> '}'; +terminator('"""') -> '"""'; terminator('<<') -> '>>'. %% Keywords checking diff --git a/lib/elixir/test/elixir/kernel/diagnostics_test.exs b/lib/elixir/test/elixir/kernel/diagnostics_test.exs index be5f0e042e..bf177ebfc9 100644 --- a/lib/elixir/test/elixir/kernel/diagnostics_test.exs +++ b/lib/elixir/test/elixir/kernel/diagnostics_test.exs @@ -361,6 +361,33 @@ defmodule Kernel.DiagnosticsTest do """ end + test "missing heredoc terminator" do + output = + capture_raise( + """ + a = \""" + test string + + IO.inspect(10 + 20) + """, + TokenMissingError + ) + + assert output == """ + ** (TokenMissingError) token missing on nofile:4:20: + error: missing terminator: \""" (for heredoc starting at line 1) + │ + 1 │ a = \""" + │ └ unclosed delimiter + 2 │ test string + 3 │ + 4 │ IO.inspect(10 + 20) + │ └ missing closing delimiter (expected \""") + │ + └─ nofile:4:20\ + """ + end + test "shows in between lines if EOL is not far below" do output = capture_raise( diff --git a/lib/elixir/test/elixir/kernel/parser_test.exs b/lib/elixir/test/elixir/kernel/parser_test.exs index e4ad788eae..4e30410b49 100644 --- a/lib/elixir/test/elixir/kernel/parser_test.exs +++ b/lib/elixir/test/elixir/kernel/parser_test.exs @@ -528,7 +528,7 @@ defmodule Kernel.ParserTest do test "heredoc with incomplete interpolation" do assert_token_missing( [ - "nofile:2:1:", + "nofile:1:4:", ~s/missing interpolation terminator: "}" (for heredoc starting at line 1)/ ], ~c"\"\"\"\n\#{\n" From 2d3429c49cfa483ec8f50d5cfe2c79e3bf1472c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 14 Dec 2023 11:14:40 +0100 Subject: [PATCH 0244/1886] Unify position handling and improve docs See #13179. See #13184. --- lib/elixir/lib/code.ex | 23 +++++++++++++++++++---- lib/mix/lib/mix/task.compiler.ex | 26 +++++++++----------------- 2 files changed, 28 insertions(+), 21 deletions(-) diff --git a/lib/elixir/lib/code.ex b/lib/elixir/lib/code.ex index 9ab9c8cf59..551f2805f3 100644 --- a/lib/elixir/lib/code.ex +++ b/lib/elixir/lib/code.ex @@ -196,21 +196,36 @@ defmodule Code do @typedoc """ Diagnostics returned by the compiler and code evaluation. + + If there is a file and position, then the diagnostic is precise + and you can use the given file and position for generating snippets, + IDEs annotations, and so on. + + Otherwise, a stacktrace may be given, which you can place your own + heuristics to provide better reporting. """ @type diagnostic(severity) :: %{ - required(:file) => Path.t(), + required(:file) => Path.t() | nil, required(:severity) => severity, required(:message) => String.t(), - required(:position) => position, + required(:position) => position(), required(:stacktrace) => Exception.stacktrace(), - required(:span) => {non_neg_integer, non_neg_integer} | nil, + required(:span) => {non_neg_integer(), non_neg_integer()} | nil, optional(:exception) => Exception.t() | nil, optional(any()) => any() } @typedoc "The line. 0 indicates no line." @type line() :: non_neg_integer() - @type position() :: line() | {pos_integer(), column :: non_neg_integer} + + @typedoc """ + The position of the diagnostic. + + Can be either a line number or a `{line, column}`. + Line and columns numbers are one-based. + A position of `0` represents unknown. + """ + @type position() :: line() | {line :: pos_integer(), column :: pos_integer()} @boolean_compiler_options [ :docs, diff --git a/lib/mix/lib/mix/task.compiler.ex b/lib/mix/lib/mix/task.compiler.ex index 9c80400f81..619eb2eb54 100644 --- a/lib/mix/lib/mix/task.compiler.ex +++ b/lib/mix/lib/mix/task.compiler.ex @@ -31,13 +31,20 @@ defmodule Mix.Task.Compiler do defmodule Diagnostic do @moduledoc """ Diagnostic information such as a warning or compilation error. + + If there is a file and position, then the diagnostic is precise + and you can use the given file and position for generating snippets, + IDEs annotations, and so on. + + Otherwise, a stacktrace may be given, which you can place your own + heuristics to provide better reporting. """ @type t :: %__MODULE__{ - file: Path.t(), + file: Path.t() | nil, severity: severity, message: IO.chardata(), - position: position, + position: Code.position(), compiler_name: String.t(), details: Exception.t() | any, stacktrace: Exception.stacktrace(), @@ -61,21 +68,6 @@ defmodule Mix.Task.Compiler do """ @type severity :: :error | :warning | :information | :hint - @typedoc """ - Where in a file the diagnostic applies. Can be either a line number, - a `{line, column}` tuple, a range specified as `{start_line, start_col, - end_line, end_col}`. `0` line represents unknown. - - Line numbers are one-based, and column numbers in a range are zero-based and refer - to the cursor position at the start of the character at that index. For example, - to indicate that a diagnostic applies to the first `n` characters of the - first line, the range would be `{1, 0, 1, n}`. - """ - @type position :: - non_neg_integer - | {pos_integer, non_neg_integer} - | {pos_integer, non_neg_integer, pos_integer, non_neg_integer} - @enforce_keys [:file, :severity, :message, :position, :compiler_name] defstruct [ :file, From a6bed6911d8b9306c7598e50e972e754361e7a55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 14 Dec 2023 12:07:20 +0100 Subject: [PATCH 0245/1886] Document diagnostic span --- lib/elixir/lib/code.ex | 5 +++-- lib/mix/lib/mix/task.compiler.ex | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/elixir/lib/code.ex b/lib/elixir/lib/code.ex index 551f2805f3..803d158f02 100644 --- a/lib/elixir/lib/code.ex +++ b/lib/elixir/lib/code.ex @@ -199,7 +199,8 @@ defmodule Code do If there is a file and position, then the diagnostic is precise and you can use the given file and position for generating snippets, - IDEs annotations, and so on. + IDEs annotations, and so on. An optional span is available with + the line and column the diagnostic ends. Otherwise, a stacktrace may be given, which you can place your own heuristics to provide better reporting. @@ -210,7 +211,7 @@ defmodule Code do required(:message) => String.t(), required(:position) => position(), required(:stacktrace) => Exception.stacktrace(), - required(:span) => {non_neg_integer(), non_neg_integer()} | nil, + required(:span) => {line :: pos_integer(), column :: pos_integer()} | nil, optional(:exception) => Exception.t() | nil, optional(any()) => any() } diff --git a/lib/mix/lib/mix/task.compiler.ex b/lib/mix/lib/mix/task.compiler.ex index 619eb2eb54..44055a710f 100644 --- a/lib/mix/lib/mix/task.compiler.ex +++ b/lib/mix/lib/mix/task.compiler.ex @@ -34,7 +34,8 @@ defmodule Mix.Task.Compiler do If there is a file and position, then the diagnostic is precise and you can use the given file and position for generating snippets, - IDEs annotations, and so on. + IDEs annotations, and so on. An optional span is available with + the line and column the diagnostic ends. Otherwise, a stacktrace may be given, which you can place your own heuristics to provide better reporting. @@ -48,7 +49,7 @@ defmodule Mix.Task.Compiler do compiler_name: String.t(), details: Exception.t() | any, stacktrace: Exception.stacktrace(), - span: {non_neg_integer, non_neg_integer} | nil + span: {line :: pos_integer(), column :: pos_integer()} | nil } @typedoc """ From 4d712b25eaa0e1dbda48ee6a3d6c17db01f37197 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 14 Dec 2023 16:58:55 +0100 Subject: [PATCH 0246/1886] Normalize token missing and mismatched delimiter exceptions Closes #13183. Closes #13185. Closes #13186. Closes #13187. --- lib/elixir/lib/exception.ex | 56 ++++++++----- lib/elixir/lib/kernel/parallel_compiler.ex | 52 +++++++----- lib/elixir/src/elixir_tokenizer.erl | 60 +++++++------- lib/elixir/test/elixir/code_test.exs | 10 ++- .../test/elixir/kernel/diagnostics_test.exs | 80 ++++++++++++++++++- lib/elixir/test/elixir/kernel/parser_test.exs | 77 ------------------ 6 files changed, 187 insertions(+), 148 deletions(-) diff --git a/lib/elixir/lib/exception.ex b/lib/elixir/lib/exception.ex index 699c3886cf..1341ce432f 100644 --- a/lib/elixir/lib/exception.ex +++ b/lib/elixir/lib/exception.ex @@ -795,12 +795,10 @@ defmodule Exception do end @doc false - def format_expected_delimiter(opening_delimiter) do - terminator = :elixir_tokenizer.terminator(opening_delimiter) - - if terminator |> Atom.to_string() |> String.contains?(["\"", "'"]), - do: terminator, - else: ~s("#{terminator}") + def format_delimiter(delimiter) do + if delimiter |> Atom.to_string() |> String.contains?(["\"", "'"]), + do: delimiter, + else: ~s("#{delimiter}") end @doc false @@ -1121,8 +1119,23 @@ defmodule MismatchedDelimiterError do An exception raised when a mismatched delimiter is found when parsing code. For example: - - `[1, 2, 3}` - - `fn a -> )` + + * `[1, 2, 3}` + * `fn a -> )` + + The following fields of this exceptions are public and can be accessed freely: + + * `:file` (`t:Path.t/0` or `nil`) - the file where the error occurred, or `nil` if + the error occurred in code that did not come from a file + * `:line` - the line for the opening delimiter + * `:column` - the column for the opening delimiter + * `:end_line` - the line for the mismatched closing delimiter + * `:end_column` - the column for the mismatched closing delimiter + * `:opening_delimiter` - an atom representing the opening delimiter + * `:closing_delimiter` - an atom representing the mismatched closing delimiter + * `:expected_delimiter` - an atom representing the closing delimiter + * `:description` - a description of the mismatched delimiter error + """ defexception [ @@ -1134,6 +1147,7 @@ defmodule MismatchedDelimiterError do :end_column, :opening_delimiter, :closing_delimiter, + :expected_delimiter, :snippet, description: "mismatched delimiter error" ] @@ -1146,15 +1160,14 @@ defmodule MismatchedDelimiterError do end_column: end_column, line_offset: line_offset, description: description, - opening_delimiter: opening_delimiter, - closing_delimiter: _closing_delimiter, + expected_delimiter: expected_delimiter, file: file, snippet: snippet }) do start_pos = {start_line, start_column} end_pos = {end_line, end_column} lines = String.split(snippet, "\n") - expected_delimiter = Exception.format_expected_delimiter(opening_delimiter) + expected_delimiter = Exception.format_delimiter(expected_delimiter) start_message = "└ unclosed delimiter" end_message = ~s/└ mismatched closing delimiter (expected #{expected_delimiter})/ @@ -1188,8 +1201,9 @@ defmodule SyntaxError do * `:file` (`t:Path.t/0` or `nil`) - the file where the error occurred, or `nil` if the error occurred in code that did not come from a file - * `:line` (`t:non_neg_integer/0`) - the line where the error occurred - * `:column` (`t:non_neg_integer/0`) - the column where the error occurred + * `:line` - the line where the error occurred + * `:column` - the column where the error occurred + * `:description` - a description of the syntax error """ @@ -1238,9 +1252,13 @@ defmodule TokenMissingError do * `:file` (`t:Path.t/0` or `nil`) - the file where the error occurred, or `nil` if the error occurred in code that did not come from a file - * `:line` (`t:non_neg_integer/0`) - the line where the error occurred - * `:column` (`t:non_neg_integer/0`) - the column where the error occurred - + * `:line` - the line for the opening delimiter + * `:column` - the column for the opening delimiter + * `:end_line` - the line for the end of the string + * `:end_column` - the column for the end of the string + * `:opening_delimiter` - an atom representing the opening delimiter + * `:expected_delimiter` - an atom representing the expected delimiter + * `:description` - a description of the missing token error """ defexception [ @@ -1248,9 +1266,11 @@ defmodule TokenMissingError do :line, :column, :end_line, + :end_column, :line_offset, :snippet, :opening_delimiter, + :expected_delimiter, description: "expression is incomplete" ] @@ -1262,7 +1282,7 @@ defmodule TokenMissingError do end_line: end_line, line_offset: line_offset, description: description, - opening_delimiter: opening_delimiter, + expected_delimiter: expected_delimiter, snippet: snippet }) when not is_nil(snippet) and not is_nil(column) and not is_nil(end_line) do @@ -1277,7 +1297,7 @@ defmodule TokenMissingError do start_pos = {line, column} end_pos = {end_line, end_column} - expected_delimiter = Exception.format_expected_delimiter(opening_delimiter) + expected_delimiter = Exception.format_delimiter(expected_delimiter) start_message = ~s/└ unclosed delimiter/ end_message = ~s/└ missing closing delimiter (expected #{expected_delimiter})/ diff --git a/lib/elixir/lib/kernel/parallel_compiler.ex b/lib/elixir/lib/kernel/parallel_compiler.ex index ee5e3c7d3e..580e60d348 100644 --- a/lib/elixir/lib/kernel/parallel_compiler.ex +++ b/lib/elixir/lib/kernel/parallel_compiler.ex @@ -890,7 +890,7 @@ defmodule Kernel.ParallelCompiler do end defp to_error(file, kind, reason, stack) do - line = get_line(file, reason, stack) + {line, span} = get_line_span(file, reason, stack) file = Path.absname(file) message = :unicode.characters_to_binary(Kernel.CLI.format_error(kind, reason, stack)) @@ -900,7 +900,7 @@ defmodule Kernel.ParallelCompiler do message: message, severity: :error, stacktrace: stack, - span: nil, + span: span, exception: get_exception(reason) } end @@ -908,35 +908,47 @@ defmodule Kernel.ParallelCompiler do defp get_exception(exception) when is_exception(exception), do: exception defp get_exception(_reason), do: nil - defp get_line(_file, %{line: line, column: column}, _stack) + defp get_line_span( + _file, + %{line: line, column: column, end_line: end_line, end_column: end_column}, + _stack + ) + when is_integer(line) and line > 0 and is_integer(column) and column >= 0 and + is_integer(end_line) and end_line > 0 and is_integer(end_column) and end_column >= 0 do + {{line, column}, {end_line, end_column}} + end + + defp get_line_span(_file, %{line: line, column: column}, _stack) when is_integer(line) and line > 0 and is_integer(column) and column >= 0 do - {line, column} + {{line, column}, nil} end - defp get_line(_file, %{line: line}, _stack) when is_integer(line) and line > 0 do - line + defp get_line_span(_file, %{line: line}, _stack) when is_integer(line) and line > 0 do + {line, nil} end - defp get_line(file, :undef, [{_, _, _, []}, {_, _, _, info} | _]) do - if Keyword.get(info, :file) == to_charlist(Path.relative_to_cwd(file)) do - Keyword.get(info, :line) - end + defp get_line_span(file, :undef, [{_, _, _, []}, {_, _, _, info} | _]) do + get_line_span_from_stacktrace_info(info, file) end - defp get_line(file, _reason, [{_, _, _, [file: expanding]}, {_, _, _, info} | _]) + defp get_line_span(file, _reason, [{_, _, _, [file: expanding]}, {_, _, _, info} | _]) when expanding in [~c"expanding macro", ~c"expanding struct"] do - if Keyword.get(info, :file) == to_charlist(Path.relative_to_cwd(file)) do - Keyword.get(info, :line) - end + get_line_span_from_stacktrace_info(info, file) end - defp get_line(file, _reason, [{_, _, _, info} | _]) do - if Keyword.get(info, :file) == to_charlist(Path.relative_to_cwd(file)) do - Keyword.get(info, :line) - end + defp get_line_span(file, _reason, [{_, _, _, info} | _]) do + get_line_span_from_stacktrace_info(info, file) + end + + defp get_line_span(_, _, _) do + {nil, nil} end - defp get_line(_, _, _) do - nil + defp get_line_span_from_stacktrace_info(info, file) do + if Keyword.get(info, :file) == to_charlist(Path.relative_to_cwd(file)) do + {Keyword.get(info, :line), nil} + else + {nil, nil} + end end end diff --git a/lib/elixir/src/elixir_tokenizer.erl b/lib/elixir/src/elixir_tokenizer.erl index f4cf79f397..0caacfd515 100644 --- a/lib/elixir/src/elixir_tokenizer.erl +++ b/lib/elixir/src/elixir_tokenizer.erl @@ -142,18 +142,20 @@ tokenize([], Line, Column, #elixir_tokenizer{cursor_completion=Cursor} = Scope, AccTokens = cursor_complete(Line, CursorColumn, CursorTerminators, CursorTokens), {ok, Line, Column, AllWarnings, AccTokens}; -tokenize([], EndLine, _, #elixir_tokenizer{terminators=[{Start, {StartLine, StartColumn, _}, _} | _]} = Scope, Tokens) -> +tokenize([], EndLine, EndColumn, #elixir_tokenizer{terminators=[{Start, {StartLine, StartColumn, _}, _} | _]} = Scope, Tokens) -> End = terminator(Start), Hint = missing_terminator_hint(Start, End, Scope), Message = "missing terminator: ~ts", Formatted = io_lib:format(Message, [End]), Meta = [ - {error_type, unclosed_delimiter}, - {opening_delimiter, Start}, - {line, StartLine}, - {column, StartColumn}, - {end_line, EndLine} - ], + {error_type, unclosed_delimiter}, + {opening_delimiter, Start}, + {expected_delimiter, End}, + {line, StartLine}, + {column, StartColumn}, + {end_line, EndLine}, + {end_column, EndColumn} + ], error({Meta, [Formatted, Hint], []}, [], Scope, Tokens); tokenize([], Line, Column, #elixir_tokenizer{} = Scope, Tokens) -> @@ -533,7 +535,7 @@ tokenize([$:, H | T] = Original, Line, Column, Scope, Tokens) when ?is_quote(H) {error, Reason} -> Message = " (for atom starting at line ~B)", - interpolation_error(Reason, Original, Scope, Tokens, Message, [Line]) + interpolation_error(Reason, Original, Scope, Tokens, Message, [Line], Line, Column + 1, [H], [H]) end; tokenize([$: | String] = Original, Line, Column, Scope, Tokens) -> @@ -774,7 +776,7 @@ handle_heredocs(T, Line, Column, H, Scope, Tokens) -> handle_strings(T, Line, Column, H, Scope, Tokens) -> case elixir_interpolation:extract(Line, Column, Scope, true, T, H) of {error, Reason} -> - interpolation_error(Reason, [H | T], Scope, Tokens, " (for string starting at line ~B)", [Line]); + interpolation_error(Reason, [H | T], Scope, Tokens, " (for string starting at line ~B)", [Line], Line, Column-1, [H], [H]); {NewLine, NewColumn, Parts, [$: | Rest], InterScope} when ?is_space(hd(Rest)) -> NewScope = case is_unnecessary_quote(Parts, InterScope) of @@ -927,7 +929,7 @@ handle_dot([$., H | T] = Original, Line, Column, DotInfo, Scope, Tokens) when ?i Message = "interpolation is not allowed when calling function/macro. Found interpolation in a call starting with: ", error({?LOC(Line, Column), Message, [H]}, Rest, NewScope, Tokens); {error, Reason} -> - interpolation_error(Reason, Original, Scope, Tokens, " (for function name starting at line ~B)", [Line]) + interpolation_error(Reason, Original, Scope, Tokens, " (for function name starting at line ~B)", [Line], Line, Column, [H], [H]) end; handle_dot([$. | Rest], Line, Column, DotInfo, Scope, Tokens) -> @@ -1032,16 +1034,7 @@ extract_heredoc_with_interpolation(Line, Column, Scope, Interpol, T, H) -> {ok, NewLine, NewColumn, tokens_to_binary(Parts2), Rest, NewScope}; {error, Reason} -> - {Position, Message, List} = interpolation_format(Reason, " (for heredoc starting at line ~B)", [Line]), - {line, EndLine} = lists:keyfind(line, 1, Position), - Meta = [ - {error_type, unclosed_delimiter}, - {opening_delimiter, '"""'}, - {line, Line}, - {column, Column}, - {end_line, EndLine} - ], - {error, {Meta, Message, List}} + {error, interpolation_format(Reason, " (for heredoc starting at line ~B)", [Line], Line, Column, [H, H, H], [H, H, H])} end; error -> @@ -1354,12 +1347,21 @@ previous_was_eol(_) -> nil. %% Error handling -interpolation_error(Reason, Rest, Scope, Tokens, Extension, Args) -> - error(interpolation_format(Reason, Extension, Args), Rest, Scope, Tokens). +interpolation_error(Reason, Rest, Scope, Tokens, Extension, Args, Line, Column, Opening, Closing) -> + error(interpolation_format(Reason, Extension, Args, Line, Column, Opening, Closing), Rest, Scope, Tokens). -interpolation_format({string, Line, Column, Message, Token}, Extension, Args) -> - {?LOC(Line, Column), [Message, io_lib:format(Extension, Args)], Token}; -interpolation_format({_, _, _} = Reason, _Extension, _Args) -> +interpolation_format({string, EndLine, EndColumn, Message, Token}, Extension, Args, Line, Column, Opening, Closing) -> + Meta = [ + {error_type, unclosed_delimiter}, + {opening_delimiter, list_to_atom(Opening)}, + {expected_delimiter, list_to_atom(Closing)}, + {line, Line}, + {column, Column}, + {end_line, EndLine}, + {end_column, EndColumn} + ], + {Meta, [Message, io_lib:format(Extension, Args)], Token}; +interpolation_format({_, _, _} = Reason, _Extension, _Args, _Line, _Column, _Opening, _Closing) -> Reason. %% Terminators @@ -1431,7 +1433,7 @@ check_terminator({End, {EndLine, EndColumn, _}}, [{Start, {StartLine, StartColum End -> {ok, Scope#elixir_tokenizer{terminators=Terminators}}; - _ExpectedEnd -> + ExpectedEnd -> Meta = [ {line, StartLine}, {column, StartColumn}, @@ -1439,7 +1441,8 @@ check_terminator({End, {EndLine, EndColumn, _}}, [{Start, {StartLine, StartColum {end_column, EndColumn}, {error_type, mismatched_delimiter}, {opening_delimiter, Start}, - {closing_delimiter, End} + {closing_delimiter, End}, + {expected_delimiter, ExpectedEnd} ], {error, {Meta, unexpected_token_or_reserved(End), [atom_to_list(End)]}} end; @@ -1492,7 +1495,6 @@ terminator('do') -> 'end'; terminator('(') -> ')'; terminator('[') -> ']'; terminator('{') -> '}'; -terminator('"""') -> '"""'; terminator('<<') -> '>>'. %% Keywords checking @@ -1598,7 +1600,7 @@ tokenize_sigil_contents([H | T] = Original, [S | _] = SigilName, Line, Column, S {error, Reason} -> Sigil = [$~, S, H], Message = " (for sigil ~ts starting at line ~B)", - interpolation_error(Reason, [$~] ++ SigilName ++ Original, Scope, Tokens, Message, [Sigil, Line]) + interpolation_error(Reason, [$~] ++ SigilName ++ Original, Scope, Tokens, Message, [Sigil, Line], Line, Column, [H], [sigil_terminator(H)]) end; tokenize_sigil_contents([H | _] = Original, SigilName, Line, Column, Scope, Tokens) -> diff --git a/lib/elixir/test/elixir/code_test.exs b/lib/elixir/test/elixir/code_test.exs index 8cef777253..7c370cd41e 100644 --- a/lib/elixir/test/elixir/code_test.exs +++ b/lib/elixir/test/elixir/code_test.exs @@ -494,9 +494,13 @@ defmodule CodeTest do end test "string_to_quoted returns error on incomplete escaped string" do - assert Code.string_to_quoted("\"\\") == - {:error, - {[line: 1, column: 3], "missing terminator: \" (for string starting at line 1)", ""}} + assert {:error, {meta, "missing terminator: \" (for string starting at line 1)", ""}} = + Code.string_to_quoted("\"\\") + + assert meta[:line] == 1 + assert meta[:column] == 1 + assert meta[:end_line] == 1 + assert meta[:end_column] == 3 end test "compile source" do diff --git a/lib/elixir/test/elixir/kernel/diagnostics_test.exs b/lib/elixir/test/elixir/kernel/diagnostics_test.exs index bf177ebfc9..4a057180b9 100644 --- a/lib/elixir/test/elixir/kernel/diagnostics_test.exs +++ b/lib/elixir/test/elixir/kernel/diagnostics_test.exs @@ -380,7 +380,7 @@ defmodule Kernel.DiagnosticsTest do 1 │ a = \""" │ └ unclosed delimiter 2 │ test string - 3 │ + 3 │\s 4 │ IO.inspect(10 + 20) │ └ missing closing delimiter (expected \""") │ @@ -388,6 +388,84 @@ defmodule Kernel.DiagnosticsTest do """ end + test "missing sigil terminator" do + output = + capture_raise("~s (for sigil ~s< starting at line 1) + │ + 1 │ ~s") + │ └ unclosed delimiter + │ + └─ nofile:1:10\ + """ + + output = + capture_raise("~s|foobar", TokenMissingError) + + assert output == """ + ** (TokenMissingError) token missing on nofile:1:10: + error: missing terminator: | (for sigil ~s| starting at line 1) + │ + 1 │ ~s|foobar + │ │ └ missing closing delimiter (expected "|") + │ └ unclosed delimiter + │ + └─ nofile:1:10\ + """ + end + + test "missing string terminator" do + output = + capture_raise("\"foobar", TokenMissingError) + + assert output == """ + ** (TokenMissingError) token missing on nofile:1:8: + error: missing terminator: " (for string starting at line 1) + │ + 1 │ "foobar + │ │ └ missing closing delimiter (expected ") + │ └ unclosed delimiter + │ + └─ nofile:1:8\ + """ + end + + test "missing atom terminator" do + output = + capture_raise(":\"foobar", TokenMissingError) + + assert output == """ + ** (TokenMissingError) token missing on nofile:1:9: + error: missing terminator: " (for atom starting at line 1) + │ + 1 │ :"foobar + │ │ └ missing closing delimiter (expected ") + │ └ unclosed delimiter + │ + └─ nofile:1:9\ + """ + end + + test "missing function terminator" do + output = + capture_raise("K.\"foobar", TokenMissingError) + + assert output == """ + ** (TokenMissingError) token missing on nofile:1:10: + error: missing terminator: " (for function name starting at line 1) + │ + 1 │ K."foobar + │ │ └ missing closing delimiter (expected ") + │ └ unclosed delimiter + │ + └─ nofile:1:10\ + """ + end + test "shows in between lines if EOL is not far below" do output = capture_raise( diff --git a/lib/elixir/test/elixir/kernel/parser_test.exs b/lib/elixir/test/elixir/kernel/parser_test.exs index 4e30410b49..b1803d0e51 100644 --- a/lib/elixir/test/elixir/kernel/parser_test.exs +++ b/lib/elixir/test/elixir/kernel/parser_test.exs @@ -491,83 +491,6 @@ defmodule Kernel.ParserTest do end end - describe "token missing errors" do - test "missing paren" do - assert_token_missing( - ["nofile:1:9:", "missing terminator: )"], - ~c"case 1 (" - ) - end - - test "dot terminator" do - assert_token_missing( - ["nofile:1:9:", "missing terminator: \" (for function name starting at line 1)"], - ~c"foo.\"bar" - ) - end - - test "sigil terminator" do - assert_token_missing( - ["nofile:3:1:", "missing terminator: \" (for sigil ~r\" starting at line 1)"], - ~c"~r\"foo\n\n" - ) - - assert_token_missing( - ["nofile:3:1:", "missing terminator: } (for sigil ~r{ starting at line 1)"], - ~c"~r{foo\n\n" - ) - end - - test "string terminator" do - assert_token_missing( - ["nofile:1:5:", "missing terminator: \" (for string starting at line 1)"], - ~c"\"bar" - ) - end - - test "heredoc with incomplete interpolation" do - assert_token_missing( - [ - "nofile:1:4:", - ~s/missing interpolation terminator: "}" (for heredoc starting at line 1)/ - ], - ~c"\"\"\"\n\#{\n" - ) - end - - test "heredoc terminator" do - assert_token_missing( - ["nofile:2:4:", ~s/missing terminator: """ (for heredoc starting at line 1)/], - ~c"\"\"\"\nbar" - ) - - assert_token_missing( - ["nofile:2:7:", ~s/missing terminator: """ (for heredoc starting at line 1)/], - ~c"\"\"\"\nbar\"\"\"" - ) - end - - test "missing end" do - assert_token_missing( - ["nofile:1:9:", "missing terminator: end"], - ~c"foo do 1" - ) - - assert_token_missing( - ["hint:", ~s/it looks like the "do" on line 2 does not have a matching "end"/], - ~c""" - defmodule MyApp do - def one do - # end - - def two do - end - end - """ - ) - end - end - describe "syntax errors" do test "invalid heredoc start" do assert_syntax_error( From 71039d97d3fd46a6895354c53caccda73bfdd20c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 14 Dec 2023 17:18:13 +0100 Subject: [PATCH 0247/1886] Do not warn unused imports twice, closes #13178 --- lib/elixir/src/elixir_import.erl | 36 +++++++++++--------------------- 1 file changed, 12 insertions(+), 24 deletions(-) diff --git a/lib/elixir/src/elixir_import.erl b/lib/elixir/src/elixir_import.erl index 520e8bcbc8..0f824a96a7 100644 --- a/lib/elixir/src/elixir_import.erl +++ b/lib/elixir/src/elixir_import.erl @@ -9,24 +9,26 @@ import(Meta, Ref, Opts, E) -> {Functions, Macros, Added} = case keyfind(only, Opts) of {only, functions} -> - {Added1, Funs} = import_functions(Meta, Ref, Opts, E), + {Added1, _Used1, Funs} = import_functions(Meta, Ref, Opts, E), {Funs, keydelete(Ref, ?key(E, macros)), Added1}; {only, macros} -> - {Added2, Macs} = import_macros(true, Meta, Ref, Opts, E), + {Added2, _Used2, Macs} = import_macros(true, Meta, Ref, Opts, E), {keydelete(Ref, ?key(E, functions)), Macs, Added2}; {only, sigils} -> - {Added1, Funs} = import_sigil_functions(Meta, Ref, Opts, E), - {Added2, Macs} = import_sigil_macros(Meta, Ref, Opts, E), + {Added1, _Used1, Funs} = import_sigil_functions(Meta, Ref, Opts, E), + {Added2, _Used2, Macs} = import_sigil_macros(Meta, Ref, Opts, E), {Funs, Macs, Added1 or Added2}; {only, List} when is_list(List) -> - {Added1, Funs} = import_functions(Meta, Ref, Opts, E), - {Added2, Macs} = import_macros(false, Meta, Ref, Opts, E), + {Added1, Used1, Funs} = import_functions(Meta, Ref, Opts, E), + {Added2, Used2, Macs} = import_macros(false, Meta, Ref, Opts, E), + [elixir_errors:file_warn(Meta, ?key(E, file), ?MODULE, {invalid_import, {Ref, Name, Arity}}) || + {Name, Arity} <- (List -- Used1) -- Used2], {Funs, Macs, Added1 or Added2}; {only, Other} -> elixir_errors:file_error(Meta, E, ?MODULE, {invalid_option, only, Other}); false -> - {Added1, Funs} = import_functions(Meta, Ref, Opts, E), - {Added2, Macs} = import_macros(false, Meta, Ref, Opts, E), + {Added1, _Used1, Funs} = import_functions(Meta, Ref, Opts, E), + {Added2, _Used2, Macs} = import_macros(false, Meta, Ref, Opts, E), {Funs, Macs, Added1 or Added2} end, @@ -95,9 +97,6 @@ calculate(Meta, Key, Opts, Old, File, Existing) -> _ -> elixir_errors:file_error(Meta, File, ?MODULE, only_and_except_given) end, - [elixir_errors:file_warn(Meta, File, ?MODULE, {invalid_import, {Key, Name, Arity}}) || - {Name, Arity} <- Only -- get_exports(Key)], - intersection(Only, Existing()); _ -> @@ -125,17 +124,14 @@ calculate(Meta, Key, Opts, Old, File, Existing) -> %% Normalize the data before storing it case ordsets:from_list(New) of [] -> - {false, keydelete(Key, Old)}; + {false, [], keydelete(Key, Old)}; Set -> ensure_no_special_form_conflict(Meta, File, Key, Set), - {true, [{Key, Set} | keydelete(Key, Old)]} + {true, Set, [{Key, Set} | keydelete(Key, Old)]} end. %% Retrieve functions and macros from modules -get_exports(Module) -> - get_functions(Module) ++ get_macros(Module). - get_functions(Module) -> try Module:'__info__'(functions) @@ -143,14 +139,6 @@ get_functions(Module) -> error:undef -> remove_internals(Module:module_info(exports)) end. -get_macros(Module) -> - case fetch_macros(Module) of - {ok, Macros} -> - Macros; - error -> - [] - end. - fetch_macros(Module) -> try {ok, Module:'__info__'(macros)} From 4cad57f5c0425e148b14e8808fc436ccab6280b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 14 Dec 2023 17:19:42 +0100 Subject: [PATCH 0248/1886] Remove unused function --- lib/elixir/test/elixir/kernel/parser_test.exs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/lib/elixir/test/elixir/kernel/parser_test.exs b/lib/elixir/test/elixir/kernel/parser_test.exs index b1803d0e51..d5a077632e 100644 --- a/lib/elixir/test/elixir/kernel/parser_test.exs +++ b/lib/elixir/test/elixir/kernel/parser_test.exs @@ -1071,11 +1071,6 @@ defmodule Kernel.ParserTest do defp parse!(string), do: Code.string_to_quoted!(string) - defp assert_token_missing(given_messages, string) do - e = assert_raise TokenMissingError, fn -> parse!(string) end - assert_exception_msg(e, given_messages) - end - defp assert_syntax_error(given_messages, source) do e = assert_raise SyntaxError, fn -> parse!(source) end assert_exception_msg(e, given_messages) From 80723f5b80e937185b309df8309a284a8c961ac8 Mon Sep 17 00:00:00 2001 From: Tobias Pfeiffer Date: Fri, 15 Dec 2023 17:00:33 +0100 Subject: [PATCH 0249/1886] Fix the port number in the port docs (#13192) Before in the docs the port number seems to be `0.1444` but in the last example it jumped to `0.1464` which I think is a small error or something I don't understand :) --- lib/elixir/lib/port.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/lib/port.ex b/lib/elixir/lib/port.ex index fefcf2fa6a..d33c0d9bd4 100644 --- a/lib/elixir/lib/port.ex +++ b/lib/elixir/lib/port.ex @@ -16,7 +16,7 @@ defmodule Port do iex> send(port, {self(), :close}) :ok iex> flush() - {#Port<0.1464>, :closed} + {#Port<0.1444>, :closed} :ok In the example above, we have created a new port that executes the From a818ee3dd0367b12ed7773b91906dbd475fea16b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sun, 17 Dec 2023 11:40:49 +0100 Subject: [PATCH 0250/1886] Normalize exception handling in diagnostics --- lib/elixir/lib/code.ex | 2 +- lib/elixir/lib/kernel/parallel_compiler.ex | 5 +---- lib/mix/lib/mix/compilers/elixir.ex | 2 +- lib/mix/lib/mix/task.compiler.ex | 2 +- lib/mix/test/mix/tasks/compile_test.exs | 2 +- 5 files changed, 5 insertions(+), 8 deletions(-) diff --git a/lib/elixir/lib/code.ex b/lib/elixir/lib/code.ex index 803d158f02..3ff922fbb4 100644 --- a/lib/elixir/lib/code.ex +++ b/lib/elixir/lib/code.ex @@ -212,7 +212,7 @@ defmodule Code do required(:position) => position(), required(:stacktrace) => Exception.stacktrace(), required(:span) => {line :: pos_integer(), column :: pos_integer()} | nil, - optional(:exception) => Exception.t() | nil, + optional(:details) => term(), optional(any()) => any() } diff --git a/lib/elixir/lib/kernel/parallel_compiler.ex b/lib/elixir/lib/kernel/parallel_compiler.ex index 580e60d348..b616baec0c 100644 --- a/lib/elixir/lib/kernel/parallel_compiler.ex +++ b/lib/elixir/lib/kernel/parallel_compiler.ex @@ -901,13 +901,10 @@ defmodule Kernel.ParallelCompiler do severity: :error, stacktrace: stack, span: span, - exception: get_exception(reason) + details: {kind, reason} } end - defp get_exception(exception) when is_exception(exception), do: exception - defp get_exception(_reason), do: nil - defp get_line_span( _file, %{line: line, column: column, end_line: end_line, end_column: end_column}, diff --git a/lib/mix/lib/mix/compilers/elixir.ex b/lib/mix/lib/mix/compilers/elixir.ex index 22e2686aff..11167a79bf 100644 --- a/lib/mix/lib/mix/compilers/elixir.ex +++ b/lib/mix/lib/mix/compilers/elixir.ex @@ -785,7 +785,7 @@ defmodule Mix.Compilers.Elixir do compiler_name: "Elixir", stacktrace: stacktrace, span: span, - details: Map.get(diagnostic, :exception, nil) + details: Map.get(diagnostic, :details, nil) } end diff --git a/lib/mix/lib/mix/task.compiler.ex b/lib/mix/lib/mix/task.compiler.ex index 44055a710f..5cd4fc989a 100644 --- a/lib/mix/lib/mix/task.compiler.ex +++ b/lib/mix/lib/mix/task.compiler.ex @@ -47,7 +47,7 @@ defmodule Mix.Task.Compiler do message: IO.chardata(), position: Code.position(), compiler_name: String.t(), - details: Exception.t() | any, + details: term(), stacktrace: Exception.stacktrace(), span: {line :: pos_integer(), column :: pos_integer()} | nil } diff --git a/lib/mix/test/mix/tasks/compile_test.exs b/lib/mix/test/mix/tasks/compile_test.exs index cb73f993a2..4efd0b2eee 100644 --- a/lib/mix/test/mix/tasks/compile_test.exs +++ b/lib/mix/test/mix/tasks/compile_test.exs @@ -183,7 +183,7 @@ defmodule Mix.Tasks.CompileTest do position: {2, 20}, message: "** (SyntaxError) invalid syntax found on lib/a.ex:2:" <> _, compiler_name: "Elixir", - details: %SyntaxError{} + details: {:error, %SyntaxError{}} } = diagnostic end) end) From d79c0d2b7a5fb1d6cc3fee7bdc26640813d644a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sun, 17 Dec 2023 13:05:48 +0100 Subject: [PATCH 0251/1886] Improve docs for URI.encode/2 --- lib/elixir/lib/uri.ex | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/lib/elixir/lib/uri.ex b/lib/elixir/lib/uri.ex index c0a02f3a85..fe612a6094 100644 --- a/lib/elixir/lib/uri.ex +++ b/lib/elixir/lib/uri.ex @@ -362,21 +362,23 @@ defmodule URI do end @doc """ - Percent-escapes all characters that require escaping in `string`. + Percent-encodes all characters that require escaping in `string`. - This means reserved characters, such as `:` and `/`, and the - so-called unreserved characters, which have the same meaning both - escaped and unescaped, won't be escaped by default. + By default, this function is meant to escape the whole URI, and + therefore it will escape all characters which are foreign to the + URI specification. Reserved characters (such as `:` and `/`) or + unreserved (such as letters and numbers) are not escaped. - See `encode_www_form/1` if you are interested in escaping reserved - characters too. - - This function also accepts a `predicate` function as an optional + Because different components of a URI require different escaping + rules, this function also accepts a `predicate` function as an optional argument. If passed, this function will be called with each byte in `string` as its argument and should return a truthy value (anything other - than `false` or `nil`) if the given byte should be left as is, or return a - falsy value (`false` or `nil`) if the character should be escaped. Defaults - to `URI.char_unescaped?/1`. + than `false` or `nil`) if the given byte should be left as is, or + return a falsy value (`false` or `nil`) if the character should be + escaped. Defaults to `URI.char_unescaped?/1`. + + See `encode_www_form/1` if you are interested in escaping reserved + characters too. ## Examples From aad341b5c25a37e74327b1e4454e74fcc18716de Mon Sep 17 00:00:00 2001 From: Gonzalo <456459+grzuy@users.noreply.github.com> Date: Mon, 18 Dec 2023 17:07:13 -0300 Subject: [PATCH 0252/1886] docs: small fix in mix deps docs (#13196) --- lib/mix/lib/mix/tasks/deps.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/mix/lib/mix/tasks/deps.ex b/lib/mix/lib/mix/tasks/deps.ex index d1acb08db5..f82141c81d 100644 --- a/lib/mix/lib/mix/tasks/deps.ex +++ b/lib/mix/lib/mix/tasks/deps.ex @@ -82,9 +82,9 @@ defmodule Mix.Tasks.Deps do (like `[:dev, :test]`) * `:targets` - the dependency is made available only for the given targets. - By default the dependency will be available in all environments. The value + By default the dependency will be available in all targets. The value of this option can either be a single target (like `:host`) or a list of - environments (like `[:host, :rpi3]`) + targets (like `[:host, :rpi3]`) * `:override` - if set to `true` the dependency will override any other definitions of itself by other dependencies From 82be1922266ebd24ff52e3dc8cd25a09812fbe29 Mon Sep 17 00:00:00 2001 From: Tobias Pfeiffer Date: Thu, 21 Dec 2023 15:46:31 +0100 Subject: [PATCH 0253/1886] Document the process anti pattern of sending large data (#13194) Follow up to/extension of #13173 --- .../anti-patterns/process-anti-patterns.md | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/lib/elixir/pages/anti-patterns/process-anti-patterns.md b/lib/elixir/pages/anti-patterns/process-anti-patterns.md index 7a7378279b..899cbccac0 100644 --- a/lib/elixir/pages/anti-patterns/process-anti-patterns.md +++ b/lib/elixir/pages/anti-patterns/process-anti-patterns.md @@ -188,6 +188,58 @@ iex> Foo.Bucket.get(bucket, "milk") This anti-pattern was formerly known as [Agent obsession](https://github.com/lucasvegi/Elixir-Code-Smells/tree/main#agent-obsession). +## Sending unnecessary data + +#### Problem + +Sending a message to a process can be an expensive operation if the message is big enough. That's because that message will be fully copied to the receiving process, which may be CPU and memory intensive. This is due to Erlang's "share nothing" architecture, where each process has its own memory, which simplifies and speeds up garbage collection. + +This is more obvious when using `send/2`, `GenServer.call/3`, or the initial data in `GenServer.start_link/3`. Notably this also happens when using `spawn/1`, `Task.async/1`, `Task.async_stream/3`, and so on. It is more subtle here as the anonymous function passed to these functions captures the variables it references, and all captured variables will be copied over. By doing this, you can accidentally send way more data to a process than you actually need. + +#### Example + +Imagine you were to implement some simple reporting of IP addresses that made requests against your application. You want to do this asynchronously and not block processing, so you decide to use `spawn/1`. It may seem like a good idea to hand over the whole connection because we might need more data later. However passing the connection results in copying a lot of unnecessary data like the request body, params, etc. + +```elixir +# log_request_ip send the ip to some external service +spawn(fn -> log_request_ip(conn) end) +``` + +This problem also occurs when accessing only the relevant parts: + +```elixir +spawn(fn -> log_request_ip(conn.remote_ip) end) +``` + +This will still copy over all of `conn`, because the `conn` variable is being captured inside the spawned function. The function then extracts the `remote_ip` field, but only after the whole `conn` has been copied over. + +`send/2` and the `GenServer` APIs also rely on message passing. In the example below, the `conn` is once again copied to the underlying `GenServer`: + +```elixir +GenServer.cast(pid, {:report_ip_address, conn}) +``` + +#### Refactoring + +This anti-pattern has many potential remedies: + +* Limit the data you send to the absolute necessary minimum instead of sending an entire struct. For example, don't send an entire `conn` struct if all you need is a couple of fields. +* If the only process that needs data is the one you are sending to, consider making the process fetch that data instead of passing it. +* Some abstractions, such as [`:persistent_term`](https://www.erlang.org/doc/man/persistent_term.html), allows you to share data between processes, as long as such data changes infrequently. + +In our case, limiting the input data is a reasonable strategy. If all we need *right now* is the IP address, then let's only work with that and make sure we're only passing the IP address into the closure, like so: + +```elixir +ip_address = conn.remote_ip +spawn(fn -> log_request_ip(ip_address) end) +``` + +Or in the `GenServer` case: + +```elixir +GenServer.cast(pid, {:report_ip_address, conn.remote_ip}) +``` + ## Unsupervised processes #### Problem From 8ecb7deece702f0690dbd9f7f0bbc75bd75e3d9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 21 Dec 2023 15:47:40 +0100 Subject: [PATCH 0254/1886] Indent lists --- lib/elixir/pages/anti-patterns/process-anti-patterns.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/elixir/pages/anti-patterns/process-anti-patterns.md b/lib/elixir/pages/anti-patterns/process-anti-patterns.md index 899cbccac0..c27c2ea055 100644 --- a/lib/elixir/pages/anti-patterns/process-anti-patterns.md +++ b/lib/elixir/pages/anti-patterns/process-anti-patterns.md @@ -223,9 +223,11 @@ GenServer.cast(pid, {:report_ip_address, conn}) This anti-pattern has many potential remedies: -* Limit the data you send to the absolute necessary minimum instead of sending an entire struct. For example, don't send an entire `conn` struct if all you need is a couple of fields. -* If the only process that needs data is the one you are sending to, consider making the process fetch that data instead of passing it. -* Some abstractions, such as [`:persistent_term`](https://www.erlang.org/doc/man/persistent_term.html), allows you to share data between processes, as long as such data changes infrequently. + * Limit the data you send to the absolute necessary minimum instead of sending an entire struct. For example, don't send an entire `conn` struct if all you need is a couple of fields. + + * If the only process that needs data is the one you are sending to, consider making the process fetch that data instead of passing it. + + * Some abstractions, such as [`:persistent_term`](https://www.erlang.org/doc/man/persistent_term.html), allows you to share data between processes, as long as such data changes infrequently. In our case, limiting the input data is a reasonable strategy. If all we need *right now* is the IP address, then let's only work with that and make sure we're only passing the IP address into the closure, like so: From 43e029cbb4614734da33e61e8faaf670edc633c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 21 Dec 2023 16:17:05 +0100 Subject: [PATCH 0255/1886] Do not reset column state in tests --- lib/eex/test/eex_test.exs | 57 ++++++++----------- .../elixir/kernel/parallel_compiler_test.exs | 16 ++++-- .../test/elixir/kernel/tracers_test.exs | 2 - 3 files changed, 35 insertions(+), 40 deletions(-) diff --git a/lib/eex/test/eex_test.exs b/lib/eex/test/eex_test.exs index 2e60d90354..8436bd65b3 100644 --- a/lib/eex/test/eex_test.exs +++ b/lib/eex/test/eex_test.exs @@ -761,38 +761,31 @@ defmodule EExTest do end test "line and column meta" do - parser_options = Code.get_compiler_option(:parser_options) - Code.put_compiler_option(:parser_options, columns: true) - - try do - indentation = 12 - - ast = - EEx.compile_string( - """ - <%= f() %> <% f() %> - <%= f fn -> %> - <%= f() %> - <% end %> - """, - indentation: indentation - ) - - {_, calls} = - Macro.prewalk(ast, [], fn - {:f, meta, _args} = expr, acc -> {expr, [meta | acc]} - other, acc -> {other, acc} - end) - - assert Enum.reverse(calls) == [ - [line: 1, column: indentation + 5], - [line: 1, column: indentation + 15], - [line: 2, column: indentation + 7], - [line: 3, column: indentation + 9] - ] - after - Code.put_compiler_option(:parser_options, parser_options) - end + indentation = 12 + + ast = + EEx.compile_string( + """ + <%= f() %> <% f() %> + <%= f fn -> %> + <%= f() %> + <% end %> + """, + indentation: indentation + ) + + {_, calls} = + Macro.prewalk(ast, [], fn + {:f, meta, _args} = expr, acc -> {expr, [meta | acc]} + other, acc -> {other, acc} + end) + + assert Enum.reverse(calls) == [ + [line: 1, column: indentation + 5], + [line: 1, column: indentation + 15], + [line: 2, column: indentation + 7], + [line: 3, column: indentation + 9] + ] end end diff --git a/lib/elixir/test/elixir/kernel/parallel_compiler_test.exs b/lib/elixir/test/elixir/kernel/parallel_compiler_test.exs index 21de7d4813..c81e201457 100644 --- a/lib/elixir/test/elixir/kernel/parallel_compiler_test.exs +++ b/lib/elixir/test/elixir/kernel/parallel_compiler_test.exs @@ -147,7 +147,7 @@ defmodule Kernel.ParallelCompilerTest do expected_msg = "Undef.__struct__/1 is undefined, cannot expand struct Undef" assert capture_io(:stderr, fn -> - assert {:error, [{^fixture, 3, msg}, {^fixture, 0, compile_msg}], []} = + assert {:error, [{^fixture, {3, 5}, msg}, {^fixture, 0, compile_msg}], []} = Kernel.ParallelCompiler.compile([fixture]) assert msg =~ expected_msg @@ -216,7 +216,7 @@ defmodule Kernel.ParallelCompilerTest do "ThisModuleWillNeverBeAvailable.__struct__/1 is undefined, cannot expand struct ThisModuleWillNeverBeAvailable" assert capture_io(:stderr, fn -> - assert {:error, [{^fixture, 7, msg}, {^fixture, 0, compile_msg}], []} = + assert {:error, [{^fixture, {7, 3}, msg}, {^fixture, 0, compile_msg}], []} = Kernel.ParallelCompiler.compile([fixture]) assert msg =~ expected_msg @@ -245,7 +245,9 @@ defmodule Kernel.ParallelCompilerTest do "ThisModuleWillNeverBeAvailable.__struct__/1 is undefined, cannot expand struct ThisModuleWillNeverBeAvailable" assert capture_io(:stderr, fn -> - assert {:error, [{^missing_struct, 2, msg}, {^missing_struct, 0, compile_msg}], []} = + assert {:error, + [{^missing_struct, {2, 3}, msg}, {^missing_struct, 0, compile_msg}], + []} = Kernel.ParallelCompiler.compile([missing_struct, depends_on]) assert msg =~ expected_msg @@ -272,7 +274,9 @@ defmodule Kernel.ParallelCompilerTest do expected_msg = "module Unknown.Module is not loaded and could not be found" assert capture_io(:stderr, fn -> - assert {:error, [{^missing_import, 2, msg}, {^missing_import, 0, compile_msg}], []} = + assert {:error, + [{^missing_import, {2, 3}, msg}, {^missing_import, 0, compile_msg}], + []} = Kernel.ParallelCompiler.compile([missing_import, depends_on]) assert msg =~ expected_msg @@ -509,7 +513,7 @@ defmodule Kernel.ParallelCompilerTest do expected_msg = "Undef.__struct__/1 is undefined, cannot expand struct Undef" assert capture_io(:stderr, fn -> - assert {:error, [{^fixture, 3, msg}, {^fixture, 0, compile_msg}], []} = + assert {:error, [{^fixture, {3, 5}, msg}, {^fixture, 0, compile_msg}], []} = Kernel.ParallelCompiler.require([fixture]) assert msg =~ expected_msg @@ -537,7 +541,7 @@ defmodule Kernel.ParallelCompilerTest do "ThisModuleWillNeverBeAvailable.__struct__/1 is undefined, cannot expand struct ThisModuleWillNeverBeAvailable" assert capture_io(:stderr, fn -> - assert {:error, [{^fixture, 7, msg}, {^fixture, 0, compile_msg}], []} = + assert {:error, [{^fixture, {7, 3}, msg}, {^fixture, 0, compile_msg}], []} = Kernel.ParallelCompiler.require([fixture]) assert msg =~ expected_msg diff --git a/lib/elixir/test/elixir/kernel/tracers_test.exs b/lib/elixir/test/elixir/kernel/tracers_test.exs index 6bc2ccf659..08826bf324 100644 --- a/lib/elixir/test/elixir/kernel/tracers_test.exs +++ b/lib/elixir/test/elixir/kernel/tracers_test.exs @@ -16,11 +16,9 @@ defmodule Kernel.TracersTest do setup_all do Code.put_compiler_option(:tracers, [__MODULE__]) - Code.put_compiler_option(:parser_options, columns: true) on_exit(fn -> Code.put_compiler_option(:tracers, []) - Code.put_compiler_option(:parser_options, []) end) end From 3a52c2d729aec45004399d17141e69e5efc2b0de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 21 Dec 2023 17:05:47 +0100 Subject: [PATCH 0256/1886] Add source field to diagnostics --- lib/elixir/lib/code.ex | 7 +++ lib/elixir/lib/kernel/parallel_compiler.ex | 50 ++++++++++--------- lib/elixir/lib/module/parallel_checker.ex | 1 + lib/elixir/src/elixir_errors.erl | 1 + lib/elixir/test/elixir/code_test.exs | 1 + .../elixir/kernel/parallel_compiler_test.exs | 18 +++++++ lib/mix/lib/mix/compilers/elixir.ex | 6 ++- lib/mix/lib/mix/compilers/erlang.ex | 5 +- lib/mix/lib/mix/task.compiler.ex | 8 +++ .../test/mix/tasks/compile.elixir_test.exs | 4 ++ .../test/mix/tasks/compile.erlang_test.exs | 2 + lib/mix/test/mix/tasks/compile.leex_test.exs | 1 + lib/mix/test/mix/tasks/compile.yecc_test.exs | 3 ++ lib/mix/test/mix/tasks/compile_test.exs | 3 ++ 14 files changed, 85 insertions(+), 25 deletions(-) diff --git a/lib/elixir/lib/code.ex b/lib/elixir/lib/code.ex index 3ff922fbb4..f71e75366d 100644 --- a/lib/elixir/lib/code.ex +++ b/lib/elixir/lib/code.ex @@ -197,6 +197,7 @@ defmodule Code do @typedoc """ Diagnostics returned by the compiler and code evaluation. + The file and position relate to where the diagnostic should be shown. If there is a file and position, then the diagnostic is precise and you can use the given file and position for generating snippets, IDEs annotations, and so on. An optional span is available with @@ -204,8 +205,14 @@ defmodule Code do Otherwise, a stacktrace may be given, which you can place your own heuristics to provide better reporting. + + The source field points to the source file the compiler tracked + the error to. For example, a file `lib/foo.ex` may embed `.eex` + templates from `lib/foo/bar.eex`. A syntax error on the EEx template + will point to file `lib/foo/bar.eex` but the source is `lib/foo.ex`. """ @type diagnostic(severity) :: %{ + required(:source) => Path.t() | nil, required(:file) => Path.t() | nil, required(:severity) => severity, required(:message) => String.t(), diff --git a/lib/elixir/lib/kernel/parallel_compiler.ex b/lib/elixir/lib/kernel/parallel_compiler.ex index b616baec0c..d88b23dc73 100644 --- a/lib/elixir/lib/kernel/parallel_compiler.ex +++ b/lib/elixir/lib/kernel/parallel_compiler.ex @@ -859,9 +859,12 @@ defmodule Kernel.ParallelCompiler do ) for {file, _, description, stacktrace} <- deadlock do + file = Path.absname(file) + %{ severity: :error, - file: Path.absname(file), + file: file, + source: file, position: nil, message: description, stacktrace: stacktrace, @@ -889,13 +892,14 @@ defmodule Kernel.ParallelCompiler do ]) end - defp to_error(file, kind, reason, stack) do - {line, span} = get_line_span(file, reason, stack) - file = Path.absname(file) + defp to_error(source, kind, reason, stack) do + {file, line, span} = get_snippet_info(source, reason, stack) + source = Path.absname(source) message = :unicode.characters_to_binary(Kernel.CLI.format_error(kind, reason, stack)) %{ - file: file, + file: file || source, + source: source, position: line || 0, message: message, severity: :error, @@ -905,47 +909,47 @@ defmodule Kernel.ParallelCompiler do } end - defp get_line_span( + defp get_snippet_info( _file, - %{line: line, column: column, end_line: end_line, end_column: end_column}, + %{file: file, line: line, column: column, end_line: end_line, end_column: end_column}, _stack ) when is_integer(line) and line > 0 and is_integer(column) and column >= 0 and is_integer(end_line) and end_line > 0 and is_integer(end_column) and end_column >= 0 do - {{line, column}, {end_line, end_column}} + {Path.absname(file), {line, column}, {end_line, end_column}} end - defp get_line_span(_file, %{line: line, column: column}, _stack) + defp get_snippet_info(_file, %{file: file, line: line, column: column}, _stack) when is_integer(line) and line > 0 and is_integer(column) and column >= 0 do - {{line, column}, nil} + {Path.absname(file), {line, column}, nil} end - defp get_line_span(_file, %{line: line}, _stack) when is_integer(line) and line > 0 do - {line, nil} + defp get_snippet_info(_file, %{line: line}, _stack) when is_integer(line) and line > 0 do + {nil, line, nil} end - defp get_line_span(file, :undef, [{_, _, _, []}, {_, _, _, info} | _]) do - get_line_span_from_stacktrace_info(info, file) + defp get_snippet_info(file, :undef, [{_, _, _, []}, {_, _, _, info} | _]) do + get_snippet_info_from_stacktrace_info(info, file) end - defp get_line_span(file, _reason, [{_, _, _, [file: expanding]}, {_, _, _, info} | _]) + defp get_snippet_info(file, _reason, [{_, _, _, [file: expanding]}, {_, _, _, info} | _]) when expanding in [~c"expanding macro", ~c"expanding struct"] do - get_line_span_from_stacktrace_info(info, file) + get_snippet_info_from_stacktrace_info(info, file) end - defp get_line_span(file, _reason, [{_, _, _, info} | _]) do - get_line_span_from_stacktrace_info(info, file) + defp get_snippet_info(file, _reason, [{_, _, _, info} | _]) do + get_snippet_info_from_stacktrace_info(info, file) end - defp get_line_span(_, _, _) do - {nil, nil} + defp get_snippet_info(_, _, _) do + {nil, nil, nil} end - defp get_line_span_from_stacktrace_info(info, file) do + defp get_snippet_info_from_stacktrace_info(info, file) do if Keyword.get(info, :file) == to_charlist(Path.relative_to_cwd(file)) do - {Keyword.get(info, :line), nil} + {nil, Keyword.get(info, :line), nil} else - {nil, nil} + {nil, nil, nil} end end end diff --git a/lib/elixir/lib/module/parallel_checker.ex b/lib/elixir/lib/module/parallel_checker.ex index 1dab57db3d..d4e51abb9e 100644 --- a/lib/elixir/lib/module/parallel_checker.ex +++ b/lib/elixir/lib/module/parallel_checker.ex @@ -324,6 +324,7 @@ defmodule Module.ParallelChecker do defp to_diagnostic(message, {file, position, mfa}) when is_list(position) do %{ severity: :warning, + source: file, file: file, position: position_to_tuple(position), message: IO.iodata_to_binary(message), diff --git a/lib/elixir/src/elixir_errors.erl b/lib/elixir/src/elixir_errors.erl index efc703949c..c11f739838 100644 --- a/lib/elixir/src/elixir_errors.erl +++ b/lib/elixir/src/elixir_errors.erl @@ -106,6 +106,7 @@ emit_diagnostic(Severity, Position, File, Message, Stacktrace, Options) -> Diagnostic = #{ severity => Severity, + source => File, file => File, position => Position, message => unicode:characters_to_binary(Message), diff --git a/lib/elixir/test/elixir/code_test.exs b/lib/elixir/test/elixir/code_test.exs index 7c370cd41e..929e46567f 100644 --- a/lib/elixir/test/elixir/code_test.exs +++ b/lib/elixir/test/elixir/code_test.exs @@ -250,6 +250,7 @@ defmodule CodeTest do message: "undefined variable \"x\"", position: 1, file: "nofile", + source: "nofile", stacktrace: [], severity: :error } diff --git a/lib/elixir/test/elixir/kernel/parallel_compiler_test.exs b/lib/elixir/test/elixir/kernel/parallel_compiler_test.exs index c81e201457..9edbe5c0fc 100644 --- a/lib/elixir/test/elixir/kernel/parallel_compiler_test.exs +++ b/lib/elixir/test/elixir/kernel/parallel_compiler_test.exs @@ -480,6 +480,24 @@ defmodule Kernel.ParallelCompilerTest do end) end + test "gets correct file+line+column number for SyntaxError" do + File.mkdir_p!(tmp_path()) + + [fixture] = + write_tmp("error", + error: """ + raise SyntaxError, file: "foo/bar.ex", line: 3, column: 10 + """ + ) + + file = Path.absname("foo/bar.ex") + + capture_io(:stderr, fn -> + assert {:error, [%{file: ^file, source: ^fixture, position: {3, 10}}], _} = + Kernel.ParallelCompiler.compile([fixture], return_diagnostics: true) + end) + end + test "gets proper beam destinations from dynamic modules" do fixtures = write_tmp( diff --git a/lib/mix/lib/mix/compilers/elixir.ex b/lib/mix/lib/mix/compilers/elixir.ex index 11167a79bf..937d7eba1a 100644 --- a/lib/mix/lib/mix/compilers/elixir.ex +++ b/lib/mix/lib/mix/compilers/elixir.ex @@ -724,9 +724,11 @@ defmodule Mix.Compilers.Elixir do )} <- sources, file = Path.absname(source), {position, message, span} <- compile_warnings ++ runtime_warnings do + # TODO: Store the whole diagnostic diagnostic = %Mix.Task.Compiler.Diagnostic{ severity: :warning, file: file, + source: file, position: position, message: message, compiler_name: "Elixir", @@ -774,11 +776,13 @@ defmodule Mix.Compilers.Elixir do message: message, severity: severity, stacktrace: stacktrace, - span: span + span: span, + source: source } = diagnostic ) do %Mix.Task.Compiler.Diagnostic{ file: file, + source: source, position: position, message: message, severity: severity, diff --git a/lib/mix/lib/mix/compilers/erlang.ex b/lib/mix/lib/mix/compilers/erlang.ex index 7cc2fa58c1..b97171df38 100644 --- a/lib/mix/lib/mix/compilers/erlang.ex +++ b/lib/mix/lib/mix/compilers/erlang.ex @@ -288,8 +288,11 @@ defmodule Mix.Compilers.Erlang do defp to_diagnostics(warnings_or_errors, severity) do for {file, issues} <- warnings_or_errors, {location, module, data} <- issues do + file = Path.absname(file) + %Mix.Task.Compiler.Diagnostic{ - file: Path.absname(file), + file: file, + source: file, position: location_normalize(location), message: to_string(module.format_error(data)), severity: severity, diff --git a/lib/mix/lib/mix/task.compiler.ex b/lib/mix/lib/mix/task.compiler.ex index 5cd4fc989a..cd0856d420 100644 --- a/lib/mix/lib/mix/task.compiler.ex +++ b/lib/mix/lib/mix/task.compiler.ex @@ -32,6 +32,7 @@ defmodule Mix.Task.Compiler do @moduledoc """ Diagnostic information such as a warning or compilation error. + The file and position relate to where the diagnostic should be shown. If there is a file and position, then the diagnostic is precise and you can use the given file and position for generating snippets, IDEs annotations, and so on. An optional span is available with @@ -39,10 +40,16 @@ defmodule Mix.Task.Compiler do Otherwise, a stacktrace may be given, which you can place your own heuristics to provide better reporting. + + The source field points to the source file the compiler tracked + the error to. For example, a file `lib/foo.ex` may embed `.eex` + templates from `lib/foo/bar.eex`. A syntax error on the EEx template + will point to file `lib/foo/bar.eex` but the source is `lib/foo.ex`. """ @type t :: %__MODULE__{ file: Path.t() | nil, + source: Path.t() | nil, severity: severity, message: IO.chardata(), position: Code.position(), @@ -72,6 +79,7 @@ defmodule Mix.Task.Compiler do @enforce_keys [:file, :severity, :message, :position, :compiler_name] defstruct [ :file, + :source, :severity, :message, :position, diff --git a/lib/mix/test/mix/tasks/compile.elixir_test.exs b/lib/mix/test/mix/tasks/compile.elixir_test.exs index 1bb509fbdd..0cb95353d3 100644 --- a/lib/mix/test/mix/tasks/compile.elixir_test.exs +++ b/lib/mix/test/mix/tasks/compile.elixir_test.exs @@ -1443,6 +1443,7 @@ defmodule Mix.Tasks.Compile.ElixirTest do assert %Diagnostic{ file: ^file, + source: ^file, severity: :warning, position: {2, 13}, compiler_name: "Elixir", @@ -1457,6 +1458,7 @@ defmodule Mix.Tasks.Compile.ElixirTest do assert %Diagnostic{ file: ^file, + source: ^file, severity: :warning, position: {2, 13}, compiler_name: "Elixir", @@ -1518,6 +1520,7 @@ defmodule Mix.Tasks.Compile.ElixirTest do assert %Diagnostic{ file: ^file, + source: ^file, severity: :error, position: {2, 20}, message: "** (SyntaxError) invalid syntax found on lib/a.ex:2:" <> _, @@ -1552,6 +1555,7 @@ defmodule Mix.Tasks.Compile.ElixirTest do assert %Diagnostic{ file: ^file, + source: ^file, severity: :error, position: 2, message: "** (KeyError) key :invalid_key not found" <> _, diff --git a/lib/mix/test/mix/tasks/compile.erlang_test.exs b/lib/mix/test/mix/tasks/compile.erlang_test.exs index d76f1cbcca..a3440ea638 100644 --- a/lib/mix/test/mix/tasks/compile.erlang_test.exs +++ b/lib/mix/test/mix/tasks/compile.erlang_test.exs @@ -95,6 +95,7 @@ defmodule Mix.Tasks.Compile.ErlangTest do assert %Mix.Task.Compiler.Diagnostic{ compiler_name: "erl_parse", file: ^file, + source: ^file, message: "syntax error before: zzz", position: position(2, 5), severity: :error @@ -120,6 +121,7 @@ defmodule Mix.Tasks.Compile.ErlangTest do assert %Mix.Task.Compiler.Diagnostic{ file: ^file, + source: ^file, compiler_name: "erl_lint", message: "function my_fn/0 is unused", position: position(2, 1), diff --git a/lib/mix/test/mix/tasks/compile.leex_test.exs b/lib/mix/test/mix/tasks/compile.leex_test.exs index 25c82b8d29..cbc5378b20 100644 --- a/lib/mix/test/mix/tasks/compile.leex_test.exs +++ b/lib/mix/test/mix/tasks/compile.leex_test.exs @@ -24,6 +24,7 @@ defmodule Mix.Tasks.Compile.LeexTest do assert %Mix.Task.Compiler.Diagnostic{ compiler_name: "leex", file: ^file, + source: ^file, message: "missing Definitions", position: 1, severity: :error diff --git a/lib/mix/test/mix/tasks/compile.yecc_test.exs b/lib/mix/test/mix/tasks/compile.yecc_test.exs index faac3b9600..0eb4cf5675 100644 --- a/lib/mix/test/mix/tasks/compile.yecc_test.exs +++ b/lib/mix/test/mix/tasks/compile.yecc_test.exs @@ -26,6 +26,7 @@ defmodule Mix.Tasks.Compile.YeccTest do assert %Mix.Task.Compiler.Diagnostic{ compiler_name: "yecc", file: ^file, + source: ^file, message: message, position: position(1, 5), severity: :error @@ -56,6 +57,7 @@ defmodule Mix.Tasks.Compile.YeccTest do assert %Mix.Task.Compiler.Diagnostic{ compiler_name: "yecc", file: ^file, + source: ^file, message: "conflicts: 1 shift/reduce, 0 reduce/reduce", position: 0, severity: :warning @@ -73,6 +75,7 @@ defmodule Mix.Tasks.Compile.YeccTest do assert %Mix.Task.Compiler.Diagnostic{ compiler_name: "yecc", file: ^file, + source: ^file, message: "conflicts: 1 shift/reduce, 0 reduce/reduce", position: 0, severity: :warning diff --git a/lib/mix/test/mix/tasks/compile_test.exs b/lib/mix/test/mix/tasks/compile_test.exs index 4efd0b2eee..a812a9bd81 100644 --- a/lib/mix/test/mix/tasks/compile_test.exs +++ b/lib/mix/test/mix/tasks/compile_test.exs @@ -179,6 +179,7 @@ defmodule Mix.Tasks.CompileTest do assert %Mix.Task.Compiler.Diagnostic{ file: ^file, + source: ^file, severity: :error, position: {2, 20}, message: "** (SyntaxError) invalid syntax found on lib/a.ex:2:" <> _, @@ -213,6 +214,7 @@ defmodule Mix.Tasks.CompileTest do assert %Mix.Task.Compiler.Diagnostic{ file: ^file, + source: ^file, severity: :error, position: 3, message: "** (RuntimeError) error\n expanding macro: A.custom_macro/0" <> _, @@ -242,6 +244,7 @@ defmodule Mix.Tasks.CompileTest do assert %Mix.Task.Compiler.Diagnostic{ compiler_name: "erl_parse", file: ^file, + source: ^file, message: "syntax error before: b", position: position(2, 5), severity: :error From 2f512c7aff7b70cf8e222c334d91a98b0abf4f3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 21 Dec 2023 17:22:05 +0100 Subject: [PATCH 0257/1886] Preserve diagnostics based on source field, closes #13142 --- lib/mix/lib/mix/compilers/elixir.ex | 41 +++++++++-------------------- 1 file changed, 13 insertions(+), 28 deletions(-) diff --git a/lib/mix/lib/mix/compilers/elixir.ex b/lib/mix/lib/mix/compilers/elixir.ex index 937d7eba1a..eb9da80e21 100644 --- a/lib/mix/lib/mix/compilers/elixir.ex +++ b/lib/mix/lib/mix/compilers/elixir.ex @@ -1,7 +1,7 @@ defmodule Mix.Compilers.Elixir do @moduledoc false - @manifest_vsn 22 + @manifest_vsn 23 @checkpoint_vsn 2 import Record @@ -171,7 +171,7 @@ defmodule Mix.Compilers.Elixir do state = {%{}, exports, sources, [], modules, removed_modules} compiler_loop(stale, stale_modules, dest, timestamp, opts, state) else - {:ok, info, state} -> + {:ok, %{runtime_warnings: runtime_warnings, compile_warnings: compile_warnings}, state} -> {modules, _exports, sources, _changed, pending_modules, _stale_exports} = state previous_warnings = @@ -179,7 +179,9 @@ defmodule Mix.Compilers.Elixir do do: previous_warnings(sources, true), else: [] - sources = apply_warnings(sources, info) + runtime_warnings = Enum.map(runtime_warnings, &diagnostic/1) + compile_warnings = Enum.map(compile_warnings, &diagnostic/1) + sources = apply_warnings(sources, runtime_warnings, compile_warnings) write_manifest( manifest, @@ -193,8 +195,7 @@ defmodule Mix.Compilers.Elixir do ) put_compile_env(sources) - info_warnings = info.runtime_warnings ++ info.compile_warnings - all_warnings = previous_warnings ++ Enum.map(info_warnings, &diagnostic/1) + all_warnings = previous_warnings ++ runtime_warnings ++ compile_warnings unless_previous_warnings_as_errors(previous_warnings, opts, {:ok, all_warnings}) {:error, errors, %{runtime_warnings: r_warnings, compile_warnings: c_warnings}, state} -> @@ -717,25 +718,9 @@ defmodule Mix.Compilers.Elixir do end defp previous_warnings(sources, print?) do - for {source, - source( - compile_warnings: compile_warnings, - runtime_warnings: runtime_warnings - )} <- sources, - file = Path.absname(source), - {position, message, span} <- compile_warnings ++ runtime_warnings do - # TODO: Store the whole diagnostic - diagnostic = %Mix.Task.Compiler.Diagnostic{ - severity: :warning, - file: file, - source: file, - position: position, - message: message, - compiler_name: "Elixir", - stacktrace: [], - span: span - } - + for {_, source(compile_warnings: compile_warnings, runtime_warnings: runtime_warnings)} <- + sources, + diagnostic <- compile_warnings ++ runtime_warnings do if print? do Mix.shell().print_app() Code.print_diagnostic(diagnostic) @@ -745,13 +730,13 @@ defmodule Mix.Compilers.Elixir do end end - defp apply_warnings(sources, %{runtime_warnings: [], compile_warnings: []}) do + defp apply_warnings(sources, [], []) do sources end - defp apply_warnings(sources, %{runtime_warnings: r_warnings, compile_warnings: c_warnings}) do - runtime_group = Enum.group_by(r_warnings, & &1.file, &{&1.position, &1.message, &1.span}) - compile_group = Enum.group_by(c_warnings, & &1.file, &{&1.position, &1.message, &1.span}) + defp apply_warnings(sources, runtime_warnings, compile_warnings) do + runtime_group = Enum.group_by(runtime_warnings, & &1.source) + compile_group = Enum.group_by(compile_warnings, & &1.source) for {source_path, source_entry} <- sources, into: %{} do key = Path.absname(source_path) From 7f3d62108b32723c753c32ea6fc0fc61429e0cee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 21 Dec 2023 19:26:06 +0100 Subject: [PATCH 0258/1886] Consider column in snippets, closes #13199 --- lib/elixir/src/elixir.hrl | 1 + lib/elixir/src/elixir_errors.erl | 30 ++++++++++++------- lib/elixir/src/elixir_interpolation.erl | 2 +- lib/elixir/src/elixir_tokenizer.erl | 14 ++++----- .../test/elixir/kernel/diagnostics_test.exs | 29 +++++++++--------- 5 files changed, 43 insertions(+), 33 deletions(-) diff --git a/lib/elixir/src/elixir.hrl b/lib/elixir/src/elixir.hrl index e5d12c9338..7aa9522298 100644 --- a/lib/elixir/src/elixir.hrl +++ b/lib/elixir/src/elixir.hrl @@ -35,6 +35,7 @@ identifier_tokenizer=elixir_tokenizer, ascii_identifiers_only=true, indentation=0, + column=1, mismatch_hints=[], warnings=[] }). diff --git a/lib/elixir/src/elixir_errors.erl b/lib/elixir/src/elixir_errors.erl index c11f739838..94947d5d08 100644 --- a/lib/elixir/src/elixir_errors.erl +++ b/lib/elixir/src/elixir_errors.erl @@ -321,7 +321,7 @@ parse_error(Location, File, Error, <<>>, Input) -> _ -> <> end, case lists:keytake(error_type, 1, Location) of - {value, {error_type, unclosed_delimiter}, Loc} -> raise_token_missing(Loc, File, Input, Message); + {value, {error_type, unclosed_delimiter}, Loc} -> raise_unclosed_delimiter(Loc, File, Input, Message); _ -> raise_snippet(Location, File, Input, 'Elixir.TokenMissingError', Message) end; @@ -408,15 +408,16 @@ parse_erl_term(Term) -> Parsed. raise_mismatched_delimiter(Location, File, Input, Message) -> - {InputString, StartLine, _} = Input, - InputBinary = elixir_utils:characters_to_binary(InputString), - KV = [{file, File}, {line_offset, StartLine - 1}, {snippet, InputBinary} | Location], - raise('Elixir.MismatchedDelimiterError', Message, KV). + {InputString, StartLine, StartColumn} = Input, + Snippet = indent(elixir_utils:characters_to_binary(InputString), StartColumn), + Opts = [{file, File}, {line_offset, StartLine - 1}, {snippet, Snippet} | Location], + raise('Elixir.MismatchedDelimiterError', Message, Opts). -raise_token_missing(Location, File, Input, Message) -> - {InputString, StartLine, _} = Input, - InputBinary = elixir_utils:characters_to_binary(InputString), - raise('Elixir.TokenMissingError', Message, [{line_offset, StartLine - 1}, {file, File}, {snippet, InputBinary} | Location]). +raise_unclosed_delimiter(Location, File, Input, Message) -> + {InputString, StartLine, StartColumn} = Input, + Snippet = indent(elixir_utils:characters_to_binary(InputString), StartColumn), + Opts = [{line_offset, StartLine - 1}, {file, File}, {snippet, Snippet} | Location], + raise('Elixir.TokenMissingError', Message, Opts). raise_reserved(Location, File, Input, Keyword) -> raise_snippet(Location, File, Input, 'Elixir.SyntaxError', @@ -425,9 +426,16 @@ raise_reserved(Location, File, Input, Keyword) -> "it can't be used as a variable or be defined nor invoked as a regular function">>). raise_snippet(Location, File, Input, Kind, Message) when is_binary(File) -> - {InputString, StartLine, _} = Input, + {InputString, StartLine, StartColumn} = Input, Snippet = snippet_line(InputString, Location, StartLine), - raise(Kind, Message, [{file, File}, {snippet, Snippet} | Location]). + raise(Kind, Message, [{file, File}, {snippet, indent(Snippet, StartColumn)} | Location]). + +indent(nil, _StartColumn) -> nil; +indent(Snippet, StartColumn) when StartColumn > 1 -> + Prefix = binary:copy(<<" ">>, StartColumn - 1), + Replaced = binary:replace(Snippet, <<"\n">>, <>, [global]), + <>; +indent(Snippet, _StartColumn) -> Snippet. snippet_line(InputString, Location, StartLine) -> {line, Line} = lists:keyfind(line, 1, Location), diff --git a/lib/elixir/src/elixir_interpolation.erl b/lib/elixir/src/elixir_interpolation.erl index e0035559a8..05986309e9 100644 --- a/lib/elixir/src/elixir_interpolation.erl +++ b/lib/elixir/src/elixir_interpolation.erl @@ -97,7 +97,7 @@ extract_nl(Rest, Buffer, Output, Line, Scope, Interpol, [H,H,H] = Last) -> extract(NewRest, NewBuffer, Output, Line + 1, Column, Scope, Interpol, Last) end; extract_nl(Rest, Buffer, Output, Line, Scope, Interpol, Last) -> - extract(Rest, Buffer, Output, Line + 1, 1, Scope, Interpol, Last). + extract(Rest, Buffer, Output, Line + 1, Scope#elixir_tokenizer.column, Scope, Interpol, Last). strip_horizontal_space([H | T], Buffer, Counter) when H =:= $\s; H =:= $\t -> strip_horizontal_space(T, [H | Buffer], Counter + 1); diff --git a/lib/elixir/src/elixir_tokenizer.erl b/lib/elixir/src/elixir_tokenizer.erl index 0caacfd515..9a687d37e4 100644 --- a/lib/elixir/src/elixir_tokenizer.erl +++ b/lib/elixir/src/elixir_tokenizer.erl @@ -125,7 +125,7 @@ tokenize(String, Line, Column, Opts) -> Acc#elixir_tokenizer{unescape=Unescape}; (_, Acc) -> Acc - end, #elixir_tokenizer{identifier_tokenizer=IdentifierTokenizer}, Opts), + end, #elixir_tokenizer{identifier_tokenizer=IdentifierTokenizer, column=Column}, Opts), tokenize(String, Line, Column, Scope, []). @@ -714,9 +714,9 @@ unexpected_token([T | Rest], Line, Column, Scope, Tokens) -> error({?LOC(Line, Column), "unexpected token: ", Message}, Rest, Scope, Tokens). tokenize_eol(Rest, Line, Scope, Tokens) -> - {StrippedRest, Indentation} = strip_horizontal_space(Rest, 0), - IndentedScope = Scope#elixir_tokenizer{indentation=Indentation}, - tokenize(StrippedRest, Line + 1, Indentation + 1, IndentedScope, Tokens). + {StrippedRest, Column} = strip_horizontal_space(Rest, Scope#elixir_tokenizer.column), + IndentedScope = Scope#elixir_tokenizer{indentation=Column-1}, + tokenize(StrippedRest, Line + 1, Column, IndentedScope, Tokens). strip_horizontal_space([H | T], Counter) when ?is_horizontal_space(H) -> strip_horizontal_space(T, Counter + 1); @@ -732,12 +732,12 @@ tokenize_dot(T, Line, Column, DotInfo, Scope, Tokens) -> {Rest, Comment} -> preserve_comments(Line, Column, Tokens, Comment, Rest, Scope), - tokenize_dot(Rest, Line, 1, DotInfo, Scope, Tokens) + tokenize_dot(Rest, Line, Scope#elixir_tokenizer.column, DotInfo, Scope, Tokens) end; {"\r\n" ++ Rest, _} -> - tokenize_dot(Rest, Line + 1, 1, DotInfo, Scope, Tokens); + tokenize_dot(Rest, Line + 1, Scope#elixir_tokenizer.column, DotInfo, Scope, Tokens); {"\n" ++ Rest, _} -> - tokenize_dot(Rest, Line + 1, 1, DotInfo, Scope, Tokens); + tokenize_dot(Rest, Line + 1, Scope#elixir_tokenizer.column, DotInfo, Scope, Tokens); {Rest, Length} -> handle_dot([$. | Rest], Line, Column + Length, DotInfo, Scope, Tokens) end. diff --git a/lib/elixir/test/elixir/kernel/diagnostics_test.exs b/lib/elixir/test/elixir/kernel/diagnostics_test.exs index 4a057180b9..d757a41f1d 100644 --- a/lib/elixir/test/elixir/kernel/diagnostics_test.exs +++ b/lib/elixir/test/elixir/kernel/diagnostics_test.exs @@ -638,24 +638,25 @@ defmodule Kernel.DiagnosticsTest do """ end - test "TokenMissingError (snippet) with offset" do + test "TokenMissingError (snippet) with offset and column" do output = capture_raise( """ 1 + """, TokenMissingError, - line: 3 + line: 3, + column: 3 ) assert output == """ - ** (TokenMissingError) token missing on nofile:3:4: + ** (TokenMissingError) token missing on nofile:3:6: error: syntax error: expression is incomplete │ - 3 │ 1 + - │ ^ + 3 │ 1 + + │ ^ │ - └─ nofile:3:4\ + └─ nofile:3:6\ """ end @@ -852,7 +853,7 @@ defmodule Kernel.DiagnosticsTest do └─ #{path}:3: Sample.a/0 """ - assert capture_eval(source, false) =~ expected + assert capture_eval(source, columns: false) =~ expected after purge(Sample) end @@ -1086,7 +1087,7 @@ defmodule Kernel.DiagnosticsTest do """ - assert capture_eval(source, false) == expected + assert capture_eval(source, columns: false) == expected after purge(Sample) end @@ -1117,7 +1118,7 @@ defmodule Kernel.DiagnosticsTest do """ - assert capture_compile(source, false) == expected + assert capture_compile(source, columns: false) == expected after purge(Sample) end @@ -1290,7 +1291,7 @@ defmodule Kernel.DiagnosticsTest do """ - assert capture_eval(source, false) == expected + assert capture_eval(source, columns: false) == expected after purge(Sample) end @@ -1443,17 +1444,17 @@ defmodule Kernel.DiagnosticsTest do |> Path.relative_to_cwd() end - defp capture_eval(source, columns? \\ true) do + defp capture_eval(source, opts \\ [columns: true]) do capture_io(:stderr, fn -> - quoted = Code.string_to_quoted!(source, columns: columns?) + quoted = Code.string_to_quoted!(source, opts) Code.eval_quoted(quoted) end) end - defp capture_compile(source, columns? \\ true) do + defp capture_compile(source, opts \\ [columns: true]) do capture_io(:stderr, fn -> assert_raise CompileError, fn -> - ast = Code.string_to_quoted!(source, columns: columns?) + ast = Code.string_to_quoted!(source, opts) Code.eval_quoted(ast) end end) From 00202a4d2f7c160ec9293a631ac5cd9e70e05a63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 21 Dec 2023 19:48:39 +0100 Subject: [PATCH 0259/1886] Fix column precision in test --- lib/elixir/test/elixir/code_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/test/elixir/code_test.exs b/lib/elixir/test/elixir/code_test.exs index 929e46567f..2fc391c908 100644 --- a/lib/elixir/test/elixir/code_test.exs +++ b/lib/elixir/test/elixir/code_test.exs @@ -480,7 +480,7 @@ defmodule CodeTest do assert_exception( SyntaxError, - ["nofile:11:5:", "syntax error before:", "1 + * 3", "^"], + ["nofile:11:7:", "syntax error before:", "1 + * 3", "^"], fn -> Code.string_to_quoted!(":ok\n1 + * 3", line: 10, column: 3) end From 926560350bcf57aa0e344b5935be147779bdb47f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 21 Dec 2023 21:14:44 +0100 Subject: [PATCH 0260/1886] Extract snippet in elixir_errors to simplify exception handling --- lib/elixir/lib/exception.ex | 64 +++++----------------- lib/elixir/src/elixir_errors.erl | 83 ++++++++++++++--------------- lib/elixir/src/elixir_tokenizer.erl | 2 - 3 files changed, 54 insertions(+), 95 deletions(-) diff --git a/lib/elixir/lib/exception.ex b/lib/elixir/lib/exception.ex index 1341ce432f..ddf887c1c8 100644 --- a/lib/elixir/lib/exception.ex +++ b/lib/elixir/lib/exception.ex @@ -805,7 +805,6 @@ defmodule Exception do def format_snippet( {start_line, _start_column} = start_pos, {end_line, end_column} = end_pos, - line_offset, description, file, lines, @@ -819,22 +818,12 @@ defmodule Exception do relevant_lines = if end_line - start_line < 5 do - line_range( - lines, - start_pos, - end_pos, - line_offset, - padding, - max_digits, - start_message, - end_message - ) + line_range(lines, start_pos, end_pos, padding, max_digits, start_message, end_message) else trimmed_inbetween_lines( lines, start_pos, end_pos, - line_offset, padding, max_digits, start_message, @@ -854,7 +843,6 @@ defmodule Exception do def format_snippet( {start_line, start_column}, {end_line, end_column}, - line_offset, description, file, lines, @@ -865,9 +853,7 @@ defmodule Exception do max_digits = digits(end_line) general_padding = max(2, max_digits) + 1 padding = n_spaces(general_padding) - - line = Enum.fetch!(lines, end_line - 1 - line_offset) - formatted_line = [line_padding(end_line, max_digits), to_string(end_line), " │ ", line] + formatted_line = [line_padding(end_line, max_digits), to_string(end_line), " │ ", hd(lines)] mismatched_closing_line = [ @@ -914,7 +900,6 @@ defmodule Exception do lines, {start_line, start_column}, {end_line, end_column}, - line_offset, padding, max_digits, start_message, @@ -922,8 +907,8 @@ defmodule Exception do ) do start_padding = line_padding(start_line, max_digits) end_padding = line_padding(end_line, max_digits) - first_line = Enum.fetch!(lines, start_line - 1 - line_offset) - last_line = Enum.fetch!(lines, end_line - 1 - line_offset) + first_line = hd(lines) + last_line = List.last(lines) """ #{start_padding}#{start_line} │ #{first_line} @@ -938,22 +923,12 @@ defmodule Exception do lines, {start_line, start_column}, {end_line, end_column}, - line_offset, padding, max_digits, start_message, end_message ) do - start_line = start_line - 1 - end_line = end_line - 1 - - lines - |> Enum.slice((start_line - line_offset)..(end_line - line_offset)) - |> Enum.zip_with(start_line..end_line, fn line, line_number -> - line_number = line_number + 1 - start_line = start_line + 1 - end_line = end_line + 1 - + Enum.zip_with(lines, start_line..end_line, fn line, line_number -> line_padding = line_padding(line_number, max_digits) cond do @@ -1142,7 +1117,6 @@ defmodule MismatchedDelimiterError do :file, :line, :column, - :line_offset, :end_line, :end_column, :opening_delimiter, @@ -1158,7 +1132,6 @@ defmodule MismatchedDelimiterError do column: start_column, end_line: end_line, end_column: end_column, - line_offset: line_offset, description: description, expected_delimiter: expected_delimiter, file: file, @@ -1176,7 +1149,6 @@ defmodule MismatchedDelimiterError do Exception.format_snippet( start_pos, end_pos, - line_offset, description, file, lines, @@ -1267,7 +1239,6 @@ defmodule TokenMissingError do :column, :end_line, :end_column, - :line_offset, :snippet, :opening_delimiter, :expected_delimiter, @@ -1280,20 +1251,19 @@ defmodule TokenMissingError do line: line, column: column, end_line: end_line, - line_offset: line_offset, description: description, expected_delimiter: expected_delimiter, snippet: snippet }) when not is_nil(snippet) and not is_nil(column) and not is_nil(end_line) do - {lines, total_trimmed_lines} = handle_trailing_newlines(snippet) - end_line = end_line - total_trimmed_lines + {trimmed, [last_line | _] = reversed_lines} = + snippet + |> String.split("\n") + |> Enum.reverse() + |> Enum.split_while(&(&1 == "")) - end_column = - lines - |> Enum.fetch!(end_line - line_offset - 1) - |> String.length() - |> Kernel.+(1) + end_line = end_line - length(trimmed) + end_column = String.length(last_line) + 1 start_pos = {line, column} end_pos = {end_line, end_column} @@ -1306,10 +1276,9 @@ defmodule TokenMissingError do Exception.format_snippet( start_pos, end_pos, - line_offset, description, file, - lines, + Enum.reverse(reversed_lines), start_message, end_message ) @@ -1331,13 +1300,6 @@ defmodule TokenMissingError do format_message(file, line, column, snippet) end - defp handle_trailing_newlines(snippet) do - trimmed_snippet = String.trim_trailing(snippet, "\n") - total_trimmed_newlines = String.length(snippet) - String.length(trimmed_snippet) - lines = String.split(trimmed_snippet, "\n") - {lines, total_trimmed_newlines} - end - defp format_message(file, line, column, message) do location = Exception.format_file_line_column(Path.relative_to_cwd(file), line, column) "token missing on " <> location <> "\n" <> message diff --git a/lib/elixir/src/elixir_errors.erl b/lib/elixir/src/elixir_errors.erl index 94947d5d08..83542097bf 100644 --- a/lib/elixir/src/elixir_errors.erl +++ b/lib/elixir/src/elixir_errors.erl @@ -26,7 +26,7 @@ print_warning(Position, File, Message) -> %% Called by Module.ParallelChecker. print_warning(Message, Diagnostic) -> #{file := File, position := Position, stacktrace := S} = Diagnostic, - Snippet = get_snippet(File, Position), + Snippet = read_snippet(File, Position), Span = get_span(Diagnostic), Output = format_snippet(Position, File, Message, Snippet, warning, S, Span), io:put_chars(standard_error, [Output, $\n, $\n]). @@ -34,7 +34,7 @@ print_warning(Message, Diagnostic) -> %% Called by Module.ParallelChecker. print_warning_group(Message, [Diagnostic | Others]) -> #{file := File, position := Position, stacktrace := S} = Diagnostic, - Snippet = get_snippet(File, Position), + Snippet = read_snippet(File, Position), Span = get_span(Diagnostic), Formatted = format_snippet(Position, File, Message, Snippet, warning, S, Span), LineNumber = extract_line(Position), @@ -49,11 +49,11 @@ print_warning_group(Message, [Diagnostic | Others]) -> get_span(#{span := nil}) -> nil; get_span(#{span := Span}) -> Span. -get_snippet(nil, _Position) -> +read_snippet(nil, _Position) -> nil; -get_snippet(<<"nofile">>, _Position) -> +read_snippet(<<"nofile">>, _Position) -> nil; -get_snippet(File, Position) -> +read_snippet(File, Position) -> LineNumber = extract_line(Position), get_file_line(File, LineNumber). @@ -80,7 +80,7 @@ traverse_file_line(IoDevice, N) -> print_diagnostic(#{severity := Severity, message := M, stacktrace := Stacktrace, position := P, file := F} = Diagnostic, ReadSnippet) -> Snippet = case ReadSnippet of - true -> get_snippet(F, P); + true -> read_snippet(F, P); false -> nil end, @@ -320,10 +320,8 @@ parse_error(Location, File, Error, <<>>, Input) -> <<"syntax error before: ">> -> <<"syntax error: expression is incomplete">>; _ -> <> end, - case lists:keytake(error_type, 1, Location) of - {value, {error_type, unclosed_delimiter}, Loc} -> raise_unclosed_delimiter(Loc, File, Input, Message); - _ -> raise_snippet(Location, File, Input, 'Elixir.TokenMissingError', Message) - end; + + raise_snippet(Location, File, Input, 'Elixir.TokenMissingError', Message); %% Show a nicer message for end of line parse_error(Location, File, <<"syntax error before: ">>, <<"eol">>, Input) -> @@ -398,8 +396,10 @@ parse_error(Location, File, <<"syntax error before: ">>, <<$$, Char/binary>>, In parse_error(Location, File, Error, Token, Input) when is_binary(Error), is_binary(Token) -> Message = <>, case lists:keytake(error_type, 1, Location) of - {value, {error_type, mismatched_delimiter}, Loc} -> raise_mismatched_delimiter(Loc, File, Input, Message); - _ -> raise_snippet(Location, File, Input, 'Elixir.SyntaxError', Message) + {value, {error_type, mismatched_delimiter}, Loc} -> + raise_snippet(Loc, File, Input, 'Elixir.MismatchedDelimiterError', Message); + _ -> + raise_snippet(Location, File, Input, 'Elixir.SyntaxError', Message) end. parse_erl_term(Term) -> @@ -407,18 +407,6 @@ parse_erl_term(Term) -> {ok, Parsed} = erl_parse:parse_term(Tokens ++ [{dot, 1}]), Parsed. -raise_mismatched_delimiter(Location, File, Input, Message) -> - {InputString, StartLine, StartColumn} = Input, - Snippet = indent(elixir_utils:characters_to_binary(InputString), StartColumn), - Opts = [{file, File}, {line_offset, StartLine - 1}, {snippet, Snippet} | Location], - raise('Elixir.MismatchedDelimiterError', Message, Opts). - -raise_unclosed_delimiter(Location, File, Input, Message) -> - {InputString, StartLine, StartColumn} = Input, - Snippet = indent(elixir_utils:characters_to_binary(InputString), StartColumn), - Opts = [{line_offset, StartLine - 1}, {file, File}, {snippet, Snippet} | Location], - raise('Elixir.TokenMissingError', Message, Opts). - raise_reserved(Location, File, Input, Keyword) -> raise_snippet(Location, File, Input, 'Elixir.SyntaxError', <<"syntax error before: ", Keyword/binary, ". \"", Keyword/binary, "\" is a " @@ -426,32 +414,43 @@ raise_reserved(Location, File, Input, Keyword) -> "it can't be used as a variable or be defined nor invoked as a regular function">>). raise_snippet(Location, File, Input, Kind, Message) when is_binary(File) -> - {InputString, StartLine, StartColumn} = Input, - Snippet = snippet_line(InputString, Location, StartLine), - raise(Kind, Message, [{file, File}, {snippet, indent(Snippet, StartColumn)} | Location]). - -indent(nil, _StartColumn) -> nil; -indent(Snippet, StartColumn) when StartColumn > 1 -> - Prefix = binary:copy(<<" ">>, StartColumn - 1), - Replaced = binary:replace(Snippet, <<"\n">>, <>, [global]), - <>; -indent(Snippet, _StartColumn) -> Snippet. - -snippet_line(InputString, Location, StartLine) -> - {line, Line} = lists:keyfind(line, 1, Location), + Snippet = cut_snippet(Location, Input), + raise(Kind, Message, [{file, File}, {snippet, Snippet} | Location]). + +cut_snippet(Location, Input) -> case lists:keyfind(column, 1, Location) of {column, _} -> - Lines = string:split(InputString, "\n", all), - Snippet = (lists:nth(Line - StartLine + 1, Lines)), - case string:trim(Snippet, leading) of - [] -> nil; - _ -> elixir_utils:characters_to_binary(Snippet) + {line, Line} = lists:keyfind(line, 1, Location), + + case lists:keyfind(end_line, 1, Location) of + {end_line, EndLine} -> + cut_snippet(Input, Line, EndLine - Line + 1); + + false -> + Snippet = cut_snippet(Input, Line, 1), + case string:trim(Snippet, leading) of + <<>> -> nil; + _ -> Snippet + end end; false -> nil end. +cut_snippet({InputString, StartLine, StartColumn}, Line, Span) -> + %% In case the code is indented, we need to add the indentation back + %% for the snippets to match the reported columns. + Indent = binary:copy(<<" ">>, StartColumn - 1), + Lines = string:split(InputString, "\n", all), + [Head | Tail] = lists:nthtail(Line - StartLine, Lines), + IndentedTail = indent_n(Tail, Span - 1, <<"\n", Indent/binary>>), + elixir_utils:characters_to_binary([Indent, Head, IndentedTail]). + +indent_n([], _Count, _Indent) -> []; +indent_n(_Lines, 0, _Indent) -> []; +indent_n([H | T], Count, Indent) -> [Indent, H | indent_n(T, Count - 1, Indent)]. + %% Helpers prefix(warning) -> highlight(<<"warning:">>, warning); diff --git a/lib/elixir/src/elixir_tokenizer.erl b/lib/elixir/src/elixir_tokenizer.erl index 9a687d37e4..2a0d1766ff 100644 --- a/lib/elixir/src/elixir_tokenizer.erl +++ b/lib/elixir/src/elixir_tokenizer.erl @@ -148,7 +148,6 @@ tokenize([], EndLine, EndColumn, #elixir_tokenizer{terminators=[{Start, {StartLi Message = "missing terminator: ~ts", Formatted = io_lib:format(Message, [End]), Meta = [ - {error_type, unclosed_delimiter}, {opening_delimiter, Start}, {expected_delimiter, End}, {line, StartLine}, @@ -1352,7 +1351,6 @@ interpolation_error(Reason, Rest, Scope, Tokens, Extension, Args, Line, Column, interpolation_format({string, EndLine, EndColumn, Message, Token}, Extension, Args, Line, Column, Opening, Closing) -> Meta = [ - {error_type, unclosed_delimiter}, {opening_delimiter, list_to_atom(Opening)}, {expected_delimiter, list_to_atom(Closing)}, {line, Line}, From b217864152a2aeff9152dd1031b3e5a8a8c1e22c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 22 Dec 2023 14:31:44 +0100 Subject: [PATCH 0261/1886] Last pass over anti-patterns --- .../pages/anti-patterns/code-anti-patterns.md | 91 ++++++++----------- .../anti-patterns/design-anti-patterns.md | 65 +------------ .../anti-patterns/macro-anti-patterns.md | 2 +- .../anti-patterns/process-anti-patterns.md | 29 +++--- 4 files changed, 61 insertions(+), 126 deletions(-) diff --git a/lib/elixir/pages/anti-patterns/code-anti-patterns.md b/lib/elixir/pages/anti-patterns/code-anti-patterns.md index 2d0147f5a6..e93f314057 100644 --- a/lib/elixir/pages/anti-patterns/code-anti-patterns.md +++ b/lib/elixir/pages/anti-patterns/code-anti-patterns.md @@ -309,9 +309,13 @@ There are few known exceptions to this anti-pattern: #### Problem -In Elixir, it is possible to access values from `Map`s, which are key-value data structures, either statically or dynamically. When a key is expected to exist in the map, it must be accessed using the `map.key` notation, which asserts the key exists. If the key does not exist, an exception is raised (and in some situations also compiler warnings), allowing developers to catch bugs early on. +In Elixir, it is possible to access values from `Map`s, which are key-value data structures, either statically or dynamically. -`map[:key]` must be used with optional keys. This way, if the informed key does not exist, `nil` is returned. When used with required keys, this return can be confusing and allow `nil` values to pass through the system, while `map.key` would raise upfront. In this way, this anti-pattern may cause bugs in the code. +When a key is expected to exist in a map, it must be accessed using the `map.key` notation, making it clear to developers (and the compiler) that the key must exist. If the key does not exist, an exception is raised (and in some cases also compiler warnings). This is also known as the static notation, as the key is known at the time of writing the code. + +When a key is optional, the `map[:key]` notation must be used instead. This way, if the informed key does not exist, `nil` is returned. This is the dynamic notation, as it also supports dynamic key access, such as `map[some_var]`. + +When you use `map[:key]` to access a key that always exists in the map, you are making the code less clear for developers and for the compiler, as they now need to work with the assumption the key may not be there. This mismatch may also make it harder to track certain bugs. If the key is unexpected missing, you will have a `nil` value propagate through the system, instead of raising on map access. #### Example @@ -321,8 +325,6 @@ The function `plot/1` tries to draw a graphic to represent the position of a poi defmodule Graphics do def plot(point) do # Some other code... - - # Dynamic access to use point values {point[:x], point[:y], point[:z]} end end @@ -331,92 +333,77 @@ end ```elixir iex> point_2d = %{x: 2, y: 3} %{x: 2, y: 3} -iex> point_3d = %{x: 5, y: 6, z: nil} -%{x: 5, y: 6, z: nil} +iex> point_3d = %{x: 5, y: 6, z: 7} +%{x: 5, y: 6, z: 7} iex> Graphics.plot(point_2d) -{2, 3, nil} # <= ambiguous return +{2, 3, nil} iex> Graphics.plot(point_3d) -{5, 6, nil} +{5, 6, 7} +``` + +Given we want to plot both 2D and 3D points, the behaviour above is expected. But what happens if we forget to pass a point with either `:x` or `:y`? + +```elixir +iex> bad_point = %{y: 3, z: 4} +%{y: 3, z: 4} +iex> Graphics.plot(bad_point) +{nil, 3, 4} ``` -As can be seen in the example above, even when the key `:z` does not exist in the map (`point_2d`), dynamic access returns the value `nil`. This return can be dangerous because of its ambiguity. It is not possible to conclude from it whether the map has the key `:z` or not. If the function relies on the return value to make decisions about how to plot a point, this can be problematic and even cause errors when testing the code. +The behaviour above is unexpected because our function should not work with points without a `:x` key. This leads to subtle bugs, as we may now pass `nil` to another function, instead of raising early on. #### Refactoring -To remove this anti-pattern, whenever accessing an existing key of `Atom` type in the map, replace the dynamic `map[:key]` syntax by the static `map.key` notation. This way, when a non-existent key is accessed, Elixir raises an error immediately, allowing developers to find bugs faster. The next code illustrates the refactoring of `plot/1`, removing this anti-pattern: +To remove this anti-pattern, we must use the dynamic `map[:key]` syntax and the static `map.key` notation according to our requirements. We expect `:x` and `:y` to always exist, but not `:z`. The next code illustrates the refactoring of `plot/1`, removing this anti-pattern: ```elixir defmodule Graphics do def plot(point) do # Some other code... - - # Strict access to use point values - {point.x, point.y, point.z} + {point.x, point.y, point[:z]} end end ``` ```elixir -iex> point_2d = %{x: 2, y: 3} -%{x: 2, y: 3} -iex> point_3d = %{x: 5, y: 6, z: nil} -%{x: 5, y: 6, z: nil} iex> Graphics.plot(point_2d) -** (KeyError) key :z not found in: %{x: 2, y: 3} # <= explicitly warns that - graphic.ex:6: Graphics.plot/1 # <= the :z key does not exist! -iex> Graphics.plot(point_3d) -{5, 6, nil} +{2, 3, nil} +iex> Graphics.plot(bad_point) +** (KeyError) key :x not found in: %{y: 3, z: 4} # <= explicitly warns that + graphic.ex:4: Graphics.plot/1 # <= the :z key does not exist! ``` Overall, the usage of `map.key` and `map[:key]` encode important information about your data structure, allowing developers to be clear about their intent. See both `Map` and `Access` module documentation for more information and examples. -An even simpler alternative to refactor this anti-pattern is to use pattern matching: +An alternative to refactor this anti-pattern is to use pattern matching, defining explicit clauses for 2d vs 3d points: ```elixir defmodule Graphics do - def plot(%{x: x, y: y, z: z}) do + # 2d + def plot(%{x: x, y: y}) do # Some other code... + {x, y} + end - # Strict access to use point values + # 3d + def plot(%{x: x, y: y, z: z}) do + # Some other code... {x, y, z} end end ``` -```elixir -iex> point_2d = %{x: 2, y: 3} -%{x: 2, y: 3} -iex> point_3d = %{x: 5, y: 6, z: nil} -%{x: 5, y: 6, z: nil} -iex> Graphics.plot(point_2d) -** (FunctionClauseError) no function clause matching in Graphics.plot/1 - graphic.ex:2: Graphics.plot/1 # <= the :z key does not exist! -iex> Graphics.plot(point_3d) -{5, 6, nil} -``` - -Pattern-matching is specially useful when matching over multiple keys at once and also when you want to match and assert on the values of a map. +Pattern-matching is specially useful when matching over multiple keys as well as on the values themselves at once. -Another alternative is to use structs. By default, structs only support static access to its fields: +Another option is to use structs. By default, structs only support static access to its fields. In such scenarios, you may consider defining structs for both 2D and 3D points: ```elixir -defmodule Point.2D do +defmodule Point2D do @enforce_keys [:x, :y] defstruct [x: nil, y: nil] end ``` -```elixir -iex> point = %Point.2D{x: 2, y: 3} -%Point.2D{x: 2, y: 3} -iex> point.x # <= strict access to use point values -2 -iex> point.z # <= trying to access a non-existent key -** (KeyError) key :z not found in: %Point{x: 2, y: 3} -iex> point[:x] # <= by default, struct does not support dynamic access -** (UndefinedFunctionError) ... (Point does not implement the Access behaviour) -``` - Generally speaking, structs are useful when sharing data structures across modules, at the cost of adding a compile time dependency between these modules. If module `A` uses a struct defined in module `B`, `A` must be recompiled if the fields in the struct `B` change. #### Additional remarks @@ -498,7 +485,7 @@ case some_function(arg) do end ``` -In particular, avoid matching solely on `_`, as shown below, as it is less clear in intent and it may hide bugs if `some_function/1` adds new return values in the future: +In particular, avoid matching solely on `_`, as shown below: ```elixir case some_function(arg) do @@ -507,6 +494,8 @@ case some_function(arg) do end ``` + Matching on `_` is less clear in intent and it may hide bugs if `some_function/1` adds new return values in the future. + #### Additional remarks This anti-pattern was formerly known as [Speculative assumptions](https://github.com/lucasvegi/Elixir-Code-Smells#speculative-assumptions). diff --git a/lib/elixir/pages/anti-patterns/design-anti-patterns.md b/lib/elixir/pages/anti-patterns/design-anti-patterns.md index 009a970b7e..0e3ecc81f7 100644 --- a/lib/elixir/pages/anti-patterns/design-anti-patterns.md +++ b/lib/elixir/pages/anti-patterns/design-anti-patterns.md @@ -1,7 +1,6 @@ # Design-related anti-patterns -This document outlines potential anti-patterns related to your modules, functions, and the role they -play within a codebase. +This document outlines potential anti-patterns related to your modules, functions, and the role they play within a codebase. ## Alternative return types @@ -242,66 +241,6 @@ defmodule MyApp do end ``` -## Propagating invalid data - -#### Problem - -This anti-pattern refers to a function that does not validate its parameters and propagates them to other functions, which can produce internal unexpected behavior. When an error is raised inside a function due to an invalid parameter value, it can be confusing for developers and make it harder to locate and fix the error. - -#### Example - -An example of this anti-pattern is when a function receives an invalid parameter and then passes it to other functions, either in the same library or in a third-party library. This can cause an error to be raised deep inside the call stack, which may be confusing for the developer who is working with invalid data. As shown next, the function `foo/1` is a user-facing API which doesn't validate its parameters at the boundary. In this way, it is possible that invalid data will be passed through, causing an error that is obscure and hard to debug. - -```elixir -defmodule MyLibrary do - def foo(invalid_data) do - # Some other code... - - MyLibrary.Internal.sum(1, invalid_data) - end -end -``` - -```elixir -iex> MyLibrary.foo(2) -3 -iex> MyLibrary.foo("José") # With invalid data -** (ArithmeticError) bad argument in arithmetic expression: 1 + "José" - :erlang.+(1, "José") - my_library.ex:4: MyLibrary.Internal.sum/2 -``` - -#### Refactoring - -To remove this anti-pattern, the client code must validate input parameters at the boundary with the user, via guard clauses, pattern matching, or conditionals. This prevents errors from occurring elsewhere in the call stack, making them easier to understand and debug. This refactoring also allows libraries to be implemented without worrying about creating internal protection mechanisms. The next code snippet illustrates the refactoring of `foo/1`, removing this anti-pattern: - -```elixir -defmodule MyLibrary do - def foo(data) when is_integer(data) do - # Some other code - - MyLibrary.Internal.sum(1, data) - end -end -``` - -```elixir -iex> MyLibrary.foo(2) # With valid data -3 -iex> MyLibrary.foo("José") # With invalid data -** (FunctionClauseError) no function clause matching in MyLibrary.foo/1. -The following arguments were given to MyLibrary.foo/1: - - # 1 - "José" - - my_library.ex:2: MyLibrary.foo/1 -``` - -#### Additional remarks - -This anti-pattern was formerly known as [Working with invalid data](https://github.com/lucasvegi/Elixir-Code-Smells#working-with-invalid-data). - ## Unrelated multi-clause function #### Problem @@ -377,7 +316,7 @@ end You can see this pattern in practice within Elixir itself. The `+/2` operator can add `Integer`s and `Float`s together, but not `String`s, which instead use the `<>/2` operator. In this sense, it is reasonable to handle integers and floats in the same operation, but strings are unrelated enough to deserve their own function. -You will also find examples in Elixir of functions that work with any struct, such as `struct/2`: +You will also find examples in Elixir of functions that work with any struct, which would seemingly be an occurrence of this anti-pattern, such as `struct/2`: ```elixir iex> struct(URI.parse("/foo/bar"), path: "/bar/baz") diff --git a/lib/elixir/pages/anti-patterns/macro-anti-patterns.md b/lib/elixir/pages/anti-patterns/macro-anti-patterns.md index 26a0c60302..1885965318 100644 --- a/lib/elixir/pages/anti-patterns/macro-anti-patterns.md +++ b/lib/elixir/pages/anti-patterns/macro-anti-patterns.md @@ -63,7 +63,7 @@ end #### Problem -**Macros** are powerful meta-programming mechanisms that can be used in Elixir to extend the language. While using macros is not an anti-pattern in itself, this meta-programming mechanism should only be used when absolutely necessary. Whenever a macro is used, but it would have been possible to solve the same problem using functions or other existing Elixir structures, the code becomes unnecessarily more complex and less readable. Because macros are more difficult to implement and reason about, their indiscriminate use can compromise the evolution of a system, reducing its maintainability. +*Macros* are powerful meta-programming mechanisms that can be used in Elixir to extend the language. While using macros is not an anti-pattern in itself, this meta-programming mechanism should only be used when absolutely necessary. Whenever a macro is used, but it would have been possible to solve the same problem using functions or other existing Elixir structures, the code becomes unnecessarily more complex and less readable. Because macros are more difficult to implement and reason about, their indiscriminate use can compromise the evolution of a system, reducing its maintainability. #### Example diff --git a/lib/elixir/pages/anti-patterns/process-anti-patterns.md b/lib/elixir/pages/anti-patterns/process-anti-patterns.md index c27c2ea055..6c513b8167 100644 --- a/lib/elixir/pages/anti-patterns/process-anti-patterns.md +++ b/lib/elixir/pages/anti-patterns/process-anti-patterns.md @@ -6,7 +6,7 @@ This document outlines potential anti-patterns related to processes and process- #### Problem -This anti-pattern refers to code that is unnecessarily organized by processes. A process itself does not represent an anti-pattern, but it should only be used to model runtime properties (such as concurrency, access to shared resources, and event scheduling). When you use a process for code organization, it can create bottlenecks in the system. +This anti-pattern refers to code that is unnecessarily organized by processes. A process itself does not represent an anti-pattern, but it should only be used to model runtime properties (such as concurrency, access to shared resources, error isolation, etc). When you use a process for code organization, it can create bottlenecks in the system. #### Example @@ -261,7 +261,9 @@ defmodule Counter do use GenServer @doc "Starts a counter process." - def start(initial_value, name \\ __MODULE__) when is_integer(initial_value) do + def start_link(opts \\ []) do + initial_valye = Keyword.get(opts, :initial_value, 0) + name = Keywoird.get(opts, :name, __MODULE__) GenServer.start(__MODULE__, initial_value, name: name) end @@ -271,7 +273,7 @@ defmodule Counter do end @doc "Bumps the value of the given counter." - def bump(value, pid_name \\ __MODULE__) do + def bump(pid_name \\ __MODULE__, value) do GenServer.call(pid_name, {:bump, value}) end @@ -292,17 +294,17 @@ end ``` ```elixir -iex> Counter.start(0) +iex> Counter.start_link() {:ok, #PID<0.115.0>} iex> Counter.get() 0 -iex> Counter.start(15, :other_counter) +iex> Counter.start_link(initial_value: 15, name: :other_counter) {:ok, #PID<0.120.0>} iex> Counter.get(:other_counter) 15 -iex> Counter.bump(-3, :other_counter) +iex> Counter.bump(:other_counter, -3) 12 -iex> Counter.bump(7) +iex> Counter.bump(Counter, 7) 7 ``` @@ -317,8 +319,13 @@ defmodule SupervisedProcess.Application do @impl true def start(_type, _args) do children = [ - %{id: Counter, start: {Counter, :start, [0]}}, - %{id: :other_counter, start: {Counter, :start, [0, :other_counter]}} + # With the default values for counter and name + Counter, + # With custom values for counter, name, and a custom ID + Supervisor.child_spec( + {Counter, name: :other_counter, initial_value: 15}, + id: :other_counter + ) ] Supervisor.start_link(children, strategy: :one_for_one, name: App.Supervisor) @@ -332,8 +339,8 @@ iex> Supervisor.count_children(App.Supervisor) iex> Counter.get(Counter) 0 iex> Counter.get(:other_counter) -0 -iex> Counter.bump(7, Counter) +15 +iex> Counter.bump(Counter, 7) 7 iex> Supervisor.terminate_child(App.Supervisor, Counter) iex> Supervisor.count_children(App.Supervisor) # Only one active child From 2325d0c50d94d55ef1d90cac4f23cdcdbc8e2f4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 22 Dec 2023 18:59:03 +0100 Subject: [PATCH 0262/1886] Update RELEASE.md instructions --- RELEASE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASE.md b/RELEASE.md index ad3a3384ef..42ead24eb8 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -12,7 +12,7 @@ 5. Wait until GitHub Actions publish artifacts to the draft release and the CI is green -6. Copy the relevant bits from /CHANGELOG.md to the GitHub release and publish it +6. Copy the relevant bits from /CHANGELOG.md to the GitHub release and publish it (link to the announcement if there is one) 7. Update `_data/elixir-versions.yml` (except for RCs) in `elixir-lang/elixir-lang.github.com` From 57290078e56923b9f02fcc133ca834c87d47aaa5 Mon Sep 17 00:00:00 2001 From: Alex Martsinovich Date: Sat, 23 Dec 2023 01:41:00 +0000 Subject: [PATCH 0263/1886] Fix example in non-assertive map access antipattern (#13201) --- lib/elixir/pages/anti-patterns/code-anti-patterns.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/elixir/pages/anti-patterns/code-anti-patterns.md b/lib/elixir/pages/anti-patterns/code-anti-patterns.md index e93f314057..a7706cf8b4 100644 --- a/lib/elixir/pages/anti-patterns/code-anti-patterns.md +++ b/lib/elixir/pages/anti-patterns/code-anti-patterns.md @@ -379,17 +379,17 @@ An alternative to refactor this anti-pattern is to use pattern matching, definin ```elixir defmodule Graphics do - # 2d - def plot(%{x: x, y: y}) do - # Some other code... - {x, y} - end - # 3d def plot(%{x: x, y: y, z: z}) do # Some other code... {x, y, z} end + + # 2d + def plot(%{x: x, y: y}) do + # Some other code... + {x, y} + end end ``` From d996df8c52758da5e8c44621c08f33f428f0cfc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sun, 24 Dec 2023 14:54:38 +0100 Subject: [PATCH 0264/1886] Additional clarity on long list of parameters anti-patterns --- lib/elixir/pages/anti-patterns/code-anti-patterns.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/elixir/pages/anti-patterns/code-anti-patterns.md b/lib/elixir/pages/anti-patterns/code-anti-patterns.md index a7706cf8b4..340256af1c 100644 --- a/lib/elixir/pages/anti-patterns/code-anti-patterns.md +++ b/lib/elixir/pages/anti-patterns/code-anti-patterns.md @@ -251,7 +251,9 @@ end #### Refactoring -To address this anti-pattern, related arguments can be grouped using maps, structs, or even tuples. This effectively reduces the number of arguments, simplifying the function's interface. In the case of `loan/6`, its arguments were grouped into two different maps, thereby reducing its arity to `loan/2`: +To address this anti-pattern, related arguments can be grouped using key-value data structures, such as maps, structs, or even keyword lists in the case of optional arguments. This effectively reduces the number of arguments and the key-value data structures adds clarity to the caller. + +For this particular example, the arguments to `loan/6` can be grouped into two different maps, thereby reducing its arity to `loan/2`: ```elixir defmodule Library do From 77d1c74d4df55b1a29c5907dc4dd27b66c97701e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1s=20Gr=C3=BCner?= <47506558+MegaRedHand@users.noreply.github.com> Date: Sun, 24 Dec 2023 20:40:13 -0300 Subject: [PATCH 0265/1886] Fix typo in docs: `Keywoird` -> `Keyword` (#13205) --- lib/elixir/pages/anti-patterns/process-anti-patterns.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/pages/anti-patterns/process-anti-patterns.md b/lib/elixir/pages/anti-patterns/process-anti-patterns.md index 6c513b8167..8025437806 100644 --- a/lib/elixir/pages/anti-patterns/process-anti-patterns.md +++ b/lib/elixir/pages/anti-patterns/process-anti-patterns.md @@ -263,7 +263,7 @@ defmodule Counter do @doc "Starts a counter process." def start_link(opts \\ []) do initial_valye = Keyword.get(opts, :initial_value, 0) - name = Keywoird.get(opts, :name, __MODULE__) + name = Keyword.get(opts, :name, __MODULE__) GenServer.start(__MODULE__, initial_value, name: name) end From 0886a604d297bace8f231cf585f006938182c214 Mon Sep 17 00:00:00 2001 From: Artem Solomatin Date: Mon, 25 Dec 2023 22:56:00 +0300 Subject: [PATCH 0266/1886] Fix typo in design-anti-patterns doc (#13207) --- lib/elixir/pages/anti-patterns/design-anti-patterns.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/pages/anti-patterns/design-anti-patterns.md b/lib/elixir/pages/anti-patterns/design-anti-patterns.md index 0e3ecc81f7..c942db2049 100644 --- a/lib/elixir/pages/anti-patterns/design-anti-patterns.md +++ b/lib/elixir/pages/anti-patterns/design-anti-patterns.md @@ -418,7 +418,7 @@ def project do end ``` -Additonally, if a Mix task is available, you can also accept these options as command line arguments (see `OptionParser`): +Additionally, if a Mix task is available, you can also accept these options as command line arguments (see `OptionParser`): ```bash mix linter --output-file /path/to/output.json --verbosity 3 From 509b8ffbef80f8f91c2234233d556e69a85cd34c Mon Sep 17 00:00:00 2001 From: Travis Vander Hoop Date: Tue, 26 Dec 2023 02:55:05 -0600 Subject: [PATCH 0267/1886] Fix typos and tweak language in anti-pattern docs (#13208) --- lib/elixir/pages/anti-patterns/code-anti-patterns.md | 2 +- lib/elixir/pages/anti-patterns/design-anti-patterns.md | 6 +++--- lib/elixir/pages/anti-patterns/macro-anti-patterns.md | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/elixir/pages/anti-patterns/code-anti-patterns.md b/lib/elixir/pages/anti-patterns/code-anti-patterns.md index 340256af1c..d97a2534b9 100644 --- a/lib/elixir/pages/anti-patterns/code-anti-patterns.md +++ b/lib/elixir/pages/anti-patterns/code-anti-patterns.md @@ -317,7 +317,7 @@ When a key is expected to exist in a map, it must be accessed using the `map.key When a key is optional, the `map[:key]` notation must be used instead. This way, if the informed key does not exist, `nil` is returned. This is the dynamic notation, as it also supports dynamic key access, such as `map[some_var]`. -When you use `map[:key]` to access a key that always exists in the map, you are making the code less clear for developers and for the compiler, as they now need to work with the assumption the key may not be there. This mismatch may also make it harder to track certain bugs. If the key is unexpected missing, you will have a `nil` value propagate through the system, instead of raising on map access. +When you use `map[:key]` to access a key that always exists in the map, you are making the code less clear for developers and for the compiler, as they now need to work with the assumption the key may not be there. This mismatch may also make it harder to track certain bugs. If the key is unexpectedly missing, you will have a `nil` value propagate through the system, instead of raising on map access. #### Example diff --git a/lib/elixir/pages/anti-patterns/design-anti-patterns.md b/lib/elixir/pages/anti-patterns/design-anti-patterns.md index c942db2049..39b2b520dc 100644 --- a/lib/elixir/pages/anti-patterns/design-anti-patterns.md +++ b/lib/elixir/pages/anti-patterns/design-anti-patterns.md @@ -245,11 +245,11 @@ end #### Problem -Using multi-clause functions in Elixir, to group functions of the same name, is a powerful Elixir feature. However, some developers may abuse this feature to group *unrelated* functionality, which configures an anti-pattern. +Using multi-clause functions is a powerful Elixir feature. However, some developers may abuse this feature to group *unrelated* functionality, which is an anti-pattern. #### Example -A frequent example of this usage of multi-clause functions is when developers mix unrelated business logic into the same function definition, in a way the behaviour of each clause is completely distinct from the other ones. Such functions often have too broad specifications, making it difficult for other developers to understand and maintain them. +A frequent example of this usage of multi-clause functions occurs when developers mix unrelated business logic into the same function definition, in a way that the behaviour of each clause becomes completely distinct from the others. Such functions often have too broad specifications, making it difficult for other developers to understand and maintain them. Some developers may use documentation mechanisms such as `@doc` annotations to compensate for poor code readability, however the documentation itself may end-up full of conditionals to describe how the function behaves for each different argument combination. This is a good indicator that the clauses are ultimately unrelated. @@ -274,7 +274,7 @@ If updating an animal is completely different from updating a product and requir #### Refactoring -As shown below, a possible solution to this anti-pattern is to break the business rules that are mixed up in a single unrelated multi-clause function in simple functions. Each function can have a specific name and `@doc`, describing its behavior and parameters received. While this refactoring sounds simple, it can impact the function's current users, so be careful! +As shown below, a possible solution to this anti-pattern is to break the business rules that are mixed up in a single unrelated multi-clause function in simple functions. Each function can have a specific name and `@doc`, describing its behavior and parameters received. While this refactoring sounds simple, it can impact the function's callers, so be careful! ```elixir @doc """ diff --git a/lib/elixir/pages/anti-patterns/macro-anti-patterns.md b/lib/elixir/pages/anti-patterns/macro-anti-patterns.md index 1885965318..b8be9d9367 100644 --- a/lib/elixir/pages/anti-patterns/macro-anti-patterns.md +++ b/lib/elixir/pages/anti-patterns/macro-anti-patterns.md @@ -165,7 +165,7 @@ error: imported ModuleA.foo/0 conflicts with local function #### Refactoring -To remove this anti-pattern, we recommend library authors to avoid providing `__using__/1` callbacks whenever it can be replaced by `alias/1` or `import/1` directives. In the following code, we assume `use Library` is no longer available and `ClientApp` was refactored in this way, and with that, the code is clearer and the conflict as previously shown no longer exists: +To remove this anti-pattern, we recommend library authors avoid providing `__using__/1` callbacks whenever it can be replaced by `alias/1` or `import/1` directives. In the following code, we assume `use Library` is no longer available and `ClientApp` was refactored in this way, and with that, the code is clearer and the conflict as previously shown no longer exists: ```elixir defmodule ClientApp do From dc270f0661246728078f25992f85db385a86ff90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 26 Dec 2023 09:55:51 +0100 Subject: [PATCH 0268/1886] Add more examples to app config anti-pattern (#13204) --- .../anti-patterns/design-anti-patterns.md | 52 ++++++++++++++++++- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/lib/elixir/pages/anti-patterns/design-anti-patterns.md b/lib/elixir/pages/anti-patterns/design-anti-patterns.md index 39b2b520dc..e566d6287c 100644 --- a/lib/elixir/pages/anti-patterns/design-anti-patterns.md +++ b/lib/elixir/pages/anti-patterns/design-anti-patterns.md @@ -374,7 +374,7 @@ iex> DashSplitter.split("Lucas-Francisco-da-Matta-Vegi") #### Refactoring -To remove this anti-pattern and make the library more adaptable and flexible, this type of configuration must be performed via parameters in function calls. The code shown below performs the refactoring of the `split/1` function by accepting [keyword lists](`Keyword`) as a new optional parameter. With this new parameter, it is possible to modify the default behavior of the function at the time of its call, allowing multiple different ways of using `split/2` within the same application: +To remove this anti-pattern, this type of configuration should be performed using a parameter passed to the function. The code shown below performs the refactoring of the `split/1` function by accepting [keyword lists](`Keyword`) as a new optional parameter. With this new parameter, it is possible to modify the default behavior of the function at the time of its call, allowing multiple different ways of using `split/2` within the same application: ```elixir defmodule DashSplitter do @@ -392,7 +392,55 @@ iex> DashSplitter.split("Lucas-Francisco-da-Matta-Vegi") #<= default config is u ["Lucas", "Francisco-da-Matta-Vegi"] ``` -#### Additional Remarks +Of course, not all uses of the application environment by libraries are incorrect. One example is using configuration to replace a component (or dependency) of a library by another that must behave the exact same. Consider a library that needs to parse CSV files. The library author may pick one package to use as default parser but allow its users to swap to different implementations via the application environment. At the end of the day, choosing a different CSV parser should not change the outcome, and library authors can even enforce this by [defining behaviours](../references/typespecs.md#behaviours) with the exact semantics they expect. + +#### Additional remarks: Supervision trees + +In practice, libraries may require additional configuration beyond keyword lists. For example, if a library needs to start a supervision tree, how can the user of said library customize its supervision tree? Given the supervision tree itself is global (as it belongs to the library), library authors may be tempted to use the application configuration once more. + +One solution is for the library to provide its own child specification, instead of starting the supervision tree itself. This allows the user to start all necessary processes under its own supervision tree, potentially passing custom configuration options during initialization. + +You can see this pattern in practice in projects like [Nx](https://github.com/elixir-nx/nx) and [DNS Cluster](https://github.com/phoenixframework/dns_cluster). These libraries require that you list processes under your own supervision tree: + +```elixir +children = [ + {DNSCluster, query: "my.subdomain"} +] +``` + +In such cases, if the users of `DNSCluster` need to configure DNSCluster per environment, they can be the ones reading from the application environment, without the library forcing them to: + +```elixir +children = [ + {DNSCluster, query: Application.get_env(:my_app, :dns_cluster_query) || :ignore} +] +``` + +Some libraries, such as [Ecto](https://github.com/elixir-ecto/ecto), allow you to pass your application name as an option (called `:otp_app` or similar) and then automatically read the environment from *your* application. While this addresses the issue with the application environment being global, as they read from each individual application, it comes at the cost of some indirection, compared to the example above where users explicitly read their application environment from their own code, whenever desired. + +#### Additional remarks: Compile-time configuration + +A similar discussion entails compile-time configuration. What if a library author requires some configuration to be provided at compilation time? + +Once again, instead of forcing users of your library to provide compile-time configuration, you may want to allow users of your library to generate the code themselves. That's the approach taken by libraries such as [Ecto](https://github.com/elixir-ecto/ecto): + +```elixir +defmodule MyApp.Repo do + use Ecto.Repo, adapter: Ecto.Adapters.Postgres +end +``` + +Instead of forcing developers to share a single repository, Ecto allows its users to define as many repositories as they want. Given the `:adapter` configuration is required at compile-time, it is a required value on `use Ecto.Repo`. If developers want to configure the adapter per environment, then it is their choice: + +```elixir +defmodule MyApp.Repo do + use Ecto.Repo, adapter: Application.compile_env(:my_app, :repo_adapter) +end +``` + +On the other hand, [code generation comes with its own anti-patterns](macro-anti-patterns.md), and must be considered carefully. That's to say: while using the application environment for libraries is discouraged, especially compile-time configuration, in some cases they may be the best option. For example, consider a library needs to parse CSV or JSON files to generate code based on data files. In such cases, it is best to provide reasonable defaults and make them customizable via the application environment, instead of asking each user of your library to generate the exact same code. + +#### Additional remarks: Mix tasks For Mix tasks and related tools, it may be necessary to provide per-project configuration. For example, imagine you have a `:linter` project, which supports setting the output file and the verbosity level. You may choose to configure it through application environment: From c3da2b8faa6bf9cbbca1fd90c5a2efe14caf03bd Mon Sep 17 00:00:00 2001 From: Hussien Liban Date: Wed, 27 Dec 2023 14:56:30 +0300 Subject: [PATCH 0269/1886] Update design-anti-patterns.md (#13210) Not setting an option returns just the integer --- lib/elixir/pages/anti-patterns/design-anti-patterns.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/pages/anti-patterns/design-anti-patterns.md b/lib/elixir/pages/anti-patterns/design-anti-patterns.md index e566d6287c..fb83e97a41 100644 --- a/lib/elixir/pages/anti-patterns/design-anti-patterns.md +++ b/lib/elixir/pages/anti-patterns/design-anti-patterns.md @@ -30,7 +30,7 @@ end ```elixir iex> AlternativeInteger.parse("13") -{13, ""} +13 iex> AlternativeInteger.parse("13", discard_rest: true) 13 iex> AlternativeInteger.parse("13", discard_rest: false) From b8e4d0e56f995b086cce8b4923072a5137031335 Mon Sep 17 00:00:00 2001 From: Adebisi Adeyeye <68188123+badeyeye1@users.noreply.github.com> Date: Thu, 28 Dec 2023 01:00:13 +0100 Subject: [PATCH 0270/1886] Fix typo in docs: initial_valye -> initial_value (#13211) Co-authored-by: Adebisi Adeyeye --- lib/elixir/pages/anti-patterns/process-anti-patterns.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/pages/anti-patterns/process-anti-patterns.md b/lib/elixir/pages/anti-patterns/process-anti-patterns.md index 8025437806..e3258213bf 100644 --- a/lib/elixir/pages/anti-patterns/process-anti-patterns.md +++ b/lib/elixir/pages/anti-patterns/process-anti-patterns.md @@ -262,7 +262,7 @@ defmodule Counter do @doc "Starts a counter process." def start_link(opts \\ []) do - initial_valye = Keyword.get(opts, :initial_value, 0) + initial_value = Keyword.get(opts, :initial_value, 0) name = Keyword.get(opts, :name, __MODULE__) GenServer.start(__MODULE__, initial_value, name: name) end From ea97378a7541f5ab6377f58a146f591296e38601 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 28 Dec 2023 17:11:12 +0100 Subject: [PATCH 0271/1886] Improve yecc/leex warnings, closes #13213 --- lib/mix/lib/mix/tasks/compile.leex.ex | 2 +- lib/mix/lib/mix/tasks/compile.yecc.ex | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/mix/lib/mix/tasks/compile.leex.ex b/lib/mix/lib/mix/tasks/compile.leex.ex index 145c54d1c2..3594e06098 100644 --- a/lib/mix/lib/mix/tasks/compile.leex.ex +++ b/lib/mix/lib/mix/tasks/compile.leex.ex @@ -67,7 +67,7 @@ defmodule Mix.Tasks.Compile.Leex do # TODO: Remove me in Elixir v2.0 unless :leex in List.wrap(project[:compilers]) do IO.warn( - "in order to compile .xrl files, you must add \"compilers: [:leex] ++ Mix.compilers()\" to the \"def project\" section of your mix.exs" + "in order to compile .xrl files, you must add \"compilers: [:leex] ++ Mix.compilers()\" to the \"def project\" section of #{project[:app]}'s mix.exs" ) end diff --git a/lib/mix/lib/mix/tasks/compile.yecc.ex b/lib/mix/lib/mix/tasks/compile.yecc.ex index ec3420e7db..22ad67f1df 100644 --- a/lib/mix/lib/mix/tasks/compile.yecc.ex +++ b/lib/mix/lib/mix/tasks/compile.yecc.ex @@ -68,7 +68,7 @@ defmodule Mix.Tasks.Compile.Yecc do # TODO: Remove me in Elixir v2.0 unless :yecc in List.wrap(project[:compilers]) do IO.warn( - "in order to compile .yrl files, you must add \"compilers: [:yecc] ++ Mix.compilers()\" to the \"def project\" section of your mix.exs" + "in order to compile .yrl files, you must add \"compilers: [:yecc] ++ Mix.compilers()\" to the \"def project\" section of #{project[:app]}'s mix.exs" ) end From 16ca292659295cfc66cf3f4a6be190fd6031389e Mon Sep 17 00:00:00 2001 From: Will Douglas Date: Fri, 29 Dec 2023 11:55:16 -0300 Subject: [PATCH 0272/1886] Fix typo in docs: for -> force (#13215) Co-authored-by: Will Douglas --- .../pages/getting-started/binaries-strings-and-charlists.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/pages/getting-started/binaries-strings-and-charlists.md b/lib/elixir/pages/getting-started/binaries-strings-and-charlists.md index d3e5940b44..abc59774bf 100644 --- a/lib/elixir/pages/getting-started/binaries-strings-and-charlists.md +++ b/lib/elixir/pages/getting-started/binaries-strings-and-charlists.md @@ -257,7 +257,7 @@ iex> heartbeats_per_minute = [99, 97, 116] ~c"cat" ``` -You can always for charlists to be printed in their list representation by calling the `inspect/2` function: +You can always force charlists to be printed in their list representation by calling the `inspect/2` function: ```elixir iex> inspect(heartbeats_per_minute, charlists: :as_list) From bfd1330d162a15ec3ae02acac2db3bd81a8f669c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 29 Dec 2023 16:29:33 +0100 Subject: [PATCH 0273/1886] Handle invalid :time in metadata, closes #13190 --- lib/logger/lib/logger/formatter.ex | 7 ++++++- lib/logger/test/logger/formatter_test.exs | 19 +++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/lib/logger/lib/logger/formatter.ex b/lib/logger/lib/logger/formatter.ex index 999436a638..ac1c4da15f 100644 --- a/lib/logger/lib/logger/formatter.ex +++ b/lib/logger/lib/logger/formatter.ex @@ -186,7 +186,12 @@ defmodule Logger.Formatter do truncate: truncate } = config - system_time = Map.get_lazy(meta, :time, fn -> :os.system_time(:microsecond) end) + system_time = + case meta do + %{time: time} when is_integer(time) and time >= 0 -> time + _ -> :os.system_time(:microsecond) + end + date_time_ms = system_time_to_date_time_ms(system_time, utc_log?) meta_list = diff --git a/lib/logger/test/logger/formatter_test.exs b/lib/logger/test/logger/formatter_test.exs index 2ea4ca08be..29b5c5a547 100644 --- a/lib/logger/test/logger/formatter_test.exs +++ b/lib/logger/test/logger/formatter_test.exs @@ -74,6 +74,25 @@ defmodule Logger.FormatterTest do |> IO.chardata_to_string() =~ "module=Logger.Formatter function=compile/1 mfa=Logger.Formatter.compile/1" end + + test "handles invalid :time in metadata" do + {_, formatter} = + new( + format: "\n$time $message\n", + colors: [enabled: false] + ) + + assert %{ + level: :warn, + msg: {:string, "message"}, + meta: %{ + mfa: {Logger.Formatter, :compile, 1}, + time: "invalid" + } + } + |> format(formatter) + |> IO.chardata_to_string() =~ ~r"\d\d\d message" + end end describe "compile + format" do From 4a14c031351caa20d8311f52b0db4df6dc1c8d7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sun, 31 Dec 2023 18:04:18 +0100 Subject: [PATCH 0274/1886] Remove unecessary @doc false function --- lib/elixir/lib/kernel/parallel_compiler.ex | 8 ++++---- lib/elixir/lib/module/parallel_checker.ex | 9 ++------- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/lib/elixir/lib/kernel/parallel_compiler.ex b/lib/elixir/lib/kernel/parallel_compiler.ex index d88b23dc73..048c27d8e7 100644 --- a/lib/elixir/lib/kernel/parallel_compiler.ex +++ b/lib/elixir/lib/kernel/parallel_compiler.ex @@ -681,12 +681,12 @@ defmodule Kernel.ParallelCompiler do state = %{state | timer_ref: timer_ref} spawn_workers(queue, spawned, waiting, files, result, warnings, errors, state) - {:diagnostic, %{severity: :warning} = diagnostic} -> - warnings = [Module.ParallelChecker.format_diagnostic_file(diagnostic) | warnings] + {:diagnostic, %{severity: :warning, file: file} = diagnostic} -> + warnings = [%{diagnostic | file: file && Path.absname(file)} | warnings] wait_for_messages(queue, spawned, waiting, files, result, warnings, errors, state) - {:diagnostic, %{severity: :error} = diagnostic} -> - errors = [Module.ParallelChecker.format_diagnostic_file(diagnostic) | errors] + {:diagnostic, %{severity: :error, file: file} = diagnostic} -> + errors = [%{diagnostic | file: file && Path.absname(file)} | errors] wait_for_messages(queue, spawned, waiting, files, result, warnings, errors, state) {:file_ok, child_pid, ref, file, lexical} -> diff --git a/lib/elixir/lib/module/parallel_checker.ex b/lib/elixir/lib/module/parallel_checker.ex index d4e51abb9e..f1d3a36edc 100644 --- a/lib/elixir/lib/module/parallel_checker.ex +++ b/lib/elixir/lib/module/parallel_checker.ex @@ -167,8 +167,8 @@ defmodule Module.ParallelChecker do defp collect_results(count, diagnostics) do receive do - {:diagnostic, diagnostic} -> - diagnostic = format_diagnostic_file(diagnostic) + {:diagnostic, %{file: file} = diagnostic} -> + diagnostic = %{diagnostic | file: file && Path.absname(file)} collect_results(count, [diagnostic | diagnostics]) {__MODULE__, _module, new_diagnostics} -> @@ -287,11 +287,6 @@ defmodule Module.ParallelChecker do end end - @doc false - def format_diagnostic_file(%{file: file} = diagnostic) do - %{diagnostic | file: file && Path.absname(file)} - end - ## Warning helpers defp group_warnings(warnings) do From 03f6ac30afbe046efa2511def303a7e80c4e0dcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=2EYasoob=20Ullah=20Khalid=20=E2=98=BA?= Date: Sun, 31 Dec 2023 20:35:24 -0800 Subject: [PATCH 0275/1886] Fixed a typo in code-anti-patterns.md (#13218) Fixed a small typo --- lib/elixir/pages/anti-patterns/code-anti-patterns.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/pages/anti-patterns/code-anti-patterns.md b/lib/elixir/pages/anti-patterns/code-anti-patterns.md index d97a2534b9..cb316e6c78 100644 --- a/lib/elixir/pages/anti-patterns/code-anti-patterns.md +++ b/lib/elixir/pages/anti-patterns/code-anti-patterns.md @@ -372,7 +372,7 @@ iex> Graphics.plot(point_2d) {2, 3, nil} iex> Graphics.plot(bad_point) ** (KeyError) key :x not found in: %{y: 3, z: 4} # <= explicitly warns that - graphic.ex:4: Graphics.plot/1 # <= the :z key does not exist! + graphic.ex:4: Graphics.plot/1 # <= the :x key does not exist! ``` Overall, the usage of `map.key` and `map[:key]` encode important information about your data structure, allowing developers to be clear about their intent. See both `Map` and `Access` module documentation for more information and examples. From f91038aaeaf121db05df8d2c00295ddda6352d58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 2 Jan 2024 16:42:05 +0100 Subject: [PATCH 0276/1886] Improve anti-pattern titles --- lib/elixir/pages/anti-patterns/code-anti-patterns.md | 2 +- lib/elixir/pages/anti-patterns/macro-anti-patterns.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/elixir/pages/anti-patterns/code-anti-patterns.md b/lib/elixir/pages/anti-patterns/code-anti-patterns.md index cb316e6c78..77ff847620 100644 --- a/lib/elixir/pages/anti-patterns/code-anti-patterns.md +++ b/lib/elixir/pages/anti-patterns/code-anti-patterns.md @@ -2,7 +2,7 @@ This document outlines potential anti-patterns related to your code and particular Elixir idioms and features. -## Comments +## Comments overuse #### Problem diff --git a/lib/elixir/pages/anti-patterns/macro-anti-patterns.md b/lib/elixir/pages/anti-patterns/macro-anti-patterns.md index b8be9d9367..4693e91c44 100644 --- a/lib/elixir/pages/anti-patterns/macro-anti-patterns.md +++ b/lib/elixir/pages/anti-patterns/macro-anti-patterns.md @@ -2,7 +2,7 @@ This document outlines potential anti-patterns related to meta-programming. -## Large code generation by macros +## Large code generation #### Problem From 14895c98f30a708661e2176c90512a63a276a5e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 2 Jan 2024 18:05:17 +0100 Subject: [PATCH 0277/1886] Remove uneeded code comment --- lib/elixir/pages/anti-patterns/code-anti-patterns.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/elixir/pages/anti-patterns/code-anti-patterns.md b/lib/elixir/pages/anti-patterns/code-anti-patterns.md index 77ff847620..9bc9e9e1d2 100644 --- a/lib/elixir/pages/anti-patterns/code-anti-patterns.md +++ b/lib/elixir/pages/anti-patterns/code-anti-patterns.md @@ -371,8 +371,8 @@ end iex> Graphics.plot(point_2d) {2, 3, nil} iex> Graphics.plot(bad_point) -** (KeyError) key :x not found in: %{y: 3, z: 4} # <= explicitly warns that - graphic.ex:4: Graphics.plot/1 # <= the :x key does not exist! +** (KeyError) key :x not found in: %{y: 3, z: 4} + graphic.ex:4: Graphics.plot/1 ``` Overall, the usage of `map.key` and `map[:key]` encode important information about your data structure, allowing developers to be clear about their intent. See both `Map` and `Access` module documentation for more information and examples. From 66d86a223111e07b66fb20b4c66f79c98fdaa5b8 Mon Sep 17 00:00:00 2001 From: Takumi Hara <69781798+takumihara@users.noreply.github.com> Date: Thu, 4 Jan 2024 00:07:00 -0800 Subject: [PATCH 0278/1886] Improve docs for assert_receive/3 (#13222) --- lib/ex_unit/lib/ex_unit/assertions.ex | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/ex_unit/lib/ex_unit/assertions.ex b/lib/ex_unit/lib/ex_unit/assertions.ex index 6ba920372e..1abd2b0899 100644 --- a/lib/ex_unit/lib/ex_unit/assertions.ex +++ b/lib/ex_unit/lib/ex_unit/assertions.ex @@ -418,8 +418,9 @@ defmodule ExUnit.Assertions do Asserts that a message matching `pattern` was or is going to be received within the `timeout` period, specified in milliseconds. - Unlike `assert_received`, it has a default `timeout` - of 100 milliseconds. + Unlike `assert_received`, it has a configurable timeout. + The default timeout duration is determined by the `assert_receive_timeout` option, + which can be set using `ExUnit.configure/1`. This option defaults to 100 milliseconds. The `pattern` argument must be a match pattern. Flunks with `failure_message` if a message matching `pattern` is not received. From fa088a43588c391baf86798ec7b29cc1ea7d978b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 4 Jan 2024 13:01:44 +0100 Subject: [PATCH 0279/1886] Do not hardcode newlines --- lib/elixir/src/elixir_expand.erl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/elixir/src/elixir_expand.erl b/lib/elixir/src/elixir_expand.erl index 16e5a54d61..50a1bb1935 100644 --- a/lib/elixir/src/elixir_expand.erl +++ b/lib/elixir/src/elixir_expand.erl @@ -1206,9 +1206,9 @@ format_error(unhandled_cons_op) -> "misplaced operator |/2\n\n" "The | operator is typically used between brackets as the cons operator:\n\n" " [head | tail]\n\n" - "where head is a sequence of elements separated by commas and the tail\n" + "where head is a sequence of elements separated by commas and the tail " "is the remaining of a list.\n\n" - "It is also used to update maps and structs, via the %{map | key: value} notation,\n" + "It is also used to update maps and structs, via the %{map | key: value} notation, " "and in typespecs, such as @type and @spec, to express the union of two types"; format_error(unhandled_type_op) -> "misplaced operator ::/2\n\n" From 748722dc0db0de45401f3e8b574521f281a6f643 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sat, 6 Jan 2024 11:33:26 +0100 Subject: [PATCH 0280/1886] Trim down current typed module implementation (#13226) --- lib/elixir/lib/module.ex | 2 +- .../lib/module/{types => }/behaviour.ex | 2 +- lib/elixir/lib/module/parallel_checker.ex | 2 +- lib/elixir/lib/module/types.ex | 447 +------- lib/elixir/lib/module/types/descr.ex | 14 + lib/elixir/lib/module/types/expr.ex | 361 +++---- lib/elixir/lib/module/types/helpers.ex | 35 +- lib/elixir/lib/module/types/of.ex | 208 ++-- lib/elixir/lib/module/types/pattern.ex | 700 +----------- lib/elixir/lib/module/types/unify.ex | 992 ------------------ lib/elixir/src/elixir_compiler.erl | 4 +- lib/elixir/test/elixir/code_test.exs | 77 -- .../test/elixir/fixtures/checker_warning.exs | 3 - .../test/elixir/module/types/expr_test.exs | 325 +----- .../elixir/module/types/integration_test.exs | 14 - .../test/elixir/module/types/map_test.exs | 377 ------- .../test/elixir/module/types/pattern_test.exs | 542 ---------- .../test/elixir/module/types/type_helper.exs | 64 +- .../test/elixir/module/types/types_test.exs | 674 +----------- .../test/elixir/module/types/unify_test.exs | 770 -------------- 20 files changed, 342 insertions(+), 5271 deletions(-) rename lib/elixir/lib/module/{types => }/behaviour.ex (99%) create mode 100644 lib/elixir/lib/module/types/descr.ex delete mode 100644 lib/elixir/lib/module/types/unify.ex delete mode 100644 lib/elixir/test/elixir/fixtures/checker_warning.exs delete mode 100644 lib/elixir/test/elixir/module/types/map_test.exs delete mode 100644 lib/elixir/test/elixir/module/types/pattern_test.exs delete mode 100644 lib/elixir/test/elixir/module/types/unify_test.exs diff --git a/lib/elixir/lib/module.ex b/lib/elixir/lib/module.ex index 6b0aca3a95..c5daf0cdaa 100644 --- a/lib/elixir/lib/module.ex +++ b/lib/elixir/lib/module.ex @@ -1443,7 +1443,7 @@ defmodule Module do "to defoverridable/1 because #{error_explanation}" end - behaviour_callbacks = Module.Types.Behaviour.callbacks(behaviour) + behaviour_callbacks = Module.Behaviour.callbacks(behaviour) tuples = for definition <- definitions_in(module), diff --git a/lib/elixir/lib/module/types/behaviour.ex b/lib/elixir/lib/module/behaviour.ex similarity index 99% rename from lib/elixir/lib/module/types/behaviour.ex rename to lib/elixir/lib/module/behaviour.ex index e57c650009..2cc857d132 100644 --- a/lib/elixir/lib/module/types/behaviour.ex +++ b/lib/elixir/lib/module/behaviour.ex @@ -1,4 +1,4 @@ -defmodule Module.Types.Behaviour do +defmodule Module.Behaviour do # Checking functionality for @behaviours and @impl @moduledoc false diff --git a/lib/elixir/lib/module/parallel_checker.ex b/lib/elixir/lib/module/parallel_checker.ex index f1d3a36edc..e5de710675 100644 --- a/lib/elixir/lib/module/parallel_checker.ex +++ b/lib/elixir/lib/module/parallel_checker.ex @@ -249,7 +249,7 @@ defmodule Module.ParallelChecker do |> merge_compiler_no_warn_undefined() behaviour_warnings = - Module.Types.Behaviour.check_behaviours_and_impls( + Module.Behaviour.check_behaviours_and_impls( module, file, line, diff --git a/lib/elixir/lib/module/types.ex b/lib/elixir/lib/module/types.ex index c1c7efdd5e..efbc065619 100644 --- a/lib/elixir/lib/module/types.ex +++ b/lib/elixir/lib/module/types.ex @@ -5,21 +5,18 @@ defmodule Module.Types do defexception [:message] end - import Module.Types.Helpers - alias Module.Types.{Expr, Pattern, Unify} + alias Module.Types.{Expr, Pattern} @doc false def warnings(module, file, defs, no_warn_undefined, cache) do - stack = stack() + context = context() Enum.flat_map(defs, fn {{fun, arity} = function, kind, meta, clauses} -> - context = context(with_file_meta(meta, file), module, function, no_warn_undefined, cache) + stack = stack(with_file_meta(meta, file), module, function, no_warn_undefined, cache) Enum.flat_map(clauses, fn {_meta, args, guards, body} -> - def_expr = {kind, meta, [guards_to_expr(guards, {fun, [], args})]} - try do - warnings_from_clause(args, guards, body, def_expr, stack, context) + warnings_from_clause(args, guards, body, stack, context) rescue e -> def_expr = {kind, meta, [guards_to_expr(guards, {fun, [], args}), [do: body]]} @@ -58,20 +55,17 @@ defmodule Module.Types do guards_to_expr(guards, {:when, [], [left, guard]}) end - defp warnings_from_clause(args, guards, body, def_expr, stack, context) do - head_stack = Unify.push_expr_stack(def_expr, stack) - - with {:ok, _types, context} <- Pattern.of_head(args, guards, head_stack, context), - {:ok, _type, context} <- Expr.of_expr(body, :dynamic, stack, context) do + defp warnings_from_clause(args, guards, body, stack, context) do + with {:ok, _types, context} <- Pattern.of_head(args, guards, stack, context), + {:ok, _type, context} <- Expr.of_expr(body, stack, context) do context.warnings else - {:error, {type, error, context}} -> - [error_to_warning(type, error, context) | context.warnings] + {:error, context} -> context.warnings end end @doc false - def context(file, module, function, no_warn_undefined, cache) do + def stack(file, module, function, no_warn_undefined, cache) do %{ # File of module file: file, @@ -82,305 +76,18 @@ defmodule Module.Types do # List of calls to not warn on as undefined no_warn_undefined: no_warn_undefined, # A list of cached modules received from the parallel compiler - cache: cache, - # Expression variable to type variable - vars: %{}, - # Type variable to expression variable - types_to_vars: %{}, - # Type variable to type - types: %{}, - # Trace of all variables that have been refined to a type, - # including the type they were refined to, why, and where - traces: %{}, - # Counter to give type variables unique names - counter: 0, - # Track if a variable was inferred from a type guard function such is_tuple/1 - # or a guard function that fails such as elem/2, possible values are: - # `:guarded` when `is_tuple(x)` - # `:guarded` when `is_tuple and elem(x, 0)` - # `:fail` when `elem(x, 0)` - guard_sources: %{}, - # A list with all warnings from the running the code - warnings: [] + cache: cache } end @doc false - def stack() do + def context() do %{ - # Stack of variables we have refined during unification, - # used for creating relevant traces - unify_stack: [], - # Last expression we have recursed through during inference, - # used for tracing - last_expr: nil, - # When false do not add a trace when a type variable is refined, - # useful when merging contexts where the variables already have traces - trace: true, - # There are two factors that control how we track guards. - # - # * consider_type_guards?: if type guards should be considered. - # This applies only at the root and root-based "and" and "or" nodes. - # - # * keep_guarded? - if a guarded clause should remain as guarded - # even on failure. Used on the right side of and. - # - type_guards: {_consider_type_guards? = true, _keep_guarded? = false}, - # Context used to determine if unification is bi-directional, :expr - # is directional, :pattern is bi-directional - context: nil + # A list of all warnings found so far + warnings: [] } end - ## ERROR TO WARNING - - # Collect relevant information from context and traces to report error - def error_to_warning(:unable_apply, {mfa, args, expected, signature, stack}, context) do - {fun, arity} = context.function - location = {context.file, get_position(stack), {context.module, fun, arity}} - - traces = type_traces(stack, context) - {[signature | args], traces} = lift_all_types([signature | args], traces, context) - error = {:unable_apply, mfa, args, expected, signature, {location, stack.last_expr, traces}} - {Module.Types, error, location} - end - - def error_to_warning(:unable_unify, {left, right, stack}, context) do - {fun, arity} = context.function - location = {context.file, get_position(stack), {context.module, fun, arity}} - - traces = type_traces(stack, context) - {[left, right], traces} = lift_all_types([left, right], traces, context) - error = {:unable_unify, left, right, {location, stack.last_expr, traces}} - {Module.Types, error, location} - end - - defp get_position(stack) do - get_meta(stack.last_expr) - end - - # Collect relevant traces from context.traces using stack.unify_stack - defp type_traces(stack, context) do - # TODO: Do we need the unify_stack or is enough to only get the last variable - # in the stack since we get related variables anyway? - stack = - stack.unify_stack - |> Enum.flat_map(&[&1 | related_variables(&1, context.types)]) - |> Enum.uniq() - - Enum.flat_map(stack, fn var_index -> - with %{^var_index => traces} <- context.traces, - %{^var_index => expr_var} <- context.types_to_vars do - Enum.map(traces, &tag_trace(expr_var, &1, context)) - else - _other -> [] - end - end) - end - - defp related_variables(var, types) do - Enum.flat_map(types, fn - {related_var, {:var, ^var}} -> - [related_var | related_variables(related_var, types)] - - _ -> - [] - end) - end - - # Tag if trace is for a concrete type or type variable - defp tag_trace(var, {type, expr, location}, context) do - with {:var, var_index} <- type, - %{^var_index => expr_var} <- context.types_to_vars do - {:var, var, expr_var, expr, location} - else - _ -> {:type, var, type, expr, location} - end - end - - defp lift_all_types(types, traces, context) do - trace_types = for({:type, _, type, _, _} <- traces, do: type) - {types, lift_context} = Unify.lift_types(types, context) - {trace_types, _lift_context} = Unify.lift_types(trace_types, lift_context) - - {traces, []} = - Enum.map_reduce(traces, trace_types, fn - {:type, var, _, expr, location}, [type | acc] -> {{:type, var, type, expr, location}, acc} - other, acc -> {other, acc} - end) - - {types, traces} - end - - ## FORMAT WARNINGS - - def format_warning({:unable_apply, mfa, args, expected, signature, {location, expr, traces}}) do - {original_module, original_function, arity} = mfa - {_, _, args} = mfa_or_fa = erl_to_ex(original_module, original_function, args, []) - {module, function, ^arity} = call_to_mfa(mfa_or_fa) - format_mfa = Exception.format_mfa(module, function, arity) - {traces, [] = _hints} = format_traces(traces, [], false) - - clauses = - Enum.map(signature, fn {ins, out} -> - {_, _, ins} = erl_to_ex(original_module, original_function, ins, []) - - {:fun, [{ins, out}]} - |> Unify.format_type(false) - |> IO.iodata_to_binary() - |> binary_slice(1..-2//1) - end) - - [ - "expected #{format_mfa} to have signature:\n\n ", - Enum.map_join(args, ", ", &Unify.format_type(&1, false)), - " -> #{Unify.format_type(expected, false)}", - "\n\nbut it has signature:\n\n ", - indent(Enum.join(clauses, "\n")), - "\n\n", - format_expr(expr, location), - traces, - "Conflict found at" - ] - end - - def format_warning({:unable_unify, left, right, {location, expr, traces}}) do - if map_type?(left) and map_type?(right) and match?({:ok, _, _}, missing_field(left, right)) do - {:ok, atom, known_atoms} = missing_field(left, right) - - # Drop the last trace which is the expression map.foo - traces = Enum.drop(traces, 1) - {traces, hints} = format_traces(traces, [left, right], true) - - [ - "undefined field \"#{atom}\" ", - format_expr(expr, location), - "expected one of the following fields: ", - Enum.join(Enum.sort(known_atoms), ", "), - "\n\n", - traces, - format_message_hints(hints), - "Conflict found at" - ] - else - simplify_left? = simplify_type?(left, right) - simplify_right? = simplify_type?(right, left) - - {traces, hints} = format_traces(traces, [left, right], simplify_left? or simplify_right?) - - [ - "incompatible types:\n\n ", - Unify.format_type(left, simplify_left?), - " !~ ", - Unify.format_type(right, simplify_right?), - "\n\n", - format_expr(expr, location), - traces, - format_message_hints(hints), - "Conflict found at" - ] - end - end - - defp missing_field( - {:map, [{:required, {:atom, atom} = type, _}, {:optional, :dynamic, :dynamic}]}, - {:map, fields} - ) do - matched_missing_field(fields, type, atom) - end - - defp missing_field( - {:map, fields}, - {:map, [{:required, {:atom, atom} = type, _}, {:optional, :dynamic, :dynamic}]} - ) do - matched_missing_field(fields, type, atom) - end - - defp missing_field(_, _), do: :error - - defp matched_missing_field(fields, type, atom) do - if List.keymember?(fields, type, 1) do - :error - else - known_atoms = for {_, {:atom, atom}, _} <- fields, do: atom - {:ok, atom, known_atoms} - end - end - - defp format_traces([], _types, _simplify?) do - {[], []} - end - - defp format_traces(traces, types, simplify?) do - traces - |> Enum.uniq() - |> Enum.reverse() - |> Enum.map_reduce([], fn - {:type, var, type, expr, location}, hints -> - {hint, hints} = format_type_hint(type, types, expr, hints) - - trace = [ - "where \"", - Macro.to_string(var), - "\" was given the type ", - Unify.format_type(type, simplify?), - hint, - " in:\n\n # ", - format_location(location), - " ", - indent(expr_to_string(expr)), - "\n\n" - ] - - {trace, hints} - - {:var, var1, var2, expr, location}, hints -> - trace = [ - "where \"", - Macro.to_string(var1), - "\" was given the same type as \"", - Macro.to_string(var2), - "\" in:\n\n # ", - format_location(location), - " ", - indent(expr_to_string(expr)), - "\n\n" - ] - - {trace, hints} - end) - end - - defp format_location({file, position, _mfa}) do - format_location({file, position[:line]}) - end - - defp format_location({file, line}) do - file = Path.relative_to_cwd(file) - line = if line, do: [Integer.to_string(line)], else: [] - [file, ?:, line, ?\n] - end - - defp simplify_type?(type, other) do - map_like_type?(type) and not map_like_type?(other) - end - - ## EXPRESSION FORMATTING - - defp format_expr(nil, _location) do - [] - end - - defp format_expr(expr, location) do - [ - "in expression:\n\n # ", - format_location(location), - " ", - indent(expr_to_string(expr)), - "\n\n" - ] - end - @doc false def expr_to_string(expr) do expr @@ -401,132 +108,4 @@ defmodule Module.Types do {mod, fun, args} -> {{:., [], [mod, fun]}, meta, args} end end - - ## Hints - - defp format_message_hints(hints) do - hints - |> Enum.uniq() - |> Enum.reverse() - |> Enum.map(&[format_message_hint(&1), "\n"]) - end - - defp format_message_hint(:inferred_dot) do - """ - #{hint()} "var.field" (without parentheses) implies "var" is a map() while \ - "var.fun()" (with parentheses) implies "var" is an atom() - """ - end - - defp format_message_hint(:inferred_bitstring_spec) do - """ - #{hint()} all expressions given to binaries are assumed to be of type \ - integer() unless said otherwise. For example, <> assumes "expr" \ - is an integer. Pass a modifier, such as <> or <>, \ - to change the default behaviour. - """ - end - - defp format_message_hint({:sized_and_unsize_tuples, {size, var}}) do - """ - #{hint()} use pattern matching or "is_tuple(#{Macro.to_string(var)}) and \ - tuple_size(#{Macro.to_string(var)}) == #{size}" to guard a sized tuple. - """ - end - - defp hint, do: :elixir_errors.prefix(:hint) - - defp format_type_hint(type, types, expr, hints) do - case format_type_hint(type, types, expr) do - {message, hint} -> {message, [hint | hints]} - :error -> {[], hints} - end - end - - defp format_type_hint(type, types, expr) do - cond do - dynamic_map_dot?(type, expr) -> - {" (due to calling var.field)", :inferred_dot} - - dynamic_remote_call?(type, expr) -> - {" (due to calling var.fun())", :inferred_dot} - - inferred_bitstring_spec?(type, expr) -> - {[], :inferred_bitstring_spec} - - message = sized_and_unsize_tuples(expr, types) -> - {[], {:sized_and_unsize_tuples, message}} - - true -> - :error - end - end - - defp dynamic_map_dot?(type, expr) do - with true <- map_type?(type), - {{:., _meta1, [_map, _field]}, meta2, []} <- expr, - true <- Keyword.get(meta2, :no_parens, false) do - true - else - _ -> false - end - end - - defp dynamic_remote_call?(type, expr) do - with true <- atom_type?(type), - {{:., _meta1, [_module, _field]}, meta2, []} <- expr, - false <- Keyword.get(meta2, :no_parens, false) do - true - else - _ -> false - end - end - - defp inferred_bitstring_spec?(type, expr) do - with true <- integer_type?(type), - {:<<>>, _, args} <- expr, - true <- Enum.any?(args, &match?({:"::", [{:inferred_bitstring_spec, true} | _], _}, &1)) do - true - else - _ -> false - end - end - - defp sized_and_unsize_tuples({{:., _, [:erlang, :is_tuple]}, _, [var]}, types) do - case Enum.find(types, &match?({:tuple, _, _}, &1)) do - {:tuple, size, _} -> - {size, var} - - nil -> - nil - end - end - - defp sized_and_unsize_tuples(_expr, _types) do - nil - end - - ## Formatting helpers - - defp indent(string) do - String.replace(string, "\n", "\n ") - end - - defp map_type?({:map, _}), do: true - defp map_type?(_other), do: false - - defp map_like_type?({:map, _}), do: true - defp map_like_type?({:union, union}), do: Enum.any?(union, &map_like_type?/1) - defp map_like_type?(_other), do: false - - defp atom_type?(:atom), do: true - defp atom_type?({:atom, _}), do: false - defp atom_type?({:union, union}), do: Enum.all?(union, &atom_type?/1) - defp atom_type?(_other), do: false - - defp integer_type?(:integer), do: true - defp integer_type?(_other), do: false - - defp call_to_mfa({{:., _, [mod, fun]}, _, args}), do: {mod, fun, length(args)} - defp call_to_mfa({fun, _, args}) when is_atom(fun), do: {Kernel, fun, length(args)} end diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex new file mode 100644 index 0000000000..15e02baee3 --- /dev/null +++ b/lib/elixir/lib/module/types/descr.ex @@ -0,0 +1,14 @@ +defmodule Module.Types.Descr do + @moduledoc false + def term(), do: :term + def atom(_atom), do: :atom + def dynamic(), do: :dynamic + def integer(), do: :integer + def float(), do: :float + def binary(), do: :binary + def pid(), do: :pid + def tuple(), do: :tuple + def empty_list(), do: :list + def non_empty_list(), do: :list + def map(), do: :map +end diff --git a/lib/elixir/lib/module/types/expr.ex b/lib/elixir/lib/module/types/expr.ex index 9931e2906b..ab06ab7bce 100644 --- a/lib/elixir/lib/module/types/expr.ex +++ b/lib/elixir/lib/module/types/expr.ex @@ -2,68 +2,55 @@ defmodule Module.Types.Expr do @moduledoc false alias Module.Types.{Of, Pattern} - import Module.Types.{Helpers, Unify} + import Module.Types.{Helpers, Descr} - def of_expr(expr, expected, %{context: stack_context} = stack, context) - when stack_context != :expr do - of_expr(expr, expected, %{stack | context: :expr}, context) + def of_expr(ast, stack, context) do + of_expr(ast, term(), stack, context) end # :atom def of_expr(atom, _expected, _stack, context) when is_atom(atom) do - {:ok, {:atom, atom}, context} + {:ok, atom(atom), context} end # 12 def of_expr(literal, _expected, _stack, context) when is_integer(literal) do - {:ok, :integer, context} + {:ok, integer(), context} end # 1.2 def of_expr(literal, _expected, _stack, context) when is_float(literal) do - {:ok, :float, context} + {:ok, float(), context} end # "..." def of_expr(literal, _expected, _stack, context) when is_binary(literal) do - {:ok, :binary, context} + {:ok, binary(), context} end # #PID<...> def of_expr(literal, _expected, _stack, context) when is_pid(literal) do - {:ok, :dynamic, context} + {:ok, pid(), context} end # <<...>>> def of_expr({:<<>>, _meta, args}, _expected, stack, context) do - case Of.binary(args, stack, context, &of_expr/4) do - {:ok, context} -> {:ok, :binary, context} + case Of.binary(args, :expr, stack, context, &of_expr/3) do + {:ok, context} -> {:ok, binary(), context} {:error, reason} -> {:error, reason} end end # left | [] - def of_expr({:|, _meta, [left_expr, []]} = expr, _expected, stack, context) do - stack = push_expr_stack(expr, stack) - of_expr(left_expr, :dynamic, stack, context) + def of_expr({:|, _meta, [left_expr, []]}, expected, stack, context) do + of_expr(left_expr, expected, stack, context) end # left | right - def of_expr({:|, _meta, [left_expr, right_expr]} = expr, _expected, stack, context) do - stack = push_expr_stack(expr, stack) - - case of_expr(left_expr, :dynamic, stack, context) do - {:ok, left, context} -> - case of_expr(right_expr, :dynamic, stack, context) do - {:ok, {:list, right}, context} -> - {:ok, to_union([left, right], context), context} - - {:ok, right, context} -> - {:ok, to_union([left, right], context), context} - - {:error, reason} -> - {:error, reason} - end + def of_expr({:|, _meta, [left_expr, right_expr]}, _expected, stack, context) do + case of_expr(left_expr, stack, context) do + {:ok, _left, context} -> + of_expr(right_expr, stack, context) {:error, reason} -> {:error, reason} @@ -72,15 +59,13 @@ defmodule Module.Types.Expr do # [] def of_expr([], _expected, _stack, context) do - {:ok, {:list, :dynamic}, context} + {:ok, empty_list(), context} end # [expr, ...] def of_expr(exprs, _expected, stack, context) when is_list(exprs) do - stack = push_expr_stack(exprs, stack) - - case map_reduce_ok(exprs, context, &of_expr(&1, :dynamic, stack, &2)) do - {:ok, types, context} -> {:ok, {:list, to_union(types, context)}, context} + case map_reduce_ok(exprs, context, &of_expr(&1, stack, &2)) do + {:ok, _types, context} -> {:ok, non_empty_list(), context} {:error, reason} -> {:error, reason} end end @@ -88,29 +73,18 @@ defmodule Module.Types.Expr do # __CALLER__ def of_expr({:__CALLER__, _meta, var_context}, _expected, _stack, context) when is_atom(var_context) do - struct_pair = {:required, {:atom, :__struct__}, {:atom, Macro.Env}} - - pairs = - Enum.map(Map.from_struct(Macro.Env.__struct__()), fn {key, _value} -> - {:required, {:atom, key}, :dynamic} - end) - - {:ok, {:map, [struct_pair | pairs]}, context} + {:ok, dynamic(), context} end # __STACKTRACE__ def of_expr({:__STACKTRACE__, _meta, var_context}, _expected, _stack, context) when is_atom(var_context) do - file = {:tuple, 2, [{:atom, :file}, {:list, :integer}]} - line = {:tuple, 2, [{:atom, :line}, :integer]} - file_line = {:list, {:union, [file, line]}} - type = {:list, {:tuple, 4, [:atom, :atom, :integer, file_line]}} - {:ok, type, context} + {:ok, dynamic(), context} end # var def of_expr(var, _expected, _stack, context) when is_var(var) do - {:ok, get_var!(var, context), context} + {:ok, dynamic(), context} end # {left, right} @@ -119,146 +93,102 @@ defmodule Module.Types.Expr do end # {...} - def of_expr({:{}, _meta, exprs} = expr, _expected, stack, context) do - stack = push_expr_stack(expr, stack) - - case map_reduce_ok(exprs, context, &of_expr(&1, :dynamic, stack, &2)) do - {:ok, types, context} -> {:ok, {:tuple, length(types), types}, context} + def of_expr({:{}, _meta, exprs}, _expected, stack, context) do + case map_reduce_ok(exprs, context, &of_expr(&1, stack, &2)) do + {:ok, _types, context} -> {:ok, tuple(), context} {:error, reason} -> {:error, reason} end end # left = right - def of_expr({:=, _meta, [left_expr, right_expr]} = expr, _expected, stack, context) do - # TODO: We might want to bring the expected type forward in case the type of this - # pattern is not useful. For example: 1 = _ = expr - - stack = push_expr_stack(expr, stack) - + def of_expr({:=, _meta, [left_expr, right_expr]}, _expected, stack, context) do with {:ok, left_type, context} <- Pattern.of_pattern(left_expr, stack, context), - {:ok, right_type, context} <- of_expr(right_expr, left_type, stack, context), - do: unify(right_type, left_type, stack, context) + {:ok, right_type, context} <- of_expr(right_expr, left_type, stack, context) do + {:ok, right_type, context} + end end # %{map | ...} - def of_expr({:%{}, _, [{:|, _, [map, args]}]} = expr, _expected, stack, context) do - stack = push_expr_stack(expr, stack) - map_type = {:map, [{:optional, :dynamic, :dynamic}]} - - with {:ok, map_type, context} <- of_expr(map, map_type, stack, context), - {:ok, {:map, arg_pairs}, context} <- Of.closed_map(args, stack, context, &of_expr/4), - dynamic_value_pairs = - Enum.map(arg_pairs, fn {:required, key, _value} -> {:required, key, :dynamic} end), - args_type = {:map, dynamic_value_pairs ++ [{:optional, :dynamic, :dynamic}]}, - {:ok, type, context} <- unify(map_type, args_type, stack, context) do - # Retrieve map type and overwrite with the new value types from the map update - case resolve_var(type, context) do - {:map, pairs} -> - updated_pairs = - Enum.reduce(arg_pairs, pairs, fn {:required, key, value}, pairs -> - List.keyreplace(pairs, key, 1, {:required, key, value}) - end) - - {:ok, {:map, updated_pairs}, context} - - _ -> - {:ok, :dynamic, context} - end + def of_expr({:%{}, _, [{:|, _, [map, args]}]}, _expected, stack, context) do + with {:ok, _, context} <- of_expr(map, stack, context), + {:ok, _, context} <- Of.closed_map(args, stack, context, &of_expr/3) do + {:ok, map(), context} end end # %Struct{map | ...} def of_expr( - {:%, meta, [module, {:%{}, _, [{:|, _, [_, _]}]} = update]} = expr, + {:%, meta, [module, {:%{}, _, [{:|, _, [_, _]}]} = update]}, _expected, stack, context ) do - stack = push_expr_stack(expr, stack) - map_type = {:map, [{:optional, :dynamic, :dynamic}]} - - with {:ok, struct, context} <- Of.struct(module, meta, context), - {:ok, update, context} <- of_expr(update, map_type, stack, context) do - unify(update, struct, stack, context) + with {:ok, _, context} <- Of.struct(module, meta, stack, context), + {:ok, _, context} <- of_expr(update, stack, context) do + {:ok, map(), context} end end # %{...} - def of_expr({:%{}, _meta, args} = expr, _expected, stack, context) do - stack = push_expr_stack(expr, stack) - Of.closed_map(args, stack, context, &of_expr/4) + def of_expr({:%{}, _meta, args}, _expected, stack, context) do + Of.closed_map(args, stack, context, &of_expr/3) end # %Struct{...} - def of_expr({:%, meta1, [module, {:%{}, _meta2, args}]} = expr, _expected, stack, context) do - stack = push_expr_stack(expr, stack) - - with {:ok, struct, context} <- Of.struct(module, meta1, context), - {:ok, map, context} <- Of.open_map(args, stack, context, &of_expr/4) do - unify(map, struct, stack, context) + def of_expr({:%, meta1, [module, {:%{}, _meta2, args}]}, _expected, stack, context) do + with {:ok, _, context} <- Of.struct(module, meta1, stack, context), + {:ok, _, context} <- Of.open_map(args, stack, context, &of_expr/3) do + {:ok, map(), context} end end # () def of_expr({:__block__, _meta, []}, _expected, _stack, context) do - {:ok, {:atom, nil}, context} + {:ok, atom(nil), context} end # (expr; expr) def of_expr({:__block__, _meta, exprs}, expected, stack, context) do - expected_types = List.duplicate(:dynamic, length(exprs) - 1) ++ [expected] + {pre, [post]} = Enum.split(exprs, -1) result = - map_reduce_ok(Enum.zip(exprs, expected_types), context, fn {expr, expected}, context -> - of_expr(expr, expected, stack, context) + map_reduce_ok(pre, context, fn expr, context -> + of_expr(expr, stack, context) end) case result do - {:ok, expr_types, context} -> {:ok, Enum.at(expr_types, -1), context} + {:ok, _, context} -> of_expr(post, expected, stack, context) {:error, reason} -> {:error, reason} end end # cond do pat -> expr end - def of_expr({:cond, _meta, [[{:do, clauses}]]} = expr, _expected, stack, context) do - stack = push_expr_stack(expr, stack) - + def of_expr({:cond, _meta, [[{:do, clauses}]]}, _expected, stack, context) do {result, context} = - reduce_ok(clauses, context, fn {:->, meta, [head, body]}, context = acc -> - case of_expr(head, :dynamic, stack, context) do - {:ok, _, context} -> - with {:ok, _expr_type, context} <- of_expr(body, :dynamic, stack, context) do - {:ok, keep_warnings(acc, context)} - end - - error -> - # Skip the clause if it the head has an error - if meta[:generated], do: {:ok, acc}, else: error - end + reduce_ok(clauses, context, fn {:->, _meta, [head, body]}, context -> + with {:ok, _, context} <- of_expr(head, stack, context), + {:ok, _, context} <- of_expr(body, stack, context), + do: {:ok, context} end) case result do - :ok -> {:ok, :dynamic, context} + :ok -> {:ok, dynamic(), context} :error -> {:error, context} end end # case expr do pat -> expr end - def of_expr({:case, _meta, [case_expr, [{:do, clauses}]]} = expr, _expected, stack, context) do - stack = push_expr_stack(expr, stack) - - with {:ok, _expr_type, context} <- of_expr(case_expr, :dynamic, stack, context), + def of_expr({:case, _meta, [case_expr, [{:do, clauses}]]}, _expected, stack, context) do + with {:ok, _expr_type, context} <- of_expr(case_expr, stack, context), {:ok, context} <- of_clauses(clauses, stack, context), - do: {:ok, :dynamic, context} + do: {:ok, dynamic(), context} end # fn pat -> expr end - def of_expr({:fn, _meta, clauses} = expr, _expected, stack, context) do - stack = push_expr_stack(expr, stack) - + def of_expr({:fn, _meta, clauses}, _expected, stack, context) do case of_clauses(clauses, stack, context) do - {:ok, context} -> {:ok, :dynamic, context} + {:ok, context} -> {:ok, dynamic(), context} {:error, reason} -> {:error, reason} end end @@ -267,47 +197,35 @@ defmodule Module.Types.Expr do @try_clause_blocks [:catch, :else, :after] # try do expr end - def of_expr({:try, _meta, [blocks]} = expr, _expected, stack, context) do - stack = push_expr_stack(expr, stack) - + def of_expr({:try, _meta, [blocks]}, _expected, stack, context) do {result, context} = reduce_ok(blocks, context, fn {:rescue, clauses}, context -> reduce_ok(clauses, context, fn - {:->, _, [[{:in, _, [var, _exceptions]}], body]}, context = acc -> - {_type, context} = new_pattern_var(var, context) - - with {:ok, context} <- of_expr_context(body, :dynamic, stack, context) do - {:ok, keep_warnings(acc, context)} - end + {:->, _, [[{:in, _, [_var, _exceptions]}], body]}, context -> + # TODO: make sure var is defined in context + of_expr_context(body, stack, context) - {:->, _, [[var], body]}, context = acc -> - {_type, context} = new_pattern_var(var, context) - - with {:ok, context} <- of_expr_context(body, :dynamic, stack, context) do - {:ok, keep_warnings(acc, context)} - end + {:->, _, [[_var], body]}, context -> + # TODO: make sure var is defined in context + of_expr_context(body, stack, context) end) - {block, body}, context = acc when block in @try_blocks -> - with {:ok, context} <- of_expr_context(body, :dynamic, stack, context) do - {:ok, keep_warnings(acc, context)} - end + {block, body}, context when block in @try_blocks -> + of_expr_context(body, stack, context) {block, clauses}, context when block in @try_clause_blocks -> of_clauses(clauses, stack, context) end) case result do - :ok -> {:ok, :dynamic, context} + :ok -> {:ok, dynamic(), context} :error -> {:error, context} end end # receive do pat -> expr end - def of_expr({:receive, _meta, [blocks]} = expr, _expected, stack, context) do - stack = push_expr_stack(expr, stack) - + def of_expr({:receive, _meta, [blocks]}, _expected, stack, context) do {result, context} = reduce_ok(blocks, context, fn {:do, {:__block__, _, []}}, context -> @@ -316,92 +234,75 @@ defmodule Module.Types.Expr do {:do, clauses}, context -> of_clauses(clauses, stack, context) - {:after, [{:->, _meta, [head, body]}]}, context = acc -> - with {:ok, _type, context} <- of_expr(head, :dynamic, stack, context), - {:ok, _type, context} <- of_expr(body, :dynamic, stack, context), - do: {:ok, keep_warnings(acc, context)} + {:after, [{:->, _meta, [head, body]}]}, context -> + with {:ok, _type, context} <- of_expr(head, stack, context), + {:ok, _type, context} <- of_expr(body, stack, context), + do: {:ok, context} end) case result do - :ok -> {:ok, :dynamic, context} + :ok -> {:ok, dynamic(), context} :error -> {:error, context} end end # for pat <- expr do expr end - def of_expr({:for, _meta, [_ | _] = args} = expr, _expected, stack, context) do - stack = push_expr_stack(expr, stack) + def of_expr({:for, _meta, [_ | _] = args}, _expected, stack, context) do {clauses, [[{:do, block} | opts]]} = Enum.split(args, -1) with {:ok, context} <- reduce_ok(clauses, context, &for_clause(&1, stack, &2)), {:ok, context} <- reduce_ok(opts, context, &for_option(&1, stack, &2)) do if Keyword.has_key?(opts, :reduce) do with {:ok, context} <- of_clauses(block, stack, context) do - {:ok, :dynamic, context} + {:ok, dynamic(), context} end else - with {:ok, _type, context} <- of_expr(block, :dynamic, stack, context) do - {:ok, :dynamic, context} + with {:ok, _type, context} <- of_expr(block, stack, context) do + {:ok, dynamic(), context} end end end end # with pat <- expr do expr end - def of_expr({:with, _meta, [_ | _] = clauses} = expr, _expected, stack, context) do - stack = push_expr_stack(expr, stack) - + def of_expr({:with, _meta, [_ | _] = clauses}, _expected, stack, context) do case reduce_ok(clauses, context, &with_clause(&1, stack, &2)) do - {:ok, context} -> {:ok, :dynamic, context} + {:ok, context} -> {:ok, dynamic(), context} {:error, reason} -> {:error, reason} end end # fun.(args) - def of_expr({{:., _meta1, [fun]}, _meta2, args} = expr, _expected, stack, context) do - # TODO: Use expected type to infer intersection return type - stack = push_expr_stack(expr, stack) - - with {:ok, _fun_type, context} <- of_expr(fun, :dynamic, stack, context), + def of_expr({{:., _meta1, [fun]}, _meta2, args}, _expected, stack, context) do + with {:ok, _fun_type, context} <- of_expr(fun, stack, context), {:ok, _arg_types, context} <- - map_reduce_ok(args, context, &of_expr(&1, :dynamic, stack, &2)) do - {:ok, :dynamic, context} + map_reduce_ok(args, context, &of_expr(&1, stack, &2)) do + {:ok, dynamic(), context} end end # expr.key_or_fun - def of_expr({{:., _meta1, [expr1, key_or_fun]}, meta2, []} = expr2, _expected, stack, context) + def of_expr({{:., _meta1, [expr1, _key_or_fun]}, meta2, []}, _expected, stack, context) when not is_atom(expr1) do - stack = push_expr_stack(expr2, stack) - if Keyword.get(meta2, :no_parens, false) do - with {:ok, expr_type, context} <- of_expr(expr1, :dynamic, stack, context), - {value_var, context} = add_var(context), - pair_type = {:required, {:atom, key_or_fun}, value_var}, - optional_type = {:optional, :dynamic, :dynamic}, - map_field_type = {:map, [pair_type, optional_type]}, - {:ok, _map_type, context} <- unify(map_field_type, expr_type, stack, context), - do: {:ok, value_var, context} + with {:ok, _, context} <- of_expr(expr1, stack, context) do + {:ok, dynamic(), context} + end else - # TODO: Use expected type to infer intersection return type - with {:ok, expr_type, context} <- of_expr(expr1, :dynamic, stack, context), - {:ok, _map_type, context} <- unify(expr_type, :atom, stack, context), - do: {:ok, :dynamic, context} + with {:ok, _, context} <- of_expr(expr1, stack, context) do + {:ok, dynamic(), context} + end end end # expr.fun(arg) - def of_expr({{:., _meta1, [expr1, fun]}, meta2, args} = expr2, _expected, stack, context) do - # TODO: Use expected type to infer intersection return type - - context = Of.remote(expr1, fun, length(args), meta2, context) - stack = push_expr_stack(expr2, stack) + def of_expr({{:., _meta1, [expr1, fun]}, meta2, args}, _expected, stack, context) do + context = Of.remote(expr1, fun, length(args), meta2, stack, context) - with {:ok, _expr_type, context} <- of_expr(expr1, :dynamic, stack, context), - {:ok, _fun_type, context} <- of_expr(fun, :dynamic, stack, context), + with {:ok, _expr_type, context} <- of_expr(expr1, stack, context), {:ok, _arg_types, context} <- - map_reduce_ok(args, context, &of_expr(&1, :dynamic, stack, &2)) do - {:ok, :dynamic, context} + map_reduce_ok(args, context, &of_expr(&1, stack, &2)) do + {:ok, dynamic(), context} end end @@ -409,31 +310,26 @@ defmodule Module.Types.Expr do def of_expr( {:&, _, [{:/, _, [{{:., _, [module, fun]}, meta, []}, arity]}]}, _expected, - _stack, + stack, context ) when is_atom(module) and is_atom(fun) do - context = Of.remote(module, fun, arity, meta, context) - {:ok, :dynamic, context} + context = Of.remote(module, fun, arity, meta, stack, context) + {:ok, dynamic(), context} end # &foo/1 # & &1 def of_expr({:&, _meta, _arg}, _expected, _stack, context) do - # TODO: Function type - {:ok, :dynamic, context} + {:ok, dynamic(), context} end # fun(arg) - def of_expr({fun, _meta, args} = expr, _expected, stack, context) + def of_expr({fun, _meta, args}, _expected, stack, context) when is_atom(fun) and is_list(args) do - # TODO: Use expected type to infer intersection return type - - stack = push_expr_stack(expr, stack) - - case map_reduce_ok(args, context, &of_expr(&1, :dynamic, stack, &2)) do - {:ok, _arg_types, context} -> {:ok, :dynamic, context} - {:error, reason} -> {:error, reason} + with {:ok, _arg_types, context} <- + map_reduce_ok(args, context, &of_expr(&1, stack, &2)) do + {:ok, dynamic(), context} end end @@ -441,14 +337,14 @@ defmodule Module.Types.Expr do {pattern, guards} = extract_head([left]) with {:ok, _pattern_type, context} <- Pattern.of_head([pattern], guards, stack, context), - {:ok, _expr_type, context} <- of_expr(expr, :dynamic, stack, context), + {:ok, _expr_type, context} <- of_expr(expr, stack, context), do: {:ok, context} end defp for_clause({:<<>>, _, [{:<-, _, [pattern, expr]}]}, stack, context) do # TODO: the compiler guarantees pattern is a binary but we need to check expr is a binary with {:ok, _pattern_type, context} <- Pattern.of_pattern(pattern, stack, context), - {:ok, _expr_type, context} <- of_expr(expr, :dynamic, stack, context), + {:ok, _expr_type, context} <- of_expr(expr, stack, context), do: {:ok, context} end @@ -457,15 +353,15 @@ defmodule Module.Types.Expr do end defp for_clause(expr, stack, context) do - of_expr_context(expr, :dynamic, stack, context) + of_expr_context(expr, stack, context) end defp for_option({:into, expr}, stack, context) do - of_expr_context(expr, :dynamic, stack, context) + of_expr_context(expr, stack, context) end defp for_option({:reduce, expr}, stack, context) do - of_expr_context(expr, :dynamic, stack, context) + of_expr_context(expr, stack, context) end defp for_option({:uniq, _}, _stack, context) do @@ -476,7 +372,7 @@ defmodule Module.Types.Expr do {pattern, guards} = extract_head([left]) with {:ok, _pattern_type, context} <- Pattern.of_head([pattern], guards, stack, context), - {:ok, _expr_type, context} <- of_expr(expr, :dynamic, stack, context), + {:ok, _expr_type, context} <- of_expr(expr, stack, context), do: {:ok, context} end @@ -485,11 +381,11 @@ defmodule Module.Types.Expr do end defp with_clause(expr, stack, context) do - of_expr_context(expr, :dynamic, stack, context) + of_expr_context(expr, stack, context) end defp with_option({:do, body}, stack, context) do - of_expr_context(body, :dynamic, stack, context) + of_expr_context(body, stack, context) end defp with_option({:else, clauses}, stack, context) do @@ -497,26 +393,15 @@ defmodule Module.Types.Expr do end defp of_clauses(clauses, stack, context) do - reduce_ok(clauses, context, fn {:->, meta, [head, body]}, context = acc -> + reduce_ok(clauses, context, fn {:->, _meta, [head, body]}, context -> {patterns, guards} = extract_head(head) - case Pattern.of_head(patterns, guards, stack, context) do - {:ok, _, context} -> - with {:ok, _expr_type, context} <- of_expr(body, :dynamic, stack, context) do - {:ok, keep_warnings(acc, context)} - end - - error -> - # Skip the clause if it the head has an error - if meta[:generated], do: {:ok, acc}, else: error - end + with {:ok, _, context} <- Pattern.of_head(patterns, guards, stack, context), + {:ok, _, context} <- of_expr(body, stack, context), + do: {:ok, context} end) end - defp keep_warnings(context, %{warnings: warnings}) do - %{context | warnings: warnings} - end - defp extract_head([{:when, _meta, args}]) do case Enum.split(args, -1) do {patterns, [guards]} -> {patterns, flatten_when(guards)} @@ -536,18 +421,10 @@ defmodule Module.Types.Expr do [other] end - defp of_expr_context(expr, expected, stack, context) do - case of_expr(expr, expected, stack, context) do + defp of_expr_context(expr, stack, context) do + case of_expr(expr, stack, context) do {:ok, _type, context} -> {:ok, context} {:error, reason} -> {:error, reason} end end - - defp new_pattern_var({:_, _meta, var_context}, context) when is_atom(var_context) do - {:dynamic, context} - end - - defp new_pattern_var(var, context) do - new_var(var, context) - end end diff --git a/lib/elixir/lib/module/types/helpers.ex b/lib/elixir/lib/module/types/helpers.ex index d4203ed38d..eecb7a6b5c 100644 --- a/lib/elixir/lib/module/types/helpers.ex +++ b/lib/elixir/lib/module/types/helpers.ex @@ -25,6 +25,15 @@ defmodule Module.Types.Helpers do def get_meta({_, meta, _}), do: meta def get_meta(_other), do: [] + @doc """ + Emits a warnings. + """ + def warn(module, warning, meta, stack, context) do + {fun, arity} = stack.function + location = {stack.file, meta, {stack.module, fun, arity}} + %{context | warnings: [{module, warning, location} | context.warnings]} + end + @doc """ Like `Enum.reduce/3` but only continues while `fun` returns `{:ok, acc}` and stops on `{:error, reason}`. @@ -142,32 +151,6 @@ defmodule Module.Types.Helpers do defp do_flat_map_reduce_ok([], {list, acc}, _fun), do: {:ok, Enum.reverse(Enum.concat(list)), acc} - @doc """ - Given a list of `[{:ok, term()} | {:error, term()}]` it returns a list of - errors `{:error, [term()]}` in case of at least one error or `{:ok, [term()]}` - if there are no errors. - """ - def oks_or_errors(list) do - case Enum.split_with(list, &match?({:ok, _}, &1)) do - {oks, []} -> {:ok, Enum.map(oks, fn {:ok, ok} -> ok end)} - {_oks, errors} -> {:error, Enum.map(errors, fn {:error, error} -> error end)} - end - end - - @doc """ - Combines a list of guard expressions `when x when y when z` to an expression - combined with `or`, `x or y or z`. - """ - # TODO: Remove this and let multiple when be treated as multiple clauses, - # meaning they will be intersection types - def guards_to_or([]) do - [] - end - - def guards_to_or(guards) do - Enum.reduce(guards, fn guard, acc -> {{:., [], [:erlang, :orelse]}, [], [guard, acc]} end) - end - @doc """ Like `Enum.zip/1` but will zip multiple lists together instead of only two. """ diff --git a/lib/elixir/lib/module/types/of.ex b/lib/elixir/lib/module/types/of.ex index 87a8926621..9a97d343c7 100644 --- a/lib/elixir/lib/module/types/of.ex +++ b/lib/elixir/lib/module/types/of.ex @@ -3,13 +3,11 @@ defmodule Module.Types.Of do # Generic AST and Enum helpers go to Module.Types.Helpers. @moduledoc false - @prefix quote(do: ...) - @suffix quote(do: ...) + # @prefix quote(do: ...) + # @suffix quote(do: ...) alias Module.ParallelChecker - - import Module.Types.Helpers - import Module.Types.Unify + import Module.Types.{Helpers, Descr} # There are important assumptions on how we work with maps. # @@ -27,36 +25,11 @@ defmodule Module.Types.Of do # assumptions. @doc """ - Handles open maps (with dynamic => dynamic). + Handles open maps. """ def open_map(args, stack, context, of_fun) do - with {:ok, pairs, context} <- map_pairs(args, stack, context, of_fun) do - # If we match on a map such as %{"foo" => "bar"}, we cannot - # assert that %{binary() => binary()}, since we are matching - # only a single binary of infinite possible values. Therefore, - # the correct would be to match it to %{binary() => binary() | var}. - # - # We can skip this in two cases: - # - # 1. If the key is a singleton, then we know that it has no - # other value than the current one - # - # 2. If the value is a variable, then there is no benefit in - # creating another variable, so we can skip it - # - # For now, we skip generating the var itself and introduce - # :dynamic instead. - pairs = - for {key, value} <- pairs, not has_unbound_var?(key, context) do - if singleton?(key, context) or match?({:var, _}, value) do - {key, value} - else - {key, to_union([value, :dynamic], context)} - end - end - - triplets = pairs_to_unions(pairs, [], context) ++ [{:optional, :dynamic, :dynamic}] - {:ok, {:map, triplets}, context} + with {:ok, _pairs, context} <- map_pairs(args, stack, context, of_fun) do + {:ok, map(), context} end end @@ -64,75 +37,25 @@ defmodule Module.Types.Of do Handles closed maps (without dynamic => dynamic). """ def closed_map(args, stack, context, of_fun) do - with {:ok, pairs, context} <- map_pairs(args, stack, context, of_fun) do - {:ok, {:map, closed_to_unions(pairs, context)}, context} + with {:ok, _pairs, context} <- map_pairs(args, stack, context, of_fun) do + {:ok, map(), context} end end defp map_pairs(pairs, stack, context, of_fun) do map_reduce_ok(pairs, context, fn {key, value}, context -> - with {:ok, key_type, context} <- of_fun.(key, :dynamic, stack, context), - {:ok, value_type, context} <- of_fun.(value, :dynamic, stack, context), + with {:ok, key_type, context} <- of_fun.(key, stack, context), + {:ok, value_type, context} <- of_fun.(value, stack, context), do: {:ok, {key_type, value_type}, context} end) end - defp closed_to_unions([{key, value}], _context), do: [{:required, key, value}] - - defp closed_to_unions(pairs, context) do - case Enum.split_with(pairs, fn {key, _value} -> has_unbound_var?(key, context) end) do - {[], pairs} -> pairs_to_unions(pairs, [], context) - {[_ | _], pairs} -> pairs_to_unions([{:dynamic, :dynamic} | pairs], [], context) - end - end - - defp pairs_to_unions([{key, value} | ahead], behind, context) do - {matched_ahead, values} = find_matching_values(ahead, key, [], []) - - # In case nothing matches, use the original ahead - ahead = matched_ahead || ahead - - all_values = - [value | values] ++ - find_subtype_values(ahead, key, context) ++ - find_subtype_values(behind, key, context) - - pairs_to_unions(ahead, [{key, to_union(all_values, context)} | behind], context) - end - - defp pairs_to_unions([], acc, context) do - acc - |> Enum.sort(&subtype?(elem(&1, 0), elem(&2, 0), context)) - |> Enum.map(fn {key, value} -> {:required, key, value} end) - end - - defp find_subtype_values(pairs, key, context) do - for {pair_key, pair_value} <- pairs, subtype?(pair_key, key, context), do: pair_value - end - - defp find_matching_values([{key, value} | ahead], key, acc, values) do - find_matching_values(ahead, key, acc, [value | values]) - end - - defp find_matching_values([{_, _} = pair | ahead], key, acc, values) do - find_matching_values(ahead, key, [pair | acc], values) - end - - defp find_matching_values([], _key, acc, [_ | _] = values), do: {Enum.reverse(acc), values} - defp find_matching_values([], _key, _acc, []), do: {nil, []} - @doc """ Handles structs. """ - def struct(struct, meta, context) do - context = remote(struct, :__struct__, 0, meta, context) - - entries = - for key <- Map.keys(struct.__struct__()), key != :__struct__ do - {:required, {:atom, key}, :dynamic} - end - - {:ok, {:map, [{:required, {:atom, :__struct__}, {:atom, struct}} | entries]}, context} + def struct(struct, meta, stack, context) do + context = remote(struct, :__struct__, 0, meta, stack, context) + {:ok, map(), context} end ## Binary @@ -143,41 +66,43 @@ defmodule Module.Types.Of do In the stack, we add nodes such as <>, <<..., expr>>, etc, based on the position of the expression within the binary. """ - def binary([], _stack, context, _of_fun) do + def binary([], _type, _stack, context, _of_fun) do {:ok, context} end - def binary([head], stack, context, of_fun) do - head_stack = push_expr_stack({:<<>>, get_meta(head), [head]}, stack) - binary_segment(head, head_stack, context, of_fun) + def binary([head], type, stack, context, of_fun) do + # stack = push_expr_stack({:<<>>, get_meta(head), [head]}, stack) + binary_segment(head, type, stack, context, of_fun) end - def binary([head | tail], stack, context, of_fun) do - head_stack = push_expr_stack({:<<>>, get_meta(head), [head, @suffix]}, stack) + def binary([head | tail], type, stack, context, of_fun) do + # stack = push_expr_stack({:<<>>, get_meta(head), [head, @suffix]}, stack) - case binary_segment(head, head_stack, context, of_fun) do - {:ok, context} -> binary_many(tail, stack, context, of_fun) + case binary_segment(head, type, stack, context, of_fun) do + {:ok, context} -> binary_many(tail, type, stack, context, of_fun) {:error, reason} -> {:error, reason} end end - defp binary_many([last], stack, context, of_fun) do - last_stack = push_expr_stack({:<<>>, get_meta(last), [@prefix, last]}, stack) - binary_segment(last, last_stack, context, of_fun) + defp binary_many([last], type, stack, context, of_fun) do + # stack = push_expr_stack({:<<>>, get_meta(last), [@prefix, last]}, stack) + binary_segment(last, type, stack, context, of_fun) end - defp binary_many([head | tail], stack, context, of_fun) do - head_stack = push_expr_stack({:<<>>, get_meta(head), [@prefix, head, @suffix]}, stack) + defp binary_many([head | tail], type, stack, context, of_fun) do + # stack = push_expr_stack({:<<>>, get_meta(head), [@prefix, head, @suffix]}, stack) - case binary_segment(head, head_stack, context, of_fun) do - {:ok, context} -> binary_many(tail, stack, context, of_fun) + case binary_segment(head, type, stack, context, of_fun) do + {:ok, context} -> binary_many(tail, type, stack, context, of_fun) {:error, reason} -> {:error, reason} end end - defp binary_segment({:"::", _meta, [expr, specifiers]}, stack, context, of_fun) do - expected_type = - collect_binary_specifier(specifiers, &binary_type(stack.context, &1)) || :integer + defp binary_segment({:"::", _meta, [expr, specifiers]}, type, stack, context, of_fun) do + # TODO: handle size in specifiers + # TODO: unpack specifiers once + _expected_type = + collect_binary_specifier(specifiers, &binary_type(type, &1)) || :integer utf? = collect_binary_specifier(specifiers, &utf_type?/1) float? = collect_binary_specifier(specifiers, &float_type?/1) @@ -185,15 +110,14 @@ defmodule Module.Types.Of do # Special case utf and float specifiers because they can be two types as literals # but only a specific type as a variable in a pattern cond do - stack.context == :pattern and utf? and is_binary(expr) -> + type == :pattern and utf? and is_binary(expr) -> {:ok, context} - stack.context == :pattern and float? and is_integer(expr) -> + type == :pattern and float? and is_integer(expr) -> {:ok, context} true -> - with {:ok, type, context} <- of_fun.(expr, expected_type, stack, context), - {:ok, _type, context} <- unify(type, expected_type, stack, context), + with {:ok, _type, context} <- of_fun.(expr, stack, context), do: {:ok, context} end end @@ -234,63 +158,61 @@ defmodule Module.Types.Of do @doc """ Handles remote calls. """ - def remote(module, fun, arity, meta, context) when is_atom(module) do - # TODO: In the future we may want to warn for modules defined - # in the local context + def remote(module, fun, arity, meta, stack, context) when is_atom(module) do if Keyword.get(meta, :context_module, false) do context else - ParallelChecker.preload_module(context.cache, module) - check_export(module, fun, arity, meta, context) + ParallelChecker.preload_module(stack.cache, module) + check_export(module, fun, arity, meta, stack, context) end end - def remote(_module, _fun, _arity, _meta, context), do: context + def remote(_module, _fun, _arity, _meta, _stack, context), do: context - defp check_export(module, fun, arity, meta, context) do - case ParallelChecker.fetch_export(context.cache, module, fun, arity) do + defp check_export(module, fun, arity, meta, stack, context) do + case ParallelChecker.fetch_export(stack.cache, module, fun, arity) do {:ok, mode, :def, reason} -> - check_deprecated(mode, module, fun, arity, reason, meta, context) + check_deprecated(mode, module, fun, arity, reason, meta, stack, context) {:ok, mode, :defmacro, reason} -> - context = warn(meta, context, {:unrequired_module, module, fun, arity}) - check_deprecated(mode, module, fun, arity, reason, meta, context) + context = warn({:unrequired_module, module, fun, arity}, meta, stack, context) + check_deprecated(mode, module, fun, arity, reason, meta, stack, context) {:error, :module} -> - if warn_undefined?(module, fun, arity, context) do - warn(meta, context, {:undefined_module, module, fun, arity}) + if warn_undefined?(module, fun, arity, stack) do + warn({:undefined_module, module, fun, arity}, meta, stack, context) else context end {:error, :function} -> - if warn_undefined?(module, fun, arity, context) do - exports = ParallelChecker.all_exports(context.cache, module) - warn(meta, context, {:undefined_function, module, fun, arity, exports}) + if warn_undefined?(module, fun, arity, stack) do + exports = ParallelChecker.all_exports(stack.cache, module) + warn({:undefined_function, module, fun, arity, exports}, meta, stack, context) else context end end end - defp check_deprecated(:elixir, module, fun, arity, reason, meta, context) do + defp check_deprecated(:elixir, module, fun, arity, reason, meta, stack, context) do if reason do - warn(meta, context, {:deprecated, module, fun, arity, reason}) + warn({:deprecated, module, fun, arity, reason}, meta, stack, context) else context end end - defp check_deprecated(:erlang, module, fun, arity, _reason, meta, context) do + defp check_deprecated(:erlang, module, fun, arity, _reason, meta, stack, context) do case :otp_internal.obsolete(module, fun, arity) do {:deprecated, string} when is_list(string) -> reason = string |> List.to_string() |> :string.titlecase() - warn(meta, context, {:deprecated, module, fun, arity, reason}) + warn({:deprecated, module, fun, arity, reason}, meta, stack, context) {:deprecated, string, removal} when is_list(string) and is_list(removal) -> reason = string |> List.to_string() |> :string.titlecase() reason = "It will be removed in #{removal}. #{reason}" - warn(meta, context, {:deprecated, module, fun, arity, reason}) + warn({:deprecated, module, fun, arity, reason}, meta, stack, context) _ -> context @@ -307,24 +229,22 @@ defmodule Module.Types.Of do # # But for protocols we don't want to traverse the protocol code anyway. # TODO: remove this clause once we no longer traverse the protocol code. - defp warn_undefined?(_module, :__impl__, 1, _context), do: false - defp warn_undefined?(_module, :module_info, 0, _context), do: false - defp warn_undefined?(_module, :module_info, 1, _context), do: false - defp warn_undefined?(:erlang, :orelse, 2, _context), do: false - defp warn_undefined?(:erlang, :andalso, 2, _context), do: false + defp warn_undefined?(_module, :__impl__, 1, _stack), do: false + defp warn_undefined?(_module, :module_info, 0, _stack), do: false + defp warn_undefined?(_module, :module_info, 1, _stack), do: false + defp warn_undefined?(:erlang, :orelse, 2, _stack), do: false + defp warn_undefined?(:erlang, :andalso, 2, _stack), do: false defp warn_undefined?(_, _, _, %{no_warn_undefined: :all}) do false end - defp warn_undefined?(module, fun, arity, context) do - not Enum.any?(context.no_warn_undefined, &(&1 == module or &1 == {module, fun, arity})) + defp warn_undefined?(module, fun, arity, stack) do + not Enum.any?(stack.no_warn_undefined, &(&1 == module or &1 == {module, fun, arity})) end - defp warn(meta, context, warning) do - {fun, arity} = context.function - location = {context.file, meta, {context.module, fun, arity}} - %{context | warnings: [{__MODULE__, warning, location} | context.warnings]} + defp warn(warning, meta, stack, context) do + warn(__MODULE__, warning, meta, stack, context) end ## Warning formatting diff --git a/lib/elixir/lib/module/types/pattern.ex b/lib/elixir/lib/module/types/pattern.ex index 36d2d25a1c..ad1ebc07d5 100644 --- a/lib/elixir/lib/module/types/pattern.ex +++ b/lib/elixir/lib/module/types/pattern.ex @@ -2,7 +2,7 @@ defmodule Module.Types.Pattern do @moduledoc false alias Module.Types.Of - import Module.Types.{Helpers, Unify} + import Module.Types.{Helpers, Descr} @doc """ Handles patterns and guards at once. @@ -11,7 +11,7 @@ defmodule Module.Types.Pattern do with {:ok, types, context} <- map_reduce_ok(patterns, context, &of_pattern(&1, stack, &2)), # TODO: Check that of_guard/4 returns boolean() | :fail - {:ok, _, context} <- of_guard(guards_to_or(guards), :dynamic, stack, context), + {:ok, _, context} <- of_guards(guards, term(), stack, context), do: {:ok, types, context} end @@ -19,61 +19,55 @@ defmodule Module.Types.Pattern do Return the type and typing context of a pattern expression or an error in case of a typing conflict. """ - def of_pattern(pattern, %{context: stack_context} = stack, context) - when stack_context != :pattern do - of_pattern(pattern, %{stack | context: :pattern}, context) - end - # _ def of_pattern({:_, _meta, atom}, _stack, context) when is_atom(atom) do - {:ok, :dynamic, context} + {:ok, dynamic(), context} end # ^var - def of_pattern({:^, _meta, [var]}, _stack, context) do - {:ok, get_var!(var, context), context} + def of_pattern({:^, _meta, [_var]}, _stack, context) do + {:ok, dynamic(), context} end # var def of_pattern(var, _stack, context) when is_var(var) do - {type, context} = new_var(var, context) - {:ok, type, context} + {:ok, dynamic(), context} end # left = right - def of_pattern({:=, _meta, [left_expr, right_expr]} = expr, stack, context) do - stack = push_expr_stack(expr, stack) - - with {:ok, left_type, context} <- of_pattern(left_expr, stack, context), - {:ok, right_type, context} <- of_pattern(right_expr, stack, context), - do: unify(left_type, right_type, stack, context) + def of_pattern({:=, _meta, [left_expr, right_expr]}, stack, context) do + with {:ok, _, context} <- of_pattern(left_expr, stack, context), + {:ok, _, context} <- of_pattern(right_expr, stack, context), + do: {:ok, dynamic(), context} end # %_{...} def of_pattern( - {:%, _meta1, [{:_, _meta2, var_context}, {:%{}, _meta3, args}]} = expr, + {:%, _meta1, [{:_, _meta2, var_context}, {:%{}, _meta3, args}]}, stack, context ) when is_atom(var_context) do - stack = push_expr_stack(expr, stack) - expected_fun = fn arg, _expected, stack, context -> of_pattern(arg, stack, context) end - - with {:ok, {:map, pairs}, context} <- Of.open_map(args, stack, context, expected_fun) do - {:ok, {:map, [{:required, {:atom, :__struct__}, :atom} | pairs]}, context} + with {:ok, _, context} <- Of.open_map(args, stack, context, &of_pattern/3) do + {:ok, map(), context} end end # %var{...} and %^var{...} - def of_pattern({:%, _meta1, [var, {:%{}, _meta2, args}]} = expr, stack, context) + def of_pattern({:%, _meta1, [var, {:%{}, _meta2, args}]}, stack, context) when not is_atom(var) do - stack = push_expr_stack(expr, stack) - expected_fun = fn arg, _expected, stack, context -> of_pattern(arg, stack, context) end + # TODO: validate var is an atom + with {:ok, _, context} = of_pattern(var, stack, context), + {:ok, _, context} <- Of.open_map(args, stack, context, &of_pattern/3) do + {:ok, map(), context} + end + end - with {:ok, var_type, context} = of_pattern(var, stack, context), - {:ok, _, context} <- unify(var_type, :atom, stack, context), - {:ok, {:map, pairs}, context} <- Of.open_map(args, stack, context, expected_fun) do - {:ok, {:map, [{:required, {:atom, :__struct__}, var_type} | pairs]}, context} + # <<...>>> + def of_pattern({:<<>>, _meta, args}, stack, context) do + case Of.binary(args, :pattern, stack, context, &of_pattern/3) do + {:ok, context} -> {:ok, binary(), context} + {:error, reason} -> {:error, reason} end end @@ -81,616 +75,46 @@ defmodule Module.Types.Pattern do of_shared(expr, stack, context, &of_pattern/3) end - ## GUARDS - - # TODO: Some guards can be changed to intersection types or higher order types - @boolean {:union, [{:atom, true}, {:atom, false}]} - @number {:union, [:integer, :float]} - @unary_number_fun [{[:integer], :integer}, {[@number], :float}] - @binary_number_fun [ - {[:integer, :integer], :integer}, - {[:float, @number], :float}, - {[@number, :float], :float} - ] - - @guard_functions %{ - {:is_atom, 1} => [{[:atom], @boolean}], - {:is_binary, 1} => [{[:binary], @boolean}], - {:is_bitstring, 1} => [{[:binary], @boolean}], - {:is_boolean, 1} => [{[@boolean], @boolean}], - {:is_float, 1} => [{[:float], @boolean}], - {:is_function, 1} => [{[:fun], @boolean}], - {:is_function, 2} => [{[:fun, :integer], @boolean}], - {:is_integer, 1} => [{[:integer], @boolean}], - {:is_list, 1} => [{[{:list, :dynamic}], @boolean}], - {:is_map, 1} => [{[{:map, [{:optional, :dynamic, :dynamic}]}], @boolean}], - {:is_map_key, 2} => [{[:dynamic, {:map, [{:optional, :dynamic, :dynamic}]}], :dynamic}], - {:is_number, 1} => [{[@number], @boolean}], - {:is_pid, 1} => [{[:pid], @boolean}], - {:is_port, 1} => [{[:port], @boolean}], - {:is_reference, 1} => [{[:reference], @boolean}], - {:is_tuple, 1} => [{[:tuple], @boolean}], - {:<, 2} => [{[:dynamic, :dynamic], @boolean}], - {:"=<", 2} => [{[:dynamic, :dynamic], @boolean}], - {:>, 2} => [{[:dynamic, :dynamic], @boolean}], - {:>=, 2} => [{[:dynamic, :dynamic], @boolean}], - {:"/=", 2} => [{[:dynamic, :dynamic], @boolean}], - {:"=/=", 2} => [{[:dynamic, :dynamic], @boolean}], - {:==, 2} => [{[:dynamic, :dynamic], @boolean}], - {:"=:=", 2} => [{[:dynamic, :dynamic], @boolean}], - {:*, 2} => @binary_number_fun, - {:+, 1} => @unary_number_fun, - {:+, 2} => @binary_number_fun, - {:-, 1} => @unary_number_fun, - {:-, 2} => @binary_number_fun, - {:/, 2} => @binary_number_fun, - {:abs, 1} => @unary_number_fun, - {:ceil, 1} => [{[@number], :integer}], - {:floor, 1} => [{[@number], :integer}], - {:round, 1} => [{[@number], :integer}], - {:trunc, 1} => [{[@number], :integer}], - {:element, 2} => [{[:integer, :tuple], :dynamic}], - {:hd, 1} => [{[{:list, :dynamic}], :dynamic}], - {:length, 1} => [{[{:list, :dynamic}], :integer}], - {:map_get, 2} => [{[:dynamic, {:map, [{:optional, :dynamic, :dynamic}]}], :dynamic}], - {:map_size, 1} => [{[{:map, [{:optional, :dynamic, :dynamic}]}], :integer}], - {:tl, 1} => [{[{:list, :dynamic}], :dynamic}], - {:tuple_size, 1} => [{[:tuple], :integer}], - {:node, 1} => [{[{:union, [:pid, :reference, :port]}], :atom}], - {:binary_part, 3} => [{[:binary, :integer, :integer], :binary}], - {:bit_size, 1} => [{[:binary], :integer}], - {:byte_size, 1} => [{[:binary], :integer}], - {:size, 1} => [{[{:union, [:binary, :tuple]}], @boolean}], - {:div, 2} => [{[:integer, :integer], :integer}], - {:rem, 2} => [{[:integer, :integer], :integer}], - {:node, 0} => [{[], :atom}], - {:self, 0} => [{[], :pid}], - {:bnot, 1} => [{[:integer], :integer}], - {:band, 2} => [{[:integer, :integer], :integer}], - {:bor, 2} => [{[:integer, :integer], :integer}], - {:bxor, 2} => [{[:integer, :integer], :integer}], - {:bsl, 2} => [{[:integer, :integer], :integer}], - {:bsr, 2} => [{[:integer, :integer], :integer}], - {:or, 2} => [{[@boolean, @boolean], @boolean}], - {:and, 2} => [{[@boolean, @boolean], @boolean}], - {:xor, 2} => [{[@boolean, @boolean], @boolean}], - {:not, 1} => [{[@boolean], @boolean}] - - # Following guards are matched explicitly to handle - # type guard functions such as is_atom/1 - # {:andalso, 2} => {[@boolean, @boolean], @boolean} - # {:orelse, 2} => {[@boolean, @boolean], @boolean} - } - - @type_guards [ - :is_atom, - :is_binary, - :is_bitstring, - :is_boolean, - :is_float, - :is_function, - :is_integer, - :is_list, - :is_map, - :is_number, - :is_pid, - :is_port, - :is_reference, - :is_tuple - ] - @doc """ Refines the type variables in the typing context using type check guards such as `is_integer/1`. """ - def of_guard(expr, expected, %{context: stack_context} = stack, context) - when stack_context != :pattern do - of_guard(expr, expected, %{stack | context: :pattern}, context) - end - - def of_guard({{:., _, [:erlang, :andalso]}, _, [left, right]} = expr, _expected, stack, context) do - stack = push_expr_stack(expr, stack) - - with {:ok, left_type, context} <- of_guard(left, @boolean, stack, context), - {:ok, _, context} <- unify(left_type, @boolean, stack, context), - {:ok, right_type, context} <- of_guard(right, :dynamic, keep_guarded(stack), context), - do: {:ok, to_union([@boolean, right_type], context), context} - end - - def of_guard({{:., _, [:erlang, :orelse]}, _, [left, right]} = expr, _expected, stack, context) do - stack = push_expr_stack(expr, stack) - left_indexes = collect_var_indexes_from_expr(left, context) - right_indexes = collect_var_indexes_from_expr(right, context) - - with {:ok, left_type, left_context} <- of_guard(left, @boolean, stack, context), - {:ok, _right_type, right_context} <- of_guard(right, :dynamic, stack, context), - context = - merge_context_or( - left_indexes, - right_indexes, - context, - stack, - left_context, - right_context - ), - {:ok, _, context} <- unify(left_type, @boolean, stack, context), - do: {:ok, @boolean, context} - end - - # The unary operators + and - are special cased to avoid common warnings until - # we add support for intersection types for the guard functions - # -integer / +integer - def of_guard({{:., _, [:erlang, guard]}, _, [integer]}, _expected, _stack, context) - when guard in [:+, :-] and is_integer(integer) do - {:ok, :integer, context} - end - - # -float / +float - def of_guard({{:., _, [:erlang, guard]}, _, [float]}, _expected, _stack, context) - when guard in [:+, :-] and is_float(float) do - {:ok, :float, context} - end - - # tuple_size(arg) == integer - def of_guard( - {{:., _, [:erlang, :==]}, _, [{{:., _, [:erlang, :tuple_size]}, _, [var]}, size]} = expr, - expected, - stack, - context - ) - when is_var(var) and is_integer(size) do - of_tuple_size(var, size, expr, expected, stack, context) - end - - # integer == tuple_size(arg) - def of_guard( - {{:., _, [:erlang, :==]}, _, [size, {{:., _, [:erlang, :tuple_size]}, _, [var]}]} = expr, - expected, - stack, - context - ) - when is_var(var) and is_integer(size) do - of_tuple_size(var, size, expr, expected, stack, context) - end - - # fun(args) - def of_guard({{:., _, [:erlang, guard]}, _, args} = expr, expected, stack, context) do - type_guard? = type_guard?(guard) - {consider_type_guards?, keep_guarded?} = stack.type_guards - signature = guard_signature(guard, length(args)) - - # Only check type guards in the context of and/or/not, - # a type guard in the context of is_tuple(x) > :foo - # should not affect the inference of x - if not type_guard? or consider_type_guards? do - stack = push_expr_stack(expr, stack) - expected_clauses = filter_clauses(signature, expected, stack, context) - param_unions = signature_to_param_unions(expected_clauses, context) - arg_stack = %{stack | type_guards: {false, keep_guarded?}} - mfa = {:erlang, guard, length(args)} - - with {:ok, arg_types, context} <- - map_reduce_ok(Enum.zip(args, param_unions), context, fn {arg, param}, context -> - of_guard(arg, param, arg_stack, context) - end), - {:ok, return_type, context} <- - unify_call( - arg_types, - expected_clauses, - expected, - mfa, - signature, - stack, - context, - type_guard? - ) do - guard_sources = guard_sources(arg_types, type_guard?, keep_guarded?, context) - {:ok, return_type, %{context | guard_sources: guard_sources}} - end - else - # Assume that type guards always return boolean - boolean = {:union, [atom: true, atom: false]} - [{_params, ^boolean}] = signature - {:ok, boolean, context} - end - end - - # map.field - def of_guard({{:., meta1, [map, field]}, meta2, []}, expected, stack, context) do - of_guard({{:., meta1, [:erlang, :map_get]}, meta2, [field, map]}, expected, stack, context) - end - - # var - def of_guard(var, _expected, _stack, context) when is_var(var) do - {:ok, get_var!(var, context), context} - end - - def of_guard(expr, _expected, stack, context) do - of_shared(expr, stack, context, &of_guard(&1, :dynamic, &2, &3)) - end - - defp of_tuple_size(var, size, expr, _expected, stack, context) do - {consider_type_guards?, _keep_guarded?} = stack.type_guards - - result = - if consider_type_guards? do - stack = push_expr_stack(expr, stack) - tuple_elems = Enum.map(1..size//1, fn _ -> :dynamic end) - - with {:ok, type, context} <- of_guard(var, :dynamic, stack, context), - {:ok, _type, context} <- unify({:tuple, size, tuple_elems}, type, stack, context), - do: {:ok, context} - else - {:ok, context} - end - - case result do - {:ok, context} -> - boolean = {:union, [atom: true, atom: false]} - {:ok, boolean, context} - - {:error, reason} -> - {:error, reason} - end - end - - defp signature_to_param_unions(signature, context) do - signature - |> Enum.map(fn {params, _return} -> params end) - |> zip_many() - |> Enum.map(&to_union(&1, context)) - end - - # Collect guard sources from argument types, see type context documentation - # for more information - defp guard_sources(arg_types, type_guard?, keep_guarded?, context) do - {arg_types, guard_sources} = - case arg_types do - [{:var, index} | rest_arg_types] when type_guard? -> - guard_sources = Map.put_new(context.guard_sources, index, :guarded) - {rest_arg_types, guard_sources} - - _ -> - {arg_types, context.guard_sources} - end - - Enum.reduce(arg_types, guard_sources, fn - {:var, index}, guard_sources -> - Map.update(guard_sources, index, :fail, &guarded_if_keep_guarded(&1, keep_guarded?)) - - _, guard_sources -> - guard_sources - end) - end - - defp collect_var_indexes_from_expr(expr, context) do - {_, vars} = - Macro.prewalk(expr, %{}, fn - {:"::", _, [left, right]}, acc -> - # Do not mistake binary modifiers as variables - {collect_exprs_from_modifiers(right, [left]), acc} - - var, acc when is_var(var) -> - var_name = var_name(var) - %{^var_name => type} = context.vars - {var, collect_var_indexes(type, context, acc)} - - other, acc -> - {other, acc} - end) - - Map.keys(vars) - end - - defp collect_exprs_from_modifiers({:-, _, [left, right]}, acc) do - collect_exprs_from_modifiers(left, collect_expr_from_modifier(right, acc)) - end - - defp collect_exprs_from_modifiers(modifier, acc) do - collect_expr_from_modifier(modifier, acc) - end - - defp collect_expr_from_modifier({:unit, _, [arg]}, acc), do: [arg | acc] - defp collect_expr_from_modifier({:size, _, [arg]}, acc), do: [arg | acc] - defp collect_expr_from_modifier({var, _, ctx}, acc) when is_atom(var) and is_atom(ctx), do: acc - - defp unify_call(args, clauses, _expected, _mfa, _signature, stack, context, true = _type_guard?) do - unify_type_guard_call(args, clauses, stack, context) - end - - defp unify_call(args, clauses, expected, mfa, signature, stack, context, false = _type_guard?) do - unify_call(args, clauses, expected, mfa, signature, stack, context) - end - - defp unify_call([], [{[], return}], _expected, _mfa, _signature, _stack, context) do - {:ok, return, context} - end - - defp unify_call(args, clauses, expected, mfa, signature, stack, context) do - # Given the arguments: - # foo | bar, {:ok, baz | bat} - - # Expand unions in arguments: - # foo | bar, {:ok, baz} | {:ok, bat} - - # Permute arguments: - # foo, {:ok, baz} - # foo, {:ok, bat} - # bar, {:ok, baz} - # bar, {:ok, bat} - - flatten_args = Enum.map(args, &flatten_union(&1, context)) - cartesian_args = cartesian_product(flatten_args) - - # Remove clauses that do not match the expected type - # Ignore type variables in parameters by changing them to dynamic - - clauses = - clauses - |> filter_clauses(expected, stack, context) - |> Enum.map(fn {params, return} -> - {Enum.map(params, &var_to_dynamic/1), return} - end) - - # For each permuted argument find the clauses they match - # All arguments must match at least one clause, but all clauses - # do not need to match - # Collect the return values from clauses that matched and collect - # the type contexts from unifying argument and parameter to - # infer type variables in arguments - result = - flat_map_ok(cartesian_args, fn cartesian_args -> - result = - Enum.flat_map(clauses, fn {params, return} -> - result = - map_ok(Enum.zip(cartesian_args, params), fn {arg, param} -> - case unify(arg, param, stack, context) do - {:ok, _type, context} -> {:ok, context} - {:error, reason} -> {:error, reason} - end - end) - - case result do - {:ok, contexts} -> [{return, contexts}] - {:error, _reason} -> [] - end - end) - - if result != [] do - {:ok, result} - else - {:error, args} - end - end) - - case result do - {:ok, returns_contexts} -> - {success_returns, contexts} = Enum.unzip(returns_contexts) - contexts = Enum.concat(contexts) - - indexes = - for types <- flatten_args, - type <- types, - index <- collect_var_indexes_from_type(type), - do: index, - uniq: true - - # Build unions from collected type contexts to unify with - # type variables from arguments - result = - map_reduce_ok(indexes, context, fn index, context -> - union = - contexts - |> Enum.map(&Map.fetch!(&1.types, index)) - |> Enum.reject(&(&1 == :unbound)) - - if union == [] do - {:ok, {:var, index}, context} - else - unify({:var, index}, to_union(union, context), stack, context) - end - end) - - case result do - {:ok, _types, context} -> {:ok, to_union(success_returns, context), context} - {:error, reason} -> {:error, reason} - end - - {:error, args} -> - error(:unable_apply, {mfa, args, expected, signature, stack}, context) - end - end - - defp unify_type_guard_call(args, [{params, return}], stack, context) do - result = - reduce_ok(Enum.zip(args, params), context, fn {arg, param}, context -> - case unify(arg, param, stack, context) do - {:ok, _, context} -> {:ok, context} - {:error, reason} -> {:error, reason} - end - end) - - case result do - {:ok, context} -> {:ok, return, context} - {:error, reason} -> {:error, reason} - end - end - - defp cartesian_product(lists) do - List.foldr(lists, [[]], fn list, acc -> - for elem_list <- list, - list_acc <- acc, - do: [elem_list | list_acc] - end) - end - - defp var_to_dynamic(type) do - {type, _acc} = - walk(type, :ok, fn - {:var, _index}, :ok -> - {:dynamic, :ok} - - other, :ok -> - {other, :ok} - end) - - type - end - - defp collect_var_indexes_from_type(type) do - {_type, indexes} = - walk(type, [], fn - {:var, index}, indexes -> - {{:var, index}, [index | indexes]} - - other, indexes -> - {other, indexes} - end) - - indexes - end - - defp merge_context_or(left_indexes, right_indexes, context, stack, left, right) do - left_different = filter_different_indexes(left_indexes, left, right) - right_different = filter_different_indexes(right_indexes, left, right) - - case {left_different, right_different} do - {[index], [index]} -> merge_context_or_equal(index, stack, left, right) - {_, _} -> merge_context_or_diff(left_different, context, left) - end - end - - defp filter_different_indexes(indexes, left, right) do - Enum.filter(indexes, fn index -> - %{^index => left_type} = left.types - %{^index => right_type} = right.types - left_type != right_type - end) - end - - defp merge_context_or_equal(index, stack, left, right) do - %{^index => left_type} = left.types - %{^index => right_type} = right.types - - cond do - left_type == :unbound -> - refine_var!(index, right_type, stack, left) - - right_type == :unbound -> - left - - true -> - # Only include right side if left side is from type guard such as is_list(x), - # do not refine in case of length(x) - if left.guard_sources[index] == :fail do - guard_sources = Map.put(left.guard_sources, index, :fail) - left = %{left | guard_sources: guard_sources} - refine_var!(index, left_type, stack, left) - else - guard_sources = merge_guard_sources([left.guard_sources, right.guard_sources]) - left = %{left | guard_sources: guard_sources} - refine_var!(index, to_union([left_type, right_type], left), stack, left) - end - end - end - - # If the variable failed, we can keep them from the left side as is. - # If they didn't fail, then we need to restore them to their original value. - defp merge_context_or_diff(indexes, old_context, new_context) do - Enum.reduce(indexes, new_context, fn index, context -> - if new_context.guard_sources[index] == :fail do - context - else - restore_var!(index, new_context, old_context) - end - end) + def of_guards(_expr, _expected, _stack, context) do + {:ok, dynamic(), context} end - defp merge_guard_sources(sources) do - Enum.reduce(sources, fn left, right -> - Map.merge(left, right, fn - _index, :guarded, :guarded -> :guarded - _index, _, _ -> :fail - end) - end) - end - - defp guarded_if_keep_guarded(:guarded, true), do: :guarded - defp guarded_if_keep_guarded(_, _), do: :fail - - defp keep_guarded(%{type_guards: {consider?, _}} = stack), - do: %{stack | type_guards: {consider?, true}} - - defp filter_clauses(signature, expected, stack, context) do - Enum.filter(signature, fn {_params, return} -> - match?({:ok, _type, _context}, unify(return, expected, stack, context)) - end) - end - - Enum.each(@guard_functions, fn {{name, arity}, signature} -> - defp guard_signature(unquote(name), unquote(arity)), do: unquote(Macro.escape(signature)) - end) - - Enum.each(@type_guards, fn name -> - defp type_guard?(unquote(name)), do: true - end) - - defp type_guard?(name) when is_atom(name), do: false - ## Shared # :atom defp of_shared(atom, _stack, context, _fun) when is_atom(atom) do - {:ok, {:atom, atom}, context} + {:ok, atom(atom), context} end # 12 defp of_shared(literal, _stack, context, _fun) when is_integer(literal) do - {:ok, :integer, context} + {:ok, integer(), context} end # 1.2 defp of_shared(literal, _stack, context, _fun) when is_float(literal) do - {:ok, :float, context} + {:ok, float(), context} end # "..." defp of_shared(literal, _stack, context, _fun) when is_binary(literal) do - {:ok, :binary, context} - end - - # <<...>>> - defp of_shared({:<<>>, _meta, args}, stack, context, fun) do - expected_fun = fn arg, _expected, stack, context -> fun.(arg, stack, context) end - - case Of.binary(args, stack, context, expected_fun) do - {:ok, context} -> {:ok, :binary, context} - {:error, reason} -> {:error, reason} - end + {:ok, binary(), context} end # left | [] - defp of_shared({:|, _meta, [left_expr, []]} = expr, stack, context, fun) do - stack = push_expr_stack(expr, stack) + defp of_shared({:|, _meta, [left_expr, []]}, stack, context, fun) do fun.(left_expr, stack, context) end # left | right - defp of_shared({:|, _meta, [left_expr, right_expr]} = expr, stack, context, fun) do - stack = push_expr_stack(expr, stack) - + defp of_shared({:|, _meta, [left_expr, right_expr]}, stack, context, fun) do case fun.(left_expr, stack, context) do - {:ok, left, context} -> - case fun.(right_expr, stack, context) do - {:ok, {:list, right}, context} -> - {:ok, to_union([left, right], context), context} - - {:ok, right, context} -> - {:ok, to_union([left, right], context), context} - - {:error, reason} -> - {:error, reason} - end + {:ok, _, context} -> + fun.(right_expr, stack, context) {:error, reason} -> {:error, reason} @@ -699,43 +123,30 @@ defmodule Module.Types.Pattern do # [] defp of_shared([], _stack, context, _fun) do - {:ok, {:list, :dynamic}, context} + {:ok, empty_list(), context} end # [expr, ...] defp of_shared(exprs, stack, context, fun) when is_list(exprs) do - stack = push_expr_stack(exprs, stack) - case map_reduce_ok(exprs, context, &fun.(&1, stack, &2)) do - {:ok, types, context} -> {:ok, {:list, to_union(types, context)}, context} + {:ok, _types, context} -> {:ok, non_empty_list(), context} {:error, reason} -> {:error, reason} end end # left ++ right defp of_shared( - {{:., _meta1, [:erlang, :++]}, _meta2, [left_expr, right_expr]} = expr, + {{:., _meta1, [:erlang, :++]}, _meta2, [left_expr, right_expr]}, stack, context, fun ) do - stack = push_expr_stack(expr, stack) - - case fun.(left_expr, stack, context) do - {:ok, {:list, left}, context} -> - case fun.(right_expr, stack, context) do - {:ok, {:list, right}, context} -> - {:ok, {:list, to_union([left, right], context)}, context} - - {:ok, right, context} -> - {:ok, {:list, to_union([left, right], context)}, context} - - {:error, reason} -> - {:error, reason} - end - - {:error, reason} -> - {:error, reason} + # The left side is always a list + with {:ok, _, context} <- fun.(left_expr, stack, context), + {:ok, _, context} <- fun.(right_expr, stack, context) do + # TODO: Both lists can be empty, so this may be an empty list, + # so we return dynamic() for now. + {:ok, dynamic(), context} end end @@ -745,31 +156,24 @@ defmodule Module.Types.Pattern do end # {...} - defp of_shared({:{}, _meta, exprs} = expr, stack, context, fun) do - stack = push_expr_stack(expr, stack) - + defp of_shared({:{}, _meta, exprs}, stack, context, fun) do case map_reduce_ok(exprs, context, &fun.(&1, stack, &2)) do - {:ok, types, context} -> {:ok, {:tuple, length(types), types}, context} + {:ok, _, context} -> {:ok, tuple(), context} {:error, reason} -> {:error, reason} end end # %{...} - defp of_shared({:%{}, _meta, args} = expr, stack, context, fun) do - stack = push_expr_stack(expr, stack) - expected_fun = fn arg, _expected, stack, context -> fun.(arg, stack, context) end - Of.open_map(args, stack, context, expected_fun) + defp of_shared({:%{}, _meta, args}, stack, context, fun) do + Of.open_map(args, stack, context, fun) end # %Struct{...} - defp of_shared({:%, meta1, [module, {:%{}, _meta2, args}]} = expr, stack, context, fun) + defp of_shared({:%, meta1, [module, {:%{}, _meta2, args}]}, stack, context, fun) when is_atom(module) do - stack = push_expr_stack(expr, stack) - expected_fun = fn arg, _expected, stack, context -> fun.(arg, stack, context) end - - with {:ok, struct, context} <- Of.struct(module, meta1, context), - {:ok, map, context} <- Of.open_map(args, stack, context, expected_fun) do - unify(map, struct, stack, context) + with {:ok, _, context} <- Of.struct(module, meta1, stack, context), + {:ok, _, context} <- Of.open_map(args, stack, context, fun) do + {:ok, map(), context} end end end diff --git a/lib/elixir/lib/module/types/unify.ex b/lib/elixir/lib/module/types/unify.ex deleted file mode 100644 index 3cb658d8f2..0000000000 --- a/lib/elixir/lib/module/types/unify.ex +++ /dev/null @@ -1,992 +0,0 @@ -defmodule Module.Types.Unify do - @moduledoc false - - import Module.Types.Helpers - - # Those are the simple types known to the system: - # - # :dynamic - # {:var, var} - # {:atom, atom} < :atom - # :integer - # :float - # :binary - # :pid - # :port - # :reference - # - # Those are the composite types: - # - # {:list, type} - # {:tuple, size, [type]} < :tuple - # {:union, [type]} - # {:map, [{:required | :optional, key_type, value_type}]} - # {:fun, [{params, return}]} - # - # Once new types are added, they should be considered in: - # - # * unify (all) - # * format_type (all) - # * subtype? (subtypes only) - # * recursive_type? (composite only) - # * collect_var_indexes (composite only) - # * lift_types (composite only) - # * flatten_union (composite only) - # * walk (composite only) - # - - @doc """ - Unifies two types and returns the unified type and an updated typing context - or an error in case of a typing conflict. - """ - def unify(same, same, _stack, context) do - {:ok, same, context} - end - - def unify({:var, var}, type, stack, context) do - unify_var(var, type, stack, context, _var_source = true) - end - - def unify(type, {:var, var}, stack, context) do - unify_var(var, type, stack, context, _var_source = false) - end - - def unify({:tuple, n, sources}, {:tuple, n, targets}, stack, context) do - result = - map_reduce_ok(Enum.zip(sources, targets), context, fn {source, target}, context -> - unify(source, target, stack, context) - end) - - case result do - {:ok, types, context} -> {:ok, {:tuple, n, types}, context} - {:error, reason} -> {:error, reason} - end - end - - def unify({:list, source}, {:list, target}, stack, context) do - case unify(source, target, stack, context) do - {:ok, type, context} -> {:ok, {:list, type}, context} - {:error, reason} -> {:error, reason} - end - end - - def unify({:map, source_pairs}, {:map, target_pairs}, stack, context) do - unify_maps(source_pairs, target_pairs, stack, context) - end - - def unify(source, :dynamic, _stack, context) do - {:ok, source, context} - end - - def unify(:dynamic, target, _stack, context) do - {:ok, target, context} - end - - def unify({:union, types}, target, stack, context) do - unify_result = - map_reduce_ok(types, context, fn type, context -> - unify(type, target, stack, context) - end) - - case unify_result do - {:ok, types, context} -> {:ok, to_union(types, context), context} - {:error, context} -> {:error, context} - end - end - - def unify(source, target, stack, context) do - cond do - # TODO: This condition exists to handle unions with unbound vars. - match?({:union, _}, target) and has_unbound_var?(target, context) -> - {:ok, source, context} - - subtype?(source, target, context) -> - {:ok, source, context} - - true -> - error(:unable_unify, {source, target, stack}, context) - end - end - - def unify_var(var, :dynamic, _stack, context, _var_source?) do - {:ok, {:var, var}, context} - end - - def unify_var(var, type, stack, context, var_source?) do - case context.types do - %{^var => :unbound} -> - context = refine_var!(var, type, stack, context) - stack = push_unify_stack(var, stack) - - if recursive_type?(type, [], context) do - if var_source? do - error(:unable_unify, {{:var, var}, type, stack}, context) - else - error(:unable_unify, {type, {:var, var}, stack}, context) - end - else - {:ok, {:var, var}, context} - end - - %{^var => {:var, _} = var_type} -> - # Do not recursively traverse type vars for now - # to avoid pathological cases related to performance. - {:ok, var_type, context} - - %{^var => var_type} -> - # Only add trace if the variable wasn't already "expanded" - context = - if variable_expanded?(var, stack, context) do - context - else - trace_var(var, type, stack, context) - end - - stack = push_unify_stack(var, stack) - - unify_result = - if var_source? do - unify(var_type, type, stack, context) - else - unify(type, var_type, stack, context) - end - - case unify_result do - {:ok, {:var, ^var}, context} -> - {:ok, {:var, var}, context} - - {:ok, res_type, context} -> - context = refine_var!(var, res_type, stack, context) - {:ok, {:var, var}, context} - - {:error, reason} -> - {:error, reason} - end - end - end - - # * All required keys on each side need to match to the other side. - # * All optional keys on each side that do not match must be discarded. - - def unify_maps(source_pairs, target_pairs, stack, context) do - {source_required, source_optional} = split_pairs(source_pairs) - {target_required, target_optional} = split_pairs(target_pairs) - - with {:ok, source_required_pairs, context} <- - unify_source_required(source_required, target_pairs, stack, context), - {:ok, target_required_pairs, context} <- - unify_target_required(target_required, source_pairs, stack, context), - {:ok, source_optional_pairs, context} <- - unify_source_optional(source_optional, target_optional, stack, context), - {:ok, target_optional_pairs, context} <- - unify_target_optional(target_optional, source_optional, stack, context) do - # Remove duplicate pairs from matching in both left and right directions - pairs = - Enum.uniq( - source_required_pairs ++ - target_required_pairs ++ - source_optional_pairs ++ - target_optional_pairs - ) - - {:ok, {:map, pairs}, context} - else - {:error, :unify} -> - error(:unable_unify, {{:map, source_pairs}, {:map, target_pairs}, stack}, context) - - {:error, context} -> - {:error, context} - end - end - - def unify_source_required(source_required, target_pairs, stack, context) do - map_reduce_ok(source_required, context, fn {source_key, source_value}, context -> - Enum.find_value(target_pairs, fn {target_kind, target_key, target_value} -> - with {:ok, key, context} <- unify(source_key, target_key, stack, context) do - case unify(source_value, target_value, stack, context) do - {:ok, value, context} -> - {:ok, {:required, key, value}, context} - - {:error, _reason} -> - source_map = {:map, [{:required, source_key, source_value}]} - target_map = {:map, [{target_kind, target_key, target_value}]} - error(:unable_unify, {source_map, target_map, stack}, context) - end - else - {:error, _reason} -> nil - end - end) || {:error, :unify} - end) - end - - def unify_target_required(target_required, source_pairs, stack, context) do - map_reduce_ok(target_required, context, fn {target_key, target_value}, context -> - Enum.find_value(source_pairs, fn {source_kind, source_key, source_value} -> - with {:ok, key, context} <- unify(source_key, target_key, stack, context) do - case unify(source_value, target_value, stack, context) do - {:ok, value, context} -> - {:ok, {:required, key, value}, context} - - {:error, _reason} -> - source_map = {:map, [{source_kind, source_key, source_value}]} - target_map = {:map, [{:required, target_key, target_value}]} - error(:unable_unify, {source_map, target_map, stack}, context) - end - else - {:error, _reason} -> nil - end - end) || {:error, :unify} - end) - end - - def unify_source_optional(source_optional, target_optional, stack, context) do - flat_map_reduce_ok(source_optional, context, fn {source_key, source_value}, context -> - Enum.find_value(target_optional, fn {target_key, target_value} -> - with {:ok, key, context} <- unify(source_key, target_key, stack, context) do - case unify(source_value, target_value, stack, context) do - {:ok, value, context} -> - {:ok, [{:optional, key, value}], context} - - {:error, _reason} -> - source_map = {:map, [{:optional, source_key, source_value}]} - target_map = {:map, [{:optional, target_key, target_value}]} - error(:unable_unify, {source_map, target_map, stack}, context) - end - else - _ -> nil - end - end) || {:ok, [], context} - end) - end - - def unify_target_optional(target_optional, source_optional, stack, context) do - flat_map_reduce_ok(target_optional, context, fn {target_key, target_value}, context -> - Enum.find_value(source_optional, fn {source_key, source_value} -> - with {:ok, key, context} <- unify(source_key, target_key, stack, context) do - case unify(source_value, target_value, stack, context) do - {:ok, value, context} -> - {:ok, [{:optional, key, value}], context} - - {:error, _reason} -> - source_map = {:map, [{:optional, source_key, source_value}]} - target_map = {:map, [{:optional, target_key, target_value}]} - error(:unable_unify, {source_map, target_map, stack}, context) - end - else - _ -> nil - end - end) || {:ok, [], context} - end) - end - - defp split_pairs(pairs) do - {required, optional} = - Enum.split_with(pairs, fn {kind, _key, _value} -> kind == :required end) - - required = Enum.map(required, fn {_kind, key, value} -> {key, value} end) - optional = Enum.map(optional, fn {_kind, key, value} -> {key, value} end) - {required, optional} - end - - def error(type, reason, context), do: {:error, {type, reason, context}} - - @doc """ - Push expression to stack. - - The expression stack is used to give the context where a type variable - was refined when show a type conflict error. - """ - def push_expr_stack(expr, stack) do - %{stack | last_expr: expr} - end - - @doc """ - Gets a variable. - """ - def get_var!(var, context) do - Map.fetch!(context.vars, var_name(var)) - end - - @doc """ - Adds a variable to the typing context and returns its type variable. - If the variable has already been added, return the existing type variable. - """ - def new_var(var, context) do - var_name = var_name(var) - - case context.vars do - %{^var_name => type} -> - {type, context} - - %{} -> - type = {:var, context.counter} - vars = Map.put(context.vars, var_name, type) - types_to_vars = Map.put(context.types_to_vars, context.counter, var) - types = Map.put(context.types, context.counter, :unbound) - traces = Map.put(context.traces, context.counter, []) - - context = %{ - context - | vars: vars, - types_to_vars: types_to_vars, - types: types, - traces: traces, - counter: context.counter + 1 - } - - {type, context} - end - end - - @doc """ - Adds an internal variable to the typing context and returns its type variable. - An internal variable is used to help unify complex expressions, - it does not belong to a specific AST expression. - """ - def add_var(context) do - type = {:var, context.counter} - types = Map.put(context.types, context.counter, :unbound) - traces = Map.put(context.traces, context.counter, []) - - context = %{ - context - | types: types, - traces: traces, - counter: context.counter + 1 - } - - {type, context} - end - - @doc """ - Maybe resolves a variable. - """ - def resolve_var({:var, var}, context) do - case context.types do - %{^var => :unbound} -> {:var, var} - %{^var => type} -> resolve_var(type, context) - end - end - - def resolve_var(other, _context), do: other - - # Check unify stack to see if variable was already expanded - defp variable_expanded?(var, stack, context) do - Enum.any?(stack.unify_stack, &variable_same?(var, &1, context)) - end - - defp variable_same?(left, right, context) do - case context.types do - %{^left => {:var, new_left}} -> - variable_same?(new_left, right, context) - - %{^right => {:var, new_right}} -> - variable_same?(left, new_right, context) - - %{} -> - false - end - end - - defp push_unify_stack(var, stack) do - %{stack | unify_stack: [var | stack.unify_stack]} - end - - @doc """ - Restores the variable information from the old context into new context. - """ - def restore_var!(var, new_context, old_context) do - %{^var => type} = old_context.types - %{^var => trace} = old_context.traces - types = Map.put(new_context.types, var, type) - traces = Map.put(new_context.traces, var, trace) - %{new_context | types: types, traces: traces} - end - - @doc """ - Set the type for a variable and add trace. - """ - def refine_var!(var, type, stack, context) do - types = Map.put(context.types, var, type) - context = %{context | types: types} - trace_var(var, type, stack, context) - end - - @doc """ - Remove type variable and all its traces. - """ - def remove_var(var, context) do - types = Map.delete(context.types, var) - traces = Map.delete(context.traces, var) - %{context | types: types, traces: traces} - end - - defp trace_var(var, type, %{trace: true, last_expr: last_expr} = _stack, context) do - line = get_meta(last_expr)[:line] - trace = {type, last_expr, {context.file, line}} - traces = Map.update!(context.traces, var, &[trace | &1]) - %{context | traces: traces} - end - - defp trace_var(_var, _type, %{trace: false} = _stack, context) do - context - end - - # Check if a variable is recursive and incompatible with itself - # Bad: `{var} = var` - # Good: `x = y; y = z; z = x` - defp recursive_type?({:var, var} = parent, parents, context) do - case context.types do - %{^var => :unbound} -> - false - - %{^var => type} -> - if type in parents do - not Enum.all?(parents, &match?({:var, _}, &1)) - else - recursive_type?(type, [parent | parents], context) - end - end - end - - defp recursive_type?({:list, type} = parent, parents, context) do - recursive_type?(type, [parent | parents], context) - end - - defp recursive_type?({:union, types} = parent, parents, context) do - Enum.any?(types, &recursive_type?(&1, [parent | parents], context)) - end - - defp recursive_type?({:tuple, _, types} = parent, parents, context) do - Enum.any?(types, &recursive_type?(&1, [parent | parents], context)) - end - - defp recursive_type?({:map, pairs} = parent, parents, context) do - Enum.any?(pairs, fn {_kind, key, value} -> - recursive_type?(key, [parent | parents], context) or - recursive_type?(value, [parent | parents], context) - end) - end - - defp recursive_type?({:fun, clauses}, parents, context) do - Enum.any?(clauses, fn {args, return} -> - Enum.any?([return | args], &recursive_type?(&1, [clauses | parents], context)) - end) - end - - defp recursive_type?(_other, _parents, _context) do - false - end - - @doc """ - Collects all type vars recursively. - """ - def collect_var_indexes(type, context, acc \\ %{}) do - {_type, indexes} = - walk(type, acc, fn - {:var, var}, acc -> - case acc do - %{^var => _} -> - {{:var, var}, acc} - - %{} -> - case context.types do - %{^var => :unbound} -> - {{:var, var}, Map.put(acc, var, true)} - - %{^var => type} -> - {{:var, var}, collect_var_indexes(type, context, Map.put(acc, var, true))} - end - end - - other, acc -> - {other, acc} - end) - - indexes - end - - @doc """ - Checks if the type has a type var. - """ - def has_unbound_var?(type, context) do - walk(type, :ok, fn - {:var, var}, acc -> - case context.types do - %{^var => :unbound} -> - throw(:has_unbound_var?) - - %{^var => type} -> - has_unbound_var?(type, context) - {{:var, var}, acc} - end - - other, acc -> - {other, acc} - end) - - false - catch - :throw, :has_unbound_var? -> true - end - - @doc """ - Returns `true` if it is a singleton type. - - Only atoms are singleton types. Unbound vars are not - considered singleton types. - """ - def singleton?({:var, var}, context) do - case context.types do - %{^var => :unbound} -> false - %{^var => type} -> singleton?(type, context) - end - end - - def singleton?({:atom, _}, _context), do: true - def singleton?(_type, _context), do: false - - @doc """ - Checks if the first argument is a subtype of the second argument. - - This function assumes that: - - * unbound variables are not subtype of anything - - * dynamic is not considered a subtype of all other types but the top type. - This allows this function can be used for ordering, in other cases, you - may need to check for both sides - - """ - def subtype?(type, type, _context), do: true - - def subtype?({:var, var}, other, context) do - case context.types do - %{^var => :unbound} -> false - %{^var => type} -> subtype?(type, other, context) - end - end - - def subtype?(other, {:var, var}, context) do - case context.types do - %{^var => :unbound} -> false - %{^var => type} -> subtype?(other, type, context) - end - end - - def subtype?(_, :dynamic, _context), do: true - def subtype?({:atom, atom}, :atom, _context) when is_atom(atom), do: true - - # Composite - - def subtype?({:tuple, _, _}, :tuple, _context), do: true - - def subtype?({:tuple, n, left_types}, {:tuple, n, right_types}, context) do - left_types - |> Enum.zip(right_types) - |> Enum.all?(fn {left, right} -> subtype?(left, right, context) end) - end - - def subtype?({:map, left_pairs}, {:map, right_pairs}, context) do - Enum.all?(left_pairs, fn - {:required, left_key, left_value} -> - Enum.any?(right_pairs, fn {_, right_key, right_value} -> - subtype?(left_key, right_key, context) and subtype?(left_value, right_value, context) - end) - - {:optional, _, _} -> - true - end) - end - - def subtype?({:list, left}, {:list, right}, context) do - subtype?(left, right, context) - end - - def subtype?({:union, left_types}, {:union, _} = right_union, context) do - Enum.all?(left_types, &subtype?(&1, right_union, context)) - end - - def subtype?(left, {:union, right_types}, context) do - Enum.any?(right_types, &subtype?(left, &1, context)) - end - - def subtype?({:union, left_types}, right, context) do - Enum.all?(left_types, &subtype?(&1, right, context)) - end - - def subtype?(_left, _right, _context), do: false - - @doc """ - Returns a "simplified" union using `subtype?/3` to remove redundant types. - - Due to limitations in `subtype?/3` some overlapping types may still be - included. For example unions with overlapping non-concrete types such as - `{boolean()} | {atom()}` will not be merged or types with variables that - are distinct but equivalent such as `a | b when a ~ b`. - """ - def to_union([type], _context), do: type - - def to_union(types, context) when types != [] do - case unique_super_types(unnest_unions(types), context) do - [type] -> type - types -> {:union, types} - end - end - - defp unnest_unions(types) do - Enum.flat_map(types, fn - {:union, types} -> unnest_unions(types) - type -> [type] - end) - end - - # Filter subtypes - # - # `boolean() | atom()` => `atom()` - # `:foo | atom()` => `atom()` - # - # Does not merge `true | false` => `boolean()` - defp unique_super_types([type | types], context) do - types = Enum.reject(types, &subtype?(&1, type, context)) - - if Enum.any?(types, &subtype?(type, &1, context)) do - unique_super_types(types, context) - else - [type | unique_super_types(types, context)] - end - end - - defp unique_super_types([], _context) do - [] - end - - ## Type lifting - - @doc """ - Lifts type variables to their inferred types from the context. - """ - def lift_types(types, %{lifted_types: _} = context) do - Enum.map_reduce(types, context, &lift_type/2) - end - - def lift_types(types, context) do - context = %{ - types: context.types, - lifted_types: %{}, - lifted_counter: 0 - } - - Enum.map_reduce(types, context, &lift_type/2) - end - - # Lift type variable to its inferred (hopefully concrete) types from the context - defp lift_type({:var, var}, context) do - case context.lifted_types do - %{^var => lifted_var} -> - {{:var, lifted_var}, context} - - %{} -> - case context.types do - %{^var => :unbound} -> - new_lifted_var(var, context) - - %{^var => type} -> - if recursive_type?(type, [], context) do - new_lifted_var(var, context) - else - # Remove visited types to avoid infinite loops - # then restore after we are done recursing on vars - types = context.types - context = put_in(context.types[var], :unbound) - {type, context} = lift_type(type, context) - {type, %{context | types: types}} - end - - %{} -> - new_lifted_var(var, context) - end - end - end - - defp lift_type({:union, types}, context) do - {types, context} = Enum.map_reduce(types, context, &lift_type/2) - {{:union, types}, context} - end - - defp lift_type({:tuple, n, types}, context) do - {types, context} = Enum.map_reduce(types, context, &lift_type/2) - {{:tuple, n, types}, context} - end - - defp lift_type({:map, pairs}, context) do - {pairs, context} = - Enum.map_reduce(pairs, context, fn {kind, key, value}, context -> - {key, context} = lift_type(key, context) - {value, context} = lift_type(value, context) - {{kind, key, value}, context} - end) - - {{:map, pairs}, context} - end - - defp lift_type({:list, type}, context) do - {type, context} = lift_type(type, context) - {{:list, type}, context} - end - - defp lift_type({:fun, clauses}, context) do - clauses = - Enum.map_reduce(clauses, context, fn {args, return}, context -> - {[return | args], context} = Enum.map_reduce([return | args], context, &lift_type/2) - {{args, return}, context} - end) - - {{:fun, clauses}, context} - end - - defp lift_type(other, context) do - {other, context} - end - - defp new_lifted_var(original_var, context) do - types = Map.put(context.lifted_types, original_var, context.lifted_counter) - counter = context.lifted_counter + 1 - - type = {:var, context.lifted_counter} - context = %{context | lifted_types: types, lifted_counter: counter} - {type, context} - end - - # TODO: Figure out function expansion - - @doc """ - Expand unions so that all unions are at the top level. - - {integer() | float()} => {integer()} | {float()} - """ - def flatten_union({:union, types}, context) do - Enum.flat_map(types, &flatten_union(&1, context)) - end - - def flatten_union(type, context) do - List.wrap(do_flatten_union(type, context)) - end - - def do_flatten_union({:tuple, num, types}, context) do - flatten_union_tuple(types, num, context, []) - end - - def do_flatten_union({:list, type}, context) do - case do_flatten_union(type, context) do - {:union, union_types} -> Enum.map(union_types, &{:list, &1}) - _type -> [{:list, type}] - end - end - - def do_flatten_union({:map, pairs}, context) do - flatten_union_map(pairs, context, []) - end - - def do_flatten_union({:var, var}, context) do - if looping_var?(var, context, []) do - {:var, var} - else - case context.types do - %{^var => :unbound} -> {:var, var} - %{^var => {:union, types}} -> Enum.map(types, &do_flatten_union(&1, context)) - %{^var => type} -> do_flatten_union(type, context) - end - end - end - - def do_flatten_union(type, _context) do - type - end - - defp flatten_union_tuple([type | types], num, context, acc) do - case do_flatten_union(type, context) do - {:union, union_types} -> - Enum.flat_map(union_types, &flatten_union_tuple(types, num, context, [&1 | acc])) - - type -> - flatten_union_tuple(types, num, context, [type | acc]) - end - end - - defp flatten_union_tuple([], num, _context, acc) do - [{:tuple, num, Enum.reverse(acc)}] - end - - defp flatten_union_map([{kind, key, value} | pairs], context, acc) do - case do_flatten_union(key, context) do - {:union, union_types} -> - Enum.flat_map(union_types, &flatten_union_map_value(kind, &1, value, pairs, context, acc)) - - type -> - flatten_union_map_value(kind, type, value, pairs, context, acc) - end - end - - defp flatten_union_map([], _context, acc) do - [{:map, Enum.reverse(acc)}] - end - - defp flatten_union_map_value(kind, key, value, pairs, context, acc) do - case do_flatten_union(value, context) do - {:union, union_types} -> - Enum.flat_map(union_types, &flatten_union_map(pairs, context, [{kind, key, &1} | acc])) - - value -> - flatten_union_map(pairs, context, [{kind, key, value} | acc]) - end - end - - defp looping_var?(var, context, parents) do - case context.types do - %{^var => :unbound} -> - false - - %{^var => {:var, type}} -> - if var in parents do - true - else - looping_var?(type, context, [var | parents]) - end - - %{^var => _type} -> - false - end - end - - @doc """ - Formats types. - - The second argument says when complex types such as maps and - structs should be simplified and not shown. - """ - def format_type({:map, pairs}, true) do - case List.keyfind(pairs, {:atom, :__struct__}, 1) do - {:required, {:atom, :__struct__}, {:atom, struct}} -> - ["%", inspect(struct), "{}"] - - _ -> - "map()" - end - end - - def format_type({:union, types}, simplify?) do - types - |> Enum.map(&format_type(&1, simplify?)) - |> Enum.intersperse(" | ") - end - - def format_type({:tuple, _, types}, simplify?) do - format = - types - |> Enum.map(&format_type(&1, simplify?)) - |> Enum.intersperse(", ") - - ["{", format, "}"] - end - - def format_type({:list, type}, simplify?) do - ["[", format_type(type, simplify?), "]"] - end - - def format_type({:map, pairs}, false) do - case List.keytake(pairs, {:atom, :__struct__}, 1) do - {{:required, {:atom, :__struct__}, {:atom, struct}}, pairs} -> - ["%", inspect(struct), "{", format_map_pairs(pairs), "}"] - - _ -> - ["%{", format_map_pairs(pairs), "}"] - end - end - - def format_type({:atom, literal}, _simplify?) do - inspect(literal) - end - - def format_type({:var, index}, _simplify?) do - ["var", Integer.to_string(index + 1)] - end - - def format_type({:fun, clauses}, simplify?) do - format = - Enum.map(clauses, fn {params, return} -> - params = Enum.intersperse(Enum.map(params, &format_type(&1, simplify?)), ", ") - params = if params == [], do: params, else: [params, " "] - return = format_type(return, simplify?) - [params, "-> ", return] - end) - - ["(", Enum.intersperse(format, "; "), ")"] - end - - def format_type(atom, _simplify?) when is_atom(atom) do - [Atom.to_string(atom), "()"] - end - - defp format_map_pairs(pairs) do - {atoms, others} = Enum.split_with(pairs, &match?({:required, {:atom, _}, _}, &1)) - {required, optional} = Enum.split_with(others, &match?({:required, _, _}, &1)) - - (atoms ++ required ++ optional) - |> Enum.map(fn - {:required, {:atom, atom}, right} -> - [Atom.to_string(atom), ": ", format_type(right, false)] - - {:required, left, right} -> - [format_type(left, false), " => ", format_type(right, false)] - - {:optional, left, right} -> - ["optional(", format_type(left, false), ") => ", format_type(right, false)] - end) - |> Enum.intersperse(", ") - end - - @doc """ - Performs a depth-first, pre-order traversal of the type tree using an accumulator. - """ - def walk({:map, pairs}, acc, fun) do - {pairs, acc} = - Enum.map_reduce(pairs, acc, fn {kind, key, value}, acc -> - {key, acc} = walk(key, acc, fun) - {value, acc} = walk(value, acc, fun) - {{kind, key, value}, acc} - end) - - fun.({:map, pairs}, acc) - end - - def walk({:union, types}, acc, fun) do - {types, acc} = Enum.map_reduce(types, acc, &walk(&1, &2, fun)) - fun.({:union, types}, acc) - end - - def walk({:tuple, num, types}, acc, fun) do - {types, acc} = Enum.map_reduce(types, acc, &walk(&1, &2, fun)) - fun.({:tuple, num, types}, acc) - end - - def walk({:list, type}, acc, fun) do - {type, acc} = walk(type, acc, fun) - fun.({:list, type}, acc) - end - - def walk({:fun, clauses}, acc, fun) do - {clauses, acc} = - Enum.map_reduce(clauses, acc, fn {params, return}, acc -> - {params, acc} = Enum.map_reduce(params, acc, &walk(&1, &2, fun)) - {return, acc} = walk(return, acc, fun) - {{params, return}, acc} - end) - - fun.({:fun, clauses}, acc) - end - - def walk(type, acc, fun) do - fun.(type, acc) - end -end diff --git a/lib/elixir/src/elixir_compiler.erl b/lib/elixir/src/elixir_compiler.erl index 980aa9e0e8..fb4963a4ca 100644 --- a/lib/elixir/src/elixir_compiler.erl +++ b/lib/elixir/src/elixir_compiler.erl @@ -185,9 +185,9 @@ bootstrap_files() -> <<"list/chars.ex">>, <<"module/locals_tracker.ex">>, <<"module/parallel_checker.ex">>, - <<"module/types/behaviour.ex">>, + <<"module/behaviour.ex">>, <<"module/types/helpers.ex">>, - <<"module/types/unify.ex">>, + <<"module/types/descr.ex">>, <<"module/types/of.ex">>, <<"module/types/pattern.ex">>, <<"module/types/expr.ex">>, diff --git a/lib/elixir/test/elixir/code_test.exs b/lib/elixir/test/elixir/code_test.exs index 2fc391c908..3a7db18335 100644 --- a/lib/elixir/test/elixir/code_test.exs +++ b/lib/elixir/test/elixir/code_test.exs @@ -211,30 +211,6 @@ defmodule CodeTest do end) =~ "an __ENV__ with outdated compilation information was given to eval" end - test "emits checker warnings" do - output = - ExUnit.CaptureIO.capture_io(:stderr, fn -> - Code.eval_string(File.read!(fixture_path("checker_warning.exs")), []) - end) - - assert output =~ "incompatible types" - after - :code.purge(CodeTest.CheckerWarning) - :code.delete(CodeTest.CheckerWarning) - end - - test "captures checker diagnostics" do - {{{:module, _, _, _}, _}, diagnostics} = - Code.with_diagnostics(fn -> - Code.eval_string(File.read!(fixture_path("checker_warning.exs")), []) - end) - - assert [%{message: "incompatible types:" <> _}] = diagnostics - after - :code.purge(CodeTest.CheckerWarning) - :code.delete(CodeTest.CheckerWarning) - end - test "formats diagnostic file paths as relatives" do {_, diagnostics} = Code.with_diagnostics(fn -> @@ -398,47 +374,6 @@ defmodule CodeTest do assert Code.compile_file(fixture_path("code_sample.exs")) == [] refute fixture_path("code_sample.exs") in Code.required_files() end - - test "emits checker warnings" do - output = - ExUnit.CaptureIO.capture_io(:stderr, fn -> - Code.compile_file(fixture_path("checker_warning.exs")) - end) - - assert output =~ "incompatible types" - after - :code.purge(CodeTest.CheckerWarning) - :code.delete(CodeTest.CheckerWarning) - end - - test "captures checker diagnostics" do - {[{CodeTest.CheckerWarning, _}], diagnostics} = - Code.with_diagnostics(fn -> - Code.compile_file(fixture_path("checker_warning.exs")) - end) - - assert [%{message: "incompatible types:" <> _}] = diagnostics - after - :code.purge(CodeTest.CheckerWarning) - :code.delete(CodeTest.CheckerWarning) - end - - test "captures checker diagnostics with logging" do - output = - ExUnit.CaptureIO.capture_io(:stderr, fn -> - {[{CodeTest.CheckerWarning, _}], diagnostics} = - Code.with_diagnostics([log: true], fn -> - Code.compile_file(fixture_path("checker_warning.exs")) - end) - - assert [%{message: "incompatible types:" <> _}] = diagnostics - end) - - assert output =~ "incompatible types" - after - :code.purge(CodeTest.CheckerWarning) - :code.delete(CodeTest.CheckerWarning) - end end test "require_file/1" do @@ -522,18 +457,6 @@ defmodule CodeTest do :code.delete(CompileSimpleSample) end - test "emits checker warnings" do - output = - ExUnit.CaptureIO.capture_io(:stderr, fn -> - Code.compile_string(File.read!(fixture_path("checker_warning.exs"))) - end) - - assert output =~ "incompatible types" - after - :code.purge(CodeTest.CheckerWarning) - :code.delete(CodeTest.CheckerWarning) - end - test "works across lexical scopes" do assert [{CompileCrossSample, _}] = Code.compile_string("CodeTest.genmodule CompileCrossSample") diff --git a/lib/elixir/test/elixir/fixtures/checker_warning.exs b/lib/elixir/test/elixir/fixtures/checker_warning.exs deleted file mode 100644 index a9c0cab9fc..0000000000 --- a/lib/elixir/test/elixir/fixtures/checker_warning.exs +++ /dev/null @@ -1,3 +0,0 @@ -defmodule CodeTest.CheckerWarning do - def foo(x) when is_atom(x) and is_list(x), do: x -end diff --git a/lib/elixir/test/elixir/module/types/expr_test.exs b/lib/elixir/test/elixir/module/types/expr_test.exs index 8a32975b2e..12c0763968 100644 --- a/lib/elixir/test/elixir/module/types/expr_test.exs +++ b/lib/elixir/test/elixir/module/types/expr_test.exs @@ -4,321 +4,18 @@ defmodule Module.Types.ExprTest do use ExUnit.Case, async: true import TypeHelper - - defmodule :"Elixir.Module.Types.ExprTest.Struct" do - defstruct foo: :atom, bar: 123, baz: %{} - end + import Module.Types.Descr test "literal" do - assert quoted_expr(true) == {:ok, {:atom, true}} - assert quoted_expr(false) == {:ok, {:atom, false}} - assert quoted_expr(:foo) == {:ok, {:atom, :foo}} - assert quoted_expr(0) == {:ok, :integer} - assert quoted_expr(0.0) == {:ok, :float} - assert quoted_expr("foo") == {:ok, :binary} - end - - describe "list" do - test "proper" do - assert quoted_expr([]) == {:ok, {:list, :dynamic}} - assert quoted_expr([123]) == {:ok, {:list, :integer}} - assert quoted_expr([123, 456]) == {:ok, {:list, :integer}} - assert quoted_expr([123 | []]) == {:ok, {:list, :integer}} - assert quoted_expr([123, "foo"]) == {:ok, {:list, {:union, [:integer, :binary]}}} - assert quoted_expr([123 | ["foo"]]) == {:ok, {:list, {:union, [:integer, :binary]}}} - end - - test "improper" do - assert quoted_expr([123 | 456]) == {:ok, {:list, :integer}} - assert quoted_expr([123, 456 | 789]) == {:ok, {:list, :integer}} - assert quoted_expr([123 | "foo"]) == {:ok, {:list, {:union, [:integer, :binary]}}} - end - - test "keyword" do - assert quoted_expr(a: 1, b: 2) == - {:ok, - {:list, - {:union, - [ - {:tuple, 2, [{:atom, :a}, :integer]}, - {:tuple, 2, [{:atom, :b}, :integer]} - ]}}} - end - end - - test "tuple" do - assert quoted_expr({}) == {:ok, {:tuple, 0, []}} - assert quoted_expr({:a}) == {:ok, {:tuple, 1, [{:atom, :a}]}} - assert quoted_expr({:a, 123}) == {:ok, {:tuple, 2, [{:atom, :a}, :integer]}} - end - - describe "binary" do - test "literal" do - assert quoted_expr(<<"foo"::binary>>) == {:ok, :binary} - assert quoted_expr(<<123::integer>>) == {:ok, :binary} - assert quoted_expr(<<123::utf8>>) == {:ok, :binary} - assert quoted_expr(<<"foo"::utf8>>) == {:ok, :binary} - end - - defmacrop custom_type do - quote do: 1 * 8 - big - signed - integer - end - - test "variable" do - assert quoted_expr([foo], <>) == {:ok, :binary} - assert quoted_expr([foo], <>) == {:ok, :binary} - assert quoted_expr([foo], <>) == {:ok, :binary} - assert quoted_expr([foo], <>) == {:ok, :binary} - assert quoted_expr([foo], <>) == {:ok, :binary} - end - - test "infer" do - assert quoted_expr( - ( - foo = 0.0 - <> - ) - ) == {:ok, :binary} - - assert quoted_expr( - ( - foo = 0 - <> - ) - ) == {:ok, :binary} - - assert quoted_expr([foo], {<>, foo}) == - {:ok, {:tuple, 2, [:binary, :integer]}} - - assert quoted_expr([foo], {<>, foo}) == {:ok, {:tuple, 2, [:binary, :binary]}} - - assert quoted_expr([foo], {<>, foo}) == - {:ok, {:tuple, 2, [:binary, {:union, [:integer, :binary]}]}} - - assert {:error, {:unable_unify, {:integer, :binary, _}}} = - quoted_expr( - ( - foo = 0 - <> - ) - ) - - assert {:error, {:unable_unify, {:binary, :integer, _}}} = - quoted_expr([foo], <>) - end - end - - test "variables" do - assert quoted_expr([foo], foo) == {:ok, {:var, 0}} - assert quoted_expr([foo], {foo}) == {:ok, {:tuple, 1, [{:var, 0}]}} - assert quoted_expr([foo, bar], {foo, bar}) == {:ok, {:tuple, 2, [{:var, 0}, {:var, 1}]}} - end - - test "pattern match" do - assert {:error, _} = quoted_expr(:foo = 1) - assert {:error, _} = quoted_expr(1 = :foo) - - assert quoted_expr(:foo = :foo) == {:ok, {:atom, :foo}} - assert quoted_expr(1 = 1) == {:ok, :integer} - end - - test "block" do - assert quoted_expr( - ( - a = 1 - a - ) - ) == {:ok, :integer} - - assert quoted_expr( - ( - a = :foo - a - ) - ) == {:ok, {:atom, :foo}} - - assert {:error, _} = - quoted_expr( - ( - a = 1 - :foo = a - ) - ) - end - - describe "case" do - test "infer pattern" do - assert quoted_expr( - [a], - case a do - :foo = b -> :foo = b - end - ) == {:ok, :dynamic} - - assert {:error, _} = - quoted_expr( - [a], - case a do - :foo = b -> :bar = b - end - ) - end - - test "do not leak pattern/guard inference between clauses" do - assert quoted_expr( - [a], - case a do - :foo = b -> b - :bar = b -> b - end - ) == {:ok, :dynamic} - - assert quoted_expr( - [a], - case a do - b when is_atom(b) -> b - b when is_integer(b) -> b - end - ) == {:ok, :dynamic} - - assert quoted_expr( - [a], - case a do - :foo = b -> :foo = b - :bar = b -> :bar = b - end - ) == {:ok, :dynamic} - end - - test "do not leak body inference between clauses" do - assert quoted_expr( - [a], - case a do - :foo -> - b = :foo - b - - :bar -> - b = :bar - b - end - ) == {:ok, :dynamic} - - assert quoted_expr( - [a, b], - case a do - :foo -> :foo = b - :bar -> :bar = b - end - ) == {:ok, :dynamic} - - assert quoted_expr( - [a, b], - case a do - :foo when is_binary(b) -> b <> "" - :foo when is_list(b) -> b - end - ) == {:ok, :dynamic} - end - end - - describe "cond" do - test "do not leak body inference between clauses" do - assert quoted_expr( - [], - cond do - 1 -> - b = :foo - b - - 2 -> - b = :bar - b - end - ) == {:ok, :dynamic} - - assert quoted_expr( - [b], - cond do - 1 -> :foo = b - 2 -> :bar = b - end - ) == {:ok, :dynamic} - end - end - - test "fn" do - assert quoted_expr(fn :foo = b -> :foo = b end) == {:ok, :dynamic} - - assert {:error, _} = quoted_expr(fn :foo = b -> :bar = b end) - end - - test "receive" do - assert quoted_expr( - receive do - after - 0 -> :ok - end - ) == {:ok, :dynamic} - end - - test "with" do - assert quoted_expr( - [a, b], - with( - :foo <- a, - :bar <- b, - c = :baz, - do: c - ) - ) == {:ok, :dynamic} - - assert quoted_expr( - [a], - ( - with(a = :baz, do: a) - a - ) - ) == {:ok, {:var, 0}} - end - - describe "for comprehension" do - test "with generators and filters" do - assert quoted_expr( - [list], - for( - foo <- list, - is_integer(foo), - do: foo == 123 - ) - ) == {:ok, :dynamic} - end - - test "with unused return" do - assert quoted_expr( - [list, bar], - ( - for( - foo <- list, - is_integer(bar), - do: foo == 123 - ) - - bar - ) - ) == {:ok, {:var, 0}} - end - - test "with reduce" do - assert quoted_expr( - [], - for(i <- [1, 2, 3], do: (acc -> i + acc), reduce: 0) - ) == {:ok, :dynamic} - - assert quoted_expr( - [], - for(i <- [1, 2, 3], do: (_ -> i), reduce: nil) - ) == {:ok, :dynamic} - end + assert typecheck!(true) == atom(true) + assert typecheck!(false) == atom(false) + assert typecheck!(:foo) == atom(:foo) + assert typecheck!(0) == integer() + assert typecheck!(0.0) == float() + assert typecheck!("foo") == binary() + assert typecheck!([]) == empty_list() + assert typecheck!([1, 2]) == non_empty_list() + assert typecheck!({1, 2}) == tuple() + assert typecheck!(%{}) == map() end end diff --git a/lib/elixir/test/elixir/module/types/integration_test.exs b/lib/elixir/test/elixir/module/types/integration_test.exs index ee8cdca18b..3c5944e18e 100644 --- a/lib/elixir/test/elixir/module/types/integration_test.exs +++ b/lib/elixir/test/elixir/module/types/integration_test.exs @@ -521,20 +521,6 @@ defmodule Module.Types.IntegrationTest do end describe "regressions" do - test "handle missing location info from quoted" do - assert capture_io(:stderr, fn -> - quote do - defmodule X do - def f() do - x = %{} - %{x | key: :value} - end - end - end - |> Code.compile_quoted() - end) =~ "warning:" - end - test "do not parse binary segments as variables" do files = %{ "a.ex" => """ diff --git a/lib/elixir/test/elixir/module/types/map_test.exs b/lib/elixir/test/elixir/module/types/map_test.exs deleted file mode 100644 index da6818f2e5..0000000000 --- a/lib/elixir/test/elixir/module/types/map_test.exs +++ /dev/null @@ -1,377 +0,0 @@ -Code.require_file("type_helper.exs", __DIR__) - -defmodule Module.Types.MapTest do - # This file holds cases for maps and structs. - use ExUnit.Case, async: true - - import TypeHelper - - defmodule :"Elixir.Module.Types.MapTest.Struct" do - defstruct foo: :atom, bar: 123, baz: %{} - end - - test "map" do - assert quoted_expr(%{}) == {:ok, {:map, []}} - assert quoted_expr(%{a: :b}) == {:ok, {:map, [{:required, {:atom, :a}, {:atom, :b}}]}} - assert quoted_expr([a], %{123 => a}) == {:ok, {:map, [{:required, :integer, {:var, 0}}]}} - - assert quoted_expr(%{123 => :foo, 456 => :bar}) == - {:ok, {:map, [{:required, :integer, {:union, [{:atom, :foo}, {:atom, :bar}]}}]}} - end - - test "struct" do - assert quoted_expr(%:"Elixir.Module.Types.MapTest.Struct"{}) == - {:ok, - {:map, - [ - {:required, {:atom, :bar}, :integer}, - {:required, {:atom, :baz}, {:map, []}}, - {:required, {:atom, :foo}, {:atom, :atom}}, - {:required, {:atom, :__struct__}, {:atom, Module.Types.MapTest.Struct}} - ]}} - - assert quoted_expr(%:"Elixir.Module.Types.MapTest.Struct"{foo: 123, bar: :atom}) == - {:ok, - {:map, - [ - {:required, {:atom, :baz}, {:map, []}}, - {:required, {:atom, :foo}, :integer}, - {:required, {:atom, :bar}, {:atom, :atom}}, - {:required, {:atom, :__struct__}, {:atom, Module.Types.MapTest.Struct}} - ]}} - end - - test "map field" do - assert quoted_expr(%{foo: :bar}.foo) == {:ok, {:atom, :bar}} - - assert quoted_expr( - ( - map = %{foo: :bar} - map.foo - ) - ) == {:ok, {:atom, :bar}} - - assert quoted_expr( - [map], - ( - map.foo - map.bar - map - ) - ) == - {:ok, - {:map, - [ - {:required, {:atom, :bar}, {:var, 0}}, - {:required, {:atom, :foo}, {:var, 1}}, - {:optional, :dynamic, :dynamic} - ]}} - - assert quoted_expr( - [map], - ( - :foo = map.foo - :bar = map.bar - map - ) - ) == - {:ok, - {:map, - [ - {:required, {:atom, :bar}, {:atom, :bar}}, - {:required, {:atom, :foo}, {:atom, :foo}}, - {:optional, :dynamic, :dynamic} - ]}} - - assert {:error, - {:unable_unify, - {{:map, [{:required, {:atom, :bar}, {:var, 1}}, {:optional, :dynamic, :dynamic}]}, - {:map, [{:required, {:atom, :foo}, {:atom, :foo}}]}, - _}}} = - quoted_expr( - ( - map = %{foo: :foo} - map.bar - ) - ) - end - - defmodule :"Elixir.Module.Types.MapTest.Struct2" do - defstruct [:field] - end - - test "map and struct fields" do - assert quoted_expr( - [map], - ( - %Module.Types.MapTest.Struct2{} = map - map.field - map - ) - ) == - {:ok, - {:map, - [ - {:required, {:atom, :field}, {:var, 0}}, - {:required, {:atom, :__struct__}, {:atom, Module.Types.MapTest.Struct2}} - ]}} - - assert quoted_expr( - [map], - ( - _ = map.field - %Module.Types.MapTest.Struct2{} = map - ) - ) == - {:ok, - {:map, - [ - {:required, {:atom, :field}, {:var, 0}}, - {:required, {:atom, :__struct__}, {:atom, Module.Types.MapTest.Struct2}} - ]}} - - assert {:error, {:unable_unify, {_, _, _}}} = - quoted_expr( - [map], - ( - %Module.Types.MapTest.Struct2{} = map - map.no_field - ) - ) - - assert {:error, {:unable_unify, {_, _, _}}} = - quoted_expr( - [map], - ( - _ = map.no_field - %Module.Types.MapTest.Struct2{} = map - ) - ) - end - - test "map pattern" do - assert quoted_expr(%{a: :b} = %{a: :b}) == - {:ok, {:map, [{:required, {:atom, :a}, {:atom, :b}}]}} - - assert quoted_expr( - ( - a = :a - %{^a => :b} = %{:a => :b} - ) - ) == {:ok, {:map, [{:required, {:atom, :a}, {:atom, :b}}]}} - - assert quoted_expr( - ( - a = :a - %{{^a, :b} => :c} = %{{:a, :b} => :c} - ) - ) == {:ok, {:map, [{:required, {:tuple, 2, [{:atom, :a}, {:atom, :b}]}, {:atom, :c}}]}} - - assert {:error, - {:unable_unify, - {{:map, [{:required, {:atom, :c}, {:atom, :d}}]}, - {:map, [{:required, {:atom, :a}, {:atom, :b}}, {:optional, :dynamic, :dynamic}]}, - _}}} = quoted_expr(%{a: :b} = %{c: :d}) - - assert {:error, - {:unable_unify, - {{:map, [{:required, {:atom, :b}, {:atom, :error}}]}, - {:map, [{:required, {:var, 0}, {:atom, :ok}}, {:optional, :dynamic, :dynamic}]}, - _}}} = - quoted_expr( - ( - a = :a - %{^a => :ok} = %{:b => :error} - ) - ) - end - - test "map update" do - assert quoted_expr( - ( - map = %{foo: :a} - %{map | foo: :b} - ) - ) == - {:ok, {:map, [{:required, {:atom, :foo}, {:atom, :b}}]}} - - assert quoted_expr([map], %{map | foo: :b}) == - {:ok, - {:map, [{:required, {:atom, :foo}, {:atom, :b}}, {:optional, :dynamic, :dynamic}]}} - - assert {:error, - {:unable_unify, - {{:map, [{:required, {:atom, :foo}, {:atom, :a}}]}, - {:map, [{:required, {:atom, :bar}, :dynamic}, {:optional, :dynamic, :dynamic}]}, - _}}} = - quoted_expr( - ( - map = %{foo: :a} - %{map | bar: :b} - ) - ) - end - - test "struct update" do - assert quoted_expr( - ( - map = %Module.Types.MapTest.Struct2{field: :a} - %Module.Types.MapTest.Struct2{map | field: :b} - ) - ) == - {:ok, - {:map, - [ - {:required, {:atom, :field}, {:atom, :b}}, - {:required, {:atom, :__struct__}, {:atom, Module.Types.MapTest.Struct2}} - ]}} - - # TODO: improve error message to translate to MULTIPLE missing fields - assert {:error, - {:unable_unify, - {{:map, - [ - {:required, {:atom, :foo}, {:var, 1}}, - {:required, {:atom, :field}, {:atom, :b}}, - {:optional, :dynamic, :dynamic} - ]}, - {:map, - [ - {:required, {:atom, :__struct__}, {:atom, Module.Types.MapTest.Struct2}}, - {:required, {:atom, :field}, :dynamic} - ]}, - _}}} = - quoted_expr( - [map], - ( - _ = map.foo - %Module.Types.MapTest.Struct2{map | field: :b} - ) - ) - - assert {:error, - {:unable_unify, - {{:map, [{:required, {:atom, :field}, {:atom, :b}}]}, - {:map, - [ - {:required, {:atom, :__struct__}, {:atom, Module.Types.MapTest.Struct2}}, - {:required, {:atom, :field}, :dynamic} - ]}, - _}}} = - quoted_expr( - ( - map = %{field: :a} - %Module.Types.MapTest.Struct2{map | field: :b} - ) - ) - - assert quoted_expr([map], %Module.Types.MapTest.Struct2{map | field: :b}) == - {:ok, - {:map, - [ - {:required, {:atom, :field}, {:atom, :b}}, - {:required, {:atom, :__struct__}, {:atom, Module.Types.MapTest.Struct2}} - ]}} - - assert {:error, - {:unable_unify, - {{:map, - [ - {:required, {:atom, :field}, {:atom, nil}}, - {:required, {:atom, :__struct__}, {:atom, Module.Types.MapTest.Struct2}} - ]}, - {:map, - [{:required, {:atom, :not_field}, :dynamic}, {:optional, :dynamic, :dynamic}]}, - _}}} = - quoted_expr( - ( - map = %Module.Types.MapTest.Struct2{} - %{map | not_field: :b} - ) - ) - end - - describe "in guards" do - test "not is_struct/2" do - assert quoted_expr([var], [not is_struct(var, URI)], var.name) == {:ok, {:var, 0}} - end - - test "map guards" do - assert quoted_expr([var], [is_map(var)], var.foo) == {:ok, {:var, 0}} - assert quoted_expr([var], [is_map_key(var, :bar)], var.foo) == {:ok, {:var, 0}} - assert quoted_expr([var], [:erlang.map_get(:bar, var)], var.foo) == {:ok, {:var, 0}} - assert quoted_expr([var], [map_size(var) == 1], var.foo) == {:ok, {:var, 0}} - end - end - - test "map creation with bound var keys" do - assert quoted_expr( - [atom, bool, true = var], - [is_atom(atom) and is_boolean(bool)], - %{atom => :atom, bool => :bool, var => true} - ) == - {:ok, - {:map, - [ - {:required, {:atom, true}, {:atom, true}}, - {:required, {:union, [atom: true, atom: false]}, - {:union, [{:atom, :bool}, {:atom, true}]}}, - {:required, :atom, {:union, [{:atom, :atom}, {:atom, :bool}, {:atom, true}]}} - ]}} - - assert quoted_expr( - [atom, bool, true = var], - [is_atom(atom) and is_boolean(bool)], - %{var => true, bool => :bool, atom => :atom} - ) == - {:ok, - {:map, - [ - {:required, {:atom, true}, {:atom, true}}, - {:required, {:union, [atom: true, atom: false]}, - {:union, [{:atom, :bool}, {:atom, true}]}}, - {:required, :atom, {:union, [{:atom, :atom}, {:atom, :bool}, {:atom, true}]}} - ]}} - - assert quoted_expr( - [atom, bool, true = var], - [is_atom(atom) and is_boolean(bool)], - %{var => true, atom => :atom, bool => :bool} - ) == - {:ok, - {:map, - [ - {:required, {:atom, true}, {:atom, true}}, - {:required, {:union, [atom: true, atom: false]}, - {:union, [{:atom, :bool}, {:atom, true}]}}, - {:required, :atom, {:union, [{:atom, :atom}, {:atom, :bool}, {:atom, true}]}} - ]}} - end - - test "map creation with unbound var keys" do - assert quoted_expr( - [var, struct], - ( - map = %{var => :foo} - %^var{} = struct - map - ) - ) == {:ok, {:map, [{:required, :atom, {:atom, :foo}}]}} - - # If we have multiple keys, the unbound key must become required(dynamic) => dynamic - assert quoted_expr( - [var, struct], - ( - map = %{var => :foo, :foo => :bar} - %^var{} = struct - map - ) - ) == - {:ok, - {:map, - [ - {:required, {:atom, :foo}, {:atom, :bar}}, - {:required, :dynamic, :dynamic} - ]}} - end -end diff --git a/lib/elixir/test/elixir/module/types/pattern_test.exs b/lib/elixir/test/elixir/module/types/pattern_test.exs deleted file mode 100644 index 70e7ca108d..0000000000 --- a/lib/elixir/test/elixir/module/types/pattern_test.exs +++ /dev/null @@ -1,542 +0,0 @@ -Code.require_file("type_helper.exs", __DIR__) - -defmodule Module.Types.PatternTest do - use ExUnit.Case, async: true - - alias Module.Types - alias Module.Types.{Unify, Pattern} - - defmacrop quoted_pattern(patterns) do - quote do - {patterns, true} = unquote(Macro.escape(expand_head(patterns, true))) - - Pattern.of_pattern(patterns, new_stack(), new_context()) - |> lift_result() - end - end - - defmacrop quoted_pattern_with_diagnostics(patterns) do - {ast, diagnostics} = Code.with_diagnostics(fn -> expand_head(patterns, true) end) - - quote do - {patterns, true} = unquote(Macro.escape(ast)) - - result = - Pattern.of_pattern(patterns, new_stack(), new_context()) - |> lift_result() - - {result, unquote(Macro.escape(diagnostics))} - end - end - - defmacrop quoted_head(patterns, guards \\ []) do - quote do - {patterns, guards} = unquote(Macro.escape(expand_head(patterns, guards))) - - Pattern.of_head(patterns, guards, new_stack(), new_context()) - |> lift_result() - end - end - - defp expand_head(patterns, guards) do - fun = - quote do - fn unquote(patterns) when unquote(guards) -> :ok end - end - - fun = - Macro.prewalk(fun, fn - {var, meta, nil} -> {var, meta, __MODULE__} - other -> other - end) - - {ast, _, _} = :elixir_expand.expand(fun, :elixir_env.env_to_ex(__ENV__), __ENV__) - {:fn, _, [{:->, _, [[{:when, _, [patterns, guards]}], _]}]} = ast - {patterns, guards} - end - - defp new_context() do - Types.context("types_test.ex", TypesTest, {:test, 0}, [], Module.ParallelChecker.test_cache()) - end - - defp new_stack() do - %{ - Types.stack() - | last_expr: {:foo, [], nil} - } - end - - defp lift_result({:ok, types, context}) when is_list(types) do - {types, _context} = Unify.lift_types(types, context) - {:ok, types} - end - - defp lift_result({:ok, type, context}) do - {[type], _context} = Unify.lift_types([type], context) - {:ok, type} - end - - defp lift_result({:error, {type, reason, _context}}) do - {:error, {type, reason}} - end - - defmodule :"Elixir.Module.Types.PatternTest.Struct" do - defstruct foo: :atom, bar: 123, baz: %{} - end - - describe "patterns" do - test "literal" do - assert quoted_pattern(true) == {:ok, {:atom, true}} - assert quoted_pattern(false) == {:ok, {:atom, false}} - assert quoted_pattern(:foo) == {:ok, {:atom, :foo}} - assert quoted_pattern(0) == {:ok, :integer} - assert quoted_pattern(+0.0) == {:ok, :float} - assert quoted_pattern(-0.0) == {:ok, :float} - assert quoted_pattern("foo") == {:ok, :binary} - - assert {{:ok, :float}, [diagnostic]} = quoted_pattern_with_diagnostics(0.0) - - assert diagnostic.message =~ - "pattern matching on 0.0 is equivalent to matching only on +0.0" - end - - test "list" do - assert quoted_pattern([]) == {:ok, {:list, :dynamic}} - assert quoted_pattern([_]) == {:ok, {:list, :dynamic}} - assert quoted_pattern([123]) == {:ok, {:list, :integer}} - assert quoted_pattern([123, 456]) == {:ok, {:list, :integer}} - assert quoted_pattern([123, _]) == {:ok, {:list, :dynamic}} - assert quoted_pattern([_, 456]) == {:ok, {:list, :dynamic}} - assert quoted_pattern([123 | []]) == {:ok, {:list, :integer}} - assert quoted_pattern([123, "foo"]) == {:ok, {:list, {:union, [:integer, :binary]}}} - assert quoted_pattern([123 | ["foo"]]) == {:ok, {:list, {:union, [:integer, :binary]}}} - - # TODO: improper list? - assert quoted_pattern([123 | 456]) == {:ok, {:list, :integer}} - assert quoted_pattern([123, 456 | 789]) == {:ok, {:list, :integer}} - assert quoted_pattern([123 | "foo"]) == {:ok, {:list, {:union, [:integer, :binary]}}} - assert quoted_pattern([123 | _]) == {:ok, {:list, :dynamic}} - assert quoted_pattern([_ | [456]]) == {:ok, {:list, :dynamic}} - assert quoted_pattern([_ | _]) == {:ok, {:list, :dynamic}} - - assert quoted_pattern([] ++ []) == {:ok, {:list, :dynamic}} - assert quoted_pattern([_] ++ _) == {:ok, {:list, :dynamic}} - assert quoted_pattern([123] ++ [456]) == {:ok, {:list, :integer}} - assert quoted_pattern([123] ++ _) == {:ok, {:list, :dynamic}} - assert quoted_pattern([123] ++ ["foo"]) == {:ok, {:list, {:union, [:integer, :binary]}}} - end - - test "tuple" do - assert quoted_pattern({}) == {:ok, {:tuple, 0, []}} - assert quoted_pattern({:a}) == {:ok, {:tuple, 1, [{:atom, :a}]}} - assert quoted_pattern({:a, 123}) == {:ok, {:tuple, 2, [{:atom, :a}, :integer]}} - end - - test "map" do - assert quoted_pattern(%{}) == {:ok, {:map, [{:optional, :dynamic, :dynamic}]}} - - assert quoted_pattern(%{a: :b}) == - {:ok, - {:map, [{:required, {:atom, :a}, {:atom, :b}}, {:optional, :dynamic, :dynamic}]}} - - assert quoted_pattern(%{123 => a}) == - {:ok, - {:map, - [ - {:required, :integer, {:var, 0}}, - {:optional, :dynamic, :dynamic} - ]}} - - assert quoted_pattern(%{123 => :foo, 456 => :bar}) == - {:ok, - {:map, - [ - {:required, :integer, :dynamic}, - {:optional, :dynamic, :dynamic} - ]}} - - assert {:error, {:unable_unify, {:integer, {:atom, :foo}, _}}} = - quoted_pattern(%{a: a = 123, b: a = :foo}) - end - - test "struct" do - assert {:ok, {:map, fields}} = quoted_pattern(%:"Elixir.Module.Types.PatternTest.Struct"{}) - - assert Enum.sort(fields) == [ - {:required, {:atom, :__struct__}, {:atom, Module.Types.PatternTest.Struct}}, - {:required, {:atom, :bar}, :dynamic}, - {:required, {:atom, :baz}, :dynamic}, - {:required, {:atom, :foo}, :dynamic} - ] - - assert {:ok, {:map, fields}} = - quoted_pattern(%:"Elixir.Module.Types.PatternTest.Struct"{foo: 123, bar: :atom}) - - assert Enum.sort(fields) == - [ - {:required, {:atom, :__struct__}, {:atom, Module.Types.PatternTest.Struct}}, - {:required, {:atom, :bar}, {:atom, :atom}}, - {:required, {:atom, :baz}, :dynamic}, - {:required, {:atom, :foo}, :integer} - ] - end - - test "struct var" do - assert quoted_pattern(%var{}) == - {:ok, - {:map, - [{:required, {:atom, :__struct__}, :atom}, {:optional, :dynamic, :dynamic}]}} - - assert quoted_pattern(%var{foo: 123}) == - {:ok, - {:map, - [ - {:required, {:atom, :__struct__}, :atom}, - {:required, {:atom, :foo}, :integer}, - {:optional, :dynamic, :dynamic} - ]}} - - assert quoted_pattern(%var{foo: var}) == - {:ok, - {:map, - [ - {:required, {:atom, :__struct__}, :atom}, - {:required, {:atom, :foo}, :atom}, - {:optional, :dynamic, :dynamic} - ]}} - end - - defmacrop custom_type do - quote do: 1 * 8 - big - signed - integer - end - - test "binary" do - assert quoted_pattern(<<"foo"::binary>>) == {:ok, :binary} - assert quoted_pattern(<<123::integer>>) == {:ok, :binary} - assert quoted_pattern(<>) == {:ok, :binary} - assert quoted_pattern(<>) == {:ok, :binary} - assert quoted_pattern(<>) == {:ok, :binary} - assert quoted_pattern(<>) == {:ok, :binary} - assert quoted_pattern(<<123::utf8>>) == {:ok, :binary} - assert quoted_pattern(<<"foo"::utf8>>) == {:ok, :binary} - assert quoted_pattern(<>) == {:ok, :binary} - - assert quoted_pattern({<>, foo}) == {:ok, {:tuple, 2, [:binary, :integer]}} - assert quoted_pattern({<>, foo}) == {:ok, {:tuple, 2, [:binary, :binary]}} - assert quoted_pattern({<>, foo}) == {:ok, {:tuple, 2, [:binary, :integer]}} - - assert {:error, {:unable_unify, {:binary, :integer, _}}} = - quoted_pattern(<>) - end - - test "variables" do - assert quoted_pattern(foo) == {:ok, {:var, 0}} - assert quoted_pattern({foo}) == {:ok, {:tuple, 1, [{:var, 0}]}} - assert quoted_pattern({foo, bar}) == {:ok, {:tuple, 2, [{:var, 0}, {:var, 1}]}} - - assert quoted_pattern(_) == {:ok, :dynamic} - assert quoted_pattern({_ = 123, _}) == {:ok, {:tuple, 2, [:integer, :dynamic]}} - end - - test "assignment" do - assert quoted_pattern(x = y) == {:ok, {:var, 0}} - assert quoted_pattern(x = 123) == {:ok, :integer} - assert quoted_pattern({foo}) == {:ok, {:tuple, 1, [{:var, 0}]}} - assert quoted_pattern({x = y}) == {:ok, {:tuple, 1, [{:var, 0}]}} - - assert quoted_pattern(x = y = 123) == {:ok, :integer} - assert quoted_pattern(x = 123 = y) == {:ok, :integer} - - assert {:error, {:unable_unify, {{:tuple, 1, [var: 0]}, {:var, 0}, _}}} = - quoted_pattern({x} = x) - end - end - - describe "heads" do - test "variable" do - assert quoted_head([a]) == {:ok, [{:var, 0}]} - assert quoted_head([a, b]) == {:ok, [{:var, 0}, {:var, 1}]} - assert quoted_head([a, a]) == {:ok, [{:var, 0}, {:var, 0}]} - - assert {:ok, [{:var, 0}, {:var, 0}], _} = - Pattern.of_head( - [{:a, [version: 0], :foo}, {:a, [version: 0], :foo}], - [], - new_stack(), - new_context() - ) - - assert {:ok, [{:var, 0}, {:var, 1}], _} = - Pattern.of_head( - [{:a, [version: 0], :foo}, {:a, [version: 1], :foo}], - [], - new_stack(), - new_context() - ) - end - - test "assignment" do - assert quoted_head([x = y, x = y]) == {:ok, [{:var, 0}, {:var, 0}]} - assert quoted_head([x = y, y = x]) == {:ok, [{:var, 0}, {:var, 0}]} - - assert quoted_head([x = :foo, x = y, y = z]) == - {:ok, [{:atom, :foo}, {:atom, :foo}, {:atom, :foo}]} - - assert quoted_head([x = y, y = :foo, y = z]) == - {:ok, [{:atom, :foo}, {:atom, :foo}, {:atom, :foo}]} - - assert quoted_head([x = y, y = z, z = :foo]) == - {:ok, [{:atom, :foo}, {:atom, :foo}, {:atom, :foo}]} - - assert {:error, {:unable_unify, {{:tuple, 1, [var: 1]}, {:var, 0}, _}}} = - quoted_head([{x} = y, {y} = x]) - end - - test "guards" do - assert quoted_head([x], [is_binary(x)]) == {:ok, [:binary]} - - assert quoted_head([x, y], [is_binary(x) and is_atom(y)]) == - {:ok, [:binary, :atom]} - - assert quoted_head([x], [is_binary(x) or is_atom(x)]) == - {:ok, [{:union, [:binary, :atom]}]} - - assert quoted_head([x, x], [is_integer(x)]) == {:ok, [:integer, :integer]} - - assert quoted_head([x = 123], [is_integer(x)]) == {:ok, [:integer]} - - assert quoted_head([x], [is_boolean(x) or is_atom(x)]) == - {:ok, [:atom]} - - assert quoted_head([x], [is_atom(x) or is_boolean(x)]) == - {:ok, [:atom]} - - assert quoted_head([x], [is_tuple(x) or is_atom(x)]) == - {:ok, [{:union, [:tuple, :atom]}]} - - assert quoted_head([x], [is_boolean(x) and is_atom(x)]) == - {:ok, [{:union, [atom: true, atom: false]}]} - - assert quoted_head([x], [is_atom(x) > :foo]) == {:ok, [var: 0]} - - assert quoted_head([x, y], [is_atom(x) or is_integer(y)]) == - {:ok, [{:var, 0}, {:var, 1}]} - - assert quoted_head([x], [is_atom(x) or is_atom(x)]) == - {:ok, [:atom]} - - assert quoted_head([x, y], [(is_atom(x) and is_atom(y)) or (is_atom(x) and is_integer(y))]) == - {:ok, [:atom, union: [:atom, :integer]]} - - assert quoted_head([x, y], [is_atom(x) or is_integer(x)]) == - {:ok, [union: [:atom, :integer], var: 0]} - - assert quoted_head([x, y], [is_atom(y) or is_integer(y)]) == - {:ok, [{:var, 0}, {:union, [:atom, :integer]}]} - - assert quoted_head([x], [true == false or is_integer(x)]) == - {:ok, [var: 0]} - - assert {:error, {:unable_unify, {:binary, :integer, _}}} = - quoted_head([x], [is_binary(x) and is_integer(x)]) - - assert {:error, {:unable_unify, {:tuple, :atom, _}}} = - quoted_head([x], [is_tuple(x) and is_atom(x)]) - - assert {:error, {:unable_unify, {{:atom, true}, :tuple, _}}} = - quoted_head([x], [is_tuple(is_atom(x))]) - end - - test "guard downcast" do - assert {:error, _} = quoted_head([x], [is_atom(x) and is_boolean(x)]) - end - - test "guard and" do - assert quoted_head([], [(true and 1) > 0]) == {:ok, []} - - assert quoted_head( - [struct], - [is_map_key(struct, :map) and map_size(:erlang.map_get(:map, struct))] - ) == {:ok, [{:map, [{:optional, :dynamic, :dynamic}]}]} - end - - test "intersection functions" do - assert quoted_head([x], [+x]) == {:ok, [{:union, [:integer, :float]}]} - assert quoted_head([x], [x + 1]) == {:ok, [{:union, [:float, :integer]}]} - assert quoted_head([x], [x + 1.0]) == {:ok, [{:union, [:integer, :float]}]} - end - - test "nested calls with intersections in guards" do - assert quoted_head([x], [:erlang.rem(x, 2)]) == {:ok, [:integer]} - assert quoted_head([x], [:erlang.rem(x + x, 2)]) == {:ok, [:integer]} - - assert quoted_head([x], [:erlang.bnot(+x)]) == {:ok, [:integer]} - assert quoted_head([x], [:erlang.bnot(x + 1)]) == {:ok, [:integer]} - - assert quoted_head([x], [is_integer(1 + x - 1)]) == {:ok, [:integer]} - - assert quoted_head([x], [is_integer(1 + x - 1) and is_integer(1 + x - 1)]) == - {:ok, [:integer]} - - assert quoted_head([x], [1 - x >= 0]) == {:ok, [{:union, [:float, :integer]}]} - assert quoted_head([x], [1 - x >= 0 and 1 - x < 0]) == {:ok, [{:union, [:float, :integer]}]} - - assert {:error, - {:unable_apply, - {_, [{:var, 0}, :float], _, - [ - {[:integer, :integer], :integer}, - {[:float, {:union, [:integer, :float]}], :float}, - {[{:union, [:integer, :float]}, :float], :float} - ], _}}} = quoted_head([x], [:erlang.bnot(x + 1.0)]) - end - - test "erlang-only guards" do - assert quoted_head([x], [:erlang.size(x)]) == - {:ok, [{:union, [:binary, :tuple]}]} - end - - test "failing guard functions" do - assert quoted_head([x], [length([])]) == {:ok, [{:var, 0}]} - - assert {:error, - {:unable_apply, - {{:erlang, :length, 1}, [{:atom, :foo}], _, [{[{:list, :dynamic}], :integer}], _}}} = - quoted_head([x], [length(:foo)]) - - assert {:error, - {:unable_apply, - {_, [{:union, [{:atom, true}, {:atom, false}]}], _, - [{[{:list, :dynamic}], :integer}], _}}} = quoted_head([x], [length(is_tuple(x))]) - - assert {:error, - {:unable_apply, - {_, [:integer, {:union, [{:atom, true}, {:atom, false}]}], _, - [{[:integer, :tuple], :dynamic}], _}}} = quoted_head([x], [elem(is_tuple(x), 0)]) - - assert {:error, - {:unable_apply, - {_, [{:union, [{:atom, true}, {:atom, false}]}, :integer], _, - [ - {[:integer, :integer], :integer}, - {[:float, {:union, [:integer, :float]}], :float}, - {[{:union, [:integer, :float]}, :float], :float} - ], _}}} = quoted_head([x], [elem({}, is_tuple(x))]) - - assert quoted_head([x], [elem({}, 1)]) == {:ok, [var: 0]} - - assert quoted_head([x], [elem(x, 1) == :foo]) == {:ok, [:tuple]} - - assert quoted_head([x], [is_tuple(x) and elem(x, 1)]) == {:ok, [:tuple]} - - assert quoted_head([x], [length(x) == 0 or elem(x, 1)]) == {:ok, [{:list, :dynamic}]} - - assert quoted_head([x], [ - (is_list(x) and length(x) == 0) or (is_tuple(x) and elem(x, 1)) - ]) == - {:ok, [{:union, [{:list, :dynamic}, :tuple]}]} - - assert quoted_head([x], [ - (length(x) == 0 and is_list(x)) or (elem(x, 1) and is_tuple(x)) - ]) == {:ok, [{:list, :dynamic}]} - - assert quoted_head([x], [elem(x, 1) or is_atom(x)]) == {:ok, [:tuple]} - - assert quoted_head([x], [is_atom(x) or elem(x, 1)]) == {:ok, [{:union, [:atom, :tuple]}]} - - assert quoted_head([x, y], [elem(x, 1) and is_atom(y)]) == {:ok, [:tuple, :atom]} - - assert quoted_head([x, y], [elem(x, 1) or is_atom(y)]) == {:ok, [:tuple, {:var, 0}]} - - assert {:error, {:unable_unify, {:tuple, :atom, _}}} = - quoted_head([x], [elem(x, 1) and is_atom(x)]) - end - - test "map" do - assert quoted_head([%{true: false} = foo, %{} = foo]) == - {:ok, - [ - {:map, - [{:required, {:atom, true}, {:atom, false}}, {:optional, :dynamic, :dynamic}]}, - {:map, - [{:required, {:atom, true}, {:atom, false}}, {:optional, :dynamic, :dynamic}]} - ]} - - assert quoted_head([%{true: bool}], [is_boolean(bool)]) == - {:ok, - [ - {:map, - [ - {:required, {:atom, true}, {:union, [atom: true, atom: false]}}, - {:optional, :dynamic, :dynamic} - ]} - ]} - - assert quoted_head([%{true: true} = foo, %{false: false} = foo]) == - {:ok, - [ - {:map, - [ - {:required, {:atom, false}, {:atom, false}}, - {:required, {:atom, true}, {:atom, true}}, - {:optional, :dynamic, :dynamic} - ]}, - {:map, - [ - {:required, {:atom, false}, {:atom, false}}, - {:required, {:atom, true}, {:atom, true}}, - {:optional, :dynamic, :dynamic} - ]} - ]} - - assert {:error, - {:unable_unify, - {{:map, [{:required, {:atom, true}, {:atom, true}}]}, - {:map, [{:required, {:atom, true}, {:atom, false}}]}, - _}}} = quoted_head([%{true: false} = foo, %{true: true} = foo]) - end - - test "binary in guards" do - assert quoted_head([a, b], [byte_size(a <> b) > 0]) == - {:ok, [:binary, :binary]} - - assert quoted_head([map], [byte_size(map.a <> map.b) > 0]) == - {:ok, [map: [{:optional, :dynamic, :dynamic}]]} - end - - test "struct var guard" do - assert quoted_head([%var{}], [is_atom(var)]) == - {:ok, - [ - {:map, - [{:required, {:atom, :__struct__}, :atom}, {:optional, :dynamic, :dynamic}]} - ]} - - assert {:error, {:unable_unify, {:atom, :integer, _}}} = - quoted_head([%var{}], [is_integer(var)]) - end - - test "tuple_size/1" do - assert quoted_head([x], [tuple_size(x) == 0]) == {:ok, [{:tuple, 0, []}]} - assert quoted_head([x], [0 == tuple_size(x)]) == {:ok, [{:tuple, 0, []}]} - assert quoted_head([x], [tuple_size(x) == 2]) == {:ok, [{:tuple, 2, [:dynamic, :dynamic]}]} - assert quoted_head([x], [is_tuple(x) and tuple_size(x) == 0]) == {:ok, [{:tuple, 0, []}]} - assert quoted_head([x], [tuple_size(x) == 0 and is_tuple(x)]) == {:ok, [{:tuple, 0, []}]} - - assert quoted_head([x = {y}], [is_integer(y) and tuple_size(x) == 1]) == - {:ok, [{:tuple, 1, [:integer]}]} - - assert {:error, {:unable_unify, {{:tuple, 0, []}, :integer, _}}} = - quoted_head([x], [tuple_size(x) == 0 and is_integer(x)]) - - assert {:error, {:unable_unify, {{:tuple, 0, []}, :integer, _}}} = - quoted_head([x], [is_integer(x) and tuple_size(x) == 0]) - - assert {:error, {:unable_unify, {{:tuple, 0, []}, :integer, _}}} = - quoted_head([x], [is_tuple(x) and tuple_size(x) == 0 and is_integer(x)]) - - assert {:error, {:unable_unify, {{:tuple, 1, [:dynamic]}, {:tuple, 0, []}, _}}} = - quoted_head([x = {}], [tuple_size(x) == 1]) - end - end -end diff --git a/lib/elixir/test/elixir/module/types/type_helper.exs b/lib/elixir/test/elixir/module/types/type_helper.exs index 351461cdf1..ecd9c56f4d 100644 --- a/lib/elixir/test/elixir/module/types/type_helper.exs +++ b/lib/elixir/test/elixir/module/types/type_helper.exs @@ -2,47 +2,61 @@ Code.require_file("../../test_helper.exs", __DIR__) defmodule TypeHelper do alias Module.Types - alias Module.Types.{Pattern, Expr, Unify} - - defmacro quoted_expr(patterns \\ [], guards \\ [], body) do - expr = expand_expr(patterns, guards, body, __CALLER__) + alias Module.Types.{Pattern, Expr} + @doc """ + Main helper for checking the given AST type checks without warnings. + """ + defmacro typecheck!(patterns \\ [], guards \\ [], body) do quote do - TypeHelper.__expr__(unquote(Macro.escape(expr))) + unquote(typecheck(patterns, guards, body, __CALLER__)) + |> TypeHelper.__typecheck__!() end end - def __expr__({patterns, guards, body}) do - with {:ok, _types, context} <- - Pattern.of_head(patterns, guards, new_stack(), new_context()), - {:ok, type, context} <- Expr.of_expr(body, :dynamic, new_stack(), context) do - {[type], _context} = Unify.lift_types([type], context) - {:ok, type} - else - {:error, {type, reason, _context}} -> - {:error, {type, reason}} - end - end + def __typecheck__!({:ok, type, %{warnings: []}}), do: type + + def __typecheck__!({:ok, _type, %{warnings: warnings}}), + do: raise("type checking ok but with warnings: #{inspect(warnings)}") - def expand_expr(patterns, guards, expr, env) do + def __typecheck__!({:error, %{warnings: warnings}}), + do: raise("type checking errored with warnings: #{inspect(warnings)}") + + @doc """ + Building block for typechecking a given AST. + """ + def typecheck(patterns, guards, body, env) do fun = quote do - fn unquote(patterns) when unquote(guards) -> unquote(expr) end + fn unquote(patterns) when unquote(guards) -> unquote(body) end end {ast, _, _} = :elixir_expand.expand(fun, :elixir_env.env_to_ex(env), env) {:fn, _, [{:->, _, [[{:when, _, [patterns, guards]}], body]}]} = ast - {patterns, guards, body} + + quote do + TypeHelper.__typecheck__( + unquote(Macro.escape(patterns)), + unquote(Macro.escape(guards)), + unquote(Macro.escape(body)) + ) + end end - def new_context() do - Types.context("types_test.ex", TypesTest, {:test, 0}, [], Module.ParallelChecker.test_cache()) + def __typecheck__(patterns, guards, body) do + stack = new_stack() + + with {:ok, _types, context} <- Pattern.of_head(patterns, guards, stack, new_context()), + {:ok, type, context} <- Expr.of_expr(body, stack, context) do + {:ok, type, context} + end end def new_stack() do - %{ - Types.stack() - | last_expr: {:foo, [], nil} - } + Types.stack("types_test.ex", TypesTest, {:test, 0}, [], Module.ParallelChecker.test_cache()) + end + + def new_context() do + Types.context() end end diff --git a/lib/elixir/test/elixir/module/types/types_test.exs b/lib/elixir/test/elixir/module/types/types_test.exs index f25f067dab..dfebb6cb1b 100644 --- a/lib/elixir/test/elixir/module/types/types_test.exs +++ b/lib/elixir/test/elixir/module/types/types_test.exs @@ -3,37 +3,30 @@ Code.require_file("type_helper.exs", __DIR__) defmodule Module.Types.TypesTest do use ExUnit.Case, async: true alias Module.Types - alias Module.Types.{Pattern, Expr} - - @hint :elixir_errors.prefix(:hint) defmacro warning(patterns \\ [], guards \\ [], body) do min_line = min_line(patterns ++ guards ++ [body]) patterns = reset_line(patterns, min_line) guards = reset_line(guards, min_line) body = reset_line(body, min_line) - expr = TypeHelper.expand_expr(patterns, guards, body, __CALLER__) quote do - Module.Types.TypesTest.__expr__(unquote(Macro.escape(expr))) + unquote(TypeHelper.typecheck(patterns, guards, body, __CALLER__)) + |> Module.Types.TypesTest.__warning__() end end - defmacro generated(ast) do - Macro.prewalk(ast, fn node -> Macro.update_meta(node, &([generated: true] ++ &1)) end) - end - - def __expr__({patterns, guards, body}) do - with {:ok, _types, context} <- - Pattern.of_head(patterns, guards, TypeHelper.new_stack(), TypeHelper.new_context()), - {:ok, _type, context} <- Expr.of_expr(body, :dynamic, TypeHelper.new_stack(), context) do - case context.warnings do - [warning] -> to_message(:warning, warning) - _ -> :none + def __warning__(result) do + context = + case result do + {:ok, _, context} -> context + {:error, context} -> context end - else - {:error, {type, reason, context}} -> - to_message(:error, {type, reason, context}) + + case context.warnings do + [warning] -> to_message(warning) + [] -> raise "no warnings" + [_ | _] = warnings -> raise "too many warnings: #{inspect(warnings)}" end end @@ -55,21 +48,12 @@ defmodule Module.Types.TypesTest do min end - defp to_message(:warning, {module, warning, _location}) do + defp to_message({module, warning, _location}) do warning |> module.format_warning() |> IO.iodata_to_binary() end - defp to_message(:error, {type, reason, context}) do - {Module.Types, error, _location} = Module.Types.error_to_warning(type, reason, context) - - error - |> Module.Types.format_warning() - |> IO.iodata_to_binary() - |> String.trim_trailing("\nConflict found at") - end - test "expr_to_string/1" do assert Types.expr_to_string({1, 2}) == "{1, 2}" assert Types.expr_to_string(quote(do: Foo.bar(arg))) == "Foo.bar(arg)" @@ -83,639 +67,13 @@ defmodule Module.Types.TypesTest do end test "undefined function warnings" do - assert warning([], URI.unknown("foo")) == + assert warning(URI.unknown("foo")) == "URI.unknown/1 is undefined or private" - assert warning([], if(true, do: URI.unknown("foo"))) == + assert warning(if(true, do: URI.unknown("foo"))) == "URI.unknown/1 is undefined or private" - assert warning([], try(do: :ok, after: URI.unknown("foo"))) == + assert warning(try(do: :ok, after: URI.unknown("foo"))) == "URI.unknown/1 is undefined or private" end - - describe "function head warnings" do - test "warns on literals" do - string = warning([var = 123, var = "abc"], var) - - assert string == """ - incompatible types: - - integer() !~ binary() - - in expression: - - # types_test.ex:1 - var = "abc" - - where "var" was given the type integer() in: - - # types_test.ex:1 - var = 123 - - where "var" was given the type binary() in: - - # types_test.ex:1 - var = "abc" - """ - end - - test "warns on binary patterns" do - string = warning([<>], var) - - assert string == """ - incompatible types: - - integer() !~ binary() - - in expression: - - # types_test.ex:1 - <<..., var::binary>> - - where "var" was given the type integer() in: - - # types_test.ex:1 - <> - - where "var" was given the type binary() in: - - # types_test.ex:1 - <<..., var::binary>> - """ - end - - test "warns on recursive patterns" do - string = warning([{var} = var], var) - - assert string == """ - incompatible types: - - {var1} !~ var1 - - in expression: - - # types_test.ex:1 - {var} = var - - where "var" was given the type {var1} in: - - # types_test.ex:1 - {var} = var - """ - end - - test "warns on guards" do - string = warning([var], [is_integer(var) and is_binary(var)], var) - - assert string == """ - incompatible types: - - integer() !~ binary() - - in expression: - - # types_test.ex:1 - is_binary(var) - - where "var" was given the type integer() in: - - # types_test.ex:1 - is_integer(var) - - where "var" was given the type binary() in: - - # types_test.ex:1 - is_binary(var) - """ - end - - test "warns on guards from cases unless generated" do - string = - warning( - [var], - [is_integer(var)], - case var do - _ when is_binary(var) -> :ok - end - ) - - assert is_binary(string) - - string = - generated( - warning( - [var], - [is_integer(var)], - case var do - _ when is_binary(var) -> :ok - end - ) - ) - - assert string == :none - end - - test "check body" do - string = warning([x], [is_integer(x)], :foo = x) - - assert string == """ - incompatible types: - - integer() !~ :foo - - in expression: - - # types_test.ex:1 - :foo = x - - where "x" was given the type integer() in: - - # types_test.ex:1 - is_integer(x) - - where "x" was given the type :foo in: - - # types_test.ex:1 - :foo = x - """ - end - - test "check binary" do - string = warning([foo], [is_binary(foo)], <>) - - assert string == """ - incompatible types: - - binary() !~ integer() - - in expression: - - # types_test.ex:1 - <> - - where "foo" was given the type binary() in: - - # types_test.ex:1 - is_binary(foo) - - where "foo" was given the type integer() in: - - # types_test.ex:1 - <> - - #{@hint} all expressions given to binaries are assumed to be of type \ - integer() unless said otherwise. For example, <> assumes "expr" \ - is an integer. Pass a modifier, such as <> or <>, \ - to change the default behaviour. - """ - - string = warning([foo], [is_binary(foo)], <>) - - assert string == """ - incompatible types: - - binary() !~ integer() - - in expression: - - # types_test.ex:1 - <> - - where "foo" was given the type binary() in: - - # types_test.ex:1 - is_binary(foo) - - where "foo" was given the type integer() in: - - # types_test.ex:1 - <> - """ - end - - test "is_tuple warning" do - string = warning([foo], [is_tuple(foo)], {_} = foo) - - assert string == """ - incompatible types: - - tuple() !~ {dynamic()} - - in expression: - - # types_test.ex:1 - {_} = foo - - where "foo" was given the type tuple() in: - - # types_test.ex:1 - is_tuple(foo) - - where "foo" was given the type {dynamic()} in: - - # types_test.ex:1 - {_} = foo - - #{@hint} use pattern matching or "is_tuple(foo) and tuple_size(foo) == 1" to guard a sized tuple. - """ - end - - test "function call" do - string = warning([foo], [rem(foo, 2.0) == 0], foo) - - assert string == """ - expected Kernel.rem/2 to have signature: - - var1, float() -> dynamic() - - but it has signature: - - integer(), integer() -> integer() - - in expression: - - # types_test.ex:1 - rem(foo, 2.0) - """ - end - - test "operator call" do - string = warning([foo], [foo - :bar == 0], foo) - - assert string == """ - expected Kernel.-/2 to have signature: - - var1, :bar -> dynamic() - - but it has signature: - - integer(), integer() -> integer() - float(), integer() | float() -> float() - integer() | float(), float() -> float() - - in expression: - - # types_test.ex:1 - foo - :bar - """ - end - - test "rewrite call" do - string = warning([foo], [is_map_key(1, foo)], foo) - - assert string == """ - expected Kernel.is_map_key/2 to have signature: - - integer(), var1 -> dynamic() - - but it has signature: - - %{optional(dynamic()) => dynamic()}, dynamic() -> dynamic() - - in expression: - - # types_test.ex:1 - is_map_key(1, foo) - """ - end - end - - describe "map warnings" do - test "handling of non-singleton types in maps" do - string = - warning( - [], - ( - event = %{"type" => "order"} - %{"amount" => amount} = event - %{"user" => user} = event - %{"id" => user_id} = user - {:order, user_id, amount} - ) - ) - - assert string == """ - incompatible types: - - binary() !~ map() - - in expression: - - # types_test.ex:5 - %{"id" => user_id} = user - - where "amount" was given the type binary() in: - - # types_test.ex:3 - %{"amount" => amount} = event - - where "amount" was given the same type as "user" in: - - # types_test.ex:4 - %{"user" => user} = event - - where "user" was given the type binary() in: - - # types_test.ex:4 - %{"user" => user} = event - - where "user" was given the type map() in: - - # types_test.ex:5 - %{"id" => user_id} = user - """ - end - - test "show map() when comparing against non-map" do - string = - warning( - [foo], - ( - foo.bar - :atom = foo - ) - ) - - assert string == """ - incompatible types: - - map() !~ :atom - - in expression: - - # types_test.ex:4 - :atom = foo - - where "foo" was given the type map() (due to calling var.field) in: - - # types_test.ex:3 - foo.bar - - where "foo" was given the type :atom in: - - # types_test.ex:4 - :atom = foo - - #{@hint} "var.field" (without parentheses) implies "var" is a map() while \ - "var.fun()" (with parentheses) implies "var" is an atom() - """ - end - - test "use module as map (without parentheses)" do - string = - warning( - [foo], - ( - %module{} = foo - module.__struct__ - ) - ) - - assert string == """ - incompatible types: - - map() !~ atom() - - in expression: - - # types_test.ex:4 - module.__struct__ - - where "module" was given the type atom() in: - - # types_test.ex:3 - %module{} - - where "module" was given the type map() (due to calling var.field) in: - - # types_test.ex:4 - module.__struct__ - - #{@hint} "var.field" (without parentheses) implies "var" is a map() while \ - "var.fun()" (with parentheses) implies "var" is an atom() - """ - end - - test "use map as module (with parentheses)" do - string = warning([foo], [is_map(foo)], foo.__struct__()) - - assert string == """ - incompatible types: - - map() !~ atom() - - in expression: - - # types_test.ex:1 - foo.__struct__() - - where "foo" was given the type map() in: - - # types_test.ex:1 - is_map(foo) - - where "foo" was given the type atom() (due to calling var.fun()) in: - - # types_test.ex:1 - foo.__struct__() - - #{@hint} "var.field" (without parentheses) implies "var" is a map() while \ - "var.fun()" (with parentheses) implies "var" is an atom() - """ - end - - test "non-existent map field warning" do - string = - warning( - ( - map = %{foo: 1} - map.bar - ) - ) - - assert string == """ - undefined field "bar" in expression: - - # types_test.ex:3 - map.bar - - expected one of the following fields: foo - - where "map" was given the type map() in: - - # types_test.ex:2 - map = %{foo: 1} - """ - end - - test "non-existent struct field warning" do - string = - warning( - [foo], - ( - %URI{} = foo - foo.bar - ) - ) - - assert string == """ - undefined field "bar" in expression: - - # types_test.ex:4 - foo.bar - - expected one of the following fields: __struct__, authority, fragment, host, path, port, query, scheme, userinfo - - where "foo" was given the type %URI{} in: - - # types_test.ex:3 - %URI{} = foo - """ - end - - test "expands type variables" do - string = - warning( - [%{foo: key} = event, other_key], - [is_integer(key) and is_atom(other_key)], - %{foo: ^other_key} = event - ) - - assert string == """ - incompatible types: - - %{foo: integer()} !~ %{foo: atom()} - - in expression: - - # types_test.ex:3 - %{foo: ^other_key} = event - - where "event" was given the type %{foo: integer(), optional(dynamic()) => dynamic()} in: - - # types_test.ex:1 - %{foo: key} = event - - where "event" was given the type %{foo: atom(), optional(dynamic()) => dynamic()} in: - - # types_test.ex:3 - %{foo: ^other_key} = event - """ - end - - test "expands map when maps are nested" do - string = - warning( - [map1, map2], - ( - [_var1, _var2] = [map1, map2] - %{} = map1 - %{} = map2.subkey - ) - ) - - assert string == """ - incompatible types: - - %{subkey: var1, optional(dynamic()) => dynamic()} !~ %{optional(dynamic()) => dynamic()} | %{optional(dynamic()) => dynamic()} - - in expression: - - # types_test.ex:5 - map2.subkey - - where "map2" was given the type %{optional(dynamic()) => dynamic()} | %{optional(dynamic()) => dynamic()} in: - - # types_test.ex:3 - [_var1, _var2] = [map1, map2] - - where "map2" was given the type %{subkey: var1, optional(dynamic()) => dynamic()} (due to calling var.field) in: - - # types_test.ex:5 - map2.subkey - - #{@hint} "var.field" (without parentheses) implies "var" is a map() while "var.fun()" (with parentheses) implies "var" is an atom() - """ - end - end - - describe "regressions" do - test "recursive map fields" do - assert warning( - [queried], - with( - true <- is_nil(queried.foo.bar), - _ = queried.foo - ) do - %{foo: %{other_id: _other_id} = foo} = queried - %{other_id: id} = foo - %{id: id} - end - ) == :none - end - - test "no-recursion on guards with map fields" do - assert warning( - [assigns], - ( - variable_enum = assigns.variable_enum - - case true do - _ when variable_enum != nil -> assigns.variable_enum - end - ) - ) == :none - end - - test "map patterns with pinned keys and field access" do - assert warning( - [x, y], - ( - key_var = y - %{^key_var => _value} = x - key_var2 = y - %{^key_var2 => _value2} = x - y.z - ) - ) == :none - end - - test "map patterns with pinned keys" do - assert warning( - [x, y], - ( - key_var = y - %{^key_var => _value} = x - key_var2 = y - %{^key_var2 => _value2} = x - key_var3 = y - %{^key_var3 => _value3} = x - ) - ) == :none - end - - test "map updates with var key" do - assert warning( - [state0, key0], - ( - state1 = %{state0 | key0 => true} - key1 = key0 - state2 = %{state1 | key1 => true} - state2 - ) - ) == :none - end - - test "nested map updates" do - assert warning( - [state], - ( - _foo = state.key.user_id - _bar = state.key.user_id - state = %{state | key: %{state.key | other_id: 1}} - _baz = state.key.user_id - ) - ) == :none - end - end end diff --git a/lib/elixir/test/elixir/module/types/unify_test.exs b/lib/elixir/test/elixir/module/types/unify_test.exs deleted file mode 100644 index 672fe3892b..0000000000 --- a/lib/elixir/test/elixir/module/types/unify_test.exs +++ /dev/null @@ -1,770 +0,0 @@ -Code.require_file("type_helper.exs", __DIR__) - -defmodule Module.Types.UnifyTest do - use ExUnit.Case, async: true - import Module.Types.Unify - alias Module.Types - - defp unify_lift(left, right, context \\ new_context()) do - unify(left, right, new_stack(), context) - |> lift_result() - end - - defp new_context() do - Types.context("types_test.ex", TypesTest, {:test, 0}, [], Module.ParallelChecker.test_cache()) - end - - defp new_stack() do - %{ - Types.stack() - | context: :pattern, - last_expr: {:foo, [], nil} - } - end - - defp unify(left, right, context) do - unify(left, right, new_stack(), context) - end - - defp lift_result({:ok, type, context}) do - {:ok, lift_type(type, context)} - end - - defp lift_result({:error, {type, reason, _context}}) do - {:error, {type, reason}} - end - - defp lift_type(type, context) do - {[type], _context} = lift_types([type], context) - type - end - - defp format_type_string(type, simplify?) do - IO.iodata_to_binary(format_type(type, simplify?)) - end - - describe "unify/3" do - test "literal" do - assert unify_lift({:atom, :foo}, {:atom, :foo}) == {:ok, {:atom, :foo}} - - assert {:error, {:unable_unify, {{:atom, :foo}, {:atom, :bar}, _}}} = - unify_lift({:atom, :foo}, {:atom, :bar}) - end - - test "type" do - assert unify_lift(:integer, :integer) == {:ok, :integer} - assert unify_lift(:binary, :binary) == {:ok, :binary} - assert unify_lift(:atom, :atom) == {:ok, :atom} - - assert {:error, {:unable_unify, {:integer, :atom, _}}} = unify_lift(:integer, :atom) - end - - test "atom subtype" do - assert unify_lift({:atom, true}, :atom) == {:ok, {:atom, true}} - assert {:error, _} = unify_lift(:atom, {:atom, true}) - end - - test "tuple" do - assert unify_lift({:tuple, 0, []}, {:tuple, 0, []}) == {:ok, {:tuple, 0, []}} - - assert unify_lift({:tuple, 1, [:integer]}, {:tuple, 1, [:integer]}) == - {:ok, {:tuple, 1, [:integer]}} - - assert unify_lift({:tuple, 1, [{:atom, :foo}]}, {:tuple, 1, [:atom]}) == - {:ok, {:tuple, 1, [{:atom, :foo}]}} - - assert {:error, {:unable_unify, {{:tuple, 1, [:integer]}, {:tuple, 0, []}, _}}} = - unify_lift({:tuple, 1, [:integer]}, {:tuple, 0, []}) - - assert {:error, {:unable_unify, {:integer, :atom, _}}} = - unify_lift({:tuple, 1, [:integer]}, {:tuple, 1, [:atom]}) - end - - test "list" do - assert unify_lift({:list, :integer}, {:list, :integer}) == {:ok, {:list, :integer}} - - assert {:error, {:unable_unify, {:atom, :integer, _}}} = - unify_lift({:list, :atom}, {:list, :integer}) - end - - test "map" do - assert unify_lift({:map, []}, {:map, []}) == {:ok, {:map, []}} - - assert unify_lift( - {:map, [{:required, :integer, :atom}]}, - {:map, [{:optional, :dynamic, :dynamic}]} - ) == - {:ok, {:map, [{:required, :integer, :atom}]}} - - assert unify_lift( - {:map, [{:optional, :dynamic, :dynamic}]}, - {:map, [{:required, :integer, :atom}]} - ) == - {:ok, {:map, [{:required, :integer, :atom}]}} - - assert unify_lift( - {:map, [{:optional, :dynamic, :dynamic}]}, - {:map, [{:required, :integer, :atom}, {:optional, :dynamic, :dynamic}]} - ) == - {:ok, {:map, [{:required, :integer, :atom}, {:optional, :dynamic, :dynamic}]}} - - assert unify_lift( - {:map, [{:required, :integer, :atom}, {:optional, :dynamic, :dynamic}]}, - {:map, [{:optional, :dynamic, :dynamic}]} - ) == - {:ok, {:map, [{:required, :integer, :atom}, {:optional, :dynamic, :dynamic}]}} - - assert unify_lift( - {:map, [{:required, :integer, :atom}]}, - {:map, [{:required, :integer, :atom}]} - ) == - {:ok, {:map, [{:required, :integer, :atom}]}} - - assert {:error, - {:unable_unify, - {{:map, [{:required, :integer, :atom}]}, {:map, [{:required, :atom, :integer}]}, _}}} = - unify_lift( - {:map, [{:required, :integer, :atom}]}, - {:map, [{:required, :atom, :integer}]} - ) - - assert {:error, {:unable_unify, {{:map, [{:required, :integer, :atom}]}, {:map, []}, _}}} = - unify_lift({:map, [{:required, :integer, :atom}]}, {:map, []}) - - assert {:error, {:unable_unify, {{:map, []}, {:map, [{:required, :integer, :atom}]}, _}}} = - unify_lift({:map, []}, {:map, [{:required, :integer, :atom}]}) - - assert {:error, - {:unable_unify, - {{:map, [{:required, {:atom, :foo}, :integer}]}, - {:map, [{:required, {:atom, :foo}, :atom}]}, - _}}} = - unify_lift( - {:map, [{:required, {:atom, :foo}, :integer}]}, - {:map, [{:required, {:atom, :foo}, :atom}]} - ) - end - - test "map required/optional key" do - assert unify_lift( - {:map, [{:required, {:atom, :foo}, {:atom, :bar}}]}, - {:map, [{:required, {:atom, :foo}, :atom}]} - ) == - {:ok, {:map, [{:required, {:atom, :foo}, {:atom, :bar}}]}} - - assert unify_lift( - {:map, [{:optional, {:atom, :foo}, {:atom, :bar}}]}, - {:map, [{:required, {:atom, :foo}, :atom}]} - ) == - {:ok, {:map, [{:required, {:atom, :foo}, {:atom, :bar}}]}} - - assert unify_lift( - {:map, [{:required, {:atom, :foo}, {:atom, :bar}}]}, - {:map, [{:optional, {:atom, :foo}, :atom}]} - ) == - {:ok, {:map, [{:required, {:atom, :foo}, {:atom, :bar}}]}} - - assert unify_lift( - {:map, [{:optional, {:atom, :foo}, {:atom, :bar}}]}, - {:map, [{:optional, {:atom, :foo}, :atom}]} - ) == - {:ok, {:map, [{:optional, {:atom, :foo}, {:atom, :bar}}]}} - end - - test "map with subtyped keys" do - assert unify_lift( - {:map, [{:required, {:atom, :foo}, :integer}]}, - {:map, [{:required, :atom, :integer}]} - ) == {:ok, {:map, [{:required, {:atom, :foo}, :integer}]}} - - assert unify_lift( - {:map, [{:optional, {:atom, :foo}, :integer}]}, - {:map, [{:required, :atom, :integer}]} - ) == {:ok, {:map, [{:required, {:atom, :foo}, :integer}]}} - - assert unify_lift( - {:map, [{:required, {:atom, :foo}, :integer}]}, - {:map, [{:optional, :atom, :integer}]} - ) == {:ok, {:map, [{:required, {:atom, :foo}, :integer}]}} - - assert unify_lift( - {:map, [{:optional, {:atom, :foo}, :integer}]}, - {:map, [{:optional, :atom, :integer}]} - ) == {:ok, {:map, [{:optional, {:atom, :foo}, :integer}]}} - - assert {:error, - {:unable_unify, - {{:map, [{:required, :atom, :integer}]}, - {:map, [{:required, {:atom, :foo}, :integer}]}, - _}}} = - unify_lift( - {:map, [{:required, :atom, :integer}]}, - {:map, [{:required, {:atom, :foo}, :integer}]} - ) - - assert {:error, - {:unable_unify, - {{:map, [{:optional, :atom, :integer}]}, - {:map, [{:required, {:atom, :foo}, :integer}]}, - _}}} = - unify_lift( - {:map, [{:optional, :atom, :integer}]}, - {:map, [{:required, {:atom, :foo}, :integer}]} - ) - - assert {:error, - {:unable_unify, - {{:map, [{:required, :atom, :integer}]}, - {:map, [{:optional, {:atom, :foo}, :integer}]}, - _}}} = - unify_lift( - {:map, [{:required, :atom, :integer}]}, - {:map, [{:optional, {:atom, :foo}, :integer}]} - ) - - assert unify_lift( - {:map, [{:optional, :atom, :integer}]}, - {:map, [{:optional, {:atom, :foo}, :integer}]} - ) == {:ok, {:map, []}} - - assert unify_lift( - {:map, [{:required, {:atom, :foo}, :integer}]}, - {:map, [{:required, {:atom, :foo}, :integer}]} - ) == {:ok, {:map, [{:required, {:atom, :foo}, :integer}]}} - - assert unify_lift( - {:map, [{:required, {:atom, :foo}, :integer}]}, - {:map, [{:optional, {:atom, :foo}, :integer}]} - ) == {:ok, {:map, [{:required, {:atom, :foo}, :integer}]}} - - assert unify_lift( - {:map, [{:optional, {:atom, :foo}, :integer}]}, - {:map, [{:required, {:atom, :foo}, :integer}]} - ) == {:ok, {:map, [{:required, {:atom, :foo}, :integer}]}} - - assert unify_lift( - {:map, [{:optional, {:atom, :foo}, :integer}]}, - {:map, [{:optional, {:atom, :foo}, :integer}]} - ) == {:ok, {:map, [{:optional, {:atom, :foo}, :integer}]}} - end - - test "map with subtyped and multiple matching keys" do - assert {:error, _} = - unify_lift( - {:map, - [ - {:required, {:atom, :foo}, :integer}, - {:required, :atom, {:union, [:integer, :boolean]}} - ]}, - {:map, [{:required, :atom, :integer}]} - ) - - assert unify_lift( - {:map, - [ - {:required, {:atom, :foo}, :integer}, - {:required, :atom, {:union, [:integer, :boolean]}} - ]}, - {:map, [{:required, :atom, {:union, [:integer, :boolean]}}]} - ) == - {:ok, - {:map, - [ - {:required, {:atom, :foo}, :integer}, - {:required, :atom, {:union, [:integer, :boolean]}} - ]}} - - assert {:error, _} = - unify_lift( - {:map, [{:required, :atom, :integer}]}, - {:map, - [ - {:required, {:atom, :foo}, :integer}, - {:required, :atom, {:union, [:integer, :boolean]}} - ]} - ) - - assert {:error, _} = - unify_lift( - {:map, [{:required, :atom, {:union, [:integer, :boolean]}}]}, - {:map, - [ - {:required, {:atom, :foo}, :integer}, - {:required, :atom, {:union, [:integer, :boolean]}} - ]} - ) - - assert unify_lift( - {:map, [{:required, :atom, :integer}]}, - {:map, - [ - {:optional, {:atom, :foo}, :integer}, - {:optional, :atom, {:union, [:integer, :boolean]}} - ]} - ) == {:ok, {:map, [{:required, :atom, :integer}]}} - - assert unify_lift( - {:map, [{:required, :atom, {:union, [:integer, :boolean]}}]}, - {:map, - [ - {:optional, {:atom, :foo}, :integer}, - {:optional, :atom, {:union, [:integer, :boolean]}} - ]} - ) == {:ok, {:map, [{:required, :atom, {:union, [:integer, :boolean]}}]}} - - assert unify_lift( - {:map, - [ - {:optional, {:atom, :foo}, :integer}, - {:optional, :atom, {:union, [:integer, :boolean]}} - ]}, - {:map, [{:required, :atom, :integer}]} - ) == {:ok, {:map, [{:required, {:atom, :foo}, :integer}]}} - - # TODO: FIX ME - # assert unify_lift( - # {:map, - # [ - # {:optional, {:atom, :foo}, :integer}, - # {:optional, :atom, {:union, [:integer, :boolean]}} - # ]}, - # {:map, [{:required, :atom, {:union, [:integer, :boolean]}}]} - # ) == - # {:ok, - # {:map, - # [ - # {:required, {:atom, :foo}, :integer}, - # {:required, :atom, {:union, [:integer, :boolean]}} - # ]}} - - assert {:error, _} = - unify_lift( - {:map, - [ - {:optional, {:atom, :foo}, :integer}, - {:optional, :atom, {:union, [:integer, :boolean]}} - ]}, - {:map, [{:optional, :atom, :integer}]} - ) - - assert unify_lift( - {:map, - [ - {:optional, {:atom, :foo}, :integer}, - {:optional, :atom, {:union, [:integer, :boolean]}} - ]}, - {:map, [{:optional, :atom, {:union, [:integer, :boolean]}}]} - ) == - {:ok, - {:map, - [ - {:optional, {:atom, :foo}, :integer}, - {:optional, :atom, {:union, [:integer, :boolean]}} - ]}} - - assert unify_lift( - {:map, [{:optional, :atom, :integer}]}, - {:map, - [ - {:optional, {:atom, :foo}, :integer}, - {:optional, :atom, {:union, [:integer, :boolean]}} - ]} - ) == {:ok, {:map, [{:optional, :atom, :integer}]}} - - assert unify_lift( - {:map, [{:optional, :atom, {:union, [:integer, :boolean]}}]}, - {:map, - [ - {:optional, {:atom, :foo}, :integer}, - {:optional, :atom, {:union, [:integer, :boolean]}} - ]} - ) == {:ok, {:map, [{:optional, :atom, {:union, [:integer, :boolean]}}]}} - end - - test "union" do - assert unify_lift({:union, []}, {:union, []}) == {:ok, {:union, []}} - assert unify_lift({:union, [:integer]}, {:union, [:integer]}) == {:ok, {:union, [:integer]}} - - assert unify_lift({:union, [:integer, :atom]}, {:union, [:integer, :atom]}) == - {:ok, {:union, [:integer, :atom]}} - - assert unify_lift({:union, [:integer, :atom]}, {:union, [:atom, :integer]}) == - {:ok, {:union, [:integer, :atom]}} - - assert unify_lift({:union, [{:atom, :bar}]}, {:union, [:atom]}) == - {:ok, {:atom, :bar}} - - assert {:error, {:unable_unify, {:integer, {:union, [:atom]}, _}}} = - unify_lift({:union, [:integer]}, {:union, [:atom]}) - end - - test "dynamic" do - assert unify_lift({:atom, :foo}, :dynamic) == {:ok, {:atom, :foo}} - assert unify_lift(:dynamic, {:atom, :foo}) == {:ok, {:atom, :foo}} - assert unify_lift(:integer, :dynamic) == {:ok, :integer} - assert unify_lift(:dynamic, :integer) == {:ok, :integer} - end - - test "vars" do - {{:var, 0}, var_context} = new_var({:foo, [version: 0], nil}, new_context()) - {{:var, 1}, var_context} = new_var({:bar, [version: 1], nil}, var_context) - - assert {:ok, {:var, 0}, context} = unify({:var, 0}, :integer, var_context) - assert lift_type({:var, 0}, context) == :integer - - assert {:ok, {:var, 0}, context} = unify(:integer, {:var, 0}, var_context) - assert lift_type({:var, 0}, context) == :integer - - assert {:ok, {:var, _}, context} = unify({:var, 0}, {:var, 1}, var_context) - assert {:var, _} = lift_type({:var, 0}, context) - assert {:var, _} = lift_type({:var, 1}, context) - - assert {:ok, {:var, 0}, context} = unify({:var, 0}, :integer, var_context) - assert {:ok, {:var, 1}, context} = unify({:var, 1}, :integer, context) - assert {:ok, {:var, _}, _context} = unify({:var, 0}, {:var, 1}, context) - - assert {:ok, {:var, 0}, context} = unify({:var, 0}, :integer, var_context) - assert {:ok, {:var, 1}, context} = unify({:var, 1}, :integer, context) - assert {:ok, {:var, _}, _context} = unify({:var, 1}, {:var, 0}, context) - - assert {:ok, {:var, 0}, context} = unify({:var, 0}, :integer, var_context) - assert {:ok, {:var, 1}, context} = unify({:var, 1}, :binary, context) - - assert {:error, {:unable_unify, {:integer, :binary, _}}} = - unify_lift({:var, 0}, {:var, 1}, context) - - assert {:ok, {:var, 0}, context} = unify({:var, 0}, :integer, var_context) - assert {:ok, {:var, 1}, context} = unify({:var, 1}, :binary, context) - - assert {:error, {:unable_unify, {:binary, :integer, _}}} = - unify_lift({:var, 1}, {:var, 0}, context) - end - - test "vars inside tuples" do - {{:var, 0}, var_context} = new_var({:foo, [version: 0], nil}, new_context()) - {{:var, 1}, var_context} = new_var({:bar, [version: 1], nil}, var_context) - - assert {:ok, {:tuple, 1, [{:var, 0}]}, context} = - unify({:tuple, 1, [{:var, 0}]}, {:tuple, 1, [:integer]}, var_context) - - assert lift_type({:var, 0}, context) == :integer - - assert {:ok, {:var, 0}, context} = unify({:var, 0}, :integer, var_context) - assert {:ok, {:var, 1}, context} = unify({:var, 1}, :integer, context) - - assert {:ok, {:tuple, 1, [{:var, _}]}, _context} = - unify({:tuple, 1, [{:var, 0}]}, {:tuple, 1, [{:var, 1}]}, context) - - assert {:ok, {:var, 1}, context} = unify({:var, 1}, {:tuple, 1, [{:var, 0}]}, var_context) - assert {:ok, {:var, 0}, context} = unify({:var, 0}, :integer, context) - assert lift_type({:var, 1}, context) == {:tuple, 1, [:integer]} - - assert {:ok, {:var, 0}, context} = unify({:var, 0}, :integer, var_context) - assert {:ok, {:var, 1}, context} = unify({:var, 1}, :binary, context) - - assert {:error, {:unable_unify, {:integer, :binary, _}}} = - unify_lift({:tuple, 1, [{:var, 0}]}, {:tuple, 1, [{:var, 1}]}, context) - end - - # TODO: Vars inside right unions - - test "vars inside left unions" do - {{:var, 0}, var_context} = new_var({:foo, [version: 0], nil}, new_context()) - - assert {:ok, {:var, 0}, context} = - unify({:union, [{:var, 0}, :integer]}, :integer, var_context) - - assert lift_type({:var, 0}, context) == :integer - - assert {:ok, {:var, 0}, context} = - unify({:union, [{:var, 0}, :integer]}, {:union, [:integer, :atom]}, var_context) - - assert lift_type({:var, 0}, context) == {:union, [:integer, :atom]} - - assert {:error, {:unable_unify, {:integer, {:union, [:binary, :atom]}, _}}} = - unify_lift( - {:union, [{:var, 0}, :integer]}, - {:union, [:binary, :atom]}, - var_context - ) - end - - test "recursive type" do - assert {{:var, 0}, var_context} = new_var({:foo, [version: 0], nil}, new_context()) - assert {{:var, 1}, var_context} = new_var({:bar, [version: 1], nil}, var_context) - assert {{:var, 2}, var_context} = new_var({:baz, [version: 2], nil}, var_context) - - assert {:ok, {:var, _}, context} = unify({:var, 0}, {:var, 1}, var_context) - assert {:ok, {:var, _}, context} = unify({:var, 1}, {:var, 0}, context) - assert context.types[0] == {:var, 1} - assert context.types[1] == {:var, 0} - - assert {:ok, {:var, _}, context} = unify({:var, 0}, :tuple, var_context) - assert {:ok, {:var, _}, context} = unify({:var, 1}, {:var, 0}, context) - assert {:ok, {:var, _}, context} = unify({:var, 0}, {:var, 1}, context) - assert context.types[0] == :tuple - assert context.types[1] == {:var, 0} - - assert {:ok, {:var, _}, context} = unify({:var, 0}, {:var, 1}, var_context) - assert {:ok, {:var, _}, context} = unify({:var, 1}, {:var, 2}, context) - assert {:ok, {:var, _}, _context} = unify({:var, 2}, {:var, 0}, context) - assert context.types[0] == {:var, 1} - assert context.types[1] == {:var, 2} - assert context.types[2] == :unbound - - assert {:ok, {:var, _}, context} = unify({:var, 0}, {:var, 1}, var_context) - - assert {:error, {:unable_unify, {{:var, 1}, {:tuple, 1, [{:var, 0}]}, _}}} = - unify_lift({:var, 1}, {:tuple, 1, [{:var, 0}]}, context) - - assert {:ok, {:var, _}, context} = unify({:var, 0}, {:var, 1}, var_context) - assert {:ok, {:var, _}, context} = unify({:var, 1}, {:var, 2}, context) - - assert {:error, {:unable_unify, {{:var, 2}, {:tuple, 1, [{:var, 0}]}, _}}} = - unify_lift({:var, 2}, {:tuple, 1, [{:var, 0}]}, context) - end - - test "error with internal variable" do - context = new_context() - {var_integer, context} = add_var(context) - {var_atom, context} = add_var(context) - - {:ok, _, context} = unify(var_integer, :integer, context) - {:ok, _, context} = unify(var_atom, :atom, context) - - assert {:error, _} = unify(var_integer, var_atom, context) - end - end - - describe "has_unbound_var?/2" do - setup do - context = new_context() - {unbound_var, context} = add_var(context) - {bound_var, context} = add_var(context) - {:ok, _, context} = unify(bound_var, :integer, context) - %{context: context, unbound_var: unbound_var, bound_var: bound_var} - end - - test "returns true when there are unbound vars", - %{context: context, unbound_var: unbound_var} do - assert has_unbound_var?(unbound_var, context) - assert has_unbound_var?({:union, [unbound_var]}, context) - assert has_unbound_var?({:tuple, 1, [unbound_var]}, context) - assert has_unbound_var?({:list, unbound_var}, context) - assert has_unbound_var?({:map, [{:required, unbound_var, :atom}]}, context) - assert has_unbound_var?({:map, [{:required, :atom, unbound_var}]}, context) - end - - test "returns false when there are no unbound vars", - %{context: context, bound_var: bound_var} do - refute has_unbound_var?(bound_var, context) - refute has_unbound_var?({:union, [bound_var]}, context) - refute has_unbound_var?({:tuple, 1, [bound_var]}, context) - refute has_unbound_var?(:integer, context) - refute has_unbound_var?({:list, bound_var}, context) - refute has_unbound_var?({:map, [{:required, :atom, :atom}]}, context) - refute has_unbound_var?({:map, [{:required, bound_var, :atom}]}, context) - refute has_unbound_var?({:map, [{:required, :atom, bound_var}]}, context) - end - end - - describe "subtype?/3" do - test "with simple types" do - assert subtype?({:atom, :foo}, :atom, new_context()) - assert subtype?({:atom, true}, :atom, new_context()) - - refute subtype?(:integer, :binary, new_context()) - refute subtype?(:atom, {:atom, :foo}, new_context()) - refute subtype?(:atom, {:atom, true}, new_context()) - end - - test "with composite types" do - assert subtype?({:list, {:atom, :foo}}, {:list, :atom}, new_context()) - assert subtype?({:tuple, 1, [{:atom, :foo}]}, {:tuple, 1, [:atom]}, new_context()) - - refute subtype?({:list, :atom}, {:list, {:atom, :foo}}, new_context()) - refute subtype?({:tuple, 1, [:atom]}, {:tuple, 1, [{:atom, :foo}]}, new_context()) - refute subtype?({:tuple, 1, [:atom]}, {:tuple, 2, [:atom, :atom]}, new_context()) - refute subtype?({:tuple, 2, [:atom, :atom]}, {:tuple, 1, [:atom]}, new_context()) - - refute subtype?( - {:tuple, 2, [{:atom, :a}, :integer]}, - {:tuple, 2, [{:atom, :b}, :integer]}, - new_context() - ) - end - - test "with maps" do - assert subtype?({:map, [{:optional, :atom, :integer}]}, {:map, []}, new_context()) - - assert subtype?( - {:map, [{:required, :atom, :integer}]}, - {:map, [{:required, :atom, :integer}]}, - new_context() - ) - - assert subtype?( - {:map, [{:required, {:atom, :foo}, :integer}]}, - {:map, [{:required, :atom, :integer}]}, - new_context() - ) - - assert subtype?( - {:map, [{:required, :integer, {:atom, :foo}}]}, - {:map, [{:required, :integer, :atom}]}, - new_context() - ) - - refute subtype?({:map, [{:required, :atom, :integer}]}, {:map, []}, new_context()) - - refute subtype?( - {:map, [{:required, :atom, :integer}]}, - {:map, [{:required, {:atom, :foo}, :integer}]}, - new_context() - ) - - refute subtype?( - {:map, [{:required, :integer, :atom}]}, - {:map, [{:required, :integer, {:atom, :foo}}]}, - new_context() - ) - end - - test "with unions" do - assert subtype?({:union, [{:atom, :foo}]}, {:union, [:atom]}, new_context()) - assert subtype?({:union, [{:atom, :foo}, {:atom, :bar}]}, {:union, [:atom]}, new_context()) - assert subtype?({:union, [{:atom, :foo}]}, {:union, [:integer, :atom]}, new_context()) - - assert subtype?({:atom, :foo}, {:union, [:atom]}, new_context()) - assert subtype?({:atom, :foo}, {:union, [:integer, :atom]}, new_context()) - - assert subtype?({:union, [{:atom, :foo}]}, :atom, new_context()) - assert subtype?({:union, [{:atom, :foo}, {:atom, :bar}]}, :atom, new_context()) - - refute subtype?({:union, [:atom]}, {:union, [{:atom, :foo}]}, new_context()) - refute subtype?({:union, [:atom]}, {:union, [{:atom, :foo}, :integer]}, new_context()) - refute subtype?(:atom, {:union, [{:atom, :foo}, :integer]}, new_context()) - refute subtype?({:union, [:atom]}, {:atom, :foo}, new_context()) - end - end - - test "to_union/2" do - assert to_union([:atom], new_context()) == :atom - assert to_union([:integer, :integer], new_context()) == :integer - assert to_union([{:atom, :foo}, {:atom, :bar}, :atom], new_context()) == :atom - - assert to_union([:binary, :atom], new_context()) == {:union, [:binary, :atom]} - assert to_union([:atom, :binary, :atom], new_context()) == {:union, [:atom, :binary]} - - assert to_union([{:atom, :foo}, :binary, :atom], new_context()) == - {:union, [:binary, :atom]} - - {{:var, 0}, var_context} = new_var({:foo, [version: 0], nil}, new_context()) - assert to_union([{:var, 0}], var_context) == {:var, 0} - - assert to_union([{:tuple, 1, [:integer]}, {:tuple, 1, [:integer]}], new_context()) == - {:tuple, 1, [:integer]} - end - - test "flatten_union/1" do - context = new_context() - assert flatten_union(:binary, context) == [:binary] - assert flatten_union({:atom, :foo}, context) == [{:atom, :foo}] - assert flatten_union({:union, [:binary, {:atom, :foo}]}, context) == [:binary, {:atom, :foo}] - - assert flatten_union({:union, [{:union, [:integer, :binary]}, {:atom, :foo}]}, context) == [ - :integer, - :binary, - {:atom, :foo} - ] - - assert flatten_union({:tuple, 2, [:binary, {:atom, :foo}]}, context) == - [{:tuple, 2, [:binary, {:atom, :foo}]}] - - assert flatten_union({:tuple, 1, [{:union, [:binary, :integer]}]}, context) == - [{:tuple, 1, [:binary]}, {:tuple, 1, [:integer]}] - - assert flatten_union( - {:tuple, 2, [{:union, [:binary, :integer]}, {:union, [:binary, :integer]}]}, - context - ) == - [ - {:tuple, 2, [:binary, :binary]}, - {:tuple, 2, [:binary, :integer]}, - {:tuple, 2, [:integer, :binary]}, - {:tuple, 2, [:integer, :integer]} - ] - - {{:var, 0}, var_context} = new_var({:foo, [version: 0], nil}, new_context()) - assert flatten_union({:var, 0}, var_context) == [{:var, 0}] - - {:ok, {:var, 0}, var_context} = unify({:var, 0}, :integer, var_context) - assert flatten_union({:var, 0}, var_context) == [:integer] - - {{:var, 0}, var_context} = new_var({:foo, [version: 0], nil}, new_context()) - {:ok, {:var, 0}, var_context} = unify({:var, 0}, {:union, [:integer, :float]}, var_context) - assert flatten_union({:var, 0}, var_context) == [:integer, :float] - - {{:var, 0}, var_context} = new_var({:foo, [version: 0], nil}, new_context()) - {{:var, 1}, var_context} = new_var({:bar, [version: 1], nil}, var_context) - {:ok, {:var, 0}, var_context} = unify({:var, 0}, {:var, 1}, var_context) - assert flatten_union({:var, 0}, var_context) == [{:var, 1}] - assert flatten_union({:var, 1}, var_context) == [{:var, 1}] - end - - test "format_type/1" do - assert format_type_string(:binary, false) == "binary()" - assert format_type_string({:atom, true}, false) == "true" - assert format_type_string({:atom, :atom}, false) == ":atom" - assert format_type_string({:list, :binary}, false) == "[binary()]" - assert format_type_string({:tuple, 0, []}, false) == "{}" - assert format_type_string({:tuple, 1, [:integer]}, false) == "{integer()}" - - assert format_type_string({:map, []}, true) == "map()" - assert format_type_string({:map, [{:required, {:atom, :foo}, :atom}]}, true) == "map()" - - assert format_type_string({:map, []}, false) == - "%{}" - - assert format_type_string({:map, [{:required, {:atom, :foo}, :atom}]}, false) == - "%{foo: atom()}" - - assert format_type_string({:map, [{:required, :integer, :atom}]}, false) == - "%{integer() => atom()}" - - assert format_type_string({:map, [{:optional, :integer, :atom}]}, false) == - "%{optional(integer()) => atom()}" - - assert format_type_string({:map, [{:optional, {:atom, :foo}, :atom}]}, false) == - "%{optional(:foo) => atom()}" - - assert format_type_string({:map, [{:required, {:atom, :__struct__}, {:atom, Struct}}]}, false) == - "%Struct{}" - - assert format_type_string( - {:map, - [{:required, {:atom, :__struct__}, {:atom, Struct}}, {:required, :integer, :atom}]}, - false - ) == - "%Struct{integer() => atom()}" - - assert format_type_string({:fun, [{[], :dynamic}]}, false) == "(-> dynamic())" - - assert format_type_string({:fun, [{[:integer], :dynamic}]}, false) == - "(integer() -> dynamic())" - - assert format_type_string({:fun, [{[:integer, :float], :dynamic}]}, false) == - "(integer(), float() -> dynamic())" - - assert format_type_string({:fun, [{[:integer], :dynamic}, {[:integer], :dynamic}]}, false) == - "(integer() -> dynamic(); integer() -> dynamic())" - end - - test "walk/3" do - assert walk(:dynamic, :acc, fn :dynamic, :acc -> {:integer, :bar} end) == {:integer, :bar} - - assert walk({:list, {:tuple, [:integer, :binary]}}, 1, fn type, counter -> - {type, counter + 1} - end) == {{:list, {:tuple, [:integer, :binary]}}, 3} - end -end From 6db9cc235e4c578e8277efd29b8124301ddc4bba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sat, 6 Jan 2024 21:17:33 +0100 Subject: [PATCH 0281/1886] Track variable definition in types --- lib/elixir/lib/module/types.ex | 12 +++--- lib/elixir/lib/module/types/expr.ex | 56 +++++++++++++------------- lib/elixir/lib/module/types/helpers.ex | 29 ++++++++++++- lib/elixir/lib/module/types/pattern.ex | 20 ++++----- 4 files changed, 69 insertions(+), 48 deletions(-) diff --git a/lib/elixir/lib/module/types.ex b/lib/elixir/lib/module/types.ex index efbc065619..8e18b24978 100644 --- a/lib/elixir/lib/module/types.ex +++ b/lib/elixir/lib/module/types.ex @@ -1,10 +1,6 @@ defmodule Module.Types do @moduledoc false - defmodule Error do - defexception [:message] - end - alias Module.Types.{Expr, Pattern} @doc false @@ -22,7 +18,7 @@ defmodule Module.Types do def_expr = {kind, meta, [guards_to_expr(guards, {fun, [], args}), [do: body]]} error = - Error.exception(""" + RuntimeError.exception(""" found error while checking types for #{Exception.format_mfa(module, fun, arity)}: #{Exception.format_banner(:error, e, __STACKTRACE__)}\ @@ -31,7 +27,7 @@ defmodule Module.Types do #{Macro.to_string(def_expr)} - In case it is a bug, please report it at: https://github.com/elixir-lang/elixir/issues + Please report this bug at: https://github.com/elixir-lang/elixir/issues """) reraise error, __STACKTRACE__ @@ -84,7 +80,9 @@ defmodule Module.Types do def context() do %{ # A list of all warnings found so far - warnings: [] + warnings: [], + # Information about all vars and their types + vars: %{} } end diff --git a/lib/elixir/lib/module/types/expr.ex b/lib/elixir/lib/module/types/expr.ex index ab06ab7bce..0c4e42b21f 100644 --- a/lib/elixir/lib/module/types/expr.ex +++ b/lib/elixir/lib/module/types/expr.ex @@ -33,6 +33,24 @@ defmodule Module.Types.Expr do {:ok, pid(), context} end + # [] + def of_expr([], _expected, _stack, context) do + {:ok, empty_list(), context} + end + + # [expr, ...] + def of_expr(exprs, _expected, stack, context) when is_list(exprs) do + case map_reduce_ok(exprs, context, &of_expr(&1, stack, &2)) do + {:ok, _types, context} -> {:ok, non_empty_list(), context} + {:error, reason} -> {:error, reason} + end + end + + # {left, right} + def of_expr({left, right}, expected, stack, context) do + of_expr({:{}, [], [left, right]}, expected, stack, context) + end + # <<...>>> def of_expr({:<<>>, _meta, args}, _expected, stack, context) do case Of.binary(args, :expr, stack, context, &of_expr/3) do @@ -57,19 +75,6 @@ defmodule Module.Types.Expr do end end - # [] - def of_expr([], _expected, _stack, context) do - {:ok, empty_list(), context} - end - - # [expr, ...] - def of_expr(exprs, _expected, stack, context) when is_list(exprs) do - case map_reduce_ok(exprs, context, &of_expr(&1, stack, &2)) do - {:ok, _types, context} -> {:ok, non_empty_list(), context} - {:error, reason} -> {:error, reason} - end - end - # __CALLER__ def of_expr({:__CALLER__, _meta, var_context}, _expected, _stack, context) when is_atom(var_context) do @@ -82,16 +87,6 @@ defmodule Module.Types.Expr do {:ok, dynamic(), context} end - # var - def of_expr(var, _expected, _stack, context) when is_var(var) do - {:ok, dynamic(), context} - end - - # {left, right} - def of_expr({left, right}, expected, stack, context) do - of_expr({:{}, [], [left, right]}, expected, stack, context) - end - # {...} def of_expr({:{}, _meta, exprs}, _expected, stack, context) do case map_reduce_ok(exprs, context, &of_expr(&1, stack, &2)) do @@ -202,12 +197,14 @@ defmodule Module.Types.Expr do reduce_ok(blocks, context, fn {:rescue, clauses}, context -> reduce_ok(clauses, context, fn - {:->, _, [[{:in, _, [_var, _exceptions]}], body]}, context -> - # TODO: make sure var is defined in context + {:->, _, [[{:in, _, [var, _exceptions]}], body]}, context -> + # TODO: Vars are a union of the structs above + {_version, _type, context} = new_var(var, dynamic(), context) of_expr_context(body, stack, context) - {:->, _, [[_var], body]}, context -> - # TODO: make sure var is defined in context + {:->, _, [[var], body]}, context -> + # TODO: Vars are structs with the exception field and that's it + {_version, _type, context} = new_var(var, dynamic(), context) of_expr_context(body, stack, context) end) @@ -333,6 +330,11 @@ defmodule Module.Types.Expr do end end + # var + def of_expr(var, _expected, _stack, context) when is_var(var) do + {:ok, fetch_var!(var, context), context} + end + defp for_clause({:<-, _, [left, expr]}, stack, context) do {pattern, guards} = extract_head([left]) diff --git a/lib/elixir/lib/module/types/helpers.ex b/lib/elixir/lib/module/types/helpers.ex index eecb7a6b5c..716c66dc90 100644 --- a/lib/elixir/lib/module/types/helpers.ex +++ b/lib/elixir/lib/module/types/helpers.ex @@ -15,9 +15,34 @@ defmodule Module.Types.Helpers do end @doc """ - Returns unique identifier for the current assignment of the variable. + Defines a new variable. """ - def var_name({_name, meta, _context}), do: Keyword.fetch!(meta, :version) + def new_var({:_, _meta, _var_context}, type, context) do + {nil, type, context} + end + + def new_var({var_name, meta, var_context}, type, context) do + version = Keyword.fetch!(meta, :version) + + # TODO: What happens if the variable is defined with another type? + case context.vars do + %{^version => data} -> + {version, type, put_in(context.vars[version], %{data | type: type})} + + %{} -> + {version, type, + put_in(context.vars[version], %{type: type, name: var_name, context: var_context})} + end + end + + @doc """ + Fetches the type of a defined variable. + """ + def fetch_var!({_name, meta, _context}, context) do + version = Keyword.fetch!(meta, :version) + %{vars: %{^version => %{type: type}}} = context + type + end @doc """ Returns the AST metadata. diff --git a/lib/elixir/lib/module/types/pattern.ex b/lib/elixir/lib/module/types/pattern.ex index ad1ebc07d5..9fb17e275c 100644 --- a/lib/elixir/lib/module/types/pattern.ex +++ b/lib/elixir/lib/module/types/pattern.ex @@ -19,19 +19,9 @@ defmodule Module.Types.Pattern do Return the type and typing context of a pattern expression or an error in case of a typing conflict. """ - # _ - def of_pattern({:_, _meta, atom}, _stack, context) when is_atom(atom) do - {:ok, dynamic(), context} - end - # ^var - def of_pattern({:^, _meta, [_var]}, _stack, context) do - {:ok, dynamic(), context} - end - - # var - def of_pattern(var, _stack, context) when is_var(var) do - {:ok, dynamic(), context} + def of_pattern({:^, _meta, [var]}, _stack, context) do + {:ok, fetch_var!(var, context), context} end # left = right @@ -71,6 +61,12 @@ defmodule Module.Types.Pattern do end end + # var or _ + def of_pattern(var, _stack, context) when is_var(var) do + {_version, type, context} = new_var(var, dynamic(), context) + {:ok, type, context} + end + def of_pattern(expr, stack, context) do of_shared(expr, stack, context, &of_pattern/3) end From ef9bcf0837bcc659e370c285fcc28797972d7217 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sat, 6 Jan 2024 21:36:12 +0100 Subject: [PATCH 0282/1886] Add TODOs to all expressions to still analyse --- lib/elixir/lib/module/types/expr.ex | 135 +++++++++++++--------------- 1 file changed, 64 insertions(+), 71 deletions(-) diff --git a/lib/elixir/lib/module/types/expr.ex b/lib/elixir/lib/module/types/expr.ex index 0c4e42b21f..13b91e3692 100644 --- a/lib/elixir/lib/module/types/expr.ex +++ b/lib/elixir/lib/module/types/expr.ex @@ -4,42 +4,38 @@ defmodule Module.Types.Expr do alias Module.Types.{Of, Pattern} import Module.Types.{Helpers, Descr} - def of_expr(ast, stack, context) do - of_expr(ast, term(), stack, context) - end - # :atom - def of_expr(atom, _expected, _stack, context) when is_atom(atom) do + def of_expr(atom, _stack, context) when is_atom(atom) do {:ok, atom(atom), context} end # 12 - def of_expr(literal, _expected, _stack, context) when is_integer(literal) do + def of_expr(literal, _stack, context) when is_integer(literal) do {:ok, integer(), context} end # 1.2 - def of_expr(literal, _expected, _stack, context) when is_float(literal) do + def of_expr(literal, _stack, context) when is_float(literal) do {:ok, float(), context} end # "..." - def of_expr(literal, _expected, _stack, context) when is_binary(literal) do + def of_expr(literal, _stack, context) when is_binary(literal) do {:ok, binary(), context} end # #PID<...> - def of_expr(literal, _expected, _stack, context) when is_pid(literal) do + def of_expr(literal, _stack, context) when is_pid(literal) do {:ok, pid(), context} end # [] - def of_expr([], _expected, _stack, context) do + def of_expr([], _stack, context) do {:ok, empty_list(), context} end - # [expr, ...] - def of_expr(exprs, _expected, stack, context) when is_list(exprs) do + # TODO: [expr, ...] + def of_expr(exprs, stack, context) when is_list(exprs) do case map_reduce_ok(exprs, context, &of_expr(&1, stack, &2)) do {:ok, _types, context} -> {:ok, non_empty_list(), context} {:error, reason} -> {:error, reason} @@ -47,25 +43,25 @@ defmodule Module.Types.Expr do end # {left, right} - def of_expr({left, right}, expected, stack, context) do - of_expr({:{}, [], [left, right]}, expected, stack, context) + def of_expr({left, right}, stack, context) do + of_expr({:{}, [], [left, right]}, stack, context) end - # <<...>>> - def of_expr({:<<>>, _meta, args}, _expected, stack, context) do + # TODO: <<...>>> + def of_expr({:<<>>, _meta, args}, stack, context) do case Of.binary(args, :expr, stack, context, &of_expr/3) do {:ok, context} -> {:ok, binary(), context} {:error, reason} -> {:error, reason} end end - # left | [] - def of_expr({:|, _meta, [left_expr, []]}, expected, stack, context) do - of_expr(left_expr, expected, stack, context) + # TODO: left | [] + def of_expr({:|, _meta, [left_expr, []]}, stack, context) do + of_expr(left_expr, stack, context) end - # left | right - def of_expr({:|, _meta, [left_expr, right_expr]}, _expected, stack, context) do + # TODO: left | right + def of_expr({:|, _meta, [left_expr, right_expr]}, stack, context) do case of_expr(left_expr, stack, context) do {:ok, _left, context} -> of_expr(right_expr, stack, context) @@ -75,47 +71,45 @@ defmodule Module.Types.Expr do end end - # __CALLER__ - def of_expr({:__CALLER__, _meta, var_context}, _expected, _stack, context) + # TODO: __CALLER__ + def of_expr({:__CALLER__, _meta, var_context}, _stack, context) when is_atom(var_context) do {:ok, dynamic(), context} end - # __STACKTRACE__ - def of_expr({:__STACKTRACE__, _meta, var_context}, _expected, _stack, context) + # TODO: __STACKTRACE__ + def of_expr({:__STACKTRACE__, _meta, var_context}, _stack, context) when is_atom(var_context) do {:ok, dynamic(), context} end - # {...} - def of_expr({:{}, _meta, exprs}, _expected, stack, context) do + # TODO: {...} + def of_expr({:{}, _meta, exprs}, stack, context) do case map_reduce_ok(exprs, context, &of_expr(&1, stack, &2)) do {:ok, _types, context} -> {:ok, tuple(), context} {:error, reason} -> {:error, reason} end end - # left = right - def of_expr({:=, _meta, [left_expr, right_expr]}, _expected, stack, context) do - with {:ok, left_type, context} <- - Pattern.of_pattern(left_expr, stack, context), - {:ok, right_type, context} <- of_expr(right_expr, left_type, stack, context) do + # TODO: left = right + def of_expr({:=, _meta, [left_expr, right_expr]}, stack, context) do + with {:ok, _left_type, context} <- Pattern.of_pattern(left_expr, stack, context), + {:ok, right_type, context} <- of_expr(right_expr, stack, context) do {:ok, right_type, context} end end - # %{map | ...} - def of_expr({:%{}, _, [{:|, _, [map, args]}]}, _expected, stack, context) do + # TODO: %{map | ...} + def of_expr({:%{}, _, [{:|, _, [map, args]}]}, stack, context) do with {:ok, _, context} <- of_expr(map, stack, context), {:ok, _, context} <- Of.closed_map(args, stack, context, &of_expr/3) do {:ok, map(), context} end end - # %Struct{map | ...} + # TODO: %Struct{map | ...} def of_expr( {:%, meta, [module, {:%{}, _, [{:|, _, [_, _]}]} = update]}, - _expected, stack, context ) do @@ -125,13 +119,13 @@ defmodule Module.Types.Expr do end end - # %{...} - def of_expr({:%{}, _meta, args}, _expected, stack, context) do + # TODO: %{...} + def of_expr({:%{}, _meta, args}, stack, context) do Of.closed_map(args, stack, context, &of_expr/3) end - # %Struct{...} - def of_expr({:%, meta1, [module, {:%{}, _meta2, args}]}, _expected, stack, context) do + # TODO: %Struct{...} + def of_expr({:%, meta1, [module, {:%{}, _meta2, args}]}, stack, context) do with {:ok, _, context} <- Of.struct(module, meta1, stack, context), {:ok, _, context} <- Of.open_map(args, stack, context, &of_expr/3) do {:ok, map(), context} @@ -139,12 +133,12 @@ defmodule Module.Types.Expr do end # () - def of_expr({:__block__, _meta, []}, _expected, _stack, context) do + def of_expr({:__block__, _meta, []}, _stack, context) do {:ok, atom(nil), context} end # (expr; expr) - def of_expr({:__block__, _meta, exprs}, expected, stack, context) do + def of_expr({:__block__, _meta, exprs}, stack, context) do {pre, [post]} = Enum.split(exprs, -1) result = @@ -153,13 +147,13 @@ defmodule Module.Types.Expr do end) case result do - {:ok, _, context} -> of_expr(post, expected, stack, context) + {:ok, _, context} -> of_expr(post, stack, context) {:error, reason} -> {:error, reason} end end - # cond do pat -> expr end - def of_expr({:cond, _meta, [[{:do, clauses}]]}, _expected, stack, context) do + # TODO: cond do pat -> expr end + def of_expr({:cond, _meta, [[{:do, clauses}]]}, stack, context) do {result, context} = reduce_ok(clauses, context, fn {:->, _meta, [head, body]}, context -> with {:ok, _, context} <- of_expr(head, stack, context), @@ -173,15 +167,15 @@ defmodule Module.Types.Expr do end end - # case expr do pat -> expr end - def of_expr({:case, _meta, [case_expr, [{:do, clauses}]]}, _expected, stack, context) do + # TODO: case expr do pat -> expr end + def of_expr({:case, _meta, [case_expr, [{:do, clauses}]]}, stack, context) do with {:ok, _expr_type, context} <- of_expr(case_expr, stack, context), {:ok, context} <- of_clauses(clauses, stack, context), do: {:ok, dynamic(), context} end - # fn pat -> expr end - def of_expr({:fn, _meta, clauses}, _expected, stack, context) do + # TODO: fn pat -> expr end + def of_expr({:fn, _meta, clauses}, stack, context) do case of_clauses(clauses, stack, context) do {:ok, context} -> {:ok, dynamic(), context} {:error, reason} -> {:error, reason} @@ -191,8 +185,8 @@ defmodule Module.Types.Expr do @try_blocks [:do, :after] @try_clause_blocks [:catch, :else, :after] - # try do expr end - def of_expr({:try, _meta, [blocks]}, _expected, stack, context) do + # TODO: try do expr end + def of_expr({:try, _meta, [blocks]}, stack, context) do {result, context} = reduce_ok(blocks, context, fn {:rescue, clauses}, context -> @@ -221,8 +215,8 @@ defmodule Module.Types.Expr do end end - # receive do pat -> expr end - def of_expr({:receive, _meta, [blocks]}, _expected, stack, context) do + # TODO: receive do pat -> expr end + def of_expr({:receive, _meta, [blocks]}, stack, context) do {result, context} = reduce_ok(blocks, context, fn {:do, {:__block__, _, []}}, context -> @@ -243,8 +237,8 @@ defmodule Module.Types.Expr do end end - # for pat <- expr do expr end - def of_expr({:for, _meta, [_ | _] = args}, _expected, stack, context) do + # TODO: for pat <- expr do expr end + def of_expr({:for, _meta, [_ | _] = args}, stack, context) do {clauses, [[{:do, block} | opts]]} = Enum.split(args, -1) with {:ok, context} <- reduce_ok(clauses, context, &for_clause(&1, stack, &2)), @@ -261,16 +255,16 @@ defmodule Module.Types.Expr do end end - # with pat <- expr do expr end - def of_expr({:with, _meta, [_ | _] = clauses}, _expected, stack, context) do + # TODO: with pat <- expr do expr end + def of_expr({:with, _meta, [_ | _] = clauses}, stack, context) do case reduce_ok(clauses, context, &with_clause(&1, stack, &2)) do {:ok, context} -> {:ok, dynamic(), context} {:error, reason} -> {:error, reason} end end - # fun.(args) - def of_expr({{:., _meta1, [fun]}, _meta2, args}, _expected, stack, context) do + # TODO: fun.(args) + def of_expr({{:., _meta1, [fun]}, _meta2, args}, stack, context) do with {:ok, _fun_type, context} <- of_expr(fun, stack, context), {:ok, _arg_types, context} <- map_reduce_ok(args, context, &of_expr(&1, stack, &2)) do @@ -278,8 +272,8 @@ defmodule Module.Types.Expr do end end - # expr.key_or_fun - def of_expr({{:., _meta1, [expr1, _key_or_fun]}, meta2, []}, _expected, stack, context) + # TODO: expr.key_or_fun + def of_expr({{:., _meta1, [expr1, _key_or_fun]}, meta2, []}, stack, context) when not is_atom(expr1) do if Keyword.get(meta2, :no_parens, false) do with {:ok, _, context} <- of_expr(expr1, stack, context) do @@ -292,8 +286,8 @@ defmodule Module.Types.Expr do end end - # expr.fun(arg) - def of_expr({{:., _meta1, [expr1, fun]}, meta2, args}, _expected, stack, context) do + # TODO: expr.fun(arg) + def of_expr({{:., _meta1, [expr1, fun]}, meta2, args}, stack, context) do context = Of.remote(expr1, fun, length(args), meta2, stack, context) with {:ok, _expr_type, context} <- of_expr(expr1, stack, context), @@ -303,10 +297,9 @@ defmodule Module.Types.Expr do end end - # &Foo.bar/1 + # TODO: &Foo.bar/1 def of_expr( {:&, _, [{:/, _, [{{:., _, [module, fun]}, meta, []}, arity]}]}, - _expected, stack, context ) @@ -316,13 +309,13 @@ defmodule Module.Types.Expr do end # &foo/1 - # & &1 - def of_expr({:&, _meta, _arg}, _expected, _stack, context) do + # TODO: & &1 + def of_expr({:&, _meta, _arg}, _stack, context) do {:ok, dynamic(), context} end - # fun(arg) - def of_expr({fun, _meta, args}, _expected, stack, context) + # TODO: fun(arg) + def of_expr({fun, _meta, args}, stack, context) when is_atom(fun) and is_list(args) do with {:ok, _arg_types, context} <- map_reduce_ok(args, context, &of_expr(&1, stack, &2)) do @@ -330,8 +323,8 @@ defmodule Module.Types.Expr do end end - # var - def of_expr(var, _expected, _stack, context) when is_var(var) do + # TODO: var + def of_expr(var, _stack, context) when is_var(var) do {:ok, fetch_var!(var, context), context} end From 3ae8475e41bb1ebe81c2e0954c88381ebe31afab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sun, 7 Jan 2024 19:30:48 +0100 Subject: [PATCH 0283/1886] Add bitmask types to descr (#13230) The bitmap represents all non-divisible types. Currently it implements all types in our system, but atoms, lists, tuples, maps, and functions will be given more precise types later on. Dynamic has not been yet implemented nor optimizations for term. --- lib/elixir/lib/module/types/descr.ex | 186 +++++++++++++++++- lib/elixir/src/elixir_compiler.erl | 1 + .../test/elixir/module/types/descr_test.exs | 74 +++++++ 3 files changed, 251 insertions(+), 10 deletions(-) create mode 100644 lib/elixir/test/elixir/module/types/descr_test.exs diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex index 15e02baee3..52b993bcf7 100644 --- a/lib/elixir/lib/module/types/descr.ex +++ b/lib/elixir/lib/module/types/descr.ex @@ -1,14 +1,180 @@ defmodule Module.Types.Descr do @moduledoc false - def term(), do: :term - def atom(_atom), do: :atom + + # The descr contains a set-theoretic implementation of types. + # Types are represented as maps of non-overlapping unions. + # A bitmap is used to represent non-divisible types. All other + # types require specific data structures. + import Bitwise + + @binary 1 <<< 1 + @empty_list 1 <<< 2 + @integer 1 <<< 3 + @float 1 <<< 4 + @pid 1 <<< 5 + @port 1 <<< 6 + @reference 1 <<< 7 + + @atom 1 <<< 8 + @non_empty_list 1 <<< 9 + @map 1 <<< 10 + @tuple 1 <<< 11 + @fun 1 <<< 12 + @top (1 <<< 13) - 1 + + # Type definitions + def dynamic(), do: :dynamic - def integer(), do: :integer - def float(), do: :float - def binary(), do: :binary - def pid(), do: :pid - def tuple(), do: :tuple - def empty_list(), do: :list - def non_empty_list(), do: :list - def map(), do: :map + def term(), do: %{bitmap: @top} + def none(), do: %{} + + def atom(_atom), do: %{bitmap: @atom} + def binary(), do: %{bitmap: @binary} + def empty_list(), do: %{bitmap: @empty_list} + def integer(), do: %{bitmap: @integer} + def float(), do: %{bitmap: @float} + def fun(), do: %{bitmap: @fun} + def map(), do: %{bitmap: @map} + def non_empty_list(), do: %{bitmap: @non_empty_list} + def pid(), do: %{bitmap: @pid} + def port(), do: %{bitmap: @port} + def reference(), do: %{bitmap: @reference} + def tuple(), do: %{bitmap: @tuple} + + ## Set operations + + @doc """ + Computes the union of two descrs. + """ + def union(%{} = left, %{} = right) do + # Erlang maps:merge_with/3 has to preserve the order in combiner. + # We don't care about the order, so we have a faster implementation. + if map_size(left) > map_size(right) do + iterator_union(:maps.next(:maps.iterator(right)), left) + else + iterator_union(:maps.next(:maps.iterator(left)), right) + end + end + + @compile {:inline, union: 3} + defp union(:bitmap, v1, v2), do: bitmap_union(v1, v2) + + @doc """ + Computes the intersection of two descrs. + """ + def intersection(%{} = left, %{} = right) do + # Erlang maps:intersect_with/3 has to preserve the order in combiner. + # We don't care about the order, so we have a faster implementation. + if map_size(left) > map_size(right) do + iterator_intersection(:maps.next(:maps.iterator(right)), left, []) + else + iterator_intersection(:maps.next(:maps.iterator(left)), right, []) + end + end + + # Returning 0 from the callback is taken as none() for that subtype. + @compile {:inline, intersection: 3} + defp intersection(:bitmap, v1, v2), do: bitmap_intersection(v1, v2) + + @doc """ + Computes the difference between two types. + """ + def difference(left = %{}, right = %{}) do + iterator_difference(:maps.next(:maps.iterator(right)), left) + end + + # Returning 0 from the callback is taken as none() for that subtype. + @compile {:inline, difference: 3} + defp difference(:bitmap, v1, v2), do: bitmap_difference(v1, v2) + + @doc """ + Converts a descr to its quoted representation. + """ + def to_quoted(%{} = descr) do + case Enum.flat_map(descr, fn {key, value} -> to_quoted(key, value) end) do + [] -> {:none, [], []} + unions -> unions |> Enum.sort() |> Enum.reduce(&{:or, [], [&2, &1]}) + end + end + + @compile {:inline, to_quoted: 2} + defp to_quoted(:bitmap, val), do: bitmap_to_quoted(val) + + ## Iterator helpers + + defp iterator_union({key, v1, iterator}, map) do + acc = + case map do + %{^key => v2} -> %{map | key => union(key, v1, v2)} + %{} -> Map.put(map, key, v1) + end + + iterator_union(:maps.next(iterator), acc) + end + + defp iterator_union(:none, map), do: map + + defp iterator_intersection({key, v1, iterator}, map, acc) do + acc = + case map do + %{^key => v2} -> + case intersection(key, v1, v2) do + 0 -> acc + value -> [{key, value} | acc] + end + + %{} -> + acc + end + + iterator_intersection(:maps.next(iterator), map, acc) + end + + defp iterator_intersection(:none, _map, acc), do: :maps.from_list(acc) + + defp iterator_difference({key, v2, iterator}, map) do + acc = + case map do + %{^key => v1} -> + case difference(key, v1, v2) do + 0 -> Map.delete(map, key) + value -> %{map | key => value} + end + + %{} -> + map + end + + iterator_difference(:maps.next(iterator), acc) + end + + defp iterator_difference(:none, map), do: map + + ## Bitmaps + + defp bitmap_union(v1, v2), do: v1 ||| v2 + defp bitmap_intersection(v1, v2), do: v1 &&& v2 + defp bitmap_difference(v1, v2), do: v1 - (v1 &&& v2) + + defp bitmap_to_quoted(val) do + pairs = + [ + binary: @binary, + empty_list: @empty_list, + integer: @integer, + float: @float, + pid: @pid, + port: @port, + reference: @reference, + atom: @atom, + non_empty_list: @non_empty_list, + map: @map, + tuple: @tuple, + fun: @fun + ] + + for {type, mask} <- pairs, + (mask &&& val) !== 0, + do: {type, [], []} + end end diff --git a/lib/elixir/src/elixir_compiler.erl b/lib/elixir/src/elixir_compiler.erl index fb4963a4ca..7d7cf88430 100644 --- a/lib/elixir/src/elixir_compiler.erl +++ b/lib/elixir/src/elixir_compiler.erl @@ -183,6 +183,7 @@ bootstrap_files() -> ], [ <<"list/chars.ex">>, + <<"bitwise.ex">>, <<"module/locals_tracker.ex">>, <<"module/parallel_checker.ex">>, <<"module/behaviour.ex">>, diff --git a/lib/elixir/test/elixir/module/types/descr_test.exs b/lib/elixir/test/elixir/module/types/descr_test.exs new file mode 100644 index 0000000000..a1e2702fc9 --- /dev/null +++ b/lib/elixir/test/elixir/module/types/descr_test.exs @@ -0,0 +1,74 @@ +Code.require_file("type_helper.exs", __DIR__) + +defmodule Module.Types.DescrTest do + use ExUnit.Case, async: true + + import Module.Types.Descr + + defp to_quoted_string(descr), do: Macro.to_string(to_quoted(descr)) + + describe "union" do + test "bitmap" do + assert union(integer(), float()) == union(float(), integer()) + end + + test "term" do + assert union(term(), float()) == term() + assert union(term(), binary()) == term() + end + + test "none" do + assert union(none(), float()) == float() + assert union(none(), binary()) == binary() + end + end + + describe "intersection" do + test "bitmap" do + assert intersection(integer(), union(integer(), float())) == integer() + assert intersection(integer(), float()) == none() + end + + test "term" do + assert intersection(term(), float()) == float() + assert intersection(term(), binary()) == binary() + end + + test "none" do + assert intersection(none(), float()) == none() + assert intersection(none(), binary()) == none() + end + end + + describe "difference" do + test "bitmap" do + assert difference(float(), integer()) == float() + assert difference(union(float(), integer()), integer()) == float() + assert difference(union(float(), integer()), binary()) == union(float(), integer()) + end + + test "term" do + assert difference(float(), term()) == none() + assert difference(float(), term()) == none() + end + + test "none" do + assert difference(none(), integer()) == none() + assert difference(none(), float()) == none() + + assert difference(integer(), none()) == integer() + assert difference(float(), none()) == float() + end + end + + describe "to_quoted" do + test "bitmap" do + assert union(integer(), union(float(), binary())) |> to_quoted_string() == + "binary() or float() or integer()" + end + + test "none" do + assert none() |> to_quoted_string() == "none()" + end + end +end From 818734b8e15031314dd0763bff41918c5218a8c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sun, 7 Jan 2024 23:07:04 +0100 Subject: [PATCH 0284/1886] Handle Windows separators on mix test (#13232) Closes #13225. --- lib/ex_unit/lib/ex_unit/filters.ex | 2 +- lib/ex_unit/test/ex_unit/filters_test.exs | 45 +++++++++++++---------- lib/mix/test/mix/tasks/test_test.exs | 22 ++++++++++- 3 files changed, 47 insertions(+), 22 deletions(-) diff --git a/lib/ex_unit/lib/ex_unit/filters.ex b/lib/ex_unit/lib/ex_unit/filters.ex index 9b3f459802..1f51908249 100644 --- a/lib/ex_unit/lib/ex_unit/filters.ex +++ b/lib/ex_unit/lib/ex_unit/filters.ex @@ -51,7 +51,7 @@ defmodule ExUnit.Filters do [path | parts] -> {path_parts, line_numbers} = Enum.split_while(parts, &(to_line_number(&1) == nil)) - path = Enum.join([path | path_parts], ":") + path = Enum.join([path | path_parts], ":") |> Path.split() |> Path.join() lines = for n <- line_numbers, valid_number = validate_line_number(n), do: valid_number case lines do diff --git a/lib/ex_unit/test/ex_unit/filters_test.exs b/lib/ex_unit/test/ex_unit/filters_test.exs index 44df58a7ba..0f0510f647 100644 --- a/lib/ex_unit/test/ex_unit/filters_test.exs +++ b/lib/ex_unit/test/ex_unit/filters_test.exs @@ -198,28 +198,33 @@ defmodule ExUnit.FiltersTest do windows_path = "C:\\some\\path.exs" for path <- [unix_path, windows_path] do + fixed_path = path |> Path.split() |> Path.join() + assert ExUnit.Filters.parse_path("#{path}:123") == - {path, [exclude: [:test], include: [location: {path, 123}]]} + {fixed_path, [exclude: [:test], include: [location: {fixed_path, 123}]]} - assert ExUnit.Filters.parse_path(path) == {path, []} + assert ExUnit.Filters.parse_path(path) == {fixed_path, []} assert ExUnit.Filters.parse_path("#{path}:123notreallyalinenumber123") == - {"#{path}:123notreallyalinenumber123", []} + {"#{fixed_path}:123notreallyalinenumber123", []} assert ExUnit.Filters.parse_path("#{path}:123:456") == - {path, [exclude: [:test], include: [location: {path, [123, 456]}]]} + {fixed_path, [exclude: [:test], include: [location: {fixed_path, [123, 456]}]]} assert ExUnit.Filters.parse_path("#{path}:123notalinenumber123:456") == - {"#{path}:123notalinenumber123", - [exclude: [:test], include: [location: {"#{path}:123notalinenumber123", 456}]]} + {"#{fixed_path}:123notalinenumber123", + [ + exclude: [:test], + include: [location: {"#{fixed_path}:123notalinenumber123", 456}] + ]} output = ExUnit.CaptureIO.capture_io(:stderr, fn -> assert ExUnit.Filters.parse_path("#{path}:123:456notalinenumber456") == - {path, [{:exclude, [:test]}, {:include, [location: {path, 123}]}]} + {fixed_path, [{:exclude, [:test]}, {:include, [location: {fixed_path, 123}]}]} assert ExUnit.Filters.parse_path("#{path}:123:0:-789:456") == - {path, [exclude: [:test], include: [location: {path, [123, 456]}]]} + {fixed_path, [exclude: [:test], include: [location: {fixed_path, [123, 456]}]]} end) assert output =~ "invalid line number given as ExUnit filter: 456notalinenumber456" @@ -231,25 +236,25 @@ defmodule ExUnit.FiltersTest do test "multiple file paths with line numbers" do unix_path = "test/some/path.exs" windows_path = "C:\\some\\path.exs" - other_unix_path = "test/some/other_path.exs" + other_unix_path = "test//some//other_path.exs" other_windows_path = "C:\\some\\other_path.exs" - for {path, other_path} <- [ - {unix_path, other_unix_path}, - {windows_path, other_windows_path} - ] do + for {path, other_path} <- [{unix_path, other_unix_path}, {windows_path, other_windows_path}] do + fixed_path = path |> Path.split() |> Path.join() + fixed_other_path = other_path |> Path.split() |> Path.join() + assert ExUnit.Filters.parse_paths([path, "#{other_path}:456:789"]) == - {[path, other_path], + {[fixed_path, fixed_other_path], [ exclude: [:test], - include: [location: {other_path, [456, 789]}] + include: [location: {fixed_other_path, [456, 789]}] ]} assert ExUnit.Filters.parse_paths(["#{path}:123", "#{other_path}:456"]) == - {[path, other_path], + {[fixed_path, fixed_other_path], [ exclude: [:test], - include: [location: {path, 123}, location: {other_path, 456}] + include: [location: {fixed_path, 123}, location: {fixed_other_path, 456}] ]} output = @@ -258,12 +263,12 @@ defmodule ExUnit.FiltersTest do "#{path}:123:0:-789:456", "#{other_path}:321:0:-987:654" ]) == - {[path, other_path], + {[fixed_path, fixed_other_path], [ exclude: [:test], include: [ - location: {path, [123, 456]}, - location: {other_path, [321, 654]} + location: {fixed_path, [123, 456]}, + location: {fixed_other_path, [321, 654]} ] ]} end) diff --git a/lib/mix/test/mix/tasks/test_test.exs b/lib/mix/test/mix/tasks/test_test.exs index b3b4a68640..f0809cd7b9 100644 --- a/lib/mix/test/mix/tasks/test_test.exs +++ b/lib/mix/test/mix/tasks/test_test.exs @@ -223,6 +223,15 @@ defmodule Mix.Tasks.TestTest do output = mix(["test", "test/passing_and_failing_test_failed.exs", "--failed"]) assert output =~ "1 test, 1 failure" + # Plus line + output = mix(["test", "test/passing_and_failing_test_failed.exs:5", "--failed"]) + assert output =~ "1 test, 1 failure" + + if windows?() do + output = mix(["test", "test\\passing_and_failing_test_failed.exs:5", "--failed"]) + assert output =~ "1 test, 1 failure" + end + # `--failed` composes with an `--only` filter by running the intersection. # Of the failing tests, 1 is tagged with `@tag :foo`. # Of the passing tests, 1 is tagged with `@tag :foo`. @@ -509,17 +518,28 @@ defmodule Mix.Tasks.TestTest do refute output =~ "==> foo" refute output =~ "Paths given to \"mix test\" did not match any directory/file" - output = mix(["test", "apps/foo/test/foo_tests.exs:9", "apps/bar/test/bar_tests.exs:5"]) + casing = + if windows?() do + "apps\\bar\\test\\bar_tests.exs:5" + else + "apps/bar/test/bar_tests.exs:5" + end + + output = mix(["test", "apps/foo/test/foo_tests.exs:9", casing]) assert output =~ """ Excluding tags: [:test] Including tags: [location: {"test/foo_tests.exs", 9}] """ + assert output =~ "1 test, 0 failures\n" + assert output =~ """ Excluding tags: [:test] Including tags: [location: {"test/bar_tests.exs", 5}] """ + + assert output =~ "4 tests, 0 failures, 3 excluded\n" end) end end From 1e05b6ee29b948648016ef3c9e8b18cf63ce4f1d Mon Sep 17 00:00:00 2001 From: Jean Klingler Date: Mon, 8 Jan 2024 18:05:11 +0900 Subject: [PATCH 0285/1886] Replace single quotes in charlist in doc (#13233) --- lib/elixir/pages/references/patterns-and-guards.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/elixir/pages/references/patterns-and-guards.md b/lib/elixir/pages/references/patterns-and-guards.md index 0a509333a1..3be4d1de09 100644 --- a/lib/elixir/pages/references/patterns-and-guards.md +++ b/lib/elixir/pages/references/patterns-and-guards.md @@ -159,13 +159,13 @@ iex> [head | tail] = [] Given charlists are represented as a list of integers, one can also perform prefix matches on charlists using the list concatenation operator ([`++`](`++/2`)): ```elixir -iex> 'hello ' ++ world = 'hello world' -'hello world' +iex> ~c"hello " ++ world = ~c"hello world" +~c"hello world" iex> world -'world' +~c"world" ``` -Which is equivalent to matching on `[?h, ?e, ?l, ?l, ?o, ?\s | world]`. Suffix matches (`hello ++ ' world'`) are not valid patterns. +Which is equivalent to matching on `[?h, ?e, ?l, ?l, ?o, ?\s | world]`. Suffix matches (`hello ++ ~c" world"`) are not valid patterns. ### Maps From 9c98fa615ff28b5e5b7d506b329339bb1827309d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 8 Jan 2024 17:12:49 +0100 Subject: [PATCH 0286/1886] Perform inference and checking inside binaries (#13237) --- lib/elixir/lib/module/types.ex | 21 -- lib/elixir/lib/module/types/descr.ex | 15 + lib/elixir/lib/module/types/expr.ex | 43 +-- lib/elixir/lib/module/types/helpers.ex | 54 ++-- lib/elixir/lib/module/types/of.ex | 157 +++++----- lib/elixir/lib/module/types/pattern.ex | 273 ++++++++++++++---- .../test/elixir/module/types/descr_test.exs | 4 +- .../test/elixir/module/types/expr_test.exs | 87 ++++++ .../test/elixir/module/types/helpers_test.exs | 18 ++ .../test/elixir/module/types/pattern_test.exs | 53 ++++ .../test/elixir/module/types/type_helper.exs | 93 +++++- .../test/elixir/module/types/types_test.exs | 79 ----- 12 files changed, 627 insertions(+), 270 deletions(-) create mode 100644 lib/elixir/test/elixir/module/types/helpers_test.exs create mode 100644 lib/elixir/test/elixir/module/types/pattern_test.exs delete mode 100644 lib/elixir/test/elixir/module/types/types_test.exs diff --git a/lib/elixir/lib/module/types.ex b/lib/elixir/lib/module/types.ex index 8e18b24978..9bf22ab168 100644 --- a/lib/elixir/lib/module/types.ex +++ b/lib/elixir/lib/module/types.ex @@ -85,25 +85,4 @@ defmodule Module.Types do vars: %{} } end - - @doc false - def expr_to_string(expr) do - expr - |> reverse_rewrite() - |> Macro.to_string() - end - - defp reverse_rewrite(guard) do - Macro.prewalk(guard, fn - {{:., _, [mod, fun]}, meta, args} -> erl_to_ex(mod, fun, args, meta) - other -> other - end) - end - - defp erl_to_ex(mod, fun, args, meta) do - case :elixir_rewrite.erl_to_ex(mod, fun, args) do - {Kernel, fun, args} -> {fun, meta, args} - {mod, fun, args} -> {{:., [], [mod, fun]}, meta, args} - end - end end diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex index 52b993bcf7..2ca38edb9d 100644 --- a/lib/elixir/lib/module/types/descr.ex +++ b/lib/elixir/lib/module/types/descr.ex @@ -22,6 +22,10 @@ defmodule Module.Types.Descr do @fun 1 <<< 12 @top (1 <<< 13) - 1 + # Guard helpers + + defguard is_none(map) when map == %{} + # Type definitions def dynamic(), do: :dynamic @@ -100,6 +104,17 @@ defmodule Module.Types.Descr do @compile {:inline, to_quoted: 2} defp to_quoted(:bitmap, val), do: bitmap_to_quoted(val) + @doc """ + Converts a descr to its quoted string representation. + """ + def to_quoted_string(descr) do + descr + |> to_quoted() + |> Code.Formatter.to_algebra() + |> Inspect.Algebra.format(98) + |> IO.iodata_to_binary() + end + ## Iterator helpers defp iterator_union({key, v1, iterator}, map) do diff --git a/lib/elixir/lib/module/types/expr.ex b/lib/elixir/lib/module/types/expr.ex index 13b91e3692..00cb9ae2d5 100644 --- a/lib/elixir/lib/module/types/expr.ex +++ b/lib/elixir/lib/module/types/expr.ex @@ -4,6 +4,10 @@ defmodule Module.Types.Expr do alias Module.Types.{Of, Pattern} import Module.Types.{Helpers, Descr} + defp of_expr(ast, _expected_expr, stack, context) do + of_expr(ast, stack, context) + end + # :atom def of_expr(atom, _stack, context) when is_atom(atom) do {:ok, atom(atom), context} @@ -38,7 +42,7 @@ defmodule Module.Types.Expr do def of_expr(exprs, stack, context) when is_list(exprs) do case map_reduce_ok(exprs, context, &of_expr(&1, stack, &2)) do {:ok, _types, context} -> {:ok, non_empty_list(), context} - {:error, reason} -> {:error, reason} + {:error, context} -> {:error, context} end end @@ -47,11 +51,12 @@ defmodule Module.Types.Expr do of_expr({:{}, [], [left, right]}, stack, context) end - # TODO: <<...>>> + # <<...>>> def of_expr({:<<>>, _meta, args}, stack, context) do - case Of.binary(args, :expr, stack, context, &of_expr/3) do + case Of.binary(args, :expr, stack, context, &of_expr/4) do {:ok, context} -> {:ok, binary(), context} - {:error, reason} -> {:error, reason} + # It is safe to discard errors from binary inside expressions + {:error, context} -> {:ok, binary(), context} end end @@ -66,8 +71,8 @@ defmodule Module.Types.Expr do {:ok, _left, context} -> of_expr(right_expr, stack, context) - {:error, reason} -> - {:error, reason} + {:error, context} -> + {:error, context} end end @@ -87,13 +92,14 @@ defmodule Module.Types.Expr do def of_expr({:{}, _meta, exprs}, stack, context) do case map_reduce_ok(exprs, context, &of_expr(&1, stack, &2)) do {:ok, _types, context} -> {:ok, tuple(), context} - {:error, reason} -> {:error, reason} + {:error, context} -> {:error, context} end end # TODO: left = right def of_expr({:=, _meta, [left_expr, right_expr]}, stack, context) do - with {:ok, _left_type, context} <- Pattern.of_pattern(left_expr, stack, context), + with {:ok, _left_type, context} <- + Pattern.of_pattern(left_expr, stack, context), {:ok, right_type, context} <- of_expr(right_expr, stack, context) do {:ok, right_type, context} end @@ -148,7 +154,7 @@ defmodule Module.Types.Expr do case result do {:ok, _, context} -> of_expr(post, stack, context) - {:error, reason} -> {:error, reason} + {:error, context} -> {:error, context} end end @@ -178,7 +184,7 @@ defmodule Module.Types.Expr do def of_expr({:fn, _meta, clauses}, stack, context) do case of_clauses(clauses, stack, context) do {:ok, context} -> {:ok, dynamic(), context} - {:error, reason} -> {:error, reason} + {:error, context} -> {:error, context} end end @@ -191,14 +197,14 @@ defmodule Module.Types.Expr do reduce_ok(blocks, context, fn {:rescue, clauses}, context -> reduce_ok(clauses, context, fn - {:->, _, [[{:in, _, [var, _exceptions]}], body]}, context -> + {:->, _, [[{:in, _, [var, _exceptions]} = expr], body]}, context -> # TODO: Vars are a union of the structs above - {_version, _type, context} = new_var(var, dynamic(), context) + {:ok, _type, context} = Pattern.of_pattern(var, {dynamic(), expr}, stack, context) of_expr_context(body, stack, context) {:->, _, [[var], body]}, context -> # TODO: Vars are structs with the exception field and that's it - {_version, _type, context} = new_var(var, dynamic(), context) + {:ok, _type, context} = Pattern.of_pattern(var, stack, context) of_expr_context(body, stack, context) end) @@ -259,7 +265,7 @@ defmodule Module.Types.Expr do def of_expr({:with, _meta, [_ | _] = clauses}, stack, context) do case reduce_ok(clauses, context, &with_clause(&1, stack, &2)) do {:ok, context} -> {:ok, dynamic(), context} - {:error, reason} -> {:error, reason} + {:error, context} -> {:error, context} end end @@ -323,9 +329,9 @@ defmodule Module.Types.Expr do end end - # TODO: var + # var def of_expr(var, _stack, context) when is_var(var) do - {:ok, fetch_var!(var, context), context} + {:ok, Pattern.of_var(var, context), context} end defp for_clause({:<-, _, [left, expr]}, stack, context) do @@ -338,7 +344,8 @@ defmodule Module.Types.Expr do defp for_clause({:<<>>, _, [{:<-, _, [pattern, expr]}]}, stack, context) do # TODO: the compiler guarantees pattern is a binary but we need to check expr is a binary - with {:ok, _pattern_type, context} <- Pattern.of_pattern(pattern, stack, context), + with {:ok, _pattern_type, context} <- + Pattern.of_pattern(pattern, stack, context), {:ok, _expr_type, context} <- of_expr(expr, stack, context), do: {:ok, context} end @@ -419,7 +426,7 @@ defmodule Module.Types.Expr do defp of_expr_context(expr, stack, context) do case of_expr(expr, stack, context) do {:ok, _type, context} -> {:ok, context} - {:error, reason} -> {:error, reason} + {:error, context} -> {:error, context} end end end diff --git a/lib/elixir/lib/module/types/helpers.ex b/lib/elixir/lib/module/types/helpers.ex index 716c66dc90..776c83ac75 100644 --- a/lib/elixir/lib/module/types/helpers.ex +++ b/lib/elixir/lib/module/types/helpers.ex @@ -15,33 +15,47 @@ defmodule Module.Types.Helpers do end @doc """ - Defines a new variable. + Formatted hints in typing errors. """ - def new_var({:_, _meta, _var_context}, type, context) do - {nil, type, context} + def format_hints(hints) do + hints + |> Enum.uniq() + |> Enum.map(fn + :inferred_bitstring_spec -> + """ + + #{hint()} all expressions given to binaries are assumed to be of type \ + integer() unless said otherwise. For example, <> assumes "expr" \ + is an integer. Pass a modifier, such as <> or <>, \ + to change the default behaviour. + """ + end) end - def new_var({var_name, meta, var_context}, type, context) do - version = Keyword.fetch!(meta, :version) + defp hint, do: :elixir_errors.prefix(:hint) - # TODO: What happens if the variable is defined with another type? - case context.vars do - %{^version => data} -> - {version, type, put_in(context.vars[version], %{data | type: type})} + @doc """ + Converts the given expression to a string, + translating inlined Erlang calls back to Elixir. + """ + def expr_to_string(expr) do + expr + |> reverse_rewrite() + |> Macro.to_string() + end - %{} -> - {version, type, - put_in(context.vars[version], %{type: type, name: var_name, context: var_context})} - end + defp reverse_rewrite(guard) do + Macro.prewalk(guard, fn + {{:., _, [mod, fun]}, meta, args} -> erl_to_ex(mod, fun, args, meta) + other -> other + end) end - @doc """ - Fetches the type of a defined variable. - """ - def fetch_var!({_name, meta, _context}, context) do - version = Keyword.fetch!(meta, :version) - %{vars: %{^version => %{type: type}}} = context - type + defp erl_to_ex(mod, fun, args, meta) do + case :elixir_rewrite.erl_to_ex(mod, fun, args) do + {Kernel, fun, args} -> {fun, meta, args} + {mod, fun, args} -> {{:., [], [mod, fun]}, meta, args} + end end @doc """ diff --git a/lib/elixir/lib/module/types/of.ex b/lib/elixir/lib/module/types/of.ex index 9a97d343c7..877a5dda7b 100644 --- a/lib/elixir/lib/module/types/of.ex +++ b/lib/elixir/lib/module/types/of.ex @@ -3,12 +3,18 @@ defmodule Module.Types.Of do # Generic AST and Enum helpers go to Module.Types.Helpers. @moduledoc false - # @prefix quote(do: ...) - # @suffix quote(do: ...) - alias Module.ParallelChecker import Module.Types.{Helpers, Descr} + @prefix quote(do: ...) + @suffix quote(do: ...) + + @integer_or_float union(integer(), float()) + @integer_or_binary union(integer(), binary()) + @integer integer() + @float float() + @binary binary() + # There are important assumptions on how we work with maps. # # First, the keys in the map must be ordered by subtyping. @@ -66,92 +72,73 @@ defmodule Module.Types.Of do In the stack, we add nodes such as <>, <<..., expr>>, etc, based on the position of the expression within the binary. """ - def binary([], _type, _stack, context, _of_fun) do + def binary([], _kind, _stack, context, _of_fun) do {:ok, context} end - def binary([head], type, stack, context, of_fun) do - # stack = push_expr_stack({:<<>>, get_meta(head), [head]}, stack) - binary_segment(head, type, stack, context, of_fun) + def binary([head], kind, stack, context, of_fun) do + binary_segment(head, kind, [head], stack, context, of_fun) end - def binary([head | tail], type, stack, context, of_fun) do - # stack = push_expr_stack({:<<>>, get_meta(head), [head, @suffix]}, stack) - - case binary_segment(head, type, stack, context, of_fun) do - {:ok, context} -> binary_many(tail, type, stack, context, of_fun) + def binary([head | tail], kind, stack, context, of_fun) do + case binary_segment(head, kind, [head, @suffix], stack, context, of_fun) do + {:ok, context} -> binary_many(tail, kind, stack, context, of_fun) {:error, reason} -> {:error, reason} end end - defp binary_many([last], type, stack, context, of_fun) do - # stack = push_expr_stack({:<<>>, get_meta(last), [@prefix, last]}, stack) - binary_segment(last, type, stack, context, of_fun) + defp binary_many([last], kind, stack, context, of_fun) do + binary_segment(last, kind, [@prefix, last], stack, context, of_fun) end - defp binary_many([head | tail], type, stack, context, of_fun) do - # stack = push_expr_stack({:<<>>, get_meta(head), [@prefix, head, @suffix]}, stack) - - case binary_segment(head, type, stack, context, of_fun) do - {:ok, context} -> binary_many(tail, type, stack, context, of_fun) + defp binary_many([head | tail], kind, stack, context, of_fun) do + case binary_segment(head, kind, [@prefix, head, @suffix], stack, context, of_fun) do + {:ok, context} -> binary_many(tail, kind, stack, context, of_fun) {:error, reason} -> {:error, reason} end end - defp binary_segment({:"::", _meta, [expr, specifiers]}, type, stack, context, of_fun) do - # TODO: handle size in specifiers - # TODO: unpack specifiers once - _expected_type = - collect_binary_specifier(specifiers, &binary_type(type, &1)) || :integer - - utf? = collect_binary_specifier(specifiers, &utf_type?/1) - float? = collect_binary_specifier(specifiers, &float_type?/1) - - # Special case utf and float specifiers because they can be two types as literals - # but only a specific type as a variable in a pattern - cond do - type == :pattern and utf? and is_binary(expr) -> - {:ok, context} + # If the segment is a literal, the compiler has already checked its validity, + # so we just skip it. + defp binary_segment({:"::", _meta, [left, _right]}, _kind, _args, _stack, context, _of_fun) + when is_binary(left) or is_number(left) do + {:ok, context} + end - type == :pattern and float? and is_integer(expr) -> + defp binary_segment({:"::", meta, [left, right]}, kind, args, stack, context, of_fun) do + expected_type = specifier_info(kind, right) + expr = {:<<>>, meta, args} + + with {:ok, actual_type, context} <- of_fun.(left, {expected_type, expr}, stack, context) do + # If we are in a pattern and we have a variable, the refinement + # will already have checked the type, so we skip the check here. + # TODO: properly handle dynamic. Do we need materialization? + if actual_type == :dynamic or + (kind == :pattern and is_var(left)) or + is_none(difference(actual_type, expected_type)) do {:ok, context} - - true -> - with {:ok, _type, context} <- of_fun.(expr, stack, context), - do: {:ok, context} + else + hints = if meta[:inferred_bitstring_spec], do: [:inferred_bitstring_spec], else: [] + {:error, incompatible_warn(expr, expected_type, actual_type, hints, meta, stack, context)} + end end end - # Collect binary type specifiers, - # from `<>` collect `integer` - defp collect_binary_specifier({:-, _meta, [left, right]}, fun) do - collect_binary_specifier(left, fun) || collect_binary_specifier(right, fun) - end - - defp collect_binary_specifier(other, fun) do - fun.(other) - end - - defp binary_type(:expr, {:float, _, _}), do: {:union, [:integer, :float]} - defp binary_type(:expr, {:utf8, _, _}), do: {:union, [:integer, :binary]} - defp binary_type(:expr, {:utf16, _, _}), do: {:union, [:integer, :binary]} - defp binary_type(:expr, {:utf32, _, _}), do: {:union, [:integer, :binary]} - defp binary_type(:pattern, {:utf8, _, _}), do: :integer - defp binary_type(:pattern, {:utf16, _, _}), do: :integer - defp binary_type(:pattern, {:utf32, _, _}), do: :integer - defp binary_type(:pattern, {:float, _, _}), do: :float - defp binary_type(_context, {:integer, _, _}), do: :integer - defp binary_type(_context, {:bits, _, _}), do: :binary - defp binary_type(_context, {:bitstring, _, _}), do: :binary - defp binary_type(_context, {:bytes, _, _}), do: :binary - defp binary_type(_context, {:binary, _, _}), do: :binary - defp binary_type(_context, _specifier), do: nil - - defp utf_type?({specifier, _, _}), do: specifier in [:utf8, :utf16, :utf32] - defp utf_type?(_), do: false - - defp float_type?({:float, _, _}), do: true - defp float_type?(_), do: false + defp specifier_info(kind, {:-, _, [left, _right]}), do: specifier_info(kind, left) + defp specifier_info(:expr, {:float, _, _}), do: @integer_or_float + defp specifier_info(:expr, {:utf8, _, _}), do: @integer_or_binary + defp specifier_info(:expr, {:utf16, _, _}), do: @integer_or_binary + defp specifier_info(:expr, {:utf32, _, _}), do: @integer_or_binary + defp specifier_info(:pattern, {:utf8, _, _}), do: @integer + defp specifier_info(:pattern, {:utf16, _, _}), do: @integer + defp specifier_info(:pattern, {:utf32, _, _}), do: @integer + defp specifier_info(:pattern, {:float, _, _}), do: @float + defp specifier_info(_kind, {:integer, _, _}), do: @integer + defp specifier_info(_kind, {:bits, _, _}), do: @binary + defp specifier_info(_kind, {:bitstring, _, _}), do: @binary + defp specifier_info(_kind, {:bytes, _, _}), do: @binary + defp specifier_info(_kind, {:binary, _, _}), do: @binary + defp specifier_info(_kind, _specifier), do: @integer ## Remote @@ -243,12 +230,44 @@ defmodule Module.Types.Of do not Enum.any?(stack.no_warn_undefined, &(&1 == module or &1 == {module, fun, arity})) end + ## Warning helpers + + @doc """ + Emits incompatible types warning for the given expression + """ + def incompatible_warn(expr, expected_type, actual_type, hints \\ [], meta, stack, context) do + warning = {:incompatible, expr, expected_type, actual_type, hints, context} + warn(__MODULE__, warning, meta, stack, context) + end + defp warn(warning, meta, stack, context) do warn(__MODULE__, warning, meta, stack, context) end ## Warning formatting + def format_warning({:incompatible, expr, expected_type, actual_type, hints, context}) do + {traces, trace_hints} = Module.Types.Pattern.format_traces(expr, context) + + [ + """ + incompatible types in expression: + + #{Macro.to_string(expr)} + + expected type: + + #{to_quoted_string(expected_type)} + + but got type: + + #{to_quoted_string(actual_type)} + """, + traces, + format_hints(hints ++ trace_hints) + ] + end + def format_warning({:undefined_module, module, fun, arity}) do [ Exception.format_mfa(module, fun, arity), diff --git a/lib/elixir/lib/module/types/pattern.ex b/lib/elixir/lib/module/types/pattern.ex index 9fb17e275c..6cba56058c 100644 --- a/lib/elixir/lib/module/types/pattern.ex +++ b/lib/elixir/lib/module/types/pattern.ex @@ -4,6 +4,8 @@ defmodule Module.Types.Pattern do alias Module.Types.Of import Module.Types.{Helpers, Descr} + @expected_expr {dynamic(), nil} + @doc """ Handles patterns and guards at once. """ @@ -11,29 +13,94 @@ defmodule Module.Types.Pattern do with {:ok, types, context} <- map_reduce_ok(patterns, context, &of_pattern(&1, stack, &2)), # TODO: Check that of_guard/4 returns boolean() | :fail - {:ok, _, context} <- of_guards(guards, term(), stack, context), + {:ok, _, context} <- of_guards(guards, {term(), nil}, stack, context), do: {:ok, types, context} end + ## Variable handling + + @doc """ + Fetches the type of a defined variable. + """ + def of_var({_name, meta, _context}, context) do + version = Keyword.fetch!(meta, :version) + %{vars: %{^version => %{type: type}}} = context + type + end + + defp refine_var({var_name, meta, var_context} = var, type, expr, stack, context) do + version = Keyword.fetch!(meta, :version) + + case context.vars do + %{^version => %{type: old_type, off_traces: off_traces} = data} -> + dynamic = dynamic() + + # TODO: Properly compute intersection and union of dynamic + new_type = + if old_type == dynamic or type == dynamic do + dynamic + else + intersection(type, old_type) + end + + data = %{data | type: new_type, off_traces: new_trace(expr, type, stack, off_traces)} + context = put_in(context.vars[version], data) + + if is_none(new_type) do + {:error, + warn(__MODULE__, {:refine_var, old_type, type, var, context}, meta, stack, context)} + else + {:ok, new_type, context} + end + + %{} -> + data = %{ + type: type, + name: var_name, + context: var_context, + off_traces: new_trace(expr, type, stack, []) + } + + context = put_in(context.vars[version], data) + {:ok, type, context} + end + end + + defp new_trace(nil, _type, _stack, traces), do: traces + defp new_trace(expr, type, stack, traces), do: [{expr, stack.file, type} | traces] + + ## Patterns + + @doc """ + Return the type and typing context of a pattern expression + with no {expected, expr} pair. of_pattern/4 must be preferred + whenever possible as it adds more context to errors. + """ + def of_pattern(expr, stack, context) do + of_pattern(expr, @expected_expr, stack, context) + end + @doc """ - Return the type and typing context of a pattern expression or an error - in case of a typing conflict. + Return the type and typing context of a pattern expression with + the given {expected, expr} pair or an error in case of a typing conflict. """ + # ^var - def of_pattern({:^, _meta, [var]}, _stack, context) do - {:ok, fetch_var!(var, context), context} + def of_pattern({:^, _meta, [var]}, _expected_expr, _stack, context) do + {:ok, of_var(var, context), context} end # left = right - def of_pattern({:=, _meta, [left_expr, right_expr]}, stack, context) do - with {:ok, _, context} <- of_pattern(left_expr, stack, context), - {:ok, _, context} <- of_pattern(right_expr, stack, context), + def of_pattern({:=, _meta, [left_expr, right_expr]}, expected_expr, stack, context) do + with {:ok, _, context} <- of_pattern(left_expr, expected_expr, stack, context), + {:ok, _, context} <- of_pattern(right_expr, expected_expr, stack, context), do: {:ok, dynamic(), context} end # %_{...} def of_pattern( {:%, _meta1, [{:_, _meta2, var_context}, {:%{}, _meta3, args}]}, + _expected_expr, stack, context ) @@ -44,7 +111,7 @@ defmodule Module.Types.Pattern do end # %var{...} and %^var{...} - def of_pattern({:%, _meta1, [var, {:%{}, _meta2, args}]}, stack, context) + def of_pattern({:%, _meta1, [var, {:%{}, _meta2, args}]}, _expected_expr, stack, context) when not is_atom(var) do # TODO: validate var is an atom with {:ok, _, context} = of_pattern(var, stack, context), @@ -53,123 +120,215 @@ defmodule Module.Types.Pattern do end end + # %Struct{...} + def of_pattern({:%, meta1, [module, {:%{}, _meta2, args}]}, _expected_expr, stack, context) + when is_atom(module) do + with {:ok, _, context} <- Of.struct(module, meta1, stack, context), + {:ok, _, context} <- Of.open_map(args, stack, context, &of_pattern/3) do + {:ok, map(), context} + end + end + + # %{...} + def of_pattern({:%{}, _meta, args}, _expected_expr, stack, context) do + Of.open_map(args, stack, context, &of_pattern/3) + end + # <<...>>> - def of_pattern({:<<>>, _meta, args}, stack, context) do - case Of.binary(args, :pattern, stack, context, &of_pattern/3) do + def of_pattern({:<<>>, _meta, args}, _expected_expr, stack, context) do + case Of.binary(args, :pattern, stack, context, &of_pattern/4) do {:ok, context} -> {:ok, binary(), context} {:error, reason} -> {:error, reason} end end - # var or _ - def of_pattern(var, _stack, context) when is_var(var) do - {_version, type, context} = new_var(var, dynamic(), context) + # _ + def of_pattern({:_, _meta, _var_context}, {type, _expr}, _stack, context) do {:ok, type, context} end - def of_pattern(expr, stack, context) do - of_shared(expr, stack, context, &of_pattern/3) + # var + def of_pattern(var, {type, expr}, stack, context) when is_var(var) do + refine_var(var, type, expr, stack, context) + end + + def of_pattern(expr, expected_expr, stack, context) do + of_shared(expr, expected_expr, stack, context, &of_pattern/4) end @doc """ Refines the type variables in the typing context using type check guards such as `is_integer/1`. """ - def of_guards(_expr, _expected, _stack, context) do + # TODO: All expressions in of_pattern plus functions calls are not handled + # by of_guards. There is a question of how much of of_shared can also be + # shared with of_expr, but still unclear. In the worst case scenario, + # Of.literal() could be added for pattern, guards, and expr. + def of_guards(_expr, _expected_expr, _stack, context) do {:ok, dynamic(), context} end ## Shared # :atom - defp of_shared(atom, _stack, context, _fun) when is_atom(atom) do + defp of_shared(atom, _expected_expr, _stack, context, _fun) when is_atom(atom) do {:ok, atom(atom), context} end # 12 - defp of_shared(literal, _stack, context, _fun) when is_integer(literal) do + defp of_shared(literal, _expected_expr, _stack, context, _fun) when is_integer(literal) do {:ok, integer(), context} end # 1.2 - defp of_shared(literal, _stack, context, _fun) when is_float(literal) do + defp of_shared(literal, _expected_expr, _stack, context, _fun) when is_float(literal) do {:ok, float(), context} end # "..." - defp of_shared(literal, _stack, context, _fun) when is_binary(literal) do + defp of_shared(literal, _expected_expr, _stack, context, _fun) when is_binary(literal) do {:ok, binary(), context} end + # [] + defp of_shared([], _expected_expr, _stack, context, _fun) do + {:ok, empty_list(), context} + end + + # [expr, ...] + defp of_shared(exprs, _expected_expr, stack, context, fun) when is_list(exprs) do + case map_reduce_ok(exprs, context, &fun.(&1, @expected_expr, stack, &2)) do + {:ok, _types, context} -> {:ok, non_empty_list(), context} + {:error, reason} -> {:error, reason} + end + end + + # {left, right} + defp of_shared({left, right}, expected_expr, stack, context, fun) do + of_shared({:{}, [], [left, right]}, expected_expr, stack, context, fun) + end + # left | [] - defp of_shared({:|, _meta, [left_expr, []]}, stack, context, fun) do - fun.(left_expr, stack, context) + defp of_shared({:|, _meta, [left_expr, []]}, _expected_expr, stack, context, fun) do + fun.(left_expr, @expected_expr, stack, context) end # left | right - defp of_shared({:|, _meta, [left_expr, right_expr]}, stack, context, fun) do - case fun.(left_expr, stack, context) do + defp of_shared({:|, _meta, [left_expr, right_expr]}, _expected_expr, stack, context, fun) do + case fun.(left_expr, @expected_expr, stack, context) do {:ok, _, context} -> - fun.(right_expr, stack, context) + fun.(right_expr, @expected_expr, stack, context) {:error, reason} -> {:error, reason} end end - # [] - defp of_shared([], _stack, context, _fun) do - {:ok, empty_list(), context} - end - - # [expr, ...] - defp of_shared(exprs, stack, context, fun) when is_list(exprs) do - case map_reduce_ok(exprs, context, &fun.(&1, stack, &2)) do - {:ok, _types, context} -> {:ok, non_empty_list(), context} - {:error, reason} -> {:error, reason} - end - end - # left ++ right defp of_shared( {{:., _meta1, [:erlang, :++]}, _meta2, [left_expr, right_expr]}, + _expected_expr, stack, context, fun ) do # The left side is always a list - with {:ok, _, context} <- fun.(left_expr, stack, context), - {:ok, _, context} <- fun.(right_expr, stack, context) do + with {:ok, _, context} <- fun.(left_expr, @expected_expr, stack, context), + {:ok, _, context} <- fun.(right_expr, @expected_expr, stack, context) do # TODO: Both lists can be empty, so this may be an empty list, # so we return dynamic() for now. {:ok, dynamic(), context} end end - # {left, right} - defp of_shared({left, right}, stack, context, fun) do - of_shared({:{}, [], [left, right]}, stack, context, fun) - end - # {...} - defp of_shared({:{}, _meta, exprs}, stack, context, fun) do - case map_reduce_ok(exprs, context, &fun.(&1, stack, &2)) do + defp of_shared({:{}, _meta, exprs}, _expected_expr, stack, context, fun) do + case map_reduce_ok(exprs, context, &fun.(&1, @expected_expr, stack, &2)) do {:ok, _, context} -> {:ok, tuple(), context} {:error, reason} -> {:error, reason} end end - # %{...} - defp of_shared({:%{}, _meta, args}, stack, context, fun) do - Of.open_map(args, stack, context, fun) + ## Format warnings + + def format_warning({:refine_var, old_type, new_type, var, context}) do + {traces, hints} = format_traces(var, context) + + [ + """ + incompatible types assigned to #{format_var(var)}: + + #{to_quoted_string(old_type)} !~ #{to_quoted_string(new_type)} + """, + traces, + format_hints(hints) + ] end - # %Struct{...} - defp of_shared({:%, meta1, [module, {:%{}, _meta2, args}]}, stack, context, fun) - when is_atom(module) do - with {:ok, _, context} <- Of.struct(module, meta1, stack, context), - {:ok, _, context} <- Of.open_map(args, stack, context, fun) do - {:ok, map(), context} + def format_traces(expr, %{vars: vars}) do + {_, versions} = + Macro.prewalk(expr, %{}, fn + {var_name, meta, var_context}, versions when is_atom(var_name) and is_atom(var_context) -> + version = meta[:version] + + case vars do + %{^version => data} -> {:ok, Map.put(versions, version, data)} + %{} -> {:ok, versions} + end + + node, versions -> + {node, versions} + end) + + vars = Map.values(versions) + + formatted_traces = + vars + |> Enum.sort_by(& &1.name) + |> Enum.map(&format_trace/1) + + {formatted_traces, trace_hints(vars)} + end + + defp format_trace(%{off_traces: []}) do + [] + end + + defp format_trace(%{name: name, context: context, off_traces: traces}) do + traces = + traces + |> Enum.reverse() + |> Enum.map(fn {expr, file, type} -> + meta = get_meta(expr) + + """ + + # #{Exception.format_file_line(file, meta[:line])} + #{Macro.to_string(expr)} + => #{to_quoted_string(type)} + """ + end) + + type_or_types = pluralize(traces, "type", "types") + ["\nwhere #{format_var(name, context)} was given the #{type_or_types}:\n" | traces] + end + + defp format_var({var, _, context}), do: format_var(var, context) + defp format_var(var, nil), do: "\"#{var}\"" + defp format_var(var, context), do: "\"#{var}\" (context #{inspect(context)})" + + defp pluralize([_], singular, _plural), do: singular + defp pluralize(_, _singular, plural), do: plural + + defp inferred_bitstring_spec?(trace) do + match?({{:<<>>, [inferred_bitstring_spec: true] ++ _meta, _}, _file, _type}, trace) + end + + defp trace_hints(vars) do + if Enum.any?(vars, fn data -> Enum.any?(data.off_traces, &inferred_bitstring_spec?/1) end) do + [:inferred_bitstring_spec] + else + [] end end end diff --git a/lib/elixir/test/elixir/module/types/descr_test.exs b/lib/elixir/test/elixir/module/types/descr_test.exs index a1e2702fc9..8ca016e7ab 100644 --- a/lib/elixir/test/elixir/module/types/descr_test.exs +++ b/lib/elixir/test/elixir/module/types/descr_test.exs @@ -5,8 +5,6 @@ defmodule Module.Types.DescrTest do import Module.Types.Descr - defp to_quoted_string(descr), do: Macro.to_string(to_quoted(descr)) - describe "union" do test "bitmap" do assert union(integer(), float()) == union(float(), integer()) @@ -49,7 +47,7 @@ defmodule Module.Types.DescrTest do test "term" do assert difference(float(), term()) == none() - assert difference(float(), term()) == none() + assert difference(integer(), term()) == none() end test "none" do diff --git a/lib/elixir/test/elixir/module/types/expr_test.exs b/lib/elixir/test/elixir/module/types/expr_test.exs index 12c0763968..3709513642 100644 --- a/lib/elixir/test/elixir/module/types/expr_test.exs +++ b/lib/elixir/test/elixir/module/types/expr_test.exs @@ -18,4 +18,91 @@ defmodule Module.Types.ExprTest do assert typecheck!({1, 2}) == tuple() assert typecheck!(%{}) == map() end + + describe "undefined functions" do + test "warnings" do + assert typewarn!(URI.unknown("foo")) == + {dynamic(), "URI.unknown/1 is undefined or private"} + + assert typewarn!(if(true, do: URI.unknown("foo"))) == + {dynamic(), "URI.unknown/1 is undefined or private"} + + assert typewarn!(try(do: :ok, after: URI.unknown("foo"))) == + {dynamic(), "URI.unknown/1 is undefined or private"} + end + end + + describe "binaries" do + test "warnings" do + assert typewarn!([<>], <>) == + {binary(), + ~l""" + incompatible types in expression: + + <> + + expected type: + + float() or integer() + + but got type: + + binary() + + where "x" was given the type: + + # types_test.ex:LINE-2: + <> + => binary() + """} + + assert typewarn!([<>], <>) == + {binary(), + ~l""" + incompatible types in expression: + + <> + + expected type: + + integer() + + but got type: + + binary() + + where "x" was given the type: + + # types_test.ex:LINE-2: + <> + => binary() + + #{hints(:inferred_bitstring_spec)} + """} + + assert typewarn!([<>], <>) == + {binary(), + ~l""" + incompatible types in expression: + + <> + + expected type: + + binary() + + but got type: + + integer() + + where "x" was given the type: + + # types_test.ex:LINE-2: + <> + => integer() + + #{hints(:inferred_bitstring_spec)} + """} + end + end end diff --git a/lib/elixir/test/elixir/module/types/helpers_test.exs b/lib/elixir/test/elixir/module/types/helpers_test.exs new file mode 100644 index 0000000000..52a0c7f534 --- /dev/null +++ b/lib/elixir/test/elixir/module/types/helpers_test.exs @@ -0,0 +1,18 @@ +Code.require_file("type_helper.exs", __DIR__) + +defmodule Module.Types.HelpersTest do + use ExUnit.Case, async: true + import Module.Types.Helpers + + test "expr_to_string/1" do + assert expr_to_string({1, 2}) == "{1, 2}" + assert expr_to_string(quote(do: Foo.bar(arg))) == "Foo.bar(arg)" + assert expr_to_string(quote(do: :erlang.band(a, b))) == "Bitwise.band(a, b)" + assert expr_to_string(quote(do: :erlang.orelse(a, b))) == "a or b" + assert expr_to_string(quote(do: :erlang."=:="(a, b))) == "a === b" + assert expr_to_string(quote(do: :erlang.list_to_atom(a))) == "List.to_atom(a)" + assert expr_to_string(quote(do: :maps.remove(a, b))) == "Map.delete(b, 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)" + end +end diff --git a/lib/elixir/test/elixir/module/types/pattern_test.exs b/lib/elixir/test/elixir/module/types/pattern_test.exs new file mode 100644 index 0000000000..7e703c99a4 --- /dev/null +++ b/lib/elixir/test/elixir/module/types/pattern_test.exs @@ -0,0 +1,53 @@ +Code.require_file("type_helper.exs", __DIR__) + +defmodule Module.Types.PatternTest do + use ExUnit.Case, async: true + + import TypeHelper + import Module.Types.Descr + + describe "binaries" do + test "ok" do + assert typecheck!([<>], x) == integer() + assert typecheck!([<>], x) == float() + assert typecheck!([<>], x) == binary() + assert typecheck!([<>], x) == integer() + end + + test "error" do + assert typeerror!([<>], x) == ~l""" + incompatible types assigned to "x": + + binary() !~ float() + + where "x" was given the types: + + # types_test.ex:LINE: + <> + => binary() + + # types_test.ex:LINE: + <<..., x::float>> + => float() + """ + + assert typeerror!([<>], x) == ~l""" + incompatible types assigned to "x": + + float() !~ integer() + + where "x" was given the types: + + # types_test.ex:LINE: + <> + => float() + + # types_test.ex:LINE: + <<..., x>> + => integer() + + #{hints(:inferred_bitstring_spec)} + """ + end + end +end diff --git a/lib/elixir/test/elixir/module/types/type_helper.exs b/lib/elixir/test/elixir/module/types/type_helper.exs index ecd9c56f4d..9ee79e0409 100644 --- a/lib/elixir/test/elixir/module/types/type_helper.exs +++ b/lib/elixir/test/elixir/module/types/type_helper.exs @@ -2,7 +2,7 @@ Code.require_file("../../test_helper.exs", __DIR__) defmodule TypeHelper do alias Module.Types - alias Module.Types.{Pattern, Expr} + alias Module.Types.{Pattern, Expr, Descr} @doc """ Main helper for checking the given AST type checks without warnings. @@ -14,6 +14,27 @@ defmodule TypeHelper do end end + @doc """ + Main helper for checking the given AST type checks errors. + """ + defmacro typeerror!(patterns \\ [], guards \\ [], body) do + quote do + unquote(typecheck(patterns, guards, body, __CALLER__)) + |> TypeHelper.__typeerror__!() + end + end + + @doc """ + Main helper for checking the given AST type warns. + """ + defmacro typewarn!(patterns \\ [], guards \\ [], body) do + quote do + unquote(typecheck(patterns, guards, body, __CALLER__)) + |> TypeHelper.__typewarn__!() + end + end + + @doc false def __typecheck__!({:ok, type, %{warnings: []}}), do: type def __typecheck__!({:ok, _type, %{warnings: warnings}}), @@ -22,6 +43,26 @@ defmodule TypeHelper do def __typecheck__!({:error, %{warnings: warnings}}), do: raise("type checking errored with warnings: #{inspect(warnings)}") + @doc false + def __typeerror__!({:error, %{warnings: [{module, warning, _locs} | _]}}), + do: warning |> module.format_warning() |> IO.iodata_to_binary() + + def __typeerror__!({:ok, type, _context}), + do: raise("type checking ok but expected error: #{Descr.to_quoted_string(type)}") + + @doc false + def __typewarn__!({:ok, type, %{warnings: [{module, warning, _locs}]}}), + do: {type, warning |> module.format_warning() |> IO.iodata_to_binary()} + + def __typewarn__!({:ok, type, %{warnings: []}}), + do: raise("type checking ok without warnings: #{Descr.to_quoted_string(type)}") + + def __typewarn__!({:ok, _type, %{warnings: warnings}}), + do: raise("type checking ok but many warnings: #{inspect(warnings)}") + + def __typewarn__!({:error, %{warnings: warnings}}), + do: raise("type checking errored with warnings: #{inspect(warnings)}") + @doc """ Building block for typechecking a given AST. """ @@ -52,11 +93,57 @@ defmodule TypeHelper do end end - def new_stack() do + defp new_stack() do Types.stack("types_test.ex", TypesTest, {:test, 0}, [], Module.ParallelChecker.test_cache()) end - def new_context() do + defp new_context() do Types.context() end + + @doc """ + Interpolate the given hints. + """ + def hints(hints) do + hints + |> List.wrap() + |> Module.Types.Helpers.format_hints() + |> IO.iodata_to_binary() + |> String.trim() + end + + @doc """ + A string-like sigil that replaces LINE references by actual line. + """ + defmacro sigil_l({:<<>>, meta, parts}, []) do + parts = + for part <- parts do + if is_binary(part) do + part + |> replace_line(__CALLER__.line) + |> :elixir_interpolation.unescape_string() + else + part + end + end + + {:<<>>, meta, parts} + end + + defp replace_line(string, line) do + [head | rest] = String.split(string, "LINE") + + rest = + for part <- rest do + case part do + <> when num in ?0..?9 -> + [Integer.to_string(line - num + ?0), part] + + part -> + [Integer.to_string(line), part] + end + end + + IO.iodata_to_binary([head | rest]) + end end diff --git a/lib/elixir/test/elixir/module/types/types_test.exs b/lib/elixir/test/elixir/module/types/types_test.exs deleted file mode 100644 index dfebb6cb1b..0000000000 --- a/lib/elixir/test/elixir/module/types/types_test.exs +++ /dev/null @@ -1,79 +0,0 @@ -Code.require_file("type_helper.exs", __DIR__) - -defmodule Module.Types.TypesTest do - use ExUnit.Case, async: true - alias Module.Types - - defmacro warning(patterns \\ [], guards \\ [], body) do - min_line = min_line(patterns ++ guards ++ [body]) - patterns = reset_line(patterns, min_line) - guards = reset_line(guards, min_line) - body = reset_line(body, min_line) - - quote do - unquote(TypeHelper.typecheck(patterns, guards, body, __CALLER__)) - |> Module.Types.TypesTest.__warning__() - end - end - - def __warning__(result) do - context = - case result do - {:ok, _, context} -> context - {:error, context} -> context - end - - case context.warnings do - [warning] -> to_message(warning) - [] -> raise "no warnings" - [_ | _] = warnings -> raise "too many warnings: #{inspect(warnings)}" - end - end - - defp reset_line(ast, min_line) do - Macro.prewalk(ast, fn ast -> - Macro.update_meta(ast, fn meta -> - Keyword.update!(meta, :line, &(&1 - min_line + 1)) - end) - end) - end - - defp min_line(ast) do - {_ast, min} = - Macro.prewalk(ast, :infinity, fn - {_fun, meta, _args} = ast, min -> {ast, min(min, Keyword.get(meta, :line, 1))} - other, min -> {other, min} - end) - - min - end - - defp to_message({module, warning, _location}) do - warning - |> module.format_warning() - |> IO.iodata_to_binary() - end - - test "expr_to_string/1" do - assert Types.expr_to_string({1, 2}) == "{1, 2}" - assert Types.expr_to_string(quote(do: Foo.bar(arg))) == "Foo.bar(arg)" - assert Types.expr_to_string(quote(do: :erlang.band(a, b))) == "Bitwise.band(a, b)" - assert Types.expr_to_string(quote(do: :erlang.orelse(a, b))) == "a or b" - assert Types.expr_to_string(quote(do: :erlang."=:="(a, b))) == "a === b" - assert Types.expr_to_string(quote(do: :erlang.list_to_atom(a))) == "List.to_atom(a)" - assert Types.expr_to_string(quote(do: :maps.remove(a, b))) == "Map.delete(b, a)" - assert Types.expr_to_string(quote(do: :erlang.element(1, a))) == "elem(a, 0)" - assert Types.expr_to_string(quote(do: :erlang.element(:erlang.+(a, 1), b))) == "elem(b, a)" - end - - test "undefined function warnings" do - assert warning(URI.unknown("foo")) == - "URI.unknown/1 is undefined or private" - - assert warning(if(true, do: URI.unknown("foo"))) == - "URI.unknown/1 is undefined or private" - - assert warning(try(do: :ok, after: URI.unknown("foo"))) == - "URI.unknown/1 is undefined or private" - end -end From 77640a7266d8683385d26a60ea3229acc6630855 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 8 Jan 2024 17:33:01 +0100 Subject: [PATCH 0287/1886] Trim paths whenever possible --- lib/elixir/lib/module/types/pattern.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/lib/module/types/pattern.ex b/lib/elixir/lib/module/types/pattern.ex index 6cba56058c..258ec60914 100644 --- a/lib/elixir/lib/module/types/pattern.ex +++ b/lib/elixir/lib/module/types/pattern.ex @@ -303,7 +303,7 @@ defmodule Module.Types.Pattern do """ - # #{Exception.format_file_line(file, meta[:line])} + # #{Exception.format_file_line(Path.relative_to_cwd(file), meta[:line])} #{Macro.to_string(expr)} => #{to_quoted_string(type)} """ From b1a3e2650b970b72da91ff788881ee0e238b5b2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 8 Jan 2024 18:24:33 +0100 Subject: [PATCH 0288/1886] Improve warning, fix pitfalls --- lib/elixir/lib/module/types/descr.ex | 94 ++++++++++--------- lib/elixir/lib/module/types/of.ex | 7 +- lib/elixir/lib/module/types/pattern.ex | 15 ++- .../test/elixir/module/types/expr_test.exs | 18 ++-- .../test/elixir/module/types/pattern_test.exs | 20 ++-- 5 files changed, 91 insertions(+), 63 deletions(-) diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex index 2ca38edb9d..2af3448c95 100644 --- a/lib/elixir/lib/module/types/descr.ex +++ b/lib/elixir/lib/module/types/descr.ex @@ -5,48 +5,58 @@ defmodule Module.Types.Descr do # Types are represented as maps of non-overlapping unions. # A bitmap is used to represent non-divisible types. All other # types require specific data structures. + + # TODO: When we convert from AST to descr, we need to normalize + # the dynamic type. import Bitwise - @binary 1 <<< 1 - @empty_list 1 <<< 2 - @integer 1 <<< 3 - @float 1 <<< 4 - @pid 1 <<< 5 - @port 1 <<< 6 - @reference 1 <<< 7 - - @atom 1 <<< 8 - @non_empty_list 1 <<< 9 - @map 1 <<< 10 - @tuple 1 <<< 11 - @fun 1 <<< 12 - @top (1 <<< 13) - 1 + @bit_binary 1 <<< 1 + @bit_empty_list 1 <<< 2 + @bit_integer 1 <<< 3 + @bit_float 1 <<< 4 + @bit_pid 1 <<< 5 + @bit_port 1 <<< 6 + @bit_reference 1 <<< 7 + + @bit_atom 1 <<< 8 + @bit_non_empty_list 1 <<< 9 + @bit_map 1 <<< 10 + @bit_tuple 1 <<< 11 + @bit_fun 1 <<< 12 + @bit_top (1 <<< 13) - 1 # Guard helpers - defguard is_none(map) when map == %{} + @term %{bitmap: @bit_top} + @none %{} # Type definitions + # TODO: Have an atom for term() def dynamic(), do: :dynamic - def term(), do: %{bitmap: @top} - def none(), do: %{} - - def atom(_atom), do: %{bitmap: @atom} - def binary(), do: %{bitmap: @binary} - def empty_list(), do: %{bitmap: @empty_list} - def integer(), do: %{bitmap: @integer} - def float(), do: %{bitmap: @float} - def fun(), do: %{bitmap: @fun} - def map(), do: %{bitmap: @map} - def non_empty_list(), do: %{bitmap: @non_empty_list} - def pid(), do: %{bitmap: @pid} - def port(), do: %{bitmap: @port} - def reference(), do: %{bitmap: @reference} - def tuple(), do: %{bitmap: @tuple} + def term(), do: @term + def none(), do: @none + + def atom(_atom), do: %{bitmap: @bit_atom} + def binary(), do: %{bitmap: @bit_binary} + def empty_list(), do: %{bitmap: @bit_empty_list} + def integer(), do: %{bitmap: @bit_integer} + def float(), do: %{bitmap: @bit_float} + def fun(), do: %{bitmap: @bit_fun} + def map(), do: %{bitmap: @bit_map} + def non_empty_list(), do: %{bitmap: @bit_non_empty_list} + def pid(), do: %{bitmap: @bit_pid} + def port(), do: %{bitmap: @bit_port} + def reference(), do: %{bitmap: @bit_reference} + def tuple(), do: %{bitmap: @bit_tuple} ## Set operations + @doc """ + Check type is empty. + """ + def empty?(descr), do: descr == @none + @doc """ Computes the union of two descrs. """ @@ -174,18 +184,18 @@ defmodule Module.Types.Descr do defp bitmap_to_quoted(val) do pairs = [ - binary: @binary, - empty_list: @empty_list, - integer: @integer, - float: @float, - pid: @pid, - port: @port, - reference: @reference, - atom: @atom, - non_empty_list: @non_empty_list, - map: @map, - tuple: @tuple, - fun: @fun + binary: @bit_binary, + empty_list: @bit_empty_list, + integer: @bit_integer, + float: @bit_float, + pid: @bit_pid, + port: @bit_port, + reference: @bit_reference, + atom: @bit_atom, + non_empty_list: @bit_non_empty_list, + map: @bit_map, + tuple: @bit_tuple, + fun: @bit_fun ] for {type, mask} <- pairs, diff --git a/lib/elixir/lib/module/types/of.ex b/lib/elixir/lib/module/types/of.ex index 877a5dda7b..1c74e17989 100644 --- a/lib/elixir/lib/module/types/of.ex +++ b/lib/elixir/lib/module/types/of.ex @@ -113,9 +113,9 @@ defmodule Module.Types.Of do # If we are in a pattern and we have a variable, the refinement # will already have checked the type, so we skip the check here. # TODO: properly handle dynamic. Do we need materialization? - if actual_type == :dynamic or + if actual_type == dynamic() or (kind == :pattern and is_var(left)) or - is_none(difference(actual_type, expected_type)) do + empty?(difference(actual_type, expected_type)) do {:ok, context} else hints = if meta[:inferred_bitstring_spec], do: [:inferred_bitstring_spec], else: [] @@ -264,7 +264,8 @@ defmodule Module.Types.Of do #{to_quoted_string(actual_type)} """, traces, - format_hints(hints ++ trace_hints) + format_hints(hints ++ trace_hints), + "\ntyping violation found at:" ] end diff --git a/lib/elixir/lib/module/types/pattern.ex b/lib/elixir/lib/module/types/pattern.ex index 258ec60914..3fbb2c72cf 100644 --- a/lib/elixir/lib/module/types/pattern.ex +++ b/lib/elixir/lib/module/types/pattern.ex @@ -46,7 +46,7 @@ defmodule Module.Types.Pattern do data = %{data | type: new_type, off_traces: new_trace(expr, type, stack, off_traces)} context = put_in(context.vars[version], data) - if is_none(new_type) do + if empty?(new_type) do {:error, warn(__MODULE__, {:refine_var, old_type, type, var, context}, meta, stack, context)} else @@ -261,7 +261,8 @@ defmodule Module.Types.Pattern do #{to_quoted_string(old_type)} !~ #{to_quoted_string(new_type)} """, traces, - format_hints(hints) + format_hints(hints), + "\ntyping violation found at:" ] end @@ -301,11 +302,17 @@ defmodule Module.Types.Pattern do |> Enum.map(fn {expr, file, type} -> meta = get_meta(expr) + location = + file + |> Path.relative_to_cwd() + |> Exception.format_file_line(meta[:line]) + |> String.replace_suffix(":", "") + """ - # #{Exception.format_file_line(Path.relative_to_cwd(file), meta[:line])} + # type: #{to_quoted_string(type)} + # from: #{location} #{Macro.to_string(expr)} - => #{to_quoted_string(type)} """ end) diff --git a/lib/elixir/test/elixir/module/types/expr_test.exs b/lib/elixir/test/elixir/module/types/expr_test.exs index 3709513642..52a6c5e829 100644 --- a/lib/elixir/test/elixir/module/types/expr_test.exs +++ b/lib/elixir/test/elixir/module/types/expr_test.exs @@ -51,9 +51,11 @@ defmodule Module.Types.ExprTest do where "x" was given the type: - # types_test.ex:LINE-2: + # type: binary() + # from: types_test.ex:LINE-2 <> - => binary() + + typing violation found at:\ """} assert typewarn!([<>], <>) == @@ -73,11 +75,13 @@ defmodule Module.Types.ExprTest do where "x" was given the type: - # types_test.ex:LINE-2: + # type: binary() + # from: types_test.ex:LINE-2 <> - => binary() #{hints(:inferred_bitstring_spec)} + + typing violation found at:\ """} assert typewarn!([<>], <>) == @@ -97,11 +101,13 @@ defmodule Module.Types.ExprTest do where "x" was given the type: - # types_test.ex:LINE-2: + # type: integer() + # from: types_test.ex:LINE-2 <> - => integer() #{hints(:inferred_bitstring_spec)} + + typing violation found at:\ """} end end diff --git a/lib/elixir/test/elixir/module/types/pattern_test.exs b/lib/elixir/test/elixir/module/types/pattern_test.exs index 7e703c99a4..623bd96246 100644 --- a/lib/elixir/test/elixir/module/types/pattern_test.exs +++ b/lib/elixir/test/elixir/module/types/pattern_test.exs @@ -22,13 +22,15 @@ defmodule Module.Types.PatternTest do where "x" was given the types: - # types_test.ex:LINE: + # type: binary() + # from: types_test.ex:LINE <> - => binary() - # types_test.ex:LINE: + # type: float() + # from: types_test.ex:LINE <<..., x::float>> - => float() + + typing violation found at:\ """ assert typeerror!([<>], x) == ~l""" @@ -38,15 +40,17 @@ defmodule Module.Types.PatternTest do where "x" was given the types: - # types_test.ex:LINE: + # type: float() + # from: types_test.ex:LINE <> - => float() - # types_test.ex:LINE: + # type: integer() + # from: types_test.ex:LINE <<..., x>> - => integer() #{hints(:inferred_bitstring_spec)} + + typing violation found at:\ """ end end From 8cde231bbd427b65c964dbe0d88304e1cf0a906a Mon Sep 17 00:00:00 2001 From: Jean Klingler Date: Tue, 9 Jan 2024 15:25:14 +0900 Subject: [PATCH 0289/1886] behaviour -> behavior when not about OTP behaviours (#13240) --- lib/elixir/lib/code.ex | 16 ++++++++-------- lib/elixir/lib/collectable.ex | 4 ++-- lib/elixir/lib/config/provider.ex | 2 +- lib/elixir/lib/enum.ex | 8 ++++---- lib/elixir/lib/file.ex | 8 ++++---- lib/elixir/lib/float.ex | 14 +++++++------- lib/elixir/lib/io/ansi.ex | 2 +- lib/elixir/lib/kernel.ex | 14 +++++++------- lib/elixir/lib/kernel/special_forms.ex | 14 +++++++------- lib/elixir/lib/list.ex | 2 +- lib/elixir/lib/module/types/helpers.ex | 2 +- lib/elixir/lib/path.ex | 2 +- lib/elixir/lib/process.ex | 4 ++-- lib/elixir/lib/protocol.ex | 4 ++-- lib/elixir/lib/range.ex | 2 +- lib/elixir/lib/regex.ex | 4 ++-- lib/elixir/lib/registry.ex | 2 +- lib/elixir/lib/string.ex | 6 +++--- lib/elixir/lib/system.ex | 6 +++--- lib/elixir/lib/task.ex | 2 +- .../pages/anti-patterns/code-anti-patterns.md | 8 ++++---- .../pages/anti-patterns/design-anti-patterns.md | 6 +++--- .../pages/getting-started/anonymous-functions.md | 2 +- .../binaries-strings-and-charlists.md | 2 +- .../getting-started/modules-and-functions.md | 2 +- lib/elixir/pages/getting-started/protocols.md | 6 +++--- .../getting-started/writing-documentation.md | 2 +- .../pages/mix-and-otp/docs-tests-and-with.md | 2 +- .../pages/mix-and-otp/introduction-to-mix.md | 2 +- lib/elixir/pages/mix-and-otp/task-and-gen-tcp.md | 2 +- .../references/compatibility-and-deprecations.md | 4 ++-- lib/elixir/src/elixir_dispatch.erl | 2 +- lib/elixir/test/elixir/file_test.exs | 10 +++++----- lib/iex/lib/iex.ex | 2 +- lib/mix/lib/mix.ex | 6 +++--- lib/mix/lib/mix/dep/loader.ex | 2 +- lib/mix/lib/mix/release.ex | 2 +- man/mix.1 | 2 +- 38 files changed, 91 insertions(+), 91 deletions(-) diff --git a/lib/elixir/lib/code.ex b/lib/elixir/lib/code.ex index f71e75366d..0f0d0b8fe0 100644 --- a/lib/elixir/lib/code.ex +++ b/lib/elixir/lib/code.ex @@ -3,14 +3,14 @@ defmodule Code do Utilities for managing code compilation, code evaluation, and code loading. This module complements Erlang's [`:code` module](`:code`) - to add behaviour which is specific to Elixir. For functions to + to add behavior which is specific to Elixir. For functions to manipulate Elixir's AST (rather than evaluating it), see the `Macro` module. ## Working with files This module contains three functions for compiling and evaluating files. - Here is a summary of them and their behaviour: + Here is a summary of them and their behavior: * `require_file/2` - compiles a file and tracks its name. It does not compile the file again if it has been previously required. @@ -710,8 +710,8 @@ defmodule Code do specially because a function is named `defmodule`, `def`, or the like. This principle mirrors Elixir's goal of being an extensible language where developers can extend the language with new constructs as if they were - part of the language. When it is absolutely necessary to change behaviour - based on the name, this behaviour should be configurable, such as the + part of the language. When it is absolutely necessary to change behavior + based on the name, this behavior should be configurable, such as the `:locals_without_parens` option. ## Running the formatter @@ -855,7 +855,7 @@ defmodule Code do * Newlines before certain operators (such as the pipeline operators) and before other operators (such as comparison operators) - The behaviours above are not guaranteed. We may remove or add new + The behaviors above are not guaranteed. We may remove or add new rules in the future. The goal of documenting them is to provide better understanding on what to expect from the formatter. @@ -1145,7 +1145,7 @@ defmodule Code do * `:static_atoms_encoder` - the static atom encoder function, see "The `:static_atoms_encoder` function" section below. Note this - option overrides the `:existing_atoms_only` behaviour for static + option overrides the `:existing_atoms_only` behavior for static atoms but `:existing_atoms_only` is still used for dynamic atoms, such as atoms with interpolations. @@ -1627,7 +1627,7 @@ defmodule Code do error. You may be set it to `:warn` if you want undefined variables to emit a warning and expand as to a local call to the zero-arity function of the same name (for example, `node` would be expanded as `node()`). - This `:warn` behaviour only exists for compatibility reasons when working + This `:warn` behavior only exists for compatibility reasons when working with old dependencies. It always returns `:ok`. Raises an error for invalid options. @@ -1961,7 +1961,7 @@ defmodule Code do @doc """ Returns `true` if the module is loaded. - This function doesn't attempt to load the module. For such behaviour, + This function doesn't attempt to load the module. For such behavior, `ensure_loaded?/1` can be used. ## Examples diff --git a/lib/elixir/lib/collectable.ex b/lib/elixir/lib/collectable.ex index 2ee45f14eb..782df76607 100644 --- a/lib/elixir/lib/collectable.ex +++ b/lib/elixir/lib/collectable.ex @@ -94,10 +94,10 @@ end defimpl Collectable, for: List do def into(list) do - # TODO: Change the behaviour so the into always comes last on Elixir v2.0 + # TODO: Change the behavior so the into always comes last on Elixir v2.0 if list != [] do IO.warn( - "the Collectable protocol is deprecated for non-empty lists. The behaviour of " <> + "the Collectable protocol is deprecated for non-empty lists. The behavior of " <> "Enum.into/2 and \"for\" comprehensions with an :into option is incorrect " <> "when collecting into non-empty lists. If you're collecting into a non-empty keyword " <> "list, consider using Keyword.merge/2 instead. If you're collecting into a non-empty " <> diff --git a/lib/elixir/lib/config/provider.ex b/lib/elixir/lib/config/provider.ex index 9cc52cf72b..b8ead82056 100644 --- a/lib/elixir/lib/config/provider.ex +++ b/lib/elixir/lib/config/provider.ex @@ -312,7 +312,7 @@ defmodule Config.Provider do """ the application #{inspect(app)} has a different value set #{path(key, path)} \ during runtime compared to compile time. Since this application environment entry was \ - marked as compile time, this difference can lead to different behaviour than expected: + marked as compile time, this difference can lead to different behavior than expected: * Compile time value #{return_to_text(compile_return)} * Runtime value #{return_to_text(runtime_return)} diff --git a/lib/elixir/lib/enum.ex b/lib/elixir/lib/enum.ex index 1a2dfe52b3..00d377d27e 100644 --- a/lib/elixir/lib/enum.ex +++ b/lib/elixir/lib/enum.ex @@ -269,12 +269,12 @@ defmodule Enum do After all, if we want to traverse every element on a list, the longer the list, the more elements we need to traverse, and the longer it will take. - This linear behaviour should also be expected on operations like `count/1`, + This linear behavior should also be expected on operations like `count/1`, `member?/2`, `at/2` and similar. While Elixir does allow data types to provide performant variants for such operations, you should not expect it to always be available, since the `Enum` module is meant to work with a large variety of data types and not all data types can provide optimized - behaviour. + behavior. Finally, note the functions in the `Enum` module are eager: they will traverse the enumerable as soon as they are invoked. This is particularly @@ -4020,7 +4020,7 @@ defmodule Enum do @doc """ Reduces over two enumerables halting as soon as either enumerable is empty. - In practice, the behaviour provided by this function can be achieved with: + In practice, the behavior provided by this function can be achieved with: Enum.reduce(Stream.zip(left, right), acc, reducer) @@ -4054,7 +4054,7 @@ defmodule Enum do The reducer will receive 2 args: a list of elements (one from each enum) and the accumulator. - In practice, the behaviour provided by this function can be achieved with: + In practice, the behavior provided by this function can be achieved with: Enum.reduce(Stream.zip(enums), acc, reducer) diff --git a/lib/elixir/lib/file.ex b/lib/elixir/lib/file.ex index ecf4b8550d..1092899526 100644 --- a/lib/elixir/lib/file.ex +++ b/lib/elixir/lib/file.ex @@ -758,7 +758,7 @@ defmodule File do Note: The command `mv` in Unix-like systems behaves differently depending on whether `source` is a file and the `destination` is an existing directory. - We have chosen to explicitly disallow this behaviour. + We have chosen to explicitly disallow this behavior. ## Examples @@ -822,7 +822,7 @@ defmodule File do The function receives arguments for `source_file` and `destination_file`. It should return `true` if the existing file should be overwritten, `false` if otherwise. The default callback returns `true`. On earlier versions, this callback could be - given as third argument, but such behaviour is now deprecated. + given as third argument, but such behavior is now deprecated. """ @spec cp(Path.t(), Path.t(), on_conflict: on_conflict_callback) :: :ok | {:error, posix} @@ -895,7 +895,7 @@ defmodule File do Note: The command `cp` in Unix-like systems behaves differently depending on whether `destination` is an existing directory or not. We have chosen to - explicitly disallow this behaviour. If `source` is a `file` and `destination` + explicitly disallow this behavior. If `source` is a `file` and `destination` is a directory, `{:error, :eisdir}` will be returned. ## Options @@ -904,7 +904,7 @@ defmodule File do The function receives arguments for `source` and `destination`. It should return `true` if the existing file should be overwritten, `false` if otherwise. The default callback returns `true`. On earlier versions, this callback could be given as third - argument, but such behaviour is now deprecated. + argument, but such behavior is now deprecated. * `:dereference_symlinks` - (since v1.14.0) By default, this function will copy symlinks by creating symlinks that point to the same location. This option forces symlinks to be diff --git a/lib/elixir/lib/float.ex b/lib/elixir/lib/float.ex index 2c6c42eee0..305395a38f 100644 --- a/lib/elixir/lib/float.ex +++ b/lib/elixir/lib/float.ex @@ -210,7 +210,7 @@ defmodule Float do ## Known issues - The behaviour of `floor/2` for floats can be surprising. For example: + The behavior of `floor/2` for floats can be surprising. For example: iex> Float.floor(12.52, 2) 12.51 @@ -218,7 +218,7 @@ defmodule Float do One may have expected it to floor to 12.52. This is not a bug. Most decimal fractions cannot be represented as a binary floating point and therefore the number above is internally represented as 12.51999999, - which explains the behaviour above. + which explains the behavior above. ## Examples @@ -254,7 +254,7 @@ defmodule Float do The operation is performed on the binary floating point, without a conversion to decimal. - The behaviour of `ceil/2` for floats can be surprising. For example: + The behavior of `ceil/2` for floats can be surprising. For example: iex> Float.ceil(-12.52, 2) -12.51 @@ -262,7 +262,7 @@ defmodule Float do One may have expected it to ceil to -12.52. This is not a bug. Most decimal fractions cannot be represented as a binary floating point and therefore the number above is internally represented as -12.51999999, - which explains the behaviour above. + which explains the behavior above. This function always returns floats. `Kernel.trunc/1` may be used instead to truncate the result to an integer afterwards. @@ -305,7 +305,7 @@ defmodule Float do ## Known issues - The behaviour of `round/2` for floats can be surprising. For example: + The behavior of `round/2` for floats can be surprising. For example: iex> Float.round(5.5675, 3) 5.567 @@ -313,8 +313,8 @@ defmodule Float do One may have expected it to round to the half up 5.568. This is not a bug. Most decimal fractions cannot be represented as a binary floating point and therefore the number above is internally represented as 5.567499999, - which explains the behaviour above. If you want exact rounding for decimals, - you must use a decimal library. The behaviour above is also in accordance + which explains the behavior above. If you want exact rounding for decimals, + you must use a decimal library. The behavior above is also in accordance to reference implementations, such as "Correctly Rounded Binary-Decimal and Decimal-Binary Conversions" by David M. Gay. diff --git a/lib/elixir/lib/io/ansi.ex b/lib/elixir/lib/io/ansi.ex index 2932418c8f..01af9d4bb2 100644 --- a/lib/elixir/lib/io/ansi.ex +++ b/lib/elixir/lib/io/ansi.ex @@ -268,7 +268,7 @@ defmodule IO.ANSI do The named sequences are represented by atoms. It will also append an `IO.ANSI.reset/0` to the chardata when a conversion is - performed. If you don't want this behaviour, use `format_fragment/2`. + performed. If you don't want this behavior, use `format_fragment/2`. An optional boolean parameter can be passed to enable or disable emitting actual ANSI codes. When `false`, no ANSI codes will be emitted. diff --git a/lib/elixir/lib/kernel.ex b/lib/elixir/lib/kernel.ex index 5c359c3c17..2fcb3d3097 100644 --- a/lib/elixir/lib/kernel.ex +++ b/lib/elixir/lib/kernel.ex @@ -125,7 +125,7 @@ defmodule Kernel do ### Supporting documents Under the "Pages" section in sidebar you will find tutorials, guides, - and reference documents that outline Elixir semantics and behaviours + and reference documents that outline Elixir semantics and behaviors in more detail. Those are: * [Compatibility and deprecations](compatibility-and-deprecations.md) - lists @@ -548,10 +548,10 @@ defmodule Kernel do * `exit({:shutdown, term})` Exiting with any other reason is considered abnormal and treated - as a crash. This means the default supervisor behaviour kicks in, + as a crash. This means the default supervisor behavior kicks in, error reports are emitted, and so forth. - This behaviour is relied on in many different places. For example, + This behavior is relied on in many different places. For example, `ExUnit` uses `exit(:shutdown)` when exiting the test process to signal linked processes, supervision trees and so on to politely shut down too. @@ -2362,7 +2362,7 @@ defmodule Kernel do Keys in the `Enumerable` that don't exist in the struct are automatically discarded. Note that keys must be atoms, as only atoms are allowed when defining a struct. If there are duplicate keys in the `Enumerable`, the last - entry will be taken (same behaviour as `Map.new/1`). + entry will be taken (same behavior as `Map.new/1`). This function is useful for dynamically creating and updating structs, as well as for converting maps to structs; in the latter case, just inserting @@ -2410,7 +2410,7 @@ defmodule Kernel do @doc """ Similar to `struct/2` but checks for key validity. - The function `struct!/2` emulates the compile time behaviour + The function `struct!/2` emulates the compile time behavior of structs. This means that: * when building a struct, as in `struct!(SomeStruct, key: :value)`, @@ -3389,7 +3389,7 @@ defmodule Kernel do The pin operator will check if the values are equal, using `===/2`, while patterns have their own rules when matching maps, lists, and so forth. - Such behaviour is not specific to `match?/2`. The following code also + Such behavior is not specific to `match?/2`. The following code also throws an exception: attrs = %{x: 1} @@ -3928,7 +3928,7 @@ defmodule Kernel do one element, which is the number itself. If first is more than last, the range will be decreasing from first - to last, albeit this behaviour is deprecated. Instead prefer to + to last, albeit this behavior is deprecated. Instead prefer to explicitly list the step with `first..last//-1`. See the `Range` module for more information. diff --git a/lib/elixir/lib/kernel/special_forms.ex b/lib/elixir/lib/kernel/special_forms.ex index b307cc5970..47d4dca75f 100644 --- a/lib/elixir/lib/kernel/special_forms.ex +++ b/lib/elixir/lib/kernel/special_forms.ex @@ -550,7 +550,7 @@ defmodule Kernel.SpecialForms do Elixir won't emit any warnings though, since the alias was not explicitly defined. - Both warning behaviours could be changed by explicitly + Both warning behaviors could be changed by explicitly setting the `:warn` option to `true` or `false`. """ @@ -674,7 +674,7 @@ defmodule Kernel.SpecialForms do Elixir won't emit any warnings though, since the import was not explicitly defined. - Both warning behaviours could be changed by explicitly + Both warning behaviors could be changed by explicitly setting the `:warn` option to `true` or `false`. ## Ambiguous function/macro names @@ -888,7 +888,7 @@ defmodule Kernel.SpecialForms do At first, there is nothing in this example that actually reveals it is a macro. But what is happening is that, at compilation time, `squared(5)` becomes `5 * 5`. The argument `5` is duplicated in the produced code, we - can see this behaviour in practice though because our macro actually has + can see this behavior in practice though because our macro actually has a bug: import Math @@ -915,7 +915,7 @@ defmodule Kernel.SpecialForms do my_number.() * my_number.() Which invokes the function twice, explaining why we get the printed value - twice! In the majority of the cases, this is actually unexpected behaviour, + twice! In the majority of the cases, this is actually unexpected behavior, and that's why one of the first things you need to keep in mind when it comes to macros is to **not unquote the same value more than once**. @@ -1444,7 +1444,7 @@ defmodule Kernel.SpecialForms do [elixir: :prolog] Given the grandparents of Erlang and Prolog were nil, those values were - filtered out. If you don't want this behaviour, a simple option is to + filtered out. If you don't want this behavior, a simple option is to move the filter inside the do-block: iex> languages = [elixir: :erlang, erlang: :prolog, prolog: nil] @@ -1496,7 +1496,7 @@ defmodule Kernel.SpecialForms do *Available since Elixir v1.8*. - While the `:into` option allows us to customize the comprehension behaviour + While the `:into` option allows us to customize the comprehension behavior to a given data type, such as putting all of the values inside a map or inside a binary, it is not always enough. @@ -1624,7 +1624,7 @@ defmodule Kernel.SpecialForms do iex> width nil - The behaviour of any expression in a clause is the same as if it was + The behavior of any expression in a clause is the same as if it was written outside of `with`. For example, `=` will raise a `MatchError` instead of returning the non-matched value: diff --git a/lib/elixir/lib/list.ex b/lib/elixir/lib/list.ex index afd19d2e79..71f370b26e 100644 --- a/lib/elixir/lib/list.ex +++ b/lib/elixir/lib/list.ex @@ -107,7 +107,7 @@ defmodule List do charlists in IEx when you encounter them, which shows you the type, description and also the raw representation in one single summary. - The rationale behind this behaviour is to better support + The rationale behind this behavior is to better support Erlang libraries which may return text as charlists instead of Elixir strings. In Erlang, charlists are the default way of handling strings, while in Elixir it's binaries. One diff --git a/lib/elixir/lib/module/types/helpers.ex b/lib/elixir/lib/module/types/helpers.ex index 776c83ac75..ff4d6dbd4e 100644 --- a/lib/elixir/lib/module/types/helpers.ex +++ b/lib/elixir/lib/module/types/helpers.ex @@ -27,7 +27,7 @@ defmodule Module.Types.Helpers do #{hint()} all expressions given to binaries are assumed to be of type \ integer() unless said otherwise. For example, <> assumes "expr" \ is an integer. Pass a modifier, such as <> or <>, \ - to change the default behaviour. + to change the default behavior. """ end) end diff --git a/lib/elixir/lib/path.ex b/lib/elixir/lib/path.ex index 4cf3445cc9..cba9d68989 100644 --- a/lib/elixir/lib/path.ex +++ b/lib/elixir/lib/path.ex @@ -261,7 +261,7 @@ defmodule Path do Path.relative("/bar/foo.ex") #=> "bar/foo.ex" """ - # Note this function does not expand paths because the behaviour + # Note this function does not expand paths because the behavior # is ambiguous. If we expand it before converting to relative, then # "/usr/../../foo" means "/foo". If we expand it after, it means "../foo". # We could expand only relative paths but it is best to say it never diff --git a/lib/elixir/lib/process.ex b/lib/elixir/lib/process.ex index abb69426a8..0129d726e8 100644 --- a/lib/elixir/lib/process.ex +++ b/lib/elixir/lib/process.ex @@ -195,7 +195,7 @@ defmodule Process do @doc """ Sends an exit signal with the given `reason` to `pid`. - The following behaviour applies if `reason` is any term except `:normal` + The following behavior applies if `reason` is any term except `:normal` or `:kill`: 1. If `pid` is not trapping exits, `pid` will exit with the given @@ -624,7 +624,7 @@ defmodule Process do exits with a reason other than `:normal` (which is also the exit reason used when a process finishes its job) and `pid1` is not trapping exits (see `flag/2`), then `pid1` will exit with the same reason as `pid2` and in turn - emit an exit signal to all its other linked processes. The behaviour when + emit an exit signal to all its other linked processes. The behavior when `pid1` is trapping exits is described in `exit/2`. See `:erlang.link/1` for more information. diff --git a/lib/elixir/lib/protocol.ex b/lib/elixir/lib/protocol.ex index b1bc957a1b..591d25354a 100644 --- a/lib/elixir/lib/protocol.ex +++ b/lib/elixir/lib/protocol.ex @@ -126,8 +126,8 @@ defmodule Protocol do Although the implementation above is arguably not a reasonable one. For example, it makes no sense to say a PID or an integer have a size of `0`. That's one of the reasons why `@fallback_to_any` - is an opt-in behaviour. For the majority of protocols, raising - an error when a protocol is not implemented is the proper behaviour. + is an opt-in behavior. For the majority of protocols, raising + an error when a protocol is not implemented is the proper behavior. ## Multiple implementations diff --git a/lib/elixir/lib/range.ex b/lib/elixir/lib/range.ex index c59a28a93a..475021626c 100644 --- a/lib/elixir/lib/range.ex +++ b/lib/elixir/lib/range.ex @@ -172,7 +172,7 @@ defmodule Range do one element, which is the number itself. If `first` is greater than `last`, the range will be decreasing from `first` - to `last`, albeit this behaviour is deprecated. Therefore, it is advised to + to `last`, albeit this behavior is deprecated. Therefore, it is advised to explicitly list the step with `new/3`. ## Examples diff --git a/lib/elixir/lib/regex.ex b/lib/elixir/lib/regex.ex index 7b7f1b8332..ee9f658f6b 100644 --- a/lib/elixir/lib/regex.ex +++ b/lib/elixir/lib/regex.ex @@ -152,10 +152,10 @@ defmodule Regex do There is another character class, `ascii`, that erroneously matches Latin-1 characters instead of the 0-127 range specified by POSIX. This - cannot be fixed without altering the behaviour of other classes, so we + cannot be fixed without altering the behavior of other classes, so we recommend matching the range with `[\\0-\x7f]` instead. - Note the behaviour of those classes may change according to the Unicode + Note the behavior of those classes may change according to the Unicode and other modifiers: iex> String.match?("josé", ~r/^[[:lower:]]+$/) diff --git a/lib/elixir/lib/registry.ex b/lib/elixir/lib/registry.ex index 4177e35bae..42e963210a 100644 --- a/lib/elixir/lib/registry.ex +++ b/lib/elixir/lib/registry.ex @@ -18,7 +18,7 @@ defmodule Registry do implementation. We explore some of those use cases below. The registry may also be transparently partitioned, which provides - more scalable behaviour for running registries on highly concurrent + more scalable behavior for running registries on highly concurrent environments with thousands or millions of entries. ## Using in `:via` diff --git a/lib/elixir/lib/string.ex b/lib/elixir/lib/string.ex index 1966c53340..a547cbd42b 100644 --- a/lib/elixir/lib/string.ex +++ b/lib/elixir/lib/string.ex @@ -167,7 +167,7 @@ defmodule String do the locale, it is not taken into account by this module. In general, the functions in this module rely on the Unicode - Standard, but do not contain any of the locale specific behaviour. + Standard, but do not contain any of the locale specific behavior. More information about graphemes can be found in the [Unicode Standard Annex #29](https://www.unicode.org/reports/tr29/). @@ -227,7 +227,7 @@ defmodule String do to the definition of the encoding) is encountered, only one code point needs to be rejected. - This module relies on this behaviour to ignore such invalid + This module relies on this behavior to ignore such invalid characters. For example, `length/1` will return a correct result even if an invalid code point is fed into it. @@ -1472,7 +1472,7 @@ defmodule String do The `replacement` may be a string or a function that receives the matched pattern and must return the replacement as a string or iodata. - By default it replaces all occurrences but this behaviour can be controlled + By default it replaces all occurrences but this behavior can be controlled through the `:global` option; see the "Options" section below. ## Options diff --git a/lib/elixir/lib/system.ex b/lib/elixir/lib/system.ex index 7f67495e31..9b958b0086 100644 --- a/lib/elixir/lib/system.ex +++ b/lib/elixir/lib/system.ex @@ -42,7 +42,7 @@ defmodule System do * `system_time/0` - the VM view of the `os_time/0`. The system time and operating system time may not match in case of time warps although the VM works towards aligning them. This time is not monotonic (i.e., it may decrease) - as its behaviour is configured [by the VM time warp + as its behavior is configured [by the VM time warp mode](https://www.erlang.org/doc/apps/erts/time_correction.html#Time_Warp_Modes); * `monotonic_time/0` - a monotonically increasing time provided @@ -522,7 +522,7 @@ defmodule System do in case trapping exists is not supported by the current OS. The first time a signal is trapped, it will override the - default behaviour from the operating system. If the same + default behavior from the operating system. If the same signal is trapped multiple times, subsequent functions given to `trap_signal` will execute *first*. In other words, you can consider each function is prepended to @@ -536,7 +536,7 @@ defmodule System do * `:sigusr1` - halts the VM via status code of 1 Therefore, if you add traps to the signals above, the - default behaviour above will be executed after all user + default behavior above will be executed after all user signals. ## Implementation notes diff --git a/lib/elixir/lib/task.ex b/lib/elixir/lib/task.ex index 1b221050fa..56bd80e892 100644 --- a/lib/elixir/lib/task.ex +++ b/lib/elixir/lib/task.ex @@ -1181,7 +1181,7 @@ defmodule Task do given time. All other tasks will have been shut down using the `Task.shutdown/2` call. - As a convenience, you can achieve a similar behaviour to above + As a convenience, you can achieve a similar behavior to above by specifying the `:on_timeout` option to be `:kill_task` (or `:ignore`). See `Task.await_many/2` if you would rather exit the caller process on timeout. diff --git a/lib/elixir/pages/anti-patterns/code-anti-patterns.md b/lib/elixir/pages/anti-patterns/code-anti-patterns.md index 9bc9e9e1d2..0264c63590 100644 --- a/lib/elixir/pages/anti-patterns/code-anti-patterns.md +++ b/lib/elixir/pages/anti-patterns/code-anti-patterns.md @@ -141,7 +141,7 @@ end An `Atom` is an Elixir basic type whose value is its own name. Atoms are often useful to identify resources or express the state, or result, of an operation. Creating atoms dynamically is not an anti-pattern by itself; however, atoms are not garbage collected by the Erlang Virtual Machine, so values of this type live in memory during a software's entire execution lifetime. The Erlang VM limits the number of atoms that can exist in an application by default to *1_048_576*, which is more than enough to cover all atoms defined in a program, but attempts to serve as an early limit for applications which are "leaking atoms" through dynamic creation. -For these reason, creating atoms dynamically can be considered an anti-pattern when the developer has no control over how many atoms will be created during the software execution. This unpredictable scenario can expose the software to unexpected behaviour caused by excessive memory usage, or even by reaching the maximum number of *atoms* possible. +For these reason, creating atoms dynamically can be considered an anti-pattern when the developer has no control over how many atoms will be created during the software execution. This unpredictable scenario can expose the software to unexpected behavior caused by excessive memory usage, or even by reaching the maximum number of *atoms* possible. #### Example @@ -343,7 +343,7 @@ iex> Graphics.plot(point_3d) {5, 6, 7} ``` -Given we want to plot both 2D and 3D points, the behaviour above is expected. But what happens if we forget to pass a point with either `:x` or `:y`? +Given we want to plot both 2D and 3D points, the behavior above is expected. But what happens if we forget to pass a point with either `:x` or `:y`? ```elixir iex> bad_point = %{y: 3, z: 4} @@ -352,7 +352,7 @@ iex> Graphics.plot(bad_point) {nil, 3, 4} ``` -The behaviour above is unexpected because our function should not work with points without a `:x` key. This leads to subtle bugs, as we may now pass `nil` to another function, instead of raising early on. +The behavior above is unexpected because our function should not work with points without a `:x` key. This leads to subtle bugs, as we may now pass `nil` to another function, instead of raising early on. #### Refactoring @@ -448,7 +448,7 @@ iex> Extract.get_value("name=Lucas&university=institution=UFMG&lab=ASERG", "univ #### Refactoring -To remove this anti-pattern, `get_value/2` can be refactored through the use of pattern matching. So, if an unexpected URL query string format is used, the function will crash instead of returning an invalid value. This behaviour, shown below, allows clients to decide how to handle these errors and doesn't give a false impression that the code is working correctly when unexpected values are extracted: +To remove this anti-pattern, `get_value/2` can be refactored through the use of pattern matching. So, if an unexpected URL query string format is used, the function will crash instead of returning an invalid value. This behavior, shown below, allows clients to decide how to handle these errors and doesn't give a false impression that the code is working correctly when unexpected values are extracted: ```elixir defmodule Extract do diff --git a/lib/elixir/pages/anti-patterns/design-anti-patterns.md b/lib/elixir/pages/anti-patterns/design-anti-patterns.md index fb83e97a41..f4c2457a33 100644 --- a/lib/elixir/pages/anti-patterns/design-anti-patterns.md +++ b/lib/elixir/pages/anti-patterns/design-anti-patterns.md @@ -75,7 +75,7 @@ This is a special case of [*Primitive obsession*](#primitive-obsession), specifi #### Example -An example of this anti-pattern is a function that receives two or more options, such as `editor: true` and `admin: true`, to configure its behaviour in overlapping ways. In the code below, the `:editor` option has no effect if `:admin` is set, meaning that the `:admin` option has higher priority than `:editor`, and they are ultimately related. +An example of this anti-pattern is a function that receives two or more options, such as `editor: true` and `admin: true`, to configure its behavior in overlapping ways. In the code below, the `:editor` option has no effect if `:admin` is set, meaning that the `:admin` option has higher priority than `:editor`, and they are ultimately related. ```elixir defmodule MyApp do @@ -249,7 +249,7 @@ Using multi-clause functions is a powerful Elixir feature. However, some develop #### Example -A frequent example of this usage of multi-clause functions occurs when developers mix unrelated business logic into the same function definition, in a way that the behaviour of each clause becomes completely distinct from the others. Such functions often have too broad specifications, making it difficult for other developers to understand and maintain them. +A frequent example of this usage of multi-clause functions occurs when developers mix unrelated business logic into the same function definition, in a way that the behavior of each clause becomes completely distinct from the others. Such functions often have too broad specifications, making it difficult for other developers to understand and maintain them. Some developers may use documentation mechanisms such as `@doc` annotations to compensate for poor code readability, however the documentation itself may end-up full of conditionals to describe how the function behaves for each different argument combination. This is a good indicator that the clauses are ultimately unrelated. @@ -331,7 +331,7 @@ iex> struct(URI.parse("/foo/bar"), path: "/bar/baz") } ``` -The difference here is that the `struct/2` function behaves precisely the same for any struct given, therefore there is no question of how the function handles different inputs. If the behaviour is clear and consistent for all inputs, then the anti-pattern does not take place. +The difference here is that the `struct/2` function behaves precisely the same for any struct given, therefore there is no question of how the function handles different inputs. If the behavior is clear and consistent for all inputs, then the anti-pattern does not take place. ## Using application configuration for libraries diff --git a/lib/elixir/pages/getting-started/anonymous-functions.md b/lib/elixir/pages/getting-started/anonymous-functions.md index 8c12cf242a..501329405d 100644 --- a/lib/elixir/pages/getting-started/anonymous-functions.md +++ b/lib/elixir/pages/getting-started/anonymous-functions.md @@ -17,7 +17,7 @@ true In the example above, we defined an anonymous function that receives two arguments, `a` and `b`, and returns the result of `a + b`. The arguments are always on the left-hand side of `->` and the code to be executed on the right-hand side. The anonymous function is stored in the variable `add`. -We can invoke anonymous functions by passing arguments to it. Note that a dot (`.`) between the variable and parentheses is required to invoke an anonymous function. The dot makes it clear when you are calling an anonymous function, stored in the variable `add`, opposed to a function named `add/2`. For example, if you have an anonymous function stored in the variable `is_atom`, there is no ambiguity between `is_atom.(:foo)` and `is_atom(:foo)`. If both used the same `is_atom(:foo)` syntax, the only way to know the actual behaviour of `is_atom(:foo)` would be by scanning all code thus far for a possible definition of the `is_atom` variable. This scanning hurts maintainability as it requires developers to track additional context in their head when reading and writing code. +We can invoke anonymous functions by passing arguments to it. Note that a dot (`.`) between the variable and parentheses is required to invoke an anonymous function. The dot makes it clear when you are calling an anonymous function, stored in the variable `add`, opposed to a function named `add/2`. For example, if you have an anonymous function stored in the variable `is_atom`, there is no ambiguity between `is_atom.(:foo)` and `is_atom(:foo)`. If both used the same `is_atom(:foo)` syntax, the only way to know the actual behavior of `is_atom(:foo)` would be by scanning all code thus far for a possible definition of the `is_atom` variable. This scanning hurts maintainability as it requires developers to track additional context in their head when reading and writing code. Anonymous functions in Elixir are also identified by the number of arguments they receive. We can check if a function is of any given arity by using `is_function/2`: diff --git a/lib/elixir/pages/getting-started/binaries-strings-and-charlists.md b/lib/elixir/pages/getting-started/binaries-strings-and-charlists.md index abc59774bf..2cbdc5c55c 100644 --- a/lib/elixir/pages/getting-started/binaries-strings-and-charlists.md +++ b/lib/elixir/pages/getting-started/binaries-strings-and-charlists.md @@ -250,7 +250,7 @@ iex> is_list(~c"hełło") true ``` -This is done to ease interoperability with Erlang, even though it may lead to some surprising behaviour. For example, if you are storing a list of integers that happen to range between 0 and 127, by default IEx will interpret this as a charlist and it will display the corresponding ASCII characters. +This is done to ease interoperability with Erlang, even though it may lead to some surprising behavior. For example, if you are storing a list of integers that happen to range between 0 and 127, by default IEx will interpret this as a charlist and it will display the corresponding ASCII characters. ```elixir iex> heartbeats_per_minute = [99, 97, 116] diff --git a/lib/elixir/pages/getting-started/modules-and-functions.md b/lib/elixir/pages/getting-started/modules-and-functions.md index 8849f1db3e..31a3cba5fd 100644 --- a/lib/elixir/pages/getting-started/modules-and-functions.md +++ b/lib/elixir/pages/getting-started/modules-and-functions.md @@ -130,7 +130,7 @@ defmodule Math do end ``` -And it will provide the same behaviour. You may use `do:` for one-liners but always use `do`-blocks for functions spanning multiple lines. If you prefer to be consistent, you can use `do`-blocks throughout your codebase. +And it will provide the same behavior. You may use `do:` for one-liners but always use `do`-blocks for functions spanning multiple lines. If you prefer to be consistent, you can use `do`-blocks throughout your codebase. ## Default arguments diff --git a/lib/elixir/pages/getting-started/protocols.md b/lib/elixir/pages/getting-started/protocols.md index ba06b0eb0e..3099332241 100644 --- a/lib/elixir/pages/getting-started/protocols.md +++ b/lib/elixir/pages/getting-started/protocols.md @@ -1,6 +1,6 @@ # Protocols -Protocols are a mechanism to achieve polymorphism in Elixir where you want the behaviour to vary depending on the data type. We are already familiar with one way of solving this type of problem: via pattern matching and guard clauses. Consider a simple utility module that would tell us the type of input variable: +Protocols are a mechanism to achieve polymorphism in Elixir where you want the behavior to vary depending on the data type. We are already familiar with one way of solving this type of problem: via pattern matching and guard clauses. Consider a simple utility module that would tell us the type of input variable: ```elixir defmodule Utility do @@ -12,7 +12,7 @@ end If the use of this module were confined to your own project, you would be able to keep defining new `type/1` functions for each new data type. However, this code could be problematic if it was shared as a dependency by multiple apps because there would be no easy way to extend its functionality. -This is where protocols can help us: protocols allow us to extend the original behaviour for as many data types as we need. That's because **dispatching on a protocol is available to any data type that has implemented the protocol** and a protocol can be implemented by anyone, at any time. +This is where protocols can help us: protocols allow us to extend the original behavior for as many data types as we need. That's because **dispatching on a protocol is available to any data type that has implemented the protocol** and a protocol can be implemented by anyone, at any time. Here's how we could write the same `Utility.type/1` functionality as a protocol: @@ -184,7 +184,7 @@ defprotocol Size do end ``` -As we said in the previous section, the implementation of `Size` for `Any` is not one that can apply to any data type. That's one of the reasons why `@fallback_to_any` is an opt-in behaviour. For the majority of protocols, raising an error when a protocol is not implemented is the proper behaviour. That said, assuming we have implemented `Any` as in the previous section: +As we said in the previous section, the implementation of `Size` for `Any` is not one that can apply to any data type. That's one of the reasons why `@fallback_to_any` is an opt-in behavior. For the majority of protocols, raising an error when a protocol is not implemented is the proper behavior. That said, assuming we have implemented `Any` as in the previous section: ```elixir defimpl Size, for: Any do diff --git a/lib/elixir/pages/getting-started/writing-documentation.md b/lib/elixir/pages/getting-started/writing-documentation.md index bb061f4fd2..8ed897bc36 100644 --- a/lib/elixir/pages/getting-started/writing-documentation.md +++ b/lib/elixir/pages/getting-started/writing-documentation.md @@ -106,7 +106,7 @@ We recommend that developers include examples in their documentation, often unde Elixir treats documentation and code comments as different concepts. Documentation is an explicit contract between you and users of your Application Programming Interface (API), be them third-party developers, co-workers, or your future self. Modules and functions must always be documented if they are part of your API. -Code comments are aimed at developers reading the code. They are useful for marking improvements, leaving notes (for example, why you had to resort to a workaround due to a bug in a library), and so forth. They are tied to the source code: you can completely rewrite a function and remove all existing code comments, and it will continue to behave the same, with no change to either its behaviour or its documentation. +Code comments are aimed at developers reading the code. They are useful for marking improvements, leaving notes (for example, why you had to resort to a workaround due to a bug in a library), and so forth. They are tied to the source code: you can completely rewrite a function and remove all existing code comments, and it will continue to behave the same, with no change to either its behavior or its documentation. Because private functions cannot be accessed externally, Elixir will warn if a private function has a `@doc` attribute and will discard its content. However, you can add code comments to private functions, as with any other piece of code, and we recommend developers to do so whenever they believe it will add relevant information to the readers and maintainers of such code. diff --git a/lib/elixir/pages/mix-and-otp/docs-tests-and-with.md b/lib/elixir/pages/mix-and-otp/docs-tests-and-with.md index 03716bd8bf..729f3ef52e 100644 --- a/lib/elixir/pages/mix-and-otp/docs-tests-and-with.md +++ b/lib/elixir/pages/mix-and-otp/docs-tests-and-with.md @@ -88,7 +88,7 @@ def parse(line) do end ``` -Our implementation splits the line on whitespace and then matches the command against a list. Using `String.split/1` means our commands will be whitespace-insensitive. Leading and trailing whitespace won't matter, nor will consecutive spaces between words. Let's add some new doctests to test this behaviour along with the other commands: +Our implementation splits the line on whitespace and then matches the command against a list. Using `String.split/1` means our commands will be whitespace-insensitive. Leading and trailing whitespace won't matter, nor will consecutive spaces between words. Let's add some new doctests to test this behavior along with the other commands: ```elixir @doc ~S""" diff --git a/lib/elixir/pages/mix-and-otp/introduction-to-mix.md b/lib/elixir/pages/mix-and-otp/introduction-to-mix.md index 9abd86bc57..af1e5fa171 100644 --- a/lib/elixir/pages/mix-and-otp/introduction-to-mix.md +++ b/lib/elixir/pages/mix-and-otp/introduction-to-mix.md @@ -295,7 +295,7 @@ def project do end ``` -When true, the `:start_permanent` option starts your application in permanent mode, which means the Erlang VM will crash if your application's supervision tree shuts down. Notice we don't want this behaviour in dev and test because it is useful to keep the VM instance running in those environments for troubleshooting purposes. +When true, the `:start_permanent` option starts your application in permanent mode, which means the Erlang VM will crash if your application's supervision tree shuts down. Notice we don't want this behavior in dev and test because it is useful to keep the VM instance running in those environments for troubleshooting purposes. Mix will default to the `:dev` environment, except for the `test` task that will default to the `:test` environment. The environment can be changed via the `MIX_ENV` environment variable: diff --git a/lib/elixir/pages/mix-and-otp/task-and-gen-tcp.md b/lib/elixir/pages/mix-and-otp/task-and-gen-tcp.md index 9e32c1e428..abb883f3a8 100644 --- a/lib/elixir/pages/mix-and-otp/task-and-gen-tcp.md +++ b/lib/elixir/pages/mix-and-otp/task-and-gen-tcp.md @@ -232,7 +232,7 @@ defp loop_acceptor(socket) do end ``` -You might notice that we added a line, `:ok = :gen_tcp.controlling_process(client, pid)`. This makes the child process the "controlling process" of the `client` socket. If we didn't do this, the acceptor would bring down all the clients if it crashed because sockets would be tied to the process that accepted them (which is the default behaviour). +You might notice that we added a line, `:ok = :gen_tcp.controlling_process(client, pid)`. This makes the child process the "controlling process" of the `client` socket. If we didn't do this, the acceptor would bring down all the clients if it crashed because sockets would be tied to the process that accepted them (which is the default behavior). Start a new server with `PORT=4040 mix run --no-halt` and we can now open up many concurrent telnet clients. You will also notice that quitting a client does not bring the acceptor down. Excellent! diff --git a/lib/elixir/pages/references/compatibility-and-deprecations.md b/lib/elixir/pages/references/compatibility-and-deprecations.md index 9f6e4ee51d..fceec9da95 100644 --- a/lib/elixir/pages/references/compatibility-and-deprecations.md +++ b/lib/elixir/pages/references/compatibility-and-deprecations.md @@ -21,13 +21,13 @@ There are currently no plans for a major v2 release. ## Between non-major Elixir versions -Elixir minor and patch releases are backwards compatible: well-defined behaviours and documented APIs in a given version will continue working on future versions. +Elixir minor and patch releases are backwards compatible: well-defined behaviors and documented APIs in a given version will continue working on future versions. Although we expect the vast majority of programs to remain compatible over time, it is impossible to guarantee that no future change will break any program. Under some unlikely circumstances, we may introduce changes that break existing code: * Security: a security issue in the implementation may arise whose resolution requires backwards incompatible changes. We reserve the right to address such security issues. - * Bugs: if an API has undesired behaviour, a program that depends on the buggy behaviour may break if the bug is fixed. We reserve the right to fix such bugs. + * Bugs: if an API has undesired behavior, a program that depends on the buggy behavior may break if the bug is fixed. We reserve the right to fix such bugs. * Compiler front-end: improvements may be done to the compiler, introducing new warnings for ambiguous modes and providing more detailed error messages. Those can lead to compilation errors (when running with `--warning-as-errors`) or tooling failures when asserting on specific error messages (although one should avoid such). We reserve the right to do such improvements. diff --git a/lib/elixir/src/elixir_dispatch.erl b/lib/elixir/src/elixir_dispatch.erl index 5e264a4f13..df9b04bdc5 100644 --- a/lib/elixir/src/elixir_dispatch.erl +++ b/lib/elixir/src/elixir_dispatch.erl @@ -409,7 +409,7 @@ check_deprecated(Meta, Kind, ?application, Name, Arity, E) -> ok end; check_deprecated(Meta, Kind, Receiver, Name, Arity, E) -> - %% Any compile time behaviour cannot be verified by the runtime group pass. + %% Any compile time behavior cannot be verified by the runtime group pass. case ((?key(E, function) == nil) or (Kind == macro)) andalso get_deprecations(Receiver) of [_ | _] = Deprecations -> case lists:keyfind({Name, Arity}, 1, Deprecations) of diff --git a/lib/elixir/test/elixir/file_test.exs b/lib/elixir/test/elixir/file_test.exs index 7d936ccdb5..aa4a6b03f6 100644 --- a/lib/elixir/test/elixir/file_test.exs +++ b/lib/elixir/test/elixir/file_test.exs @@ -14,7 +14,7 @@ defmodule FileTest do # Following Erlang's underlying implementation # # Renaming files - # :ok -> rename file to existing file default behaviour + # :ok -> rename file to existing file default behavior # {:error, :eisdir} -> rename file to existing empty dir # {:error, :eisdir} -> rename file to existing non-empty dir # :ok -> rename file to non-existing location @@ -26,7 +26,7 @@ defmodule FileTest do # :ok -> rename dir to non-existing leaf location # {:error, ??} -> rename dir to non-existing parent location # :ok -> rename dir to itself - # :ok -> rename dir to existing empty dir default behaviour + # :ok -> rename dir to existing empty dir default behavior # {:error, :eexist} -> rename dir to existing empty dir # {:error, :einval} -> rename parent dir to existing sub dir # {:error, :einval} -> rename parent dir to non-existing sub dir @@ -35,7 +35,7 @@ defmodule FileTest do # other tests # {:error, :enoent} -> rename unknown source # :ok -> rename preserves mode - test "rename file to existing file default behaviour" do + test "rename file to existing file default behavior" do src = tmp_fixture_path("file.txt") dest = tmp_path("tmp.file") @@ -128,7 +128,7 @@ defmodule FileTest do end end - test "rename! file to existing file default behaviour" do + test "rename! file to existing file default behavior" do src = tmp_fixture_path("file.txt") dest = tmp_path("tmp.file") @@ -256,7 +256,7 @@ defmodule FileTest do end end - test "rename dir to existing empty dir default behaviour" do + test "rename dir to existing empty dir default behavior" do src = tmp_fixture_path("cp_r") dest = tmp_path("tmp") diff --git a/lib/iex/lib/iex.ex b/lib/iex/lib/iex.ex index c6f2dcc430..0b200a7ba5 100644 --- a/lib/iex/lib/iex.ex +++ b/lib/iex/lib/iex.ex @@ -115,7 +115,7 @@ defmodule IEx do iex(1)> [1, [2], 3] [1, [2], 3] - To prevent this behaviour breaking valid code where the subsequent line + To prevent this behavior breaking valid code where the subsequent line begins with a binary operator, such as `|>/2` or `++/2` , IEx automatically treats such lines as if they were prepended with `IEx.Helpers.v/0`, which returns the value of the previous expression, if available. diff --git a/lib/mix/lib/mix.ex b/lib/mix/lib/mix.ex index 8532964475..3afe169049 100644 --- a/lib/mix/lib/mix.ex +++ b/lib/mix/lib/mix.ex @@ -320,7 +320,7 @@ defmodule Mix do ## Environment variables - Several environment variables can be used to modify Mix's behaviour. + Several environment variables can be used to modify Mix's behavior. Mix responds to the following variables: @@ -367,7 +367,7 @@ defmodule Mix do * `MIX_TARGET` - specifies which target should be used. See [Targets](#module-targets) * `MIX_XDG` - asks Mix to follow the [XDG Directory Specification](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html) - for its home directory and configuration files. This behaviour needs to + for its home directory and configuration files. This behavior needs to be opt-in due to backwards compatibility. `MIX_HOME` has higher preference than `MIX_XDG`. If none of the variables are set, the default directory `~/.mix` will be used @@ -749,7 +749,7 @@ defmodule Mix do ## Limitations There is one limitation to `Mix.install/2`, which is actually an Elixir - behaviour. If you are installing a dependency that defines a struct or + behavior. If you are installing a dependency that defines a struct or macro, you cannot use the struct or macro immediately after the install call. For example, this won't work: diff --git a/lib/mix/lib/mix/dep/loader.ex b/lib/mix/lib/mix/dep/loader.ex index f9b532c9fa..5e988d8c07 100644 --- a/lib/mix/lib/mix/dep/loader.ex +++ b/lib/mix/lib/mix/dep/loader.ex @@ -12,7 +12,7 @@ defmodule Mix.Dep.Loader do are included as children. By default, it will filter all dependencies that does not match - current environment, behaviour can be overridden via options. + current environment, behavior can be overridden via options. """ def children(locked?) do mix_children(Mix.Project.config(), locked?, []) ++ Mix.Dep.Umbrella.unloaded() diff --git a/lib/mix/lib/mix/release.ex b/lib/mix/lib/mix/release.ex index ce3060a51f..0e2659dfbf 100644 --- a/lib/mix/lib/mix/release.ex +++ b/lib/mix/lib/mix/release.ex @@ -433,7 +433,7 @@ defmodule Mix.Release do the `:elixir` application configuration in `sys_config` to be read during boot and trigger the providers. - It uses the following release options to customize its behaviour: + It uses the following release options to customize its behavior: * `:reboot_system_after_config` * `:start_distribution_during_config` diff --git a/man/mix.1 b/man/mix.1 index 44d3ed8c76..e35a14a4ab 100644 --- a/man/mix.1 +++ b/man/mix.1 @@ -31,7 +31,7 @@ All the .Nm functionality is represented by a set of tasks. A .Em task -is a piece of code written in Elixir and intended for solving a particular problem. Like programs, many tasks accept input parameters and/or support options which slightly modify their behaviour, but others do not. There are two types of tasks: those that are available after installation this or that archive +is a piece of code written in Elixir and intended for solving a particular problem. Like programs, many tasks accept input parameters and/or support options which slightly modify their behavior, but others do not. There are two types of tasks: those that are available after installation this or that archive .Pq local tasks and those that are offered by .Nm From 31a1e6d41afa6fb4c4240459b314415f868c13d1 Mon Sep 17 00:00:00 2001 From: Josh Davis <18429247+jdav-dev@users.noreply.github.com> Date: Tue, 9 Jan 2024 03:12:46 -0500 Subject: [PATCH 0290/1886] Fix typespec for the list concatenation operator (#13239) Closes #13238. --- lib/elixir/lib/kernel.ex | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/elixir/lib/kernel.ex b/lib/elixir/lib/kernel.ex index 2fcb3d3097..34fb9194a0 100644 --- a/lib/elixir/lib/kernel.ex +++ b/lib/elixir/lib/kernel.ex @@ -1525,6 +1525,7 @@ defmodule Kernel do If the `right` operand is not a proper list, it returns an improper list. If the `left` operand is not a proper list, it raises `ArgumentError`. + If the `left` operand is an empty list, it returns the `right` operand. Inlined by the compiler. @@ -1547,6 +1548,10 @@ defmodule Kernel do iex> [1] ++ [2 | 3] [1, 2 | 3] + # empty list on the left will return the right operand + iex> [] ++ 1 + 1 + The `++/2` operator is right associative, meaning: iex> [1, 2, 3] -- [1] ++ [2] @@ -1558,7 +1563,8 @@ defmodule Kernel do [3] """ - @spec list ++ term :: maybe_improper_list + @spec [] ++ a :: a when a: term() + @spec nonempty_list() ++ term() :: maybe_improper_list() def left ++ right do :erlang.++(left, right) end From 6245a611f373356e5eb45bbb53b91d78d41813b4 Mon Sep 17 00:00:00 2001 From: Brad Hanks Date: Wed, 10 Jan 2024 01:41:08 -0700 Subject: [PATCH 0291/1886] Raise clearer error messages on collectable (#13235) --- lib/elixir/lib/collectable.ex | 12 +++++++ lib/elixir/lib/enum.ex | 3 +- lib/elixir/test/elixir/enum_test.exs | 53 ++++++++++++++++++++++++++++ 3 files changed, 66 insertions(+), 2 deletions(-) diff --git a/lib/elixir/lib/collectable.ex b/lib/elixir/lib/collectable.ex index 782df76607..9803ac727e 100644 --- a/lib/elixir/lib/collectable.ex +++ b/lib/elixir/lib/collectable.ex @@ -140,6 +140,10 @@ defimpl Collectable, for: BitString do __acc, :halt -> :ok + + _acc, {:cont, other} -> + raise ArgumentError, + "collecting into a binary requires a bitstring, got: #{inspect(other)}" end {[binary], fun} @@ -155,6 +159,10 @@ defimpl Collectable, for: BitString do _acc, :halt -> :ok + + _acc, {:cont, other} -> + raise ArgumentError, + "collecting into a bitstring requires a bitstring, got: #{inspect(other)}" end {bitstring, fun} @@ -172,6 +180,10 @@ defimpl Collectable, for: Map do _map_acc, :halt -> :ok + + _map_acc, {:cont, other} -> + raise ArgumentError, + "collecting into a map requires {key, value} tuples, got: #{inspect(other)}" end {map, fun} diff --git a/lib/elixir/lib/enum.ex b/lib/elixir/lib/enum.ex index 00d377d27e..c5f6df4599 100644 --- a/lib/elixir/lib/enum.ex +++ b/lib/elixir/lib/enum.ex @@ -1528,8 +1528,7 @@ defmodule Enum do defp into_map(enumerable) when is_list(enumerable), do: :maps.from_list(enumerable) defp into_map(enumerable), do: enumerable |> Enum.to_list() |> :maps.from_list() - defp into_map(%{} = enumerable, collectable), - do: Map.merge(collectable, enumerable) + defp into_map(%{} = enumerable, collectable), do: Map.merge(collectable, enumerable) defp into_map(enumerable, collectable) when is_list(enumerable), do: Map.merge(collectable, :maps.from_list(enumerable)) diff --git a/lib/elixir/test/elixir/enum_test.exs b/lib/elixir/test/elixir/enum_test.exs index eee7d772cd..ae0d341ed7 100644 --- a/lib/elixir/test/elixir/enum_test.exs +++ b/lib/elixir/test/elixir/enum_test.exs @@ -417,6 +417,59 @@ defmodule EnumTest do assert Enum.into(["H", "i"], "") == "Hi" end + test "into/2 exceptions" do + item = 1 + date = ~D[2015-01-01] + map = %{a: 1} + tuple = {:a, 1} + + map_msg = "collecting into a map requires {key, value} tuples, got: #{inspect(item)}" + map_date_msg = "collecting into a map requires {key, value} tuples, got: #{inspect(date)}" + assert_raise ArgumentError, map_msg, fn -> Enum.into(1..10, %{}) end + + assert_raise ArgumentError, map_date_msg, fn -> + Enum.into(Date.range(date, ~D[2015-01-03]), %{}) + end + + assert_raise ArgumentError, map_msg, fn -> Enum.into(Range.new(item, 10), %{a: 1}) end + + assert_raise ArgumentError, map_msg, fn -> + Enum.into(MapSet.new([item, 2, 3]), %{a: 1}) + end + + assert_raise ArgumentError, map_date_msg, fn -> + Enum.into(Date.range(date, ~D[2019-01-01]), %{a: 1}) + end + + bit_msg = "collecting into a binary requires a bitstring, got: #{inspect(item)}" + bit_map_msg = "collecting into a binary requires a bitstring, got: #{inspect(tuple)}" + bit_date_msg = "collecting into a binary requires a bitstring, got: #{inspect(date)}" + assert_raise ArgumentError, bit_msg, fn -> Enum.into(Range.new(item, 10, 1), <<>>) end + assert_raise ArgumentError, bit_msg, fn -> Enum.into([item, 2, 3], <<>>) end + assert_raise ArgumentError, bit_msg, fn -> Enum.into(MapSet.new([item, 2, 3, 4]), <<>>) end + assert_raise ArgumentError, bit_map_msg, fn -> Enum.into(map, <<>>) end + + assert_raise ArgumentError, bit_date_msg, fn -> + Enum.into(Date.range(date, ~D[2019-01-01]), <<>>) + end + + bit_msg = "collecting into a bitstring requires a bitstring, got: #{inspect(item)}" + bit_map_msg = "collecting into a bitstring requires a bitstring, got: #{inspect(tuple)}" + bit_date_msg = "collecting into a bitstring requires a bitstring, got: #{inspect(date)}" + assert_raise ArgumentError, bit_msg, fn -> Enum.into(Range.new(item, 10, 1), <<1::1>>) end + assert_raise ArgumentError, bit_msg, fn -> Enum.into([item, 2, 3], <<1::1>>) end + + assert_raise ArgumentError, bit_msg, fn -> + Enum.into(MapSet.new([item, 2, 3, 4]), <<1::1>>) + end + + assert_raise ArgumentError, bit_map_msg, fn -> Enum.into(map, <<1::1>>) end + + assert_raise ArgumentError, bit_date_msg, fn -> + Enum.into(Date.range(date, ~D[2019-01-01]), <<1::1>>) + end + end + test "into/3" do assert Enum.into([1, 2, 3], [], fn x -> x * 2 end) == [2, 4, 6] assert Enum.into([1, 2, 3], "numbers: ", &to_string/1) == "numbers: 123" From af67554c448523e8ab5c08e7b39b2b38cdc15c28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 10 Jan 2024 09:43:51 +0100 Subject: [PATCH 0292/1886] Simplify into/2 exception testing --- lib/elixir/test/elixir/enum_test.exs | 53 ++++------------------------ 1 file changed, 7 insertions(+), 46 deletions(-) diff --git a/lib/elixir/test/elixir/enum_test.exs b/lib/elixir/test/elixir/enum_test.exs index ae0d341ed7..11e715a008 100644 --- a/lib/elixir/test/elixir/enum_test.exs +++ b/lib/elixir/test/elixir/enum_test.exs @@ -418,55 +418,16 @@ defmodule EnumTest do end test "into/2 exceptions" do - item = 1 - date = ~D[2015-01-01] - map = %{a: 1} - tuple = {:a, 1} - - map_msg = "collecting into a map requires {key, value} tuples, got: #{inspect(item)}" - map_date_msg = "collecting into a map requires {key, value} tuples, got: #{inspect(date)}" - assert_raise ArgumentError, map_msg, fn -> Enum.into(1..10, %{}) end - - assert_raise ArgumentError, map_date_msg, fn -> - Enum.into(Date.range(date, ~D[2015-01-03]), %{}) - end - - assert_raise ArgumentError, map_msg, fn -> Enum.into(Range.new(item, 10), %{a: 1}) end - - assert_raise ArgumentError, map_msg, fn -> - Enum.into(MapSet.new([item, 2, 3]), %{a: 1}) - end - - assert_raise ArgumentError, map_date_msg, fn -> - Enum.into(Date.range(date, ~D[2019-01-01]), %{a: 1}) - end - - bit_msg = "collecting into a binary requires a bitstring, got: #{inspect(item)}" - bit_map_msg = "collecting into a binary requires a bitstring, got: #{inspect(tuple)}" - bit_date_msg = "collecting into a binary requires a bitstring, got: #{inspect(date)}" - assert_raise ArgumentError, bit_msg, fn -> Enum.into(Range.new(item, 10, 1), <<>>) end - assert_raise ArgumentError, bit_msg, fn -> Enum.into([item, 2, 3], <<>>) end - assert_raise ArgumentError, bit_msg, fn -> Enum.into(MapSet.new([item, 2, 3, 4]), <<>>) end - assert_raise ArgumentError, bit_map_msg, fn -> Enum.into(map, <<>>) end - - assert_raise ArgumentError, bit_date_msg, fn -> - Enum.into(Date.range(date, ~D[2019-01-01]), <<>>) - end - - bit_msg = "collecting into a bitstring requires a bitstring, got: #{inspect(item)}" - bit_map_msg = "collecting into a bitstring requires a bitstring, got: #{inspect(tuple)}" - bit_date_msg = "collecting into a bitstring requires a bitstring, got: #{inspect(date)}" - assert_raise ArgumentError, bit_msg, fn -> Enum.into(Range.new(item, 10, 1), <<1::1>>) end - assert_raise ArgumentError, bit_msg, fn -> Enum.into([item, 2, 3], <<1::1>>) end + assert_raise ArgumentError, + "collecting into a map requires {key, value} tuples, got: 1", + fn -> Enum.into(1..10, %{}) end - assert_raise ArgumentError, bit_msg, fn -> - Enum.into(MapSet.new([item, 2, 3, 4]), <<1::1>>) + assert_raise ArgumentError, "collecting into a binary requires a bitstring, got: 1", fn -> + Enum.into(1..10, <<>>) end - assert_raise ArgumentError, bit_map_msg, fn -> Enum.into(map, <<1::1>>) end - - assert_raise ArgumentError, bit_date_msg, fn -> - Enum.into(Date.range(date, ~D[2019-01-01]), <<1::1>>) + assert_raise ArgumentError, "collecting into a bitstring requires a bitstring, got: 1", fn -> + Enum.into(1..10, <<1::1>>) end end From af2b21d67d4865313807e485e43d4e38a1676a54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 10 Jan 2024 10:16:49 +0100 Subject: [PATCH 0293/1886] Do not crash parallel compiler on external reports, closes #13224 --- lib/elixir/lib/kernel/parallel_compiler.ex | 1 - .../elixir/kernel/parallel_compiler_test.exs | 22 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/lib/elixir/lib/kernel/parallel_compiler.ex b/lib/elixir/lib/kernel/parallel_compiler.ex index 048c27d8e7..5837e8d91d 100644 --- a/lib/elixir/lib/kernel/parallel_compiler.ex +++ b/lib/elixir/lib/kernel/parallel_compiler.ex @@ -455,7 +455,6 @@ defmodule Kernel.ParallelCompiler do # No more queue, nothing waiting, this cycle is done defp spawn_workers([], spawned, waiting, files, result, warnings, errors, state) when map_size(spawned) == 0 and map_size(waiting) == 0 do - [] = errors [] = files cycle_return = each_cycle_return(state.each_cycle.()) state = cycle_timing(result, state) diff --git a/lib/elixir/test/elixir/kernel/parallel_compiler_test.exs b/lib/elixir/test/elixir/kernel/parallel_compiler_test.exs index 9edbe5c0fc..a9d2a83a6a 100644 --- a/lib/elixir/test/elixir/kernel/parallel_compiler_test.exs +++ b/lib/elixir/test/elixir/kernel/parallel_compiler_test.exs @@ -196,6 +196,28 @@ defmodule Kernel.ParallelCompilerTest do purge([QuickExample]) end + test "does not crash on external reports" do + [fixture] = + write_tmp( + "compile_quoted", + quick_example: """ + defmodule CompileQuoted do + try do + Code.compile_quoted({:fn, [], [{:->, [], [[], quote(do: unknown_var)]}]}) + rescue + _ -> :ok + end + end + """ + ) + + assert capture_io(:stderr, fn -> + assert {:ok, [CompileQuoted], []} = Kernel.ParallelCompiler.compile([fixture]) + end) =~ "undefined variable \"unknown_var\"" + after + purge([CompileQuoted]) + end + test "does not hang on missing dependencies" do [fixture] = write_tmp( From bea4a9c11bbeeb264fea4151750cdd88748fb432 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Adri=C3=A1n?= Date: Wed, 10 Jan 2024 10:47:00 -0600 Subject: [PATCH 0294/1886] Fix typo in Access docs (#13244) --- lib/elixir/lib/access.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/lib/access.ex b/lib/elixir/lib/access.ex index a6e4569f09..e9ed1e4799 100644 --- a/lib/elixir/lib/access.ex +++ b/lib/elixir/lib/access.ex @@ -552,7 +552,7 @@ defmodule Access do {"john", %{user: %{name: "JOHN"}}} However, it is not possible to remove fields using the dot notation, - as it is implified those fields must also be present. In any case, + as it is implied those fields must also be present. In any case, `Access.key!/1` is useful when the key is not known in advance and must be accessed dynamically. From 19bdfd044492a525d09bfe6a52589caaf5cee718 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 12 Jan 2024 20:14:27 +0100 Subject: [PATCH 0295/1886] Improve misplaced operator | message --- lib/elixir/src/elixir_expand.erl | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/lib/elixir/src/elixir_expand.erl b/lib/elixir/src/elixir_expand.erl index 50a1bb1935..c3b72933eb 100644 --- a/lib/elixir/src/elixir_expand.erl +++ b/lib/elixir/src/elixir_expand.erl @@ -1204,10 +1204,9 @@ format_error(unhandled_arrow_op) -> "This typically means invalid syntax or a macro is not available in scope"; format_error(unhandled_cons_op) -> "misplaced operator |/2\n\n" - "The | operator is typically used between brackets as the cons operator:\n\n" - " [head | tail]\n\n" - "where head is a sequence of elements separated by commas and the tail " - "is the remaining of a list.\n\n" + "The | operator is typically used between brackets to mark the tail of a list:\n\n" + " [head | tail]\n" + " [head, middle, ... | tail]\n\n" "It is also used to update maps and structs, via the %{map | key: value} notation, " "and in typespecs, such as @type and @spec, to express the union of two types"; format_error(unhandled_type_op) -> From 53f45a93b62842e202458c3bc1bc604e3c154e43 Mon Sep 17 00:00:00 2001 From: Artem Solomatin Date: Sat, 13 Jan 2024 00:38:47 +0300 Subject: [PATCH 0296/1886] Specify guide links for incorrect guards message (#13247) --- lib/elixir/src/elixir_expand.erl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/elixir/src/elixir_expand.erl b/lib/elixir/src/elixir_expand.erl index c3b72933eb..fa237b988b 100644 --- a/lib/elixir/src/elixir_expand.erl +++ b/lib/elixir/src/elixir_expand.erl @@ -1260,12 +1260,12 @@ format_error({invalid_expr_in_scope, Scope, Kind}) -> format_error({invalid_expr_in_guard, Kind}) -> Message = "invalid expression in guards, ~ts is not allowed in guards. To learn more about " - "guards, visit: https://hexdocs.pm/elixir/patterns-and-guards.html", + "guards, visit: https://hexdocs.pm/elixir/patterns-and-guards.html#guards", io_lib:format(Message, [Kind]); format_error({invalid_expr_in_bitsize, Kind}) -> Message = "~ts is not allowed in bitstring size specifier. The size specifier in matches works like guards. " - "To learn more about guards, visit: https://hexdocs.pm/elixir/patterns-and-guards.html", + "To learn more about guards, visit: https://hexdocs.pm/elixir/patterns-and-guards.html#guards", io_lib:format(Message, [Kind]); format_error({invalid_alias, Expr}) -> Message = From bc50d94943d967103ca2ba8a7b58e3734331e323 Mon Sep 17 00:00:00 2001 From: Jean Klingler Date: Sat, 13 Jan 2024 17:58:45 +0900 Subject: [PATCH 0297/1886] Fix Code.Normalizer for keyword operand with :do key (#13250) --- lib/elixir/lib/code/normalizer.ex | 10 +++++----- .../test/elixir/code_normalizer/quoted_ast_test.exs | 4 ++++ 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/lib/elixir/lib/code/normalizer.ex b/lib/elixir/lib/code/normalizer.ex index 9715c940a9..6317eeeafa 100644 --- a/lib/elixir/lib/code/normalizer.ex +++ b/lib/elixir/lib/code/normalizer.ex @@ -353,6 +353,10 @@ defmodule Code.Normalizer do last = List.last(args) cond do + not allow_keyword?(form, arity) -> + args = normalize_args(args, %{state | parent_meta: meta}) + {form, meta, args} + Keyword.has_key?(meta, :do) or match?([{{:__block__, _, [:do]}, _} | _], last) -> # def foo do :ok end # def foo, do: :ok @@ -364,7 +368,7 @@ defmodule Code.Normalizer do meta = meta ++ [do: [line: line], end: [line: line]] normalize_kw_blocks(form, meta, args, state) - allow_keyword?(form, arity) -> + true -> args = normalize_args(args, %{state | parent_meta: meta}) {last_arg, leading_args} = List.pop_at(args, -1, []) @@ -385,10 +389,6 @@ defmodule Code.Normalizer do end {form, meta, leading_args ++ last_args} - - true -> - args = normalize_args(args, %{state | parent_meta: meta}) - {form, meta, args} end end diff --git a/lib/elixir/test/elixir/code_normalizer/quoted_ast_test.exs b/lib/elixir/test/elixir/code_normalizer/quoted_ast_test.exs index 7e0b894971..387e109e48 100644 --- a/lib/elixir/test/elixir/code_normalizer/quoted_ast_test.exs +++ b/lib/elixir/test/elixir/code_normalizer/quoted_ast_test.exs @@ -539,6 +539,10 @@ defmodule Code.Normalizer.QuotedASTTest do "\e[34m[\e[0m\e[32ma:\e[0m \e[33m1\e[0m, \e[32mb:\e[0m \e[33m2\e[0m\e[34m]\e[0m" end + test "keyword list with :do as operand" do + assert quoted_to_string(quote(do: a = [do: 1])) == "a = [do: 1]" + end + test "interpolation" do assert quoted_to_string(quote(do: "foo#{bar}baz")) == ~S["foo#{bar}baz"] end From b7e633efa97c6ca94bbe971f16cd0d8dd8836aae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sat, 13 Jan 2024 11:02:56 +0100 Subject: [PATCH 0298/1886] Improve docs for loadconfig, closes #13246 --- lib/mix/lib/mix/tasks/loadconfig.ex | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/mix/lib/mix/tasks/loadconfig.ex b/lib/mix/lib/mix/tasks/loadconfig.ex index 537213e351..c86bc8c6c3 100644 --- a/lib/mix/lib/mix/tasks/loadconfig.ex +++ b/lib/mix/lib/mix/tasks/loadconfig.ex @@ -9,7 +9,9 @@ defmodule Mix.Tasks.Loadconfig do $ mix loadconfig path/to/config.exs Any configuration file loaded with `loadconfig` is treated - as a compile-time configuration. + as a compile-time configuration. This means all application + keys are merged into their respective applications, however + the values themselves are not deep merged. `config/config.exs` is **always loaded automatically** by the Mix CLI when it boots. `config/runtime.exs` is loaded From 7d7ea09ac3623ab016815c2b6f487c53df8f588f Mon Sep 17 00:00:00 2001 From: Mitchell Hanberg Date: Sat, 13 Jan 2024 11:42:44 -0500 Subject: [PATCH 0299/1886] Fix :from_interpolation docs (#13251) --- lib/elixir/lib/macro.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/lib/macro.ex b/lib/elixir/lib/macro.ex index d8e4f39ef3..a0f344fd42 100644 --- a/lib/elixir/lib/macro.ex +++ b/lib/elixir/lib/macro.ex @@ -115,7 +115,7 @@ defmodule Macro do * `:from_brackets` - Used to determine whether a call to `Access.get/3` is from bracket syntax. - * `:from_interpolation` - Used to determine whether a call to `Access.get/3` is + * `:from_interpolation` - Used to determine whether a call to `Kernel.to_string/1` is from interpolation. * `:generated` - Whether the code should be considered as generated by From 06fb7c03309da83e85fc27cd43c70751ef68f461 Mon Sep 17 00:00:00 2001 From: Roman Date: Mon, 15 Jan 2024 13:42:31 +0100 Subject: [PATCH 0300/1886] Fix the explanation to match the explained code example in docs (#13255) The description incorrectly states that both processes are initialized with 0, while in the code the second process receives a non-default initial value. --- lib/elixir/pages/anti-patterns/process-anti-patterns.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/pages/anti-patterns/process-anti-patterns.md b/lib/elixir/pages/anti-patterns/process-anti-patterns.md index e3258213bf..ad13b623cc 100644 --- a/lib/elixir/pages/anti-patterns/process-anti-patterns.md +++ b/lib/elixir/pages/anti-patterns/process-anti-patterns.md @@ -310,7 +310,7 @@ iex> Counter.bump(Counter, 7) #### Refactoring -To ensure that clients of a library have full control over their systems, regardless of the number of processes used and the lifetime of each one, all processes must be started inside a supervision tree. As shown below, this code uses a `Supervisor` as a supervision tree. When this Elixir application is started, two different counters (`Counter` and `:other_counter`) are also started as child processes of the `Supervisor` named `App.Supervisor`. Both are initialized to `0`. By means of this supervision tree, it is possible to manage the lifecycle of all child processes (stopping or restarting each one), improving the visibility of the entire app. +To ensure that clients of a library have full control over their systems, regardless of the number of processes used and the lifetime of each one, all processes must be started inside a supervision tree. As shown below, this code uses a `Supervisor` as a supervision tree. When this Elixir application is started, two different counters (`Counter` and `:other_counter`) are also started as child processes of the `Supervisor` named `App.Supervisor`. One is initialized with `0`, the other with `15`. By means of this supervision tree, it is possible to manage the lifecycle of all child processes (stopping or restarting each one), improving the visibility of the entire app. ```elixir defmodule SupervisedProcess.Application do From 2fe859ef835caf8f189418275c16ffa8026cc264 Mon Sep 17 00:00:00 2001 From: Roman Date: Tue, 16 Jan 2024 21:15:26 +0100 Subject: [PATCH 0301/1886] Macro anti-patterns: fix incorrect error message in code examples (#13259) --- lib/elixir/pages/anti-patterns/macro-anti-patterns.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/elixir/pages/anti-patterns/macro-anti-patterns.md b/lib/elixir/pages/anti-patterns/macro-anti-patterns.md index 4693e91c44..1ca5362ce3 100644 --- a/lib/elixir/pages/anti-patterns/macro-anti-patterns.md +++ b/lib/elixir/pages/anti-patterns/macro-anti-patterns.md @@ -24,7 +24,7 @@ defmodule Routes do end if not is_atom(handler) do - raise ArgumentError, "route must be a module" + raise ArgumentError, "handler must be a module" end @store_route_for_compilation {route, handler} @@ -51,7 +51,7 @@ defmodule Routes do end if not is_atom(handler) do - raise ArgumentError, "route must be a module" + raise ArgumentError, "handler must be a module" end Module.put_attribute(module, :store_route_for_compilation, {route, handler}) From 93605209dc6b7640456b9efacd6aa4ed1e6e932b Mon Sep 17 00:00:00 2001 From: Wei Huang Date: Wed, 17 Jan 2024 14:24:59 +0100 Subject: [PATCH 0302/1886] Check if inside Mix project folder for recompile (#13261) --- lib/iex/lib/iex/helpers.ex | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/lib/iex/lib/iex/helpers.ex b/lib/iex/lib/iex/helpers.ex index fa49873562..84933f8e83 100644 --- a/lib/iex/lib/iex/helpers.ex +++ b/lib/iex/lib/iex/helpers.ex @@ -94,22 +94,34 @@ defmodule IEx.Helpers do """ def recompile(options \\ []) do if mix_started?() do - config = Mix.Project.config() - consolidation = Mix.Project.consolidation_path(config) - reenable_tasks(config) + project = Mix.Project.get() - force? = Keyword.get(options, :force, false) - args = ["--purge-consolidation-path-if-stale", "--return-errors", consolidation] - args = if force?, do: ["--force" | args], else: args - - {result, _} = Mix.Task.run("compile", args) - result + if is_nil(project) or + project.__info__(:compile)[:source] == String.to_charlist(Path.absname("mix.exs")) do + do_recompile(options) + else + message = "Cannot recompile because the current working directory changed" + IO.puts(IEx.color(:eval_error, message)) + end else IO.puts(IEx.color(:eval_error, "Mix is not running. Please start IEx with: iex -S mix")) :error end end + defp do_recompile(options) do + config = Mix.Project.config() + consolidation = Mix.Project.consolidation_path(config) + reenable_tasks(config) + + force? = Keyword.get(options, :force, false) + args = ["--purge-consolidation-path-if-stale", "--return-errors", consolidation] + args = if force?, do: ["--force" | args], else: args + + {result, _} = Mix.Task.run("compile", args) + result + end + defp mix_started? do List.keyfind(Application.started_applications(), :mix, 0) != nil end From 53b38372fc9bd85cc4d1508b2fee08f48206cfbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sun, 14 Jan 2024 16:22:38 +0100 Subject: [PATCH 0303/1886] Improve ast metadata docs --- lib/elixir/lib/macro.ex | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lib/elixir/lib/macro.ex b/lib/elixir/lib/macro.ex index a0f344fd42..6caf9b7933 100644 --- a/lib/elixir/lib/macro.ex +++ b/lib/elixir/lib/macro.ex @@ -136,8 +136,9 @@ defmodule Macro do * `:closing` - contains metadata about the closing pair, such as a `}` in a tuple or in a map, or such as the closing `)` in a function call - with parens. The `:closing` does not delimit the end of expression if - there are `:do` and `:end` metadata (when `:token_metadata` is true) + with parens (when `:token_metadata` is true). If the function call + has a do-end block attached to it, its metadata is found under the + `:do` and `:end` metadata * `:column` - the column number of the AST node (when `:columns` is true). Note column information is always discarded from quoted code. @@ -154,8 +155,8 @@ defmodule Macro do `do`-`end` blocks (when `:token_metadata` is true) * `:end_of_expression` - denotes when the end of expression effectively - happens. Available for all expressions except the last one inside a - `__block__` (when `:token_metadata` is true) + happens (when `:token_metadata` is true). Available for all expressions + except the last one inside a `__block__` * `:indentation` - indentation of a sigil heredoc From e4a71032d5e87f2146fe871e6616b81fe923d16f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 17 Jan 2024 23:11:40 +0100 Subject: [PATCH 0304/1886] Always log errors at the end --- lib/elixir/lib/kernel/parallel_compiler.ex | 41 ++++++++++++++----- lib/elixir/lib/module/parallel_checker.ex | 3 +- lib/elixir/src/elixir_errors.erl | 17 ++++---- .../elixir/kernel/parallel_compiler_test.exs | 4 +- 4 files changed, 44 insertions(+), 21 deletions(-) diff --git a/lib/elixir/lib/kernel/parallel_compiler.ex b/lib/elixir/lib/kernel/parallel_compiler.ex index 5837e8d91d..e148af1886 100644 --- a/lib/elixir/lib/kernel/parallel_compiler.ex +++ b/lib/elixir/lib/kernel/parallel_compiler.ex @@ -455,6 +455,11 @@ defmodule Kernel.ParallelCompiler do # No more queue, nothing waiting, this cycle is done defp spawn_workers([], spawned, waiting, files, result, warnings, errors, state) when map_size(spawned) == 0 and map_size(waiting) == 0 do + # Print any spurious error that we may have found + Enum.map(errors, fn {diagnostic, read_snippet} -> + :elixir_errors.print_diagnostic(diagnostic, read_snippet) + end) + [] = files cycle_return = each_cycle_return(state.each_cycle.()) state = cycle_timing(result, state) @@ -509,8 +514,9 @@ defmodule Kernel.ParallelCompiler do if deadlocked do spawn_workers(deadlocked, spawned, waiting, files, result, warnings, errors, state) else - deadlock_errors = handle_deadlock(waiting, files) - {return_error(deadlock_errors ++ errors, warnings), state} + return_error(warnings, errors, state, fn -> + handle_deadlock(waiting, files) + end) end end @@ -680,12 +686,13 @@ defmodule Kernel.ParallelCompiler do state = %{state | timer_ref: timer_ref} spawn_workers(queue, spawned, waiting, files, result, warnings, errors, state) - {:diagnostic, %{severity: :warning, file: file} = diagnostic} -> + {:diagnostic, %{severity: :warning, file: file} = diagnostic, read_snippet} -> + :elixir_errors.print_diagnostic(diagnostic, read_snippet) warnings = [%{diagnostic | file: file && Path.absname(file)} | warnings] wait_for_messages(queue, spawned, waiting, files, result, warnings, errors, state) - {:diagnostic, %{severity: :error, file: file} = diagnostic} -> - errors = [%{diagnostic | file: file && Path.absname(file)} | errors] + {:diagnostic, %{severity: :error} = diagnostic, read_snippet} -> + errors = [{diagnostic, read_snippet} | errors] wait_for_messages(queue, spawned, waiting, files, result, warnings, errors, state) {:file_ok, child_pid, ref, file, lexical} -> @@ -705,10 +712,13 @@ defmodule Kernel.ParallelCompiler do spawn_workers(queue, new_spawned, waiting, new_files, result, warnings, errors, state) {:file_error, child_pid, file, {kind, reason, stack}} -> - print_error(file, kind, reason, stack) {_file, _new_spawned, new_files} = discard_file_pid(spawned, files, child_pid) terminate(new_files) - {return_error([to_error(file, kind, reason, stack) | errors], warnings), state} + + return_error(warnings, errors, state, fn -> + print_error(file, kind, reason, stack) + [to_error(file, kind, reason, stack)] + end) {:DOWN, ref, :process, pid, reason} when is_map_key(spawned, ref) -> # async spawned processes have no file, so we always have to delete the ref directly @@ -717,18 +727,27 @@ defmodule Kernel.ParallelCompiler do {file, spawned, files} = discard_file_pid(spawned, files, pid) if file do - print_error(file.file, :exit, reason, []) terminate(files) - {return_error([to_error(file.file, :exit, reason, []) | errors], warnings), state} + + return_error(warnings, errors, state, fn -> + print_error(file.file, :exit, reason, []) + [to_error(file.file, :exit, reason, [])] + end) else wait_for_messages(queue, spawned, waiting, files, result, warnings, errors, state) end end end - defp return_error(errors, warnings) do + defp return_error(warnings, errors, state, fun) do + errors = + Enum.map(errors, fn {%{file: file} = diagnostic, read_snippet} -> + :elixir_errors.print_diagnostic(diagnostic, read_snippet) + %{diagnostic | file: file && Path.absname(file)} + end) + info = %{compile_warnings: Enum.reverse(warnings), runtime_warnings: []} - {:error, Enum.reverse(errors), info} + {{:error, Enum.reverse(errors, fun.()), info}, state} end defp update_result(result, kind, module, value) do diff --git a/lib/elixir/lib/module/parallel_checker.ex b/lib/elixir/lib/module/parallel_checker.ex index e5de710675..6c06323769 100644 --- a/lib/elixir/lib/module/parallel_checker.ex +++ b/lib/elixir/lib/module/parallel_checker.ex @@ -167,7 +167,8 @@ defmodule Module.ParallelChecker do defp collect_results(count, diagnostics) do receive do - {:diagnostic, %{file: file} = diagnostic} -> + {:diagnostic, %{file: file} = diagnostic, read_snippet} -> + :elixir_errors.print_diagnostic(diagnostic, read_snippet) diagnostic = %{diagnostic | file: file && Path.absname(file)} collect_results(count, [diagnostic | diagnostics]) diff --git a/lib/elixir/src/elixir_errors.erl b/lib/elixir/src/elixir_errors.erl index 83542097bf..f1e2cb0c36 100644 --- a/lib/elixir/src/elixir_errors.erl +++ b/lib/elixir/src/elixir_errors.erl @@ -115,14 +115,17 @@ emit_diagnostic(Severity, Position, File, Message, Stacktrace, Options) -> }, case get(elixir_code_diagnostics) of - undefined -> print_diagnostic(Diagnostic, ReadSnippet); - {Tail, true} -> put(elixir_code_diagnostics, {[print_diagnostic(Diagnostic, ReadSnippet) | Tail], true}); - {Tail, false} -> put(elixir_code_diagnostics, {[Diagnostic | Tail], false}) - end, + undefined -> + case get(elixir_compiler_info) of + undefined -> print_diagnostic(Diagnostic, ReadSnippet); + {CompilerPid, _} -> CompilerPid ! {diagnostic, Diagnostic, ReadSnippet} + end; + + {Tail, true} -> + put(elixir_code_diagnostics, {[print_diagnostic(Diagnostic, ReadSnippet) | Tail], true}); - case get(elixir_compiler_info) of - undefined -> ok; - {CompilerPid, _} -> CompilerPid ! {diagnostic, Diagnostic} + {Tail, false} -> + put(elixir_code_diagnostics, {[Diagnostic | Tail], false}) end, ok. diff --git a/lib/elixir/test/elixir/kernel/parallel_compiler_test.exs b/lib/elixir/test/elixir/kernel/parallel_compiler_test.exs index a9d2a83a6a..fa41968160 100644 --- a/lib/elixir/test/elixir/kernel/parallel_compiler_test.exs +++ b/lib/elixir/test/elixir/kernel/parallel_compiler_test.exs @@ -327,7 +327,7 @@ defmodule Kernel.ParallelCompilerTest do msg = capture_io(:stderr, fn -> fixtures = [foo, bar] - assert {:error, [foo_error, bar_error], []} = Kernel.ParallelCompiler.compile(fixtures) + assert {:error, [bar_error, foo_error], []} = Kernel.ParallelCompiler.compile(fixtures) assert bar_error == {bar, nil, "deadlocked waiting on module FooDeadlock"} assert foo_error == {foo, nil, "deadlocked waiting on module BarDeadlock"} end) @@ -415,7 +415,7 @@ defmodule Kernel.ParallelCompilerTest do capture_io(:stderr, fn -> fixtures = [foo, bar] - assert {:error, [foo_error, bar_error], []} = Kernel.ParallelCompiler.compile(fixtures) + assert {:error, [bar_error, foo_error], []} = Kernel.ParallelCompiler.compile(fixtures) assert {^bar, nil, "deadlocked waiting on module FooAsyncDeadlock"} = bar_error assert {^foo, nil, "deadlocked waiting on pmap [#PID<" <> _} = foo_error end) From 949e38d3cec8bbe26d23b761bed7fd36036fb53b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 17 Jan 2024 23:14:14 +0100 Subject: [PATCH 0305/1886] Improve end_of_expression docs --- lib/elixir/lib/macro.ex | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/elixir/lib/macro.ex b/lib/elixir/lib/macro.ex index 6caf9b7933..50e0cd60cd 100644 --- a/lib/elixir/lib/macro.ex +++ b/lib/elixir/lib/macro.ex @@ -155,8 +155,10 @@ defmodule Macro do `do`-`end` blocks (when `:token_metadata` is true) * `:end_of_expression` - denotes when the end of expression effectively - happens (when `:token_metadata` is true). Available for all expressions - except the last one inside a `__block__` + happens (when `:token_metadata` is true). This is only available for + direct children of a `__block__`, and it is either the location of a + newline or of the `;` character. The last expression of `__block__` + does not have this metadata. * `:indentation` - indentation of a sigil heredoc From 55cd5a4a224fd8e28913f251380145242e90cee2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 17 Jan 2024 23:28:21 +0100 Subject: [PATCH 0306/1886] Revert "Do not escape \ in uppercase sigils, closes #8989" This reverts commit 51d23cbba8199936101bda9d57b341105a9efc14 due to a regression. Closes #13262. --- lib/elixir/src/elixir_interpolation.erl | 4 ++-- lib/elixir/test/elixir/code_formatter/literals_test.exs | 4 ++-- lib/elixir/test/elixir/kernel/sigils_test.exs | 1 - 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/elixir/src/elixir_interpolation.erl b/lib/elixir/src/elixir_interpolation.erl index 05986309e9..d4ccf3086c 100644 --- a/lib/elixir/src/elixir_interpolation.erl +++ b/lib/elixir/src/elixir_interpolation.erl @@ -60,8 +60,8 @@ extract([$#, ${ | Rest], Buffer, Output, Line, Column, Scope, true, Last) -> {error, {string, Line, Column, "missing interpolation terminator: \"}\"", []}} end; -extract([$\\ | Rest], Buffer, Output, Line, Column, Scope, true, Last) -> - extract_char(Rest, [$\\ | Buffer], Output, Line, Column + 1, Scope, true, Last); +extract([$\\ | Rest], Buffer, Output, Line, Column, Scope, Interpol, Last) -> + extract_char(Rest, [$\\ | Buffer], Output, Line, Column + 1, Scope, Interpol, Last); %% Catch all clause diff --git a/lib/elixir/test/elixir/code_formatter/literals_test.exs b/lib/elixir/test/elixir/code_formatter/literals_test.exs index 4a6325d3be..ec614b6949 100644 --- a/lib/elixir/test/elixir/code_formatter/literals_test.exs +++ b/lib/elixir/test/elixir/code_formatter/literals_test.exs @@ -85,8 +85,8 @@ defmodule Code.Formatter.LiteralsTest do end test "without escapes" do - assert_same ~s[:foo] - assert_same ~s[:\\\\] + assert_same ~S[:foo] + assert_same ~S[:\\] end test "with escapes" do diff --git a/lib/elixir/test/elixir/kernel/sigils_test.exs b/lib/elixir/test/elixir/kernel/sigils_test.exs index 834db25ed6..f81cb0bc41 100644 --- a/lib/elixir/test/elixir/kernel/sigils_test.exs +++ b/lib/elixir/test/elixir/kernel/sigils_test.exs @@ -29,7 +29,6 @@ defmodule Kernel.SigilsTest do assert ~S(f\no) == "f\\no" assert ~S(foo\)) == "foo)" assert ~S[foo\]] == "foo]" - assert ~S[foo\\]] == "foo\\]" end test "sigil S newline" do From 4b568d2c033983dd80f58bacca0e2eff113224df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 17 Jan 2024 23:39:15 +0100 Subject: [PATCH 0307/1886] Update CHANGELOG --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index af9adf7d46..bcc5c95111 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ ### 1. Enhancements +#### IEx + + * [IEx.Helpers] Warns if `recompile` was called and the current working directory changed + ### 2. Bug fixes ### 3. Soft deprecations (no warnings emitted) From fe649feaf4ce0546884cdeb9b5232848bf5c3925 Mon Sep 17 00:00:00 2001 From: Jean Klingler Date: Thu, 18 Jan 2024 08:06:35 +0900 Subject: [PATCH 0308/1886] Resolve relative paths in exunit filter (#13258) --- lib/ex_unit/lib/ex_unit/filters.ex | 2 +- lib/ex_unit/test/ex_unit/filters_test.exs | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/ex_unit/lib/ex_unit/filters.ex b/lib/ex_unit/lib/ex_unit/filters.ex index 1f51908249..82f509c6c7 100644 --- a/lib/ex_unit/lib/ex_unit/filters.ex +++ b/lib/ex_unit/lib/ex_unit/filters.ex @@ -45,7 +45,7 @@ defmodule ExUnit.Filters do end defp extract_line_numbers(file_path) do - case String.split(file_path, ":") do + case Path.relative_to_cwd(file_path) |> String.split(":") do [path] -> {path, []} diff --git a/lib/ex_unit/test/ex_unit/filters_test.exs b/lib/ex_unit/test/ex_unit/filters_test.exs index 0f0510f647..a11947df37 100644 --- a/lib/ex_unit/test/ex_unit/filters_test.exs +++ b/lib/ex_unit/test/ex_unit/filters_test.exs @@ -196,9 +196,10 @@ defmodule ExUnit.FiltersTest do test "file paths with line numbers" do unix_path = "test/some/path.exs" windows_path = "C:\\some\\path.exs" + unix_path_with_dot = "./test/some/path.exs" - for path <- [unix_path, windows_path] do - fixed_path = path |> Path.split() |> Path.join() + for path <- [unix_path, windows_path, unix_path_with_dot] do + fixed_path = path |> Path.split() |> Path.join() |> Path.relative_to_cwd() assert ExUnit.Filters.parse_path("#{path}:123") == {fixed_path, [exclude: [:test], include: [location: {fixed_path, 123}]]} From 9a2fb136217cb0cf809e677c61ad342073e44bd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 18 Jan 2024 12:25:55 +0100 Subject: [PATCH 0309/1886] Bring next_grapheme_size back We should have a cheaper mechanism for computing grapheme size without instantiating intermediate binaries. --- lib/elixir/lib/string.ex | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/lib/elixir/lib/string.ex b/lib/elixir/lib/string.ex index a547cbd42b..a19bc3fe72 100644 --- a/lib/elixir/lib/string.ex +++ b/lib/elixir/lib/string.ex @@ -2089,12 +2089,27 @@ defmodule String do end end - @doc false - @deprecated "Use String.next_grapheme/1 instead" + @doc """ + Returns the size (in bytes) of the next grapheme. + + The result is a tuple with the next grapheme size in bytes and + the remainder of the string or `nil` in case the string + reached its end. + + ## Examples + + iex> String.next_grapheme_size("olá") + {1, "lá"} + + iex> String.next_grapheme_size("") + nil + + """ @spec next_grapheme_size(t) :: {pos_integer, t} | nil def next_grapheme_size(string) when is_binary(string) do case :unicode_util.gc(string) do [gc] -> {grapheme_byte_size(gc), <<>>} + [gc, rest] -> {grapheme_byte_size(gc), rest} [gc | rest] -> {grapheme_byte_size(gc), rest} [] -> nil {:error, <<_, rest::bits>>} -> {1, rest} From 0da0b2899eea5fd261ccad8f3f205d4b36096112 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 18 Jan 2024 15:47:56 +0100 Subject: [PATCH 0310/1886] Ensure the compiler is notified immediately after async --- lib/elixir/lib/kernel/parallel_compiler.ex | 61 +++++++++++----------- 1 file changed, 31 insertions(+), 30 deletions(-) diff --git a/lib/elixir/lib/kernel/parallel_compiler.ex b/lib/elixir/lib/kernel/parallel_compiler.ex index e148af1886..4a9427e2bc 100644 --- a/lib/elixir/lib/kernel/parallel_compiler.ex +++ b/lib/elixir/lib/kernel/parallel_compiler.ex @@ -18,23 +18,36 @@ defmodule Kernel.ParallelCompiler do # TODO: Deprecate this on Elixir v1.20. @doc deprecated: "Use `pmap/2` instead" def async(fun) when is_function(fun, 0) do + {ref, task} = inner_async(fun) + send(task.pid, ref) + task + end + + defp inner_async(fun) do case :erlang.get(:elixir_compiler_info) do {compiler_pid, file_pid} -> + ref = make_ref() file = :erlang.get(:elixir_compiler_file) dest = :erlang.get(:elixir_compiler_dest) {:error_handler, error_handler} = :erlang.process_info(self(), :error_handler) {_parent, checker} = Module.ParallelChecker.get() - Task.async(fn -> - send(compiler_pid, {:async, self()}) - Module.ParallelChecker.put(compiler_pid, checker) - :erlang.put(:elixir_compiler_info, {compiler_pid, file_pid}) - :erlang.put(:elixir_compiler_file, file) - dest != :undefined and :erlang.put(:elixir_compiler_dest, dest) - :erlang.process_flag(:error_handler, error_handler) - fun.() - end) + task = + Task.async(fn -> + Module.ParallelChecker.put(compiler_pid, checker) + :erlang.put(:elixir_compiler_info, {compiler_pid, file_pid}) + :erlang.put(:elixir_compiler_file, file) + dest != :undefined and :erlang.put(:elixir_compiler_dest, dest) + :erlang.process_flag(:error_handler, error_handler) + + receive do + ^ref -> fun.() + end + end) + + send(compiler_pid, {:async, task.pid}) + {ref, task} :undefined -> raise ArgumentError, @@ -52,41 +65,29 @@ defmodule Kernel.ParallelCompiler do """ @doc since: "1.16.0" def pmap(collection, fun) when is_function(fun, 1) do - parent = self() ref = make_ref() # We spawn a series of tasks for parallel processing. - # The tasks notify themselves to the compiler. - tasks = + # The tasks are waiting until we give the go ahead. + refs_tasks = Enum.map(collection, fn item -> - async(fn -> - send(parent, {ref, self()}) - - receive do - ^ref -> fun.(item) - end - end) + inner_async(fn -> fun.(item) end) end) - # Then the tasks notify us. This is important because if - # we wait before the tasks notify the compiler, we may be - # released as there is nothing else running. - on = - for %{pid: pid} <- tasks do - receive do - {^ref, ^pid} -> pid - end - end - # Notify the compiler we are waiting on the tasks. {compiler_pid, file_pid} = :erlang.get(:elixir_compiler_info) defining = :elixir_module.compiler_modules() + on = Enum.map(refs_tasks, fn {_ref, %{pid: pid}} -> pid end) send(compiler_pid, {:waiting, :pmap, self(), ref, file_pid, on, defining, :raise}) # Now we allow the tasks to run. This step is not strictly # necessary but it makes compilation more deterministic by # only allowing tasks to run once we are waiting. - Enum.each(on, &send(&1, ref)) + tasks = + Enum.map(refs_tasks, fn {ref, task} -> + send(task.pid, ref) + task + end) # Await tasks and notify the compiler they are done. We could # have the tasks report directly to the compiler, which in turn From 1a36b680752a256a4674bdd1efbcf5313f6b814c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 18 Jan 2024 16:16:00 +0100 Subject: [PATCH 0311/1886] Add String.byte_slice/3 --- lib/elixir/lib/string.ex | 156 +++++++++++++++++++++---- lib/elixir/test/elixir/string_test.exs | 41 +++++++ 2 files changed, 177 insertions(+), 20 deletions(-) diff --git a/lib/elixir/lib/string.ex b/lib/elixir/lib/string.ex index a19bc3fe72..a7569e8e1d 100644 --- a/lib/elixir/lib/string.ex +++ b/lib/elixir/lib/string.ex @@ -1907,14 +1907,14 @@ defmodule String do end # Valid ASCII (for better average speed) - defp do_replace_invalid(<> = rest, rep, acc) + defp do_replace_invalid(<> = rest, rep, acc) when ascii in 0..127 and replace_invalid_is_next(next) do - <<_::8, rest::bytes>> = rest + <<_::8, rest::binary>> = rest do_replace_invalid(rest, rep, acc <> <>) end # Valid UTF-8 - defp do_replace_invalid(<>, rep, acc) do + defp do_replace_invalid(<>, rep, acc) do do_replace_invalid(rest, rep, acc <> <>) end @@ -1924,9 +1924,13 @@ defmodule String do acc <> rep end - defp do_replace_invalid(<<0b1110::4, i::4, 0b10::2, ii::6, next::8, _::bytes>> = rest, rep, acc) + defp do_replace_invalid( + <<0b1110::4, i::4, 0b10::2, ii::6, next::8, _::binary>> = rest, + rep, + acc + ) when replace_invalid_ii_of_iii(i, ii) and replace_invalid_is_next(next) do - <<_::16, rest::bytes>> = rest + <<_::16, rest::binary>> = rest do_replace_invalid(rest, rep, acc <> rep) end @@ -1937,12 +1941,12 @@ defmodule String do end defp do_replace_invalid( - <<0b11110::5, i::3, 0b10::2, ii::6, next::8, _::bytes>> = rest, + <<0b11110::5, i::3, 0b10::2, ii::6, next::8, _::binary>> = rest, rep, acc ) when replace_invalid_ii_of_iv(i, ii) and replace_invalid_is_next(next) do - <<_::16, rest::bytes>> = rest + <<_::16, rest::binary>> = rest do_replace_invalid(rest, rep, acc <> rep) end @@ -1953,17 +1957,17 @@ defmodule String do end defp do_replace_invalid( - <<0b11110::5, i::3, 0b10::2, ii::6, 0b10::2, iii::6, next::8, _::bytes>> = rest, + <<0b11110::5, i::3, 0b10::2, ii::6, 0b10::2, iii::6, next::8, _::binary>> = rest, rep, acc ) when replace_invalid_iii_of_iv(i, ii, iii) and replace_invalid_is_next(next) do - <<_::24, rest::bytes>> = rest + <<_::24, rest::binary>> = rest do_replace_invalid(rest, rep, acc <> rep) end # Everything else - defp do_replace_invalid(<<_, rest::bytes>>, rep, acc), + defp do_replace_invalid(<<_, rest::binary>>, rep, acc), do: do_replace_invalid(rest, rep, acc <> rep) # Final @@ -2252,12 +2256,14 @@ defmodule String do @doc """ Returns a substring starting at the offset `start`, and of the given `length`. - If the offset is greater than string length, then it returns `""`. + This function works on Unicode graphemes. For example, slicing the first + three characters of the string "héllo" will return "hél", which internally + is represented by more than three bytes. Use `String.byte_slice/3` if you + want to slice by a given number of bytes, while respecting the codepoint + boundaries. If you want to work on raw bytes, check `Kernel.binary_part/3` + or `Kernel.binary_slice/3` instead. - Remember this function works with Unicode graphemes and considers - the slices to represent grapheme offsets. If you want to split - on raw bytes, check `Kernel.binary_part/3` or `Kernel.binary_slice/3` - instead. + If the offset is greater than string length, then it returns `""`. ## Examples @@ -2317,6 +2323,13 @@ defmodule String do Returns a substring from the offset given by the start of the range to the offset given by the end of the range. + This function works on Unicode graphemes. For example, slicing the first + three characters of the string "héllo" will return "hél", which internally + is represented by more than three bytes. Use `String.byte_slice/3` if you + want to slice by a given number of bytes, while respecting the codepoint + boundaries. If you want to work on raw bytes, check `Kernel.binary_part/3` + or `Kernel.binary_slice/3` instead. + If the start of the range is not a valid offset for the given string or if the range is in reverse order, returns `""`. @@ -2324,11 +2337,6 @@ defmodule String do is traversed first in order to convert the negative indices into positive ones. - Remember this function works with Unicode graphemes and considers - the slices to represent grapheme offsets. If you want to split - on raw bytes, check `Kernel.binary_part/3` or - `Kernel.binary_slice/2` instead - ## Examples iex> String.slice("elixir", 1..3) @@ -2496,6 +2504,114 @@ defmodule String do defp split_bytes([], acc, _), do: {acc, 0} defp split_bytes([head | tail], acc, count), do: split_bytes(tail, head + acc, count - 1) + @doc """ + Returns a substring starting at (or after) `start_bytes` and of at most + the given `size_bytes`. + + This function works on bytes and then adjusts the string to eliminate + truncated codepoints. This is useful when you have a string and you need + to guarantee it does not exceed a certain amount of bytes. + + If the size is greater than the number of bytes in the string, then it + returns `""`. Similar to `String.slice/2`, a negative `start_bytes` + will be adjusted to the end of the string (but in bytes). + + This function does not guarantee the string won't have invalid codepoints, + it only guarantees to remove truncated codepoints immediately at the beginning + or the end of the slice. + + ## Examples + + Consider the string "héllo". Let's see its representation: + + iex> inspect("héllo", binaries: :as_binaries) + "<<104, 195, 169, 108, 108, 111>>" + + Although the string has 5 characters, it is made of 6 bytes. Now imagine + we want to get only the first two bytes. To do so, let's use `binary_slice/3`, + which is unaware of codepoints: + + iex> binary_slice("héllo", 0, 2) + <<104, 195>> + + As you can see, this operation is unsafe and returns an invalid string. + That's because we cut the string in the middle of the bytes representing + "é". On the other hand, we could use `String.slice/3`: + + iex> String.slice("héllo", 0, 2) + "hé" + + While the above is correct, it has 3 bytes. If you have a requirement where + you need *at most* 2 bytes, the result would also be invalid. In such scenarios, + you can use this function, which will slice the given bytes, but clean up + the truncated codepoints: + + iex> String.byte_slice("héllo", 0, 2) + "h" + + Truncated codepoints at the beginning are also cleaned up: + + iex> String.byte_slice("héllo", 2, 3) + "llo" + + Note that, if you have a raw bytes, then you must use `binary_slice/3` + instead. + """ + @doc since: "1.17.0" + @spec byte_slice(t, integer, non_neg_integer) :: grapheme + def byte_slice(string, start_bytes, size_bytes) + when is_binary(string) and is_integer(start_bytes) and is_integer(size_bytes) and + size_bytes >= 0 do + total = byte_size(string) + start_bytes = if start_bytes < 0, do: max(total + start_bytes, 0), else: start_bytes + + if start_bytes < total do + :erlang.binary_part(string, start_bytes, total - start_bytes) + |> invalid_prefix() + |> invalid_suffix(size_bytes) + else + "" + end + end + + defp invalid_prefix(<<0b10::2, _::6, rest::binary>>), do: invalid_prefix(rest) + defp invalid_prefix(rest), do: rest + + defp invalid_suffix(string, size) do + last = invalid_suffix(string, min(size, byte_size(string)) - 1, 0) + :erlang.binary_part(string, 0, last) + end + + defp invalid_suffix(string, last, truncated) when last >= 0 do + byte = :binary.at(string, last) + + cond do + # This byte is valid, discard all truncated entries + byte <= 127 -> + last + 1 + + byte <= 191 -> + invalid_suffix(string, last - 1, truncated + 1) + + # 2 bytes + byte <= 223 -> + if truncated == 1, do: last + truncated + 1, else: last + + # 3 bytes + byte <= 239 -> + if truncated == 2, do: last + truncated + 1, else: last + + # 3 bytes + byte <= 247 -> + if truncated == 3, do: last + truncated + 1, else: last + + true -> + last + end + end + + defp invalid_suffix(_string, _last, _truncated), do: 0 + @doc """ Returns `true` if `string` starts with any of the prefixes given. diff --git a/lib/elixir/test/elixir/string_test.exs b/lib/elixir/test/elixir/string_test.exs index f808c8d602..e96f606260 100644 --- a/lib/elixir/test/elixir/string_test.exs +++ b/lib/elixir/test/elixir/string_test.exs @@ -808,6 +808,47 @@ defmodule StringTest do assert String.slice("a·̀ͯ‿.⁀:", 0..-2//2) == "a‿⁀" end + test "byte_slice/2" do + # ASCII + assert String.byte_slice("elixir", 0, 6) == "elixir" + assert String.byte_slice("elixir", 0, 5) == "elixi" + assert String.byte_slice("elixir", 1, 4) == "lixi" + assert String.byte_slice("elixir", 0, 10) == "elixir" + assert String.byte_slice("elixir", -3, 10) == "xir" + assert String.byte_slice("elixir", -10, 10) == "elixir" + assert String.byte_slice("elixir", 1, 0) == "" + assert String.byte_slice("elixir", 10, 10) == "" + + # 2 byte + assert String.byte_slice("héllò", 1, 4) == "éll" + assert String.byte_slice("héllò", 1, 5) == "éll" + assert String.byte_slice("héllò", 1, 6) == "éllò" + assert String.byte_slice("héllò", 2, 4) == "llò" + + # 3 byte + assert String.byte_slice("hかllか", 1, 4) == "かl" + assert String.byte_slice("hかllか", 1, 5) == "かll" + assert String.byte_slice("hかllか", 1, 6) == "かll" + assert String.byte_slice("hかllか", 1, 7) == "かll" + assert String.byte_slice("hかllか", 1, 8) == "かllか" + assert String.byte_slice("hかllか", 2, 4) == "ll" + assert String.byte_slice("hかllか", 2, 5) == "llか" + + # 4 byte + assert String.byte_slice("h😍ll😍", 1, 4) == "😍" + assert String.byte_slice("h😍ll😍", 1, 5) == "😍l" + assert String.byte_slice("h😍ll😍", 1, 6) == "😍ll" + assert String.byte_slice("h😍ll😍", 1, 7) == "😍ll" + assert String.byte_slice("h😍ll😍", 1, 8) == "😍ll" + assert String.byte_slice("h😍ll😍", 1, 9) == "😍ll" + assert String.byte_slice("h😍ll😍", 1, 10) == "😍ll😍" + assert String.byte_slice("h😍ll😍", 2, 5) == "ll" + assert String.byte_slice("h😍ll😍", 2, 6) == "ll😍" + + # Already invalid + assert String.byte_slice(<<178, "ll", 178>>, 0, 10) == "ll" + end + test "valid?/1" do assert String.valid?("afds") assert String.valid?("øsdfh") From f14bcdcac9df251bee72cfd060a2782ef0e471e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 18 Jan 2024 16:26:40 +0100 Subject: [PATCH 0312/1886] Update docs, distinguish truncated/invalid --- lib/elixir/lib/string.ex | 12 +++++++----- lib/elixir/test/elixir/string_test.exs | 5 ++++- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/lib/elixir/lib/string.ex b/lib/elixir/lib/string.ex index a7569e8e1d..0408540d45 100644 --- a/lib/elixir/lib/string.ex +++ b/lib/elixir/lib/string.ex @@ -2586,27 +2586,29 @@ defmodule String do byte = :binary.at(string, last) cond do - # This byte is valid, discard all truncated entries + # ASCII byte, discard all truncated entries byte <= 127 -> last + 1 + # In the middle of a codepoint byte <= 191 -> invalid_suffix(string, last - 1, truncated + 1) - # 2 bytes + # 2 bytes codepoint start byte <= 223 -> if truncated == 1, do: last + truncated + 1, else: last - # 3 bytes + # 3 bytes codepoint start byte <= 239 -> if truncated == 2, do: last + truncated + 1, else: last - # 3 bytes + # 4 bytes codepoint start byte <= 247 -> if truncated == 3, do: last + truncated + 1, else: last + # Invalid codepoint, discard it, stop checking true -> - last + last + 1 end end diff --git a/lib/elixir/test/elixir/string_test.exs b/lib/elixir/test/elixir/string_test.exs index e96f606260..cedec53f38 100644 --- a/lib/elixir/test/elixir/string_test.exs +++ b/lib/elixir/test/elixir/string_test.exs @@ -845,8 +845,11 @@ defmodule StringTest do assert String.byte_slice("h😍ll😍", 2, 5) == "ll" assert String.byte_slice("h😍ll😍", 2, 6) == "ll😍" - # Already invalid + # Already truncated assert String.byte_slice(<<178, "ll", 178>>, 0, 10) == "ll" + + # Already invalid + assert String.byte_slice(<<255, "ll", 255>>, 0, 10) == <<255, "ll", 255>> end test "valid?/1" do From f415c895ce2090b4c6334d6cd7c9b7fe0cf2c12a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 18 Jan 2024 20:57:37 +0100 Subject: [PATCH 0313/1886] Improve docs for mix test --raise --- lib/mix/lib/mix/tasks/test.ex | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/mix/lib/mix/tasks/test.ex b/lib/mix/lib/mix/tasks/test.ex index d49b9dca28..2074388a36 100644 --- a/lib/mix/lib/mix/tasks/test.ex +++ b/lib/mix/lib/mix/tasks/test.ex @@ -163,9 +163,10 @@ defmodule Mix.Tasks.Test do * `--preload-modules` - preloads all modules defined in applications * `--profile-require time` - profiles the time spent to require test files. - Used only for debugging. The test suite does not run. + Used only for debugging. The test suite does not run - * `--raise` - raises if the test suite failed + * `--raise` - immediately raises if the test suite fails, instead of continuing + the execution of other Mix tasks * `--seed` - seeds the random number generator used to randomize the order of tests; `--seed 0` disables randomization so the tests in a single file will always be ran From d3285b176e87b025a6949867b58a7ce0e3839b58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 19 Jan 2024 19:42:55 +0100 Subject: [PATCH 0314/1886] Fix capitalize for single codepoint (#13268) --- lib/elixir/lib/string.ex | 1 + lib/elixir/test/elixir/string_test.exs | 1 + 2 files changed, 2 insertions(+) diff --git a/lib/elixir/lib/string.ex b/lib/elixir/lib/string.ex index 0408540d45..bde2233f02 100644 --- a/lib/elixir/lib/string.ex +++ b/lib/elixir/lib/string.ex @@ -966,6 +966,7 @@ defmodule String do def capitalize(string, mode) when is_binary(string) do case :unicode_util.gc(string) do + [gc] -> grapheme_to_binary(:string.titlecase([gc])) [gc, rest] -> grapheme_to_binary(:string.titlecase([gc])) <> downcase(rest, mode) [gc | rest] -> grapheme_to_binary(:string.titlecase([gc])) <> downcase(rest, mode) [] -> "" diff --git a/lib/elixir/test/elixir/string_test.exs b/lib/elixir/test/elixir/string_test.exs index cedec53f38..9339aae765 100644 --- a/lib/elixir/test/elixir/string_test.exs +++ b/lib/elixir/test/elixir/string_test.exs @@ -254,6 +254,7 @@ defmodule StringTest do test "capitalize/1" do assert String.capitalize("") == "" + assert String.capitalize("1") == "1" assert String.capitalize("abc") == "Abc" assert String.capitalize("ABC") == "Abc" assert String.capitalize("c b a") == "C b a" From 991baf9e88765a22963086e5691c8096e6eb8622 Mon Sep 17 00:00:00 2001 From: Jean Klingler Date: Sat, 20 Jan 2024 17:36:47 +0900 Subject: [PATCH 0315/1886] Fix typespec and doc for String.byte_slice/3 (#13270) --- lib/elixir/lib/string.ex | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/elixir/lib/string.ex b/lib/elixir/lib/string.ex index bde2233f02..6de7a20b1f 100644 --- a/lib/elixir/lib/string.ex +++ b/lib/elixir/lib/string.ex @@ -2513,7 +2513,7 @@ defmodule String do truncated codepoints. This is useful when you have a string and you need to guarantee it does not exceed a certain amount of bytes. - If the size is greater than the number of bytes in the string, then it + If the offset is greater than the number of bytes in the string, then it returns `""`. Similar to `String.slice/2`, a negative `start_bytes` will be adjusted to the end of the string (but in bytes). @@ -2555,11 +2555,11 @@ defmodule String do iex> String.byte_slice("héllo", 2, 3) "llo" - Note that, if you have a raw bytes, then you must use `binary_slice/3` + Note that, if you want to work on raw bytes, then you must use `binary_slice/3` instead. """ @doc since: "1.17.0" - @spec byte_slice(t, integer, non_neg_integer) :: grapheme + @spec byte_slice(t, integer, non_neg_integer) :: t def byte_slice(string, start_bytes, size_bytes) when is_binary(string) and is_integer(start_bytes) and is_integer(size_bytes) and size_bytes >= 0 do From 97caaa56abbe103970f387e598d4c26c9bba3d5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sun, 21 Jan 2024 09:18:20 +0100 Subject: [PATCH 0316/1886] More docs to debug projects --- lib/elixir/pages/getting-started/debugging.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lib/elixir/pages/getting-started/debugging.md b/lib/elixir/pages/getting-started/debugging.md index 7c32407753..eee215eab3 100644 --- a/lib/elixir/pages/getting-started/debugging.md +++ b/lib/elixir/pages/getting-started/debugging.md @@ -110,6 +110,18 @@ When using `IEx`, you may pass `--dbg pry` as an option to "stop" the code execu $ iex --dbg pry ``` +Or to debug inside a of a project: + +```console +$ iex --dbg pry -S mix +``` + +Or during tests (the `--trace` flag on `mix test` avoid tests from timing out): + +```console +$ iex --dbg pry -S mix test --trace +``` + Now a call to `dbg` will ask if you want to pry the existing code. If you accept, you'll be able to access all variables, as well as imports and aliases from the code, directly from IEx. This is called "prying". While the pry session is running, the code execution stops, until `continue` or `next` are called. Remember you can always run `iex` in the context of a project with `iex -S mix TASK`. From 91de7e13b5cdffe6ef21426d9dac303f98942e14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sun, 21 Jan 2024 11:12:04 +0100 Subject: [PATCH 0317/1886] Add logo to docs --- Makefile | 3 +-- lib/elixir/pages/images/logo.png | Bin 0 -> 7636 bytes 2 files changed, 1 insertion(+), 2 deletions(-) create mode 100644 lib/elixir/pages/images/logo.png diff --git a/Makefile b/Makefile index 7548d62a8a..695f3d43b3 100644 --- a/Makefile +++ b/Makefile @@ -177,9 +177,8 @@ clean_residual_files: #==> Documentation tasks -LOGO_PATH = $(shell test -f ../docs/logo.png && echo "--logo ../docs/logo.png") SOURCE_REF = $(shell tag="$(call GIT_TAG)" revision="$(call GIT_REVISION)"; echo "$${tag:-$$revision}") -DOCS_COMPILE = CANONICAL=$(CANONICAL) bin/elixir ../ex_doc/bin/ex_doc "$(1)" "$(VERSION)" "lib/$(2)/ebin" --main "$(3)" --source-url "https://github.com/elixir-lang/elixir" --source-ref "$(call SOURCE_REF)" $(call LOGO_PATH) --output doc/$(2) --canonical "https://hexdocs.pm/$(2)/$(CANONICAL)" --homepage-url "https://elixir-lang.org/docs.html" $(4) +DOCS_COMPILE = CANONICAL=$(CANONICAL) bin/elixir ../ex_doc/bin/ex_doc "$(1)" "$(VERSION)" "lib/$(2)/ebin" --main "$(3)" --source-url "https://github.com/elixir-lang/elixir" --source-ref "$(call SOURCE_REF)" --logo lib/elixir/pages/images/logo.png --output doc/$(2) --canonical "https://hexdocs.pm/$(2)/$(CANONICAL)" --homepage-url "https://elixir-lang.org/docs.html" $(4) DOCS_CONFIG = bin/elixir lib/elixir/scripts/docs_config.exs "$(1)" docs: compile ../ex_doc/bin/ex_doc docs_elixir docs_eex docs_mix docs_iex docs_ex_unit docs_logger diff --git a/lib/elixir/pages/images/logo.png b/lib/elixir/pages/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..fcb34f09d60ad7cff4f4252073fe3e6c0de26a20 GIT binary patch literal 7636 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 Qy}wWOwNGjlYS>) Date: Sun, 21 Jan 2024 13:35:32 +0100 Subject: [PATCH 0318/1886] Improve wording in iex -S mix test --trace doc (#13272) avoid -> prevents --- lib/elixir/pages/getting-started/debugging.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/pages/getting-started/debugging.md b/lib/elixir/pages/getting-started/debugging.md index eee215eab3..072ff8085c 100644 --- a/lib/elixir/pages/getting-started/debugging.md +++ b/lib/elixir/pages/getting-started/debugging.md @@ -116,7 +116,7 @@ Or to debug inside a of a project: $ iex --dbg pry -S mix ``` -Or during tests (the `--trace` flag on `mix test` avoid tests from timing out): +Or during tests (the `--trace` flag on `mix test` prevents tests from timing out): ```console $ iex --dbg pry -S mix test --trace From 8062fa08e014815814320a8eabe400bb11973aa6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 23 Jan 2024 08:20:41 +0100 Subject: [PATCH 0319/1886] Bump DavidAnson/markdownlint-cli2-action from 14.0.0 to 15.0.0 (#13275) Bumps [DavidAnson/markdownlint-cli2-action](https://github.com/davidanson/markdownlint-cli2-action) from 14.0.0 to 15.0.0. - [Release notes](https://github.com/davidanson/markdownlint-cli2-action/releases) - [Commits](https://github.com/davidanson/markdownlint-cli2-action/compare/v14.0.0...v15.0.0) --- updated-dependencies: - dependency-name: DavidAnson/markdownlint-cli2-action dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci-markdown.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-markdown.yml b/.github/workflows/ci-markdown.yml index c53358c514..8ba7ab31c3 100644 --- a/.github/workflows/ci-markdown.yml +++ b/.github/workflows/ci-markdown.yml @@ -27,7 +27,7 @@ jobs: fetch-depth: 10 - name: Run markdownlint - uses: DavidAnson/markdownlint-cli2-action@v14.0.0 + uses: DavidAnson/markdownlint-cli2-action@v15.0.0 with: globs: | lib/elixir/pages/**/*.md From 35dc65d23ce23be9b3c41054e27e29e0be66a8ba Mon Sep 17 00:00:00 2001 From: Panagiotis Nezis Date: Tue, 23 Jan 2024 21:16:42 +0200 Subject: [PATCH 0320/1886] Improve `Path.expand/1` docs (#13276) --- lib/elixir/lib/path.ex | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/elixir/lib/path.ex b/lib/elixir/lib/path.ex index cba9d68989..e2f8469f80 100644 --- a/lib/elixir/lib/path.ex +++ b/lib/elixir/lib/path.ex @@ -172,11 +172,17 @@ defmodule Path do Converts the path to an absolute one, expanding any `.` and `..` components and a leading `~`. + If a relative path is provided it is expanded relatively to + the current working directory. + ## Examples Path.expand("/foo/bar/../baz") #=> "/foo/baz" + Path.expand("foo/bar/../baz") + #=> "$PWD/foo/baz" + """ @spec expand(t) :: binary def expand(path) do From 4d2a18bb83d59ea5b4441dacd81e98140cc662d8 Mon Sep 17 00:00:00 2001 From: Gonzalo <456459+grzuy@users.noreply.github.com> Date: Tue, 23 Jan 2024 18:14:18 -0300 Subject: [PATCH 0321/1886] docs: fixes README CI badge (#13278) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index de23421e07..260968769a 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ -[![CI](https://github.com/elixir-lang/elixir/workflows/CI/badge.svg?branch=main)](https://github.com/elixir-lang/elixir/actions?query=branch%3Amain+workflow%3ACI) +[![CI](https://github.com/elixir-lang/elixir/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/elixir-lang/elixir/actions/workflows/ci.yml?query=branch%3Amain) Elixir is a dynamic, functional language designed for building scalable and maintainable applications. From 4506074acfcb37d770bcd5cd8595ac7ab07eeeda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 24 Jan 2024 12:28:26 +0100 Subject: [PATCH 0322/1886] Improve docs, closes #13279 --- lib/elixir/lib/uri.ex | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/elixir/lib/uri.ex b/lib/elixir/lib/uri.ex index fe612a6094..0a1858cf96 100644 --- a/lib/elixir/lib/uri.ex +++ b/lib/elixir/lib/uri.ex @@ -365,9 +365,9 @@ defmodule URI do Percent-encodes all characters that require escaping in `string`. By default, this function is meant to escape the whole URI, and - therefore it will escape all characters which are foreign to the - URI specification. Reserved characters (such as `:` and `/`) or - unreserved (such as letters and numbers) are not escaped. + therefore it will only escape characters which are foreign in + all parts of a URI. Reserved characters (such as `:` and `/`) + or unreserved (such as letters and numbers) are not escaped. Because different components of a URI require different escaping rules, this function also accepts a `predicate` function as an optional From 75ac733bb5ac77c934388e04b1527d1d68952d40 Mon Sep 17 00:00:00 2001 From: Gonzalo <456459+grzuy@users.noreply.github.com> Date: Wed, 24 Jan 2024 12:25:10 -0300 Subject: [PATCH 0323/1886] docs: minor wording and typo fixes (#13281) --- lib/elixir/lib/application.ex | 6 +++--- lib/mix/lib/mix.ex | 2 +- lib/mix/lib/mix/state.ex | 2 +- lib/mix/lib/mix/tasks/run.ex | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/elixir/lib/application.ex b/lib/elixir/lib/application.ex index 1ba3e348bd..256bcebc5b 100644 --- a/lib/elixir/lib/application.ex +++ b/lib/elixir/lib/application.ex @@ -258,8 +258,8 @@ defmodule Application do Application.stop(:ex_unit) #=> :ok - Stopping an application without a callback module is defined, but except for - some system tracing, it is in practice a no-op. + Stopping an application without a callback module defined, is in practice a + no-op, except for some system tracing. Stopping an application with a callback module has three steps: @@ -277,7 +277,7 @@ defmodule Application do invoked only after termination of the whole supervision tree. Shutting down a live system cleanly can be done by calling `System.stop/1`. It - will shut down every application in the opposite order they had been started. + will shut down every application in the reverse order they were started. By default, a SIGTERM from the operating system will automatically translate to `System.stop/0`. You can also have more explicit control over operating system diff --git a/lib/mix/lib/mix.ex b/lib/mix/lib/mix.ex index 3afe169049..7716f5af53 100644 --- a/lib/mix/lib/mix.ex +++ b/lib/mix/lib/mix.ex @@ -760,7 +760,7 @@ defmodule Mix do it executes the code. This means that, by the time Elixir tries to expand the `%Decimal{}` struct, the dependency has not been installed yet. - Luckily this has a straightforward solution, which is move the code to + Luckily this has a straightforward solution, which is to move the code inside a module: Mix.install([:decimal]) diff --git a/lib/mix/lib/mix/state.ex b/lib/mix/lib/mix/state.ex index 7c04a4f739..41e6744b08 100644 --- a/lib/mix/lib/mix/state.ex +++ b/lib/mix/lib/mix/state.ex @@ -22,7 +22,7 @@ defmodule Mix.State do GenServer.call(@name, :builtin_apps, @timeout) end - ## ETS state storage (mutable, not cleared ion tests) + ## ETS state storage (mutable, not cleared in tests) def fetch(key) do case :ets.lookup(@name, key) do diff --git a/lib/mix/lib/mix/tasks/run.ex b/lib/mix/lib/mix/tasks/run.ex index 3eab6eb42d..9b7a50523b 100644 --- a/lib/mix/lib/mix/tasks/run.ex +++ b/lib/mix/lib/mix/tasks/run.ex @@ -21,7 +21,7 @@ defmodule Mix.Tasks.Run do $ mix run -e "DbUtils.delete_old_records()" -- arg1 arg2 arg3 In both cases, the command-line arguments for the script or expression - are available in `System.argv/0`. This mirror the command line interface + are available in `System.argv/0`. This mirrors the command line interface in the `elixir` executable. For starting long running systems, one typically passes the `--no-halt` From f7b17e6e2ff91439db360d7a3d01f951e4ea4449 Mon Sep 17 00:00:00 2001 From: Ben Swift Date: Thu, 25 Jan 2024 18:59:33 +1100 Subject: [PATCH 0324/1886] Fix typo in Typespecs reference (#13282) --- lib/elixir/pages/references/typespecs.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/pages/references/typespecs.md b/lib/elixir/pages/references/typespecs.md index 163219d88d..20b91ef438 100644 --- a/lib/elixir/pages/references/typespecs.md +++ b/lib/elixir/pages/references/typespecs.md @@ -191,7 +191,7 @@ It also allows composition with existing types. For example: ```elixir -type option :: {:my_option, String.t()} | GenServer.option() +@type option :: {:my_option, String.t()} | GenServer.option() @spec start_link([option()]) :: GenServer.on_start() def start_link(opts) do From 035ff543657526034d40e02e7e2be3446035f106 Mon Sep 17 00:00:00 2001 From: Guillaume Duboc Date: Thu, 25 Jan 2024 09:43:57 +0100 Subject: [PATCH 0325/1886] Add atom singleton types to descr (#13277) The :atom field now contains a representation for all possible atom types. The representation is a pair `{type, set}` where `set` is a set of atoms. If `type` is `:union` it represents the union of the atoms in `set`. If `:negation` it represents every atom except those in `set`. `boolean()` is defined as the singletons true, false. --- lib/elixir/lib/module/types/descr.ex | 104 ++++++++++++++++-- lib/elixir/lib/module/types/expr.ex | 4 +- lib/elixir/lib/module/types/pattern.ex | 2 +- .../test/elixir/module/types/descr_test.exs | 37 +++++++ .../test/elixir/module/types/expr_test.exs | 6 +- 5 files changed, 137 insertions(+), 16 deletions(-) diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex index 2af3448c95..b403852d6b 100644 --- a/lib/elixir/lib/module/types/descr.ex +++ b/lib/elixir/lib/module/types/descr.ex @@ -18,26 +18,27 @@ defmodule Module.Types.Descr do @bit_port 1 <<< 6 @bit_reference 1 <<< 7 - @bit_atom 1 <<< 8 - @bit_non_empty_list 1 <<< 9 - @bit_map 1 <<< 10 - @bit_tuple 1 <<< 11 - @bit_fun 1 <<< 12 - @bit_top (1 <<< 13) - 1 + @bit_non_empty_list 1 <<< 8 + @bit_map 1 <<< 9 + @bit_tuple 1 <<< 10 + @bit_fun 1 <<< 11 + @bit_top (1 <<< 12) - 1 + + @atom_top {:negation, :sets.new(version: 2)} # Guard helpers - @term %{bitmap: @bit_top} + @term %{bitmap: @bit_top, atom: @atom_top} @none %{} # Type definitions - # TODO: Have an atom for term() def dynamic(), do: :dynamic def term(), do: @term def none(), do: @none - def atom(_atom), do: %{bitmap: @bit_atom} + def atom(as), do: %{atom: atom_new(as)} + def atom(), do: %{atom: @atom_top} def binary(), do: %{bitmap: @bit_binary} def empty_list(), do: %{bitmap: @bit_empty_list} def integer(), do: %{bitmap: @bit_integer} @@ -50,6 +51,9 @@ defmodule Module.Types.Descr do def reference(), do: %{bitmap: @bit_reference} def tuple(), do: %{bitmap: @bit_tuple} + @boolset :sets.from_list([true, false], version: 2) + def boolean(), do: %{atom: {:union, @boolset}} + ## Set operations @doc """ @@ -72,6 +76,7 @@ defmodule Module.Types.Descr do @compile {:inline, union: 3} defp union(:bitmap, v1, v2), do: bitmap_union(v1, v2) + defp union(:atom, v1, v2), do: atom_union(v1, v2) @doc """ Computes the intersection of two descrs. @@ -89,6 +94,7 @@ defmodule Module.Types.Descr do # Returning 0 from the callback is taken as none() for that subtype. @compile {:inline, intersection: 3} defp intersection(:bitmap, v1, v2), do: bitmap_intersection(v1, v2) + defp intersection(:atom, v1, v2), do: atom_intersection(v1, v2) @doc """ Computes the difference between two types. @@ -100,6 +106,12 @@ defmodule Module.Types.Descr do # Returning 0 from the callback is taken as none() for that subtype. @compile {:inline, difference: 3} defp difference(:bitmap, v1, v2), do: bitmap_difference(v1, v2) + defp difference(:atom, v1, v2), do: atom_difference(v1, v2) + + @doc """ + Compute the negation of a type. + """ + def negation(%{} = descr), do: difference(term(), descr) @doc """ Converts a descr to its quoted representation. @@ -113,6 +125,7 @@ defmodule Module.Types.Descr do @compile {:inline, to_quoted: 2} defp to_quoted(:bitmap, val), do: bitmap_to_quoted(val) + defp to_quoted(:atom, val), do: atom_to_quoted(val) @doc """ Converts a descr to its quoted string representation. @@ -191,7 +204,6 @@ defmodule Module.Types.Descr do pid: @bit_pid, port: @bit_port, reference: @bit_reference, - atom: @bit_atom, non_empty_list: @bit_non_empty_list, map: @bit_map, tuple: @bit_tuple, @@ -202,4 +214,76 @@ defmodule Module.Types.Descr do (mask &&& val) !== 0, do: {type, [], []} end + + ## Atoms + + # The atom component of a type consists of pairs `{tag, set}` where `set` is a + # set of atoms. + # If `tag = :union` the pair represents the union of the atoms in `set`. + # Else, if `tag = :negation`, it represents every atom except those in `set`. + # + # Example: + # - `{:union, :sets.from_list([:a, :b])}` represents type `:a or :b` + # - `{:negation, :sets.from_list([:c, :d])}` represents type `atom() \ (:c or :d) + # + # `{:negation, :sets.new()}` is the `atom()` top type, as it is the difference + # of `atom()` with an empty list. + + defp atom_new(as) when is_list(as), do: {:union, :sets.from_list(as, version: 2)} + + defp atom_intersection({tag1, s1}, {tag2, s2}) do + {tag, s} = + case {tag1, tag2} do + {:union, :union} -> {:union, :sets.intersection(s1, s2)} + {:negation, :negation} -> {:negation, :sets.union(s1, s2)} + {:union, :negation} -> {:union, :sets.subtract(s1, s2)} + {:negation, :union} -> {:union, :sets.subtract(s2, s1)} + end + + if :sets.size(s) == 0, do: 0, else: {tag, s} + end + + defp atom_union({:union, s1}, {:union, s2}), do: {:union, :sets.union(s1, s2)} + defp atom_union({:negation, s1}, {:negation, s2}), do: {:negation, :sets.intersection(s1, s2)} + defp atom_union({:union, s1}, {:negation, s2}), do: {:negation, :sets.subtract(s2, s1)} + defp atom_union({:negation, s1}, {:union, s2}), do: {:negation, :sets.subtract(s1, s2)} + + defp atom_difference({tag1, s1}, {tag2, s2}) do + {tag, s} = + case {tag1, tag2} do + {:union, :union} -> {:union, :sets.subtract(s1, s2)} + {:negation, :negation} -> {:union, :sets.subtract(s2, s1)} + {:union, :negation} -> {:union, :sets.intersection(s1, s2)} + {:negation, :union} -> {:negation, :sets.union(s1, s2)} + end + + if :sets.size(s) == 0, do: 0, else: {tag, s} + end + + defp literal(lit), do: {:__block__, [], [lit]} + + defp atom_to_quoted({:union, a}) do + if :sets.is_subset(@boolset, a) do + :sets.subtract(a, @boolset) + |> :sets.to_list() + |> Enum.sort() + |> Enum.reduce({:boolean, [], []}, &{:or, [], [&2, literal(&1)]}) + else + :sets.to_list(a) + |> Enum.sort() + |> Enum.map(&literal/1) + |> Enum.reduce(&{:or, [], [&2, &1]}) + end + |> List.wrap() + end + + defp atom_to_quoted({:negation, a}) do + if :sets.size(a) == 0 do + {:atom, [], []} + else + atom_to_quoted({:union, a}) + |> Kernel.then(&{:and, [], [{:atom, [], []}, {:not, [], &1}]}) + end + |> List.wrap() + end end diff --git a/lib/elixir/lib/module/types/expr.ex b/lib/elixir/lib/module/types/expr.ex index 00cb9ae2d5..6f51d05c61 100644 --- a/lib/elixir/lib/module/types/expr.ex +++ b/lib/elixir/lib/module/types/expr.ex @@ -10,7 +10,7 @@ defmodule Module.Types.Expr do # :atom def of_expr(atom, _stack, context) when is_atom(atom) do - {:ok, atom(atom), context} + {:ok, atom([atom]), context} end # 12 @@ -140,7 +140,7 @@ defmodule Module.Types.Expr do # () def of_expr({:__block__, _meta, []}, _stack, context) do - {:ok, atom(nil), context} + {:ok, atom([nil]), context} end # (expr; expr) diff --git a/lib/elixir/lib/module/types/pattern.ex b/lib/elixir/lib/module/types/pattern.ex index 3fbb2c72cf..eef050aac8 100644 --- a/lib/elixir/lib/module/types/pattern.ex +++ b/lib/elixir/lib/module/types/pattern.ex @@ -172,7 +172,7 @@ defmodule Module.Types.Pattern do # :atom defp of_shared(atom, _expected_expr, _stack, context, _fun) when is_atom(atom) do - {:ok, atom(atom), context} + {:ok, atom([atom]), context} end # 12 diff --git a/lib/elixir/test/elixir/module/types/descr_test.exs b/lib/elixir/test/elixir/module/types/descr_test.exs index 8ca016e7ab..4155b7f89f 100644 --- a/lib/elixir/test/elixir/module/types/descr_test.exs +++ b/lib/elixir/test/elixir/module/types/descr_test.exs @@ -19,6 +19,13 @@ defmodule Module.Types.DescrTest do assert union(none(), float()) == float() assert union(none(), binary()) == binary() end + + test "atom" do + assert union(atom(), atom([:a])) == atom() + assert union(atom([:a]), atom([:b])) == atom([:a, :b]) + assert union(atom([:a]), negation(atom([:b]))) == negation(atom([:b])) + assert union(negation(atom([:a, :b])), negation(atom([:b, :c]))) == negation(atom([:b])) + end end describe "intersection" do @@ -36,6 +43,12 @@ defmodule Module.Types.DescrTest do assert intersection(none(), float()) == none() assert intersection(none(), binary()) == none() end + + test "atom" do + assert intersection(atom(), atom([:a])) == atom([:a]) + assert intersection(atom([:a]), atom([:b])) == none() + assert intersection(atom([:a]), negation(atom([:b]))) == atom([:a]) + end end describe "difference" do @@ -57,6 +70,11 @@ defmodule Module.Types.DescrTest do assert difference(integer(), none()) == integer() assert difference(float(), none()) == float() end + + test "atom" do + assert difference(atom([:a]), atom()) == none() + assert difference(atom([:a]), atom([:b])) == atom([:a]) + end end describe "to_quoted" do @@ -68,5 +86,24 @@ defmodule Module.Types.DescrTest do test "none" do assert none() |> to_quoted_string() == "none()" end + + test "negation" do + assert negation(negation(integer())) |> to_quoted_string() == "integer()" + assert negation(negation(atom([:foo, :bar]))) |> to_quoted_string() == ":bar or :foo" + end + + test "atom" do + assert atom() |> to_quoted_string() == "atom()" + assert atom([:a]) |> to_quoted_string() == ":a" + assert atom([:a, :b]) |> to_quoted_string() == ":a or :b" + assert difference(atom(), atom([:a])) |> to_quoted_string() == "atom() and not :a" + end + + test "boolean" do + assert boolean() |> to_quoted_string() == "boolean()" + assert atom([true, false, :a]) |> to_quoted_string() == "boolean() or :a" + assert atom([true, :a]) |> to_quoted_string() == ":a or true" + assert difference(atom(), boolean()) |> to_quoted_string() == "atom() and not boolean()" + end end end diff --git a/lib/elixir/test/elixir/module/types/expr_test.exs b/lib/elixir/test/elixir/module/types/expr_test.exs index 52a6c5e829..dde4dd4bf4 100644 --- a/lib/elixir/test/elixir/module/types/expr_test.exs +++ b/lib/elixir/test/elixir/module/types/expr_test.exs @@ -7,9 +7,9 @@ defmodule Module.Types.ExprTest do import Module.Types.Descr test "literal" do - assert typecheck!(true) == atom(true) - assert typecheck!(false) == atom(false) - assert typecheck!(:foo) == atom(:foo) + assert typecheck!(true) == atom([true]) + assert typecheck!(false) == atom([false]) + assert typecheck!(:foo) == atom([:foo]) assert typecheck!(0) == integer() assert typecheck!(0.0) == float() assert typecheck!("foo") == binary() From 2d50a3612b07376bf4c16c8f4451adb47348f754 Mon Sep 17 00:00:00 2001 From: Jean Klingler Date: Fri, 26 Jan 2024 21:27:03 +0900 Subject: [PATCH 0326/1886] Document the behavior of async_stream_nolink (#13285) * Document the behavior of async_stream_nolink * Apply suggestions from code review Co-authored-by: Andrea Leopardi * Fix format --------- Co-authored-by: Andrea Leopardi --- lib/elixir/lib/task/supervisor.ex | 34 ++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/lib/elixir/lib/task/supervisor.ex b/lib/elixir/lib/task/supervisor.ex index 7dacc2d6ff..6e20449af4 100644 --- a/lib/elixir/lib/task/supervisor.ex +++ b/lib/elixir/lib/task/supervisor.ex @@ -404,7 +404,7 @@ defmodule Task.Supervisor do build_stream(supervisor, :nolink, enumerable, {module, function, args}, options) end - @doc """ + @doc ~S""" Returns a stream that runs the given `function` concurrently on each element in `enumerable`. @@ -414,6 +414,38 @@ defmodule Task.Supervisor do to `async_nolink/3`. See `async_stream/6` for discussion and examples. + + ## Error handling and cleanup + + Even if tasks are not linked to the caller, there is no risk of leaving dangling tasks + running after the stream halts. + + Consider the following example: + + Task.Supervisor.async_stream_nolink(MySupervisor, collection, fun, on_timeout: :kill_task, ordered: false) + |> Enum.each(fn + {:ok, _} -> :ok + {:exit, reason} -> raise "Task exited: #{Exception.format_exit(reason)}" + end) + + If one task raises or times out: + + 1. the second clause gets called + 2. an exception is raised + 3. the stream halts + 4. all ongoing tasks will be shut down + + Here is another example: + + Task.Supervisor.async_stream_nolink(MySupervisor, collection, fun, on_timeout: :kill_task, ordered: false) + |> Stream.filter(&match?({:ok, _}, &1)) + |> Enum.take(3) + + This will return the three first tasks to succeed, ignoring timeouts and errors, and shut down + every ongoing task. + + Just running the stream with `Stream.run/1` on the other hand would ignore errors and process the whole stream. + """ @doc since: "1.4.0" @spec async_stream_nolink(Supervisor.supervisor(), Enumerable.t(), (term -> term), keyword) :: From da4107bd340a9262143dbbaa91216fef273b7708 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Samson?= Date: Fri, 26 Jan 2024 14:02:38 +0100 Subject: [PATCH 0327/1886] Document new parser exception (#13287) --- lib/elixir/lib/code.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/elixir/lib/code.ex b/lib/elixir/lib/code.ex index 0f0d0b8fe0..cdfc9ac220 100644 --- a/lib/elixir/lib/code.ex +++ b/lib/elixir/lib/code.ex @@ -1214,6 +1214,7 @@ defmodule Code do It returns the AST if it succeeds, raises an exception otherwise. The exception is a `TokenMissingError` in case a token is missing (usually because the expression is incomplete), + `MismatchedDelimiterError` (in case of mismatched opening and closing delimiters) and `SyntaxError` otherwise. Check `string_to_quoted/2` for options information. From 0d6c2a2ab52724b6e607127ebe881bd7cfb0d952 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Samson?= Date: Fri, 26 Jan 2024 14:57:56 +0100 Subject: [PATCH 0328/1886] Fix `CaseClauseError` in `Code.fetch_docs` (#13286) --- lib/elixir/lib/code.ex | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/elixir/lib/code.ex b/lib/elixir/lib/code.ex index cdfc9ac220..cdaa27ed22 100644 --- a/lib/elixir/lib/code.ex +++ b/lib/elixir/lib/code.ex @@ -2029,7 +2029,11 @@ defmodule Code do @spec fetch_docs(module | String.t()) :: {:docs_v1, annotation, beam_language, format, module_doc :: doc_content, metadata, docs :: [doc_element]} - | {:error, :module_not_found | :chunk_not_found | {:invalid_chunk, binary}} + | {:error, + :module_not_found + | :chunk_not_found + | {:invalid_chunk, binary} + | :invalid_beam} when annotation: :erl_anno.anno(), beam_language: :elixir | :erlang | atom(), doc_content: %{optional(binary) => binary} | :none | :hidden, @@ -2100,6 +2104,9 @@ defmodule Code do {:error, :beam_lib, {:file_error, _, :enoent}} -> {:error, :module_not_found} + + {:error, :beam_lib, _} -> + {:error, :invalid_beam} end end From 0e4247c724ce43922ce25c1754d28e5afee2b7e0 Mon Sep 17 00:00:00 2001 From: Gonzalo <456459+grzuy@users.noreply.github.com> Date: Fri, 26 Jan 2024 18:44:48 -0300 Subject: [PATCH 0329/1886] Use correct path on error messages with custom MIX_EXS (#13288) --- lib/mix/lib/mix/dep/loader.ex | 2 +- lib/mix/test/mix/tasks/deps_test.exs | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/mix/lib/mix/dep/loader.ex b/lib/mix/lib/mix/dep/loader.ex index 5e988d8c07..d2c6624b9e 100644 --- a/lib/mix/lib/mix/dep/loader.ex +++ b/lib/mix/lib/mix/dep/loader.ex @@ -358,7 +358,7 @@ defmodule Mix.Dep.Loader do end defp mix_children(config, locked?, opts) do - from = Path.absname("mix.exs") + from = Mix.Project.project_file() (config[:deps] || []) |> Enum.map(&to_dep(&1, from, _manager = nil, locked?)) diff --git a/lib/mix/test/mix/tasks/deps_test.exs b/lib/mix/test/mix/tasks/deps_test.exs index 200a978d9e..768f8d3441 100644 --- a/lib/mix/test/mix/tasks/deps_test.exs +++ b/lib/mix/test/mix/tasks/deps_test.exs @@ -537,7 +537,7 @@ defmodule Mix.Tasks.DepsTest do test "fails on diverged dependencies on get/update" do in_fixture("deps_status", fn -> - Mix.Project.push(ConflictDepsApp) + Mix.Project.push(ConflictDepsApp, "mix.exs") assert_raise Mix.Error, fn -> Mix.Tasks.Deps.Loadpaths.run([]) @@ -608,11 +608,11 @@ defmodule Mix.Tasks.DepsTest do end) end - @overriding_msg " the dependency git_repo in mix.exs is overriding" + @overriding_msg " the dependency git_repo in custom/deps_repo/mix.exs is overriding" test "fails on diverged dependencies even when optional" do in_fixture("deps_status", fn -> - Mix.Project.push(ConvergedDepsApp) + Mix.Project.push(ConvergedDepsApp, "custom/deps_repo/mix.exs") File.write!("custom/deps_repo/mix.exs", """ defmodule DepsRepo do @@ -700,15 +700,15 @@ defmodule Mix.Tasks.DepsTest do test "converged dependencies errors if not overriding" do in_fixture("deps_status", fn -> - Mix.Project.push(NonOverriddenDepsApp) + Mix.Project.push(NonOverriddenDepsApp, "custom_mix.exs") assert_raise Mix.Error, fn -> Mix.Tasks.Deps.Loadpaths.run([]) end receive do - {:mix_shell, :error, [" the dependency git_repo in mix.exs" <> _ = msg]} -> - assert msg =~ "In mix.exs:" + {:mix_shell, :error, [" the dependency git_repo in custom_mix.exs" <> _ = msg]} -> + assert msg =~ "In custom_mix.exs:" assert msg =~ "{:git_repo, \"0.1.0\", [env: :prod, git: #{inspect(fixture_path("git_repo"))}]}" From a75371ad18a75c6809b03880c076ef2f47a7ea0b Mon Sep 17 00:00:00 2001 From: Jean Klingler Date: Sat, 27 Jan 2024 18:00:05 +0900 Subject: [PATCH 0330/1886] Make c/e/fprof clickable links in doc (#13290) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Make c/e/fprof clickable links in doc * Update lib/elixir/pages/getting-started/debugging.md Co-authored-by: José Valim * Fix links --------- Co-authored-by: José Valim --- lib/elixir/pages/getting-started/debugging.md | 2 +- lib/mix/lib/mix/tasks/profile.cprof.ex | 2 +- lib/mix/lib/mix/tasks/profile.eprof.ex | 4 ++-- lib/mix/lib/mix/tasks/profile.fprof.ex | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/elixir/pages/getting-started/debugging.md b/lib/elixir/pages/getting-started/debugging.md index 072ff8085c..132bb457c8 100644 --- a/lib/elixir/pages/getting-started/debugging.md +++ b/lib/elixir/pages/getting-started/debugging.md @@ -174,7 +174,7 @@ We have just scratched the surface of what the Erlang VM has to offer, for examp * [Microstate accounting](http://www.erlang.org/doc/man/msacc.html) measures how much time the runtime spends in several low-level tasks in a short time interval - * Mix ships with many tasks under the `profile` namespace, such as `cprof` and `fprof` + * Mix ships with many tasks under the `profile` namespace, such as `mix profile.cprof` and `mix profile.fprof` * For more advanced use cases, we recommend the excellent [Erlang in Anger](https://www.erlang-in-anger.com/), which is available as a free ebook diff --git a/lib/mix/lib/mix/tasks/profile.cprof.ex b/lib/mix/lib/mix/tasks/profile.cprof.ex index 0e811565d0..e1353315ba 100644 --- a/lib/mix/lib/mix/tasks/profile.cprof.ex +++ b/lib/mix/lib/mix/tasks/profile.cprof.ex @@ -6,7 +6,7 @@ defmodule Mix.Tasks.Profile.Cprof do @moduledoc """ Profiles the given file or expression using Erlang's `cprof` tool. - `cprof` can be useful when you want to discover the bottlenecks related + [`:cprof`](`:cprof`) can be useful when you want to discover the bottlenecks related to function calls. Before running the code, it invokes the `app.start` task which compiles diff --git a/lib/mix/lib/mix/tasks/profile.eprof.ex b/lib/mix/lib/mix/tasks/profile.eprof.ex index f9ffdf9dca..14ff7413f7 100644 --- a/lib/mix/lib/mix/tasks/profile.eprof.ex +++ b/lib/mix/lib/mix/tasks/profile.eprof.ex @@ -6,7 +6,7 @@ defmodule Mix.Tasks.Profile.Eprof do @moduledoc """ Profiles the given file or expression using Erlang's `eprof` tool. - `:eprof` provides time information of each function call and can be useful + [`:eprof`](`:eprof`) provides time information of each function call and can be useful when you want to discover the bottlenecks related to this. Before running the code, it invokes the `app.start` task which compiles @@ -94,7 +94,7 @@ defmodule Mix.Tasks.Profile.Eprof do some performance impact on the execution, but the impact is considerably lower than `Mix.Tasks.Profile.Fprof`. If you have a large system try to profile a limited scenario or focus on the main modules or processes. Another alternative is to use - `Mix.Tasks.Profile.Cprof` that uses `:cprof` and has a low performance degradation effect. + `Mix.Tasks.Profile.Cprof` that uses [`:cprof`](`:cprof`) and has a low performance degradation effect. """ @switches [ diff --git a/lib/mix/lib/mix/tasks/profile.fprof.ex b/lib/mix/lib/mix/tasks/profile.fprof.ex index f2431bdc44..a016d9de80 100644 --- a/lib/mix/lib/mix/tasks/profile.fprof.ex +++ b/lib/mix/lib/mix/tasks/profile.fprof.ex @@ -6,7 +6,7 @@ defmodule Mix.Tasks.Profile.Fprof do @moduledoc """ Profiles the given file or expression using Erlang's `fprof` tool. - `fprof` can be useful when you want to discover the bottlenecks of a + [`:fprof`](`:fprof`) can be useful when you want to discover the bottlenecks of a sequential code. Before running the code, it invokes the `app.start` task which compiles From 5c45ea6fed9fab8de769004814439b97869f1f19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sat, 27 Jan 2024 11:45:59 +0100 Subject: [PATCH 0331/1886] Fix docs link Related to #13284. --- Makefile | 1 - lib/elixir/lib/enum.ex | 2 +- lib/elixir/pages/anti-patterns/design-anti-patterns.md | 2 +- lib/elixir/pages/getting-started/basic-types.md | 2 +- lib/elixir/pages/getting-started/enumerable-and-streams.md | 2 +- lib/elixir/pages/meta-programming/macros.md | 2 +- lib/elixir/pages/mix-and-otp/dynamic-supervisor.md | 2 +- lib/elixir/pages/references/operators.md | 2 +- lib/elixir/pages/references/patterns-and-guards.md | 2 +- lib/elixir/pages/references/typespecs.md | 2 +- lib/elixir/pages/references/unicode-syntax.md | 2 +- lib/elixir/scripts/elixir_docs.exs | 1 + 12 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Makefile b/Makefile index 695f3d43b3..da0caa1ec9 100644 --- a/Makefile +++ b/Makefile @@ -188,7 +188,6 @@ docs_elixir: compile ../ex_doc/bin/ex_doc $(Q) rm -rf doc/elixir $(call DOCS_COMPILE,Elixir,elixir,Kernel,--config "lib/elixir/scripts/elixir_docs.exs") $(call DOCS_CONFIG,elixir) - cp -R lib/elixir/pages/images doc/elixir docs_eex: compile ../ex_doc/bin/ex_doc @ echo "==> ex_doc (eex)" diff --git a/lib/elixir/lib/enum.ex b/lib/elixir/lib/enum.ex index c5f6df4599..2ce7c7a465 100644 --- a/lib/elixir/lib/enum.ex +++ b/lib/elixir/lib/enum.ex @@ -261,7 +261,7 @@ defmodule Enum do traversed as if it was an enumerable. For a general overview of all functions in the `Enum` module, see - [the `Enum` cheatsheet](enum-cheat.html). + [the `Enum` cheatsheet](enum-cheat.cheatmd). The functions in this module work in linear time. This means that, the time it takes to perform an operation grows at the same rate as the length diff --git a/lib/elixir/pages/anti-patterns/design-anti-patterns.md b/lib/elixir/pages/anti-patterns/design-anti-patterns.md index f4c2457a33..068f2d2f85 100644 --- a/lib/elixir/pages/anti-patterns/design-anti-patterns.md +++ b/lib/elixir/pages/anti-patterns/design-anti-patterns.md @@ -167,7 +167,7 @@ defmodule MyModule do end ``` -This is only possible because the `File` module provides APIs for reading files with tuples as results (`File.read/1`), as well as a version that raises an exception (`File.read!/1`). The bang (exclamation point) is effectively part of [Elixir's naming conventions](naming-conventions.html#trailing-bang-foo). +This is only possible because the `File` module provides APIs for reading files with tuples as results (`File.read/1`), as well as a version that raises an exception (`File.read!/1`). The bang (exclamation point) is effectively part of [Elixir's naming conventions](naming-conventions.md#trailing-bang-foo). Library authors are encouraged to follow the same practices. In practice, the bang variant is implemented on top of the non-raising version of the code. For example, `File.read!/1` is implemented as: diff --git a/lib/elixir/pages/getting-started/basic-types.md b/lib/elixir/pages/getting-started/basic-types.md index 6b26fba71c..525d9bb129 100644 --- a/lib/elixir/pages/getting-started/basic-types.md +++ b/lib/elixir/pages/getting-started/basic-types.md @@ -325,6 +325,6 @@ iex> 1 === 1.0 false ``` -The comparison operators in Elixir can compare across any data type. We say these operators perform _structural comparison_. For more information, you can read our documentation on [Structural vs Semantic comparisons](Kernel.html#module-structural-comparison). +The comparison operators in Elixir can compare across any data type. We say these operators perform _structural comparison_. For more information, you can read our documentation on [Structural vs Semantic comparisons](`Kernel#module-structural-comparison`). Elixir also provides data-types for expressing collections, such as lists and tuples, which we learn next. When we talk about concurrency and fault-tolerance via processes, we will also discuss ports, pids, and references, but that will come on later chapters. Let's move forward. diff --git a/lib/elixir/pages/getting-started/enumerable-and-streams.md b/lib/elixir/pages/getting-started/enumerable-and-streams.md index bc4f81516f..07526b1629 100644 --- a/lib/elixir/pages/getting-started/enumerable-and-streams.md +++ b/lib/elixir/pages/getting-started/enumerable-and-streams.md @@ -13,7 +13,7 @@ iex> Enum.map(%{1 => 2, 3 => 4}, fn {k, v} -> k * v end) [2, 12] ``` -The `Enum` module provides a huge range of functions to transform, sort, group, filter and retrieve items from enumerables. It is one of the modules developers use frequently in their Elixir code. For a general overview of all functions in the `Enum` module, see [the `Enum` cheatsheet](enum-cheat.html). +The `Enum` module provides a huge range of functions to transform, sort, group, filter and retrieve items from enumerables. It is one of the modules developers use frequently in their Elixir code. For a general overview of all functions in the `Enum` module, see [the `Enum` cheatsheet](enum-cheat.cheatmd). Elixir also provides ranges (see `Range`), which are also enumerable: diff --git a/lib/elixir/pages/meta-programming/macros.md b/lib/elixir/pages/meta-programming/macros.md index ceed3c2153..1f04a344c8 100644 --- a/lib/elixir/pages/meta-programming/macros.md +++ b/lib/elixir/pages/meta-programming/macros.md @@ -26,7 +26,7 @@ defmodule Unless do end ``` -The function receives the arguments and passes them to `if/2`. However, as we learned in the [previous guide](quote-and-unquote.html), the macro will receive quoted expressions, inject them into the quote, and finally return another quoted expression. +The function receives the arguments and passes them to `if/2`. However, as we learned in the [previous guide](quote-and-unquote.md), the macro will receive quoted expressions, inject them into the quote, and finally return another quoted expression. Let's start `iex` with the module above: diff --git a/lib/elixir/pages/mix-and-otp/dynamic-supervisor.md b/lib/elixir/pages/mix-and-otp/dynamic-supervisor.md index e353133372..da3789c7d5 100644 --- a/lib/elixir/pages/mix-and-otp/dynamic-supervisor.md +++ b/lib/elixir/pages/mix-and-otp/dynamic-supervisor.md @@ -183,7 +183,7 @@ A GUI should pop up containing all sorts of information about our system, from g In the Applications tab, you will see all applications currently running in your system alongside their supervision tree. You can select the `kv` application to explore it further: -Observer GUI screenshot +Observer GUI screenshot Not only that, as you create new buckets on the terminal, you should see new processes spawned in the supervision tree shown in Observer: diff --git a/lib/elixir/pages/references/operators.md b/lib/elixir/pages/references/operators.md index 3cf97835bd..1cb7d6c7f8 100644 --- a/lib/elixir/pages/references/operators.md +++ b/lib/elixir/pages/references/operators.md @@ -64,7 +64,7 @@ Finally, these operators appear in the precedence table above but are only meani * `=>` - see [`%{}`](`%{}/1`) * `when` - see [Guards](patterns-and-guards.md#guards) * `<-` - see [`for`](`for/1`) and [`with`](`with/1`) - * `\\` - see [Default arguments](Kernel.html#def/2-default-arguments) + * `\\` - see [Default arguments](`Kernel#def/2-default-arguments`) ## Comparison operators diff --git a/lib/elixir/pages/references/patterns-and-guards.md b/lib/elixir/pages/references/patterns-and-guards.md index 3be4d1de09..909d291544 100644 --- a/lib/elixir/pages/references/patterns-and-guards.md +++ b/lib/elixir/pages/references/patterns-and-guards.md @@ -274,7 +274,7 @@ Not all expressions are allowed in guard clauses, but only a handful of them. Th ### List of allowed functions and operators -You can find the built-in list of guards [in the `Kernel` module](Kernel.html#guards). Here is an overview: +You can find the built-in list of guards [in the `Kernel` module](`Kernel#guards`). Here is an overview: * comparison operators ([`==`](`==/2`), [`!=`](`!=/2`), [`===`](`===/2`), [`!==`](`!==/2`), [`<`](``](`>/2`), [`>=`](`>=/2`)) diff --git a/lib/elixir/pages/references/typespecs.md b/lib/elixir/pages/references/typespecs.md index 20b91ef438..8875469d4f 100644 --- a/lib/elixir/pages/references/typespecs.md +++ b/lib/elixir/pages/references/typespecs.md @@ -321,7 +321,7 @@ end ``` This code generates a warning letting you know that you are mistakenly implementing `parse/0` instead of `parse/1`. -You can read more about `@impl` in the [module documentation](Module.html#module-impl). +You can read more about `@impl` in the [module documentation](`Module#module-impl`). ### Using behaviours diff --git a/lib/elixir/pages/references/unicode-syntax.md b/lib/elixir/pages/references/unicode-syntax.md index 40dbfdc677..8a29f80f01 100644 --- a/lib/elixir/pages/references/unicode-syntax.md +++ b/lib/elixir/pages/references/unicode-syntax.md @@ -81,7 +81,7 @@ Unicode atoms in Elixir follow the identifier rule above with the following modi * `` additionally includes the code point `_` (005F) * `` additionally includes the code point `@` (0040) -Note atoms can also be quoted, which allows any characters, such as `:"hello elixir"`. All Elixir operators are also valid atoms, such as `:+`, `:@`, `:|>`, and others. The full description of valid atoms is available in the ["Atoms" section in the syntax reference](syntax-reference.html#atoms). +Note atoms can also be quoted, which allows any characters, such as `:"hello elixir"`. All Elixir operators are also valid atoms, such as `:+`, `:@`, `:|>`, and others. The full description of valid atoms is available in the ["Atoms" section in the syntax reference](syntax-reference.md#atoms). #### Variables, local calls, and remote calls diff --git a/lib/elixir/scripts/elixir_docs.exs b/lib/elixir/scripts/elixir_docs.exs index 1f7303d91e..fd20e730c9 100644 --- a/lib/elixir/scripts/elixir_docs.exs +++ b/lib/elixir/scripts/elixir_docs.exs @@ -2,6 +2,7 @@ canonical = System.fetch_env!("CANONICAL") [ + assets: "lib/elixir/pages/images", extras: [ "lib/elixir/pages/getting-started/introduction.md", "lib/elixir/pages/getting-started/basic-types.md", From 810140b86b0fefeb6221f988002cec215bb16f2a Mon Sep 17 00:00:00 2001 From: Steve Johns Date: Sun, 28 Jan 2024 17:07:38 +0000 Subject: [PATCH 0332/1886] Fix string interpolation example in basic-types.md (#13292) --- lib/elixir/pages/getting-started/basic-types.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/pages/getting-started/basic-types.md b/lib/elixir/pages/getting-started/basic-types.md index 525d9bb129..3bbaa7c2b4 100644 --- a/lib/elixir/pages/getting-started/basic-types.md +++ b/lib/elixir/pages/getting-started/basic-types.md @@ -227,7 +227,7 @@ Elixir also supports string interpolation: ```elixir iex> string = "world" iex> "hello #{string}!" -"hello world" +"hello world!" ``` String concatenation requires both sides to be strings but interpolation supports any data type that may be converted to a string: From ab756dafc2e400ce292d047183436cbbd66a00be Mon Sep 17 00:00:00 2001 From: Benjamin Milde Date: Sun, 28 Jan 2024 22:22:37 +0100 Subject: [PATCH 0333/1886] Integrate `start_supervised` supervisor with $callers (#13253) --- lib/ex_unit/lib/ex_unit.ex | 3 +-- lib/ex_unit/lib/ex_unit/callbacks.ex | 22 +++++++++++----- .../lib/ex_unit/on_exit_handler/supervisor.ex | 25 +++++++++++++++++++ lib/ex_unit/test/ex_unit/supervised_test.exs | 18 +++++++++++++ 4 files changed, 60 insertions(+), 8 deletions(-) create mode 100644 lib/ex_unit/lib/ex_unit/on_exit_handler/supervisor.ex diff --git a/lib/ex_unit/lib/ex_unit.ex b/lib/ex_unit/lib/ex_unit.ex index dd5fa35dbc..b10c1c8a49 100644 --- a/lib/ex_unit/lib/ex_unit.ex +++ b/lib/ex_unit/lib/ex_unit.ex @@ -463,8 +463,7 @@ defmodule ExUnit do def fetch_test_supervisor() do case ExUnit.OnExitHandler.get_supervisor(self()) do {:ok, nil} -> - opts = [strategy: :one_for_one, max_restarts: 1_000_000, max_seconds: 1] - {:ok, sup} = Supervisor.start_link([], opts) + {:ok, sup} = ExUnit.OnExitHandler.Supervisor.start_link([]) ExUnit.OnExitHandler.put_supervisor(self(), sup) {:ok, sup} diff --git a/lib/ex_unit/lib/ex_unit/callbacks.ex b/lib/ex_unit/lib/ex_unit/callbacks.ex index cbebdd31af..6637784e41 100644 --- a/lib/ex_unit/lib/ex_unit/callbacks.ex +++ b/lib/ex_unit/lib/ex_unit/callbacks.ex @@ -520,6 +520,13 @@ defmodule ExUnit.Callbacks do See the `Supervisor` module for a discussion on child specifications and the available specification keys. + The started process is not linked to the test process and a crash will + not necessarily fail the test. To start and link a process to guarantee + that any crash would also fail the test use `start_link_supervised!/2`. + + This function returns `{:ok, pid}` in case of success, otherwise it + returns `{:error, reason}`. + The advantage of starting a process under the test supervisor is that it is guaranteed to exit before the next test starts. Therefore, you don't need to remove the process at the end of your tests via @@ -528,12 +535,15 @@ defmodule ExUnit.Callbacks do test, as simply shutting down the process would cause it to be restarted according to its `:restart` value. - The started process is not linked to the test process and a crash will - not necessarily fail the test. To start and link a process to guarantee - that any crash would also fail the test use `start_link_supervised!/2`. - - This function returns `{:ok, pid}` in case of success, otherwise it - returns `{:error, reason}`. + Another advantage is that the test process will act as both an ancestor + as well as a caller to the supervised processes. When a process is started + under a supervision tree, it typically populates the `$ancestors` key in + its process dictionary with all of its ancestors, which will include the test + process. Additionally, `start_supervised/2` will also store the test process + in the `$callers` key of the started process, allowing tools that perform + either ancestor or caller tracking to reach the test process. You can learn + more about these keys in + [the `Task` module](`Task#module-ancestor-and-caller-tracking`). """ @doc since: "1.5.0" @spec start_supervised(Supervisor.child_spec() | module | {module, term}, keyword) :: diff --git a/lib/ex_unit/lib/ex_unit/on_exit_handler/supervisor.ex b/lib/ex_unit/lib/ex_unit/on_exit_handler/supervisor.ex new file mode 100644 index 0000000000..06cb90912c --- /dev/null +++ b/lib/ex_unit/lib/ex_unit/on_exit_handler/supervisor.ex @@ -0,0 +1,25 @@ +defmodule ExUnit.OnExitHandler.Supervisor do + @moduledoc false + use Supervisor + + def start_link(children) do + Supervisor.start_link(__MODULE__, {children, get_callers(self())}) + end + + @impl true + def init({children, callers}) do + put_callers(callers) + Supervisor.init(children, strategy: :one_for_one, max_restarts: 1_000_000, max_seconds: 1) + end + + defp get_callers(owner) do + case :erlang.get(:"$callers") do + [_ | _] = list -> [owner | list] + _ -> [owner] + end + end + + defp put_callers(callers) do + :erlang.put(:"$callers", callers) + end +end diff --git a/lib/ex_unit/test/ex_unit/supervised_test.exs b/lib/ex_unit/test/ex_unit/supervised_test.exs index a02dfe3c0c..d8bd311a3e 100644 --- a/lib/ex_unit/test/ex_unit/supervised_test.exs +++ b/lib/ex_unit/test/ex_unit/supervised_test.exs @@ -83,6 +83,24 @@ defmodule ExUnit.SupervisedTest do end end + test "starts a supervised process with correct :\"$callers\"" do + test_pid = self() + fun = fn -> send(test_pid, {:callers, Process.get(:"$callers")}) end + {:ok, _pid} = start_supervised({Task, fun}) + + assert_receive {:callers, callers} + assert List.last(callers) == test_pid + end + + test "starts a supervised process with correct :\"$ancestors\"" do + test_pid = self() + fun = fn -> send(test_pid, {:ancestors, Process.get(:"$ancestors")}) end + {:ok, _pid} = start_supervised({Task, fun}) + + assert_receive {:ancestors, ancestors} + assert List.last(ancestors) == test_pid + end + test "stops a supervised process" do {:ok, pid} = start_supervised({MyAgent, 0}) assert stop_supervised(MyAgent) == :ok From f5cbd03f56a3cc6af75288dc13b77f2c9396e529 Mon Sep 17 00:00:00 2001 From: Colin Caine Date: Mon, 29 Jan 2024 07:23:07 +0000 Subject: [PATCH 0334/1886] docs: minor simplification of anti-patterns page (#13293) --- lib/elixir/pages/anti-patterns/code-anti-patterns.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/pages/anti-patterns/code-anti-patterns.md b/lib/elixir/pages/anti-patterns/code-anti-patterns.md index 0264c63590..032df277cd 100644 --- a/lib/elixir/pages/anti-patterns/code-anti-patterns.md +++ b/lib/elixir/pages/anti-patterns/code-anti-patterns.md @@ -117,7 +117,7 @@ def drive(%User{name: name, age: age}) when age < 18 do end ``` -While the example above is small and does not configure an anti-pattern, it is an example of mixed extraction and pattern matching. A situation where `drive/1` was more complex, having many more clauses, arguments, and extractions, would make it hard to know at a glance which variables are used for pattern/guards and which ones are not. +While the example above is small and does not constitute an anti-pattern, it is an example of mixed extraction and pattern matching. A situation where `drive/1` was more complex, having many more clauses, arguments, and extractions, would make it hard to know at a glance which variables are used for pattern/guards and which ones are not. #### Refactoring From 811f3f00872071a0295c02fb69220df97319e41b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 29 Jan 2024 10:19:00 +0100 Subject: [PATCH 0335/1886] Escape rebar3 paths --- lib/mix/lib/mix/tasks/deps.compile.ex | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/mix/lib/mix/tasks/deps.compile.ex b/lib/mix/lib/mix/tasks/deps.compile.ex index dd0ce4ec4d..0e7e3cf4d0 100644 --- a/lib/mix/lib/mix/tasks/deps.compile.ex +++ b/lib/mix/lib/mix/tasks/deps.compile.ex @@ -207,7 +207,7 @@ defmodule Mix.Tasks.Deps.Compile do {"TERM", "dumb"} ] - cmd = "#{rebar_cmd(dep)} bare compile --paths #{lib_path}" + cmd = "#{escape_path(rebar_cmd(dep))} bare compile --paths #{escape_path(lib_path)}" do_command(dep, config, cmd, false, env) # Check if we have any new symlinks after compilation @@ -220,6 +220,11 @@ defmodule Mix.Tasks.Deps.Compile do true end + defp escape_path(path) do + escape = if match?({:win32, _}, :os.type()), do: "^ ", else: "\\ " + String.replace(path, " ", escape) + end + defp rebar_config(dep) do dep.extra |> Mix.Rebar.dependency_config() From 3c8a005a03f1feb27b099833f8ee0b9c81ffc438 Mon Sep 17 00:00:00 2001 From: Wojtek Mach Date: Mon, 29 Jan 2024 16:33:39 +0100 Subject: [PATCH 0336/1886] Default `defimpl` to `@moduledoc false` (#13295) --- lib/elixir/lib/protocol.ex | 1 + lib/elixir/scripts/elixir_docs.exs | 4 ++++ lib/elixir/test/elixir/protocol_test.exs | 18 ++++++++++++++++++ 3 files changed, 23 insertions(+) diff --git a/lib/elixir/lib/protocol.ex b/lib/elixir/lib/protocol.ex index 591d25354a..30f92a6047 100644 --- a/lib/elixir/lib/protocol.ex +++ b/lib/elixir/lib/protocol.ex @@ -972,6 +972,7 @@ defmodule Protocol do Protocol.__ensure_defimpl__(protocol, for, __ENV__) defmodule name do + @moduledoc false @behaviour protocol @protocol protocol @for for diff --git a/lib/elixir/scripts/elixir_docs.exs b/lib/elixir/scripts/elixir_docs.exs index fd20e730c9..011547e81a 100644 --- a/lib/elixir/scripts/elixir_docs.exs +++ b/lib/elixir/scripts/elixir_docs.exs @@ -79,6 +79,10 @@ canonical = System.fetch_env!("CANONICAL") skip_undefined_reference_warnings_on: [ "lib/elixir/pages/references/compatibility-and-deprecations.md" ], + skip_code_autolink_to: [ + "Enumerable.List", + "Inspect.MapSet" + ], formatters: ["html", "epub"], groups_for_modules: [ # [Kernel, Kernel.SpecialForms], diff --git a/lib/elixir/test/elixir/protocol_test.exs b/lib/elixir/test/elixir/protocol_test.exs index 9931917bf1..e043f1adbb 100644 --- a/lib/elixir/test/elixir/protocol_test.exs +++ b/lib/elixir/test/elixir/protocol_test.exs @@ -133,6 +133,20 @@ defmodule ProtocolTest do end ) + write_beam( + defimpl SampleDocsProto, for: List do + def ok(_), do: true + end + ) + + write_beam( + defimpl SampleDocsProto, for: Map do + @moduledoc "for map" + + def ok(_), do: true + end + ) + {:docs_v1, _, _, _, _, _, docs} = Code.fetch_docs(SampleDocsProto) assert {{:type, :t, 0}, _, [], %{"en" => type_doc}, _} = List.keyfind(docs, {:type, :t, 0}, 0) @@ -143,6 +157,10 @@ defmodule ProtocolTest do deprecated = SampleDocsProto.__info__(:deprecated) assert [{{:ok, 1}, "Reason"}] = deprecated + + {:docs_v1, _, _, _, :hidden, _, _} = Code.fetch_docs(SampleDocsProto.List) + {:docs_v1, _, _, _, moduledoc, _, _} = Code.fetch_docs(SampleDocsProto.Map) + assert moduledoc == %{"en" => "for map"} end @compile {:no_warn_undefined, WithAll} From 96110c84c1acdea407b3c77191c2a11b3bc08eee Mon Sep 17 00:00:00 2001 From: shionryuu <8946974+shionryuu@users.noreply.github.com> Date: Tue, 30 Jan 2024 00:05:18 +0800 Subject: [PATCH 0337/1886] Fix some broken links in the guides (#13297) --- lib/elixir/pages/mix-and-otp/erlang-term-storage.md | 2 +- lib/elixir/pages/mix-and-otp/introduction-to-mix.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/elixir/pages/mix-and-otp/erlang-term-storage.md b/lib/elixir/pages/mix-and-otp/erlang-term-storage.md index d5f4efb74b..c266098a29 100644 --- a/lib/elixir/pages/mix-and-otp/erlang-term-storage.md +++ b/lib/elixir/pages/mix-and-otp/erlang-term-storage.md @@ -266,6 +266,6 @@ Our tests should now (always) pass! This concludes our optimization chapter. We have used ETS as a cache mechanism where reads can happen from any processes but writes are still serialized through a single process. More importantly, we have also learned that once data can be read asynchronously, we need to be aware of the race conditions it might introduce. -In practice, if you find yourself in a position where you need a registry for dynamic processes, you should use the `Registry` module provided as part of Elixir. It provides functionality similar to the one we have built using a GenServer + `:ets` while also being able to perform both writes and reads concurrently. [It has been benchmarked to scale across all cores even on machines with 40 cores](https://elixir-lang.org/blog/2017/01/05/elixir-v1-4-0-released/). +In practice, if you find yourself in a position where you need a registry for dynamic processes, you should use the `Registry` module provided as part of Elixir. It provides functionality similar to the one we have built using a GenServer + `:ets` while also being able to perform both writes and reads concurrently. [It has been benchmarked to scale across all cores even on machines with 40 cores](https://elixir-lang.org/releases/elixir-v1-4-0-released.html). Next, let's discuss external and internal dependencies and how Mix helps us manage large codebases. diff --git a/lib/elixir/pages/mix-and-otp/introduction-to-mix.md b/lib/elixir/pages/mix-and-otp/introduction-to-mix.md index af1e5fa171..7b3ef556ce 100644 --- a/lib/elixir/pages/mix-and-otp/introduction-to-mix.md +++ b/lib/elixir/pages/mix-and-otp/introduction-to-mix.md @@ -271,7 +271,7 @@ Most editors provide built-in integration with the formatter, allowing a file to For companies and teams, we recommend developers to run `mix format --check-formatted` on their continuous integration servers, ensuring all current and future code follows the standard. -You can learn more about the code formatter by checking [the format task documentation](`mix format`) or by reading [the release announcement for Elixir v1.6](https://elixir-lang.org/blog/2018/01/17/elixir-v1-6-0-released/), the first version to include the formatter. +You can learn more about the code formatter by checking [the format task documentation](`mix format`) or by reading [the release announcement for Elixir v1.6](https://elixir-lang.org/releases/elixir-v1-6-0-released.html), the first version to include the formatter. ## Environments From 92db97eab2228fe86344da91cbfe6f103b529a42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 29 Jan 2024 17:47:49 +0100 Subject: [PATCH 0338/1886] Revert "Fix some broken links in the guides (#13297)" This reverts commit 96110c84c1acdea407b3c77191c2a11b3bc08eee. --- lib/elixir/pages/mix-and-otp/erlang-term-storage.md | 2 +- lib/elixir/pages/mix-and-otp/introduction-to-mix.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/elixir/pages/mix-and-otp/erlang-term-storage.md b/lib/elixir/pages/mix-and-otp/erlang-term-storage.md index c266098a29..d5f4efb74b 100644 --- a/lib/elixir/pages/mix-and-otp/erlang-term-storage.md +++ b/lib/elixir/pages/mix-and-otp/erlang-term-storage.md @@ -266,6 +266,6 @@ Our tests should now (always) pass! This concludes our optimization chapter. We have used ETS as a cache mechanism where reads can happen from any processes but writes are still serialized through a single process. More importantly, we have also learned that once data can be read asynchronously, we need to be aware of the race conditions it might introduce. -In practice, if you find yourself in a position where you need a registry for dynamic processes, you should use the `Registry` module provided as part of Elixir. It provides functionality similar to the one we have built using a GenServer + `:ets` while also being able to perform both writes and reads concurrently. [It has been benchmarked to scale across all cores even on machines with 40 cores](https://elixir-lang.org/releases/elixir-v1-4-0-released.html). +In practice, if you find yourself in a position where you need a registry for dynamic processes, you should use the `Registry` module provided as part of Elixir. It provides functionality similar to the one we have built using a GenServer + `:ets` while also being able to perform both writes and reads concurrently. [It has been benchmarked to scale across all cores even on machines with 40 cores](https://elixir-lang.org/blog/2017/01/05/elixir-v1-4-0-released/). Next, let's discuss external and internal dependencies and how Mix helps us manage large codebases. diff --git a/lib/elixir/pages/mix-and-otp/introduction-to-mix.md b/lib/elixir/pages/mix-and-otp/introduction-to-mix.md index 7b3ef556ce..af1e5fa171 100644 --- a/lib/elixir/pages/mix-and-otp/introduction-to-mix.md +++ b/lib/elixir/pages/mix-and-otp/introduction-to-mix.md @@ -271,7 +271,7 @@ Most editors provide built-in integration with the formatter, allowing a file to For companies and teams, we recommend developers to run `mix format --check-formatted` on their continuous integration servers, ensuring all current and future code follows the standard. -You can learn more about the code formatter by checking [the format task documentation](`mix format`) or by reading [the release announcement for Elixir v1.6](https://elixir-lang.org/releases/elixir-v1-6-0-released.html), the first version to include the formatter. +You can learn more about the code formatter by checking [the format task documentation](`mix format`) or by reading [the release announcement for Elixir v1.6](https://elixir-lang.org/blog/2018/01/17/elixir-v1-6-0-released/), the first version to include the formatter. ## Environments From 5e13403068afd25689d0ff57fa90ca28d2cef66b Mon Sep 17 00:00:00 2001 From: Steve Johns Date: Tue, 30 Jan 2024 08:57:29 +0000 Subject: [PATCH 0339/1886] Add explicit clarification that values like 0 and "" are truthy in Elixir (#13300) --- lib/elixir/pages/getting-started/basic-types.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/elixir/pages/getting-started/basic-types.md b/lib/elixir/pages/getting-started/basic-types.md index 3bbaa7c2b4..223c51568c 100644 --- a/lib/elixir/pages/getting-started/basic-types.md +++ b/lib/elixir/pages/getting-started/basic-types.md @@ -167,6 +167,8 @@ iex> !nil true ``` +Similarly, values like `0` and `""`, which some other programming languages consider to be "falsy", are also "truthy" in Elixir. + ## Atoms An atom is a constant whose value is its own name. Some other languages call these symbols. They are often useful to enumerate over distinct values, such as: From 0b95ca45c2c81213a9469dff9fa516f3c7b708c1 Mon Sep 17 00:00:00 2001 From: Jean Klingler Date: Tue, 30 Jan 2024 21:26:39 +0900 Subject: [PATCH 0340/1886] Translate with/1 as a closure (#13299) * Translate with/1 as a closure * Emit leaner code on with var <- --- lib/elixir/src/elixir_clauses.erl | 9 ++++- lib/elixir/src/elixir_erl_pass.erl | 62 ++++++++++++++---------------- 2 files changed, 35 insertions(+), 36 deletions(-) diff --git a/lib/elixir/src/elixir_clauses.erl b/lib/elixir/src/elixir_clauses.erl index 179e2d7eb1..61e388dc5f 100644 --- a/lib/elixir/src/elixir_clauses.erl +++ b/lib/elixir/src/elixir_clauses.erl @@ -169,11 +169,16 @@ with(Meta, Args, S, E) -> {{with, Meta, EExprs ++ [[{do, EDo} | EOpts]]}, S3, E}. -expand_with({'<-', Meta, [Left, Right]}, {S, E, _HasMatch}) -> +expand_with({'<-', Meta, [Left, Right]}, {S, E, HasMatch}) -> {ERight, SR, ER} = elixir_expand:expand(Right, S, E), SM = elixir_env:reset_read(SR, S), {[ELeft], SL, EL} = head([Left], SM, ER), - {{'<-', Meta, [ELeft, ERight]}, {SL, EL, true}}; + NewHasMatch = + case ELeft of + {Var, _, Ctx} when is_atom(Var), is_atom(Ctx) -> HasMatch; + _ -> true + end, + {{'<-', Meta, [ELeft, ERight]}, {SL, EL, NewHasMatch}}; expand_with(Expr, {S, E, HasMatch}) -> {EExpr, SE, EE} = elixir_expand:expand(Expr, S, E), {EExpr, {SE, EE, HasMatch}}. diff --git a/lib/elixir/src/elixir_erl_pass.erl b/lib/elixir/src/elixir_erl_pass.erl index 575004934e..86242ab90d 100644 --- a/lib/elixir/src/elixir_erl_pass.erl +++ b/lib/elixir/src/elixir_erl_pass.erl @@ -185,9 +185,15 @@ translate({for, Meta, [_ | _] = Args}, _Ann, S) -> elixir_erl_for:translate(Meta, Args, S); translate({with, Meta, [_ | _] = Args}, _Ann, S) -> + Ann = ?ann(Meta), {Exprs, [{do, Do} | Opts]} = elixir_utils:split_last(Args), - {ElseClause, SE} = translate_with_else(Meta, Opts, S), - translate_with_do(Exprs, ?ann(Meta), Do, ElseClause, SE); + {ElseClause, MaybeFun, SE} = translate_with_else(Meta, Opts, S), + {Case, SD} = translate_with_do(Exprs, Ann, Do, ElseClause, SE), + + case MaybeFun of + nil -> {Case, SD}; + FunAssign -> {{block, Ann, [FunAssign, Case]}, SD} + end; %% Variables @@ -400,48 +406,37 @@ returns_boolean(Condition, Body) -> %% with translate_with_else(Meta, [], S) -> - Generated = ?ann(?generated(Meta)), + Ann = ?ann(Meta), {VarName, SC} = elixir_erl_var:build('_', S), - Var = {var, Generated, VarName}, - {{clause, Generated, [Var], [], [Var]}, SC}; + Var = {var, Ann, VarName}, + {{clause, Ann, [Var], [], [Var]}, nil, SC}; translate_with_else(Meta, [{'else', [{'->', _, [[{Var, VarMeta, Kind}], Clause]}]}], S) when is_atom(Var), is_atom(Kind) -> Ann = ?ann(Meta), - Generated = erl_anno:set_generated(true, Ann), {ElseVarErl, SV} = elixir_erl_var:translate(VarMeta, Var, Kind, S#elixir_erl{context=match}), {TranslatedClause, SC} = translate(Clause, Ann, SV#elixir_erl{context=nil}), - {{clause, Generated, [ElseVarErl], [], [TranslatedClause]}, SC}; + Clauses = [{clause, Ann, [ElseVarErl], [], [TranslatedClause]}], + with_else_closure(Meta, Clauses, SC); translate_with_else(Meta, [{'else', Else}], S) -> Generated = ?generated(Meta), - {ElseVarEx, ElseVarErl, SE} = elixir_erl_var:assign(Generated, S), - {RaiseVar, _, SV} = elixir_erl_var:assign(Generated, SE), + {RaiseVar, _, SV} = elixir_erl_var:assign(Generated, S), RaiseExpr = {{'.', Generated, [erlang, error]}, Generated, [{else_clause, RaiseVar}]}, RaiseClause = {'->', Generated, [[RaiseVar], RaiseExpr]}, - GeneratedElse = [build_generated_clause(Generated, ElseClause) || ElseClause <- Else], - Case = {'case', Generated, [ElseVarEx, [{do, GeneratedElse ++ [RaiseClause]}]]}, - {TranslatedCase, SC} = translate(Case, ?ann(Meta), SV), - {{clause, ?ann(Generated), [ElseVarErl], [], [TranslatedCase]}, SC}. - -build_generated_clause(Generated, {'->', _, [Args, Clause]}) -> - NewArgs = [build_generated_clause_arg(Generated, Arg) || Arg <- Args], - {'->', Generated, [NewArgs, Clause]}. - -build_generated_clause_arg(Generated, Arg) -> - {Expr, Guards} = elixir_utils:extract_guards(Arg), - NewGuards = [build_generated_guard(Generated, Guard) || Guard <- Guards], - concat_guards(Generated, Expr, NewGuards). - -build_generated_guard(Generated, {{'.', _, _} = Call, _, Args}) -> - {Call, Generated, [build_generated_guard(Generated, Arg) || Arg <- Args]}; -build_generated_guard(_, Expr) -> - Expr. - -concat_guards(_Meta, Expr, []) -> - Expr; -concat_guards(Meta, Expr, [Guard | Tail]) -> - {'when', Meta, [Expr, concat_guards(Meta, Guard, Tail)]}. + Clauses = elixir_erl_clauses:get_clauses('else', [{'else', Else ++ [RaiseClause]}], match), + {TranslatedClauses, SC} = elixir_erl_clauses:clauses(Clauses, SV), + with_else_closure(Meta, TranslatedClauses, SC). +with_else_closure(Meta, TranslatedClauses, S) -> + Ann = ?ann(Meta), + {_, FunErlVar, SC} = elixir_erl_var:assign(Meta, S), + {_, ArgErlVar, SA} = elixir_erl_var:assign(Meta, SC), + FunAssign = {match, Ann, FunErlVar, {'fun', Ann, {clauses, TranslatedClauses}}}, + FunCall = {call, Ann, FunErlVar, [ArgErlVar]}, + {{clause, Ann, [ArgErlVar], [], [FunCall]}, FunAssign, SA}. + +translate_with_do([{'<-', Meta, [{Var, _, Ctx} = Left, Expr]} | Rest], Ann, Do, Else, S) when is_atom(Var), is_atom(Ctx) -> + translate_with_do([{'=', Meta, [Left, Expr]} | Rest], Ann, Do, Else, S); translate_with_do([{'<-', Meta, [Left, Expr]} | Rest], _Ann, Do, Else, S) -> Ann = ?ann(Meta), {Args, Guards} = elixir_utils:extract_guards(Left), @@ -449,9 +444,8 @@ translate_with_do([{'<-', Meta, [Left, Expr]} | Rest], _Ann, Do, Else, S) -> {TArgs, SA} = elixir_erl_clauses:match(Ann, fun translate/3, Args, SR), TGuards = elixir_erl_clauses:guards(Ann, Guards, SA#elixir_erl.extra_guards, SA), {TBody, SB} = translate_with_do(Rest, Ann, Do, Else, SA#elixir_erl{extra_guards=[]}), - Clause = {clause, Ann, [TArgs], TGuards, unblock(TBody)}, - {{'case', erl_anno:set_generated(true, Ann), TExpr, [Clause, Else]}, SB}; + {{'case', Ann, TExpr, [Clause, Else]}, SB}; translate_with_do([Expr | Rest], Ann, Do, Else, S) -> {TExpr, TS} = translate(Expr, Ann, S), {TRest, RS} = translate_with_do(Rest, Ann, Do, Else, TS), From 59b6e2ee8bce86b424a9825915f6d91f81d8e770 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 30 Jan 2024 11:17:50 +0100 Subject: [PATCH 0341/1886] Remove unecessary .fetch file from fetchable deps --- lib/mix/lib/mix/compilers/elixir.ex | 2 +- lib/mix/lib/mix/dep/fetcher.ex | 30 +++++++++-------------- lib/mix/lib/mix/dep/loader.ex | 10 +------- lib/mix/test/mix/tasks/deps.git_test.exs | 8 +++--- lib/mix/test/mix/tasks/deps.path_test.exs | 10 -------- 5 files changed, 17 insertions(+), 43 deletions(-) diff --git a/lib/mix/lib/mix/compilers/elixir.ex b/lib/mix/lib/mix/compilers/elixir.ex index eb9da80e21..d788bb47be 100644 --- a/lib/mix/lib/mix/compilers/elixir.ex +++ b/lib/mix/lib/mix/compilers/elixir.ex @@ -68,7 +68,7 @@ defmodule Mix.Compilers.Elixir do do: [Mix.Project | stale], else: stale - # If the lock has changed or a local dependency was added ore removed, + # If the lock has changed or a local dependency was added or removed, # we need to traverse lock/config files. deps_changed? = Mix.Utils.stale?([Mix.Project.config_mtime()], [modified]) or diff --git a/lib/mix/lib/mix/dep/fetcher.ex b/lib/mix/lib/mix/dep/fetcher.ex index 2f3a3b3bbb..fc4d1b3900 100644 --- a/lib/mix/lib/mix/dep/fetcher.ex +++ b/lib/mix/lib/mix/dep/fetcher.ex @@ -65,17 +65,7 @@ defmodule Mix.Dep.Fetcher do end if new do - # There is a race condition where if you compile deps - # and then immediately update them, we would not detect - # a mismatch with .mix/compile.fetch, so we go ahead and - # delete all of them. - Mix.Project.build_path() - |> Path.dirname() - |> Path.join("*/lib/#{dep.app}/.mix/compile.fetch") - |> Path.wildcard(match_dot: true) - |> Enum.each(&File.rm/1) - - File.touch!(Path.join(opts[:dest], ".fetch")) + mark_as_fetched([dep]) dep = put_in(dep.opts[:lock], new) {dep, [app | acc], Map.put(lock, app, new)} else @@ -123,14 +113,16 @@ defmodule Mix.Dep.Fetcher do end defp mark_as_fetched(deps) do - # If the dependency is fetchable, we are going to write a .fetch - # file to it. Each build, regardless of the environment and location, - # will compared against this .fetch file to know if the dependency - # needs recompiling. - _ = - for %Mix.Dep{scm: scm, opts: opts} <- deps, scm.fetchable?() do - File.touch!(Path.join(opts[:dest], ".fetch")) - end + build_path = + Mix.Project.build_path() + |> Path.dirname() + + for %Mix.Dep{app: app, scm: scm} <- deps, scm.fetchable?() do + build_path + |> Path.join("*/lib/#{app}/.mix/compile.fetch") + |> Path.wildcard(match_dot: true) + |> Enum.each(&File.rm/1) + end :ok end diff --git a/lib/mix/lib/mix/dep/loader.ex b/lib/mix/lib/mix/dep/loader.ex index d2c6624b9e..b164174765 100644 --- a/lib/mix/lib/mix/dep/loader.ex +++ b/lib/mix/lib/mix/dep/loader.ex @@ -403,15 +403,7 @@ defmodule Mix.Dep.Loader do end defp recently_fetched?(%Mix.Dep{opts: opts, scm: scm}) do - scm.fetchable?() && - Mix.Utils.stale?( - join_stale(opts, :dest, ".fetch"), - join_stale(opts, :build, ".mix/compile.fetch") - ) - end - - defp join_stale(opts, key, file) do - [Path.join(opts[key], file)] + scm.fetchable?() and not File.exists?(Path.join(opts[:build], ".mix/compile.fetch")) end defp app_status(app_path, app, req) do diff --git a/lib/mix/test/mix/tasks/deps.git_test.exs b/lib/mix/test/mix/tasks/deps.git_test.exs index a5338b2db3..8ebca0d474 100644 --- a/lib/mix/test/mix/tasks/deps.git_test.exs +++ b/lib/mix/test/mix/tasks/deps.git_test.exs @@ -147,17 +147,17 @@ defmodule Mix.Tasks.DepsGitTest do assert_received {:mix_shell, :info, [^message]} assert File.exists?("deps/deps_on_git_repo/mix.exs") - assert File.rm("deps/deps_on_git_repo/.fetch") == :ok assert File.exists?("deps/git_repo/mix.exs") - assert File.rm("deps/git_repo/.fetch") == :ok # Compile the dependencies Mix.Tasks.Deps.Compile.run([]) + assert File.exists?("_build/dev/lib/deps_on_git_repo/.mix/compile.fetch") + assert File.exists?("_build/dev/lib/git_repo/.mix/compile.fetch") # Now update children and make sure it propagates Mix.Tasks.Deps.Update.run(["git_repo"]) - assert File.exists?("deps/deps_on_git_repo/.fetch") - assert File.exists?("deps/git_repo/.fetch") + refute File.exists?("_build/dev/lib/deps_on_git_repo/.mix/compile.fetch") + refute File.exists?("_build/dev/lib/git_repo/.mix/compile.fetch") # Clear tasks to recompile Git repo but unload it so... purge([GitRepo]) diff --git a/lib/mix/test/mix/tasks/deps.path_test.exs b/lib/mix/test/mix/tasks/deps.path_test.exs index 32333892a6..257969c602 100644 --- a/lib/mix/test/mix/tasks/deps.path_test.exs +++ b/lib/mix/test/mix/tasks/deps.path_test.exs @@ -27,16 +27,6 @@ defmodule Mix.Tasks.DepsPathTest do end end - @tag apps: [:raw_sample] - test "does not mark for compilation on get/update" do - in_fixture("deps_status", fn -> - Mix.Project.push(DepsApp) - - Mix.Tasks.Deps.Get.run(["--all"]) - refute File.exists?("custom/raw_repo/.fetch") - end) - end - @tag apps: [:raw_sample] test "compiles and runs even if lock does not match" do in_fixture("deps_status", fn -> From ffed9e08f2f5d40cb5b8d336ae3cd31cfc7970d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 30 Jan 2024 11:35:48 +0100 Subject: [PATCH 0342/1886] Optimize module computation in compile.app --- lib/mix/lib/mix/tasks/compile.app.ex | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/lib/mix/lib/mix/tasks/compile.app.ex b/lib/mix/lib/mix/tasks/compile.app.ex index 162fec230f..8e2a2ca1b2 100644 --- a/lib/mix/lib/mix/tasks/compile.app.ex +++ b/lib/mix/lib/mix/tasks/compile.app.ex @@ -138,7 +138,7 @@ defmodule Mix.Tasks.Compile.App do validate_version(version) path = Keyword.get_lazy(opts, :compile_path, &Mix.Project.compile_path/0) - modules = modules_from(Path.wildcard("#{path}/*.beam")) |> Enum.sort() + modules = modules_from(path) |> Enum.sort() target = Path.join(path, "#{app}.app") sources = [Mix.Project.config_mtime(), Mix.Project.project_file()] @@ -213,8 +213,16 @@ defmodule Mix.Tasks.Compile.App do defp ensure_present(_name, _val), do: :ok - defp modules_from(beams) do - Enum.map(beams, &(&1 |> Path.basename() |> Path.rootname(".beam") |> String.to_atom())) + defp modules_from(path) do + case File.ls(path) do + {:ok, entries} -> + for entry <- entries, + String.ends_with?(entry, ".beam"), + do: entry |> binary_part(0, byte_size(entry) - 5) |> String.to_atom() + + {:error, _} -> + [] + end end defp merge_project_application(best_guess, project) do From c837a7790fd41b1d2907debcb97378e0f79fe274 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 30 Jan 2024 13:35:56 +0100 Subject: [PATCH 0343/1886] Compare mtime with manifest entries rather than manifest itself, closes #13298 --- lib/mix/lib/mix/compilers/elixir.ex | 60 +++++++++++------- lib/mix/lib/mix/tasks/compile.protocols.ex | 62 ++++++++++--------- .../test/mix/tasks/compile.elixir_test.exs | 3 +- lib/mix/test/mix/umbrella_test.exs | 11 ++-- 4 files changed, 75 insertions(+), 61 deletions(-) diff --git a/lib/mix/lib/mix/compilers/elixir.ex b/lib/mix/lib/mix/compilers/elixir.ex index d788bb47be..2583197869 100644 --- a/lib/mix/lib/mix/compilers/elixir.ex +++ b/lib/mix/lib/mix/compilers/elixir.ex @@ -1,7 +1,7 @@ defmodule Mix.Compilers.Elixir do @moduledoc false - @manifest_vsn 23 + @manifest_vsn 24 @checkpoint_vsn 2 import Record @@ -10,6 +10,7 @@ defmodule Mix.Compilers.Elixir do defrecord :source, size: 0, + mtime: 0, digest: nil, compile_references: [], export_references: [], @@ -137,7 +138,6 @@ defmodule Mix.Compilers.Elixir do else compiler_info_from_updated( manifest, - modified, all_paths -- prev_paths, all_modules, all_sources, @@ -225,8 +225,7 @@ defmodule Mix.Compilers.Elixir do # any other change, but the diff check should be reasonably fast anyway. status = if removed != [] or deps_changed? or stale_modules != %{}, do: :ok, else: :noop - # If nothing changed but there is one more recent mtime, bump the manifest - if status != :noop or Enum.any?(Map.values(sources_stats), &(elem(&1, 0) > modified)) do + if status != :noop do write_manifest( manifest, modules, @@ -281,12 +280,19 @@ defmodule Mix.Compilers.Elixir do @doc """ Returns protocols and implementations for the given `manifest`. """ - def protocols_and_impls(manifest, compile_path) do - {modules, _} = read_manifest(manifest) - - for {module, module(kind: kind)} <- modules, - match?(:protocol, kind) or match?({:impl, _}, kind), - do: {module, kind, beam_path(compile_path, module)} + def protocols_and_impls(paths) do + Enum.reduce(paths, {%{}, %{}}, fn path, acc -> + {modules, _} = read_manifest(Path.join(path, ".mix/compile.elixir")) + + Enum.reduce(modules, acc, fn + {module, module(kind: kind, timestamp: timestamp)}, {protocols, impls} -> + case kind do + :protocol -> {Map.put(protocols, module, timestamp), impls} + {:impl, protocol} -> {protocols, Map.put(impls, module, protocol)} + _ -> {protocols, impls} + end + end) + end) end @doc """ @@ -329,7 +335,6 @@ defmodule Mix.Compilers.Elixir do defp compiler_info_from_updated( manifest, - modified, new_paths, all_modules, all_sources, @@ -387,7 +392,8 @@ defmodule Mix.Compilers.Elixir do # Sources that have changed on disk or # any modules associated with them need to be recompiled changed = - for {source, source(external: external, size: size, digest: digest, modules: modules)} <- + for {source, + source(external: external, size: size, mtime: mtime, digest: digest, modules: modules)} <- all_sources, {last_mtime, last_size} = Map.fetch!(sources_stats, source), # If the user does a change, compilation fails, and then they revert @@ -396,8 +402,8 @@ defmodule Mix.Compilers.Elixir do # files are available. size != last_size or Enum.any?(modules, &Map.has_key?(modules_to_recompile, &1)) or - Enum.any?(external, &stale_external?(&1, modified, sources_stats)) or - (last_mtime > modified and + Enum.any?(external, &stale_external?(&1, sources_stats)) or + (last_mtime > mtime and (missing_beam_file?(dest, modules) or digest_changed?(source, digest))), do: source @@ -425,10 +431,13 @@ defmodule Mix.Compilers.Elixir do {modules, exports, changed, sources_stats} end - defp stale_external?({external, digest}, modified, sources_stats) do + defp stale_external?({external, {mtime, size}, digest}, sources_stats) do case sources_stats do - %{^external => {0, 0}} -> digest != nil - %{^external => {mtime, _}} -> mtime > modified and digest_changed?(external, digest) + %{^external => {0, 0}} -> + digest != nil + + %{^external => {last_mtime, last_size}} -> + size != last_size or (last_mtime > mtime and digest_changed?(external, digest)) end end @@ -436,7 +445,7 @@ defmodule Mix.Compilers.Elixir do Enum.reduce(sources, %{}, fn {source, source(external: external)}, map -> map = Map.put_new_lazy(map, source, fn -> Mix.Utils.last_modified_and_size(source) end) - Enum.reduce(external, map, fn {file, _}, map -> + Enum.reduce(external, map, fn {file, _, _}, map -> Map.put_new_lazy(map, file, fn -> Mix.Utils.last_modified_and_size(file) end) end) end) @@ -489,7 +498,7 @@ defmodule Mix.Compilers.Elixir do # so we rely on sources_stats to avoid multiple FS lookups. defp update_stale_sources(sources, stale, removed_modules, sources_stats) do Enum.reduce(stale, {sources, removed_modules}, fn file, {acc_sources, acc_modules} -> - %{^file => {_, size}} = sources_stats + %{^file => {mtime, size}} = sources_stats modules = case acc_sources do @@ -498,7 +507,7 @@ defmodule Mix.Compilers.Elixir do end acc_modules = Enum.reduce(modules, acc_modules, &Map.put(&2, &1, true)) - {Map.put(acc_sources, file, source(size: size)), acc_modules} + {Map.put(acc_sources, file, source(size: size, mtime: mtime)), acc_modules} end) end @@ -1199,10 +1208,13 @@ defmodule Mix.Compilers.Elixir do defp process_external_resources(external, cwd) do for file <- external do - case File.read(file) do - {:ok, binary} -> {Path.relative_to(file, cwd), digest_contents(binary)} - {:error, _} -> {Path.relative_to(file, cwd), nil} - end + digest = + case File.read(file) do + {:ok, binary} -> digest_contents(binary) + {:error, _} -> nil + end + + {Path.relative_to(file, cwd), Mix.Utils.last_modified_and_size(file), digest} end end end diff --git a/lib/mix/lib/mix/tasks/compile.protocols.ex b/lib/mix/lib/mix/tasks/compile.protocols.ex index 62a34bc739..bed2f942b2 100644 --- a/lib/mix/lib/mix/tasks/compile.protocols.ex +++ b/lib/mix/lib/mix/tasks/compile.protocols.ex @@ -2,7 +2,7 @@ defmodule Mix.Tasks.Compile.Protocols do use Mix.Task.Compiler @manifest "compile.protocols" - @manifest_vsn 1 + @manifest_vsn 2 @moduledoc ~S""" Consolidates all protocols in all paths. @@ -100,11 +100,7 @@ defmodule Mix.Tasks.Compile.Protocols do [Mix.Project.app_path(config) | deps] end - Enum.flat_map(paths, fn path -> - manifest_path = Path.join(path, ".mix/compile.elixir") - compile_path = Path.join(path, "ebin") - Mix.Compilers.Elixir.protocols_and_impls(manifest_path, compile_path) - end) + Mix.Compilers.Elixir.protocols_and_impls(paths) end defp consolidation_paths do @@ -179,7 +175,7 @@ defmodule Mix.Tasks.Compile.Protocols do _ -> # If there is no manifest or it is out of date, remove old files File.rm_rf(output) - [] + {%{}, %{}} end end @@ -189,37 +185,46 @@ defmodule Mix.Tasks.Compile.Protocols do File.write!(manifest, manifest_data) end - defp diff_manifest(manifest, new_metadata, output) do - modified = Mix.Utils.last_modified(manifest) - old_metadata = read_manifest(manifest, output) + defp diff_manifest(manifest, {new_protocols, new_impls}, output) do + {old_protocols, old_impls} = read_manifest(manifest, output) protocols = - for {protocol, :protocol, beam} <- new_metadata, - Mix.Utils.last_modified(beam) > modified, - remove_consolidated(protocol, output), - do: protocol - - protocols = - Enum.reduce(new_metadata -- old_metadata, Map.from_keys(protocols, true), fn - {_, {:impl, protocol}, _beam}, protocols -> - Map.put(protocols, protocol, true) - - {protocol, :protocol, _beam}, protocols -> - Map.put(protocols, protocol, true) + new_protocols + |> Enum.filter(fn {protocol, new_timestamp} -> + case old_protocols do + # There is a new version, removed the consolidated + %{^protocol => old_timestamp} when new_timestamp > old_timestamp -> + remove_consolidated(protocol, output) + true + + # Nothing changed + %{^protocol => _} -> + false + + # New protocol + %{} -> + true + end end) + |> Map.new() - removed_metadata = old_metadata -- new_metadata + protocols = + for {impl, protocol} <- new_impls, + not is_map_key(old_impls, impl), + do: {protocol, true}, + into: protocols removed_protocols = - for {protocol, :protocol, _beam} <- removed_metadata, - remove_consolidated(protocol, output), - do: protocol + for {protocol, _timestamp} <- old_protocols, + not is_map_key(new_protocols, protocol), + do: remove_consolidated(protocol, output) removed_protocols = Map.from_keys(removed_protocols, true) protocols = - for {_, {:impl, protocol}, _beam} <- removed_metadata, - not Map.has_key?(removed_protocols, protocol), + for {impl, protocol} <- old_impls, + not is_map_key(new_impls, impl), + not is_map_key(removed_protocols, protocol), do: {protocol, true}, into: protocols @@ -228,5 +233,6 @@ defmodule Mix.Tasks.Compile.Protocols do defp remove_consolidated(protocol, output) do File.rm(Path.join(output, "#{Atom.to_string(protocol)}.beam")) + protocol end end diff --git a/lib/mix/test/mix/tasks/compile.elixir_test.exs b/lib/mix/test/mix/tasks/compile.elixir_test.exs index 0cb95353d3..739b19ea66 100644 --- a/lib/mix/test/mix/tasks/compile.elixir_test.exs +++ b/lib/mix/test/mix/tasks/compile.elixir_test.exs @@ -1019,8 +1019,7 @@ defmodule Mix.Tasks.Compile.ElixirTest do File.touch!("lib/a.eex", {{2038, 1, 1}, {0, 0, 0}}) assert Mix.Tasks.Compile.Elixir.run(["--verbose"]) == {:noop, []} - File.write!("lib/a.eex", [File.read!("lib/a.eex"), ?\n]) - File.touch!("lib/a.eex", {{2038, 1, 1}, {0, 0, 0}}) + force_recompilation("lib/a.eex") assert Mix.Tasks.Compile.Elixir.run(["--verbose"]) == {:ok, []} assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]} refute_received {:mix_shell, :info, ["Compiled lib/b.ex"]} diff --git a/lib/mix/test/mix/umbrella_test.exs b/lib/mix/test/mix/umbrella_test.exs index c1d2d13ec3..a2c6696221 100644 --- a/lib/mix/test/mix/umbrella_test.exs +++ b/lib/mix/test/mix/umbrella_test.exs @@ -573,13 +573,10 @@ defmodule Mix.UmbrellaTest do # Mark protocol as outdated File.touch!("_build/dev/lib/bar/consolidated/Elixir.Foo.beam", {{2010, 1, 1}, {0, 0, 0}}) - - ensure_touched( - "_build/dev/lib/foo/ebin/Elixir.Foo.beam", - "_build/dev/lib/bar/.mix/compile.protocols" - ) - - assert Mix.Tasks.Compile.Protocols.run([]) == :ok + force_recompilation("../foo/lib/foo.ex") + ensure_touched("../foo/lib/foo.ex", "_build/dev/lib/bar/.mix/compile.elixir") + Mix.Task.clear() + Mix.Task.run("compile") # Check new timestamp mtime = File.stat!("_build/dev/lib/bar/consolidated/Elixir.Foo.beam").mtime From 4a11a3362d0c24ce4b9125794b825736d5d8a720 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 30 Jan 2024 14:24:58 +0100 Subject: [PATCH 0344/1886] Avoid comparing config mtime in Elixir compiler --- lib/mix/lib/mix/compilers/elixir.ex | 33 ++++++++++++++----- lib/mix/lib/mix/project.ex | 5 +++ .../test/mix/tasks/compile.elixir_test.exs | 13 +++++--- 3 files changed, 38 insertions(+), 13 deletions(-) diff --git a/lib/mix/lib/mix/compilers/elixir.ex b/lib/mix/lib/mix/compilers/elixir.ex index 2583197869..e3f416b9a4 100644 --- a/lib/mix/lib/mix/compilers/elixir.ex +++ b/lib/mix/lib/mix/compilers/elixir.ex @@ -1,7 +1,7 @@ defmodule Mix.Compilers.Elixir do @moduledoc false - @manifest_vsn 24 + @manifest_vsn 25 @checkpoint_vsn 2 import Record @@ -35,6 +35,8 @@ defmodule Mix.Compilers.Elixir do def compile(manifest, srcs, dest, new_cache_key, new_parent_manifests, new_parents, opts) do Mix.ensure_application!(:crypto) modified = Mix.Utils.last_modified(manifest) + config_mtime = Mix.Project.config_mtime() + project_mtime = Mix.Utils.last_modified(Mix.Project.project_file()) new_parents = :ordsets.from_list(new_parents) # We fetch the time from before we read files so any future @@ -43,7 +45,9 @@ defmodule Mix.Compilers.Elixir do timestamp = System.os_time(:second) all_paths = Mix.Utils.extract_files(srcs, [:ex]) - {all_modules, all_sources, all_local_exports, old_parents, old_cache_key, old_deps_config} = + {all_modules, all_sources, all_local_exports, old_parents, old_cache_key, old_deps_config, + old_project_mtime, + old_config_mtime} = parse_manifest(manifest, dest) # Prepend ourselves early because of __mix_recompile__? checks @@ -65,14 +69,14 @@ defmodule Mix.Compilers.Elixir do # If mix.exs has changed, recompile anything that calls Mix.Project. stale = - if Mix.Utils.stale?([Mix.Project.project_file()], [modified]), + if project_mtime > old_project_mtime, do: [Mix.Project | stale], else: stale # If the lock has changed or a local dependency was added or removed, # we need to traverse lock/config files. deps_changed? = - Mix.Utils.stale?([Mix.Project.config_mtime()], [modified]) or + config_mtime > old_config_mtime or local_deps_changed?(old_deps_config, local_deps) # If a configuration is only accessed at compile-time, we don't need to @@ -191,6 +195,8 @@ defmodule Mix.Compilers.Elixir do new_parents, new_cache_key, new_deps_config, + project_mtime, + config_mtime, timestamp ) @@ -234,6 +240,8 @@ defmodule Mix.Compilers.Elixir do new_parents, new_cache_key, new_deps_config, + project_mtime, + config_mtime, timestamp ) end @@ -304,7 +312,7 @@ defmodule Mix.Compilers.Elixir do rescue _ -> {[], []} else - {@manifest_vsn, modules, sources, _, _, _, _} -> {modules, sources} + {@manifest_vsn, modules, sources, _, _, _, _, _, _} -> {modules, sources} _ -> {[], []} end end @@ -839,7 +847,7 @@ defmodule Mix.Compilers.Elixir do ## Manifest handling - @default_manifest {%{}, %{}, %{}, [], nil, nil} + @default_manifest {%{}, %{}, %{}, [], nil, nil, 0, 0} # Similar to read_manifest, but for internal consumption and with data migration support. defp parse_manifest(manifest, compile_path) do @@ -849,8 +857,10 @@ defmodule Mix.Compilers.Elixir do _ -> @default_manifest else - {@manifest_vsn, modules, sources, local_exports, parent, cache_key, deps_config} -> - {modules, sources, local_exports, parent, cache_key, deps_config} + {@manifest_vsn, modules, sources, local_exports, parent, cache_key, deps_config, + project_mtime, config_mtime} -> + {modules, sources, local_exports, parent, cache_key, deps_config, project_mtime, + config_mtime} # {vsn, %{module => record}, sources, ...} v22-? # {vsn, [module_record], sources, ...} v5-v21 @@ -902,6 +912,8 @@ defmodule Mix.Compilers.Elixir do parents, cache_key, deps_config, + project_mtime, + config_mtime, timestamp ) do if modules == %{} and sources == %{} do @@ -909,7 +921,10 @@ defmodule Mix.Compilers.Elixir do else File.mkdir_p!(Path.dirname(manifest)) - term = {@manifest_vsn, modules, sources, exports, parents, cache_key, deps_config} + term = + {@manifest_vsn, modules, sources, exports, parents, cache_key, deps_config, project_mtime, + config_mtime} + manifest_data = :erlang.term_to_binary(term, [:compressed]) File.write!(manifest, manifest_data) File.touch!(manifest, timestamp) diff --git a/lib/mix/lib/mix/project.ex b/lib/mix/lib/mix/project.ex index 9c11f2f4ad..3522c8041a 100644 --- a/lib/mix/lib/mix/project.ex +++ b/lib/mix/lib/mix/project.ex @@ -325,6 +325,11 @@ defmodule Mix.Project do a full recompilation whenever such configuration files change. For this reason, the mtime is cached to avoid file system lookups. + However, for effective used of this function, you must avoid + comparing source files with the `config_mtime` itself. Instead, + store the previous `config_mtime` and compare it with the new + `config_mtime` in order to detect if something is stale. + Note: before Elixir v1.13.0, the `mix.exs` file was also included in the mtimes, but not anymore. You can compute its modification date by calling `project_file/0`. diff --git a/lib/mix/test/mix/tasks/compile.elixir_test.exs b/lib/mix/test/mix/tasks/compile.elixir_test.exs index 739b19ea66..c0c954b279 100644 --- a/lib/mix/test/mix/tasks/compile.elixir_test.exs +++ b/lib/mix/test/mix/tasks/compile.elixir_test.exs @@ -96,7 +96,7 @@ defmodule Mix.Tasks.Compile.ElixirTest do assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]} assert_received {:mix_shell, :info, ["Compiled lib/b.ex"]} - File.touch!("_build/dev/lib/sample/.mix/compile.elixir", @old_time) + ensure_touched(__ENV__.file, "_build/dev/lib/sample/.mix/compile.elixir") assert Mix.Tasks.Compile.Elixir.run(["--verbose"]) == {:ok, []} assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]} refute_received {:mix_shell, :info, ["Compiled lib/b.ex"]} @@ -107,14 +107,13 @@ defmodule Mix.Tasks.Compile.ElixirTest do end """) + ensure_touched(__ENV__.file, "_build/dev/lib/sample/.mix/compile.elixir") File.touch!("_build/dev/lib/sample/.mix/compile.elixir", @old_time) assert Mix.Tasks.Compile.Elixir.run(["--verbose"]) == {:ok, []} assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]} refute_received {:mix_shell, :info, ["Compiled lib/b.ex"]} assert File.stat!("_build/dev/lib/sample/.mix/compile.elixir").mtime > @old_time - # Making the manifest olds returns :ok, but does not recompile. - # Note we use ensure_touched instead of @old_time for preciseness. ensure_touched(__ENV__.file, "_build/dev/lib/sample/.mix/compile.elixir") assert Mix.Tasks.Compile.Elixir.run(["--verbose"]) == {:ok, []} refute_received {:mix_shell, :info, ["Compiled lib/a.ex"]} @@ -146,6 +145,7 @@ defmodule Mix.Tasks.Compile.ElixirTest do recompile = fn -> Mix.ProjectStack.pop() Mix.Project.push(MixTest.Case.Sample) + ensure_touched("config/config.exs") Mix.Tasks.Loadconfig.load_compile("config/config.exs") Mix.Tasks.Compile.Elixir.run(["--verbose"]) end @@ -276,6 +276,7 @@ defmodule Mix.Tasks.Compile.ElixirTest do recompile = fn -> Mix.ProjectStack.pop() Mix.Project.push(MixTest.Case.Sample) + ensure_touched("config/config.exs") Mix.Tasks.Loadconfig.load_compile("config/config.exs") Mix.Tasks.Compile.Elixir.run(["--verbose"]) end @@ -345,6 +346,7 @@ defmodule Mix.Tasks.Compile.ElixirTest do recompile = fn -> Mix.ProjectStack.pop() Mix.Project.push(MixTest.Case.Sample) + ensure_touched("config/config.exs") Mix.Tasks.Loadconfig.load_compile("config/config.exs") Mix.Tasks.Compile.Elixir.run(["--verbose"]) end @@ -404,6 +406,7 @@ defmodule Mix.Tasks.Compile.ElixirTest do recompile = fn -> Mix.ProjectStack.pop() Mix.Project.push(MixTest.Case.Sample) + ensure_touched("config/config.exs") Mix.Tasks.Loadconfig.load_compile("config/config.exs") Mix.Tasks.Compile.Elixir.run(["--verbose"]) end @@ -499,8 +502,8 @@ defmodule Mix.Tasks.Compile.ElixirTest do %{"logger": :unused} """) + ensure_touched("mix.lock") File.touch!("_build/dev/lib/sample/.mix/compile.elixir", @old_time) - File.touch!("_build/dev/lib/sample/.mix/compile.app_cache", @old_time) assert recompile.() == {:ok, []} assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]} refute_received {:mix_shell, :info, ["Compiled lib/b.ex"]} @@ -511,6 +514,7 @@ defmodule Mix.Tasks.Compile.ElixirTest do %{"logger": :another} """) + ensure_touched("mix.lock") File.touch!("_build/dev/lib/sample/.mix/compile.elixir", @old_time) assert recompile.() == {:ok, []} assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]} @@ -522,6 +526,7 @@ defmodule Mix.Tasks.Compile.ElixirTest do %{} """) + ensure_touched("mix.lock") File.touch!("_build/dev/lib/sample/.mix/compile.elixir", @old_time) assert recompile.() == {:ok, []} assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]} From 6e706db0ece81cf5ebfdca12091ca529f2059864 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 30 Jan 2024 14:45:00 +0100 Subject: [PATCH 0345/1886] Do not compare manifest mtime in compile.protocols --- lib/mix/lib/mix/tasks/compile.protocols.ex | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/lib/mix/lib/mix/tasks/compile.protocols.ex b/lib/mix/lib/mix/tasks/compile.protocols.ex index bed2f942b2..c3acaf8ebd 100644 --- a/lib/mix/lib/mix/tasks/compile.protocols.ex +++ b/lib/mix/lib/mix/tasks/compile.protocols.ex @@ -2,7 +2,7 @@ defmodule Mix.Tasks.Compile.Protocols do use Mix.Task.Compiler @manifest "compile.protocols" - @manifest_vsn 2 + @manifest_vsn 3 @moduledoc ~S""" Consolidates all protocols in all paths. @@ -49,23 +49,26 @@ defmodule Mix.Tasks.Compile.Protocols do manifest = manifest() output = Mix.Project.consolidation_path(config) + config_mtime = Mix.Project.config_mtime() protocols_and_impls = protocols_and_impls(config) + metadata = {config_mtime, protocols_and_impls} + {old_config_mtime, old_protocols_and_impls} = read_manifest(manifest, output) cond do # We need to reconsolidate all protocols whenever the dependency changes # because we only track protocols from the current app and from local deps. - opts[:force] || Mix.Utils.stale?([Mix.Project.config_mtime()], [manifest]) -> + opts[:force] || config_mtime > old_config_mtime -> clean() paths = consolidation_paths() paths |> Protocol.extract_protocols() - |> consolidate(paths, output, manifest, protocols_and_impls, opts) + |> consolidate(paths, output, manifest, metadata, opts) protocols_and_impls -> - manifest - |> diff_manifest(protocols_and_impls, output) - |> consolidate(consolidation_paths(), output, manifest, protocols_and_impls, opts) + protocols_and_impls + |> diff_manifest(old_protocols_and_impls, output) + |> consolidate(consolidation_paths(), output, manifest, metadata, opts) true -> :noop @@ -175,7 +178,7 @@ defmodule Mix.Tasks.Compile.Protocols do _ -> # If there is no manifest or it is out of date, remove old files File.rm_rf(output) - {%{}, %{}} + {0, {%{}, %{}}} end end @@ -185,9 +188,7 @@ defmodule Mix.Tasks.Compile.Protocols do File.write!(manifest, manifest_data) end - defp diff_manifest(manifest, {new_protocols, new_impls}, output) do - {old_protocols, old_impls} = read_manifest(manifest, output) - + defp diff_manifest({new_protocols, new_impls}, {old_protocols, old_impls}, output) do protocols = new_protocols |> Enum.filter(fn {protocol, new_timestamp} -> From b705597dda028e0ba8b8d527750774e904d4715c Mon Sep 17 00:00:00 2001 From: Steve Johns Date: Tue, 30 Jan 2024 13:49:34 +0000 Subject: [PATCH 0346/1886] docs: fix grammar errors in syntax reference docs (#13302) --- lib/elixir/pages/references/syntax-reference.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/elixir/pages/references/syntax-reference.md b/lib/elixir/pages/references/syntax-reference.md index f9ec122012..2feff8f84d 100644 --- a/lib/elixir/pages/references/syntax-reference.md +++ b/lib/elixir/pages/references/syntax-reference.md @@ -92,7 +92,7 @@ Variables in Elixir must start with an underscore or a Unicode letter that is no ### Non-qualified calls (local calls) -Non-qualified calls, such as `add(1, 2)`, must start with characters and then follow the same rules as as variables, which are optionally followed by parentheses, and then arguments. +Non-qualified calls, such as `add(1, 2)`, must start with characters and then follow the same rules as variables, which are optionally followed by parentheses, and then arguments. Parentheses are required for zero-arity calls (i.e. calls without arguments), to avoid ambiguity with variables. If parentheses are used, they must immediately follow the function name *without spaces*. For example, `add (1, 2)` is a syntax error, since `(1, 2)` is treated as an invalid block which is attempted to be given as a single argument to `add`. @@ -104,7 +104,7 @@ As many programming languages, Elixir also support operators as non-qualified ca ### Qualified calls (remote calls) -Qualified calls, such as `Math.add(1, 2)`, must start with characters and then follow the same rules as as variables, which are optionally followed by parentheses, and then arguments. Qualified calls also support operators, such as `Kernel.+(1, 2)`. Elixir also allows the function name to be written between double- or single-quotes, allowing any character in between the quotes, such as `Math."++add++"(1, 2)`. +Qualified calls, such as `Math.add(1, 2)`, must start with characters and then follow the same rules as variables, which are optionally followed by parentheses, and then arguments. Qualified calls also support operators, such as `Kernel.+(1, 2)`. Elixir also allows the function name to be written between double- or single-quotes, allowing any character in between the quotes, such as `Math."++add++"(1, 2)`. Similar to non-qualified calls, parentheses have different meaning for zero-arity calls (i.e. calls without arguments). If parentheses are used, such as `mod.fun()`, it means a function call. If parenthesis are skipped, such as `map.field`, it means accessing a field of a map. From 0ba650ee547a591c033b01780d4a3fe2022a5f96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 30 Jan 2024 15:17:59 +0100 Subject: [PATCH 0347/1886] Do not compare config_mtime against manifest in compile.app --- lib/mix/lib/mix/tasks/compile.app.ex | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/mix/lib/mix/tasks/compile.app.ex b/lib/mix/lib/mix/tasks/compile.app.ex index 8e2a2ca1b2..994848ace4 100644 --- a/lib/mix/lib/mix/tasks/compile.app.ex +++ b/lib/mix/lib/mix/tasks/compile.app.ex @@ -141,12 +141,15 @@ defmodule Mix.Tasks.Compile.App do modules = modules_from(path) |> Enum.sort() target = Path.join(path, "#{app}.app") - sources = [Mix.Project.config_mtime(), Mix.Project.project_file()] + + new_mtime = + max(Mix.Project.config_mtime(), Mix.Utils.last_modified(Mix.Project.project_file())) current_properties = current_app_properties(target) compile_env = load_compile_env(current_properties) + old_mtime = Keyword.get(current_properties, :config_mtime, 0) - if opts[:force] || Mix.Utils.stale?(sources, [target]) || + if opts[:force] || new_mtime > old_mtime || app_changed?(current_properties, modules, compile_env) do properties = [ @@ -160,6 +163,7 @@ defmodule Mix.Tasks.Compile.App do |> handle_extra_applications(config) |> add_compile_env(compile_env) + properties = [config_mtime: new_mtime] ++ properties contents = :io_lib.format("~p.~n", [{:application, app, properties}]) Mix.Project.ensure_structure() From 1e47e26f5be4ba95f4b5c929266ccd302db11305 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 30 Jan 2024 17:09:57 +0100 Subject: [PATCH 0348/1886] Add code comments to mtime management decisions --- lib/mix/lib/mix/app_loader.ex | 5 +++++ lib/mix/lib/mix/tasks/compile.app.ex | 2 ++ lib/mix/lib/mix/tasks/compile.protocols.ex | 4 ++++ 3 files changed, 11 insertions(+) diff --git a/lib/mix/lib/mix/app_loader.ex b/lib/mix/lib/mix/app_loader.ex index 2392c528d5..a7f9bd5be6 100644 --- a/lib/mix/lib/mix/app_loader.ex +++ b/lib/mix/lib/mix/app_loader.ex @@ -33,6 +33,11 @@ defmodule Mix.AppLoader do manifest = manifest(config) modified = Mix.Utils.last_modified(manifest) + # We depend both on the lockfile via compile.lock (a build artifact) via + # `config_mtime` and `project_file`. Ideally we compare the `project_file` + # timestamp (a source artifact) against its old timestamp (instead of the + # manifest timestamp which is a build artifact), but at the moment there + # is no trivial place to store it. if Mix.Utils.stale?([Mix.Project.config_mtime(), Mix.Project.project_file()], [modified]) do manifest else diff --git a/lib/mix/lib/mix/tasks/compile.app.ex b/lib/mix/lib/mix/tasks/compile.app.ex index 994848ace4..5f1445a662 100644 --- a/lib/mix/lib/mix/tasks/compile.app.ex +++ b/lib/mix/lib/mix/tasks/compile.app.ex @@ -142,6 +142,8 @@ defmodule Mix.Tasks.Compile.App do target = Path.join(path, "#{app}.app") + # We mostly depend on the project_file through the def application function, + # but it doesn't hurt to also include config_mtime. new_mtime = max(Mix.Project.config_mtime(), Mix.Utils.last_modified(Mix.Project.project_file())) diff --git a/lib/mix/lib/mix/tasks/compile.protocols.ex b/lib/mix/lib/mix/tasks/compile.protocols.ex index c3acaf8ebd..6734f67dd1 100644 --- a/lib/mix/lib/mix/tasks/compile.protocols.ex +++ b/lib/mix/lib/mix/tasks/compile.protocols.ex @@ -57,6 +57,10 @@ defmodule Mix.Tasks.Compile.Protocols do cond do # We need to reconsolidate all protocols whenever the dependency changes # because we only track protocols from the current app and from local deps. + # + # We are only interested in the compile.lock from config_mtime (which is + # a build artifact), so it would be fine to compare it directly against + # the manifest, but let's follow best practices anyway. opts[:force] || config_mtime > old_config_mtime -> clean() paths = consolidation_paths() From 00d2be5af7ee52f0714cac7d381b553185270706 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 30 Jan 2024 20:02:19 +0100 Subject: [PATCH 0349/1886] Fix error message 'a guards', closes #13296 --- lib/elixir/src/elixir_expand.erl | 12 ++++++------ lib/elixir/src/elixir_rewrite.erl | 2 +- lib/elixir/test/elixir/kernel/expansion_test.exs | 16 ++++++++-------- lib/elixir/test/elixir/kernel/guard_test.exs | 12 ++++++------ 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/lib/elixir/src/elixir_expand.erl b/lib/elixir/src/elixir_expand.erl index fa237b988b..50354e28e5 100644 --- a/lib/elixir/src/elixir_expand.erl +++ b/lib/elixir/src/elixir_expand.erl @@ -836,7 +836,7 @@ expand_local(Meta, Name, Args, S, #{module := Module, function := Function, cont %% so we can print multiple entries at the same time. case Context of match -> - module_error(Meta, E, ?MODULE, {invalid_local_invocation, match, {Name, Meta, Args}}); + module_error(Meta, E, ?MODULE, {invalid_local_invocation, "match", {Name, Meta, Args}}); guard -> module_error(Meta, E, ?MODULE, {invalid_local_invocation, guard_context(S), {Name, Meta, Args}}); @@ -1166,7 +1166,7 @@ assert_no_underscore_clause_in_cond(_Other, _E) -> %% Errors guard_context(#elixir_ex{prematch={_, _, {bitsize, _}}}) -> "bitstring size specifier"; -guard_context(_) -> "guards". +guard_context(_) -> "guard". format_error(invalid_match_on_zero_float) -> "pattern matching on 0.0 is equivalent to matching only on +0.0 from Erlang/OTP 27+. Instead you must match on +0.0 or -0.0"; @@ -1264,7 +1264,7 @@ format_error({invalid_expr_in_guard, Kind}) -> io_lib:format(Message, [Kind]); format_error({invalid_expr_in_bitsize, Kind}) -> Message = - "~ts is not allowed in bitstring size specifier. The size specifier in matches works like guards. " + "~ts is not allowed inside a bitstring size specifier. The size specifier in matches works like guards. " "To learn more about guards, visit: https://hexdocs.pm/elixir/patterns-and-guards.html#guards", io_lib:format(Message, [Kind]); format_error({invalid_alias, Expr}) -> @@ -1313,8 +1313,8 @@ format_error({invalid_quoted_expr, Expr}) -> io_lib:format(Message, ['Elixir.Kernel':inspect(Expr, [])]); format_error({invalid_local_invocation, Context, {Name, _, Args} = Call}) -> Message = - "cannot find or invoke local ~ts/~B inside ~ts. " - "Only macros can be invoked in a ~ts and they must be defined before their invocation. Called as: ~ts", + "cannot find or invoke local ~ts/~B inside a ~ts. " + "Only macros can be invoked inside a ~ts and they must be defined before their invocation. Called as: ~ts", io_lib:format(Message, [Name, length(Args), Context, Context, 'Elixir.Macro':to_string(Call)]); format_error({invalid_pid_in_function, Pid, {Name, Arity}}) -> io_lib:format("cannot compile PID ~ts inside quoted expression for function ~ts/~B", @@ -1369,7 +1369,7 @@ format_error({undefined_var_to_call, Name}) -> io_lib:format("variable \"~ts\" does not exist and is being expanded to \"~ts()\"," " please use parentheses to remove the ambiguity or change the variable name", [Name, Name]); format_error({parens_map_lookup, Map, Field, Context}) -> - io_lib:format("cannot invoke remote function in ~ts. " + io_lib:format("cannot invoke remote function inside a ~ts. " "If you want to do a map lookup instead, please remove parens from ~ts.~ts()", [Context, 'Elixir.Macro':to_string(Map), Field]); format_error({super_in_genserver, {Name, Arity}}) -> diff --git a/lib/elixir/src/elixir_rewrite.erl b/lib/elixir/src/elixir_rewrite.erl index ad2cf5905b..aa262a21fc 100644 --- a/lib/elixir/src/elixir_rewrite.erl +++ b/lib/elixir/src/elixir_rewrite.erl @@ -340,7 +340,7 @@ allowed_guard(Right, Arity) -> erl_internal:guard_bif(Right, Arity) orelse elixir_utils:guard_op(Right, Arity). format_error({invalid_guard, Receiver, Right, Arity, Context}) -> - io_lib:format("cannot invoke remote function ~ts.~ts/~B inside ~ts", + io_lib:format("cannot invoke remote function ~ts.~ts/~B inside a ~ts", ['Elixir.Macro':to_string(Receiver), Right, Arity, Context]); format_error({invalid_match, Receiver, Right, Arity}) -> io_lib:format("cannot invoke remote function ~ts.~ts/~B inside a match", diff --git a/lib/elixir/test/elixir/kernel/expansion_test.exs b/lib/elixir/test/elixir/kernel/expansion_test.exs index 6ee156d51e..26ee6a0bd2 100644 --- a/lib/elixir/test/elixir/kernel/expansion_test.exs +++ b/lib/elixir/test/elixir/kernel/expansion_test.exs @@ -434,7 +434,7 @@ defmodule Kernel.ExpansionTest do test "in matches" do assert_compile_error( - ~r"cannot find or invoke local foo/1 inside match. .+ Called as: foo\(:bar\)", + ~r"cannot find or invoke local foo/1 inside a match. .+ Called as: foo\(:bar\)", fn -> expand(quote(do: foo(:bar) = :bar)) end @@ -737,13 +737,13 @@ defmodule Kernel.ExpansionTest do test "in guards" do message = - ~r"cannot invoke remote function Hello.something_that_does_not_exist/1 inside guard" + ~r"cannot invoke remote function Hello.something_that_does_not_exist/1 inside a guard" assert_compile_error(message, fn -> expand(quote(do: fn arg when Hello.something_that_does_not_exist(arg) -> arg end)) end) - message = ~r"cannot invoke remote function :erlang.make_ref/0 inside guard" + message = ~r"cannot invoke remote function :erlang.make_ref/0 inside a guard" assert_compile_error(message, fn -> expand(quote(do: fn arg when make_ref() -> arg end)) @@ -751,7 +751,7 @@ defmodule Kernel.ExpansionTest do end test "in guards with bitstrings" do - message = ~r"cannot invoke remote function String.Chars.to_string/1 inside guards" + message = ~r"cannot invoke remote function String.Chars.to_string/1 inside a guard" assert_compile_error(message, fn -> expand(quote(do: fn arg when "#{arg}foo" == "argfoo" -> arg end)) @@ -2731,7 +2731,7 @@ defmodule Kernel.ExpansionTest do end) assert_compile_error( - ~r"cannot find or invoke local foo/0 inside bitstring size specifier", + ~r"cannot find or invoke local foo/0 inside a bitstring size specifier", fn -> code = quote do @@ -2742,7 +2742,7 @@ defmodule Kernel.ExpansionTest do end ) - message = ~r"anonymous call is not allowed in bitstring size specifier" + message = ~r"anonymous call is not allowed inside a bitstring size specifier" assert_compile_error(message, fn -> code = @@ -2753,7 +2753,7 @@ defmodule Kernel.ExpansionTest do expand(code, []) end) - message = ~r"cannot invoke remote function in bitstring size specifier" + message = ~r"cannot invoke remote function inside a bitstring size specifier" assert_compile_error(message, fn -> code = @@ -2765,7 +2765,7 @@ defmodule Kernel.ExpansionTest do expand(code, []) end) - message = ~r"cannot invoke remote function Foo.bar/0 inside bitstring size specifier" + message = ~r"cannot invoke remote function Foo.bar/0 inside a bitstring size specifier" assert_compile_error(message, fn -> code = diff --git a/lib/elixir/test/elixir/kernel/guard_test.exs b/lib/elixir/test/elixir/kernel/guard_test.exs index d1a2b66742..18a2e0654c 100644 --- a/lib/elixir/test/elixir/kernel/guard_test.exs +++ b/lib/elixir/test/elixir/kernel/guard_test.exs @@ -311,7 +311,7 @@ defmodule Kernel.GuardTest do end assert_compile_error( - "cannot invoke remote function :erlang\.is_record/2 inside guards", + "cannot invoke remote function :erlang\.is_record/2 inside a guard", fn -> defmodule IsRecord2Usage do defguard foo(rec) when :erlang.is_record(rec, :tag) @@ -320,7 +320,7 @@ defmodule Kernel.GuardTest do ) assert_compile_error( - "cannot invoke remote function :erlang\.is_record/3 inside guards", + "cannot invoke remote function :erlang\.is_record/3 inside a guard", fn -> defmodule IsRecord3Usage do defguard foo(rec) when :erlang.is_record(rec, :tag, 7) @@ -329,7 +329,7 @@ defmodule Kernel.GuardTest do ) assert_compile_error( - ~r"cannot invoke remote function :erlang\.\+\+/2 inside guards", + ~r"cannot invoke remote function :erlang\.\+\+/2 inside a guard", fn -> defmodule ListSubtractionUsage do defguard foo(list) when list ++ [] @@ -338,7 +338,7 @@ defmodule Kernel.GuardTest do ) assert_compile_error( - "cannot invoke remote function :erlang\.\-\-/2 inside guards", + "cannot invoke remote function :erlang\.\-\-/2 inside a guard", fn -> defmodule ListSubtractionUsage do defguard foo(list) when list -- [] @@ -419,7 +419,7 @@ defmodule Kernel.GuardTest do end) assert_compile_error( - "cannot invoke remote function in guards. " <> + "cannot invoke remote function inside a guard. " <> "If you want to do a map lookup instead, please remove parens from map.field()", fn -> defmodule MapDot do @@ -428,7 +428,7 @@ defmodule Kernel.GuardTest do end ) - assert_compile_error("cannot invoke remote function Module.fun/0 inside guards", fn -> + assert_compile_error("cannot invoke remote function Module.fun/0 inside a guard", fn -> defmodule MapDot do def map_dot(map) when Module.fun(), do: true end From 2030cc9d4402de843f972c14085bfdc010847ef7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 30 Jan 2024 20:21:23 +0100 Subject: [PATCH 0350/1886] Fix Rebar3 env var with spaces (#13303) --- lib/mix/lib/mix/rebar.ex | 4 ++-- lib/mix/lib/mix/tasks/deps.compile.ex | 2 +- lib/mix/test/mix/rebar_test.exs | 23 ++++++++++++++++++++++- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/lib/mix/lib/mix/rebar.ex b/lib/mix/lib/mix/rebar.ex index 0704fa4bf3..2a38dd36ee 100644 --- a/lib/mix/lib/mix/rebar.ex +++ b/lib/mix/lib/mix/rebar.ex @@ -36,7 +36,7 @@ defmodule Mix.Rebar do @doc """ Returns the path to the available `rebar` command. """ - # TODO: Remove on Elixir v1.18 because phx_new and other installers rely on it. + # TODO: Remove on Elixir v1.20 because phx_new and other installers rely on it. def rebar_cmd(:rebar) do Mix.shell().error("[warning] :rebar is no longer supported in Mix, falling back to :rebar3") rebar_cmd(:rebar3) @@ -218,7 +218,7 @@ defmodule Mix.Rebar do defp wrap_cmd(rebar) do cond do not match?({:win32, _}, :os.type()) -> - rebar + String.replace(rebar, " ", "\\ ") String.ends_with?(rebar, ".cmd") -> "\"#{String.replace(rebar, "/", "\\")}\"" diff --git a/lib/mix/lib/mix/tasks/deps.compile.ex b/lib/mix/lib/mix/tasks/deps.compile.ex index 0e7e3cf4d0..ed7d3173a6 100644 --- a/lib/mix/lib/mix/tasks/deps.compile.ex +++ b/lib/mix/lib/mix/tasks/deps.compile.ex @@ -207,7 +207,7 @@ defmodule Mix.Tasks.Deps.Compile do {"TERM", "dumb"} ] - cmd = "#{escape_path(rebar_cmd(dep))} bare compile --paths #{escape_path(lib_path)}" + cmd = "#{rebar_cmd(dep)} bare compile --paths #{escape_path(lib_path)}" do_command(dep, config, cmd, false, env) # Check if we have any new symlinks after compilation diff --git a/lib/mix/test/mix/rebar_test.exs b/lib/mix/test/mix/rebar_test.exs index a5154b87a8..c187e2d604 100644 --- a/lib/mix/test/mix/rebar_test.exs +++ b/lib/mix/test/mix/rebar_test.exs @@ -219,7 +219,7 @@ defmodule Mix.RebarTest do # We run only on Unix because Windows has a hard time # removing the Rebar executable after executed. @tag :unix - test "applies variables from :system_env option when compiling dependencies" do + test "applies variables from :system_env option on config/compilation" do in_tmp("applies variables from system_env", fn -> Mix.Project.push(RebarAsDepWithEnv) @@ -233,6 +233,27 @@ defmodule Mix.RebarTest do end) end + # We run only on Unix because Windows has a hard time + # removing the Rebar executable after executed. + @tag :unix + test "gets and compiles dependencies with MIX_REBAR3 with spaces" do + in_tmp("rebar3 env with spaces", fn -> + File.cp!(Mix.Rebar.local_rebar_path(:rebar3), "rebar3") + System.put_env("MIX_REBAR3", Path.absname("rebar3")) + assert Mix.Rebar.rebar_cmd(:rebar3) =~ " " + + Mix.Project.push(RebarAsDep) + Mix.Tasks.Deps.Get.run([]) + assert_received {:mix_shell, :info, ["* Getting git_rebar " <> _]} + + Mix.Tasks.Deps.Compile.run([]) + assert_received {:mix_shell, :run, ["===> Compiling git_rebar\n"]} + assert_received {:mix_shell, :run, ["===> Compiling rebar_dep\n"]} + end) + after + System.delete_env("MIX_REBAR3") + end + test "gets and compiles dependencies with Mix" do in_tmp("get and compile dependencies with Mix", fn -> Mix.Project.push(RebarAsDep) From f48da2c1ff18d68118389fc48432af6bd9cab8b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 30 Jan 2024 20:36:00 +0100 Subject: [PATCH 0351/1886] Remove duplicate config --- lib/mix/test/mix/tasks/compile.elixir_test.exs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/lib/mix/test/mix/tasks/compile.elixir_test.exs b/lib/mix/test/mix/tasks/compile.elixir_test.exs index c0c954b279..52bbc4bf65 100644 --- a/lib/mix/test/mix/tasks/compile.elixir_test.exs +++ b/lib/mix/test/mix/tasks/compile.elixir_test.exs @@ -127,11 +127,6 @@ defmodule Mix.Tasks.Compile.ElixirTest do Process.put({MixTest.Case.Sample, :application}, extra_applications: [:logger]) File.mkdir_p!("config") - File.write!("config/config.exs", """ - import Config - config :logger, :level, :debug - """) - File.write!("lib/a.ex", """ defmodule A do _ = Logger.metadata() From d85f86a4ae58cea1b1cc4a8d0934681998a19fe7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 30 Jan 2024 21:15:48 +0100 Subject: [PATCH 0352/1886] Fix timing CI issues (#13305) --- .../test/mix/tasks/compile.elixir_test.exs | 44 ++----------------- 1 file changed, 4 insertions(+), 40 deletions(-) diff --git a/lib/mix/test/mix/tasks/compile.elixir_test.exs b/lib/mix/test/mix/tasks/compile.elixir_test.exs index 52bbc4bf65..61f57866c9 100644 --- a/lib/mix/test/mix/tasks/compile.elixir_test.exs +++ b/lib/mix/test/mix/tasks/compile.elixir_test.exs @@ -140,7 +140,7 @@ defmodule Mix.Tasks.Compile.ElixirTest do recompile = fn -> Mix.ProjectStack.pop() Mix.Project.push(MixTest.Case.Sample) - ensure_touched("config/config.exs") + ensure_touched("config/config.exs", "_build/dev/lib/sample/.mix/compile.elixir") Mix.Tasks.Loadconfig.load_compile("config/config.exs") Mix.Tasks.Compile.Elixir.run(["--verbose"]) end @@ -151,11 +151,9 @@ defmodule Mix.Tasks.Compile.ElixirTest do config :logger, :level, :debug """) - File.touch!("_build/dev/lib/sample/.mix/compile.elixir", @old_time) assert recompile.() == {:ok, []} assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]} refute_received {:mix_shell, :info, ["Compiled lib/b.ex"]} - assert File.stat!("_build/dev/lib/sample/.mix/compile.elixir").mtime > @old_time # Changing config recompiles File.write!("config/config.exs", """ @@ -163,25 +161,20 @@ defmodule Mix.Tasks.Compile.ElixirTest do config :logger, :level, :info """) - File.touch!("_build/dev/lib/sample/.mix/compile.elixir", @old_time) assert recompile.() == {:ok, []} assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]} refute_received {:mix_shell, :info, ["Compiled lib/b.ex"]} - assert File.stat!("_build/dev/lib/sample/.mix/compile.elixir").mtime > @old_time # Removing config recompiles File.write!("config/config.exs", """ import Config """) - File.touch!("_build/dev/lib/sample/.mix/compile.elixir", @old_time) assert recompile.() == {:ok, []} assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]} refute_received {:mix_shell, :info, ["Compiled lib/b.ex"]} - assert File.stat!("_build/dev/lib/sample/.mix/compile.elixir").mtime > @old_time # No-op does not recompile - File.touch!("_build/dev/lib/sample/.mix/compile.elixir", @old_time) assert recompile.() == {:ok, []} refute_received {:mix_shell, :info, ["Compiled lib/a.ex"]} refute_received {:mix_shell, :info, ["Compiled lib/b.ex"]} @@ -192,11 +185,9 @@ defmodule Mix.Tasks.Compile.ElixirTest do config :sample, :foo, :bar """) - File.touch!("_build/dev/lib/sample/.mix/compile.elixir", @old_time) assert recompile.() == {:ok, []} assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]} assert_received {:mix_shell, :info, ["Compiled lib/b.ex"]} - assert File.stat!("_build/dev/lib/sample/.mix/compile.elixir").mtime > @old_time # Changing an unknown dependency returns :ok but does not recompile File.write!("config/config.exs", """ @@ -204,12 +195,6 @@ defmodule Mix.Tasks.Compile.ElixirTest do config :sample, :foo, :bar config :unknown, :unknown, :unknown """) - - # We use ensure_touched because an outdated manifest would recompile anyway. - ensure_touched("config/config.exs", "_build/dev/lib/sample/.mix/compile.elixir") - assert recompile.() == {:ok, []} - refute_received {:mix_shell, :info, ["Compiled lib/a.ex"]} - refute_received {:mix_shell, :info, ["Compiled lib/b.ex"]} end) after Application.delete_env(:sample, :foo, persistent: true) @@ -271,7 +256,7 @@ defmodule Mix.Tasks.Compile.ElixirTest do recompile = fn -> Mix.ProjectStack.pop() Mix.Project.push(MixTest.Case.Sample) - ensure_touched("config/config.exs") + ensure_touched("config/config.exs", "_build/dev/lib/sample/.mix/compile.elixir") Mix.Tasks.Loadconfig.load_compile("config/config.exs") Mix.Tasks.Compile.Elixir.run(["--verbose"]) end @@ -282,11 +267,9 @@ defmodule Mix.Tasks.Compile.ElixirTest do config :ex_unit, :some, :config """) - File.touch!("_build/dev/lib/sample/.mix/compile.elixir", @old_time) assert recompile.() == {:ok, []} assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]} refute_received {:mix_shell, :info, ["Compiled lib/b.ex"]} - assert File.stat!("_build/dev/lib/sample/.mix/compile.elixir").mtime > @old_time # Changing config recompiles File.write!("config/config.exs", """ @@ -294,25 +277,20 @@ defmodule Mix.Tasks.Compile.ElixirTest do config :ex_unit, :some, :another """) - File.touch!("_build/dev/lib/sample/.mix/compile.elixir", @old_time) assert recompile.() == {:ok, []} assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]} refute_received {:mix_shell, :info, ["Compiled lib/b.ex"]} - assert File.stat!("_build/dev/lib/sample/.mix/compile.elixir").mtime > @old_time # Removing config recompiles File.write!("config/config.exs", """ import Config """) - File.touch!("_build/dev/lib/sample/.mix/compile.elixir", @old_time) assert recompile.() == {:ok, []} assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]} refute_received {:mix_shell, :info, ["Compiled lib/b.ex"]} - assert File.stat!("_build/dev/lib/sample/.mix/compile.elixir").mtime > @old_time # No-op does not recompile - File.touch!("_build/dev/lib/sample/.mix/compile.elixir", @old_time) assert recompile.() == {:ok, []} refute_received {:mix_shell, :info, ["Compiled lib/a.ex"]} refute_received {:mix_shell, :info, ["Compiled lib/b.ex"]} @@ -341,7 +319,7 @@ defmodule Mix.Tasks.Compile.ElixirTest do recompile = fn -> Mix.ProjectStack.pop() Mix.Project.push(MixTest.Case.Sample) - ensure_touched("config/config.exs") + ensure_touched("config/config.exs", "_build/dev/lib/sample/.mix/compile.elixir") Mix.Tasks.Loadconfig.load_compile("config/config.exs") Mix.Tasks.Compile.Elixir.run(["--verbose"]) end @@ -352,11 +330,9 @@ defmodule Mix.Tasks.Compile.ElixirTest do config :logger, :compile_time_purge_matching, [] """) - File.touch!("_build/dev/lib/sample/.mix/compile.elixir", @old_time) assert recompile.() == {:ok, []} assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]} refute_received {:mix_shell, :info, ["Compiled lib/b.ex"]} - assert File.stat!("_build/dev/lib/sample/.mix/compile.elixir").mtime > @old_time # Inserting a bogus config should crash File.write!("config/config.exs", """ @@ -364,7 +340,6 @@ defmodule Mix.Tasks.Compile.ElixirTest do config :logger, :compile_time_purge_matching, [level_lower_than: :debug] """) - File.touch!("_build/dev/lib/sample/.mix/compile.elixir", @old_time) ExUnit.CaptureIO.capture_io(:stderr, fn -> assert {:error, _} = recompile.() end) # Revering the original config should recompile @@ -373,11 +348,9 @@ defmodule Mix.Tasks.Compile.ElixirTest do config :logger, :compile_time_purge_matching, [] """) - File.touch!("_build/dev/lib/sample/.mix/compile.elixir", @old_time) assert recompile.() == {:ok, []} assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]} refute_received {:mix_shell, :info, ["Compiled lib/b.ex"]} - assert File.stat!("_build/dev/lib/sample/.mix/compile.elixir").mtime > @old_time end) after Application.put_env(:logger, :compile_time_purge_matching, []) @@ -401,7 +374,7 @@ defmodule Mix.Tasks.Compile.ElixirTest do recompile = fn -> Mix.ProjectStack.pop() Mix.Project.push(MixTest.Case.Sample) - ensure_touched("config/config.exs") + ensure_touched("config/config.exs", "_build/dev/lib/sample/.mix/compile.elixir") Mix.Tasks.Loadconfig.load_compile("config/config.exs") Mix.Tasks.Compile.Elixir.run(["--verbose"]) end @@ -412,11 +385,9 @@ defmodule Mix.Tasks.Compile.ElixirTest do config :logger, :level, :debug """) - File.touch!("_build/dev/lib/sample/.mix/compile.elixir", @old_time) assert recompile.() == {:ok, []} assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]} refute_received {:mix_shell, :info, ["Compiled lib/b.ex"]} - assert File.stat!("_build/dev/lib/sample/.mix/compile.elixir").mtime > @old_time # Changing config recompiles File.write!("config/config.exs", """ @@ -424,22 +395,18 @@ defmodule Mix.Tasks.Compile.ElixirTest do config :logger, :level, :info """) - File.touch!("_build/dev/lib/sample/.mix/compile.elixir", @old_time) assert recompile.() == {:ok, []} assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]} refute_received {:mix_shell, :info, ["Compiled lib/b.ex"]} - assert File.stat!("_build/dev/lib/sample/.mix/compile.elixir").mtime > @old_time # Removing config recompiles File.write!("config/config.exs", """ import Config """) - File.touch!("_build/dev/lib/sample/.mix/compile.elixir", @old_time) assert recompile.() == {:ok, []} assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]} refute_received {:mix_shell, :info, ["Compiled lib/b.ex"]} - assert File.stat!("_build/dev/lib/sample/.mix/compile.elixir").mtime > @old_time # Changing self fully recompiles File.write!("config/config.exs", """ @@ -447,11 +414,9 @@ defmodule Mix.Tasks.Compile.ElixirTest do config :sample, :foo, :bar """) - File.touch!("_build/dev/lib/sample/.mix/compile.elixir", @old_time) assert recompile.() == {:ok, []} assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]} assert_received {:mix_shell, :info, ["Compiled lib/b.ex"]} - assert File.stat!("_build/dev/lib/sample/.mix/compile.elixir").mtime > @old_time # Changing an unknown dependency returns :ok but does not recompile File.write!("config/config.exs", """ @@ -461,7 +426,6 @@ defmodule Mix.Tasks.Compile.ElixirTest do """) # We use ensure_touched because an outdated manifest would recompile anyway. - ensure_touched("config/config.exs", "_build/dev/lib/sample/.mix/compile.elixir") assert recompile.() == {:ok, []} refute_received {:mix_shell, :info, ["Compiled lib/a.ex"]} refute_received {:mix_shell, :info, ["Compiled lib/b.ex"]} From 7ac64110f556edc67af7af61416d4124a4809e24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 31 Jan 2024 10:12:50 +0100 Subject: [PATCH 0353/1886] Fix autocompletion on Erlang/OTP 26, closes #13307 --- lib/iex/lib/iex/autocomplete.ex | 4 +--- lib/iex/test/iex/autocomplete_test.exs | 7 ++----- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/lib/iex/lib/iex/autocomplete.ex b/lib/iex/lib/iex/autocomplete.ex index 084738fef9..89b2c69add 100644 --- a/lib/iex/lib/iex/autocomplete.ex +++ b/lib/iex/lib/iex/autocomplete.ex @@ -197,9 +197,7 @@ defmodule IEx.Autocomplete do end defp expand_signatures([_ | _] = signatures, _shell) do - [head | tail] = Enum.sort(signatures, &(String.length(&1) <= String.length(&2))) - if tail != [], do: IO.write("\n" <> (tail |> Enum.reverse() |> Enum.join("\n"))) - yes("", [head]) + yes("", Enum.sort_by(signatures, &String.length/1)) end defp expand_signatures([], shell), do: expand_local_or_var("", shell) diff --git a/lib/iex/test/iex/autocomplete_test.exs b/lib/iex/test/iex/autocomplete_test.exs index 2720a43327..0827daf82e 100644 --- a/lib/iex/test/iex/autocomplete_test.exs +++ b/lib/iex/test/iex/autocomplete_test.exs @@ -492,11 +492,8 @@ defmodule IEx.AutocompleteTest do eval("import Enum; import Protocol") - assert ExUnit.CaptureIO.capture_io(fn -> - send(self(), expand(~c"reduce(")) - end) == "\nreduce(enumerable, acc, fun)" - - assert_received {:yes, ~c"", [~c"reduce(enumerable, fun)"]} + assert expand(~c"reduce(") == + {:yes, ~c"", [~c"reduce(enumerable, fun)", ~c"reduce(enumerable, acc, fun)"]} assert expand(~c"take(") == {:yes, ~c"", [~c"take(enumerable, amount)"]} assert expand(~c"derive(") == {:yes, ~c"", [~c"derive(protocol, module, options \\\\ [])"]} From c54ff0e25d9e990c665f7a27208827c9bc29c282 Mon Sep 17 00:00:00 2001 From: Jean Klingler Date: Wed, 31 Jan 2024 19:56:47 +0900 Subject: [PATCH 0354/1886] Validate options in async_stream* functions (#13289) --- lib/elixir/lib/task.ex | 2 + lib/elixir/lib/task/supervised.ex | 58 ++++++++++++------- lib/elixir/lib/task/supervisor.ex | 9 ++- .../test/elixir/task/supervisor_test.exs | 8 +++ lib/elixir/test/elixir/task_test.exs | 14 ++++- 5 files changed, 69 insertions(+), 22 deletions(-) diff --git a/lib/elixir/lib/task.ex b/lib/elixir/lib/task.ex index 56bd80e892..e043f6a07a 100644 --- a/lib/elixir/lib/task.ex +++ b/lib/elixir/lib/task.ex @@ -717,6 +717,8 @@ defmodule Task do end defp build_stream(enumerable, fun, options) do + options = Task.Supervised.validate_stream_options(options) + fn acc, acc_fun -> owner = get_owner(self()) diff --git a/lib/elixir/lib/task/supervised.ex b/lib/elixir/lib/task/supervised.ex index b41a862953..a15eb355c2 100644 --- a/lib/elixir/lib/task/supervised.ex +++ b/lib/elixir/lib/task/supervised.ex @@ -189,18 +189,36 @@ defmodule Task.Supervised do ## Stream - def stream(enumerable, acc, reducer, callers, mfa, options, spawn) do - next = &Enumerable.reduce(enumerable, &1, fn x, acc -> {:suspend, [x | acc]} end) - max_concurrency = Keyword.get(options, :max_concurrency, System.schedulers_online()) + def validate_stream_options(options) do + max_concurrency = Keyword.get_lazy(options, :max_concurrency, &System.schedulers_online/0) + on_timeout = Keyword.get(options, :on_timeout, :exit) + timeout = Keyword.get(options, :timeout, 5000) + ordered = Keyword.get(options, :ordered, true) + zip_input_on_exit = Keyword.get(options, :zip_input_on_exit, false) unless is_integer(max_concurrency) and max_concurrency > 0 do raise ArgumentError, ":max_concurrency must be an integer greater than zero" end - ordered? = Keyword.get(options, :ordered, true) - timeout = Keyword.get(options, :timeout, 5000) - on_timeout = Keyword.get(options, :on_timeout, :exit) - zip_input_on_exit? = Keyword.get(options, :zip_input_on_exit, false) + unless on_timeout in [:exit, :kill_task] do + raise ArgumentError, ":on_timeout must be either :exit or :kill_task" + end + + unless (is_integer(timeout) and timeout >= 0) or timeout == :infinity do + raise ArgumentError, ":timeout must be either a positive integer or :infinity" + end + + %{ + max_concurrency: max_concurrency, + on_timeout: on_timeout, + timeout: timeout, + ordered: ordered, + zip_input_on_exit: zip_input_on_exit + } + end + + def stream(enumerable, acc, reducer, callers, mfa, options, spawn) when is_map(options) do + next = &Enumerable.reduce(enumerable, &1, fn x, acc -> {:suspend, [x | acc]} end) parent = self() {:trap_exit, trap_exit?} = Process.info(self(), :trap_exit) @@ -212,7 +230,7 @@ defmodule Task.Supervised do {monitor_pid, monitor_ref} = Process.spawn( - fn -> stream_monitor(parent, spawn, trap_exit?, timeout) end, + fn -> stream_monitor(parent, spawn, trap_exit?, options.timeout) end, spawn_opts ) @@ -221,21 +239,21 @@ defmodule Task.Supervised do # about our reference to it. send(monitor_pid, {parent, monitor_ref}) - config = %{ - reducer: reducer, - monitor_pid: monitor_pid, - monitor_ref: monitor_ref, - ordered: ordered?, - timeout: timeout, - on_timeout: on_timeout, - zip_input_on_exit: zip_input_on_exit?, - callers: callers, - mfa: mfa - } + config = + Map.merge( + options, + %{ + reducer: reducer, + monitor_pid: monitor_pid, + monitor_ref: monitor_ref, + callers: callers, + mfa: mfa + } + ) stream_reduce( acc, - max_concurrency, + options.max_concurrency, _spawned = 0, _delivered = 0, _waiting = %{}, diff --git a/lib/elixir/lib/task/supervisor.ex b/lib/elixir/lib/task/supervisor.ex index 6e20449af4..9b84d0fe88 100644 --- a/lib/elixir/lib/task/supervisor.ex +++ b/lib/elixir/lib/task/supervisor.ex @@ -592,8 +592,15 @@ defmodule Task.Supervisor do end defp build_stream(supervisor, link_type, enumerable, fun, options) do + shutdown = Keyword.get(options, :shutdown, 5000) + + unless (is_integer(shutdown) and shutdown >= 0) or shutdown == :brutal_kill do + raise ArgumentError, ":shutdown must be either a positive integer or :brutal_kill" + end + + options = Task.Supervised.validate_stream_options(options) + fn acc, acc_fun -> - shutdown = options[:shutdown] owner = get_owner(self()) Task.Supervised.stream(enumerable, acc, acc_fun, get_callers(self()), fun, options, fn -> diff --git a/lib/elixir/test/elixir/task/supervisor_test.exs b/lib/elixir/test/elixir/task/supervisor_test.exs index 3656f573c9..8b9fa8d34f 100644 --- a/lib/elixir/test/elixir/task/supervisor_test.exs +++ b/lib/elixir/test/elixir/task/supervisor_test.exs @@ -477,6 +477,14 @@ defmodule Task.SupervisorTest do |> Task.Supervisor.async_stream(1..8, &exit/1, opts) |> Enum.take(4) == [exit: {1, 1}, exit: {2, 2}, exit: {3, 3}, exit: {4, 4}] end + + test "does not allow streaming with invalid :shutdown", %{supervisor: supervisor} do + message = ":shutdown must be either a positive integer or :brutal_kill" + + assert_raise ArgumentError, message, fn -> + Task.Supervisor.async_stream(supervisor, [], fn _ -> :ok end, shutdown: :unknown) + end + end end describe "async_stream_nolink" do diff --git a/lib/elixir/test/elixir/task_test.exs b/lib/elixir/test/elixir/task_test.exs index f5ab6f55b8..afe4dfe8d7 100644 --- a/lib/elixir/test/elixir/task_test.exs +++ b/lib/elixir/test/elixir/task_test.exs @@ -869,7 +869,19 @@ defmodule TaskTest do test "does not allow streaming with max_concurrency = 0" do assert_raise ArgumentError, ":max_concurrency must be an integer greater than zero", fn -> - Task.async_stream([1], fn _ -> :ok end, max_concurrency: 0) |> Enum.to_list() + Task.async_stream([1], fn _ -> :ok end, max_concurrency: 0) + end + end + + test "does not allow streaming with invalid :on_timeout" do + assert_raise ArgumentError, ":on_timeout must be either :exit or :kill_task", fn -> + Task.async_stream([1], fn _ -> :ok end, on_timeout: :unknown) + end + end + + test "does not allow streaming with invalid :timeout" do + assert_raise ArgumentError, ":timeout must be either a positive integer or :infinity", fn -> + Task.async_stream([1], fn _ -> :ok end, timeout: :unknown) end end From 3b5bd6853c1558c157a45493415a2b9493e912d2 Mon Sep 17 00:00:00 2001 From: Jean Klingler Date: Wed, 31 Jan 2024 20:01:54 +0900 Subject: [PATCH 0355/1886] Add @type async_stream_option to Task and Task.Supervisor (#13306) * Add @type async_stream_option to Task and Task.Supervisor * Update lib/elixir/lib/task.ex * Update lib/elixir/lib/task/supervisor.ex --------- Co-authored-by: Andrea Leopardi --- lib/elixir/lib/task.ex | 16 +++++++++++++-- lib/elixir/lib/task/supervisor.ex | 34 ++++++++++++++++++++++++------- 2 files changed, 41 insertions(+), 9 deletions(-) diff --git a/lib/elixir/lib/task.ex b/lib/elixir/lib/task.ex index e043f6a07a..f1ff981df0 100644 --- a/lib/elixir/lib/task.ex +++ b/lib/elixir/lib/task.ex @@ -293,6 +293,17 @@ defmodule Task do """ @opaque ref :: reference() + @typedoc """ + Options given to `async_stream` functions. + """ + @typedoc since: "1.17.0" + @type async_stream_option :: + {:max_concurrency, pos_integer()} + | {:ordered, boolean()} + | {:timeout, timeout()} + | {:on_timeout, :exit | :kill_task} + | {:zip_input_on_exit, boolean()} + defguardp is_timeout(timeout) when timeout == :infinity or (is_integer(timeout) and timeout >= 0) @@ -681,7 +692,8 @@ defmodule Task do example above. """ @doc since: "1.4.0" - @spec async_stream(Enumerable.t(), module, atom, [term], keyword) :: Enumerable.t() + @spec async_stream(Enumerable.t(), module, atom, [term], [async_stream_option]) :: + Enumerable.t() def async_stream(enumerable, module, function_name, args, options \\ []) when is_atom(module) and is_atom(function_name) and is_list(args) do build_stream(enumerable, {module, function_name, args}, options) @@ -710,7 +722,7 @@ defmodule Task do See `async_stream/5` for discussion, options, and more examples. """ @doc since: "1.4.0" - @spec async_stream(Enumerable.t(), (term -> term), keyword) :: Enumerable.t() + @spec async_stream(Enumerable.t(), (term -> term), [async_stream_option]) :: Enumerable.t() def async_stream(enumerable, fun, options \\ []) when is_function(fun, 1) and is_list(options) do build_stream(enumerable, fun, options) diff --git a/lib/elixir/lib/task/supervisor.ex b/lib/elixir/lib/task/supervisor.ex index 9b84d0fe88..ec8459c582 100644 --- a/lib/elixir/lib/task/supervisor.ex +++ b/lib/elixir/lib/task/supervisor.ex @@ -77,6 +77,12 @@ defmodule Task.Supervisor do DynamicSupervisor.option() | DynamicSupervisor.init_option() + @typedoc """ + Options given to `async_stream` and `async_stream_nolink` functions. + """ + @typedoc since: "1.17.0" + @type async_stream_option :: Task.async_stream_option() | {:shutdown, Supervisor.shutdown()} + @doc false def child_spec(opts) when is_list(opts) do id = @@ -356,8 +362,14 @@ defmodule Task.Supervisor do """ @doc since: "1.4.0" - @spec async_stream(Supervisor.supervisor(), Enumerable.t(), module, atom, [term], keyword) :: - Enumerable.t() + @spec async_stream( + Supervisor.supervisor(), + Enumerable.t(), + module, + atom, + [term], + [async_stream_option] + ) :: Enumerable.t() def async_stream(supervisor, enumerable, module, function, args, options \\ []) when is_atom(module) and is_atom(function) and is_list(args) do build_stream(supervisor, :link, enumerable, {module, function, args}, options) @@ -374,8 +386,12 @@ defmodule Task.Supervisor do See `async_stream/6` for discussion, options, and examples. """ @doc since: "1.4.0" - @spec async_stream(Supervisor.supervisor(), Enumerable.t(), (term -> term), keyword) :: - Enumerable.t() + @spec async_stream( + Supervisor.supervisor(), + Enumerable.t(), + (term -> term), + [async_stream_option] + ) :: Enumerable.t() def async_stream(supervisor, enumerable, fun, options \\ []) when is_function(fun, 1) do build_stream(supervisor, :link, enumerable, fun, options) end @@ -397,7 +413,7 @@ defmodule Task.Supervisor do module, atom, [term], - keyword + [async_stream_option] ) :: Enumerable.t() def async_stream_nolink(supervisor, enumerable, module, function, args, options \\ []) when is_atom(module) and is_atom(function) and is_list(args) do @@ -448,8 +464,12 @@ defmodule Task.Supervisor do """ @doc since: "1.4.0" - @spec async_stream_nolink(Supervisor.supervisor(), Enumerable.t(), (term -> term), keyword) :: - Enumerable.t() + @spec async_stream_nolink( + Supervisor.supervisor(), + Enumerable.t(), + (term -> term), + [async_stream_option] + ) :: Enumerable.t() def async_stream_nolink(supervisor, enumerable, fun, options \\ []) when is_function(fun, 1) do build_stream(supervisor, :nolink, enumerable, fun, options) end From 760ce065399523f5b147cf256f0c9b14f8259211 Mon Sep 17 00:00:00 2001 From: Marcelo Dominguez Date: Wed, 31 Jan 2024 10:10:06 -0300 Subject: [PATCH 0356/1886] Add more examples in DateTime.diff docs (#13308) --- lib/elixir/lib/calendar/datetime.ex | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/elixir/lib/calendar/datetime.ex b/lib/elixir/lib/calendar/datetime.ex index 396f4984e5..119bf71a08 100644 --- a/lib/elixir/lib/calendar/datetime.ex +++ b/lib/elixir/lib/calendar/datetime.ex @@ -1494,6 +1494,11 @@ defmodule DateTime do ## Examples + iex> DateTime.diff(~U[2024-01-15 10:00:10Z], ~U[2024-01-15 10:00:00Z]) + 10 + + This function also considers timezone offsets: + iex> dt1 = %DateTime{year: 2000, month: 2, day: 29, zone_abbr: "AMT", ...> hour: 23, minute: 0, second: 7, microsecond: {0, 0}, ...> utc_offset: -14400, std_offset: 0, time_zone: "America/Manaus"} From 0a144ec43d421d80fe992ed643290fc2fcf2650e Mon Sep 17 00:00:00 2001 From: Daniel Jaouen Date: Wed, 31 Jan 2024 11:26:16 -0500 Subject: [PATCH 0357/1886] Add "c" alias to IEx.Helpers (#13309) Co-authored-by: Daniel Jaouen --- lib/iex/lib/iex/helpers.ex | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/iex/lib/iex/helpers.ex b/lib/iex/lib/iex/helpers.ex index 84933f8e83..956efac4fd 100644 --- a/lib/iex/lib/iex/helpers.ex +++ b/lib/iex/lib/iex/helpers.ex @@ -985,6 +985,14 @@ defmodule IEx.Helpers do next() end + @doc """ + A shortcut for `continue/0`. + """ + @doc since: "1.17.0" + def c do + continue() + end + @doc """ Sets up a breakpoint in the AST of shape `Module.function/arity` with the given number of `stops`. From ecfc82ce63716407643fdb888f0170209a8dd780 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 1 Feb 2024 12:38:11 +0100 Subject: [PATCH 0358/1886] Preserve . semantics in Path.relative_to, closes #13310 --- lib/elixir/lib/path.ex | 2 ++ lib/elixir/test/elixir/path_test.exs | 2 ++ 2 files changed, 4 insertions(+) diff --git a/lib/elixir/lib/path.ex b/lib/elixir/lib/path.ex index e2f8469f80..7dca45f018 100644 --- a/lib/elixir/lib/path.ex +++ b/lib/elixir/lib/path.ex @@ -433,6 +433,8 @@ defmodule Path do defp relative_to_unforced(_, _, original), do: join(original) defp relative_to_forced(path, path, _original), do: "." + defp relative_to_forced(["."], _path, _original), do: "." + defp relative_to_forced(path, ["."], _original), do: join(path) defp relative_to_forced([h | t1], [h | t2], original), do: relative_to_forced(t1, t2, original) # this should only happen if we have two paths on different drives on windows diff --git a/lib/elixir/test/elixir/path_test.exs b/lib/elixir/test/elixir/path_test.exs index 99c43a48bc..dd775c707b 100644 --- a/lib/elixir/test/elixir/path_test.exs +++ b/lib/elixir/test/elixir/path_test.exs @@ -316,6 +316,8 @@ defmodule PathTest do assert Path.relative_to("./foo/../bar/..", File.cwd!()) == "." # both relative + assert Path.relative_to("usr/local/foo", ".") == "usr/local/foo" + assert Path.relative_to(".", "usr/local/foo") == "." assert Path.relative_to("usr/local/foo", "usr/local") == "foo" assert Path.relative_to("usr/local/foo", "etc") == "../usr/local/foo" assert Path.relative_to(~c"usr/local/foo", "etc") == "../usr/local/foo" From 1f7d18e0054cfb17cd2967648163eadf9fa5e5df Mon Sep 17 00:00:00 2001 From: felipe stival <14948182+v0idpwn@users.noreply.github.com> Date: Thu, 1 Feb 2024 15:29:21 -0300 Subject: [PATCH 0359/1886] Fix typo and wording in Config docs (#13312) --- lib/elixir/lib/config.ex | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/elixir/lib/config.ex b/lib/elixir/lib/config.ex index abde56cb1d..728a2b2cbd 100644 --- a/lib/elixir/lib/config.ex +++ b/lib/elixir/lib/config.ex @@ -82,9 +82,9 @@ defmodule Config do ... end - The only files where you may access functions from the `Mix` module are - the `mix.exs` file and inside custom Mix tasks, which always within the - `Mix.Tasks` namespace. + The only places where you may access functions from the `Mix` module are + the `mix.exs` file and inside custom Mix tasks, which are always within + the `Mix.Tasks` namespace. ## `config/runtime.exs` From cd12b152387d98293f81340f9dd8462db7ec12a9 Mon Sep 17 00:00:00 2001 From: Steve Johns Date: Thu, 1 Feb 2024 19:13:17 +0000 Subject: [PATCH 0360/1886] Clarify "shorthands" in type specifications (#13311) --- lib/elixir/pages/references/typespecs.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/pages/references/typespecs.md b/lib/elixir/pages/references/typespecs.md index 8875469d4f..e79fc66bc8 100644 --- a/lib/elixir/pages/references/typespecs.md +++ b/lib/elixir/pages/references/typespecs.md @@ -44,7 +44,7 @@ In the example above: ## Types and their syntax -The syntax Elixir provides for type specifications is similar to [the one in Erlang](https://www.erlang.org/doc/reference_manual/typespec.html). Most of the built-in types provided in Erlang (for example, `pid()`) are expressed in the same way: `pid()` (or simply `pid`). Parameterized types (such as `list(integer)`) are supported as well and so are remote types (such as [`Enum.t()`](`t:Enum.t/0`)). Integers and atom literals are allowed as types (for example, `1`, `:atom`, or `false`). All other types are built out of unions of predefined types. Some shorthands are allowed, such as `[...]`, `<<>>`, and `{...}`. +The syntax Elixir provides for type specifications is similar to [the one in Erlang](https://www.erlang.org/doc/reference_manual/typespec.html). Most of the built-in types provided in Erlang (for example, `pid()`) are expressed in the same way: `pid()` (or simply `pid`). Parameterized types (such as `list(integer)`) are supported as well and so are remote types (such as [`Enum.t()`](`t:Enum.t/0`)). Integers and atom literals are allowed as types (for example, `1`, `:atom`, or `false`). All other types are built out of unions of predefined types. Some types can also be declared using their syntactical notation, such as `[type]` for lists, `{type1, type2, ...}` for tuples and `<<_ * _>>` for binaries. The notation to represent the union of types is the pipe `|`. For example, the typespec `type :: atom() | pid() | tuple()` creates a type `type` that can be either an `atom`, a `pid`, or a `tuple`. This is usually called a [sum type](https://en.wikipedia.org/wiki/Tagged_union) in other languages From c3ed7bb2e5c2b3c7b48599fa58f26c8674997642 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 2 Feb 2024 15:28:51 +0100 Subject: [PATCH 0361/1886] Improve Logger docs, closes #13313 --- lib/logger/lib/logger.ex | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/lib/logger/lib/logger.ex b/lib/logger/lib/logger.ex index fff2c4dc8d..5432913cea 100644 --- a/lib/logger/lib/logger.ex +++ b/lib/logger/lib/logger.ex @@ -326,13 +326,14 @@ defmodule Logger do ## Erlang/OTP handlers Handlers represent the ability to integrate into the logging system to - handle each logged message/event. Elixir automatically configures the - default handler, but you can use Erlang's [`:logger`](`:logger`) module - to add other handlers too. + handle each logged message/event. Elixir's Logger automatically sets a + default handler based on Erlang's `:logger_std_h`, which you can configure + using the `:default_handler` boot configuration outlined above. You may + also attach additional handlers when you boot your application. - Erlang/OTP handlers must be listed under your own application. - For example, to setup an additional handler, so you write to - console and file: + To do so, you must list a series of handlers under the `:logger` key + of your application configuration. For example, to setup an additional + handler that writes to a file: config :my_app, :logger, [ {:handler, :file_log, :logger_std_h, %{ @@ -354,7 +355,10 @@ defmodule Logger do Logger.add_handlers(:my_app) - You can also develop your own handlers. Handlers run in the same + You can also add, remove, and update handlers at runtime with the help + of the Erlang's [`:logger`](`:logger`) module. + + You may also develop your own handlers. Handlers run in the same process as the process logging the message/event. This gives developers flexibility but they should avoid performing any long running action in such handlers, as it may slow down the action being executed considerably. @@ -368,7 +372,7 @@ defmodule Logger do ### Filtering - You can add filters to Erlang's `:logger`. For example, to filter out logs + You can add filters to any handler. For example, to filter out logs that contain a particular string, you could create a module: defmodule LogFilter do From eb1499ac297f6a026d2636d05969f187c0a2f277 Mon Sep 17 00:00:00 2001 From: Mitchell Hanberg Date: Mon, 5 Feb 2024 03:40:16 -0500 Subject: [PATCH 0362/1886] Include from_brackets metadata in all cases (#13317) --- lib/elixir/src/elixir_parser.yrl | 6 ++-- .../test/elixir/kernel/tracers_test.exs | 29 +++++++++++++++++++ 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/lib/elixir/src/elixir_parser.yrl b/lib/elixir/src/elixir_parser.yrl index ce6afd3217..f2495e93b3 100644 --- a/lib/elixir/src/elixir_parser.yrl +++ b/lib/elixir/src/elixir_parser.yrl @@ -292,13 +292,13 @@ bracket_arg -> open_bracket container_expr close_bracket : build_access_arg('$1' bracket_arg -> open_bracket container_expr ',' close_bracket : build_access_arg('$1', '$2', '$4'). bracket_arg -> open_bracket container_expr ',' container_args close_bracket : error_too_many_access_syntax('$3'). -bracket_expr -> dot_bracket_identifier bracket_arg : build_access(build_no_parens('$1', nil), '$2'). +bracket_expr -> dot_bracket_identifier bracket_arg : build_access(build_no_parens('$1', nil), meta_with_from_brackets('$2')). bracket_expr -> access_expr bracket_arg : build_access('$1', meta_with_from_brackets('$2')). bracket_at_expr -> at_op_eol dot_bracket_identifier bracket_arg : - build_access(build_unary_op('$1', build_no_parens('$2', nil)), '$3'). + build_access(build_unary_op('$1', build_no_parens('$2', nil)), meta_with_from_brackets('$3')). bracket_at_expr -> at_op_eol access_expr bracket_arg : - build_access(build_unary_op('$1', '$2'), '$3'). + build_access(build_unary_op('$1', '$2'), meta_with_from_brackets('$3')). %% Blocks diff --git a/lib/elixir/test/elixir/kernel/tracers_test.exs b/lib/elixir/test/elixir/kernel/tracers_test.exs index 08826bf324..7374ea41ef 100644 --- a/lib/elixir/test/elixir/kernel/tracers_test.exs +++ b/lib/elixir/test/elixir/kernel/tracers_test.exs @@ -217,6 +217,35 @@ defmodule Kernel.TracersTest do assert meta[:from_interpolation] end + test "traces bracket access" do + compile_string(""" + foo = %{bar: 3} + foo[:bar] + """) + + assert_receive {{:remote_function, meta, Access, :get, 2}, _env} + assert meta[:from_brackets] + + compile_string(""" + defmodule Foo do + @foo %{bar: 3} + def a() do + @foo[:bar] + end + end + """) + + assert_receive {{:remote_function, meta, Access, :get, 2}, _env} + assert meta[:from_brackets] + + compile_string(""" + %{bar: 3}[:bar] + """) + + assert_receive {{:remote_function, meta, Access, :get, 2}, _env} + assert meta[:from_brackets] + end + """ # Make sure this module is compiled with column information defmodule MacroWithColumn do From fd4e6b530c0e010712b06909c89820b08e49c238 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 5 Feb 2024 15:13:31 +0100 Subject: [PATCH 0363/1886] Fix column marker for maps --- lib/elixir/src/elixir_parser.yrl | 13 ++++++++++++- lib/elixir/test/elixir/kernel/parser_test.exs | 15 +++++++++++---- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/lib/elixir/src/elixir_parser.yrl b/lib/elixir/src/elixir_parser.yrl index f2495e93b3..480fcf5c99 100644 --- a/lib/elixir/src/elixir_parser.yrl +++ b/lib/elixir/src/elixir_parser.yrl @@ -644,7 +644,7 @@ struct_expr -> at_op_eol struct_expr : build_unary_op('$1', '$2'). struct_expr -> unary_op_eol struct_expr : build_unary_op('$1', '$2'). struct_expr -> parens_call : '$1'. -map -> map_op map_args : '$2'. +map -> map_op map_args : adjust_map_column('$2'). map -> struct_op struct_expr map_args : {'%', meta_from_token('$1'), ['$2', '$3']}. map -> struct_op struct_expr eol map_args : {'%', meta_from_token('$1'), ['$2', '$4']}. @@ -778,6 +778,17 @@ build_map_update(Left, {Pipe, Struct, Map}, Right, Extra) -> Op = build_op(Struct, Pipe, append_non_empty(Map, Extra)), {'%{}', newlines_pair(Left, Right) ++ meta_from_token(Left), [Op]}. +adjust_map_column(Map) -> + case ?columns() of + true -> + {'%{}', Meta, Pairs} = Map, + UpdatedMeta = [{Key, if Key =:= column -> Value - 1; true -> Value end} || + {Key, Value} <- Meta], + {'%{}', UpdatedMeta, Pairs}; + false -> + Map + end. + %% Blocks build_block([{unquote_splicing, _, [_]}]=Exprs) -> diff --git a/lib/elixir/test/elixir/kernel/parser_test.exs b/lib/elixir/test/elixir/kernel/parser_test.exs index d5a077632e..ab517680b3 100644 --- a/lib/elixir/test/elixir/kernel/parser_test.exs +++ b/lib/elixir/test/elixir/kernel/parser_test.exs @@ -332,13 +332,21 @@ defmodule Kernel.ParserTest do nfc_abba = [225, 98, 98, 224] nfd_abba = [97, 769, 98, 98, 97, 768] context = [line: 1, column: 8] - expr = "'ábbà' = 1" + expr = "\"ábbà\" = 1" assert string_to_quoted.(String.normalize(expr, :nfc)) == - {:ok, {:=, context, [nfc_abba, 1]}} + {:ok, {:=, context, [List.to_string(nfc_abba), 1]}} assert string_to_quoted.(String.normalize(expr, :nfd)) == - {:ok, {:=, context, [nfd_abba, 1]}} + {:ok, {:=, context, [List.to_string(nfd_abba), 1]}} + end + + test "handles maps and structs" do + assert Code.string_to_quoted("%{}", columns: true) == + {:ok, {:%{}, [line: 1, column: 1], []}} + + assert Code.string_to_quoted("%:atom{}", columns: true) == + {:ok, {:%, [line: 1, column: 1], [:atom, {:%{}, [line: 1, column: 7], []}]}} end end @@ -411,7 +419,6 @@ defmodule Kernel.ParserTest do string_to_quoted = &Code.string_to_quoted!(&1, opts) assert string_to_quoted.(~s("one")) == {:__block__, [delimiter: "\"", line: 1], ["one"]} - assert string_to_quoted.("'one'") == {:__block__, [delimiter: "'", line: 1], [~c"one"]} assert string_to_quoted.("?é") == {:__block__, [token: "?é", line: 1], [233]} assert string_to_quoted.("0b10") == {:__block__, [token: "0b10", line: 1], [2]} assert string_to_quoted.("12") == {:__block__, [token: "12", line: 1], [12]} From ef0ce41d4db9dc2a1f2894d469a4fbd761f73676 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 6 Feb 2024 13:23:33 +0100 Subject: [PATCH 0364/1886] Remove unecessary line in code normalizer --- lib/elixir/lib/code/normalizer.ex | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/elixir/lib/code/normalizer.ex b/lib/elixir/lib/code/normalizer.ex index 6317eeeafa..d31e40a276 100644 --- a/lib/elixir/lib/code/normalizer.ex +++ b/lib/elixir/lib/code/normalizer.ex @@ -170,9 +170,6 @@ defmodule Code.Normalizer do right = normalize_map_args(right, state) [{:|, pipe_meta, [left, right]}] - [{_, _, _} = call] -> - [do_normalize(call, state)] - args -> normalize_map_args(args, state) end From d244eaf8be7b85176792748f2fb22e2e63ca0664 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 6 Feb 2024 13:46:46 +0100 Subject: [PATCH 0365/1886] Remove end_of_expression from ->, closes #13318 --- lib/elixir/src/elixir_parser.yrl | 2 +- lib/elixir/test/elixir/kernel/parser_test.exs | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/lib/elixir/src/elixir_parser.yrl b/lib/elixir/src/elixir_parser.yrl index 480fcf5c99..6f26e5e4a4 100644 --- a/lib/elixir/src/elixir_parser.yrl +++ b/lib/elixir/src/elixir_parser.yrl @@ -830,7 +830,7 @@ annotate_eoe(Token, Stack) -> {{_, Location}, [{'->', StabMeta, [StabArgs, {Left, Meta, Right}]} | Rest]} when is_list(Meta) -> [{'->', StabMeta, [StabArgs, {Left, [{end_of_expression, end_of_expression(Location)} | Meta], Right}]} | Rest]; - {{_, Location}, [{Left, Meta, Right} | Rest]} when is_list(Meta) -> + {{_, Location}, [{Left, Meta, Right} | Rest]} when is_list(Meta), Left =/= '->' -> [{Left, [{end_of_expression, end_of_expression(Location)} | Meta], Right} | Rest]; _ -> diff --git a/lib/elixir/test/elixir/kernel/parser_test.exs b/lib/elixir/test/elixir/kernel/parser_test.exs index ab517680b3..673b59409d 100644 --- a/lib/elixir/test/elixir/kernel/parser_test.exs +++ b/lib/elixir/test/elixir/kernel/parser_test.exs @@ -398,6 +398,27 @@ defmodule Kernel.ParserTest do {:__block__, [], args} end + test "does not add end of expression to ->" do + file = """ + case true do + :foo -> :bar + :baz -> :bat + end + """ + + assert Code.string_to_quoted!(file, token_metadata: true) == + {:case, [do: [line: 1], end: [line: 4], line: 1], + [ + true, + [ + do: [ + {:->, [line: 2], [[:foo], :bar]}, + {:->, [line: 3], [[:baz], :bat]} + ] + ] + ]} + end + test "adds pairing information" do string_to_quoted = &Code.string_to_quoted!(&1, token_metadata: true) From 8783d762e22576dade802121b397f7db4d144d52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 6 Feb 2024 13:49:42 +0100 Subject: [PATCH 0366/1886] Fix CI on map columns --- lib/elixir/test/elixir/kernel/errors_test.exs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/elixir/test/elixir/kernel/errors_test.exs b/lib/elixir/test/elixir/kernel/errors_test.exs index 3335b5835a..fba081c5a5 100644 --- a/lib/elixir/test/elixir/kernel/errors_test.exs +++ b/lib/elixir/test/elixir/kernel/errors_test.exs @@ -185,7 +185,7 @@ defmodule Kernel.ErrorsTest do test "literal on map and struct" do assert_compile_error( - ["nofile:1:11", "expected key-value pairs in a map, got: put_in(foo.bar.baz, nil)"], + ["nofile:1:10", "expected key-value pairs in a map, got: put_in(foo.bar.baz, nil)"], ~c"foo = 1; %{put_in(foo.bar.baz, nil), foo}" ) end @@ -992,11 +992,11 @@ defmodule Kernel.ErrorsTest do end test "duplicate map keys" do - assert_compile_error(["nofile:1:4", "key :a will be overridden in map"], """ + assert_compile_error(["nofile:1:3", "key :a will be overridden in map"], """ %{a: :b, a: :c} = %{a: :c} """) - assert_compile_error(["nofile:1:4", "key :a will be overridden in map"], """ + assert_compile_error(["nofile:1:3", "key :a will be overridden in map"], """ %{a: :b, a: :c, a: :d} = %{a: :c} """) end From 52eaf1456182d5d6cce22a4f5c3f6ec9f4dcbfd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 6 Feb 2024 13:54:46 +0100 Subject: [PATCH 0367/1886] Fix CI on map columns warnings --- lib/elixir/test/elixir/kernel/warning_test.exs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/elixir/test/elixir/kernel/warning_test.exs b/lib/elixir/test/elixir/kernel/warning_test.exs index 5196bd0b4e..ec23c8b0c0 100644 --- a/lib/elixir/test/elixir/kernel/warning_test.exs +++ b/lib/elixir/test/elixir/kernel/warning_test.exs @@ -904,11 +904,11 @@ defmodule Kernel.WarningTest do assert_warn_eval( [ "key :a will be overridden in map", - "nofile:4:11\n", + "nofile:4:10\n", "key :m will be overridden in map", - "nofile:5:11\n", + "nofile:5:10\n", "key 1 will be overridden in map", - "nofile:6:11\n" + "nofile:6:10\n" ], """ defmodule DuplicateMapKeys do From d68c8d6cddadd8ec5995dddaf4cee0e22e4ca440 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 7 Feb 2024 12:26:39 +0100 Subject: [PATCH 0368/1886] Unify handling of .. and ... --- lib/elixir/lib/code/formatter.ex | 5 ++ lib/elixir/lib/code/identifier.ex | 2 +- lib/elixir/lib/kernel/typespec.ex | 4 +- lib/elixir/lib/macro.ex | 4 +- lib/elixir/pages/references/operators.md | 2 +- lib/elixir/src/elixir_parser.yrl | 47 ++++++++++--------- lib/elixir/src/elixir_tokenizer.erl | 33 +++++-------- .../elixir/kernel/parallel_compiler_test.exs | 2 +- lib/elixir/test/elixir/kernel/parser_test.exs | 21 +++++++-- lib/elixir/test/elixir/kernel/quote_test.exs | 9 ---- .../test/elixir/kernel/warning_test.exs | 6 +-- lib/elixir/test/erlang/tokenizer_test.erl | 4 -- 12 files changed, 70 insertions(+), 69 deletions(-) diff --git a/lib/elixir/lib/code/formatter.ex b/lib/elixir/lib/code/formatter.ex index e8fd2a1a69..b663fa0597 100644 --- a/lib/elixir/lib/code/formatter.ex +++ b/lib/elixir/lib/code/formatter.ex @@ -478,6 +478,11 @@ defmodule Code.Formatter do end end + # ... + defp quoted_to_algebra({:..., _meta, []}, _context, state) do + {"...", state} + end + # 1..2//3 defp quoted_to_algebra({:"..//", meta, [left, middle, right]}, context, state) do quoted_to_algebra({:"//", meta, [{:.., meta, [left, middle]}, right]}, context, state) diff --git a/lib/elixir/lib/code/identifier.ex b/lib/elixir/lib/code/identifier.ex index 25163cbd5a..6ce9ca6094 100644 --- a/lib/elixir/lib/code/identifier.ex +++ b/lib/elixir/lib/code/identifier.ex @@ -13,7 +13,7 @@ defmodule Code.Identifier do @spec unary_op(atom) :: {:non_associative, precedence :: pos_integer} | :error def unary_op(op) do cond do - op in [:&] -> {:non_associative, 90} + op in [:&, :...] -> {:non_associative, 90} op in [:!, :^, :not, :+, :-, :"~~~"] -> {:non_associative, 300} op in [:@] -> {:non_associative, 320} true -> :error diff --git a/lib/elixir/lib/kernel/typespec.ex b/lib/elixir/lib/kernel/typespec.ex index d95c34fc45..88ea028c32 100644 --- a/lib/elixir/lib/kernel/typespec.ex +++ b/lib/elixir/lib/kernel/typespec.ex @@ -917,11 +917,11 @@ defmodule Kernel.Typespec do typespec({nil, [], []}, vars, caller, state) end - defp typespec([{:..., _, atom}], vars, caller, state) when is_atom(atom) do + defp typespec([{:..., _, _}], vars, caller, state) do typespec({:nonempty_list, [], []}, vars, caller, state) end - defp typespec([spec, {:..., _, atom}], vars, caller, state) when is_atom(atom) do + defp typespec([spec, {:..., _, _}], vars, caller, state) do typespec({:nonempty_list, [], [spec]}, vars, caller, state) end diff --git a/lib/elixir/lib/macro.ex b/lib/elixir/lib/macro.ex index 50e0cd60cd..7951e0f72f 100644 --- a/lib/elixir/lib/macro.ex +++ b/lib/elixir/lib/macro.ex @@ -1966,8 +1966,8 @@ defmodule Macro do def operator?(name, 1) when is_atom(name), do: Identifier.unary_op(name) != :error - def operator?(:.., 0), - do: true + def operator?(:.., 0), do: true + def operator?(:..., 0), do: true def operator?(name, arity) when is_atom(name) and is_integer(arity), do: false diff --git a/lib/elixir/pages/references/operators.md b/lib/elixir/pages/references/operators.md index 1cb7d6c7f8..8cefd445bc 100644 --- a/lib/elixir/pages/references/operators.md +++ b/lib/elixir/pages/references/operators.md @@ -22,7 +22,7 @@ Operator | Associativity `&&` `&&&` `and` | Left `\|\|` `\|\|\|` `or` | Left `=` | Right -`&` | Unary +`&`, `...` | Unary `=>` (valid only inside `%{}`) | Right `\|` | Right `::` | Right diff --git a/lib/elixir/src/elixir_parser.yrl b/lib/elixir/src/elixir_parser.yrl index 6f26e5e4a4..d92e98bba1 100644 --- a/lib/elixir/src/elixir_parser.yrl +++ b/lib/elixir/src/elixir_parser.yrl @@ -2,7 +2,7 @@ Nonterminals grammar expr_list expr container_expr block_expr access_expr no_parens_expr no_parens_zero_expr no_parens_one_expr no_parens_one_ambig_expr - bracket_expr bracket_at_expr bracket_arg matched_expr unmatched_expr + bracket_expr bracket_at_expr bracket_arg matched_expr unmatched_expr sub_matched_expr unmatched_op_expr matched_op_expr no_parens_op_expr no_parens_many_expr comp_op_eol at_op_eol unary_op_eol and_op_eol or_op_eol capture_op_eol dual_op_eol mult_op_eol power_op_eol concat_op_eol xor_op_eol pipe_op_eol @@ -12,8 +12,8 @@ Nonterminals list list_args open_bracket close_bracket tuple open_curly close_curly bitstring open_bit close_bit - map map_op map_close map_args struct_expr struct_op - assoc_op_eol assoc_expr assoc_base assoc_update assoc_update_kw assoc + map map_op map_base_expr map_close map_args + assoc_op_eol assoc_expr assoc_base assoc assoc_update assoc_update_kw container_args_base container_args call_args_parens_expr call_args_parens_base call_args_parens parens_call call_args_no_parens_one call_args_no_parens_ambig call_args_no_parens_expr @@ -32,7 +32,7 @@ Terminals fn 'end' alias atom atom_quoted atom_safe atom_unsafe bin_string list_string sigil bin_heredoc list_heredoc - comp_op at_op unary_op and_op or_op arrow_op match_op in_op in_match_op + comp_op at_op unary_op and_op or_op arrow_op match_op in_op in_match_op ellipsis_op type_op dual_op mult_op power_op concat_op range_op xor_op pipe_op stab_op when_op capture_int capture_op assoc_op rel_op ternary_op dot_call_op 'true' 'false' 'nil' 'do' eol ';' ',' '.' @@ -65,6 +65,7 @@ Right 60 type_op_eol. %% :: Right 70 pipe_op_eol. %% | Right 80 assoc_op_eol. %% => Nonassoc 90 capture_op_eol. %% & +Nonassoc 90 ellipsis_op. %% ... Right 100 match_op_eol. %% = Left 120 or_op_eol. %% ||, |||, or Left 130 and_op_eol. %% &&, &&&, and @@ -142,13 +143,12 @@ expr -> unmatched_expr : '$1'. %% if calls without parentheses are do blocks in particular %% segments and act accordingly. matched_expr -> matched_expr matched_op_expr : build_op('$1', '$2'). +matched_expr -> no_parens_one_expr : '$1'. matched_expr -> unary_op_eol matched_expr : build_unary_op('$1', '$2'). matched_expr -> at_op_eol matched_expr : build_unary_op('$1', '$2'). matched_expr -> capture_op_eol matched_expr : build_unary_op('$1', '$2'). -matched_expr -> no_parens_one_expr : '$1'. -matched_expr -> no_parens_zero_expr : '$1'. -matched_expr -> access_expr : '$1'. -matched_expr -> access_expr kw_identifier : error_invalid_kw_identifier('$2'). +matched_expr -> ellipsis_op matched_expr : build_unary_op('$1', '$2'). +matched_expr -> sub_matched_expr : '$1'. unmatched_expr -> matched_expr unmatched_op_expr : build_op('$1', '$2'). unmatched_expr -> unmatched_expr matched_op_expr : build_op('$1', '$2'). @@ -157,12 +157,14 @@ unmatched_expr -> unmatched_expr no_parens_op_expr : warn_no_parens_after_do_op( unmatched_expr -> unary_op_eol expr : build_unary_op('$1', '$2'). unmatched_expr -> at_op_eol expr : build_unary_op('$1', '$2'). unmatched_expr -> capture_op_eol expr : build_unary_op('$1', '$2'). +unmatched_expr -> ellipsis_op expr : build_unary_op('$1', '$2'). unmatched_expr -> block_expr : '$1'. no_parens_expr -> matched_expr no_parens_op_expr : build_op('$1', '$2'). no_parens_expr -> unary_op_eol no_parens_expr : build_unary_op('$1', '$2'). no_parens_expr -> at_op_eol no_parens_expr : build_unary_op('$1', '$2'). no_parens_expr -> capture_op_eol no_parens_expr : build_unary_op('$1', '$2'). +no_parens_expr -> ellipsis_op no_parens_expr : build_unary_op('$1', '$2'). no_parens_expr -> no_parens_one_ambig_expr : '$1'. no_parens_expr -> no_parens_many_expr : '$1'. @@ -246,6 +248,12 @@ no_parens_one_expr -> dot_identifier call_args_no_parens_one : build_no_parens(' no_parens_zero_expr -> dot_do_identifier : build_no_parens('$1', nil). no_parens_zero_expr -> dot_identifier : build_no_parens('$1', nil). +sub_matched_expr -> no_parens_zero_expr : '$1'. +sub_matched_expr -> range_op : build_nullary_op('$1'). +sub_matched_expr -> ellipsis_op : build_nullary_op('$1'). +sub_matched_expr -> access_expr : '$1'. +sub_matched_expr -> access_expr kw_identifier : error_invalid_kw_identifier('$2'). + %% From this point on, we just have constructs that can be %% used with the access syntax. Note that (dot_)identifier %% is not included in this list simply because the tokenizer @@ -281,7 +289,6 @@ access_expr -> atom_safe : build_quoted_atom('$1', true, delimiter(<<$">>)). access_expr -> atom_unsafe : build_quoted_atom('$1', false, delimiter(<<$">>)). access_expr -> dot_alias : '$1'. access_expr -> parens_call : '$1'. -access_expr -> range_op : build_nullary_op('$1'). %% Also used by maps and structs parens_call -> dot_call_identifier call_args_parens : build_parens('$1', '$2', {[], []}). @@ -598,6 +605,11 @@ bitstring -> open_bit container_args close_bit : build_bit('$1', '$2', '$3'). % Map and structs +map_base_expr -> sub_matched_expr : '$1'. +map_base_expr -> at_op_eol map_base_expr : build_unary_op('$1', '$2'). +map_base_expr -> unary_op_eol map_base_expr : build_unary_op('$1', '$2'). +map_base_expr -> ellipsis_op map_base_expr : build_unary_op('$1', '$2'). + assoc_op_eol -> assoc_op : '$1'. assoc_op_eol -> assoc_op eol : '$1'. @@ -605,9 +617,7 @@ assoc_expr -> matched_expr assoc_op_eol matched_expr : {'$1', '$3'}. assoc_expr -> unmatched_expr assoc_op_eol unmatched_expr : {'$1', '$3'}. assoc_expr -> matched_expr assoc_op_eol unmatched_expr : {'$1', '$3'}. assoc_expr -> unmatched_expr assoc_op_eol matched_expr : {'$1', '$3'}. -assoc_expr -> dot_identifier : build_identifier('$1', nil). -assoc_expr -> no_parens_one_expr : '$1'. -assoc_expr -> parens_call : '$1'. +assoc_expr -> map_base_expr : '$1'. assoc_update -> matched_expr pipe_op_eol assoc_expr : {'$2', '$1', ['$3']}. assoc_update -> unmatched_expr pipe_op_eol assoc_expr : {'$2', '$1', ['$3']}. @@ -635,18 +645,9 @@ map_args -> open_curly assoc_update ',' close_curly : build_map_update('$1', '$2 map_args -> open_curly assoc_update ',' map_close : build_map_update('$1', '$2', element(2, '$4'), element(1, '$4')). map_args -> open_curly assoc_update_kw close_curly : build_map_update('$1', '$2', '$3', []). -struct_op -> '%' : '$1'. -struct_expr -> atom : handle_literal(?exprs('$1'), '$1', []). -struct_expr -> atom_quoted : handle_literal(?exprs('$1'), '$1', delimiter(<<$">>)). -struct_expr -> dot_alias : '$1'. -struct_expr -> dot_identifier : build_identifier('$1', nil). -struct_expr -> at_op_eol struct_expr : build_unary_op('$1', '$2'). -struct_expr -> unary_op_eol struct_expr : build_unary_op('$1', '$2'). -struct_expr -> parens_call : '$1'. - map -> map_op map_args : adjust_map_column('$2'). -map -> struct_op struct_expr map_args : {'%', meta_from_token('$1'), ['$2', '$3']}. -map -> struct_op struct_expr eol map_args : {'%', meta_from_token('$1'), ['$2', '$4']}. +map -> '%' map_base_expr map_args : {'%', meta_from_token('$1'), ['$2', '$3']}. +map -> '%' map_base_expr eol map_args : {'%', meta_from_token('$1'), ['$2', '$4']}. Erlang code. diff --git a/lib/elixir/src/elixir_tokenizer.erl b/lib/elixir/src/elixir_tokenizer.erl index 2a0d1766ff..c0d01366e9 100644 --- a/lib/elixir/src/elixir_tokenizer.erl +++ b/lib/elixir/src/elixir_tokenizer.erl @@ -95,6 +95,9 @@ -define(pipe_op(T), T =:= $|). +-define(ellipsis_op3(T1, T2, T3), + T1 =:= $., T2 =:= $., T3 =:= $.). + %% Deprecated operators -define(unary_op3(T1, T2, T3), @@ -272,8 +275,6 @@ tokenize([$' | T], Line, Column, Scope, Tokens) -> tokenize(".:" ++ Rest, Line, Column, Scope, Tokens) when ?is_space(hd(Rest)) -> tokenize(Rest, Line, Column + 2, Scope, [{kw_identifier, {Line, Column, nil}, '.'} | Tokens]); -tokenize("...:" ++ Rest, Line, Column, Scope, Tokens) when ?is_space(hd(Rest)) -> - tokenize(Rest, Line, Column + 4, Scope, [{kw_identifier, {Line, Column, nil}, '...'} | Tokens]); tokenize("<<>>:" ++ Rest, Line, Column, Scope, Tokens) when ?is_space(hd(Rest)) -> tokenize(Rest, Line, Column + 5, Scope, [{kw_identifier, {Line, Column, nil}, '<<>>'} | Tokens]); tokenize("%{}:" ++ Rest, Line, Column, Scope, Tokens) when ?is_space(hd(Rest)) -> @@ -287,8 +288,6 @@ tokenize("{}:" ++ Rest, Line, Column, Scope, Tokens) when ?is_space(hd(Rest)) -> tokenize("..//:" ++ Rest, Line, Column, Scope, Tokens) when ?is_space(hd(Rest)) -> tokenize(Rest, Line, Column + 5, Scope, [{kw_identifier, {Line, Column, nil}, '..//'} | Tokens]); -tokenize(":..." ++ Rest, Line, Column, Scope, Tokens) -> - tokenize(Rest, Line, Column + 4, Scope, [{atom, {Line, Column, nil}, '...'} | Tokens]); tokenize(":<<>>" ++ Rest, Line, Column, Scope, Tokens) -> tokenize(Rest, Line, Column + 5, Scope, [{atom, {Line, Column, nil}, '<<>>'} | Tokens]); tokenize(":%{}" ++ Rest, Line, Column, Scope, Tokens) -> @@ -303,7 +302,7 @@ tokenize(":..//" ++ Rest, Line, Column, Scope, Tokens) -> % ## Three Token Operators tokenize([$:, T1, T2, T3 | Rest], Line, Column, Scope, Tokens) when ?unary_op3(T1, T2, T3); ?comp_op3(T1, T2, T3); ?and_op3(T1, T2, T3); ?or_op3(T1, T2, T3); - ?arrow_op3(T1, T2, T3); ?xor_op3(T1, T2, T3); ?concat_op3(T1, T2, T3) -> + ?arrow_op3(T1, T2, T3); ?xor_op3(T1, T2, T3); ?concat_op3(T1, T2, T3); ?ellipsis_op3(T1, T2, T3) -> Token = {atom, {Line, Column, nil}, list_to_atom([T1, T2, T3])}, tokenize(Rest, Line, Column + 4, Scope, [Token | Tokens]); @@ -331,13 +330,6 @@ tokenize([$:, T | Rest], Line, Column, Scope, Tokens) when % ## Stand-alone tokens -%% TODO: Consider either making ... as nullary operator (same as ..) -%% or deprecating it. In Elixir itself it is only used in typespecs. -tokenize("..." ++ Rest, Line, Column, Scope, Tokens) -> - NewScope = maybe_warn_too_many_of_same_char("...", Rest, Line, Column, Scope), - Token = check_call_identifier(Line, Column, "...", '...', Rest), - tokenize(Rest, Line, Column + 3, NewScope, [Token | Tokens]); - tokenize("=>" ++ Rest, Line, Column, Scope, Tokens) -> Token = {assoc_op, {Line, Column, previous_was_eol(Tokens)}, '=>'}, tokenize(Rest, Line, Column + 2, Scope, add_token_with_eol(Token, Tokens)); @@ -357,6 +349,9 @@ tokenize("..//" ++ Rest = String, Line, Column, Scope, Tokens) -> tokenize([T1, T2, T3 | Rest], Line, Column, Scope, Tokens) when ?unary_op3(T1, T2, T3) -> handle_unary_op(Rest, Line, Column, unary_op, 3, list_to_atom([T1, T2, T3]), Scope, Tokens); +tokenize([T1, T2, T3 | Rest], Line, Column, Scope, Tokens) when ?ellipsis_op3(T1, T2, T3) -> + handle_unary_op(Rest, Line, Column, ellipsis_op, 3, list_to_atom([T1, T2, T3]), Scope, Tokens); + tokenize([T1, T2, T3 | Rest], Line, Column, Scope, Tokens) when ?comp_op3(T1, T2, T3) -> handle_op(Rest, Line, Column, comp_op, 3, list_to_atom([T1, T2, T3]), Scope, Tokens); @@ -877,7 +872,7 @@ handle_op(Rest, Line, Column, Kind, Length, Op, Scope, Tokens) -> % ## Three Token Operators handle_dot([$., T1, T2, T3 | Rest], Line, Column, DotInfo, Scope, Tokens) when ?unary_op3(T1, T2, T3); ?comp_op3(T1, T2, T3); ?and_op3(T1, T2, T3); ?or_op3(T1, T2, T3); - ?arrow_op3(T1, T2, T3); ?xor_op3(T1, T2, T3); ?concat_op3(T1, T2, T3) -> + ?arrow_op3(T1, T2, T3); ?xor_op3(T1, T2, T3); ?concat_op3(T1, T2, T3); ?ellipsis_op3(T1, T2, T3) -> handle_call_identifier(Rest, Line, Column, DotInfo, 3, [T1, T2, T3], Scope, Tokens); % ## Two Token Operators @@ -1687,12 +1682,10 @@ invalid_do_with_fn_error(Prefix) -> % TODO: Turn into an error on v2.0 maybe_warn_too_many_of_same_char([T | _] = Token, [T | _] = _Rest, Line, Column, Scope) -> - Warning = - case T of - $. -> "please use parens around \"...\" instead"; - _ -> io_lib:format("please use a space between \"~ts\" and the next \"~ts\"", [Token, [T]]) - end, - Message = io_lib:format("found \"~ts\" followed by \"~ts\", ~ts", [Token, [T], Warning]), + Message = io_lib:format( + "found \"~ts\" followed by \"~ts\", please use a space between \"~ts\" and the next \"~ts\"", + [Token, [T], Token, [T]] + ), prepend_warning(Line, Column, Message, Scope); maybe_warn_too_many_of_same_char(_Token, _Rest, _Line, _Column, Scope) -> Scope. @@ -1826,7 +1819,7 @@ prune_tokens([{OpType, _, _} | _] = Tokens, [], Terminators) OpType =:= in_match_op; OpType =:= type_op; OpType =:= dual_op; OpType =:= mult_op; OpType =:= power_op; OpType =:= concat_op; OpType =:= range_op; OpType =:= xor_op; OpType =:= pipe_op; OpType =:= stab_op; OpType =:= when_op; OpType =:= assoc_op; - OpType =:= rel_op; OpType =:= ternary_op; OpType =:= capture_op -> + OpType =:= rel_op; OpType =:= ternary_op; OpType =:= capture_op; OpType =:= ellipsis_op -> {Tokens, Terminators}; %%% or we traverse until the end. prune_tokens([_ | Tokens], Opener, Terminators) -> diff --git a/lib/elixir/test/elixir/kernel/parallel_compiler_test.exs b/lib/elixir/test/elixir/kernel/parallel_compiler_test.exs index fa41968160..17697dc93c 100644 --- a/lib/elixir/test/elixir/kernel/parallel_compiler_test.exs +++ b/lib/elixir/test/elixir/kernel/parallel_compiler_test.exs @@ -387,7 +387,7 @@ defmodule Kernel.ParallelCompilerTest do capture_io(:stderr, fn -> fixtures = [foo, bar] - assert assert {:ok, modules, []} = Kernel.ParallelCompiler.compile(fixtures) + assert {:ok, modules, []} = Kernel.ParallelCompiler.compile(fixtures) assert FooAsync in modules assert BarAsync in modules end) diff --git a/lib/elixir/test/elixir/kernel/parser_test.exs b/lib/elixir/test/elixir/kernel/parser_test.exs index 673b59409d..da3cf66f16 100644 --- a/lib/elixir/test/elixir/kernel/parser_test.exs +++ b/lib/elixir/test/elixir/kernel/parser_test.exs @@ -6,9 +6,15 @@ defmodule Kernel.ParserTest do describe "nullary ops" do test "in expressions" do assert parse!("..") == {:.., [line: 1], []} + assert parse!("...") == {:..., [line: 1], []} end - test "raises on ambiguous uses" do + test "in capture" do + assert parse!("&../0") == {:&, [line: 1], [{:/, [line: 1], [{:.., [line: 1], nil}, 0]}]} + assert parse!("&.../0") == {:&, [line: 1], [{:/, [line: 1], [{:..., [line: 1], nil}, 0]}]} + end + + test "raises on ambiguous uses when also binary" do assert_raise SyntaxError, ~r/syntax error before: do/, fn -> parse!("if .. do end") end @@ -21,6 +27,16 @@ defmodule Kernel.ParserTest do assert parse!("f @: :ok") == {:f, [line: 1], [[@: :ok]]} end + test "in maps" do + assert parse!("%{+foo, bar => bat, ...baz}") == + {:%{}, [line: 1], + [ + {:+, [line: 1], [{:foo, [line: 1], nil}]}, + {{:bar, [line: 1], nil}, {:bat, [line: 1], nil}}, + {:..., [line: 1], [{:baz, [line: 1], nil}]} + ]} + end + test "ambiguous ops in keywords" do assert parse!("f(+: :ok)") == {:f, [line: 1], [[+: :ok]]} assert parse!("f +: :ok") == {:f, [line: 1], [[+: :ok]]} @@ -890,8 +906,7 @@ defmodule Kernel.ParserTest do end test "invalid map/struct" do - assert_syntax_error(["nofile:1:5:", "syntax error before: '}'"], ~c"%{:a}") - assert_syntax_error(["nofile:1:11:", "syntax error before: '}'"], ~c"%{{:a, :b}}") + assert_syntax_error(["nofile:1:15:", "syntax error before: '}'"], ~c"%{foo bar, baz}") assert_syntax_error(["nofile:1:8:", "syntax error before: '{'"], ~c"%{a, b}{a: :b}") end diff --git a/lib/elixir/test/elixir/kernel/quote_test.exs b/lib/elixir/test/elixir/kernel/quote_test.exs index 60cb368857..6b41d9dd84 100644 --- a/lib/elixir/test/elixir/kernel/quote_test.exs +++ b/lib/elixir/test/elixir/kernel/quote_test.exs @@ -203,15 +203,6 @@ defmodule Kernel.QuoteTest do map = %{foo: :default} assert %{map | unquote_splicing(foo: :bar)} == %{foo: :bar} - - assert Code.eval_string("quote do: %{unquote_splicing foo: :bar}") == - {{:%{}, [], [foo: :bar]}, []} - - assert Code.eval_string("quote do: %{:baz => :bat, unquote_splicing foo: :bar}") == - {{:%{}, [], [{:baz, :bat}, {:foo, :bar}]}, []} - - assert Code.eval_string("quote do: %{foo bar | baz}") == - {{:%{}, [], [{:foo, [], [{:|, [], [{:bar, [], Elixir}, {:baz, [], Elixir}]}]}]}, []} end test "when" do diff --git a/lib/elixir/test/elixir/kernel/warning_test.exs b/lib/elixir/test/elixir/kernel/warning_test.exs index ec23c8b0c0..0cdb71be63 100644 --- a/lib/elixir/test/elixir/kernel/warning_test.exs +++ b/lib/elixir/test/elixir/kernel/warning_test.exs @@ -196,10 +196,10 @@ defmodule Kernel.WarningTest do test "operators formed by many of the same character followed by that character" do assert_warn_eval( [ - "nofile:1:11", - "found \"...\" followed by \".\", please use parens around \"...\" instead" + "nofile:1:12", + "found \"+++\" followed by \"+\", please use a space between \"+++\" and the next \"+\"" ], - "quote do: ....()" + "quote do: 1++++1" ) end diff --git a/lib/elixir/test/erlang/tokenizer_test.erl b/lib/elixir/test/erlang/tokenizer_test.erl index f03904e021..c07b847000 100644 --- a/lib/elixir/test/erlang/tokenizer_test.erl +++ b/lib/elixir/test/erlang/tokenizer_test.erl @@ -109,10 +109,6 @@ identifier_test() -> module_macro_test() -> [{identifier, {1, 1, _}, '__MODULE__'}] = tokenize("__MODULE__"). -triple_dot_test() -> - [{identifier, {1, 1, _}, '...'}] = tokenize("..."), - [{'.', {1, 1, nil}}, {identifier, {1, 3, _}, '..'}] = tokenize(". .."). - dot_test() -> [{identifier, {1, 1, _}, foo}, {'.', {1, 4, nil}}, From 6655d40376e9b94e79c519b661d1515bd139f3ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 7 Feb 2024 13:06:59 +0100 Subject: [PATCH 0369/1886] Streamline and document the parser --- lib/elixir/lib/code/formatter.ex | 19 ++++++++----- lib/elixir/lib/code/identifier.ex | 1 - lib/elixir/src/elixir_parser.yrl | 43 ++++++++++++++++------------- lib/elixir/src/elixir_tokenizer.erl | 4 +-- 4 files changed, 38 insertions(+), 29 deletions(-) diff --git a/lib/elixir/lib/code/formatter.ex b/lib/elixir/lib/code/formatter.ex index b663fa0597..1d4aa84f66 100644 --- a/lib/elixir/lib/code/formatter.ex +++ b/lib/elixir/lib/code/formatter.ex @@ -634,7 +634,7 @@ defmodule Code.Formatter do defp maybe_binary_op_to_algebra(fun, meta, args, context, state) do with [left, right] <- args, - {_, _} <- Code.Identifier.binary_op(fun) do + {_, _} <- augmented_binary_op(fun) do binary_op_to_algebra(fun, Atom.to_string(fun), meta, left, right, context, state) else _ -> :error @@ -661,7 +661,7 @@ defmodule Code.Formatter do defp binary_op_to_algebra(op, op_string, meta, left_arg, right_arg, context, state, _nesting) when op in @right_new_line_before_binary_operators do - op_info = Code.Identifier.binary_op(op) + op_info = augmented_binary_op(op) op_string = op_string <> " " left_context = left_op_context(context) right_context = right_op_context(context) @@ -698,7 +698,7 @@ defmodule Code.Formatter do defp binary_op_to_algebra(op, _, meta, left_arg, right_arg, context, state, _nesting) when op in @pipeline_operators do - op_info = Code.Identifier.binary_op(op) + op_info = augmented_binary_op(op) left_context = left_op_context(context) right_context = right_op_context(context) max_line = line(meta) @@ -712,7 +712,7 @@ defmodule Code.Formatter do {{doc, @empty, 1}, state} {{op, context}, arg}, _args, state -> - op_info = Code.Identifier.binary_op(op) + op_info = augmented_binary_op(op) op_string = Atom.to_string(op) <> " " {doc, state} = binary_operand_to_algebra(arg, context, state, op, op_info, :right, 0) {{concat(op_string, doc), @empty, 1}, state} @@ -722,7 +722,7 @@ defmodule Code.Formatter do end defp binary_op_to_algebra(op, op_string, meta, left_arg, right_arg, context, state, nesting) do - op_info = Code.Identifier.binary_op(op) + op_info = augmented_binary_op(op) left_context = left_op_context(context) right_context = right_op_context(context) @@ -781,7 +781,7 @@ defmodule Code.Formatter do {parent_assoc, parent_prec} = parent_info with {op, meta, [left, right]} <- operand, - op_info = Code.Identifier.binary_op(op), + op_info = augmented_binary_op(op), {_assoc, prec} <- op_info do op_string = Atom.to_string(op) @@ -2192,10 +2192,15 @@ defmodule Code.Formatter do unary_operator?(quoted) or binary_operator?(quoted) end + # We convert ..// into two operators for simplicity, + # so we need to augment the binary table. + defp augmented_binary_op(:"//"), do: {:right, 190} + defp augmented_binary_op(op), do: Code.Identifier.binary_op(op) + defp binary_operator?(quoted) do case quoted do {op, _, [_, _, _]} when op in @multi_binary_operators -> true - {op, _, [_, _]} when is_atom(op) -> Code.Identifier.binary_op(op) != :error + {op, _, [_, _]} when is_atom(op) -> augmented_binary_op(op) != :error _ -> false end end diff --git a/lib/elixir/lib/code/identifier.ex b/lib/elixir/lib/code/identifier.ex index 6ce9ca6094..34aeeb7e9b 100644 --- a/lib/elixir/lib/code/identifier.ex +++ b/lib/elixir/lib/code/identifier.ex @@ -44,7 +44,6 @@ defmodule Code.Identifier do op in [:|>, :<<<, :>>>, :<~, :~>, :<<~, :~>>, :<~>, :"<|>"] -> {:left, 160} op in [:in] -> {:left, 170} op in [:"^^^"] -> {:left, 180} - op in [:"//"] -> {:right, 190} op in [:++, :--, :.., :<>, :+++, :---] -> {:right, 200} op in [:+, :-] -> {:left, 210} op in [:*, :/] -> {:left, 220} diff --git a/lib/elixir/src/elixir_parser.yrl b/lib/elixir/src/elixir_parser.yrl index d92e98bba1..7a29b1311d 100644 --- a/lib/elixir/src/elixir_parser.yrl +++ b/lib/elixir/src/elixir_parser.yrl @@ -143,11 +143,11 @@ expr -> unmatched_expr : '$1'. %% if calls without parentheses are do blocks in particular %% segments and act accordingly. matched_expr -> matched_expr matched_op_expr : build_op('$1', '$2'). -matched_expr -> no_parens_one_expr : '$1'. matched_expr -> unary_op_eol matched_expr : build_unary_op('$1', '$2'). matched_expr -> at_op_eol matched_expr : build_unary_op('$1', '$2'). matched_expr -> capture_op_eol matched_expr : build_unary_op('$1', '$2'). matched_expr -> ellipsis_op matched_expr : build_unary_op('$1', '$2'). +matched_expr -> no_parens_one_expr : '$1'. matched_expr -> sub_matched_expr : '$1'. unmatched_expr -> matched_expr unmatched_op_expr : build_op('$1', '$2'). @@ -192,6 +192,10 @@ matched_op_expr -> pipe_op_eol matched_expr : {'$1', '$2'}. matched_op_expr -> comp_op_eol matched_expr : {'$1', '$2'}. matched_op_expr -> rel_op_eol matched_expr : {'$1', '$2'}. matched_op_expr -> arrow_op_eol matched_expr : {'$1', '$2'}. + +%% We warn exclusively for |> and friends because they are used +%% in other languages with lower precedence than function application, +%% which can be the source of confusion. matched_op_expr -> arrow_op_eol no_parens_one_expr : warn_pipe('$1', '$2'), {'$1', '$2'}. unmatched_op_expr -> match_op_eol unmatched_expr : {'$1', '$2'}. @@ -230,9 +234,7 @@ no_parens_op_expr -> when_op_eol no_parens_expr : {'$1', '$2'}. no_parens_op_expr -> pipe_op_eol no_parens_expr : {'$1', '$2'}. no_parens_op_expr -> comp_op_eol no_parens_expr : {'$1', '$2'}. no_parens_op_expr -> rel_op_eol no_parens_expr : {'$1', '$2'}. -no_parens_op_expr -> arrow_op_eol no_parens_expr : {'$1', '$2'}. -no_parens_op_expr -> arrow_op_eol no_parens_one_ambig_expr : warn_pipe('$1', '$2'), {'$1', '$2'}. -no_parens_op_expr -> arrow_op_eol no_parens_many_expr : warn_pipe('$1', '$2'), {'$1', '$2'}. +no_parens_op_expr -> arrow_op_eol no_parens_expr : warn_pipe('$1', '$2'), {'$1', '$2'}. %% Allow when (and only when) with keywords no_parens_op_expr -> when_op_eol call_args_no_parens_kw : {'$1', '$2'}. @@ -245,8 +247,8 @@ no_parens_many_expr -> dot_identifier call_args_no_parens_many_strict : build_no no_parens_one_expr -> dot_op_identifier call_args_no_parens_one : build_no_parens('$1', '$2'). no_parens_one_expr -> dot_identifier call_args_no_parens_one : build_no_parens('$1', '$2'). -no_parens_zero_expr -> dot_do_identifier : build_no_parens('$1', nil). -no_parens_zero_expr -> dot_identifier : build_no_parens('$1', nil). +no_parens_zero_expr -> dot_do_identifier : build_identifier('$1'). +no_parens_zero_expr -> dot_identifier : build_identifier('$1'). sub_matched_expr -> no_parens_zero_expr : '$1'. sub_matched_expr -> range_op : build_nullary_op('$1'). @@ -299,11 +301,11 @@ bracket_arg -> open_bracket container_expr close_bracket : build_access_arg('$1' bracket_arg -> open_bracket container_expr ',' close_bracket : build_access_arg('$1', '$2', '$4'). bracket_arg -> open_bracket container_expr ',' container_args close_bracket : error_too_many_access_syntax('$3'). -bracket_expr -> dot_bracket_identifier bracket_arg : build_access(build_no_parens('$1', nil), meta_with_from_brackets('$2')). +bracket_expr -> dot_bracket_identifier bracket_arg : build_access(build_identifier('$1'), meta_with_from_brackets('$2')). bracket_expr -> access_expr bracket_arg : build_access('$1', meta_with_from_brackets('$2')). bracket_at_expr -> at_op_eol dot_bracket_identifier bracket_arg : - build_access(build_unary_op('$1', build_no_parens('$2', nil)), meta_with_from_brackets('$3')). + build_access(build_unary_op('$1', build_identifier('$2')), meta_with_from_brackets('$3')). bracket_at_expr -> at_op_eol access_expr bracket_arg : build_access(build_unary_op('$1', '$2'), meta_with_from_brackets('$3')). @@ -897,32 +899,35 @@ build_nested_parens(Dot, Args1, {Args2Meta, Args2}, {BlockMeta, Block}) -> {Identifier, Meta, append_non_empty(Args2, Block)}. build_parens(Expr, {ArgsMeta, Args}, {BlockMeta, Block}) -> - {BuiltExpr, BuiltMeta, BuiltArgs} = build_identifier(Expr, append_non_empty(Args, Block)), + {BuiltExpr, BuiltMeta, BuiltArgs} = build_call(Expr, append_non_empty(Args, Block)), {BuiltExpr, BlockMeta ++ ArgsMeta ++ BuiltMeta, BuiltArgs}. build_no_parens_do_block(Expr, Args, {BlockMeta, Block}) -> - {BuiltExpr, BuiltMeta, BuiltArgs} = build_no_parens(Expr, Args ++ Block), + {BuiltExpr, BuiltMeta, BuiltArgs} = build_call(Expr, Args ++ Block), {BuiltExpr, BlockMeta ++ BuiltMeta, BuiltArgs}. build_no_parens(Expr, Args) -> - build_identifier(Expr, Args). + build_call(Expr, Args). -build_identifier({'.', Meta, IdentifierLocation, DotArgs}, nil) -> +build_identifier({'.', Meta, IdentifierLocation, DotArgs}) -> {{'.', Meta, DotArgs}, [{no_parens, true} | IdentifierLocation], []}; -build_identifier({'.', Meta, IdentifierLocation, DotArgs}, Args) -> - {{'.', Meta, DotArgs}, IdentifierLocation, Args}; - -build_identifier({'.', Meta, _} = Dot, nil) -> +build_identifier({'.', Meta, _} = Dot) -> {Dot, [{no_parens, true} | Meta], []}; -build_identifier({'.', Meta, _} = Dot, Args) -> +build_identifier({_, Location, Identifier}) -> + {Identifier, meta_from_location(Location), nil}. + +build_call({'.', Meta, IdentifierLocation, DotArgs}, Args) -> + {{'.', Meta, DotArgs}, IdentifierLocation, Args}; + +build_call({'.', Meta, _} = Dot, Args) -> {Dot, Meta, Args}; -build_identifier({op_identifier, Location, Identifier}, [Arg]) -> +build_call({op_identifier, Location, Identifier}, [Arg]) -> {Identifier, [{ambiguous_op, nil} | meta_from_location(Location)], [Arg]}; -build_identifier({_, Location, Identifier}, Args) -> +build_call({_, Location, Identifier}, Args) -> {Identifier, meta_from_location(Location), Args}. %% Fn diff --git a/lib/elixir/src/elixir_tokenizer.erl b/lib/elixir/src/elixir_tokenizer.erl index c0d01366e9..5262ec265c 100644 --- a/lib/elixir/src/elixir_tokenizer.erl +++ b/lib/elixir/src/elixir_tokenizer.erl @@ -872,14 +872,14 @@ handle_op(Rest, Line, Column, Kind, Length, Op, Scope, Tokens) -> % ## Three Token Operators handle_dot([$., T1, T2, T3 | Rest], Line, Column, DotInfo, Scope, Tokens) when ?unary_op3(T1, T2, T3); ?comp_op3(T1, T2, T3); ?and_op3(T1, T2, T3); ?or_op3(T1, T2, T3); - ?arrow_op3(T1, T2, T3); ?xor_op3(T1, T2, T3); ?concat_op3(T1, T2, T3); ?ellipsis_op3(T1, T2, T3) -> + ?arrow_op3(T1, T2, T3); ?xor_op3(T1, T2, T3); ?concat_op3(T1, T2, T3) -> handle_call_identifier(Rest, Line, Column, DotInfo, 3, [T1, T2, T3], Scope, Tokens); % ## Two Token Operators handle_dot([$., T1, T2 | Rest], Line, Column, DotInfo, Scope, Tokens) when ?comp_op2(T1, T2); ?rel_op2(T1, T2); ?and_op(T1, T2); ?or_op(T1, T2); ?arrow_op(T1, T2); ?in_match_op(T1, T2); ?concat_op(T1, T2); ?power_op(T1, T2); - ?type_op(T1, T2); ?range_op(T1, T2) -> + ?type_op(T1, T2) -> handle_call_identifier(Rest, Line, Column, DotInfo, 2, [T1, T2], Scope, Tokens); % ## Single Token Operators From ba1de9d6cf5f7c5d6ff46b2ff7aa2886de079e12 Mon Sep 17 00:00:00 2001 From: Aaron Renner Date: Wed, 7 Feb 2024 10:05:08 -0700 Subject: [PATCH 0370/1886] Fix typo in docs (#13321) --- lib/mix/lib/mix.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mix/lib/mix.ex b/lib/mix/lib/mix.ex index 7716f5af53..cdc16a071b 100644 --- a/lib/mix/lib/mix.ex +++ b/lib/mix/lib/mix.ex @@ -291,7 +291,7 @@ defmodule Mix do then `mix cmd` to execute a command line shell script. This shows how powerful aliases mixed with Mix tasks can be. - One commit pitfall of aliases comes when trying to invoke the same task + One common pitfall of aliases comes when trying to invoke the same task multiple times. Mix tasks are designed to run only once. This prevents the same task from being executed multiple times. For example, if there are several tasks depending on `mix compile`, the code will be compiled From c50863615c0e8ac957e22ae01a6f9af23978c3f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 8 Feb 2024 08:08:03 +0100 Subject: [PATCH 0371/1886] Skip tests if Erlang was compiled without docs, closes #13322 --- lib/iex/test/iex/helpers_test.exs | 12 +++++++++--- lib/iex/test/test_helper.exs | 10 +++++++++- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/lib/iex/test/iex/helpers_test.exs b/lib/iex/test/iex/helpers_test.exs index 54f946516c..171acc72ab 100644 --- a/lib/iex/test/iex/helpers_test.exs +++ b/lib/iex/test/iex/helpers_test.exs @@ -332,17 +332,20 @@ defmodule IEx.HelpersTest do assert help =~ "Welcome to Interactive Elixir" end + @tag :erlang_doc test "prints Erlang module documentation" do captured = capture_io(fn -> h(:timer) end) assert captured =~ "This module provides useful functions related to time." end + @tag :erlang_doc test "prints Erlang module function specs" do captured = capture_io(fn -> h(:timer.sleep() / 1) end) assert captured =~ ":timer.sleep/1" assert captured =~ "-spec sleep(Time) -> ok when Time :: timeout()." end + @tag :erlang_doc test "handles non-existing Erlang module function" do captured = capture_io(fn -> h(:timer.baz() / 1) end) assert captured =~ "No documentation for :timer.baz was found" @@ -1008,13 +1011,15 @@ defmodule IEx.HelpersTest do cleanup_modules([TypeSample]) end - test "prints all types in erlang module" do + @tag :erlang_doc + test "prints all types in Erlang module" do captured = capture_io(fn -> t(:queue) end) assert captured =~ "-type queue() :: queue(_)" assert captured =~ "-opaque queue(Item)" end - test "prints single type from erlang module" do + @tag :erlang_doc + test "prints single type from Erlang module" do captured = capture_io(fn -> t(:erlang.iovec()) end) assert captured =~ "-type iovec() :: [binary()]" assert captured =~ "A list of binaries." @@ -1024,7 +1029,8 @@ defmodule IEx.HelpersTest do assert captured =~ "A list of binaries." end - test "handles non-existing types from erlang module" do + @tag :erlang_doc + test "handles non-existing types from Erlang module" do captured = capture_io(fn -> t(:erlang.foo()) end) assert captured =~ "No type information for :erlang.foo was found or :erlang.foo is private" diff --git a/lib/iex/test/test_helper.exs b/lib/iex/test/test_helper.exs index f5a55f0aa8..b32c8be4e9 100644 --- a/lib/iex/test/test_helper.exs +++ b/lib/iex/test/test_helper.exs @@ -7,11 +7,19 @@ IEx.configure(colors: [enabled: false]) {line_exclude, line_include} = if line = System.get_env("LINE"), do: {[:test], [line: line]}, else: {[], []} +erlang_doc_exclude = + if match?({:docs_v1, _, _, _, _, _, _}, Code.fetch_docs(:array)) do + [] + else + IO.puts("Erlang/OTP compiled without docs, some tests are excluded...") + [:erlang_doc] + end + ExUnit.start( assert_receive_timeout: assert_timeout, trace: !!System.get_env("TRACE"), include: line_include, - exclude: line_exclude + exclude: line_exclude ++ erlang_doc_exclude ) defmodule IEx.Case do From 9973a2eded13afc7955de3b7ddbd87807c3006a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 8 Feb 2024 15:43:20 +0100 Subject: [PATCH 0372/1886] Remove typespec from default requires --- lib/elixir/src/elixir_dispatch.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/src/elixir_dispatch.erl b/lib/elixir/src/elixir_dispatch.erl index df9b04bdc5..ea53729498 100644 --- a/lib/elixir/src/elixir_dispatch.erl +++ b/lib/elixir/src/elixir_dispatch.erl @@ -17,7 +17,7 @@ default_functions() -> default_macros() -> [{?kernel, elixir_imported_macros()}]. default_requires() -> - ['Elixir.Application', 'Elixir.Kernel', 'Elixir.Kernel.Typespec']. + ['Elixir.Application', 'Elixir.Kernel']. %% This is used by elixir_quote. Note we don't record the %% import locally because at that point there is no From 555eac76a16e2546b1171eacf043b0228e55f168 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 8 Feb 2024 16:57:43 +0100 Subject: [PATCH 0373/1886] Do not treat streaming errors as file errors, closes #13323 --- lib/elixir/lib/io/stream.ex | 8 +++----- lib/elixir/test/elixir/string_io_test.exs | 2 +- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/elixir/lib/io/stream.ex b/lib/elixir/lib/io/stream.ex index 40397e4b6c..8558f16036 100644 --- a/lib/elixir/lib/io/stream.ex +++ b/lib/elixir/lib/io/stream.ex @@ -1,11 +1,9 @@ defmodule IO.StreamError do - defexception [:reason, :message] + defexception [:reason] @impl true - def exception(opts) do - reason = opts[:reason] - formatted = IO.iodata_to_binary(:file.format_error(reason)) - %IO.StreamError{message: "error during streaming: #{formatted}", reason: reason} + def message(%{reason: reason}) do + "error during streaming: #{inspect(reason)}" end end diff --git a/lib/elixir/test/elixir/string_io_test.exs b/lib/elixir/test/elixir/string_io_test.exs index 03c070aab9..b623279e3c 100644 --- a/lib/elixir/test/elixir/string_io_test.exs +++ b/lib/elixir/test/elixir/string_io_test.exs @@ -267,7 +267,7 @@ defmodule StringIOTest do test "IO.stream with invalid UTF-8" do {:ok, pid} = StringIO.open(<<130, 227, 129, 132, 227, 129, 134>>) - assert_raise IO.StreamError, fn -> + assert_raise IO.StreamError, "error during streaming: :invalid_unicode", fn -> IO.stream(pid, 2) |> Enum.to_list() end From 3fb82c3735c1cacf10229d55e18a0b588b8d06b5 Mon Sep 17 00:00:00 2001 From: Jean Klingler Date: Fri, 9 Feb 2024 16:49:27 +0900 Subject: [PATCH 0374/1886] Update argument error message when matching with <> (#13325) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update argument error message when matching with <> * Update lib/elixir/lib/kernel.ex Co-authored-by: José Valim --------- Co-authored-by: José Valim --- lib/elixir/lib/kernel.ex | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/elixir/lib/kernel.ex b/lib/elixir/lib/kernel.ex index 34fb9194a0..7357aeb4f7 100644 --- a/lib/elixir/lib/kernel.ex +++ b/lib/elixir/lib/kernel.ex @@ -2082,8 +2082,9 @@ defmodule Kernel do defp invalid_concat_left_argument_error(arg) do :erlang.error( ArgumentError.exception( - "the left argument of <> operator inside a match should always be a literal " <> - "binary because its size can't be verified. Got: #{arg}" + "cannot perform prefix match because the left operand of <> has unknown size. " <> + "The left operand of <> inside a match should either be a literal binary or " <> + "an existing variable with the pin operator (such as ^some_var). Got: #{arg}" ) ) end From 1f1e341b1fb8d03d9cb4d162df580014d9a09b4a Mon Sep 17 00:00:00 2001 From: Jean Klingler Date: Fri, 9 Feb 2024 17:02:56 +0900 Subject: [PATCH 0375/1886] Fix <> match test (#13327) --- lib/elixir/test/elixir/kernel/binary_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/test/elixir/kernel/binary_test.exs b/lib/elixir/test/elixir/kernel/binary_test.exs index fd26ad3aee..dadcfaee67 100644 --- a/lib/elixir/test/elixir/kernel/binary_test.exs +++ b/lib/elixir/test/elixir/kernel/binary_test.exs @@ -119,7 +119,7 @@ defmodule Kernel.BinaryTest do Code.eval_string(~s["foo" <> 1]) end - message = ~r"left argument of <> operator inside a match" + message = ~r"cannot perform prefix match because the left operand of <> has unknown size." assert_raise ArgumentError, message, fn -> Code.eval_string(~s[a <> "b" = "ab"]) From 50fba3c776fc2ce3ed8a29a4fb065f8ea7d6d6ca Mon Sep 17 00:00:00 2001 From: Brad Hanks Date: Fri, 9 Feb 2024 04:20:02 -0700 Subject: [PATCH 0376/1886] Fix docs for Kernel.binary_part/3 (#13328) --- lib/elixir/lib/kernel.ex | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/elixir/lib/kernel.ex b/lib/elixir/lib/kernel.ex index 7357aeb4f7..9c3a363c34 100644 --- a/lib/elixir/lib/kernel.ex +++ b/lib/elixir/lib/kernel.ex @@ -379,9 +379,9 @@ defmodule Kernel do end @doc """ - Extracts the part of the binary at `start` with `size`. + Extracts the part of the binary at `start` with `length`. - If `start` or `size` reference in any way outside the binary, + If `start` or `length` reference in any way outside the binary, an `ArgumentError` exception is raised. Allowed in guard tests. Inlined by the compiler. @@ -391,13 +391,13 @@ defmodule Kernel do iex> binary_part("foo", 1, 2) "oo" - A negative `size` can be used to extract bytes that come *before* the byte + A negative `length` can be used to extract bytes that come *before* the byte at `start`: iex> binary_part("Hello", 5, -3) "llo" - An `ArgumentError` is raised when the size is outside of the binary: + An `ArgumentError` is raised when the length is outside of the binary: binary_part("Hello", 0, 10) ** (ArgumentError) argument error From e758fa1c6e4d87d80e68346a20ffbb32af608872 Mon Sep 17 00:00:00 2001 From: Jean Klingler Date: Fri, 9 Feb 2024 21:47:57 +0900 Subject: [PATCH 0377/1886] Rename "length" to "size" in binary_part/3 (#13329) --- lib/elixir/lib/kernel.ex | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/elixir/lib/kernel.ex b/lib/elixir/lib/kernel.ex index 9c3a363c34..844f76a1ff 100644 --- a/lib/elixir/lib/kernel.ex +++ b/lib/elixir/lib/kernel.ex @@ -379,9 +379,9 @@ defmodule Kernel do end @doc """ - Extracts the part of the binary at `start` with `length`. + Extracts the part of the binary at `start` with `size`. - If `start` or `length` reference in any way outside the binary, + If `start` or `size` reference in any way outside the binary, an `ArgumentError` exception is raised. Allowed in guard tests. Inlined by the compiler. @@ -391,13 +391,13 @@ defmodule Kernel do iex> binary_part("foo", 1, 2) "oo" - A negative `length` can be used to extract bytes that come *before* the byte + A negative `size` can be used to extract bytes that come *before* the byte at `start`: iex> binary_part("Hello", 5, -3) "llo" - An `ArgumentError` is raised when the length is outside of the binary: + An `ArgumentError` is raised when the size is outside of the binary: binary_part("Hello", 0, 10) ** (ArgumentError) argument error @@ -405,8 +405,8 @@ defmodule Kernel do """ @doc guard: true @spec binary_part(binary, non_neg_integer, integer) :: binary - def binary_part(binary, start, length) do - :erlang.binary_part(binary, start, length) + def binary_part(binary, start, size) do + :erlang.binary_part(binary, start, size) end @doc """ From a210e5832e1fbdffd4d577f4e80b6bc4ae0e8fd6 Mon Sep 17 00:00:00 2001 From: Marcus Kruse Date: Sat, 10 Feb 2024 09:45:27 +0100 Subject: [PATCH 0378/1886] Handle empty string in System.shell (#13331) --- lib/elixir/lib/system.ex | 6 ++++++ lib/elixir/test/elixir/system_test.exs | 5 +++++ 2 files changed, 11 insertions(+) diff --git a/lib/elixir/lib/system.ex b/lib/elixir/lib/system.ex index 9b958b0086..dfc4556564 100644 --- a/lib/elixir/lib/system.ex +++ b/lib/elixir/lib/system.ex @@ -955,6 +955,12 @@ defmodule System do @doc since: "1.12.0" @spec shell(binary, keyword) :: {Collectable.t(), exit_status :: non_neg_integer} def shell(command, opts \\ []) when is_binary(command) do + command |> String.trim() |> do_shell(opts) + end + + defp do_shell("", _opts), do: {"", 0} + + defp do_shell(command, opts) do assert_no_null_byte!(command, "System.shell/2") {close_stdin?, opts} = Keyword.pop(opts, :close_stdin, false) diff --git a/lib/elixir/test/elixir/system_test.exs b/lib/elixir/test/elixir/system_test.exs index dde86e5a2c..56f9c84df7 100644 --- a/lib/elixir/test/elixir/system_test.exs +++ b/lib/elixir/test/elixir/system_test.exs @@ -209,6 +209,11 @@ defmodule SystemTest do assert {"1\n2\n", 0} = System.shell("x=1; echo $x; echo '2'") end + test "shell/1 with empty string" do + assert {"", 0} = System.shell("") + assert {"", 0} = System.shell(" ") + end + @tag timeout: 1_000 test "shell/1 returns when command awaits input" do assert {"", 0} = System.shell("cat", close_stdin: true) From 8e1c0787baf55fd6966d8729e1f53af171174921 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Samson?= Date: Sat, 10 Feb 2024 15:18:49 +0100 Subject: [PATCH 0379/1886] Document `:max_failures_reached` event in `ExUnit.Formatter` (#13332) --- lib/ex_unit/lib/ex_unit/formatter.ex | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/ex_unit/lib/ex_unit/formatter.ex b/lib/ex_unit/lib/ex_unit/formatter.ex index da41f6c3ef..f3661fe78b 100644 --- a/lib/ex_unit/lib/ex_unit/formatter.ex +++ b/lib/ex_unit/lib/ex_unit/formatter.ex @@ -30,6 +30,10 @@ defmodule ExUnit.Formatter do the VM is going to shutdown. It receives the test cases (or test module in case of `setup_all`) still running. + * `:max_failures_reached` - + the test run has been aborted due to reaching max failures limit set + with `:max_failures` option + The formatter will also receive the following events but they are deprecated and should be ignored: From cc7b8f3f3df79b83e6e69bc7593538092b8e7553 Mon Sep 17 00:00:00 2001 From: felipe stival <14948182+v0idpwn@users.noreply.github.com> Date: Sun, 11 Feb 2024 07:50:27 -0300 Subject: [PATCH 0380/1886] Fix bug in iex history (#13333) Makes the pruning update the history size. Without this update, the size didn't reflect the actual length of the history, which caused the error message to return non-sensical values as the upper limit of the history. --- lib/iex/lib/iex/history.ex | 6 +++--- lib/iex/test/iex/helpers_test.exs | 9 +++++++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/lib/iex/lib/iex/history.ex b/lib/iex/lib/iex/history.ex index 2006f46761..a19cf44e53 100644 --- a/lib/iex/lib/iex/history.ex +++ b/lib/iex/lib/iex/history.ex @@ -55,7 +55,7 @@ defmodule IEx.History do end def nth(%History{size: size, start: start}, n) do - raise "v(#{n}) is out of bounds, the currently preserved history ranges from #{start} to #{start + size} " <> + raise "v(#{n}) is out of bounds, the currently preserved history ranges from #{start} to #{start + size - 1} " <> "(or use negative numbers to look from the end)" end @@ -95,10 +95,10 @@ defmodule IEx.History do {collect?, %{state | start: counter}} end - defp prune(%{queue: q} = state, counter, limit, collect?) do + defp prune(%{queue: q, size: size} = state, counter, limit, collect?) do {{:value, entry}, q} = :queue.out(q) collect? = collect? || has_binary(entry) - prune(%{state | queue: q}, counter + 1, limit, collect?) + prune(%{state | queue: q, size: size - 1}, counter + 1, limit, collect?) end # Checks val and each of its elements (if it is a list or a tuple) diff --git a/lib/iex/test/iex/helpers_test.exs b/lib/iex/test/iex/helpers_test.exs index 171acc72ab..7c289ccf8e 100644 --- a/lib/iex/test/iex/helpers_test.exs +++ b/lib/iex/test/iex/helpers_test.exs @@ -1048,6 +1048,15 @@ defmodule IEx.HelpersTest do assert capture_iex("1\n2\nv(2)") == capture_iex("1\n2\nv(-1)") assert capture_iex("1\n2\nv(2)") == capture_iex("1\n2\nv()") end + + test "returns proper error when trying to access history out of bounds" do + # We evaluate 22 statements as 20 is the current limit for iex history + assert capture_iex(""" + \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n + v(1) + """) =~ + "(RuntimeError) v(1) is out of bounds, the currently preserved history ranges from 2 to 22" + end end describe "flush" do From 338476352de2d8d68f12ad303c8f770e8dee5142 Mon Sep 17 00:00:00 2001 From: Juan Barrios <03juan@users.noreply.github.com> Date: Sun, 11 Feb 2024 13:30:53 +0200 Subject: [PATCH 0381/1886] Fix docs for Node.spawn/5 (#13334) --- lib/elixir/lib/node.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/lib/node.ex b/lib/elixir/lib/node.ex index b323faf151..a7ab13e45c 100644 --- a/lib/elixir/lib/node.ex +++ b/lib/elixir/lib/node.ex @@ -219,7 +219,7 @@ defmodule Node do If `node` does not exist, a useless PID is returned. - For the list of available options, see `:erlang.spawn/4`. + For the list of available options, see `:erlang.spawn_opt/5`. Inlined by the compiler. """ From 2f22a3fcc0462ad408ab3a778450b3a738a857a5 Mon Sep 17 00:00:00 2001 From: felipe stival <14948182+v0idpwn@users.noreply.github.com> Date: Sun, 11 Feb 2024 16:24:08 -0300 Subject: [PATCH 0382/1886] Fix unbounded growth regression in iex history (#13336) --- lib/iex/lib/iex/history.ex | 2 +- lib/iex/test/iex/helpers_test.exs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/iex/lib/iex/history.ex b/lib/iex/lib/iex/history.ex index a19cf44e53..70c05a8012 100644 --- a/lib/iex/lib/iex/history.ex +++ b/lib/iex/lib/iex/history.ex @@ -91,7 +91,7 @@ defmodule IEx.History do {false, state} end - defp prune(%{size: size} = state, counter, limit, collect?) when size - counter < limit do + defp prune(%{size: size} = state, counter, limit, collect?) when size <= limit do {collect?, %{state | start: counter}} end diff --git a/lib/iex/test/iex/helpers_test.exs b/lib/iex/test/iex/helpers_test.exs index 7c289ccf8e..33528e1ca3 100644 --- a/lib/iex/test/iex/helpers_test.exs +++ b/lib/iex/test/iex/helpers_test.exs @@ -1055,7 +1055,7 @@ defmodule IEx.HelpersTest do \n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n v(1) """) =~ - "(RuntimeError) v(1) is out of bounds, the currently preserved history ranges from 2 to 22" + "(RuntimeError) v(1) is out of bounds, the currently preserved history ranges from 3 to 22" end end From a2676855fd2bfddbc47d5c6d4764861cdf3b305a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sun, 11 Feb 2024 23:36:33 +0100 Subject: [PATCH 0383/1886] Remove unused Args argument --- lib/elixir/src/elixir_compiler.erl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/elixir/src/elixir_compiler.erl b/lib/elixir/src/elixir_compiler.erl index 7d7cf88430..8f8e5e88a7 100644 --- a/lib/elixir/src/elixir_compiler.erl +++ b/lib/elixir/src/elixir_compiler.erl @@ -16,7 +16,7 @@ quoted(Forms, File, Callback) -> elixir_lexical:run( Env, - fun (LexicalEnv) -> maybe_fast_compile(Forms, [], LexicalEnv) end, + fun (LexicalEnv) -> maybe_fast_compile(Forms, LexicalEnv) end, fun (#{lexical_tracker := Pid}) -> Callback(File, Pid) end ), @@ -32,11 +32,11 @@ file(File, Callback) -> %% Evaluates the given code through the Erlang compiler. %% It may end-up evaluating the code if it is deemed a %% more efficient strategy depending on the code snippet. -maybe_fast_compile(Forms, Args, E) -> +maybe_fast_compile(Forms, E) -> case (?key(E, module) == nil) andalso allows_fast_compilation(Forms) andalso (not elixir_config:is_bootstrap()) of true -> fast_compile(Forms, E); - false -> compile(Forms, Args, [], E) + false -> compile(Forms, [], [], E) end, ok. From 3de7164dd1de4ac73f24cb94bf42da55d512e771 Mon Sep 17 00:00:00 2001 From: Jean Klingler Date: Mon, 12 Feb 2024 17:11:34 +0900 Subject: [PATCH 0384/1886] Fix bit offset in Module.Types.Descr (#13338) --- lib/elixir/lib/module/types/descr.ex | 26 +++++++++---------- .../test/elixir/module/types/descr_test.exs | 19 ++++++++++++++ 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex index b403852d6b..bfc7414a17 100644 --- a/lib/elixir/lib/module/types/descr.ex +++ b/lib/elixir/lib/module/types/descr.ex @@ -10,19 +10,19 @@ defmodule Module.Types.Descr do # the dynamic type. import Bitwise - @bit_binary 1 <<< 1 - @bit_empty_list 1 <<< 2 - @bit_integer 1 <<< 3 - @bit_float 1 <<< 4 - @bit_pid 1 <<< 5 - @bit_port 1 <<< 6 - @bit_reference 1 <<< 7 - - @bit_non_empty_list 1 <<< 8 - @bit_map 1 <<< 9 - @bit_tuple 1 <<< 10 - @bit_fun 1 <<< 11 - @bit_top (1 <<< 12) - 1 + @bit_binary 1 <<< 0 + @bit_empty_list 1 <<< 1 + @bit_integer 1 <<< 2 + @bit_float 1 <<< 3 + @bit_pid 1 <<< 4 + @bit_port 1 <<< 5 + @bit_reference 1 <<< 6 + + @bit_non_empty_list 1 <<< 7 + @bit_map 1 <<< 8 + @bit_tuple 1 <<< 9 + @bit_fun 1 <<< 10 + @bit_top (1 <<< 11) - 1 @atom_top {:negation, :sets.new(version: 2)} diff --git a/lib/elixir/test/elixir/module/types/descr_test.exs b/lib/elixir/test/elixir/module/types/descr_test.exs index 4155b7f89f..c39a436cb7 100644 --- a/lib/elixir/test/elixir/module/types/descr_test.exs +++ b/lib/elixir/test/elixir/module/types/descr_test.exs @@ -26,6 +26,25 @@ defmodule Module.Types.DescrTest do assert union(atom([:a]), negation(atom([:b]))) == negation(atom([:b])) assert union(negation(atom([:a, :b])), negation(atom([:b, :c]))) == negation(atom([:b])) end + + test "all primitive types" do + all = [ + atom(), + integer(), + float(), + binary(), + map(), + non_empty_list(), + empty_list(), + tuple(), + fun(), + pid(), + port(), + reference() + ] + + assert Enum.reduce(all, &union/2) == term() + end end describe "intersection" do From 809eccf5a0b1b24b0304759830394a2c4814ab22 Mon Sep 17 00:00:00 2001 From: felipe stival <14948182+v0idpwn@users.noreply.github.com> Date: Mon, 12 Feb 2024 09:18:58 -0300 Subject: [PATCH 0385/1886] Fix bug in negative lookups in v/1 (#13337) --- lib/iex/lib/iex/history.ex | 4 ++-- lib/iex/test/iex/helpers_test.exs | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/lib/iex/lib/iex/history.ex b/lib/iex/lib/iex/history.ex index 70c05a8012..e2d5480e9f 100644 --- a/lib/iex/lib/iex/history.ex +++ b/lib/iex/lib/iex/history.ex @@ -45,8 +45,8 @@ defmodule IEx.History do end # Traverses the queue back-to-front if the index is negative. - def nth(%History{queue: q, size: size, start: start}, n) - when n < 0 and size + n >= start - 1 do + def nth(%History{queue: q, size: size}, n) + when n < 0 and size + n >= 0 do get_nth(:queue.reverse(q), abs(n) - 1) end diff --git a/lib/iex/test/iex/helpers_test.exs b/lib/iex/test/iex/helpers_test.exs index 33528e1ca3..acc33522c2 100644 --- a/lib/iex/test/iex/helpers_test.exs +++ b/lib/iex/test/iex/helpers_test.exs @@ -1057,6 +1057,20 @@ defmodule IEx.HelpersTest do """) =~ "(RuntimeError) v(1) is out of bounds, the currently preserved history ranges from 3 to 22" end + + test "negative lookup works properly after crashes" do + capture = + capture_iex(""" + \n + \n + \n + Process.exit(self(), :some_reason)\n + :target_value + v(-1) + """) + + assert capture |> String.split("\n") |> List.last() == ":target_value" + end end describe "flush" do From 0ae3bb5c35883d75df2633c9b846516296ac9da2 Mon Sep 17 00:00:00 2001 From: Guillaume Duboc Date: Mon, 12 Feb 2024 22:46:31 +0100 Subject: [PATCH 0386/1886] Add gradual types to descr (#13330) Implements the dynamic type, and the full representation of set-theoretic gradual types. Modified operators (intersection, union, difference) and added subtyping Includes tests for subtyping and dynamic() Bugfix for singleton atoms: intersection of {:negation, []} and {:negation, []} no longer produces 0. --- lib/elixir/lib/module/types/descr.ex | 174 +++++++++++++++++- .../test/elixir/module/types/descr_test.exs | 61 +++++- 2 files changed, 231 insertions(+), 4 deletions(-) diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex index bfc7414a17..8d6e71ca5b 100644 --- a/lib/elixir/lib/module/types/descr.ex +++ b/lib/elixir/lib/module/types/descr.ex @@ -30,10 +30,11 @@ defmodule Module.Types.Descr do @term %{bitmap: @bit_top, atom: @atom_top} @none %{} + @dynamic %{dynamic: @term} # Type definitions - def dynamic(), do: :dynamic + def dynamic(), do: @dynamic def term(), do: @term def none(), do: @none @@ -60,11 +61,31 @@ defmodule Module.Types.Descr do Check type is empty. """ def empty?(descr), do: descr == @none + def term?(descr), do: Map.delete(descr, :dynamic) == @term + def gradual?(descr), do: is_map_key(descr, :dynamic) @doc """ Computes the union of two descrs. """ def union(%{} = left, %{} = right) do + is_gradual_left = gradual?(left) + is_gradual_right = gradual?(right) + + cond do + is_gradual_left and not is_gradual_right -> + right_with_dynamic = Map.put(right, :dynamic, right) + union_static(left, right_with_dynamic) + + is_gradual_right and not is_gradual_left -> + left_with_dynamic = Map.put(left, :dynamic, left) + union_static(left_with_dynamic, right) + + true -> + union_static(left, right) + end + end + + defp union_static(left, right) do # Erlang maps:merge_with/3 has to preserve the order in combiner. # We don't care about the order, so we have a faster implementation. if map_size(left) > map_size(right) do @@ -77,11 +98,30 @@ defmodule Module.Types.Descr do @compile {:inline, union: 3} defp union(:bitmap, v1, v2), do: bitmap_union(v1, v2) defp union(:atom, v1, v2), do: atom_union(v1, v2) + defp union(:dynamic, v1, v2), do: dynamic_union(v1, v2) @doc """ Computes the intersection of two descrs. """ def intersection(%{} = left, %{} = right) do + is_gradual_left = gradual?(left) + is_gradual_right = gradual?(right) + + cond do + is_gradual_left and not is_gradual_right -> + right_with_dynamic = Map.put(right, :dynamic, right) + intersection_static(left, right_with_dynamic) + + is_gradual_right and not is_gradual_left -> + left_with_dynamic = Map.put(left, :dynamic, left) + intersection_static(left_with_dynamic, right) + + true -> + intersection_static(left, right) + end + end + + defp intersection_static(left, right) do # Erlang maps:intersect_with/3 has to preserve the order in combiner. # We don't care about the order, so we have a faster implementation. if map_size(left) > map_size(right) do @@ -95,11 +135,27 @@ defmodule Module.Types.Descr do @compile {:inline, intersection: 3} defp intersection(:bitmap, v1, v2), do: bitmap_intersection(v1, v2) defp intersection(:atom, v1, v2), do: atom_intersection(v1, v2) + defp intersection(:dynamic, v1, v2), do: dynamic_intersection(v1, v2) @doc """ Computes the difference between two types. """ def difference(left = %{}, right = %{}) do + if gradual?(left) or gradual?(right) do + {left_dynamic, left_static} = Map.pop(left, :dynamic, left) + {right_dynamic, right_static} = Map.pop(right, :dynamic, right) + dynamic_part = difference_static(left_dynamic, right_static) + + if empty?(dynamic_part), + do: @none, + else: Map.put(difference_static(left_static, right_dynamic), :dynamic, dynamic_part) + else + difference_static(left, right) + end + end + + # For static types, the difference is component-wise. + defp difference_static(left, right) do iterator_difference(:maps.next(:maps.iterator(right)), left) end @@ -107,6 +163,7 @@ defmodule Module.Types.Descr do @compile {:inline, difference: 3} defp difference(:bitmap, v1, v2), do: bitmap_difference(v1, v2) defp difference(:atom, v1, v2), do: atom_difference(v1, v2) + defp difference(:dynamic, v1, v2), do: dynamic_difference(v1, v2) @doc """ Compute the negation of a type. @@ -126,6 +183,7 @@ defmodule Module.Types.Descr do @compile {:inline, to_quoted: 2} defp to_quoted(:bitmap, val), do: bitmap_to_quoted(val) defp to_quoted(:atom, val), do: atom_to_quoted(val) + defp to_quoted(:dynamic, descr), do: dynamic_to_quoted(descr) @doc """ Converts a descr to its quoted string representation. @@ -188,6 +246,51 @@ defmodule Module.Types.Descr do defp iterator_difference(:none, map), do: map + ## Type relations + + @doc """ + Check if a type is a subtype of another. + + If `left = (left_dyn and dynamic()) or left_static` + and `right = (right_dyn and dynamic()) or right_static` + + then the gradual subtyping relation defined in Definition 6.5 page 125 of + the thesis https://vlanvin.fr/papers/thesis.pdf is: + + `left <= right` if and only if + - `left_static <= right_static` + - `left_dyn <= right_dyn` + + Because of the dynamic/static invariant in the `descr`, subtyping can be + simplified in several cases according to which type is gradual or not. + """ + def subtype?(%{} = left, %{} = right) do + is_grad_left = gradual?(left) + is_grad_right = gradual?(right) + + cond do + is_grad_left and not is_grad_right -> + left_dynamic = Map.get(left, :dynamic) + subtype_static(left_dynamic, right) + + is_grad_right and not is_grad_left -> + right_static = Map.delete(right, :dynamic) + subtype_static(left, right_static) + + true -> + subtype_static(left, right) + end + end + + defp subtype_static(left, right), do: empty?(difference_static(left, right)) + + @doc """ + Check if a type is equal to another. + + It is currently not optimized. Only to be used in tests. + """ + def equal?(left, right), do: subtype?(left, right) and subtype?(right, left) + ## Bitmaps defp bitmap_union(v1, v2), do: v1 ||| v2 @@ -228,6 +331,10 @@ defmodule Module.Types.Descr do # # `{:negation, :sets.new()}` is the `atom()` top type, as it is the difference # of `atom()` with an empty list. + # + # `{:union, :sets.new()}` is the empty type for atoms, as it is the union of + # an empty list of atoms. It is simplified to `0` in set operations, and the key + # is removed from the map. defp atom_new(as) when is_list(as), do: {:union, :sets.from_list(as, version: 2)} @@ -240,7 +347,7 @@ defmodule Module.Types.Descr do {:negation, :union} -> {:union, :sets.subtract(s2, s1)} end - if :sets.size(s) == 0, do: 0, else: {tag, s} + if tag == :union and :sets.size(s) == 0, do: 0, else: {tag, s} end defp atom_union({:union, s1}, {:union, s2}), do: {:union, :sets.union(s1, s2)} @@ -257,7 +364,7 @@ defmodule Module.Types.Descr do {:negation, :union} -> {:negation, :sets.union(s1, s2)} end - if :sets.size(s) == 0, do: 0, else: {tag, s} + if tag == :union and :sets.size(s) == 0, do: 0, else: {tag, s} end defp literal(lit), do: {:__block__, [], [lit]} @@ -286,4 +393,65 @@ defmodule Module.Types.Descr do end |> List.wrap() end + + # Dynamic + # + # A type with a dynamic component is a gradual type; without, it is a static + # type. The dynamic component itself is a static type; hence, any gradual type + # can be written using a pair of static types as the union: + # + # `type = (dynamic_component and dynamic()) or static_component` + # + # where the `static_component` is simply the rest of the `descr`, and `dynamic()` + # is the base type that can represent any value at runtime. The dynamic and + # static parts can be considered separately for a mixed-typed analysis. For + # example, the type + # + # `type = (dynamic() and integer()) or boolean()` + # + # denotes an expression that evaluates to booleans or integers; however, there is + # uncertainty about the source of these integers. In static mode, the + # type-checker refuses to apply a function of type `boolean() -> boolean()` to + # this argument (since the argument may turn out to be an integer()), but in + # dynamic mode, it considers the type obtained by replacing `dynamic()` with + # `none()` (that is, `boolean()`), accepts the application, but types it with a + # type that contains `dynamic()`. + # + # When pattern matching on an expression of type `type`, the static part (here, + # booleans) has to be handled exhaustively. In contrast, the dynamic part can + # produce potential warnings in specific user-induced conditions, such as asking + # for stronger enforcement of static types. + # + # During construction and through set operations, we maintain the invariant that + # the dynamic component is a supertype of the static one, formally + # `dynamic_component >= static_component` + # + # With this invariant, the dynamic component always represents every value that + # a given gradual type can take at runtime, allowing us to simplify set operations, + # compared, for example, to keeping only the extra dynamic type that can obtained + # on top of the static type. Though, the latter may be used for printing purposes. + # + # There are two ways for a descr to represent a static type: either the + # `:dynamic` field is unset, or it contains a type equal to the static component + # (that is, there are no extra dynamic values). + + defp dynamic_intersection(left, right) do + inter = intersection_static(left, right) + if empty?(inter), do: 0, else: inter + end + + defp dynamic_difference(left, right) do + diff = difference_static(left, right) + if empty?(diff), do: 0, else: diff + end + + defp dynamic_union(left, right), do: union_static(left, right) + + defp dynamic_to_quoted(%{} = descr) do + if term?(descr) do + [{:dynamic, [], []}] + else + [{:and, [], [to_quoted(descr), {:dynamic, [], []}]}] + end + end end diff --git a/lib/elixir/test/elixir/module/types/descr_test.exs b/lib/elixir/test/elixir/module/types/descr_test.exs index c39a436cb7..48bec85fdb 100644 --- a/lib/elixir/test/elixir/module/types/descr_test.exs +++ b/lib/elixir/test/elixir/module/types/descr_test.exs @@ -26,7 +26,7 @@ defmodule Module.Types.DescrTest do assert union(atom([:a]), negation(atom([:b]))) == negation(atom([:b])) assert union(negation(atom([:a, :b])), negation(atom([:b, :c]))) == negation(atom([:b])) end - + test "all primitive types" do all = [ atom(), @@ -45,6 +45,13 @@ defmodule Module.Types.DescrTest do assert Enum.reduce(all, &union/2) == term() end + + test "dynamic" do + assert equal?(union(dynamic(), dynamic()), dynamic()) + assert equal?(union(dynamic(), term()), term()) + assert equal?(union(term(), dynamic()), term()) + assert equal?(union(intersection(dynamic(), atom()), atom()), atom()) + end end describe "intersection" do @@ -54,6 +61,7 @@ defmodule Module.Types.DescrTest do end test "term" do + assert intersection(term(), term()) == term() assert intersection(term(), float()) == float() assert intersection(term(), binary()) == binary() end @@ -64,10 +72,19 @@ defmodule Module.Types.DescrTest do end test "atom" do + assert intersection(atom(), atom()) == atom() assert intersection(atom(), atom([:a])) == atom([:a]) assert intersection(atom([:a]), atom([:b])) == none() assert intersection(atom([:a]), negation(atom([:b]))) == atom([:a]) end + + test "dynamic" do + assert equal?(intersection(dynamic(), dynamic()), dynamic()) + assert equal?(intersection(dynamic(), term()), dynamic()) + assert equal?(intersection(term(), dynamic()), dynamic()) + assert empty?(intersection(dynamic(), none())) + assert empty?(intersection(intersection(dynamic(), atom()), integer())) + end end describe "difference" do @@ -94,6 +111,40 @@ defmodule Module.Types.DescrTest do assert difference(atom([:a]), atom()) == none() assert difference(atom([:a]), atom([:b])) == atom([:a]) end + + test "dynamic" do + assert equal?(dynamic(), difference(dynamic(), dynamic())) + assert equal?(dynamic(), difference(term(), dynamic())) + assert empty?(difference(dynamic(), term())) + assert empty?(difference(none(), dynamic())) + end + end + + describe "subtype" do + test "bitmap" do + assert subtype?(integer(), union(integer(), float())) + assert subtype?(integer(), integer()) + assert subtype?(integer(), term()) + assert subtype?(none(), integer()) + assert subtype?(integer(), negation(float())) + end + + test "atom" do + assert subtype?(atom([:a]), atom()) + assert subtype?(atom([:a]), atom([:a])) + assert subtype?(atom([:a]), term()) + assert subtype?(none(), atom([:a])) + assert subtype?(atom([:a]), atom([:a, :b])) + assert subtype?(atom([:a]), negation(atom([:b]))) + end + + test "dynamic" do + assert subtype?(dynamic(), term()) + assert subtype?(dynamic(), dynamic()) + refute subtype?(term(), dynamic()) + assert subtype?(intersection(dynamic(), integer()), integer()) + assert subtype?(integer(), union(dynamic(), integer())) + end end describe "to_quoted" do @@ -124,5 +175,13 @@ defmodule Module.Types.DescrTest do assert atom([true, :a]) |> to_quoted_string() == ":a or true" assert difference(atom(), boolean()) |> to_quoted_string() == "atom() and not boolean()" end + + test "dynamic" do + assert dynamic() |> to_quoted_string() == "dynamic()" + assert intersection(atom(), dynamic()) |> to_quoted_string() == "atom() and dynamic()" + + assert union(atom([:foo, :bar]), dynamic()) |> to_quoted_string() == + "dynamic() or (:bar or :foo)" + end end end From d99a1da5651bd2cd5f1314e93ec798f176f03335 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 12 Feb 2024 22:54:58 +0100 Subject: [PATCH 0387/1886] Do not hardcode dynamic when refining variables in patterns and guards --- lib/elixir/lib/module/types/of.ex | 3 ++- lib/elixir/lib/module/types/pattern.ex | 11 +---------- lib/elixir/test/elixir/module/types/descr_test.exs | 2 +- 3 files changed, 4 insertions(+), 12 deletions(-) diff --git a/lib/elixir/lib/module/types/of.ex b/lib/elixir/lib/module/types/of.ex index 1c74e17989..210d3d8fcf 100644 --- a/lib/elixir/lib/module/types/of.ex +++ b/lib/elixir/lib/module/types/of.ex @@ -111,7 +111,8 @@ defmodule Module.Types.Of do with {:ok, actual_type, context} <- of_fun.(left, {expected_type, expr}, stack, context) do # If we are in a pattern and we have a variable, the refinement - # will already have checked the type, so we skip the check here. + # will already have checked the type, so we skip the check here + # as an optimization. # TODO: properly handle dynamic. Do we need materialization? if actual_type == dynamic() or (kind == :pattern and is_var(left)) or diff --git a/lib/elixir/lib/module/types/pattern.ex b/lib/elixir/lib/module/types/pattern.ex index eef050aac8..afd3b8de85 100644 --- a/lib/elixir/lib/module/types/pattern.ex +++ b/lib/elixir/lib/module/types/pattern.ex @@ -33,16 +33,7 @@ defmodule Module.Types.Pattern do case context.vars do %{^version => %{type: old_type, off_traces: off_traces} = data} -> - dynamic = dynamic() - - # TODO: Properly compute intersection and union of dynamic - new_type = - if old_type == dynamic or type == dynamic do - dynamic - else - intersection(type, old_type) - end - + new_type = intersection(type, old_type) data = %{data | type: new_type, off_traces: new_trace(expr, type, stack, off_traces)} context = put_in(context.vars[version], data) diff --git a/lib/elixir/test/elixir/module/types/descr_test.exs b/lib/elixir/test/elixir/module/types/descr_test.exs index 48bec85fdb..3bbefc3874 100644 --- a/lib/elixir/test/elixir/module/types/descr_test.exs +++ b/lib/elixir/test/elixir/module/types/descr_test.exs @@ -26,7 +26,7 @@ defmodule Module.Types.DescrTest do assert union(atom([:a]), negation(atom([:b]))) == negation(atom([:b])) assert union(negation(atom([:a, :b])), negation(atom([:b, :c]))) == negation(atom([:b])) end - + test "all primitive types" do all = [ atom(), From 672f962d565405be1dbdb324ce067d8b37cb72b0 Mon Sep 17 00:00:00 2001 From: Jean Klingler Date: Tue, 13 Feb 2024 07:38:49 +0900 Subject: [PATCH 0388/1886] Fix :trim_doc example in doc (#13340) --- lib/elixir/lib/file.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/elixir/lib/file.ex b/lib/elixir/lib/file.ex index 1092899526..b0fa3572c8 100644 --- a/lib/elixir/lib/file.ex +++ b/lib/elixir/lib/file.ex @@ -1764,7 +1764,7 @@ defmodule File do ## Examples # Read a utf8 text file which may include BOM - File.stream!("./test/test.txt", encoding: :utf8, trim_bom: true) + File.stream!("./test/test.txt", [:trim_bom, encoding: :utf8]) # Read in 2048 byte chunks rather than lines File.stream!("./test/test.data", 2048) From e36624994050acecab041859a16530884a6c1959 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 13 Feb 2024 17:11:49 +0100 Subject: [PATCH 0389/1886] Add node to deprecate on_undefined_variable :warn --- lib/elixir/lib/code.ex | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/elixir/lib/code.ex b/lib/elixir/lib/code.ex index cdaa27ed22..e7971b81e8 100644 --- a/lib/elixir/lib/code.ex +++ b/lib/elixir/lib/code.ex @@ -1629,7 +1629,8 @@ defmodule Code do emit a warning and expand as to a local call to the zero-arity function of the same name (for example, `node` would be expanded as `node()`). This `:warn` behavior only exists for compatibility reasons when working - with old dependencies. + with old dependencies, its usage is discouraged and it will be removed + in future releases. It always returns `:ok`. Raises an error for invalid options. @@ -1680,6 +1681,7 @@ defmodule Code do end # TODO: Make this option have no effect on Elixir v2.0 + # TODO: Warn if mode is :warn on Elixir v1.19 def put_compiler_option(:on_undefined_variable, value) when value in [:raise, :warn] do :elixir_config.put(:on_undefined_variable, value) :ok From 2ba6f95e5d7e996aea700ff4f30732b75ece80c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 14 Feb 2024 14:58:16 +0100 Subject: [PATCH 0390/1886] Do not include dynamic() and ... if type is indivisible (#13341) --- lib/elixir/lib/module/types/descr.ex | 15 ++++++++++++--- .../test/elixir/module/types/descr_test.exs | 7 ++++++- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/lib/elixir/lib/module/types/descr.ex b/lib/elixir/lib/module/types/descr.ex index 8d6e71ca5b..b823f3de7c 100644 --- a/lib/elixir/lib/module/types/descr.ex +++ b/lib/elixir/lib/module/types/descr.ex @@ -448,10 +448,19 @@ defmodule Module.Types.Descr do defp dynamic_union(left, right), do: union_static(left, right) defp dynamic_to_quoted(%{} = descr) do - if term?(descr) do - [{:dynamic, [], []}] + cond do + term?(descr) -> [{:dynamic, [], []}] + single = indivisible_bitmap(descr) -> [single] + true -> [{:and, [], [{:dynamic, [], []}, to_quoted(descr)]}] + end + end + + defp indivisible_bitmap(descr) do + with %{bitmap: bitmap} when map_size(descr) == 1 <- descr, + [single] <- bitmap_to_quoted(bitmap) do + single else - [{:and, [], [to_quoted(descr), {:dynamic, [], []}]}] + _ -> nil end end end diff --git a/lib/elixir/test/elixir/module/types/descr_test.exs b/lib/elixir/test/elixir/module/types/descr_test.exs index 3bbefc3874..c16238f3d7 100644 --- a/lib/elixir/test/elixir/module/types/descr_test.exs +++ b/lib/elixir/test/elixir/module/types/descr_test.exs @@ -178,7 +178,12 @@ defmodule Module.Types.DescrTest do test "dynamic" do assert dynamic() |> to_quoted_string() == "dynamic()" - assert intersection(atom(), dynamic()) |> to_quoted_string() == "atom() and dynamic()" + assert intersection(binary(), dynamic()) |> to_quoted_string() == "binary()" + + assert intersection(union(binary(), pid()), dynamic()) |> to_quoted_string() == + "dynamic() and (binary() or pid())" + + assert intersection(atom(), dynamic()) |> to_quoted_string() == "dynamic() and atom()" assert union(atom([:foo, :bar]), dynamic()) |> to_quoted_string() == "dynamic() or (:bar or :foo)" From 9100f9fee2c082b1f77f2c0e7849fa288b13a677 Mon Sep 17 00:00:00 2001 From: Kevin Genus Date: Thu, 15 Feb 2024 02:13:22 -0500 Subject: [PATCH 0391/1886] Fix grammar and phrasing in README.md (#13345) --- README.md | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 260968769a..9b24b44222 100644 --- a/README.md +++ b/README.md @@ -35,22 +35,22 @@ privately at **. All currently open bugs related to the Elixir repository are listed in the issues tracker. The Elixir team uses the issues tracker to focus -on *actionable items*, including planned enhancements in the short- and -medium-term. We also do our best to label entries for clarity and to ease +on *actionable items*, including planned enhancements in the short and +medium term. We also do our best to label entries for clarity and to ease collaboration. Our *actionable item policy* has some important consequences, such as: - * Proposing new features as well as request for support, help, and + * Proposing new features as well as requests for support, help, and guidance must be done in their own spaces, detailed next. - * Issues where we have identified to be outside of Elixir scope, - such as a bug upstream, will be closed (and requested to be moved + * Issues we have identified to be outside of Elixir's scope, + such as an upstream bug, will be closed (and requested to be moved elsewhere if appropriate). * We actively close unrelated and non-actionable issues to keep the - issues tracker tidy. However, we may get things wrong from time to - time, so we are glad to revisit issues and reopen if necessary. + issues tracker tidy. We may get things wrong from time to + time and will gladly revisit issues, reopening when necessary. Keep the tone positive and be kind! For more information, see the [Code of Conduct][1]. @@ -76,7 +76,7 @@ in the next release are then "closed" and added to the [changelog][7]. ### Discussions, support, and help -For general discussions, support, and help, please use many of the community +For general discussions, support, and help, please use the community spaces [listed on the sidebar of the Elixir website](https://elixir-lang.org/), such as forums, chat platforms, etc, where the wider community will be available to help you. @@ -107,9 +107,9 @@ Additionally, you may choose to run the test suite with `make clean test`. ## Contributing -We welcome everyone to contribute to Elixir. To do so, there are a few +We invite contributions to Elixir. To contribute, there are a few things you need to know about the code. First, Elixir code is divided -in applications inside the `lib` folder: +by each application inside the `lib` folder: * `elixir` - Elixir's kernel and standard library @@ -123,13 +123,13 @@ in applications inside the `lib` folder: * `mix` - Mix is Elixir's build tool -You can run all tests in the root directory with `make test` and you can -also run tests for a specific framework `make test_#{APPLICATION}`, for example, +You can run all tests in the root directory with `make test`. You can +also run tests for a specific framework with `make test_#{APPLICATION}`, for example, `make test_ex_unit`. If you just changed something in Elixir's standard library, you can run only that portion through `make test_stdlib`. -If you are changing just one file, you can choose to compile and run tests only -for that particular file for fast development cycles. For example, if you +If you are only changing one file, you can choose to compile and run tests +for that specific file for faster development cycles. For example, if you are changing the String module, you can compile it and run its tests as: ```sh @@ -150,7 +150,7 @@ make compile ``` After your changes are done, please remember to run `make format` to guarantee -all files are properly formatted and then run the full suite with +all files are properly formatted, then run the full suite with `make test`. If your contribution fails during the bootstrapping of the language, @@ -160,7 +160,7 @@ you can rebuild the language from scratch with: make clean_elixir compile ``` -Similarly, if you can't get Elixir to compile or the tests to pass after +Similarly, if you can not get Elixir to compile or the tests to pass after updating an existing checkout, run `make clean compile`. You can check [the official build status](https://github.com/elixir-lang/elixir/actions/workflows/ci.yml). More tasks can be found by reading the [Makefile](Makefile). @@ -180,7 +180,7 @@ Once a pull request is sent, the Elixir team will review your changes. We outline our process below to clarify the roles of everyone involved. All pull requests must be approved by two committers before being merged into -the repository. If any changes are necessary, the team will leave appropriate +the repository. If changes are necessary, the team will leave appropriate comments requesting changes to the code. Unfortunately, we cannot guarantee a pull request will be merged, even when modifications are requested, as the Elixir team will re-evaluate the contribution as it changes. @@ -200,8 +200,8 @@ a comment. ## Building documentation -Building the documentation requires [ExDoc](https://github.com/elixir-lang/ex_doc) -to be installed and built alongside Elixir: +Building the documentation requires that [ExDoc](https://github.com/elixir-lang/ex_doc) +is installed and built alongside Elixir: ```sh # After cloning and compiling Elixir, in its parent directory: From ac3ad4def652dac0385faf122161a9974517d7b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 15 Feb 2024 12:21:23 +0100 Subject: [PATCH 0392/1886] Add API for deleting SCM This is used by Hex when its application is stopped to revert its changes to Mix state. --- lib/mix/lib/mix/scm.ex | 8 ++++++++ lib/mix/test/mix/scm_test.exs | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/lib/mix/lib/mix/scm.ex b/lib/mix/lib/mix/scm.ex index 5efeb0c67d..92170f8eba 100644 --- a/lib/mix/lib/mix/scm.ex +++ b/lib/mix/lib/mix/scm.ex @@ -131,6 +131,14 @@ defmodule Mix.SCM do Mix.State.get(:scm) end + @doc """ + Deletes the given SCM from the list of available SCMs. + """ + @doc since: "1.16.2" + def delete(mod) when is_atom(mod) do + Mix.State.update(:scm, &List.delete(&1, mod)) + end + @doc """ Prepends the given SCM module to the list of available SCMs. """ diff --git a/lib/mix/test/mix/scm_test.exs b/lib/mix/test/mix/scm_test.exs index 4c107d5b5a..2ec3e515f5 100644 --- a/lib/mix/test/mix/scm_test.exs +++ b/lib/mix/test/mix/scm_test.exs @@ -12,10 +12,14 @@ defmodule Mix.SCMTest do test "prepends an SCM" do Mix.SCM.prepend(Hello) assert Enum.at(Mix.SCM.available(), 0) == Hello + Mix.SCM.delete(Hello) + assert Hello not in Mix.SCM.available() end test "appends an SCM" do Mix.SCM.append(Hello) assert Enum.at(Mix.SCM.available(), -1) == Hello + Mix.SCM.delete(Hello) + assert Hello not in Mix.SCM.available() end end From 04e2724591cb3a81a440960e1861a87c7a88b265 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 15 Feb 2024 12:48:53 +0100 Subject: [PATCH 0393/1886] Do not purge compiler modules during compilation, only after (#13346) The compiler freezing is mostly caused by a bug upstream, but it may be speed up compilation to avoid purging as we compile, as purging blocks the code server. A potential downside is higher memory usage during compilation. Closes #13264. --- lib/elixir/lib/kernel/parallel_compiler.ex | 2 ++ lib/elixir/src/elixir_code_server.erl | 26 ++++++++++++---------- lib/elixir/src/elixir_compiler.erl | 18 ++++++--------- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/lib/elixir/lib/kernel/parallel_compiler.ex b/lib/elixir/lib/kernel/parallel_compiler.ex index 4a9427e2bc..850a097bc0 100644 --- a/lib/elixir/lib/kernel/parallel_compiler.ex +++ b/lib/elixir/lib/kernel/parallel_compiler.ex @@ -467,9 +467,11 @@ defmodule Kernel.ParallelCompiler do case cycle_return do {:runtime, dependent_modules, extra_warnings} -> + :elixir_code_server.cast(:purge_compiler_modules) verify_modules(result, extra_warnings ++ warnings, dependent_modules, state) {:compile, [], extra_warnings} -> + :elixir_code_server.cast(:purge_compiler_modules) verify_modules(result, extra_warnings ++ warnings, [], state) {:compile, more, extra_warnings} -> diff --git a/lib/elixir/src/elixir_code_server.erl b/lib/elixir/src/elixir_code_server.erl index 7b1c965651..f4e7c58194 100644 --- a/lib/elixir/src/elixir_code_server.erl +++ b/lib/elixir/src/elixir_code_server.erl @@ -64,10 +64,8 @@ handle_call(retrieve_compiler_module, _From, Config) -> end; handle_call(purge_compiler_modules, _From, Config) -> - {Used, Unused, Counter} = Config#elixir_code_server.mod_pool, - _ = [code:purge(Module) || Module <- Used], - ModPool = {[], Used ++ Unused, Counter}, - {reply, {ok, length(Used)}, Config#elixir_code_server{mod_pool=ModPool}}; + {Used, NewConfig} = purge_compiler_modules(Config), + {reply, {ok, length(Used)}, NewConfig}; handle_call(Request, _From, Config) -> {stop, {badcall, Request}, Config}. @@ -91,17 +89,15 @@ handle_cast({unrequire_files, Files}, Config) -> Unrequired = maps:without(Files, Current), {noreply, Config#elixir_code_server{required=Unrequired}}; -handle_cast({return_compiler_module, Module, Purgeable}, Config) -> +handle_cast({return_compiler_module, Module}, Config) -> {Used, Unused, Counter} = Config#elixir_code_server.mod_pool, - - ModPool = - case Purgeable of - true -> {Used, [Module | Unused], Counter}; - false -> {[Module | Used], Unused, Counter} - end, - + ModPool = {[Module | Used], Unused, Counter}, {noreply, Config#elixir_code_server{mod_pool=ModPool}}; +handle_cast(purge_compiler_modules, Config) -> + {_Used, NewConfig} = purge_compiler_modules(Config), + {noreply, NewConfig}; + handle_cast(Request, Config) -> {stop, {badcast, Request}, Config}. @@ -120,6 +116,12 @@ code_change(_Old, Config, _Extra) -> compiler_module(I) -> list_to_atom("elixir_compiler_" ++ integer_to_list(I)). +purge_compiler_modules(Config) -> + {Used, Unused, Counter} = Config#elixir_code_server.mod_pool, + _ = [code:purge(Module) || Module <- Used], + ModPool = {[], Used ++ Unused, Counter}, + {Used, Config#elixir_code_server{mod_pool=ModPool}}. + defmodule(Pid, Tuple, #elixir_code_server{mod_ets=ModEts} = Config) -> ets:insert(elixir_modules, Tuple), Ref = erlang:monitor(process, Pid), diff --git a/lib/elixir/src/elixir_compiler.erl b/lib/elixir/src/elixir_compiler.erl index 8f8e5e88a7..0e4edd149d 100644 --- a/lib/elixir/src/elixir_compiler.erl +++ b/lib/elixir/src/elixir_compiler.erl @@ -45,11 +45,11 @@ compile(Quoted, ArgsList, CompilerOpts, #{line := Line} = E) -> {Expanded, SE, EE} = elixir_expand:expand(Block, elixir_env:env_to_ex(E), E), elixir_env:check_unused_vars(SE, EE), - {Module, Fun, Purgeable} = + {Module, Fun} = elixir_erl_compiler:spawn(fun() -> spawned_compile(Expanded, CompilerOpts, E) end), Args = list_to_tuple(ArgsList), - {dispatch(Module, Fun, Args, Purgeable), SE, EE}. + {dispatch(Module, Fun, Args), SE, EE}. spawned_compile(ExExprs, CompilerOpts, #{line := Line, file := File} = E) -> {Vars, S} = elixir_erl_var:from_env(E), @@ -61,13 +61,12 @@ spawned_compile(ExExprs, CompilerOpts, #{line := Line, file := File} = E) -> {Module, Binary} = elixir_erl_compiler:noenv_forms(Forms, File, [nowarn_nomatch | CompilerOpts]), code:load_binary(Module, "", Binary), - {Module, Fun, is_purgeable(Module, Binary)}. + {Module, Fun}. -dispatch(Module, Fun, Args, Purgeable) -> +dispatch(Module, Fun, Args) -> Res = Module:Fun(Args), code:delete(Module), - Purgeable andalso code:purge(Module), - return_compiler_module(Module, Purgeable), + return_compiler_module(Module), Res. code_fun(nil) -> '__FILE__'; @@ -92,11 +91,8 @@ code_mod(Fun, Expr, Line, File, Module, Vars) when is_binary(File), is_integer(L retrieve_compiler_module() -> elixir_code_server:call(retrieve_compiler_module). -return_compiler_module(Module, Purgeable) -> - elixir_code_server:cast({return_compiler_module, Module, Purgeable}). - -is_purgeable(Module, Binary) -> - beam_lib:chunks(Binary, [labeled_locals]) == {ok, {Module, [{labeled_locals, []}]}}. +return_compiler_module(Module) -> + elixir_code_server:cast({return_compiler_module, Module}). allows_fast_compilation({'__block__', _, Exprs}) -> lists:all(fun allows_fast_compilation/1, Exprs); From 636dd575ea46adb327bcc8f8c204355e0cb4329b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 15 Feb 2024 15:47:43 +0100 Subject: [PATCH 0394/1886] Require Hex v2.0.6 due to Erlang/OTP 27 precompiled support --- lib/mix/lib/mix/hex.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mix/lib/mix/hex.ex b/lib/mix/lib/mix/hex.ex index 7b294de078..20b630e58e 100644 --- a/lib/mix/lib/mix/hex.ex +++ b/lib/mix/lib/mix/hex.ex @@ -1,7 +1,7 @@ defmodule Mix.Hex do @moduledoc false @compile {:no_warn_undefined, Hex} - @hex_requirement ">= 0.19.0" + @hex_requirement ">= 2.0.6" @hex_builds_url "https://builds.hex.pm" @doc """ From de74ea5d529545db891eb33430afc142419c8309 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 15 Feb 2024 17:24:11 +0100 Subject: [PATCH 0395/1886] Remove duplicate purge compiler modules call --- lib/elixir/lib/kernel/parallel_compiler.ex | 3 +++ lib/mix/lib/mix/compilers/elixir.ex | 1 - 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/elixir/lib/kernel/parallel_compiler.ex b/lib/elixir/lib/kernel/parallel_compiler.ex index 850a097bc0..f7c2fa6d8e 100644 --- a/lib/elixir/lib/kernel/parallel_compiler.ex +++ b/lib/elixir/lib/kernel/parallel_compiler.ex @@ -743,6 +743,9 @@ defmodule Kernel.ParallelCompiler do end defp return_error(warnings, errors, state, fun) do + # Also prune compiler modules in case of errors + :elixir_code_server.cast(:purge_compiler_modules) + errors = Enum.map(errors, fn {%{file: file} = diagnostic, read_snippet} -> :elixir_errors.print_diagnostic(diagnostic, read_snippet) diff --git a/lib/mix/lib/mix/compilers/elixir.ex b/lib/mix/lib/mix/compilers/elixir.ex index e3f416b9a4..a39a1abd02 100644 --- a/lib/mix/lib/mix/compilers/elixir.ex +++ b/lib/mix/lib/mix/compilers/elixir.ex @@ -213,7 +213,6 @@ defmodule Mix.Compilers.Elixir do {:error, previous_warnings(sources, all_warnings) ++ warnings ++ errors} after Code.compiler_options(previous_opts) - Code.purge_compiler_modules() end else # We need to return ok if deps_changed? or stale_modules changed, From 38a571b73a59b72b34a6d70501b3e20bda34ae0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 15 Feb 2024 20:33:31 +0100 Subject: [PATCH 0396/1886] Purge modules asynchronously (#13350) --- lib/elixir/src/elixir_code_server.erl | 33 +++++++++++++++++++-------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/lib/elixir/src/elixir_code_server.erl b/lib/elixir/src/elixir_code_server.erl index f4e7c58194..cee841cd93 100644 --- a/lib/elixir/src/elixir_code_server.erl +++ b/lib/elixir/src/elixir_code_server.erl @@ -64,8 +64,10 @@ handle_call(retrieve_compiler_module, _From, Config) -> end; handle_call(purge_compiler_modules, _From, Config) -> - {Used, NewConfig} = purge_compiler_modules(Config), - {reply, {ok, length(Used)}, NewConfig}; + {Used, Unused, Counter} = Config#elixir_code_server.mod_pool, + _ = [code:purge(Module) || Module <- Used], + ModPool = {[], Used ++ Unused, Counter}, + {reply, {ok, length(Used)}, Config#elixir_code_server{mod_pool=ModPool}}; handle_call(Request, _From, Config) -> {stop, {badcall, Request}, Config}. @@ -95,12 +97,29 @@ handle_cast({return_compiler_module, Module}, Config) -> {noreply, Config#elixir_code_server{mod_pool=ModPool}}; handle_cast(purge_compiler_modules, Config) -> - {_Used, NewConfig} = purge_compiler_modules(Config), - {noreply, NewConfig}; + {Used, Unused, Counter} = Config#elixir_code_server.mod_pool, + + case Used of + [] -> ok; + _ -> + Opts = [{monitor, [{tag, {purged, Used}}]}], + erlang:spawn_opt(fun() -> + [code:purge(Module) || Module <- Used], + ok + end, Opts) + end, + + ModPool = {[], Unused, Counter}, + {noreply, Config#elixir_code_server{mod_pool=ModPool}}; handle_cast(Request, Config) -> {stop, {badcast, Request}, Config}. +handle_info({{purged, Purged}, _Ref, process, _Pid, _Reason}, Config) -> + {Used, Unused, Counter} = Config#elixir_code_server.mod_pool, + ModPool = {Used, Purged ++ Unused, Counter}, + {noreply, Config#elixir_code_server{mod_pool=ModPool}}; + handle_info({'DOWN', Ref, process, _Pid, _Reason}, Config) -> {noreply, undefmodule(Ref, Config)}; @@ -116,12 +135,6 @@ code_change(_Old, Config, _Extra) -> compiler_module(I) -> list_to_atom("elixir_compiler_" ++ integer_to_list(I)). -purge_compiler_modules(Config) -> - {Used, Unused, Counter} = Config#elixir_code_server.mod_pool, - _ = [code:purge(Module) || Module <- Used], - ModPool = {[], Used ++ Unused, Counter}, - {Used, Config#elixir_code_server{mod_pool=ModPool}}. - defmodule(Pid, Tuple, #elixir_code_server{mod_ets=ModEts} = Config) -> ets:insert(elixir_modules, Tuple), Ref = erlang:monitor(process, Pid), From 91af0171cbfbc3e2e98f14818092065bc32aeb41 Mon Sep 17 00:00:00 2001 From: Jean Klingler Date: Fri, 16 Feb 2024 16:47:28 +0900 Subject: [PATCH 0397/1886] Fixes to support 0TP27 (#13351) * Fix non-deterministic key-value tests * Fix non-deterministic Enum tests * Fix :only option when deriving Inspect * Float.ceil and Float.floor return -0.0 for negative numbers * Fix non-deterministic Registry doctest * Simplify check --- lib/elixir/lib/enum.ex | 5 +---- lib/elixir/lib/float.ex | 16 +++++++++++++++- lib/elixir/lib/inspect.ex | 5 +++-- lib/elixir/lib/map.ex | 4 ++-- lib/elixir/lib/registry.ex | 8 ++++---- lib/elixir/test/elixir/enum_test.exs | 8 ++++---- lib/elixir/test/elixir/float_test.exs | 22 +++++++++++++++++++--- 7 files changed, 48 insertions(+), 20 deletions(-) diff --git a/lib/elixir/lib/enum.ex b/lib/elixir/lib/enum.ex index 2ce7c7a465..e81c961831 100644 --- a/lib/elixir/lib/enum.ex +++ b/lib/elixir/lib/enum.ex @@ -3809,9 +3809,6 @@ defmodule Enum do iex> Enum.unzip([{:a, 1}, {:b, 2}, {:c, 3}]) {[:a, :b, :c], [1, 2, 3]} - iex> Enum.unzip(%{a: 1, b: 2}) - {[:a, :b], [1, 2]} - """ @spec unzip(t) :: {[element], [element]} @@ -4067,7 +4064,7 @@ defmodule Enum do ...> end) [{1, 2, 3}, {1, 2, 3}] - iex> enums = [[1, 2], %{a: 3, b: 4}, [5, 6]] + iex> enums = [[1, 2], [a: 3, b: 4], [5, 6]] ...> Enum.zip_reduce(enums, [], fn elements, acc -> ...> [List.to_tuple(elements) | acc] ...> end) diff --git a/lib/elixir/lib/float.ex b/lib/elixir/lib/float.ex index 305395a38f..7810d21b3b 100644 --- a/lib/elixir/lib/float.ex +++ b/lib/elixir/lib/float.ex @@ -275,6 +275,8 @@ defmodule Float do -56.0 iex> Float.ceil(34.251, 2) 34.26 + iex> Float.ceil(-0.01) + -0.0 """ @spec ceil(float, precision_range) :: float @@ -332,6 +334,8 @@ defmodule Float do -6.0 iex> Float.round(12.341444444444441, 15) 12.341444444444441 + iex> Float.round(-0.01) + -0.0 """ @spec round(float, precision_range) :: float @@ -340,8 +344,13 @@ defmodule Float do # and could be implemented in the future. def round(float, precision \\ 0) + def round(float, 0) when float == 0.0, do: float + def round(float, 0) when is_float(float) do - float |> :erlang.round() |> :erlang.float() + case float |> :erlang.round() |> :erlang.float() do + zero when zero == 0.0 and float < 0.0 -> -0.0 + rounded -> rounded + end end def round(float, precision) when is_float(float) and precision in @precision_range do @@ -365,6 +374,8 @@ defmodule Float do case rounding do :ceil when sign === 0 -> 1 / power_of_10(precision) :floor when sign === 1 -> -1 / power_of_10(precision) + :ceil when sign === 1 -> -0.0 + :half_up when sign === 1 -> -0.0 _ -> 0.0 end @@ -394,6 +405,9 @@ defmodule Float do boundary = den <<< 52 cond do + num == 0 and sign == 1 -> + -0.0 + num == 0 -> 0.0 diff --git a/lib/elixir/lib/inspect.ex b/lib/elixir/lib/inspect.ex index d901f18916..326cefe0ba 100644 --- a/lib/elixir/lib/inspect.ex +++ b/lib/elixir/lib/inspect.ex @@ -535,7 +535,8 @@ end defimpl Inspect, for: Any do defmacro __deriving__(module, struct, options) do - fields = Map.keys(struct) -- [:__exception__, :__struct__] + fields = Enum.sort(Map.keys(struct) -- [:__exception__, :__struct__]) + only = Keyword.get(options, :only, fields) except = Keyword.get(options, :except, []) optional = Keyword.get(options, :optional, []) @@ -545,7 +546,7 @@ defimpl Inspect, for: Any do :ok = validate_option(:optional, optional, fields, module) inspect_module = - if fields == only and except == [] do + if fields == Enum.sort(only) and except == [] do Inspect.Map else Inspect.Any diff --git a/lib/elixir/lib/map.ex b/lib/elixir/lib/map.ex index 8082b40168..a9507dc60b 100644 --- a/lib/elixir/lib/map.ex +++ b/lib/elixir/lib/map.ex @@ -147,7 +147,7 @@ defmodule Map do ## Examples - iex> Map.keys(%{a: 1, b: 2}) + Map.keys(%{a: 1, b: 2}) [:a, :b] """ @@ -161,7 +161,7 @@ defmodule Map do ## Examples - iex> Map.values(%{a: 1, b: 2}) + Map.values(%{a: 1, b: 2}) [1, 2] """ diff --git a/lib/elixir/lib/registry.ex b/lib/elixir/lib/registry.ex index 42e963210a..418bc23462 100644 --- a/lib/elixir/lib/registry.ex +++ b/lib/elixir/lib/registry.ex @@ -1301,16 +1301,16 @@ defmodule Registry do iex> Registry.start_link(keys: :unique, name: Registry.SelectAllTest) iex> {:ok, _} = Registry.register(Registry.SelectAllTest, "hello", :value) iex> {:ok, _} = Registry.register(Registry.SelectAllTest, "world", :value) - iex> Registry.select(Registry.SelectAllTest, [{{:"$1", :"$2", :"$3"}, [], [{{:"$1", :"$2", :"$3"}}]}]) - [{"world", self(), :value}, {"hello", self(), :value}] + iex> Registry.select(Registry.SelectAllTest, [{{:"$1", :"$2", :"$3"}, [], [{{:"$1", :"$2", :"$3"}}]}]) |> Enum.sort() + [{"hello", self(), :value}, {"world", self(), :value}] Get all keys in the registry: iex> Registry.start_link(keys: :unique, name: Registry.SelectAllTest) iex> {:ok, _} = Registry.register(Registry.SelectAllTest, "hello", :value) iex> {:ok, _} = Registry.register(Registry.SelectAllTest, "world", :value) - iex> Registry.select(Registry.SelectAllTest, [{{:"$1", :_, :_}, [], [:"$1"]}]) - ["world", "hello"] + iex> Registry.select(Registry.SelectAllTest, [{{:"$1", :_, :_}, [], [:"$1"]}]) |> Enum.sort() + ["hello", "world"] """ @doc since: "1.9.0" diff --git a/lib/elixir/test/elixir/enum_test.exs b/lib/elixir/test/elixir/enum_test.exs index 11e715a008..a7aaa90b8b 100644 --- a/lib/elixir/test/elixir/enum_test.exs +++ b/lib/elixir/test/elixir/enum_test.exs @@ -56,9 +56,9 @@ defmodule EnumTest do end test "mix and match" do - enums = [[1, 2], %{a: 3, b: 4}, [5, 6]] + enums = [[1, 2], 3..4, [5, 6]] result = Enum.zip_reduce(enums, [], fn elements, acc -> [List.to_tuple(elements) | acc] end) - assert result == [{2, {:b, 4}, 6}, {1, {:a, 3}, 5}] + assert result == [{2, 4, 6}, {1, 3, 5}] end end @@ -412,7 +412,7 @@ defmodule EnumTest do assert Enum.into([a: 1, b: 2], %{c: 3}) == %{a: 1, b: 2, c: 3} assert Enum.into(MapSet.new(a: 1, b: 2), %{}) == %{a: 1, b: 2} assert Enum.into(MapSet.new(a: 1, b: 2), %{c: 3}) == %{a: 1, b: 2, c: 3} - assert Enum.into(%{a: 1, b: 2}, []) == [a: 1, b: 2] + assert Enum.into(%{a: 1, b: 2}, []) |> Enum.sort() == [a: 1, b: 2] assert Enum.into(1..3, []) == [1, 2, 3] assert Enum.into(["H", "i"], "") == "Hi" end @@ -1444,7 +1444,7 @@ defmodule EnumTest do test "unzip/1" do assert Enum.unzip([{:a, 1}, {:b, 2}, {:c, 3}]) == {[:a, :b, :c], [1, 2, 3]} assert Enum.unzip([]) == {[], []} - assert Enum.unzip(%{a: 1, b: 2}) == {[:a, :b], [1, 2]} + assert Enum.unzip(%{a: 1}) == {[:a], [1]} assert Enum.unzip(foo: "a", bar: "b") == {[:foo, :bar], ["a", "b"]} assert_raise FunctionClauseError, fn -> Enum.unzip([{:a, 1}, {:b, 2, "foo"}]) end diff --git a/lib/elixir/test/elixir/float_test.exs b/lib/elixir/test/elixir/float_test.exs index 34e991659c..e56c3b991a 100644 --- a/lib/elixir/test/elixir/float_test.exs +++ b/lib/elixir/test/elixir/float_test.exs @@ -104,9 +104,9 @@ defmodule FloatTest do assert Float.ceil(7.5432e3) === 7544.0 assert Float.ceil(7.5e-3) === 1.0 assert Float.ceil(-12.32453e4) === -123_245.0 - assert Float.ceil(-12.32453e-10) === 0.0 + assert Float.ceil(-12.32453e-10) === -0.0 assert Float.ceil(0.32453e-10) === 1.0 - assert Float.ceil(-0.32453e-10) === 0.0 + assert Float.ceil(-0.32453e-10) === -0.0 assert Float.ceil(1.32453e-10) === 1.0 assert Float.ceil(0.0) === 0.0 end @@ -130,7 +130,7 @@ defmodule FloatTest do assert Float.ceil(-12.524235, 3) === -12.524 assert Float.ceil(12.32453e-20, 2) === 0.01 - assert Float.ceil(-12.32453e-20, 2) === 0.0 + assert Float.ceil(-12.32453e-20, 2) === -0.0 assert Float.ceil(0.0, 2) === 0.0 @@ -139,6 +139,11 @@ defmodule FloatTest do end end + test "with small floats rounded up to -0.0" do + assert Float.ceil(-0.1, 0) === -0.0 + assert Float.ceil(-0.01, 1) === -0.0 + end + test "with subnormal floats" do assert Float.ceil(5.0e-324, 0) === 1.0 assert Float.ceil(5.0e-324, 1) === 0.1 @@ -172,6 +177,17 @@ defmodule FloatTest do end end + test "with small floats rounded to +0.0 / -0.0" do + assert Float.round(0.01, 0) === 0.0 + assert Float.round(0.01, 1) === 0.0 + + assert Float.round(-0.01, 0) === -0.0 + assert Float.round(-0.01, 1) === -0.0 + + assert Float.round(-0.49999, 0) === -0.0 + assert Float.round(-0.049999, 1) === -0.0 + end + test "with subnormal floats" do for precision <- 0..15 do assert Float.round(5.0e-324, precision) === 0.0 From 7baca5e00e673b0b1974533f5c4916721e830b1e Mon Sep 17 00:00:00 2001 From: Philip Munksgaard Date: Sat, 17 Feb 2024 12:51:07 +0100 Subject: [PATCH 0398/1886] Escape pinned values when computing diff (#13354) Take these two tests: ```elixir test "correctly colored" do assert [{:foo}] = [{:bar}] end test "incorrectly colored" do val = [{:foo}] assert ^val = [{:bar}] end ``` That's because when diffing a pin, we were not converting the underlying diff context from match to ===. This fixes #13348 --- lib/ex_unit/lib/ex_unit/diff.ex | 4 ++-- lib/ex_unit/test/ex_unit/diff_test.exs | 10 +++++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/lib/ex_unit/lib/ex_unit/diff.ex b/lib/ex_unit/lib/ex_unit/diff.ex index 06548cfed0..2fd75bfa51 100644 --- a/lib/ex_unit/lib/ex_unit/diff.ex +++ b/lib/ex_unit/lib/ex_unit/diff.ex @@ -286,10 +286,10 @@ defmodule ExUnit.Diff do defp diff_pin({:^, _, [var]} = pin, right, %{pins: pins} = env) do identifier = var_context(var) %{^identifier => pin_value} = pins - {diff, post_env} = diff_value(pin_value, right, env) + {diff, post_env} = diff_value(pin_value, right, %{env | context: :===}) diff_left = update_diff_meta(pin, not diff.equivalent?) - {%{diff | left: diff_left}, post_env} + {%{diff | left: diff_left}, %{post_env | context: :match}} end # Vars diff --git a/lib/ex_unit/test/ex_unit/diff_test.exs b/lib/ex_unit/test/ex_unit/diff_test.exs index e443c42a08..04340b8db5 100644 --- a/lib/ex_unit/test/ex_unit/diff_test.exs +++ b/lib/ex_unit/test/ex_unit/diff_test.exs @@ -251,7 +251,12 @@ defmodule ExUnit.DiffTest do refute_diff([:a, [:c, :b]] = [:a, [:b, :c]], "[:a, [-:c-, :b]]", "[:a, [:b, +:c+]]") refute_diff(:a = [:a, [:b, :c]], "-:a-", "+[:a, [:b, :c]]+") - pins = %{{:a, nil} => :a, {:b, nil} => :b, {:list_ab, nil} => [:a, :b]} + pins = %{ + {:a, nil} => :a, + {:b, nil} => :b, + {:list_ab, nil} => [:a, :b], + {:list_tuple, nil} => [{:foo}] + } assert_diff(x = [], [x: []], pins) assert_diff(x = [:a, :b], [x: [:a, :b]], pins) @@ -282,6 +287,9 @@ defmodule ExUnit.DiffTest do refute_diff([:a, :b] = :a, "-[:a, :b]-", "+:a+") refute_diff([:foo] = [:foo, {:a, :b, :c}], "[:foo]", "[:foo, +{:a, :b, :c}+]") + + refute_diff([{:foo}] = [{:bar}], "[{-:foo-}]", "[{+:bar+}]") + refute_diff(^list_tuple = [{:bar}], "-^list_tuple-", "[{+:bar+}]", pins) end test "improper lists" do From 514615d0347cb9bb513faa44ae1e36406979e516 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sun, 18 Feb 2024 11:25:19 +0100 Subject: [PATCH 0399/1886] Apply end of expression more consistently, closes #13355 --- lib/elixir/lib/macro.ex | 7 +- lib/elixir/src/elixir_parser.yrl | 21 +++--- lib/elixir/test/elixir/kernel/parser_test.exs | 64 ++++++++++++++++++- 3 files changed, 74 insertions(+), 18 deletions(-) diff --git a/lib/elixir/lib/macro.ex b/lib/elixir/lib/macro.ex index 7951e0f72f..5ee24d69b1 100644 --- a/lib/elixir/lib/macro.ex +++ b/lib/elixir/lib/macro.ex @@ -156,9 +156,10 @@ defmodule Macro do * `:end_of_expression` - denotes when the end of expression effectively happens (when `:token_metadata` is true). This is only available for - direct children of a `__block__`, and it is either the location of a - newline or of the `;` character. The last expression of `__block__` - does not have this metadata. + expressions inside "blocks of code", which are either direct children + of a `__block__` or the right side of `->`. The last expression of the + block does not have metadata if it is not followed by an end of line + character (either a newline or `;`) * `:indentation` - indentation of a sigil heredoc diff --git a/lib/elixir/src/elixir_parser.yrl b/lib/elixir/src/elixir_parser.yrl index 7a29b1311d..47cd547ad8 100644 --- a/lib/elixir/src/elixir_parser.yrl +++ b/lib/elixir/src/elixir_parser.yrl @@ -23,7 +23,7 @@ Nonterminals kw_eol kw_base kw_data kw_call call_args_no_parens_kw_expr call_args_no_parens_kw dot_op dot_alias dot_bracket_identifier dot_call_identifier dot_identifier dot_op_identifier dot_do_identifier dot_paren_identifier - do_block fn_eoe do_eoe end_eoe block_eoe block_item block_list + do_block fn_eoe do_eoe block_eoe block_item block_list . Terminals @@ -91,8 +91,8 @@ Nonassoc 330 dot_identifier. grammar -> eoe : {'__block__', meta_from_token('$1'), []}. grammar -> expr_list : build_block(reverse('$1')). grammar -> eoe expr_list : build_block(reverse('$2')). -grammar -> expr_list eoe : build_block(reverse('$1')). -grammar -> eoe expr_list eoe : build_block(reverse('$2')). +grammar -> expr_list eoe : build_block(reverse(annotate_eoe('$2', '$1'))). +grammar -> eoe expr_list eoe : build_block(reverse(annotate_eoe('$3', '$2'))). grammar -> '$empty' : {'__block__', [], []}. % Note expressions are on reverse order @@ -263,11 +263,9 @@ sub_matched_expr -> access_expr kw_identifier : error_invalid_kw_identifier('$2' access_expr -> bracket_at_expr : '$1'. access_expr -> bracket_expr : '$1'. access_expr -> capture_int int : build_unary_op('$1', number_value('$2')). -access_expr -> fn_eoe stab end_eoe : build_fn('$1', '$2', '$3'). -access_expr -> open_paren stab close_paren : build_paren_stab('$1', '$2', '$3'). -access_expr -> open_paren stab ';' close_paren : build_paren_stab('$1', '$2', '$4'). -access_expr -> open_paren ';' stab ';' close_paren : build_paren_stab('$1', '$3', '$5'). -access_expr -> open_paren ';' stab close_paren : build_paren_stab('$1', '$3', '$4'). +access_expr -> fn_eoe stab_eoe 'end' : build_fn('$1', '$2', '$3'). +access_expr -> open_paren stab_eoe ')' : build_paren_stab('$1', '$2', '$3'). +access_expr -> open_paren ';' stab_eoe ')' : build_paren_stab('$1', '$3', '$4'). access_expr -> open_paren ';' close_paren : build_paren_stab('$1', [], '$3'). access_expr -> empty_paren : warn_empty_paren('$1'), {'__block__', [], []}. access_expr -> int : handle_number(number_value('$1'), '$1', ?exprs('$1')). @@ -313,7 +311,7 @@ bracket_at_expr -> at_op_eol access_expr bracket_arg : do_block -> do_eoe 'end' : {do_end_meta('$1', '$2'), [[{handle_literal(do, '$1'), {'__block__', [], []}}]]}. -do_block -> do_eoe stab end_eoe : +do_block -> do_eoe stab_eoe 'end' : {do_end_meta('$1', '$3'), [[{handle_literal(do, '$1'), build_stab('$2')}]]}. do_block -> do_eoe block_list 'end' : {do_end_meta('$1', '$3'), [[{handle_literal(do, '$1'), {'__block__', [], []}} | '$2']]}. @@ -330,9 +328,6 @@ fn_eoe -> 'fn' eoe : next_is_eol('$1', '$2'). do_eoe -> 'do' : '$1'. do_eoe -> 'do' eoe : '$1'. -end_eoe -> 'end' : '$1'. -end_eoe -> eoe 'end' : '$2'. - block_eoe -> block_identifier : '$1'. block_eoe -> block_identifier eoe : '$1'. @@ -340,7 +335,7 @@ stab -> stab_expr : ['$1']. stab -> stab eoe stab_expr : ['$3' | annotate_eoe('$2', '$1')]. stab_eoe -> stab : '$1'. -stab_eoe -> stab eoe : '$1'. +stab_eoe -> stab eoe : annotate_eoe('$2', '$1'). stab_expr -> expr : '$1'. diff --git a/lib/elixir/test/elixir/kernel/parser_test.exs b/lib/elixir/test/elixir/kernel/parser_test.exs index da3cf66f16..39d2485623 100644 --- a/lib/elixir/test/elixir/kernel/parser_test.exs +++ b/lib/elixir/test/elixir/kernel/parser_test.exs @@ -407,19 +407,79 @@ defmodule Kernel.ParserTest do line: 4, column: 1 ], []}, - {:five, [closing: [line: 7, column: 6], line: 7, column: 1], []} + {:five, + [ + end_of_expression: [newlines: 1, line: 7, column: 7], + closing: [line: 7, column: 6], + line: 7, + column: 1 + ], []} ] assert Code.string_to_quoted!(file, token_metadata: true, columns: true) == {:__block__, [], args} end + test "adds end_of_expression to the right hand side of ->" do + file = """ + case true do + :foo -> bar(); two() + :baz -> bat() + end + """ + + assert Code.string_to_quoted!(file, token_metadata: true) == + {:case, + [ + end_of_expression: [newlines: 1, line: 4], + do: [line: 1], + end: [line: 4], + line: 1 + ], + [ + true, + [ + do: [ + {:->, [line: 2], + [ + [:foo], + {:__block__, [], + [ + {:bar, + [ + end_of_expression: [newlines: 0, line: 2], + closing: [line: 2], + line: 2 + ], []}, + {:two, + [ + end_of_expression: [newlines: 1, line: 2], + closing: [line: 2], + line: 2 + ], []} + ]} + ]}, + {:->, [line: 3], + [ + [:baz], + {:bat, + [ + end_of_expression: [newlines: 1, line: 3], + closing: [line: 3], + line: 3 + ], []} + ]} + ] + ] + ]} + end + test "does not add end of expression to ->" do file = """ case true do :foo -> :bar :baz -> :bat - end + end\ """ assert Code.string_to_quoted!(file, token_metadata: true) == From 28248f4fc58cdd2fd1c7f0f420e36fa276d2e163 Mon Sep 17 00:00:00 2001 From: Jean Klingler Date: Mon, 19 Feb 2024 19:36:05 +0900 Subject: [PATCH 0400/1886] Fix assertion for ex_unit test in OTP27 (#13357) Most likely due to this change https://github.com/erlang/otp/commit/be9c343f7188f5845c8e2f31a614e4d257452abf --- lib/ex_unit/test/ex_unit/capture_io_test.exs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/lib/ex_unit/test/ex_unit/capture_io_test.exs b/lib/ex_unit/test/ex_unit/capture_io_test.exs index f2f6a6f38b..d26d3f96b8 100644 --- a/lib/ex_unit/test/ex_unit/capture_io_test.exs +++ b/lib/ex_unit/test/ex_unit/capture_io_test.exs @@ -216,7 +216,15 @@ defmodule ExUnit.CaptureIOTest do end) capture_io("\"a", fn -> - assert :io.scan_erl_form(~c">") == {:error, {1, :erl_scan, {:string, 34, ~c"a"}}, 1} + # TODO: Remove me when we require Erlang/OTP 27+ + expected_error = + if :erlang.system_info(:otp_release) >= ~c"27" do + {1, :erl_scan, {:unterminated, :string, ~c"a"}} + else + {1, :erl_scan, {:string, 34, ~c"a"}} + end + + assert :io.scan_erl_form(~c">") == {:error, expected_error, 1} assert :io.scan_erl_form(~c">") == {:eof, 1} end) From c1b54c61253503bf3a04c97362e856f829d9f838 Mon Sep 17 00:00:00 2001 From: Cameron Duley Date: Thu, 22 Feb 2024 02:51:34 -0500 Subject: [PATCH 0401/1886] Add callout to full bitstring reference in the getting started guide (#13360) --- .../pages/getting-started/binaries-strings-and-charlists.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/elixir/pages/getting-started/binaries-strings-and-charlists.md b/lib/elixir/pages/getting-started/binaries-strings-and-charlists.md index 2cbdc5c55c..9fcd587e17 100644 --- a/lib/elixir/pages/getting-started/binaries-strings-and-charlists.md +++ b/lib/elixir/pages/getting-started/binaries-strings-and-charlists.md @@ -94,7 +94,7 @@ We are getting a little bit ahead of ourselves. Let's talk about bitstrings to l ## Bitstrings -Although we have covered code points and UTF-8 encoding, we still need to go a bit deeper into how exactly we store the encoded bytes, and this is where we introduce the **bitstring**. A bitstring is a fundamental data type in Elixir, denoted with the `<<>>/1` syntax. **A bitstring is a contiguous sequence of bits in memory.** +Although we have covered code points and UTF-8 encoding, we still need to go a bit deeper into how exactly we store the encoded bytes, and this is where we introduce the **bitstring**. A bitstring is a fundamental data type in Elixir, denoted with the [`<<>>`](`<<>>/1`) syntax. **A bitstring is a contiguous sequence of bits in memory.** By default, 8 bits (i.e. 1 byte) is used to store each number in a bitstring, but you can manually specify the number of bits via a `::n` modifier to denote the size in `n` bits, or you can use the more verbose declaration `::size(n)`: @@ -121,6 +121,8 @@ true Here, 257 in base 2 would be represented as `100000001`, but since we have reserved only 8 bits for its representation (by default), the left-most bit is ignored and the value becomes truncated to `00000001`, or simply `1` in decimal. +A complete reference for the bitstring constructor can be found in [`<<>>`](`<<>>/1`)'s documentation. + ## Binaries **A binary is a bitstring where the number of bits is divisible by 8.** That means that every binary is a bitstring, but not every bitstring is a binary. We can use the `is_bitstring/1` and `is_binary/1` functions to demonstrate this. From 73ef1c58453f5696558f403b4491aadb8a3f362d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 22 Feb 2024 08:54:22 +0100 Subject: [PATCH 0402/1886] Remove trailing newline in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9b24b44222..3a75701745 100644 --- a/README.md +++ b/README.md @@ -128,7 +128,7 @@ also run tests for a specific framework with `make test_#{APPLICATION}`, for exa `make test_ex_unit`. If you just changed something in Elixir's standard library, you can run only that portion through `make test_stdlib`. -If you are only changing one file, you can choose to compile and run tests +If you are only changing one file, you can choose to compile and run tests for that specific file for faster development cycles. For example, if you are changing the String module, you can compile it and run its tests as: From e6aefcd364744ae11b8c6dd811875186553c21f1 Mon Sep 17 00:00:00 2001 From: Jean Klingler Date: Fri, 23 Feb 2024 23:31:01 +0900 Subject: [PATCH 0403/1886] Fix unexpected rounding signs on OPT26- (#13365) --- .formatter.exs | 5 +- lib/elixir/lib/float.ex | 11 +- lib/elixir/test/elixir/float_test.exs | 155 ++++++++++++++------------ 3 files changed, 95 insertions(+), 76 deletions(-) diff --git a/.formatter.exs b/.formatter.exs index f9bb2b1f0f..b09e896884 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -13,7 +13,10 @@ assert_same: 2, # Errors tests - assert_eval_raise: 3 + assert_eval_raise: 3, + + # Float tests + float_assert: 1 ], normalize_bitstring_modifiers: false ] diff --git a/lib/elixir/lib/float.ex b/lib/elixir/lib/float.ex index 7810d21b3b..d837b13b01 100644 --- a/lib/elixir/lib/float.ex +++ b/lib/elixir/lib/float.ex @@ -374,8 +374,8 @@ defmodule Float do case rounding do :ceil when sign === 0 -> 1 / power_of_10(precision) :floor when sign === 1 -> -1 / power_of_10(precision) - :ceil when sign === 1 -> -0.0 - :half_up when sign === 1 -> -0.0 + :ceil when sign === 1 -> minus_zero() + :half_up when sign === 1 -> minus_zero() _ -> 0.0 end @@ -406,7 +406,7 @@ defmodule Float do cond do num == 0 and sign == 1 -> - -0.0 + minus_zero() num == 0 -> 0.0 @@ -422,6 +422,11 @@ defmodule Float do end end + # TODO remove once we require Erlang/OTP 27+ + # This function tricks the compiler to avoid this bug in previous versions: + # https://github.com/elixir-lang/elixir/blob/main/lib/elixir/lib/float.ex#L408-L412 + defp minus_zero, do: -0.0 + defp decompose(significant, initial) do decompose(significant, 1, 0, initial) end diff --git a/lib/elixir/test/elixir/float_test.exs b/lib/elixir/test/elixir/float_test.exs index e56c3b991a..cc4c96c035 100644 --- a/lib/elixir/test/elixir/float_test.exs +++ b/lib/elixir/test/elixir/float_test.exs @@ -5,6 +5,17 @@ defmodule FloatTest do doctest Float + # TODO remove and replace by assert once we require Erlang/OTP 27+ + # We can't easily distinguish between -0.0 and +0.0 on previous version + defmacrop float_assert({:===, _, [left, right]}) do + quote do + # note: these are pure functions so no need to use bind_quoted + # we favor a useful error message instead + assert unquote(left) === unquote(right) + assert to_string(unquote(left)) === to_string(unquote(right)) + end + end + test "parse/1" do assert Float.parse("12") === {12.0, ""} assert Float.parse("-12") === {-12.0, ""} @@ -45,39 +56,39 @@ defmodule FloatTest do end test "floor/1" do - assert Float.floor(12.524235) === 12.0 - assert Float.floor(-12.5) === -13.0 - assert Float.floor(-12.524235) === -13.0 - assert Float.floor(7.5e3) === 7500.0 - assert Float.floor(7.5432e3) === 7543.0 - assert Float.floor(7.5e-3) === 0.0 - assert Float.floor(-12.32453e4) === -123_246.0 - assert Float.floor(-12.32453e-10) === -1.0 - assert Float.floor(0.32453e-10) === 0.0 - assert Float.floor(-0.32453e-10) === -1.0 - assert Float.floor(1.32453e-10) === 0.0 + float_assert Float.floor(12.524235) === 12.0 + float_assert Float.floor(-12.5) === -13.0 + float_assert Float.floor(-12.524235) === -13.0 + float_assert Float.floor(7.5e3) === 7500.0 + float_assert Float.floor(7.5432e3) === 7543.0 + float_assert Float.floor(7.5e-3) === 0.0 + float_assert Float.floor(-12.32453e4) === -123_246.0 + float_assert Float.floor(-12.32453e-10) === -1.0 + float_assert Float.floor(0.32453e-10) === 0.0 + float_assert Float.floor(-0.32453e-10) === -1.0 + float_assert Float.floor(1.32453e-10) === 0.0 end describe "floor/2" do test "with 0.0" do for precision <- 0..15 do - assert Float.floor(0.0, precision) === 0.0 - assert Float.floor(-0.0, precision) === -0.0 + float_assert Float.floor(0.0, precision) === 0.0 + float_assert Float.floor(-0.0, precision) === -0.0 end end test "floor/2 with precision" do - assert Float.floor(12.524235, 0) === 12.0 - assert Float.floor(-12.524235, 0) === -13.0 + float_assert Float.floor(12.524235, 0) === 12.0 + float_assert Float.floor(-12.524235, 0) === -13.0 - assert Float.floor(12.52, 2) === 12.51 - assert Float.floor(-12.52, 2) === -12.52 + float_assert Float.floor(12.52, 2) === 12.51 + float_assert Float.floor(-12.52, 2) === -12.52 - assert Float.floor(12.524235, 2) === 12.52 - assert Float.floor(-12.524235, 3) === -12.525 + float_assert Float.floor(12.524235, 2) === 12.52 + float_assert Float.floor(-12.524235, 3) === -12.525 - assert Float.floor(12.32453e-20, 2) === 0.0 - assert Float.floor(-12.32453e-20, 2) === -0.01 + float_assert Float.floor(12.32453e-20, 2) === 0.0 + float_assert Float.floor(-12.32453e-20, 2) === -0.01 assert_raise ArgumentError, "precision 16 is out of valid range of 0..15", fn -> Float.floor(1.1, 16) @@ -85,54 +96,54 @@ defmodule FloatTest do end test "with subnormal floats" do - assert Float.floor(-5.0e-324, 0) === -1.0 - assert Float.floor(-5.0e-324, 1) === -0.1 - assert Float.floor(-5.0e-324, 2) === -0.01 - assert Float.floor(-5.0e-324, 15) === -0.000000000000001 + float_assert Float.floor(-5.0e-324, 0) === -1.0 + float_assert Float.floor(-5.0e-324, 1) === -0.1 + float_assert Float.floor(-5.0e-324, 2) === -0.01 + float_assert Float.floor(-5.0e-324, 15) === -0.000000000000001 for precision <- 0..15 do - assert Float.floor(5.0e-324, precision) === 0.0 + float_assert Float.floor(5.0e-324, precision) === 0.0 end end end test "ceil/1" do - assert Float.ceil(12.524235) === 13.0 - assert Float.ceil(-12.5) === -12.0 - assert Float.ceil(-12.524235) === -12.0 - assert Float.ceil(7.5e3) === 7500.0 - assert Float.ceil(7.5432e3) === 7544.0 - assert Float.ceil(7.5e-3) === 1.0 - assert Float.ceil(-12.32453e4) === -123_245.0 - assert Float.ceil(-12.32453e-10) === -0.0 - assert Float.ceil(0.32453e-10) === 1.0 - assert Float.ceil(-0.32453e-10) === -0.0 - assert Float.ceil(1.32453e-10) === 1.0 - assert Float.ceil(0.0) === 0.0 + float_assert Float.ceil(12.524235) === 13.0 + float_assert Float.ceil(-12.5) === -12.0 + float_assert Float.ceil(-12.524235) === -12.0 + float_assert Float.ceil(7.5e3) === 7500.0 + float_assert Float.ceil(7.5432e3) === 7544.0 + float_assert Float.ceil(7.5e-3) === 1.0 + float_assert Float.ceil(-12.32453e4) === -123_245.0 + float_assert Float.ceil(-12.32453e-10) === -0.0 + float_assert Float.ceil(0.32453e-10) === 1.0 + float_assert Float.ceil(-0.32453e-10) === -0.0 + float_assert Float.ceil(1.32453e-10) === 1.0 + float_assert Float.ceil(0.0) === 0.0 end describe "ceil/2" do test "with 0.0" do for precision <- 0..15 do - assert Float.ceil(0.0, precision) === 0.0 - assert Float.ceil(-0.0, precision) === -0.0 + float_assert Float.ceil(0.0, precision) === 0.0 + float_assert Float.ceil(-0.0, precision) === -0.0 end end test "with regular floats" do - assert Float.ceil(12.524235, 0) === 13.0 - assert Float.ceil(-12.524235, 0) === -12.0 + float_assert Float.ceil(12.524235, 0) === 13.0 + float_assert Float.ceil(-12.524235, 0) === -12.0 - assert Float.ceil(12.52, 2) === 12.52 - assert Float.ceil(-12.52, 2) === -12.51 + float_assert Float.ceil(12.52, 2) === 12.52 + float_assert Float.ceil(-12.52, 2) === -12.51 - assert Float.ceil(12.524235, 2) === 12.53 - assert Float.ceil(-12.524235, 3) === -12.524 + float_assert Float.ceil(12.524235, 2) === 12.53 + float_assert Float.ceil(-12.524235, 3) === -12.524 - assert Float.ceil(12.32453e-20, 2) === 0.01 - assert Float.ceil(-12.32453e-20, 2) === -0.0 + float_assert Float.ceil(12.32453e-20, 2) === 0.01 + float_assert Float.ceil(-12.32453e-20, 2) === -0.0 - assert Float.ceil(0.0, 2) === 0.0 + float_assert Float.ceil(0.0, 2) === 0.0 assert_raise ArgumentError, "precision 16 is out of valid range of 0..15", fn -> Float.ceil(1.1, 16) @@ -140,18 +151,18 @@ defmodule FloatTest do end test "with small floats rounded up to -0.0" do - assert Float.ceil(-0.1, 0) === -0.0 - assert Float.ceil(-0.01, 1) === -0.0 + float_assert Float.ceil(-0.1, 0) === -0.0 + float_assert Float.ceil(-0.01, 1) === -0.0 end test "with subnormal floats" do - assert Float.ceil(5.0e-324, 0) === 1.0 - assert Float.ceil(5.0e-324, 1) === 0.1 - assert Float.ceil(5.0e-324, 2) === 0.01 - assert Float.ceil(5.0e-324, 15) === 0.000000000000001 + float_assert Float.ceil(5.0e-324, 0) === 1.0 + float_assert Float.ceil(5.0e-324, 1) === 0.1 + float_assert Float.ceil(5.0e-324, 2) === 0.01 + float_assert Float.ceil(5.0e-324, 15) === 0.000000000000001 for precision <- 0..15 do - assert Float.ceil(-5.0e-324, precision) === -0.0 + float_assert Float.ceil(-5.0e-324, precision) === -0.0 end end end @@ -159,18 +170,18 @@ defmodule FloatTest do describe "round/2" do test "with 0.0" do for precision <- 0..15 do - assert Float.round(0.0, precision) === 0.0 - assert Float.round(-0.0, precision) === -0.0 + float_assert Float.round(0.0, precision) === 0.0 + float_assert Float.round(-0.0, precision) === -0.0 end end test "with regular floats" do - assert Float.round(5.5675, 3) === 5.567 - assert Float.round(-5.5674, 3) === -5.567 - assert Float.round(5.5, 3) === 5.5 - assert Float.round(5.5e-10, 10) === 5.0e-10 - assert Float.round(5.5e-10, 8) === 0.0 - assert Float.round(5.0, 0) === 5.0 + float_assert Float.round(5.5675, 3) === 5.567 + float_assert Float.round(-5.5674, 3) === -5.567 + float_assert Float.round(5.5, 3) === 5.5 + float_assert Float.round(5.5e-10, 10) === 5.0e-10 + float_assert Float.round(5.5e-10, 8) === 0.0 + float_assert Float.round(5.0, 0) === 5.0 assert_raise ArgumentError, "precision 16 is out of valid range of 0..15", fn -> Float.round(1.1, 16) @@ -178,20 +189,20 @@ defmodule FloatTest do end test "with small floats rounded to +0.0 / -0.0" do - assert Float.round(0.01, 0) === 0.0 - assert Float.round(0.01, 1) === 0.0 + float_assert Float.round(0.01, 0) === 0.0 + float_assert Float.round(0.01, 1) === 0.0 - assert Float.round(-0.01, 0) === -0.0 - assert Float.round(-0.01, 1) === -0.0 + float_assert Float.round(-0.01, 0) === -0.0 + float_assert Float.round(-0.01, 1) === -0.0 - assert Float.round(-0.49999, 0) === -0.0 - assert Float.round(-0.049999, 1) === -0.0 + float_assert Float.round(-0.49999, 0) === -0.0 + float_assert Float.round(-0.049999, 1) === -0.0 end test "with subnormal floats" do for precision <- 0..15 do - assert Float.round(5.0e-324, precision) === 0.0 - assert Float.round(-5.0e-324, precision) === -0.0 + float_assert Float.round(5.0e-324, precision) === 0.0 + float_assert Float.round(-5.0e-324, precision) === -0.0 end end end From 5ec63f0e1ae84f9460c5356cb89adc774d3df537 Mon Sep 17 00:00:00 2001 From: Jean Klingler Date: Sat, 24 Feb 2024 00:02:00 +0900 Subject: [PATCH 0404/1886] Fix charlist formatting issue on '\"' (#13364) --- lib/elixir/lib/code/formatter.ex | 34 +++++++++++-------- .../elixir/code_formatter/literals_test.exs | 11 ++++-- 2 files changed, 29 insertions(+), 16 deletions(-) diff --git a/lib/elixir/lib/code/formatter.ex b/lib/elixir/lib/code/formatter.ex index 1d4aa84f66..ed2466f379 100644 --- a/lib/elixir/lib/code/formatter.ex +++ b/lib/elixir/lib/code/formatter.ex @@ -6,7 +6,8 @@ defmodule Code.Formatter do @double_heredoc "\"\"\"" @single_quote "'" @single_heredoc "'''" - @sigil_c "~c\"" + @sigil_c_double "~c\"" + @sigil_c_single "~c'" @sigil_c_heredoc "~c\"\"\"" @newlines 2 @min_line 0 @@ -300,7 +301,7 @@ defmodule Code.Formatter do remote_to_algebra(quoted, context, state) meta[:delimiter] == ~s['''] -> - {opener, quotes} = get_charlist_quotes(true, state) + {opener, quotes} = get_charlist_quotes(:heredoc, state) {doc, state} = entries @@ -310,7 +311,7 @@ defmodule Code.Formatter do {force_unfit(doc), state} true -> - {opener, quotes} = get_charlist_quotes(false, state) + {opener, quotes} = get_charlist_quotes({:regular, entries}, state) list_interpolation_to_algebra(entries, quotes, state, opener, quotes) end end @@ -369,13 +370,14 @@ defmodule Code.Formatter do defp quoted_to_algebra({:__block__, meta, [list]}, _context, state) when is_list(list) do case meta[:delimiter] do ~s['''] -> - {opener, quotes} = get_charlist_quotes(true, state) + {opener, quotes} = get_charlist_quotes(:heredoc, state) string = list |> List.to_string() |> escape_heredoc(quotes) {opener |> concat(string) |> concat(quotes) |> force_unfit(), state} ~s['] -> - {opener, quotes} = get_charlist_quotes(false, state) - string = list |> List.to_string() |> escape_string(quotes) + string = list |> List.to_string() + {opener, quotes} = get_charlist_quotes({:regular, [string]}, state) + string = escape_string(string, quotes) {opener |> concat(string) |> concat(quotes), state} _other -> @@ -2422,19 +2424,23 @@ defmodule Code.Formatter do {left, right} end - defp get_charlist_quotes(_heredoc = false, state) do + defp get_charlist_quotes(:heredoc, state) do if state.normalize_charlists_as_sigils do - {@sigil_c, @double_quote} + {@sigil_c_heredoc, @double_heredoc} else - {@single_quote, @single_quote} + {@single_heredoc, @single_heredoc} end end - defp get_charlist_quotes(_heredoc = true, state) do - if state.normalize_charlists_as_sigils do - {@sigil_c_heredoc, @double_heredoc} - else - {@single_heredoc, @single_heredoc} + defp get_charlist_quotes({:regular, chunks}, state) do + cond do + !state.normalize_charlists_as_sigils -> {@single_quote, @single_quote} + Enum.any?(chunks, &has_double_quote?/1) -> {@sigil_c_single, @single_quote} + true -> {@sigil_c_double, @double_quote} end end + + defp has_double_quote?(chunk) do + is_binary(chunk) and chunk =~ @double_quote + end end diff --git a/lib/elixir/test/elixir/code_formatter/literals_test.exs b/lib/elixir/test/elixir/code_formatter/literals_test.exs index ec614b6949..7f5a7632fa 100644 --- a/lib/elixir/test/elixir/code_formatter/literals_test.exs +++ b/lib/elixir/test/elixir/code_formatter/literals_test.exs @@ -210,10 +210,15 @@ defmodule Code.Formatter.LiteralsTest do test "with escapes" do assert_format ~S['f\a\b\ro'], ~S[~c"f\a\b\ro"] assert_format ~S['single \' quote'], ~S[~c"single ' quote"] - assert_format ~S['double " quote'], ~S[~c"double \" quote"] + assert_format ~S['double " quote'], ~S[~c'double " quote'] + assert_format ~S['escaped \" quote'], ~S[~c'escaped \" quote'] + assert_format ~S['\\"'], ~S[~c'\\"'] assert_same ~S['f\a\b\ro'], @keep_charlists assert_same ~S['single \' quote'], @keep_charlists + assert_same ~S['double " quote'], @keep_charlists + assert_same ~S['escaped \" quote'], @keep_charlists + assert_same ~S['\\"'], @keep_charlists end test "keeps literal new lines" do @@ -235,13 +240,15 @@ defmodule Code.Formatter.LiteralsTest do test "with interpolation" do assert_format ~S['one #{2} three'], ~S[~c"one #{2} three"] + assert_format ~S['#{1}\n \\ " \"'], ~S[~c'#{1}\n \\ " \"'] assert_same ~S['one #{2} three'], @keep_charlists + assert_same ~S['#{1}\n \\ " \"'], @keep_charlists end test "with escape and interpolation" do assert_format ~S['one\n\'#{2}\'\nthree'], ~S[~c"one\n'#{2}'\nthree"] - assert_format ~S['one\n"#{2}"\nthree'], ~S[~c"one\n\"#{2}\"\nthree"] + assert_format ~S['one\n"#{2}"\nthree'], ~S[~c'one\n"#{2}"\nthree'] assert_same ~S['one\n\'#{2}\'\nthree'], @keep_charlists end From 99107e74f7158c281fb4bd874a656eddec68fbd5 Mon Sep 17 00:00:00 2001 From: Jean Klingler Date: Sat, 24 Feb 2024 22:20:24 +0900 Subject: [PATCH 0405/1886] Fix doctest example since Inspect.MapSet changed (#13366) --- lib/ex_unit/lib/ex_unit/doc_test.ex | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/ex_unit/lib/ex_unit/doc_test.ex b/lib/ex_unit/lib/ex_unit/doc_test.ex index 7d99db9339..2119cf123e 100644 --- a/lib/ex_unit/lib/ex_unit/doc_test.ex +++ b/lib/ex_unit/lib/ex_unit/doc_test.ex @@ -96,9 +96,9 @@ defmodule ExUnit.DocTest do values are treated as comments in Elixir code due to the leading `#` sign, they require special care when being used in doctests. - Imagine you have a map that contains a MapSet and is printed as: + Imagine you have a map that contains a `DateTime` and is printed as: - %{users: #MapSet<[:foo, :bar]>} + %{datetime: #DateTime<2023-06-26 09:30:00+09:00 JST Asia/Tokyo>} If you try to match on such an expression, `doctest` will fail to compile. There are two ways to resolve this. @@ -106,20 +106,20 @@ defmodule ExUnit.DocTest do The first is to rely on the fact that doctest can compare internal structures as long as they are at the root. So one could write: - iex> map = %{users: Enum.into([:foo, :bar], MapSet.new())} - iex> map.users - #MapSet<[:foo, :bar]> + iex> map = %{datetime: DateTime.from_naive!(~N[2023-06-26T09:30:00], "Asia/Tokyo")} + iex> map.datetime + #DateTime<2023-06-26 09:30:00+09:00 JST Asia/Tokyo> Whenever a doctest starts with "#Name<", `doctest` will perform a string comparison. For example, the above test will perform the following match: - inspect(map.users) == "#MapSet<[:foo, :bar]>" + inspect(map.datetime) == "#DateTime<2023-06-26 09:30:00+09:00 JST Asia/Tokyo>" Alternatively, since doctest results are actually evaluated, you can have - the MapSet building expression as the doctest result: + the `DateTime` building expression as the doctest result: - iex> %{users: Enum.into([:foo, :bar], MapSet.new())} - %{users: Enum.into([:foo, :bar], MapSet.new())} + iex> %{datetime: DateTime.from_naive!(~N[2023-06-26T09:30:00], "Asia/Tokyo")} + %{datetime: DateTime.from_naive!(~N[2023-06-26T09:30:00], "Asia/Tokyo")} The downside of this approach is that the doctest result is not really what users would see in the terminal. From 4ec15f371f0365cd0c6a35219b5c1066c0b834bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Samson?= Date: Sat, 24 Feb 2024 16:23:05 +0100 Subject: [PATCH 0406/1886] Add documentation to `Exception` callbacks (#13367) --- lib/elixir/lib/exception.ex | 14 ++++++++++++++ lib/iex/test/iex/helpers_test.exs | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/elixir/lib/exception.ex b/lib/elixir/lib/exception.ex index ddf887c1c8..bf4621d1d8 100644 --- a/lib/elixir/lib/exception.ex +++ b/lib/elixir/lib/exception.ex @@ -31,7 +31,21 @@ defmodule Exception do @type arity_or_args :: non_neg_integer | list @type location :: keyword + @doc """ + Receives the arguments given to `raise/2` and returns the exception struct. + + The default implementation accepts either a set of keyword arguments + that is merged into the struct or a string to be used as the exception's message. + """ @callback exception(term) :: t + + @doc """ + Receives the exception struct and must return its message. + + Most commonly exceptions have a message field which by default is accessed + by this function. However, if an exception does not have a message field, + this function must be explicitly implemented. + """ @callback message(t) :: String.t() @doc """ diff --git a/lib/iex/test/iex/helpers_test.exs b/lib/iex/test/iex/helpers_test.exs index acc33522c2..11bcc59458 100644 --- a/lib/iex/test/iex/helpers_test.exs +++ b/lib/iex/test/iex/helpers_test.exs @@ -805,7 +805,7 @@ defmodule IEx.HelpersTest do assert capture_io(fn -> b(NoMix.run()) end) == "Could not load module NoMix, got: nofile\n" - assert capture_io(fn -> b(Exception.message() / 1) end) == + assert capture_io(fn -> b(Exception.message() / 1) end) =~ "@callback message(t()) :: String.t()\n\n" assert capture_io(fn -> b(:gen_server.handle_cast() / 2) end) =~ From dab3d229930b1e0379a35d7f56697aac6bf177a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sat, 24 Feb 2024 19:51:20 +0100 Subject: [PATCH 0407/1886] Deprecate escaping closing delimiter in uppercase sigils This aligns our uppercase sigils with Erlang/OTP 27. --- lib/elixir/lib/kernel.ex | 15 +++------------ lib/elixir/src/elixir_interpolation.erl | 15 ++++++++++++++- lib/elixir/test/elixir/kernel/sigils_test.exs | 10 ---------- lib/elixir/test/elixir/kernel/warning_test.exs | 4 ++++ 4 files changed, 21 insertions(+), 23 deletions(-) diff --git a/lib/elixir/lib/kernel.ex b/lib/elixir/lib/kernel.ex index 844f76a1ff..8b5026f4af 100644 --- a/lib/elixir/lib/kernel.ex +++ b/lib/elixir/lib/kernel.ex @@ -6054,8 +6054,7 @@ defmodule Kernel do Handles the sigil `~S` for strings. It returns a string without interpolations and without escape - characters, except for the escaping of the closing sigil character - itself. + characters. ## Examples @@ -6066,12 +6065,6 @@ defmodule Kernel do iex> ~S(\o/) "\\o/" - However, if you want to reuse the sigil character itself on - the string, you need to escape it: - - iex> ~S((\)) - "()" - """ defmacro sigil_S(term, modifiers) defmacro sigil_S({:<<>>, _, [binary]}, []) when is_binary(binary), do: binary @@ -6108,8 +6101,7 @@ defmodule Kernel do Handles the sigil `~C` for charlists. It returns a charlist without interpolations and without escape - characters, except for the escaping of the closing sigil character - itself. + characters. A charlist is a list of integers where all the integers are valid code points. The three expressions below are equivalent: @@ -6519,8 +6511,7 @@ defmodule Kernel do Handles the sigil `~W` for list of words. It returns a list of "words" split by whitespace without interpolations - and without escape characters, except for the escaping of the closing - sigil character itself. + and without escape characters. ## Modifiers diff --git a/lib/elixir/src/elixir_interpolation.erl b/lib/elixir/src/elixir_interpolation.erl index d4ccf3086c..30665691cb 100644 --- a/lib/elixir/src/elixir_interpolation.erl +++ b/lib/elixir/src/elixir_interpolation.erl @@ -33,7 +33,17 @@ extract([$\n | Rest], Buffer, Output, Line, _Column, Scope, Interpol, Last) -> extract_nl(Rest, [$\n | Buffer], Output, Line, Scope, Interpol, Last); extract([$\\, Last | Rest], Buffer, Output, Line, Column, Scope, Interpol, Last) -> - extract(Rest, [Last | Buffer], Output, Line, Column+2, Scope, Interpol, Last); + NewScope = + %% TODO: Remove this on Elixir v2.0 + case Interpol of + true -> + Scope; + false -> + Msg = "using \\~ts to escape the closing of an uppercase sigil is deprecated, please use another delimiter or a lowercase sigil instead", + prepend_warning(Line, Column, io_lib:format(Msg, [[Last]]), Scope) + end, + + extract(Rest, [Last | Buffer], Output, Line, Column+2, NewScope, Interpol, Last); extract([$\\, Last, Last, Last | Rest], Buffer, Output, Line, Column, Scope, Interpol, [Last, Last, Last] = All) -> extract(Rest, [Last, Last, Last | Buffer], Output, Line, Column+4, Scope, Interpol, All); @@ -279,3 +289,6 @@ build_string(Buffer, Output) -> [lists:reverse(Buffer) | Output]. build_interpol(Line, Column, EndLine, EndColumn, Buffer, Output) -> [{{Line, Column, nil}, {EndLine, EndColumn, nil}, Buffer} | Output]. + +prepend_warning(Line, Column, Msg, #elixir_tokenizer{warnings=Warnings} = Scope) -> + Scope#elixir_tokenizer{warnings = [{{Line, Column}, Msg} | Warnings]}. diff --git a/lib/elixir/test/elixir/kernel/sigils_test.exs b/lib/elixir/test/elixir/kernel/sigils_test.exs index f81cb0bc41..e81e2532bb 100644 --- a/lib/elixir/test/elixir/kernel/sigils_test.exs +++ b/lib/elixir/test/elixir/kernel/sigils_test.exs @@ -27,8 +27,6 @@ defmodule Kernel.SigilsTest do assert ~S(f#{o}o) == "f\#{o}o" assert ~S(f\#{o}o) == "f\\\#{o}o" assert ~S(f\no) == "f\\no" - assert ~S(foo\)) == "foo)" - assert ~S[foo\]] == "foo]" end test "sigil S newline" do @@ -42,14 +40,6 @@ bar) in ["foo\\\nbar", "foo\\\r\nbar"] """ end - test "sigil S with escaping" do - assert "\"" == ~S"\"" - - assert "\"\"\"\n" == ~S""" - \""" - """ - end - test "sigil s/S expand to binary when possible" do assert Macro.expand(quote(do: ~s(foo)), __ENV__) == "foo" assert Macro.expand(quote(do: ~S(foo)), __ENV__) == "foo" diff --git a/lib/elixir/test/elixir/kernel/warning_test.exs b/lib/elixir/test/elixir/kernel/warning_test.exs index 0cdb71be63..492b02a57c 100644 --- a/lib/elixir/test/elixir/kernel/warning_test.exs +++ b/lib/elixir/test/elixir/kernel/warning_test.exs @@ -1099,6 +1099,10 @@ defmodule Kernel.WarningTest do purge(UseSample) end + test "deprecated closing sigil delimiter" do + assert_warn_eval(["nofile:1:7", "deprecated"], "~S(foo\\))") + end + test "deprecated not left in right" do assert_warn_eval(["nofile:1:7", "deprecated"], "not 1 in [1, 2, 3]") end From 129c5beef8daaa45ecb6ab7ae6a9ac9044d26fd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 22 Feb 2024 11:12:41 +0100 Subject: [PATCH 0408/1886] Decouple require expansion from alias --- lib/elixir/src/elixir_expand.erl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/elixir/src/elixir_expand.erl b/lib/elixir/src/elixir_expand.erl index 50354e28e5..22faf8947a 100644 --- a/lib/elixir/src/elixir_expand.erl +++ b/lib/elixir/src/elixir_expand.erl @@ -94,7 +94,8 @@ expand({require, Meta, [Ref, Opts]}, S, E) -> false when is_atom(ERef) -> elixir_aliases:ensure_loaded(Meta, ERef, ET), - {ERef, ST, expand_require(Meta, ERef, EOpts, ET)}; + RE = expand_require(Meta, ERef, EOpts, ET), + {ERef, ST, expand_alias(Meta, false, ERef, EOpts, RE)}; false -> file_error(Meta, E, ?MODULE, {expected_compile_time_module, require, Ref}) @@ -984,8 +985,7 @@ no_alias_expansion(Other) -> expand_require(Meta, Ref, Opts, E) -> elixir_env:trace({require, Meta, Ref, Opts}, E), - RE = E#{requires := ordsets:add_element(Ref, ?key(E, requires))}, - expand_alias(Meta, false, Ref, Opts, RE). + E#{requires := ordsets:add_element(Ref, ?key(E, requires))}. expand_alias(Meta, IncludeByDefault, Ref, Opts, E) -> case expand_as(lists:keyfind(as, 1, Opts), Meta, IncludeByDefault, Ref, E) of From 80af632a7e87df35345b93ab3d928277dce6e1d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sat, 24 Feb 2024 22:47:59 +0100 Subject: [PATCH 0409/1886] Wrap (a -> b) into literals instead of plain lists, closes #13358 --- lib/elixir/lib/code/formatter.ex | 11 ++-- lib/elixir/lib/code/normalizer.ex | 5 -- lib/elixir/src/elixir_parser.yrl | 20 +++---- lib/elixir/test/elixir/kernel/parser_test.exs | 52 ++++++++++++++++++- 4 files changed, 67 insertions(+), 21 deletions(-) diff --git a/lib/elixir/lib/code/formatter.ex b/lib/elixir/lib/code/formatter.ex index ed2466f379..ef4283a944 100644 --- a/lib/elixir/lib/code/formatter.ex +++ b/lib/elixir/lib/code/formatter.ex @@ -367,6 +367,11 @@ defmodule Code.Formatter do tuple_to_algebra(meta, [left, right], :flex_break, state) end + # (left -> right) + defp quoted_to_algebra({:__block__, _, [[{:->, _, _} | _] = clauses]}, _context, state) do + paren_fun_to_algebra(clauses, @max_line, @min_line, state) + end + defp quoted_to_algebra({:__block__, meta, [list]}, _context, state) when is_list(list) do case meta[:delimiter] do ~s['''] -> @@ -417,6 +422,7 @@ defmodule Code.Formatter do {Keyword.fetch!(meta, :token) |> float_to_algebra(state.inspect_opts), state} end + # (unquote_splicing(...)) defp quoted_to_algebra( {:__block__, _meta, [{:unquote_splicing, meta, [_] = args}]}, context, @@ -505,11 +511,6 @@ defmodule Code.Formatter do remote_to_algebra(quoted, context, state) end - # (left -> right) - defp quoted_to_algebra([{:->, _, _} | _] = clauses, _context, state) do - paren_fun_to_algebra(clauses, @max_line, @min_line, state) - end - # [keyword: :list] (inner part) # %{:foo => :bar} (inner part) defp quoted_to_algebra(list, context, state) when is_list(list) do diff --git a/lib/elixir/lib/code/normalizer.ex b/lib/elixir/lib/code/normalizer.ex index d31e40a276..f86ca83142 100644 --- a/lib/elixir/lib/code/normalizer.ex +++ b/lib/elixir/lib/code/normalizer.ex @@ -137,11 +137,6 @@ defmodule Code.Normalizer do {:., meta, [left, right]} end - # A list of left to right arrows is not considered as a list literal, so it's not wrapped - defp do_normalize([{:->, _, [_ | _]} | _] = quoted, state) do - normalize_args(quoted, state) - end - # left -> right defp do_normalize({:->, meta, [left, right]}, state) do meta = patch_meta_line(meta, state.parent_meta) diff --git a/lib/elixir/src/elixir_parser.yrl b/lib/elixir/src/elixir_parser.yrl index 47cd547ad8..c818deb572 100644 --- a/lib/elixir/src/elixir_parser.yrl +++ b/lib/elixir/src/elixir_parser.yrl @@ -789,12 +789,14 @@ adjust_map_column(Map) -> %% Blocks -build_block([{unquote_splicing, _, [_]}]=Exprs) -> - {'__block__', [], Exprs}; -build_block([Expr]) -> +build_block(Exprs) -> build_block(Exprs, []). + +build_block([{unquote_splicing, _, [_]}]=Exprs, Meta) -> + {'__block__', Meta, Exprs}; +build_block([Expr], _Meta) -> Expr; -build_block(Exprs) -> - {'__block__', [], Exprs}. +build_block(Exprs, Meta) -> + {'__block__', Meta, Exprs}. %% Newlines @@ -1072,11 +1074,9 @@ build_stab(Stab) -> build_paren_stab(_Before, [{Op, _, [_]}]=Exprs, _After) when ?rearrange_uop(Op) -> {'__block__', [], Exprs}; build_paren_stab(Before, Stab, After) -> - case build_stab(Stab) of - {'__block__', Meta, Block} -> - {'__block__', Meta ++ meta_from_token_with_closing(Before, After), Block}; - Other -> - Other + case check_stab(Stab, none) of + block -> build_block(reverse(Stab), meta_from_token_with_closing(Before, After)); + stab -> handle_literal(collect_stab(Stab, [], []), Before, newlines_pair(Before, After)) end. collect_stab([{'->', Meta, [Left, Right]} | T], Exprs, Stabs) -> diff --git a/lib/elixir/test/elixir/kernel/parser_test.exs b/lib/elixir/test/elixir/kernel/parser_test.exs index 39d2485623..c171da7677 100644 --- a/lib/elixir/test/elixir/kernel/parser_test.exs +++ b/lib/elixir/test/elixir/kernel/parser_test.exs @@ -474,6 +474,56 @@ defmodule Kernel.ParserTest do ]} end + test "end of expression with literal" do + file = """ + a do + d -> + ( + b -> c + ) + end + """ + + assert Code.string_to_quoted!(file, + token_metadata: true, + literal_encoder: &{:ok, {:__block__, &2, [&1]}} + ) == + {:a, + [ + end_of_expression: [newlines: 1, line: 6], + do: [line: 1], + end: [line: 6], + line: 1 + ], + [ + [ + {{:__block__, [line: 1], [:do]}, + [ + {:->, [newlines: 1, line: 2], + [ + [{:d, [line: 2], nil}], + {:__block__, + [ + end_of_expression: [newlines: 1, line: 5], + newlines: 1, + closing: [line: 5], + line: 3 + ], + [ + [ + {:->, [line: 4], + [ + [{:b, [line: 4], nil}], + {:c, [end_of_expression: [newlines: 1, line: 4], line: 4], nil} + ]} + ] + ]} + ]} + ]} + ] + ]} + end + test "does not add end of expression to ->" do file = """ case true do @@ -551,7 +601,7 @@ defmodule Kernel.ParserTest do [ {:->, [line: 1], [ - [{:__block__, [token: "1", line: 1, closing: [line: 1], line: 1], [1]}], + [{:__block__, [token: "1", line: 1], [1]}], {:__block__, [delimiter: "\"", line: 1], ["hello"]} ]} ]} From 3c55db78f02a93724971d2d4284e5979b50b32f9 Mon Sep 17 00:00:00 2001 From: Jean Klingler Date: Mon, 26 Feb 2024 19:25:31 +0900 Subject: [PATCH 0410/1886] Use Erlang's implementation of jaro distance, fix bugs (#13369) * Use Erlang's implementation of jaro distance, fix bugs * Remove conditional compilation --- lib/elixir/lib/string.ex | 84 +------------------------ lib/elixir/src/elixir_utils.erl | 85 +++++++++++++++++++++++++- lib/elixir/test/elixir/string_test.exs | 3 +- 3 files changed, 89 insertions(+), 83 deletions(-) diff --git a/lib/elixir/lib/string.ex b/lib/elixir/lib/string.ex index 6de7a20b1f..0ca14c3c07 100644 --- a/lib/elixir/lib/string.ex +++ b/lib/elixir/lib/string.ex @@ -3066,74 +3066,13 @@ defmodule String do @spec jaro_distance(t, t) :: float def jaro_distance(string1, string2) - def jaro_distance(string, string), do: 1.0 + def jaro_distance(string, string) when is_binary(string), do: 1.0 def jaro_distance(_string, ""), do: 0.0 def jaro_distance("", _string), do: 0.0 def jaro_distance(string1, string2) when is_binary(string1) and is_binary(string2) do - {chars1, len1} = graphemes_and_length(string1) - {chars2, len2} = graphemes_and_length(string2) - - case match(chars1, len1, chars2, len2) do - {0, _trans} -> - 0.0 - - {comm, trans} -> - (comm / len1 + comm / len2 + (comm - trans) / comm) / 3 - end - end - - defp match(chars1, len1, chars2, len2) do - if len1 < len2 do - match(chars1, chars2, div(len2, 2) - 1) - else - match(chars2, chars1, div(len1, 2) - 1) - end - end - - defp match(chars1, chars2, lim) do - match(chars1, chars2, {0, lim}, {0, 0, -1}, 0) - end - - defp match([char | rest], chars, range, state, idx) do - {chars, state} = submatch(char, chars, range, state, idx) - - case range do - {lim, lim} -> match(rest, tl(chars), range, state, idx + 1) - {pre, lim} -> match(rest, chars, {pre + 1, lim}, state, idx + 1) - end - end - - defp match([], _, _, {comm, trans, _}, _), do: {comm, trans} - - defp submatch(char, chars, {pre, _} = range, state, idx) do - case detect(char, chars, range) do - nil -> - {chars, state} - - {subidx, chars} -> - {chars, proceed(state, idx - pre + subidx)} - end - end - - defp detect(char, chars, {pre, lim}) do - detect(char, chars, pre + 1 + lim, 0, []) - end - - defp detect(_char, _chars, 0, _idx, _acc), do: nil - defp detect(_char, [], _lim, _idx, _acc), do: nil - - defp detect(char, [char | rest], _lim, idx, acc), do: {idx, Enum.reverse(acc, [nil | rest])} - - defp detect(char, [other | rest], lim, idx, acc), - do: detect(char, rest, lim - 1, idx + 1, [other | acc]) - - defp proceed({comm, trans, former}, current) do - if current < former do - {comm + 1, trans + 1, current} - else - {comm + 1, trans, current} - end + # TODO: Replace by :string.jaro_similarity/2 when we require Erlang/OTP 27+ + :elixir_utils.jaro_similarity(string1, string2) end @doc """ @@ -3168,7 +3107,6 @@ defmodule String do codepoint_byte_size: 1, grapheme_byte_size: 1, grapheme_to_binary: 1, - graphemes_and_length: 1, reverse_characters_to_binary: 1} defp byte_size_unicode(binary) when is_binary(binary), do: byte_size(binary) @@ -3205,22 +3143,6 @@ defmodule String do defp grapheme_byte_size([], acc), do: acc - defp graphemes_and_length(string), - do: graphemes_and_length(string, [], 0) - - defp graphemes_and_length(string, acc, length) do - case :unicode_util.gc(string) do - [gc | rest] -> - graphemes_and_length(rest, [gc | acc], length + 1) - - [] -> - {:lists.reverse(acc), length} - - {:error, <>} -> - graphemes_and_length(rest, [<> | acc], length + 1) - end - end - defp reverse_characters_to_binary(acc), do: acc |> :lists.reverse() |> :unicode.characters_to_binary() end diff --git a/lib/elixir/src/elixir_utils.erl b/lib/elixir/src/elixir_utils.erl index 69e80c2a54..539300f2be 100644 --- a/lib/elixir/src/elixir_utils.erl +++ b/lib/elixir/src/elixir_utils.erl @@ -8,7 +8,7 @@ read_file_type/1, read_file_type/2, read_link_type/1, read_posix_mtime_and_size/1, change_posix_time/2, change_universal_time/2, guard_op/2, extract_splat_guards/1, extract_guards/1, - erlang_comparison_op_to_elixir/1, erl_fa_to_elixir_fa/2]). + erlang_comparison_op_to_elixir/1, erl_fa_to_elixir_fa/2, jaro_similarity/2]). -include("elixir.hrl"). -include_lib("kernel/include/file.hrl"). @@ -223,3 +223,86 @@ returns_boolean({'__block__', _, Exprs}) -> returns_boolean(lists:last(Exprs)); returns_boolean(_) -> false. + + +% TODO: Remove me when we require Erlang/OTP 27+ +% This is a polyfill for older versions, copying the code from +% https://github.com/erlang/otp/pull/7879 +-spec jaro_similarity(String1, String2) -> Similarity when + String1 :: unicode:chardata(), + String2 :: unicode:chardata(), + Similarity :: float(). %% Between +0.0 and 1.0 +jaro_similarity(A0, B0) -> + {A, ALen} = str_to_gcl_and_length(A0), + {B, BLen} = str_to_indexmap(B0), + Dist = max(ALen, BLen) div 2, + {AM, BM} = jaro_match(A, B, -Dist, Dist, [], []), + if + ALen =:= 0 andalso BLen =:= 0 -> + 1.0; + ALen =:= 0 orelse BLen =:= 0 -> + 0.0; + AM =:= [] -> + 0.0; + true -> + {M,T} = jaro_calc_mt(AM, BM, 0, 0), + (M/ALen + M/BLen + (M-T/2)/M) / 3 + end. + +jaro_match([A|As], B0, Min, Max, AM, BM) -> + case jaro_detect(maps:get(A, B0, []), Min, Max) of + false -> + jaro_match(As, B0, Min+1, Max+1, AM, BM); + {J, Remain} -> + B = B0#{A => Remain}, + jaro_match(As, B, Min+1, Max+1, [A|AM], add_rsorted({J,A},BM)) + end; +jaro_match(_A, _B, _Min, _Max, AM, BM) -> + {AM, BM}. + +jaro_detect([Idx|Rest], Min, Max) when Min < Idx, Idx < Max -> + {Idx, Rest}; +jaro_detect([Idx|Rest], Min, Max) when Idx < Max -> + jaro_detect(Rest, Min, Max); +jaro_detect(_, _, _) -> + false. + +jaro_calc_mt([CharA|AM], [{_, CharA}|BM], M, T) -> + jaro_calc_mt(AM, BM, M+1, T); +jaro_calc_mt([_|AM], [_|BM], M, T) -> + jaro_calc_mt(AM, BM, M+1, T+1); +jaro_calc_mt([], [], M, T) -> + {M, T}. + + +%% Returns GC list and length +str_to_gcl_and_length(S0) -> + gcl_and_length(unicode_util:gc(S0), [], 0). + +gcl_and_length([C|Str], Acc, N) -> + gcl_and_length(unicode_util:gc(Str), [C|Acc], N+1); +gcl_and_length([], Acc, N) -> + {lists:reverse(Acc), N}; +gcl_and_length({error, Err}, _, _) -> + error({badarg, Err}). + +%% Returns GC map with index and length +str_to_indexmap(S) -> + [M|L] = str_to_map(unicode_util:gc(S), 0), + {M,L}. + +str_to_map([], L) -> [#{}|L]; +str_to_map([G | Gs], I) -> + [M|L] = str_to_map(unicode_util:gc(Gs), I+1), + [maps:put(G, [I | maps:get(G, M, [])], M)| L]; +str_to_map({error,Error}, _) -> + error({badarg, Error}). + +%% Add in decreasing order +add_rsorted(A, [H|_]=BM) when A > H -> + [A|BM]; +add_rsorted(A, [H|BM]) -> + [H|add_rsorted(A,BM)]; +add_rsorted(A, []) -> + [A]. + diff --git a/lib/elixir/test/elixir/string_test.exs b/lib/elixir/test/elixir/string_test.exs index 9339aae765..1980fbffe2 100644 --- a/lib/elixir/test/elixir/string_test.exs +++ b/lib/elixir/test/elixir/string_test.exs @@ -982,7 +982,7 @@ defmodule StringTest do assert String.jaro_distance("marhha", "martha") == 0.888888888888889 assert String.jaro_distance("dwayne", "duane") == 0.8222222222222223 assert String.jaro_distance("dixon", "dicksonx") == 0.7666666666666666 - assert String.jaro_distance("xdicksonx", "dixon") == 0.7851851851851852 + assert String.jaro_distance("xdicksonx", "dixon") == 0.7518518518518519 assert String.jaro_distance("shackleford", "shackelford") == 0.9696969696969697 assert String.jaro_distance("dunningham", "cunnigham") == 0.8962962962962964 assert String.jaro_distance("nichleson", "nichulson") == 0.9259259259259259 @@ -999,6 +999,7 @@ defmodule StringTest do assert String.jaro_distance("jon", "john") == 0.9166666666666666 assert String.jaro_distance("jon", "jan") == 0.7777777777777777 assert String.jaro_distance("семена", "стремя") == 0.6666666666666666 + assert String.jaro_distance("Sunday", "Saturday") == 0.7194444444444444 end test "myers_difference/2" do From d120affb1132be9ce197e696f92c98a675786f91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 26 Feb 2024 18:28:35 +0100 Subject: [PATCH 0411/1886] Emit defmodule tracing event --- lib/elixir/lib/code.ex | 4 ++++ lib/elixir/src/elixir_module.erl | 1 + lib/elixir/test/elixir/kernel/tracers_test.exs | 13 +++++++++++++ 3 files changed, 18 insertions(+) diff --git a/lib/elixir/lib/code.ex b/lib/elixir/lib/code.ex index e7971b81e8..c306e661af 100644 --- a/lib/elixir/lib/code.ex +++ b/lib/elixir/lib/code.ex @@ -158,6 +158,10 @@ defmodule Code do of keys to traverse in the application environment and `return` is either `{:ok, value}` or `:error`. + * `:defmodule` - (since v1.16.2) traced as soon as the definition of a module + starts. This is invoked early on in the module life-cycle, `Module.open?/1` + still returns `false` for such traces + * `{:on_module, bytecode, _ignore}` - (since v1.13.0) traced whenever a module is defined. This is equivalent to the `@after_compile` callback and invoked after any `@after_compile` in the given module. The third element is currently diff --git a/lib/elixir/src/elixir_module.erl b/lib/elixir/src/elixir_module.erl index 637598d6f2..5f8d066ee4 100644 --- a/lib/elixir/src/elixir_module.erl +++ b/lib/elixir/src/elixir_module.erl @@ -118,6 +118,7 @@ invalid_module_name(Module) -> compile(Line, Module, ModuleAsCharlist, Block, Vars, Prune, E) -> File = ?key(E, file), check_module_availability(Module, Line, E), + elixir_env:trace(defmodule, E), CompilerModules = compiler_modules(), {Tables, Ref} = build(Module, Line, File, E), diff --git a/lib/elixir/test/elixir/kernel/tracers_test.exs b/lib/elixir/test/elixir/kernel/tracers_test.exs index 7374ea41ef..ee282c1026 100644 --- a/lib/elixir/test/elixir/kernel/tracers_test.exs +++ b/lib/elixir/test/elixir/kernel/tracers_test.exs @@ -201,6 +201,19 @@ defmodule Kernel.TracersTest do end """) + assert_receive {:defmodule, %{module: Sample, function: nil}} + assert_receive {{:on_module, <<_::binary>>, :none}, %{module: Sample, function: nil}} + after + :code.purge(Sample) + :code.delete(Sample) + end + + test "traces dynamic modules" do + compile_string(""" + Module.create(Sample, :ok, __ENV__) + """) + + assert_receive {:defmodule, %{module: Sample, function: nil}} assert_receive {{:on_module, <<_::binary>>, :none}, %{module: Sample, function: nil}} after :code.purge(Sample) From 7ffdc2ba9edd09471100a2342d05eef790a6f7d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 26 Feb 2024 20:12:21 +0100 Subject: [PATCH 0412/1886] Clarify the meaning of --overwrite, closes #13371 --- lib/mix/lib/mix/tasks/release.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mix/lib/mix/tasks/release.ex b/lib/mix/lib/mix/tasks/release.ex index e9d13d58f0..4db846e732 100644 --- a/lib/mix/lib/mix/tasks/release.ex +++ b/lib/mix/lib/mix/tasks/release.ex @@ -1021,7 +1021,7 @@ defmodule Mix.Tasks.Release do * `--no-deps-check` - does not check dependencies * `--no-elixir-version-check` - does not check Elixir version * `--no-compile` - does not compile before assembling the release - * `--overwrite` - if there is an existing release version, overwrite it + * `--overwrite` - overwrite existing files instead of prompting the user for action * `--path` - the path of the release * `--quiet` - does not write progress to the standard output * `--version` - the version of the release From a52d201784e751221e569a70eb8fc76dc00043c1 Mon Sep 17 00:00:00 2001 From: Artem Solomatin Date: Tue, 27 Feb 2024 10:04:16 +0300 Subject: [PATCH 0413/1886] Add some specs to IEx.Helpers (#13372) --- lib/iex/lib/iex/helpers.ex | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/iex/lib/iex/helpers.ex b/lib/iex/lib/iex/helpers.ex index 956efac4fd..212ca9cc20 100644 --- a/lib/iex/lib/iex/helpers.ex +++ b/lib/iex/lib/iex/helpers.ex @@ -1346,6 +1346,7 @@ defmodule IEx.Helpers do #PID<0.0.0> """ + @spec pid(binary | atom) :: pid() def pid("#PID<" <> string) do :erlang.list_to_pid(~c"<#{string}") end @@ -1373,6 +1374,7 @@ defmodule IEx.Helpers do #PID<0.64.2048> """ + @spec pid(non_neg_integer, non_neg_integer, non_neg_integer) :: pid() def pid(x, y, z) when is_integer(x) and x >= 0 and is_integer(y) and y >= 0 and is_integer(z) and z >= 0 do :erlang.list_to_pid( @@ -1392,6 +1394,7 @@ defmodule IEx.Helpers do """ @doc since: "1.8.0" + @spec port(binary) :: port() def port(string) when is_binary(string) do :erlang.list_to_port(~c"#Port<#{string}>") end @@ -1408,6 +1411,7 @@ defmodule IEx.Helpers do """ @doc since: "1.8.0" + @spec port(non_neg_integer, non_neg_integer) :: port() def port(major, minor) when is_integer(major) and major >= 0 and is_integer(minor) and minor >= 0 do :erlang.list_to_port( @@ -1425,6 +1429,7 @@ defmodule IEx.Helpers do """ @doc since: "1.6.0" + @spec ref(binary) :: reference() def ref(string) when is_binary(string) do :erlang.list_to_ref(~c"#Ref<#{string}>") end @@ -1439,6 +1444,7 @@ defmodule IEx.Helpers do """ @doc since: "1.6.0" + @spec ref(non_neg_integer, non_neg_integer, non_neg_integer, non_neg_integer) :: reference() def ref(w, x, y, z) when is_integer(w) and w >= 0 and is_integer(x) and x >= 0 and is_integer(y) and y >= 0 and is_integer(z) and z >= 0 do From 9dcdc1a5b2d03394c91419827df4a23204377d2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 27 Feb 2024 08:04:36 +0100 Subject: [PATCH 0414/1886] Add get_in/1 with safe nil-handling for access and structs (#13370) --- lib/elixir/lib/access.ex | 79 ++++++++------ lib/elixir/lib/kernel.ex | 103 +++++++++++++----- .../getting-started/keywords-and-maps.md | 4 +- lib/elixir/test/elixir/kernel_test.exs | 16 +++ 4 files changed, 139 insertions(+), 63 deletions(-) diff --git a/lib/elixir/lib/access.ex b/lib/elixir/lib/access.ex index e9ed1e4799..7b6d944a78 100644 --- a/lib/elixir/lib/access.ex +++ b/lib/elixir/lib/access.ex @@ -35,61 +35,74 @@ defmodule Access do iex> nil[:a] nil - The access syntax can also be used with the `Kernel.put_in/2`, - `Kernel.update_in/2` and `Kernel.get_and_update_in/2` macros - to allow values to be set in nested data structures: - - iex> users = %{"john" => %{age: 27}, "meg" => %{age: 23}} - iex> put_in(users["john"][:age], 28) - %{"john" => %{age: 28}, "meg" => %{age: 23}} - ## Maps and structs While the access syntax is allowed in maps via `map[key]`, if your map is made of predefined atom keys, you should prefer to access those atom keys with `map.key` instead of `map[key]`, as `map.key` will raise if the key is missing (which is not - supposed to happen if the keys are predefined). + supposed to happen if the keys are predefined) or if `map` is + `nil`. Similarly, since structs are maps and structs have predefined keys, they only allow the `struct.key` syntax and they do not - allow the `struct[key]` access syntax. `Access.key/1` can also - be used to construct dynamic access to structs and maps. + allow the `struct[key]` access syntax. - In a nutshell, when using `put_in/2` and friends: + In other words, the `map[key]` syntax is loose, returning `nil` + for missing keys, while the `map.key` syntax is strict, raising + for both nil values and missing keys. - put_in(struct_or_map.key, :value) - put_in(keyword_or_map[:key], :value) + To bridge this gap, Elixir provides the `get_in/1` and `get_in/2` + functions, which are capable of traversing nested data structures, + even in the presence of `nil`s: - When using `put_in/3` and friends: + iex> users = %{"john" => %{age: 27}, "meg" => %{age: 23}} + iex> get_in(users["john"].age) + 27 + iex> get_in(users["unknown"].age) + nil - put_in(struct_or_map, [Access.key!(:key)], :value) - put_in(keyword_or_map, [:key], :value) + Notice how, even if no user was found, `get_in/1` returned `nil`. + Outside of `get_in/1`, trying to access the field `.age` on `nil` + would raise. - This covers the dual nature of maps in Elixir, as they can be - either for structured data or as a key-value store. See the `Map` - module for more information. + The `get_in/2` function takes one step further by allowing + different accessors to be mixed in. For example, given a user + map with the `:name` and `:languages` keys, here is how to + access the name of all programming languages: - ## Nested data structures + iex> languages = [ + ...> %{name: "elixir", type: :functional}, + ...> %{name: "c", type: :procedural} + ...> ] + iex> user = %{name: "john", languages: languages} + iex> get_in(user, [:languages, Access.all(), :name]) + ["elixir", "c"] - Both key-based access syntaxes can be used with the nested update - functions and macros in `Kernel`, such as `Kernel.get_in/2`, - `Kernel.put_in/3`, `Kernel.update_in/3`, `Kernel.pop_in/2`, and - `Kernel.get_and_update_in/3`. + This module provides convenience functions for traversing other + structures, like tuples and lists. As we will see next, they can + even be used to update nested data structures. + + If you want to learn more about the dual nature of maps in Elixir, + as they can be either for structured data or as a key-value store, + see the `Map` module. - For example, to update a map inside another map: + ## Updating nested data structures + + The access syntax can also be used with the `Kernel.put_in/2`, + `Kernel.update_in/2`, `Kernel.get_and_update_in/2`, and `Kernel.pop_in/1` + macros to further manipulate values in nested data structures: iex> users = %{"john" => %{age: 27}, "meg" => %{age: 23}} iex> put_in(users["john"].age, 28) %{"john" => %{age: 28}, "meg" => %{age: 23}} - This module provides convenience functions for traversing other - structures, like tuples and lists. These functions can be used - in all the `Access`-related functions and macros in `Kernel`. - - For instance, given a user map with the `:name` and `:languages` - keys, here is how to deeply traverse the map and convert all - language names to uppercase: + As shown in the previous section, you can also use the + `Kernel.put_in/3`, `Kernel.update_in/3`, `Kernel.pop_in/2`, and + `Kernel.get_and_update_in/3` functions to provide nested + custom accessors. For instance, given a user map with the + `:name` and `:languages` keys, here is how to deeply traverse + the map and convert all language names to uppercase: iex> languages = [ ...> %{name: "elixir", type: :functional}, diff --git a/lib/elixir/lib/kernel.ex b/lib/elixir/lib/kernel.ex index 8b5026f4af..fd428e1d7c 100644 --- a/lib/elixir/lib/kernel.ex +++ b/lib/elixir/lib/kernel.ex @@ -2680,16 +2680,12 @@ defmodule Kernel do end @doc """ - Gets a value from a nested structure. + Gets a value from a nested structure with nil-safe handling. Uses the `Access` module to traverse the structures according to the given `keys`, unless the `key` is a function, which is detailed in a later section. - Note that if none of the given keys are functions, - there is rarely a reason to use `get_in` over - writing "regular" Elixir code using `[]`. - ## Examples iex> users = %{"john" => %{age: 27}, "meg" => %{age: 23}} @@ -2717,18 +2713,6 @@ defmodule Kernel do iex> users["unknown"][:age] nil - iex> users = nil - iex> get_in(users, [Access.all(), :age]) - nil - - Alternatively, if you need to access complex data-structures, you can - use pattern matching: - - case users do - %{"john" => %{age: age}} -> age - _ -> default_value - end - ## Functions as keys If a key given to `get_in/2` is a function, the function will be invoked @@ -2758,13 +2742,19 @@ defmodule Kernel do get_in(some_struct, [:some_key, :nested_key]) - The good news is that structs have predefined shape. Therefore, - you can write instead: + There are two alternatives. Given structs have predefined keys, + we can use the `struct.field` notation: some_struct.some_key.nested_key - If, by any chance, `some_key` can return nil, you can always - fallback to pattern matching to provide nested struct handling: + However, the code above will fail if any of the values return `nil`. + If you also want to handle nil values, you can use `get_in/1`: + + get_in(some_struct.some_key.nested_key) + + Pattern-matching is another option for handling such cases, + which can be especially useful if you want to match on several + fields at once or provide custom return values: case some_struct do %{some_key: %{nested_key: value}} -> value @@ -2982,6 +2972,63 @@ defmodule Kernel do defp pop_in_data(data, [key | tail]), do: Access.get_and_update(data, key, &pop_in_data(&1, tail)) + @doc """ + Gets a key from the nested structure via the given `path`, with + nil-safe handling. + + This is similar to `get_in/2`, except the path is extracted via + a macro rather than passing a list. For example: + + get_in(opts[:foo][:bar]) + + Is equivalent to: + + get_in(opts, [:foo, :bar]) + + Additionally, this macro can traverse structs: + + get_in(struct.foo.bar) + + In case any of the keys returns `nil`, then `nil` will be returned + and `get_in/1` won't traverse any further. + + Note that in order for this macro to work, the complete path must always + be visible by this macro. For more information about the supported path + expressions, please check `get_and_update_in/2` docs. + + ## Examples + + iex> users = %{"john" => %{age: 27}, "meg" => %{age: 23}} + iex> get_in(users["john"].age) + 27 + iex> get_in(users["unknown"].age) + nil + + """ + defmacro get_in(path) do + {[h | t], _} = unnest(path, [], true, "get_in/1") + nest_get_in(h, quote(do: x), t) + end + + defp nest_get_in(h, _var, []) do + h + end + + defp nest_get_in(h, var, [{:map, key} | tail]) do + quote generated: true do + case unquote(h) do + %{unquote(key) => unquote(var)} -> unquote(nest_get_in(var, var, tail)) + nil -> nil + unquote(var) -> :erlang.error({:badkey, unquote(key), unquote(var)}) + end + end + end + + defp nest_get_in(h, var, [{:access, key} | tail]) do + h = quote do: Access.get(unquote(h), unquote(key)) + nest_get_in(h, var, tail) + end + @doc """ Puts a value in a nested structure via the given `path`. @@ -3017,7 +3064,7 @@ defmodule Kernel do defmacro put_in(path, value) do case unnest(path, [], true, "put_in/2") do {[h | t], true} -> - nest_update_in(h, t, quote(do: fn _ -> unquote(value) end)) + nest_map_update_in(h, t, quote(do: fn _ -> unquote(value) end)) {[h | t], false} -> expr = nest_get_and_update_in(h, t, quote(do: fn _ -> {nil, unquote(value)} end)) @@ -3094,7 +3141,7 @@ defmodule Kernel do defmacro update_in(path, fun) do case unnest(path, [], true, "update_in/2") do {[h | t], true} -> - nest_update_in(h, t, fun) + nest_map_update_in(h, t, fun) {[h | t], false} -> expr = nest_get_and_update_in(h, t, quote(do: fn x -> {nil, unquote(fun).(x)} end)) @@ -3160,17 +3207,17 @@ defmodule Kernel do nest_get_and_update_in(h, t, fun) end - defp nest_update_in([], fun), do: fun + defp nest_map_update_in([], fun), do: fun - defp nest_update_in(list, fun) do + defp nest_map_update_in(list, fun) do quote do - fn x -> unquote(nest_update_in(quote(do: x), list, fun)) end + fn x -> unquote(nest_map_update_in(quote(do: x), list, fun)) end end end - defp nest_update_in(h, [{:map, key} | t], fun) do + defp nest_map_update_in(h, [{:map, key} | t], fun) do quote do - Map.update!(unquote(h), unquote(key), unquote(nest_update_in(t, fun))) + Map.update!(unquote(h), unquote(key), unquote(nest_map_update_in(t, fun))) end end diff --git a/lib/elixir/pages/getting-started/keywords-and-maps.md b/lib/elixir/pages/getting-started/keywords-and-maps.md index 11b1c01cbf..dabcc720ce 100644 --- a/lib/elixir/pages/getting-started/keywords-and-maps.md +++ b/lib/elixir/pages/getting-started/keywords-and-maps.md @@ -217,7 +217,7 @@ Elixir developers typically prefer to use the `map.key` syntax and pattern match ## Nested data structures -Often we will have maps inside maps, or even keywords lists inside maps, and so forth. Elixir provides conveniences for manipulating nested data structures via the `put_in/2`, `update_in/2` and other macros giving the same conveniences you would find in imperative languages while keeping the immutable properties of the language. +Often we will have maps inside maps, or even keywords lists inside maps, and so forth. Elixir provides conveniences for manipulating nested data structures via the `get_in/1`, `put_in/2`, `update_in/2`, and other macros giving the same conveniences you would find in imperative languages while keeping the immutable properties of the language. Imagine you have the following structure: @@ -259,7 +259,7 @@ iex> users = update_in users[:mary].languages, fn languages -> List.delete(langu ] ``` -There is more to learn about `put_in/2` and `update_in/2`, including the `get_and_update_in/2` that allows us to extract a value and update the data structure at once. There are also `put_in/3`, `update_in/3` and `get_and_update_in/3` which allow dynamic access into the data structure. +There is more to learn about `get_in/1`, `pop_in/1` and others, including the `get_and_update_in/2` that allows us to extract a value and update the data structure at once. There are also `get_in/3`, `put_in/3`, `update_in/3`, `get_and_update_in/3`, `pop_in/2` which allow dynamic access into the data structure. ## Summary diff --git a/lib/elixir/test/elixir/kernel_test.exs b/lib/elixir/test/elixir/kernel_test.exs index 220702fb8d..25258e0e39 100644 --- a/lib/elixir/test/elixir/kernel_test.exs +++ b/lib/elixir/test/elixir/kernel_test.exs @@ -929,6 +929,22 @@ defmodule KernelTest do defstruct [:foo, :bar] end + test "get_in/1" do + users = %{"john" => %{age: 27}, :meg => %{age: 23}} + assert get_in(users["john"][:age]) == 27 + assert get_in(users["dave"][:age]) == nil + assert get_in(users["john"].age) == 27 + assert get_in(users["dave"].age) == nil + assert get_in(users.meg[:age]) == 23 + assert get_in(users.meg.age) == 23 + + is_nil = nil + assert get_in(is_nil.age) == nil + + assert_raise KeyError, ~r"key :unknown not found", fn -> get_in(users.unknown) end + assert_raise KeyError, ~r"key :unknown not found", fn -> get_in(users.meg.unknown) end + end + test "get_in/2" do users = %{"john" => %{age: 27}, "meg" => %{age: 23}} assert get_in(users, ["john", :age]) == 27 From 80eef8c571e7e5980fc6ec9834638bf28051a73c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 27 Feb 2024 08:30:53 +0100 Subject: [PATCH 0415/1886] Load current application in compile.app --- lib/mix/lib/mix/tasks/compile.app.ex | 5 +++++ lib/mix/test/mix/tasks/compile.app_test.exs | 4 ++++ lib/mix/test/mix/tasks/eval_test.exs | 2 +- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/mix/lib/mix/tasks/compile.app.ex b/lib/mix/lib/mix/tasks/compile.app.ex index 5f1445a662..0dc525de42 100644 --- a/lib/mix/lib/mix/tasks/compile.app.ex +++ b/lib/mix/lib/mix/tasks/compile.app.ex @@ -83,6 +83,9 @@ defmodule Mix.Tasks.Compile.App do The complete list can be found on [Erlang's application specification](https://www.erlang.org/doc/man/app). + From Elixir v1.17 onwards, the application .app file is also loaded + whenever the task runs. + ## Command line options * `--force` - forces compilation regardless of modification times @@ -167,12 +170,14 @@ defmodule Mix.Tasks.Compile.App do properties = [config_mtime: new_mtime] ++ properties contents = :io_lib.format("~p.~n", [{:application, app, properties}]) + :application.load({:application, app, properties}) Mix.Project.ensure_structure() File.write!(target, IO.chardata_to_string(contents)) Mix.shell().info("Generated #{app} app") {:ok, []} else + :application.load({:application, app, current_properties}) {:noop, []} end end diff --git a/lib/mix/test/mix/tasks/compile.app_test.exs b/lib/mix/test/mix/tasks/compile.app_test.exs index 3e340234ed..26123d4846 100644 --- a/lib/mix/test/mix/tasks/compile.app_test.exs +++ b/lib/mix/test/mix/tasks/compile.app_test.exs @@ -71,12 +71,15 @@ defmodule Mix.Tasks.Compile.AppTest do assert Mix.Tasks.Compile.App.run([]) == {:ok, []} properties = parse_resource_file(:sample) + assert Application.spec(:sample, :vsn) == ~c"0.1.0" assert properties[:vsn] == ~c"0.1.0" assert properties[:modules] == [A, B] assert properties[:applications] == [:kernel, :stdlib, :elixir] refute Keyword.has_key?(properties, :compile_env) + Application.unload(:sample) assert Mix.Tasks.Compile.App.run([]) == {:noop, []} + assert Application.spec(:sample, :vsn) == ~c"0.1.0" end) end @@ -114,6 +117,7 @@ defmodule Mix.Tasks.Compile.AppTest do Mix.Tasks.Compile.App.run([]) properties = parse_resource_file(:custom_project) + assert Application.spec(:custom_project, :vsn) == ~c"0.2.0" assert properties[:vsn] == ~c"0.2.0" assert properties[:maxT] == :infinity assert properties[:optional_applications] == [:ex_unit, :mix] diff --git a/lib/mix/test/mix/tasks/eval_test.exs b/lib/mix/test/mix/tasks/eval_test.exs index a92032d432..fe113f6618 100644 --- a/lib/mix/test/mix/tasks/eval_test.exs +++ b/lib/mix/test/mix/tasks/eval_test.exs @@ -9,7 +9,7 @@ defmodule Mix.Tasks.EvalTest do test "does not start applications", context do in_tmp(context.test, fn -> - expr = "send self(), {:apps, Application.loaded_applications()}" + expr = "send self(), {:apps, Application.started_applications()}" Mix.Tasks.Eval.run([expr]) assert_received {:apps, apps} refute List.keyfind(apps, :sample, 0) From d2276d8f85e212bc26a81b4364998421da4beae7 Mon Sep 17 00:00:00 2001 From: James Lavin Date: Tue, 27 Feb 2024 04:12:05 -0500 Subject: [PATCH 0416/1886] Document zip/2 functions to create keyword lists (#13356) --- lib/elixir/lib/enum.ex | 7 +++++++ lib/elixir/lib/stream.ex | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/lib/elixir/lib/enum.ex b/lib/elixir/lib/enum.ex index e81c961831..49b088aced 100644 --- a/lib/elixir/lib/enum.ex +++ b/lib/elixir/lib/enum.ex @@ -3889,6 +3889,10 @@ defmodule Enum do Zips corresponding elements from two enumerables into a list of tuples. + Because a list of two-element tuples with atoms as the first + tuple element is a keyword list (`Keyword`), zipping a first list + of atoms with a second list of any kind creates a keyword list. + The zipping finishes as soon as either enumerable completes. ## Examples @@ -3896,6 +3900,9 @@ defmodule Enum do iex> Enum.zip([1, 2, 3], [:a, :b, :c]) [{1, :a}, {2, :b}, {3, :c}] + iex> Enum.zip([:a, :b, :c], [1, 2, 3]) + [a: 1, b: 2, c: 3] + iex> Enum.zip([1, 2, 3, 4, 5], [:a, :b, :c]) [{1, :a}, {2, :b}, {3, :c}] diff --git a/lib/elixir/lib/stream.ex b/lib/elixir/lib/stream.ex index 55cab5aaed..74f3feef53 100644 --- a/lib/elixir/lib/stream.ex +++ b/lib/elixir/lib/stream.ex @@ -1200,6 +1200,11 @@ defmodule Stream do @doc """ Zips two enumerables together, lazily. + Because a list of two-element tuples with atoms as the first + tuple element is a keyword list (`Keyword`), zipping a first `Stream` + of atoms with a second `Stream` of any kind creates a `Stream` + that generates a keyword list. + The zipping finishes as soon as either enumerable completes. ## Examples @@ -1208,6 +1213,8 @@ defmodule Stream do iex> cycle = Stream.cycle([:a, :b, :c]) iex> Stream.zip(concat, cycle) |> Enum.to_list() [{1, :a}, {2, :b}, {3, :c}, {4, :a}, {5, :b}, {6, :c}] + iex> Stream.zip(cycle, concat) |> Enum.to_list() + [a: 1, b: 2, c: 3, a: 4, b: 5, c: 6] """ @spec zip(Enumerable.t(), Enumerable.t()) :: Enumerable.t() From fc90206862816f872dc2cab070a3f989a77c852e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Tue, 27 Feb 2024 16:33:15 +0100 Subject: [PATCH 0417/1886] Document and ensure invalid tests have proper formatter state, closes #13373 --- lib/ex_unit/lib/ex_unit.ex | 6 +++++- lib/ex_unit/lib/ex_unit/cli_formatter.ex | 12 +++++++++--- lib/ex_unit/lib/ex_unit/runner.ex | 2 +- lib/ex_unit/test/ex_unit/failures_manifest_test.exs | 2 +- lib/ex_unit/test/ex_unit/runner_stats_test.exs | 2 +- 5 files changed, 17 insertions(+), 7 deletions(-) diff --git a/lib/ex_unit/lib/ex_unit.ex b/lib/ex_unit/lib/ex_unit.ex index b10c1c8a49..edaa4a0d48 100644 --- a/lib/ex_unit/lib/ex_unit.ex +++ b/lib/ex_unit/lib/ex_unit.ex @@ -70,7 +70,11 @@ defmodule ExUnit do """ @type state :: - nil | {:excluded, binary} | {:failed, failed} | {:invalid, module} | {:skipped, binary} + nil + | {:excluded, binary} + | {:failed, failed} + | {:invalid, ExUnit.TestModule.t()} + | {:skipped, binary} @typedoc "The error state returned by `ExUnit.Test` and `ExUnit.TestModule`" @type failed :: [{Exception.kind(), reason :: term, Exception.stacktrace()}] diff --git a/lib/ex_unit/lib/ex_unit/cli_formatter.ex b/lib/ex_unit/lib/ex_unit/cli_formatter.ex index 472a3bbea9..9e2c4d0d4a 100644 --- a/lib/ex_unit/lib/ex_unit/cli_formatter.ex +++ b/lib/ex_unit/lib/ex_unit/cli_formatter.ex @@ -65,7 +65,8 @@ defmodule ExUnit.CLIFormatter do {:noreply, update_test_timings(config, test)} end - def handle_cast({:test_finished, %ExUnit.Test{state: {:excluded, _}} = test}, config) do + def handle_cast({:test_finished, %ExUnit.Test{state: {:excluded, reason}} = test}, config) + when is_binary(reason) do if config.trace, do: IO.puts(trace_test_excluded(test)) test_counter = update_test_counter(config.test_counter, test) @@ -74,7 +75,8 @@ defmodule ExUnit.CLIFormatter do {:noreply, config} end - def handle_cast({:test_finished, %ExUnit.Test{state: {:skipped, _}} = test}, config) do + def handle_cast({:test_finished, %ExUnit.Test{state: {:skipped, reason}} = test}, config) + when is_binary(reason) do if config.trace do IO.puts(skipped(trace_test_skipped(test), config)) else @@ -87,7 +89,11 @@ defmodule ExUnit.CLIFormatter do {:noreply, config} end - def handle_cast({:test_finished, %ExUnit.Test{state: {:invalid, _}} = test}, config) do + def handle_cast( + {:test_finished, + %ExUnit.Test{state: {:invalid, %ExUnit.TestModule{state: {:failed, _}}}} = test}, + config + ) do if config.trace do IO.puts(invalid(trace_test_result(test), config)) else diff --git a/lib/ex_unit/lib/ex_unit/runner.ex b/lib/ex_unit/lib/ex_unit/runner.ex index 526a834084..34bef5e43f 100644 --- a/lib/ex_unit/lib/ex_unit/runner.ex +++ b/lib/ex_unit/lib/ex_unit/runner.ex @@ -293,8 +293,8 @@ defmodule ExUnit.Runner do {test_module, invalid_tests, []} {:DOWN, ^module_ref, :process, ^module_pid, error} -> - invalid_tests = mark_tests_invalid(tests, test_module) test_module = %{test_module | state: failed({:EXIT, module_pid}, error, [])} + invalid_tests = mark_tests_invalid(tests, test_module) {test_module, invalid_tests, []} end diff --git a/lib/ex_unit/test/ex_unit/failures_manifest_test.exs b/lib/ex_unit/test/ex_unit/failures_manifest_test.exs index 98a5562157..9850a21f47 100644 --- a/lib/ex_unit/test/ex_unit/failures_manifest_test.exs +++ b/lib/ex_unit/test/ex_unit/failures_manifest_test.exs @@ -9,7 +9,7 @@ defmodule ExUnit.FailuresManifestTest do @skipped {:skipped, "reason"} @excluded {:excluded, "reason"} @failed {:failed, []} - @invalid {:invalid, SomeMod} + @invalid {:invalid, %ExUnit.TestModule{}} describe "files_with_failures/1" do test "returns the set of files with failures" do diff --git a/lib/ex_unit/test/ex_unit/runner_stats_test.exs b/lib/ex_unit/test/ex_unit/runner_stats_test.exs index c618c87784..bed956e942 100644 --- a/lib/ex_unit/test/ex_unit/runner_stats_test.exs +++ b/lib/ex_unit/test/ex_unit/runner_stats_test.exs @@ -159,7 +159,7 @@ defmodule ExUnit.RunnerStatsTest do defp state_for(:passed), do: nil defp state_for(:failed), do: {:failed, []} - defp state_for(:invalid), do: {:invalid, TestModule} + defp state_for(:invalid), do: {:invalid, %ExUnit.TestModule{}} defp state_for(:skipped), do: {:skipped, "reason"} defp state_for(:excluded), do: {:excluded, "reason"} From 6ce06d512a5842528e95d08297136a0e902047bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 28 Feb 2024 08:16:18 +0100 Subject: [PATCH 0418/1886] Ensure logger compiler app is available if compile.elixir is called directly --- lib/mix/lib/mix/tasks/compile.all.ex | 20 +++----------- lib/mix/lib/mix/tasks/compile.elixir.ex | 36 ++++++++++++++++++------- 2 files changed, 29 insertions(+), 27 deletions(-) diff --git a/lib/mix/lib/mix/tasks/compile.all.ex b/lib/mix/lib/mix/tasks/compile.all.ex index 268a672754..3adad730c4 100644 --- a/lib/mix/lib/mix/tasks/compile.all.ex +++ b/lib/mix/lib/mix/tasks/compile.all.ex @@ -53,11 +53,9 @@ defmodule Mix.Tasks.Compile.All do # Build the project structure so we can write down compiled files. Mix.Project.build_structure(config) - with_logger_app(config, fn -> - config - |> Mix.Tasks.Compile.compilers() - |> compile(args, :noop, []) - end) + config + |> Mix.Tasks.Compile.compilers() + |> compile(args, :noop, []) end if app_cache do @@ -84,18 +82,6 @@ defmodule Mix.Tasks.Compile.All do result end - defp with_logger_app(config, fun) do - app = Keyword.fetch!(config, :app) - logger_config_app = Application.get_env(:logger, :compile_time_application) - - try do - Logger.configure(compile_time_application: app) - fun.() - after - Logger.configure(compile_time_application: logger_config_app) - end - end - defp compile([], _, status, diagnostics) do {status, diagnostics} end diff --git a/lib/mix/lib/mix/tasks/compile.elixir.ex b/lib/mix/lib/mix/tasks/compile.elixir.ex index eb814d3099..48372b855c 100644 --- a/lib/mix/lib/mix/tasks/compile.elixir.ex +++ b/lib/mix/lib/mix/tasks/compile.elixir.ex @@ -126,16 +126,19 @@ defmodule Mix.Tasks.Compile.Elixir do # Having compilations racing with other is most undesired, # so we wrap the compiler in a lock. Ideally we would use # flock in the future. - Mix.State.lock(__MODULE__, fn -> - Mix.Compilers.Elixir.compile( - manifest, - srcs, - dest, - cache_key, - Mix.Tasks.Compile.Erlang.manifests(), - Mix.Tasks.Compile.Erlang.modules(), - opts - ) + + with_logger_app(project, fn -> + Mix.State.lock(__MODULE__, fn -> + Mix.Compilers.Elixir.compile( + manifest, + srcs, + dest, + cache_key, + Mix.Tasks.Compile.Erlang.manifests(), + Mix.Tasks.Compile.Erlang.modules(), + opts + ) + end) end) end @@ -149,6 +152,19 @@ defmodule Mix.Tasks.Compile.Elixir do Mix.Compilers.Elixir.clean(manifest(), dest) end + # Run this operation in compile.elixir as the compiler can be called directly + defp with_logger_app(config, fun) do + app = Keyword.fetch!(config, :app) + logger_config_app = Application.get_env(:logger, :compile_time_application) + + try do + Logger.configure(compile_time_application: app) + fun.() + after + Logger.configure(compile_time_application: logger_config_app) + end + end + defp xref_exclude_opts(opts, project) do exclude = List.wrap(project[:xref][:exclude]) From 7b9e907a521decda7391a60511a37d200ac3d0e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 28 Feb 2024 08:38:17 +0100 Subject: [PATCH 0419/1886] Solve bootstrap issues on mix compile.elixir --- lib/mix/lib/mix/tasks/compile.elixir.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/mix/lib/mix/tasks/compile.elixir.ex b/lib/mix/lib/mix/tasks/compile.elixir.ex index 48372b855c..1a0f168c25 100644 --- a/lib/mix/lib/mix/tasks/compile.elixir.ex +++ b/lib/mix/lib/mix/tasks/compile.elixir.ex @@ -158,10 +158,10 @@ defmodule Mix.Tasks.Compile.Elixir do logger_config_app = Application.get_env(:logger, :compile_time_application) try do - Logger.configure(compile_time_application: app) + Application.put_env(:logger, :compile_time_application, app) fun.() after - Logger.configure(compile_time_application: logger_config_app) + Application.put_env(:logger, :compile_time_application, logger_config_app) end end From 0a51c21290c76ef8f211f13a210a804bd90d04a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 28 Feb 2024 13:03:10 +0100 Subject: [PATCH 0420/1886] Optimize ExUnit runner by short-circuiting logic and merging list passes --- lib/ex_unit/lib/ex_unit/filters.ex | 41 +++++++++++++++++------------- lib/ex_unit/lib/ex_unit/runner.ex | 19 ++++++++------ 2 files changed, 34 insertions(+), 26 deletions(-) diff --git a/lib/ex_unit/lib/ex_unit/filters.ex b/lib/ex_unit/lib/ex_unit/filters.ex index 82f509c6c7..749f17570b 100644 --- a/lib/ex_unit/lib/ex_unit/filters.ex +++ b/lib/ex_unit/lib/ex_unit/filters.ex @@ -1,9 +1,8 @@ defmodule ExUnit.Filters do - alias ExUnit.FailuresManifest - @moduledoc """ Conveniences for parsing and evaluating filters. """ + alias ExUnit.FailuresManifest @type t :: list({atom, Regex.t() | String.Chars.t()} | atom) @type location :: {:location, {String.t(), pos_integer | [pos_integer, ...]}} @@ -195,26 +194,32 @@ defmodule ExUnit.Filters do @spec eval(t, t, map, [ExUnit.Test.t()]) :: :ok | {:excluded, String.t()} | {:skipped, String.t()} def eval(include, exclude, tags, collection) when is_map(tags) do - excluded = Enum.find_value(exclude, &has_tag(&1, tags, collection)) - excluded? = excluded != nil - included? = Enum.any?(include, &has_tag(&1, tags, collection)) + cond do + Enum.any?(include, &has_tag(&1, tags, collection)) -> + maybe_skipped(include, tags, collection) + + excluded = Enum.find_value(exclude, &has_tag(&1, tags, collection)) -> + {:excluded, "due to #{excluded} filter"} - if included? or not excluded? do - skip_tag = %{skip: Map.get(tags, :skip, true)} - skip_included_explicitly? = Enum.any?(include, &has_tag(&1, skip_tag, collection)) + true -> + maybe_skipped(include, tags, collection) + end + end - case Map.fetch(tags, :skip) do - {:ok, msg} when is_binary(msg) and not skip_included_explicitly? -> - {:skipped, msg} + defp maybe_skipped(include, tags, collection) do + case tags do + %{skip: skip} when is_binary(skip) or skip == true -> + skip_tags = %{skip: skip} + skip_included_explicitly? = Enum.any?(include, &has_tag(&1, skip_tags, collection)) - {:ok, true} when not skip_included_explicitly? -> - {:skipped, "due to skip tag"} + cond do + skip_included_explicitly? -> :ok + is_binary(skip) -> {:skipped, skip} + skip -> {:skipped, "due to skip tag"} + end - _ -> - :ok - end - else - {:excluded, "due to #{excluded} filter"} + _ -> + :ok end end diff --git a/lib/ex_unit/lib/ex_unit/runner.ex b/lib/ex_unit/lib/ex_unit/runner.ex index 34bef5e43f..b4c6a2bfde 100644 --- a/lib/ex_unit/lib/ex_unit/runner.ex +++ b/lib/ex_unit/lib/ex_unit/runner.ex @@ -216,8 +216,7 @@ defmodule ExUnit.Runner do EM.module_started(config.manager, test_module) # Prepare tests, selecting which ones should be run or skipped - tests = prepare_tests(config, test_module.tests) - {excluded_and_skipped_tests, to_run_tests} = Enum.split_with(tests, & &1.state) + {to_run_tests, excluded_and_skipped_tests} = prepare_tests(config, test_module.tests) for excluded_or_skipped_test <- excluded_and_skipped_tests do EM.test_started(config.manager, excluded_or_skipped_test) @@ -257,14 +256,18 @@ defmodule ExUnit.Runner do exclude = config.exclude test_ids = config.only_test_ids - for test <- tests, include_test?(test_ids, test) do - tags = Map.merge(test.tags, %{test: test.name, module: test.module}) + {to_run, to_skip} = + for test <- tests, include_test?(test_ids, test), reduce: {[], []} do + {to_run, to_skip} -> + tags = Map.merge(test.tags, %{test: test.name, module: test.module}) - case ExUnit.Filters.eval(include, exclude, tags, tests) do - :ok -> %{test | tags: tags} - excluded_or_skipped -> %{test | state: excluded_or_skipped} + case ExUnit.Filters.eval(include, exclude, tags, tests) do + :ok -> {[%{test | tags: tags} | to_run], to_skip} + excluded_or_skipped -> {to_run, [%{test | state: excluded_or_skipped} | to_skip]} + end end - end + + {Enum.reverse(to_run), Enum.reverse(to_skip)} end defp include_test?(test_ids, test) do From 2d05f57ddf9eb12a41f94bf124f71fae0e0b9182 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 28 Feb 2024 13:30:12 +0100 Subject: [PATCH 0421/1886] Update IEx TODOs --- lib/iex/lib/iex.ex | 5 ++++- lib/iex/lib/iex/cli.ex | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/iex/lib/iex.ex b/lib/iex/lib/iex.ex index 0b200a7ba5..d5e2cea182 100644 --- a/lib/iex/lib/iex.ex +++ b/lib/iex/lib/iex.ex @@ -887,7 +887,10 @@ defmodule IEx do end end - # TODO: Make this public when we require Erlang/OTP 26+ + # TODO: The idea is to expose this as a public API once we require + # Erlang/OTP 26 but it may not be possible. In such cases, we may + # want to move this to another module. + # See https://github.com/erlang/otp/issues/8113#issuecomment-1941613281 @compile {:no_warn_undefined, {:shell, :start_interactive, 1}} @doc false diff --git a/lib/iex/lib/iex/cli.ex b/lib/iex/lib/iex/cli.ex index ffb8466b49..1e73222bec 100644 --- a/lib/iex/lib/iex/cli.ex +++ b/lib/iex/lib/iex/cli.ex @@ -1,4 +1,4 @@ -# Remove this whole module on Erlang/OTP 26+. +# TODO: Remove this whole module on Erlang/OTP 26+. defmodule IEx.CLI do @moduledoc false From d9005ebdcaa718642f6cc246ee1568cce08d6b0b Mon Sep 17 00:00:00 2001 From: Artem Solomatin Date: Thu, 29 Feb 2024 02:23:43 +0300 Subject: [PATCH 0422/1886] Add example and update links in debugging doc (#13376) --- lib/elixir/pages/getting-started/debugging.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/elixir/pages/getting-started/debugging.md b/lib/elixir/pages/getting-started/debugging.md index 132bb457c8..28004643d8 100644 --- a/lib/elixir/pages/getting-started/debugging.md +++ b/lib/elixir/pages/getting-started/debugging.md @@ -120,6 +120,7 @@ Or during tests (the `--trace` flag on `mix test` prevents tests from timing out ```console $ iex --dbg pry -S mix test --trace +$ iex --dbg pry -S mix test path/to/file:line --trace ``` Now a call to `dbg` will ask if you want to pry the existing code. If you accept, you'll be able to access all variables, as well as imports and aliases from the code, directly from IEx. This is called "prying". While the pry session is running, the code execution stops, until `continue` or `next` are called. Remember you can always run `iex` in the context of a project with `iex -S mix TASK`. @@ -170,7 +171,7 @@ We have just scratched the surface of what the Erlang VM has to offer, for examp * Alongside the observer application, Erlang also includes a [`:crashdump_viewer`](https://www.erlang.org/doc/man/crashdump_viewer.html) to view crash dumps - * Integration with OS level tracers, such as [Linux Trace Toolkit,](http://www.erlang.org/doc/apps/runtime_tools/LTTng.html) [DTRACE,](http://www.erlang.org/doc/apps/runtime_tools/DTRACE.html) and [SystemTap](http://www.erlang.org/doc/apps/runtime_tools/SYSTEMTAP.html) + * Integration with OS level tracers, such as [Linux Trace Toolkit,](https://www.erlang.org/doc/apps/runtime_tools/lttng) [DTRACE,](https://www.erlang.org/doc/apps/runtime_tools/dtrace) and [SystemTap](https://www.erlang.org/doc/apps/runtime_tools/systemtap) * [Microstate accounting](http://www.erlang.org/doc/man/msacc.html) measures how much time the runtime spends in several low-level tasks in a short time interval From 2a430b1795645830444797698523710e90566ff2 Mon Sep 17 00:00:00 2001 From: Douglas Vought Date: Thu, 29 Feb 2024 02:21:25 -0500 Subject: [PATCH 0423/1886] Support IO capture of group leader from external process (#13374) --- lib/ex_unit/lib/ex_unit/capture_io.ex | 114 ++++++++++++++----- lib/ex_unit/test/ex_unit/capture_io_test.exs | 58 ++++++++++ 2 files changed, 142 insertions(+), 30 deletions(-) diff --git a/lib/ex_unit/lib/ex_unit/capture_io.ex b/lib/ex_unit/lib/ex_unit/capture_io.ex index f16ecb5750..4a861b8563 100644 --- a/lib/ex_unit/lib/ex_unit/capture_io.ex +++ b/lib/ex_unit/lib/ex_unit/capture_io.ex @@ -29,15 +29,15 @@ defmodule ExUnit.CaptureIO do Returns the binary which is the captured output. - By default, `capture_io` replaces the `group_leader` (`:stdio`) - for the current process. Capturing the group leader is done per - process and therefore can be done concurrently. + By default, `capture_io` replaces the `Process.group_leader/0` of the current + process, which is the process used by default for all IO operations. Capturing + the group leader of the current process is safe to run concurrently, under + `async: true` tests. You may also explicitly capture the group leader of + another process, however that is not safe to do concurrently. - However, the capturing of any other named device, such as `:stderr`, - happens globally and persists until the function has ended. While this means - it is safe to run your tests with `async: true` in many cases, captured output - may include output from a different test and care must be taken when using - `capture_io` with a named process asynchronously. + You may also capture any other named IO device, such as `:stderr`. This is + also safe to run concurrently but, if several tests are writting to the same + device at once, captured output may include output from a different test. A developer can set a string as an input. The default input is an empty string. If capturing a named device asynchronously, an input can only be given @@ -51,15 +51,28 @@ defmodule ExUnit.CaptureIO do ## IO devices - You may capture the IO from any registered IO device. The device name given - must be an atom representing the name of a registered process. In addition, - Elixir provides two shortcuts: + You may capture the IO of the group leader of any process, by passing a `pid` + as argument, or from any registered IO device given as an `atom`. Here are + some example values: - * `:stdio` - a shortcut for `:standard_io`, which maps to - the current `Process.group_leader/0` in Erlang + * `:stdio`, `:standard_io` - a shortcut for capturing the group leader + of the current process. It is equivalent to passing `self()` as the + first argument. This is safe to run concurrently and captures only + the of the current process or any child process spawned inside the + given function - * `:stderr` - a shortcut for the named process `:standard_error` - provided in Erlang + * `:stderr`, `:standard_error` - captures all IO to standard error + (represented internally by an Erlang process named `:standard_error`). + This is safe to run concurrently but it will capture the output + of any other test writing to the same named device + + * any other atom - captures all IO to the given device given by the + atom. This is safe to run concurrently but it will capture the output + of any other test writing to the same named device + + * any other pid (since v1.17.0) - captures all IO to the group leader + of the given process. This option is not safe to run concurrently + if the pid is not `self()`. Tests using this value must set `async: true` ## Options @@ -91,10 +104,10 @@ defmodule ExUnit.CaptureIO do ...> end) == "this is input" true - Note it is fine to use `==` with standard IO, because the content is captured - per test process. However, `:stderr` is shared across all tests, so you will - want to use `=~` instead of `==` for assertions on `:stderr` if your tests - are async: + Note it is fine to use `==` with `:stdio` (the default IO device), because + the content is captured per test process. However, `:stderr` is shared + across all tests, so you will want to use `=~` instead of `==` for assertions + on `:stderr` if your tests are async: iex> capture_io(:stderr, fn -> IO.write(:stderr, "john") end) =~ "john" true @@ -110,6 +123,14 @@ defmodule ExUnit.CaptureIO do Otherwise, if the standard error of any other test is captured, the test will fail. + To capture the IO from another process, you can pass a `pid`: + + capture_io(GenServer.whereis(MyServer), fn -> + GenServer.call(MyServer, :do_something) + end) + + Tests that directly capture a PID cannot run concurrently. + ## Returning values As seen in the examples above, `capture_io` returns the captured output. @@ -127,14 +148,19 @@ defmodule ExUnit.CaptureIO do See `capture_io/1` for more information. """ - @spec capture_io(atom() | String.t() | keyword(), (-> any())) :: String.t() - def capture_io(device_input_or_options, fun) + @spec capture_io(atom() | pid() | String.t() | keyword(), (-> any())) :: String.t() + def capture_io(device_pid_input_or_options, fun) def capture_io(device, fun) when is_atom(device) and is_function(fun, 0) do {_result, capture} = with_io(device, fun) capture end + def capture_io(pid, fun) when is_pid(pid) and is_function(fun, 0) do + {_result, capture} = with_io(pid, fun) + capture + end + def capture_io(input, fun) when is_binary(input) and is_function(fun, 0) do {_result, capture} = with_io(input, fun) capture @@ -150,8 +176,8 @@ defmodule ExUnit.CaptureIO do See `capture_io/1` for more information. """ - @spec capture_io(atom(), String.t() | keyword(), (-> any())) :: String.t() - def capture_io(device, input_or_options, fun) + @spec capture_io(atom() | pid(), String.t() | keyword(), (-> any())) :: String.t() + def capture_io(device_or_pid, input_or_options, fun) def capture_io(device, input, fun) when is_atom(device) and is_binary(input) and is_function(fun, 0) do @@ -165,6 +191,18 @@ defmodule ExUnit.CaptureIO do capture end + def capture_io(pid, input, fun) + when is_pid(pid) and is_binary(input) and is_function(fun, 0) do + {_result, capture} = with_io(pid, input, fun) + capture + end + + def capture_io(pid, options, fun) + when is_pid(pid) and is_list(options) and is_function(fun, 0) do + {_result, capture} = with_io(pid, options, fun) + capture + end + @doc ~S""" Invokes the given `fun` and returns the result and captured output. @@ -194,13 +232,17 @@ defmodule ExUnit.CaptureIO do See `with_io/1` for more information. """ @doc since: "1.13.0" - @spec with_io(atom() | String.t() | keyword(), (-> any())) :: {any(), String.t()} - def with_io(device_input_or_options, fun) + @spec with_io(atom() | pid() | String.t() | keyword(), (-> any())) :: {any(), String.t()} + def with_io(device_pid_input_or_options, fun) def with_io(device, fun) when is_atom(device) and is_function(fun, 0) do with_io(device, [], fun) end + def with_io(pid, fun) when is_pid(pid) and is_function(fun, 0) do + with_io(pid, [], fun) + end + def with_io(input, fun) when is_binary(input) and is_function(fun, 0) do with_io(:stdio, [input: input], fun) end @@ -215,8 +257,8 @@ defmodule ExUnit.CaptureIO do See `with_io/1` for more information. """ @doc since: "1.13.0" - @spec with_io(atom(), String.t() | keyword(), (-> any())) :: {any(), String.t()} - def with_io(device, input_or_options, fun) + @spec with_io(atom() | pid(), String.t() | keyword(), (-> any())) :: {any(), String.t()} + def with_io(device_or_pid, input_or_options, fun) def with_io(device, input, fun) when is_atom(device) and is_binary(input) and is_function(fun, 0) do @@ -228,23 +270,35 @@ defmodule ExUnit.CaptureIO do do_with_io(map_dev(device), options, fun) end + def with_io(pid, input, fun) + when is_pid(pid) and is_binary(input) and is_function(fun, 0) do + with_io(pid, [input: input], fun) + end + + def with_io(pid, options, fun) + when is_pid(pid) and is_list(options) and is_function(fun, 0) do + do_with_io(pid, options, fun) + end + defp map_dev(:stdio), do: :standard_io defp map_dev(:stderr), do: :standard_error defp map_dev(other), do: other - defp do_with_io(:standard_io, options, fun) do + defp do_with_io(device_or_pid, options, fun) + when device_or_pid == :standard_io or is_pid(device_or_pid) do prompt_config = Keyword.get(options, :capture_prompt, true) encoding = Keyword.get(options, :encoding, :unicode) input = Keyword.get(options, :input, "") original_gl = Process.group_leader() {:ok, capture_gl} = StringIO.open(input, capture_prompt: prompt_config, encoding: encoding) + pid = if is_pid(device_or_pid), do: device_or_pid, else: self() try do - Process.group_leader(self(), capture_gl) + Process.group_leader(pid, capture_gl) do_capture_gl(capture_gl, fun) after - Process.group_leader(self(), original_gl) + Process.group_leader(pid, original_gl) end end diff --git a/lib/ex_unit/test/ex_unit/capture_io_test.exs b/lib/ex_unit/test/ex_unit/capture_io_test.exs index d26d3f96b8..1a2f22b846 100644 --- a/lib/ex_unit/test/ex_unit/capture_io_test.exs +++ b/lib/ex_unit/test/ex_unit/capture_io_test.exs @@ -28,6 +28,38 @@ defmodule ExUnit.CaptureIOTest do end end + defmodule MockProc do + use GenServer + + def start_link do + GenServer.start_link(__MODULE__, []) + end + + @impl GenServer + def init(_), do: {:ok, nil} + + @impl GenServer + def handle_call({:stdio, message}, _from, state) do + IO.puts(message) + {:reply, :ok, state} + end + + @impl GenServer + def handle_call({:prompt, prompt}, _from, state) do + prompt + |> IO.gets() + |> IO.puts() + + {:reply, :ok, state} + end + + @impl GenServer + def handle_call({:stderr, message}, _from, state) do + IO.puts(:stderr, message) + {:reply, :ok, state} + end + end + import ExUnit.CaptureIO doctest ExUnit.CaptureIO, import: true @@ -469,6 +501,32 @@ defmodule ExUnit.CaptureIOTest do end end + test "capture_io with a separate process" do + {:ok, pid} = MockProc.start_link() + + assert capture_io(pid, fn -> + GenServer.call(pid, {:stdio, "a"}) + end) == "a\n" + + assert capture_io(pid, [input: "b"], fn -> + GenServer.call(pid, {:prompt, "> "}) + end) == "> b\n" + + assert capture_io(pid, "c", fn -> + GenServer.call(pid, {:prompt, "> "}) + end) == "> c\n" + + assert capture_io(pid, [input: "d", capture_prompt: false], fn -> + GenServer.call(pid, {:prompt, "> "}) + end) == "d\n" + + assert capture_io(:stderr, fn -> + GenServer.call(pid, {:stderr, "uhoh"}) + end) == "uhoh\n" + + GenServer.stop(pid) + end + test "with_io" do assert with_io(fn -> :io.put_chars("xyz") From 7e5ccce5ef6bf795825f9395254bcd8334328662 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 29 Feb 2024 08:35:59 +0100 Subject: [PATCH 0424/1886] Simplify capture clauses and ensure group leader is reversed --- lib/ex_unit/lib/ex_unit/capture_io.ex | 62 ++++++-------------- lib/ex_unit/test/ex_unit/capture_io_test.exs | 17 ++++-- 2 files changed, 30 insertions(+), 49 deletions(-) diff --git a/lib/ex_unit/lib/ex_unit/capture_io.ex b/lib/ex_unit/lib/ex_unit/capture_io.ex index 4a861b8563..c28e200bed 100644 --- a/lib/ex_unit/lib/ex_unit/capture_io.ex +++ b/lib/ex_unit/lib/ex_unit/capture_io.ex @@ -151,23 +151,15 @@ defmodule ExUnit.CaptureIO do @spec capture_io(atom() | pid() | String.t() | keyword(), (-> any())) :: String.t() def capture_io(device_pid_input_or_options, fun) - def capture_io(device, fun) when is_atom(device) and is_function(fun, 0) do - {_result, capture} = with_io(device, fun) + def capture_io(device_or_pid, fun) + when (is_atom(device_or_pid) or is_pid(device_or_pid)) and is_function(fun, 0) do + {_result, capture} = with_io(device_or_pid, fun) capture end - def capture_io(pid, fun) when is_pid(pid) and is_function(fun, 0) do - {_result, capture} = with_io(pid, fun) - capture - end - - def capture_io(input, fun) when is_binary(input) and is_function(fun, 0) do - {_result, capture} = with_io(input, fun) - capture - end - - def capture_io(options, fun) when is_list(options) and is_function(fun, 0) do - {_result, capture} = with_io(options, fun) + def capture_io(input_or_options, fun) + when (is_binary(input_or_options) or is_list(input_or_options)) and is_function(fun, 0) do + {_result, capture} = with_io(input_or_options, fun) capture end @@ -178,28 +170,9 @@ defmodule ExUnit.CaptureIO do """ @spec capture_io(atom() | pid(), String.t() | keyword(), (-> any())) :: String.t() def capture_io(device_or_pid, input_or_options, fun) - - def capture_io(device, input, fun) - when is_atom(device) and is_binary(input) and is_function(fun, 0) do - {_result, capture} = with_io(device, input, fun) - capture - end - - def capture_io(device, options, fun) - when is_atom(device) and is_list(options) and is_function(fun, 0) do - {_result, capture} = with_io(device, options, fun) - capture - end - - def capture_io(pid, input, fun) - when is_pid(pid) and is_binary(input) and is_function(fun, 0) do - {_result, capture} = with_io(pid, input, fun) - capture - end - - def capture_io(pid, options, fun) - when is_pid(pid) and is_list(options) and is_function(fun, 0) do - {_result, capture} = with_io(pid, options, fun) + when (is_atom(device_or_pid) or is_pid(device_or_pid)) and + (is_binary(input_or_options) or is_list(input_or_options)) and is_function(fun, 0) do + {_result, capture} = with_io(device_or_pid, input_or_options, fun) capture end @@ -262,7 +235,7 @@ defmodule ExUnit.CaptureIO do def with_io(device, input, fun) when is_atom(device) and is_binary(input) and is_function(fun, 0) do - with_io(device, [input: input], fun) + do_with_io(map_dev(device), [input: input], fun) end def with_io(device, options, fun) @@ -272,7 +245,7 @@ defmodule ExUnit.CaptureIO do def with_io(pid, input, fun) when is_pid(pid) and is_binary(input) and is_function(fun, 0) do - with_io(pid, [input: input], fun) + do_with_io(pid, [input: input], fun) end def with_io(pid, options, fun) @@ -280,19 +253,20 @@ defmodule ExUnit.CaptureIO do do_with_io(pid, options, fun) end - defp map_dev(:stdio), do: :standard_io + defp map_dev(:standard_io), do: self() + defp map_dev(:stdio), do: self() defp map_dev(:stderr), do: :standard_error defp map_dev(other), do: other - defp do_with_io(device_or_pid, options, fun) - when device_or_pid == :standard_io or is_pid(device_or_pid) do + defp do_with_io(pid, options, fun) when is_pid(pid) do prompt_config = Keyword.get(options, :capture_prompt, true) encoding = Keyword.get(options, :encoding, :unicode) input = Keyword.get(options, :input, "") - original_gl = Process.group_leader() + {:group_leader, original_gl} = + Process.info(pid, :group_leader) || {:group_leader, Process.group_leader()} + {:ok, capture_gl} = StringIO.open(input, capture_prompt: prompt_config, encoding: encoding) - pid = if is_pid(device_or_pid), do: device_or_pid, else: self() try do Process.group_leader(pid, capture_gl) @@ -302,7 +276,7 @@ defmodule ExUnit.CaptureIO do end end - defp do_with_io(device, options, fun) do + defp do_with_io(device, options, fun) when is_atom(device) do input = Keyword.get(options, :input, "") encoding = Keyword.get(options, :encoding, :unicode) diff --git a/lib/ex_unit/test/ex_unit/capture_io_test.exs b/lib/ex_unit/test/ex_unit/capture_io_test.exs index 1a2f22b846..a98d3bf32b 100644 --- a/lib/ex_unit/test/ex_unit/capture_io_test.exs +++ b/lib/ex_unit/test/ex_unit/capture_io_test.exs @@ -31,12 +31,15 @@ defmodule ExUnit.CaptureIOTest do defmodule MockProc do use GenServer - def start_link do - GenServer.start_link(__MODULE__, []) + def start_link(gl) do + GenServer.start_link(__MODULE__, gl) end @impl GenServer - def init(_), do: {:ok, nil} + def init(gl) do + Process.group_leader(self(), gl) + {:ok, nil} + end @impl GenServer def handle_call({:stdio, message}, _from, state) do @@ -502,7 +505,10 @@ defmodule ExUnit.CaptureIOTest do end test "capture_io with a separate process" do - {:ok, pid} = MockProc.start_link() + {:ok, gl} = StringIO.open("") + pid = start_supervised!({MockProc, gl}) + + assert Process.info(pid, :group_leader) == {:group_leader, gl} assert capture_io(pid, fn -> GenServer.call(pid, {:stdio, "a"}) @@ -524,7 +530,8 @@ defmodule ExUnit.CaptureIOTest do GenServer.call(pid, {:stderr, "uhoh"}) end) == "uhoh\n" - GenServer.stop(pid) + assert Process.info(pid, :group_leader) == {:group_leader, gl} + assert StringIO.contents(gl) == {"", ""} end test "with_io" do From fc0c487c5d5470891c7bb6529b969602f81c1220 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonatan=20K=C5=82osko?= Date: Thu, 29 Feb 2024 08:38:29 +0100 Subject: [PATCH 0425/1886] Support recompiling local Mix.install/2 dependencies (#13375) --- lib/iex/lib/iex/helpers.ex | 60 ++++++++++++++++++++++------------- lib/mix/lib/mix.ex | 65 +++++++++++++++++++++++++++++--------- lib/mix/test/mix_test.exs | 36 +++++++++++++++++++-- 3 files changed, 122 insertions(+), 39 deletions(-) diff --git a/lib/iex/lib/iex/helpers.ex b/lib/iex/lib/iex/helpers.ex index 212ca9cc20..6dd7720c9d 100644 --- a/lib/iex/lib/iex/helpers.ex +++ b/lib/iex/lib/iex/helpers.ex @@ -69,17 +69,22 @@ defmodule IEx.Helpers do import IEx, only: [dont_display_result: 0] @doc """ - Recompiles the current Mix project. + Recompiles the current Mix project or Mix install + dependencies. - This helper only works when IEx is started with a Mix - project, for example, `iex -S mix`. Note this function - simply recompiles Elixir modules, without reloading - configuration, recompiling dependencies, or restarting - applications. + This helper requires either `Mix.install/2` to have been + called within the current IEx session or for IEx to be + started alongside, for example, `iex -S mix`. - Therefore, any long running process may crash on recompilation, - as changed modules will be temporarily removed and recompiled, - without going through the proper code change callback. + In the `Mix.install/1` case, it will recompile any outdated + path dependency declared during install. Within a project, + it will recompile any outdated module. + + Note this function simply recompiles Elixir modules, without + reloading configuration or restarting applications. This means + any long running process may crash on recompilation, as changed + modules will be temporarily removed and recompiled, without + going through the proper code change callback. If you want to reload a single module, consider using `r(ModuleName)` instead. @@ -93,19 +98,30 @@ defmodule IEx.Helpers do """ def recompile(options \\ []) do - if mix_started?() do - project = Mix.Project.get() - - if is_nil(project) or - project.__info__(:compile)[:source] == String.to_charlist(Path.absname("mix.exs")) do - do_recompile(options) - else - message = "Cannot recompile because the current working directory changed" - IO.puts(IEx.color(:eval_error, message)) - end - else - IO.puts(IEx.color(:eval_error, "Mix is not running. Please start IEx with: iex -S mix")) - :error + cond do + not mix_started?() -> + IO.puts(IEx.color(:eval_error, "Mix is not running. Please start IEx with: iex -S mix")) + :error + + Mix.installed?() -> + Mix.in_install_project(fn -> + do_recompile(options) + # Just as with Mix.install/2 we clear all task invocations, + # so that we can recompile the dependencies again next time + Mix.Task.clear() + :ok + end) + + true -> + project = Mix.Project.get() + + if is_nil(project) or + project.__info__(:compile)[:source] == String.to_charlist(Path.absname("mix.exs")) do + do_recompile(options) + else + message = "Cannot recompile because the current working directory changed" + IO.puts(IEx.color(:eval_error, message)) + end end end diff --git a/lib/mix/lib/mix.ex b/lib/mix/lib/mix.ex index cdc16a071b..dfd7e244c3 100644 --- a/lib/mix/lib/mix.ex +++ b/lib/mix/lib/mix.ex @@ -855,23 +855,14 @@ defmodule Mix do File.rm_rf!(install_dir) end - config = [ - version: "0.1.0", - build_embedded: false, - build_per_environment: true, - build_path: "_build", - lockfile: "mix.lock", - deps_path: "deps", + dynamic_config = [ deps: deps, - app: :mix_install, - erlc_paths: [], - elixirc_paths: [], - compilers: [], consolidate_protocols: consolidate_protocols?, - config_path: config_path, - prune_code_paths: false + config_path: config_path ] + config = install_project_config(dynamic_config) + started_apps = Application.started_applications() :ok = Mix.ProjectStack.push(@mix_install_project, config, "nofile") build_dir = Path.join(install_dir, "_build") @@ -937,13 +928,18 @@ defmodule Mix do end end - Mix.State.put(:installed, id) + Mix.State.put(:installed, {id, dynamic_config}) :ok after Mix.ProjectStack.pop() + # Clear all tasks invoked during installation, since there + # is no reason to keep this in memory. Additionally this + # allows us to rerun tasks for the dependencies later on, + # such as recompilation + Mix.Task.clear() end - ^id when not force? -> + {^id, _dynamic_config} when not force? -> :ok _ -> @@ -978,6 +974,45 @@ defmodule Mix do Path.join([install_root, version, cache_id]) end + defp install_project_config(dynamic_config) do + [ + version: "0.1.0", + build_embedded: false, + build_per_environment: true, + build_path: "_build", + lockfile: "mix.lock", + deps_path: "deps", + app: :mix_install, + erlc_paths: [], + elixirc_paths: [], + compilers: [], + prune_code_paths: false + ] ++ dynamic_config + end + + @doc false + def in_install_project(fun) do + case Mix.State.get(:installed) do + {id, dynamic_config} -> + config = install_project_config(dynamic_config) + + install_dir = install_dir(id) + + File.cd!(install_dir, fn -> + :ok = Mix.ProjectStack.push(@mix_install_project, config, "nofile") + + try do + fun.() + after + Mix.ProjectStack.pop() + end + end) + + nil -> + Mix.raise("trying to call Mix.in_install_project/1, but Mix.install/2 was never called") + end + end + @doc """ Returns whether `Mix.install/2` was called in the current node. """ diff --git a/lib/mix/test/mix_test.exs b/lib/mix/test/mix_test.exs index e7b02da1e7..618d4cba67 100644 --- a/lib/mix/test/mix_test.exs +++ b/lib/mix/test/mix_test.exs @@ -39,7 +39,7 @@ defmodule MixTest do assert Protocol.consolidated?(InstallTest.Protocol) assert_received {:mix_shell, :info, ["==> install_test"]} - assert_received {:mix_shell, :info, ["Compiling 1 file (.ex)"]} + assert_received {:mix_shell, :info, ["Compiling 2 files (.ex)"]} assert_received {:mix_shell, :info, ["Generated install_test app"]} refute_received _ @@ -67,7 +67,7 @@ defmodule MixTest do assert File.dir?(Path.join(tmp_dir, "installs")) assert_received {:mix_shell, :info, ["==> install_test"]} - assert_received {:mix_shell, :info, ["Compiling 1 file (.ex)"]} + assert_received {:mix_shell, :info, ["Compiling 2 files (.ex)"]} assert_received {:mix_shell, :info, ["Generated install_test app"]} refute_received _ @@ -345,6 +345,36 @@ defmodule MixTest do assert Mix.installed?() end + test "in_install_project", %{tmp_dir: tmp_dir} do + Mix.install([ + {:install_test, path: Path.join(tmp_dir, "install_test")} + ]) + + Mix.in_install_project(fn -> + config = Mix.Project.config() + assert [{:install_test, [path: _]}] = config[:deps] + end) + end + + test "in_install_project recompile", %{tmp_dir: tmp_dir} do + Mix.install([ + {:install_test, path: Path.join(tmp_dir, "install_test")} + ]) + + File.write!("#{tmp_dir}/install_test/lib/install_test.ex", """ + defmodule InstallTest do + def hello do + :universe + end + end + """) + + Mix.in_install_project(fn -> + Mix.Task.run("compile") + assert apply(InstallTest, :hello, []) == :universe + end) + end + defp test_project(%{tmp_dir: tmp_dir}) do path = :code.get_path() @@ -384,7 +414,9 @@ defmodule MixTest do :world end end + """) + File.write!("#{tmp_dir}/install_test/lib/install_test_protocol.ex", """ defprotocol InstallTest.Protocol do def foo(x) end From b3710dea972ddcbd5d2aa2472adc806305260f1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 28 Feb 2024 19:38:10 +0100 Subject: [PATCH 0426/1886] Always start IEx shell via -user There is some uncertainty as to wether it will be possible to start an IEx shell dynamically from a -noshell environment. Therefore, this PR simplifies the shell booting to still rely on -user, as it did earlier than Erlang/OTP 26. --- bin/elixir | 3 +- bin/elixir.bat | 8 ++-- lib/elixir/lib/system.ex | 16 ------- lib/elixir/src/elixir.erl | 23 ++--------- lib/elixir/src/elixir_config.erl | 31 ++++---------- lib/elixir/src/elixir_errors.erl | 4 +- lib/elixir/src/iex.erl | 55 +++++++++++++++++++------ lib/iex/lib/iex.ex | 71 +++----------------------------- lib/iex/lib/iex/broker.ex | 41 ------------------ lib/iex/lib/iex/cli.ex | 6 +-- lib/iex/lib/iex/pry.ex | 13 +++--- lib/iex/lib/iex/server.ex | 51 +++++++++++++---------- 12 files changed, 108 insertions(+), 214 deletions(-) diff --git a/bin/elixir b/bin/elixir index a30bf064dc..27c478e1ef 100755 --- a/bin/elixir +++ b/bin/elixir @@ -218,6 +218,7 @@ SELF=$(readlink_f "$0") SCRIPT_PATH=$(dirname "$SELF") if [ "$OSTYPE" = "cygwin" ]; then SCRIPT_PATH=$(cygpath -m "$SCRIPT_PATH"); fi +if [ "$MODE" != "iex" ]; then ERL="-noshell -s elixir start_cli $ERL"; fi if [ "$OS" != "Windows_NT" ] && [ -z "$NO_COLOR" ]; then if test -t 1 -a -t 2; then ERL="-elixir ansi_enabled true $ERL"; fi @@ -228,7 +229,7 @@ fi ERTS_BIN= ERTS_BIN="$ERTS_BIN" -set -- "$ERTS_BIN$ERL_EXEC" -noshell -elixir_root "$SCRIPT_PATH"/../lib -pa "$SCRIPT_PATH"/../lib/elixir/ebin $ELIXIR_ERL_OPTIONS -s elixir start_$MODE $ERL "$@" +set -- "$ERTS_BIN$ERL_EXEC" -elixir_root "$SCRIPT_PATH"/../lib -pa "$SCRIPT_PATH"/../lib/elixir/ebin $ELIXIR_ERL_OPTIONS $ERL "$@" if [ -n "$RUN_ERL_PIPE" ]; then ESCAPED="" diff --git a/bin/elixir.bat b/bin/elixir.bat index 7974d006cc..edea2ff928 100644 --- a/bin/elixir.bat +++ b/bin/elixir.bat @@ -161,13 +161,11 @@ reg query HKCU\Console /v VirtualTerminalLevel 2>nul | findstr /e "0x1" >nul 2>n if %errorlevel% == 0 ( set beforeExtra=-elixir ansi_enabled true !beforeExtra! ) -if defined useIEx ( - set beforeExtra=-s elixir start_iex !beforeExtra! -) else ( - set beforeExtra=-s elixir start_cli !beforeExtra! +if not defined useIEx ( + set beforeExtra=-noshell -s elixir start_cli !beforeExtra! ) -set beforeExtra=-noshell -elixir_root "!SCRIPT_PATH!..\lib" -pa "!SCRIPT_PATH!..\lib\elixir\ebin" !beforeExtra! +set beforeExtra=-elixir_root "!SCRIPT_PATH!..\lib" -pa "!SCRIPT_PATH!..\lib\elixir\ebin" !beforeExtra! if defined ELIXIR_CLI_DRY_RUN ( if defined useWerl ( diff --git a/lib/elixir/lib/system.ex b/lib/elixir/lib/system.ex index dfc4556564..0457d9d730 100644 --- a/lib/elixir/lib/system.ex +++ b/lib/elixir/lib/system.ex @@ -305,22 +305,6 @@ defmodule System do :elixir_config.get(:no_halt) end - @doc """ - Waits until the system boots. - - Calling this function blocks until all of ARGV is processed. - Inside a release, this means the boot script and then ARGV - have been processed. This is only useful for those implementing - custom shells/consoles on top of Elixir. - - However, be careful to not invoke this command from within - the process that is processing the command line arguments, - as doing so would lead to a deadlock. - """ - @doc since: "1.15.0" - @spec wait_until_booted() :: :ok - defdelegate wait_until_booted(), to: :elixir_config - @doc """ Current working directory. diff --git a/lib/elixir/src/elixir.erl b/lib/elixir/src/elixir.erl index 3884a859da..ff73dcc2fa 100644 --- a/lib/elixir/src/elixir.erl +++ b/lib/elixir/src/elixir.erl @@ -2,7 +2,7 @@ %% private to the Elixir compiler and reserved to be used by Elixir only. -module(elixir). -behaviour(application). --export([start_cli/0, start/0, start_iex/0]). +-export([start_cli/0, start/0]). -export([start/2, stop/1, config_change/3]). -export([ string_to_tokens/5, tokens_to_quoted/3, 'string_to_quoted!'/5, @@ -176,16 +176,12 @@ check_file_encoding(Encoding) -> end. %% Boot and process given options. Invoked by Elixir's script. - -%% TODO: Delete prim_tty branches and -user on Erlang/OTP 26. -%% TODO: Remove IEx.CLI module -%% TODO: Replace "-user elixir" and "-s elixir start_$MODE" -%% by calls to "-s elixir cli" and "-e iex:cli()" +%% TODO: Delete prim_tty branches on Erlang/OTP 26. start() -> case code:ensure_loaded(prim_tty) of {module, _} -> - user_drv:start(#{initial_shell => noshell}); + user_drv:start(#{initial_shell => iex:shell()}); {error, _} -> case init:get_argument(elixir_root) of {ok, [[Root]]} -> code:add_patha(Root ++ "/iex/ebin"); @@ -207,18 +203,7 @@ start_cli() -> {error, _} -> ok end, - 'Elixir.Kernel.CLI':main(init:get_plain_arguments()), - elixir_config:booted(). - -start_iex() -> - case code:ensure_loaded(prim_tty) of - {module, _} -> - start_cli(), - iex:cli(); - - {error, _} -> - ok - end. + 'Elixir.Kernel.CLI':main(init:get_plain_arguments()). %% EVAL HOOKS diff --git a/lib/elixir/src/elixir_config.erl b/lib/elixir/src/elixir_config.erl index e5c84cc7ee..7178583078 100644 --- a/lib/elixir/src/elixir_config.erl +++ b/lib/elixir/src/elixir_config.erl @@ -1,6 +1,6 @@ -module(elixir_config). -compile({no_auto_import, [get/1]}). --export([new/1, warn/2, serial/1, booted/0, wait_until_booted/0]). +-export([new/1, warn/2, serial/1]). -export([static/1, is_bootstrap/0, identifier_tokenizer/0]). -export([delete/1, put/2, get/1, get/2, update/2, get_and_put/2]). -export([start_link/0, init/1, handle_call/3, handle_cast/2]). @@ -48,11 +48,6 @@ delete(?MODULE) -> %% MISC -booted() -> - gen_server:call(?MODULE, booted, infinity). -wait_until_booted() -> - gen_server:call(?MODULE, wait_until_booted, infinity). - serial(Fun) -> gen_server:call(?MODULE, {serial, Fun}, infinity). @@ -75,27 +70,19 @@ start_link() -> init(?MODULE) -> {ok, []}. -handle_call(booted, _From, Booted) when is_list(Booted) -> - [gen_server:reply(Caller, ok) || Caller <- Booted], - {reply, ok, done}; -handle_call(wait_until_booted, From, Booted) -> - if - is_list(Booted) -> {noreply, [From | Booted]}; - Booted =:= done -> {reply, ok, Booted} - end; -handle_call({serial, Fun}, _From, Booted) -> - {reply, Fun(), Booted}; -handle_call({put, Key, Value}, _From, Booted) -> +handle_call({serial, Fun}, _From, State) -> + {reply, Fun(), State}; + handle_call({put, Key, Value}, _From, State) -> ets:insert(?MODULE, {Key, Value}), - {reply, ok, Booted}; -handle_call({update, Key, Fun}, _From, Booted) -> + {reply, ok, State}; +handle_call({update, Key, Fun}, _From, State) -> Value = Fun(get(Key)), ets:insert(?MODULE, {Key, Value}), - {reply, Value, Booted}; -handle_call({get_and_put, Key, Value}, _From, Booted) -> + {reply, Value, State}; +handle_call({get_and_put, Key, Value}, _From, State) -> OldValue = get(Key), ets:insert(?MODULE, {Key, Value}), - {reply, OldValue, Booted}. + {reply, OldValue, State}. handle_cast(Cast, Tab) -> {stop, {bad_cast, Cast}, Tab}. diff --git a/lib/elixir/src/elixir_errors.erl b/lib/elixir/src/elixir_errors.erl index f1e2cb0c36..e925be8491 100644 --- a/lib/elixir/src/elixir_errors.erl +++ b/lib/elixir/src/elixir_errors.erl @@ -299,7 +299,9 @@ print_error(Meta, Env, Module, Desc) -> %% Compilation error. -spec compile_error(#{file := binary(), _ => _}) -> no_return(). -compile_error(#{module := Module, file := File}) when Module /= nil -> +%% We check for the lexical tracker because pry() inside a module +%% will have the environment but not a tracker. +compile_error(#{module := Module, file := File, lexical_tracker := LT}) when Module /= nil, LT /= nil -> Inspected = elixir_aliases:inspect(Module), Message = io_lib:format("cannot compile module ~ts (errors have been logged)", [Inspected]), compile_error([], File, Message); diff --git a/lib/elixir/src/iex.erl b/lib/elixir/src/iex.erl index add3fbded6..44983367e7 100644 --- a/lib/elixir/src/iex.erl +++ b/lib/elixir/src/iex.erl @@ -1,5 +1,5 @@ -module(iex). --export([cli/0]). +-export([start/0, start/2, shell/0, sync_remote/2]). %% Manual tests for changing the CLI boot. %% @@ -24,28 +24,57 @@ %% 4. Finally, in some other circumstances, printing messages may become %% borked. This can be verified with: %% -%% $ iex -e ":logger.info('foo~nbar', [])" +%% $ iex -e ":logger.info(~c'foo~nbar', [])" %% %% By the time those instructions have been written, all tests above pass. -cli() -> + +start() -> + start([], {elixir_utils, noop, []}). + +start(Opts, MFA) -> + {ok, _} = application:ensure_all_started(elixir), + {ok, _} = application:ensure_all_started(iex), + spawn(fun() -> - elixir_config:wait_until_booted(), - (shell:whereis() =:= undefined) andalso start_shell() + case init:notify_when_started(self()) of + started -> ok; + _ -> init:wait_until_started() + end, + + ok = io:setopts([{binary, true}, {encoding, unicode}]), + 'Elixir.IEx.Server':run_from_shell(Opts, MFA) end). -start_shell() -> +shell() -> Args = init:get_plain_arguments(), - Opts = [{remote, get_remsh(Args)}, {dot_iex_path, get_dot_iex(Args)}, {on_eof, halt}], - case 'Elixir.IEx':shell(Opts) of - {ok, _Shell} -> - ok; + case get_remsh(Args) of + nil -> + start_mfa(Args, {elixir, start_cli, []}); - {error, Reason} -> - io:format(standard_error, "Could not start IEx CLI due to reason: ~tp", [Reason]), - erlang:halt(1) + Remote -> + Ref = make_ref(), + + Parent = + spawn_link(fun() -> + receive + {'begin', Ref, Other} -> + elixir:start_cli(), + Other ! {done, Ref} + end + end), + + {remote, Remote, start_mfa(Args, {?MODULE, sync_remote, [Parent, Ref]})} end. +sync_remote(Parent, Ref) -> + Parent ! {'begin', Ref, self()}, + receive {done, Ref} -> ok end. + +start_mfa(Args, MFA) -> + Opts = [{dot_iex_path, get_dot_iex(Args)}, {on_eof, halt}], + {?MODULE, start, [Opts, MFA]}. + get_dot_iex(["--dot-iex", H | _]) -> elixir_utils:characters_to_binary(H); get_dot_iex([_ | T]) -> get_dot_iex(T); get_dot_iex([]) -> nil. diff --git a/lib/iex/lib/iex.ex b/lib/iex/lib/iex.ex index d5e2cea182..1bb0fc219b 100644 --- a/lib/iex/lib/iex.ex +++ b/lib/iex/lib/iex.ex @@ -226,7 +226,7 @@ defmodule IEx do alternate between them. Let's give it a try: User switch command - --> s 'Elixir.IEx' + --> s iex --> c The command above will start a new shell and connect to it. @@ -858,71 +858,10 @@ defmodule IEx do ## CLI - # This is a callback invoked by Erlang shell utilities - # when someone presses Ctrl+G and adds `s 'Elixir.IEx'`. + # TODO: Remove me on Elixir v1.20+ @doc false - def start(opts \\ [], mfa \\ {IEx, :dont_display_result, []}) do - # TODO: Expose this as iex:start() instead on Erlang/OTP 26+ - # TODO: Keep only this branch, delete optional args and mfa, - # and delete IEx.Server.run_from_shell/2 on Erlang/OTP 26+ - if Code.ensure_loaded?(:prim_tty) do - spawn(fn -> - {:ok, _} = Application.ensure_all_started(:iex) - :ok = :io.setopts(binary: true, encoding: :unicode) - _ = for fun <- Enum.reverse(after_spawn()), do: fun.() - IEx.Server.run([register: false] ++ opts) - end) - else - spawn(fn -> - case :init.notify_when_started(self()) do - :started -> :ok - _ -> :init.wait_until_started() - end - - {:ok, _} = Application.ensure_all_started(:iex) - :ok = :io.setopts(binary: true, encoding: :unicode) - _ = for fun <- Enum.reverse(after_spawn()), do: fun.() - IEx.Server.run_from_shell(opts, mfa) - end) - end - end - - # TODO: The idea is to expose this as a public API once we require - # Erlang/OTP 26 but it may not be possible. In such cases, we may - # want to move this to another module. - # See https://github.com/erlang/otp/issues/8113#issuecomment-1941613281 - @compile {:no_warn_undefined, {:shell, :start_interactive, 1}} - - @doc false - def shell(opts) do - {remote, opts} = Keyword.pop(opts, :remote) - ref = make_ref() - mfa = {__MODULE__, :__shell__, [self(), ref, opts]} - - shell = - if remote do - {:remote, to_charlist(remote), mfa} - else - mfa - end - - case :shell.start_interactive(shell) do - :ok -> - receive do - {^ref, shell} -> {:ok, shell} - after - 15_000 -> {:error, :timeout} - end - - {:error, reason} -> - {:error, reason} - end - end - - @doc false - def __shell__(parent, ref, opts) do - pid = start(opts) - send(parent, {ref, pid}) - pid + def start do + IO.warn("Use \"s iex\" instead") + :iex.start() end end diff --git a/lib/iex/lib/iex/broker.ex b/lib/iex/lib/iex/broker.ex index 4c7042b983..c329394571 100644 --- a/lib/iex/lib/iex/broker.ex +++ b/lib/iex/lib/iex/broker.ex @@ -75,24 +75,6 @@ defmodule IEx.Broker do GenServer.call(broker_pid, {:refuse, take_ref}) end - @doc """ - Asks to IO if we want to take over. - """ - def take_over?(location, whereami, opts) do - evaluator = opts[:evaluator] || self() - message = "Request to pry #{inspect(evaluator)} at #{location}#{whereami}" - interrupt = IEx.color(:eval_interrupt, "#{message}\nAllow? [Yn] ") - yes?(IO.gets(:stdio, interrupt)) - end - - defp yes?(string) when is_binary(string), - do: String.trim(string) in ["", "y", "Y", "yes", "YES", "Yes"] - - defp yes?(charlist) when is_list(charlist), - do: yes?(List.to_string(charlist)) - - defp yes?(_), do: false - @doc """ Client requests a takeover. """ @@ -100,29 +82,6 @@ defmodule IEx.Broker do {:ok, server :: pid, group_leader :: pid, counter :: integer} | {:error, :no_iex | :refused | atom()} def take_over(location, whereami, opts) do - case take_over_existing(location, whereami, opts) do - {:error, :no_iex} -> - cond do - # TODO: Remove this check on Erlang/OTP 26+ and {:error, :no_iex} return - not Code.ensure_loaded?(:prim_tty) -> - {:error, :no_iex} - - take_over?(location, whereami, opts) -> - case IEx.shell(opts) do - {:ok, shell} -> {:ok, shell, Process.group_leader(), 1} - {:error, reason} -> {:error, reason} - end - - true -> - {:error, :refused} - end - - other -> - other - end - end - - defp take_over_existing(location, whereami, opts) do case GenServer.whereis(@name) do nil -> {:error, :no_iex} _pid -> GenServer.call(@name, {:take_over, location, whereami, opts}, :infinity) diff --git a/lib/iex/lib/iex/cli.ex b/lib/iex/lib/iex/cli.ex index 1e73222bec..c9800d51c5 100644 --- a/lib/iex/lib/iex/cli.ex +++ b/lib/iex/lib/iex/cli.ex @@ -20,7 +20,7 @@ defmodule IEx.CLI do # IEx.Broker is capable of considering all groups under user_drv but # when we use :user.start(), we need to explicitly register it instead. # If we don't register, pry doesn't work. - IEx.start([register: true] ++ options(), {:elixir, :start_cli, []}) + :iex.start([register: true] ++ options(), {:elixir, :start_cli, []}) end end @@ -73,7 +73,7 @@ defmodule IEx.CLI do end defp local_start_mfa do - {IEx, :start, [options(), {:elixir, :start_cli, []}]} + {:iex, :start, [options(), {:elixir, :start_cli, []}]} end def remote_start(parent, ref) do @@ -94,7 +94,7 @@ defmodule IEx.CLI do end end) - {IEx, :start, [opts, {__MODULE__, :remote_start, [parent, ref]}]} + {:iex, :start, [opts, {__MODULE__, :remote_start, [parent, ref]}]} end defp options do diff --git a/lib/iex/lib/iex/pry.ex b/lib/iex/lib/iex/pry.ex index 8e9236c173..a8df74ba51 100644 --- a/lib/iex/lib/iex/pry.ex +++ b/lib/iex/lib/iex/pry.ex @@ -63,8 +63,9 @@ defmodule IEx.Pry do IEx.Evaluator.init(:no_ack, server, group_leader, start, opts) {:error, :no_iex} -> + # TODO: Remove this conditional on Erlang/OTP 26+ extra = - if match?({:win32, _}, :os.type()) do + if match?({:win32, _}, :os.type()) and :erlang.system_info(:otp_release) < ~c"26" do " If you are using Windows, you may need to start IEx with the --werl option." else "" @@ -551,12 +552,12 @@ defmodule IEx.Pry do {_, {min, max}} = Macro.prewalk(ast, {:infinity, line}, fn {_, meta, _} = ast, {min_line, max_line} when is_list(meta) -> - line = meta[:line] + case Keyword.fetch(meta, :line) do + {:ok, line} when line > 0 -> + {ast, {min(line, min_line), max(line, max_line)}} - if line > 0 do - {ast, {min(line, min_line), max(line, max_line)}} - else - {ast, {min_line, max_line}} + _ -> + {ast, {min_line, max_line}} end ast, acc -> diff --git a/lib/iex/lib/iex/server.ex b/lib/iex/lib/iex/server.ex index ac5e6cfa46..3bf745e8aa 100644 --- a/lib/iex/lib/iex/server.ex +++ b/lib/iex/lib/iex/server.ex @@ -61,7 +61,9 @@ defmodule IEx.Server do defp shell_loop(opts, pid, ref) do receive do {:take_over, take_pid, take_ref, take_location, take_whereami, take_opts} -> - if take_over?(take_pid, take_ref, take_location, take_whereami, take_opts, 1) do + IO.puts(IEx.color(:eval_interrupt, "Break reached: #{take_location}#{take_whereami}")) + + if take_over?(take_pid, take_ref, 1, true) do run_without_registration(init_state(opts), take_opts, nil) else shell_loop(opts, pid, ref) @@ -256,17 +258,6 @@ defmodule IEx.Server do wait_common(state, evaluator, evaluator_ref, input) end - defp handle_common( - {:DOWN, evaluator_ref, :process, evaluator, :normal}, - state, - evaluator, - evaluator_ref, - input, - _callback - ) do - rerun(state, [], evaluator, evaluator_ref, input) - end - defp handle_common( {:DOWN, evaluator_ref, :process, evaluator, reason}, state, @@ -275,14 +266,16 @@ defmodule IEx.Server do input, _callback ) do - try do - io_error( - "** (EXIT from #{inspect(evaluator)}) shell process exited with reason: " <> - Exception.format_exit(reason) - ) - catch - type, detail -> - io_error("** (IEx.Error) #{type} when printing EXIT message: #{inspect(detail)}") + if abnormal_reason?(reason) do + try do + io_error( + "** (EXIT from #{inspect(evaluator)}) shell process exited with reason: " <> + Exception.format_exit(reason) + ) + catch + type, detail -> + io_error("** (IEx.Error) #{type} when printing EXIT message: #{inspect(detail)}") + end end rerun(state, [], evaluator, evaluator_ref, input) @@ -292,8 +285,16 @@ defmodule IEx.Server do callback.(state) end + defp abnormal_reason?(:normal), do: false + defp abnormal_reason?(:shutdown), do: false + defp abnormal_reason?({:shutdown, _}), do: false + defp abnormal_reason?(_), do: true + defp take_over?(take_pid, take_ref, take_location, take_whereami, take_opts, counter) do - answer = IEx.Broker.take_over?(take_location, take_whereami, take_opts) + evaluator = take_opts[:evaluator] || self() + message = "Request to pry #{inspect(evaluator)} at #{take_location}#{take_whereami}" + interrupt = IEx.color(:eval_interrupt, "#{message}\nAllow? [Yn] ") + answer = yes?(IO.gets(:stdio, interrupt)) take_over?(take_pid, take_ref, counter, answer) end @@ -311,6 +312,14 @@ defmodule IEx.Server do end end + defp yes?(string) when is_binary(string), + do: String.trim(string) in ["", "y", "Y", "yes", "YES", "Yes"] + + defp yes?(charlist) when is_list(charlist), + do: yes?(List.to_string(charlist)) + + defp yes?(_), do: false + ## State defp init_state(opts) do From dd13c508e8cbf4650d3fe5f142980933d5ba8351 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 29 Feb 2024 14:48:20 +0100 Subject: [PATCH 0427/1886] Add mix test --breakpoints --- lib/elixir/pages/getting-started/debugging.md | 36 +++-- lib/ex_unit/lib/ex_unit/case.ex | 46 ++++-- lib/iex/lib/iex/pry.ex | 144 +++++++++++------- lib/iex/test/iex/pry_test.exs | 70 +++++++++ lib/mix/lib/mix/tasks/test.ex | 35 +++-- 5 files changed, 241 insertions(+), 90 deletions(-) diff --git a/lib/elixir/pages/getting-started/debugging.md b/lib/elixir/pages/getting-started/debugging.md index 28004643d8..0be3fb8b4e 100644 --- a/lib/elixir/pages/getting-started/debugging.md +++ b/lib/elixir/pages/getting-started/debugging.md @@ -79,7 +79,7 @@ feature #=> %{inspiration: "Rust", name: :dbg} Map.put(feature, :in_version, "1.14.0") #=> %{in_version: "1.14.0", inspiration: "Rust", name: :dbg} ``` -When talking about `IO.inspect/2`, we mentioned its usefulness when placed between steps of `|>` pipelines. `dbg` does it better: it understands Elixir code, so it will print values at *every step of the pipeline*. +When talking about `IO.inspect/2`, we mentioned its usefulness when placed between steps of `|>` pipelines. `dbg` does it better: it understands Elixir code, so it will print values at _every step of the pipeline_. ```elixir # In dbg_pipes.exs @@ -102,7 +102,7 @@ __ENV__.file #=> "/home/myuser/dbg_pipes.exs" While `dbg` provides conveniences around Elixir constructs, you will need `IEx` if you want to execute code and set breakpoints while debugging. -## Breakpoints +## Pry When using `IEx`, you may pass `--dbg pry` as an option to "stop" the code execution where the `dbg` call is: @@ -116,22 +116,30 @@ Or to debug inside a of a project: $ iex --dbg pry -S mix ``` -Or during tests (the `--trace` flag on `mix test` prevents tests from timing out): - -```console -$ iex --dbg pry -S mix test --trace -$ iex --dbg pry -S mix test path/to/file:line --trace -``` +Now any call to `dbg` will ask if you want to pry the existing code. If you accept, you'll be able to access all variables, as well as imports and aliases from the code, directly from IEx. This is called "prying". While the pry session is running, the code execution stops, until `continue` (or `c`) or `next` (or `n`) are called. Remember you can always run `iex` in the context of a project with `iex -S mix TASK`. -Now a call to `dbg` will ask if you want to pry the existing code. If you accept, you'll be able to access all variables, as well as imports and aliases from the code, directly from IEx. This is called "prying". While the pry session is running, the code execution stops, until `continue` or `next` are called. Remember you can always run `iex` in the context of a project with `iex -S mix TASK`. + - +## Breakpoints `dbg` calls require us to change the code we intend to debug and has limited stepping functionality. Luckily IEx also provides a `IEx.break!/2` function which allows you to set and manage breakpoints on any Elixir code without modifying its source: -Similar to `dbg`, once a breakpoint is reached code execution stops until `continue` or `next` are invoked. However, `break!/2` does not have access to aliases and imports from the debugged code as it works on the compiled artifact rather than on source code. +Similar to `dbg`, once a breakpoint is reached, code execution stops until `continue` (or `c`) or `next` (or `n`) are invoked. Breakpoints can navigate line-by-line by default, however, they do not have access to aliases and imports when breakpoints are set on compiled modules. + +The `mix test` task direct integration with breakpoints via the `-b`/`--breakpoints` flag. When the flag is used, a breakpoint is set at the beginning of every test that will run: + + + +Here are some commands you can use in practice: + +```console +# Debug all failed tests +$ iex -S mix test --breakpoints --failed +# Debug the test at the given file:line +$ iex -S mix test -b path/to/file:line +``` ## Observer @@ -147,15 +155,13 @@ iex> :observer.start() > When running `iex` inside a project with `iex -S mix`, `observer` won't be available as a dependency. To do so, you will need to call the following functions before: > > ```elixir -> iex> Mix.ensure_application!(:wx) -> iex> Mix.ensure_application!(:runtime_tools) +> iex> Mix.ensure_application!(:wx) # Not necessary on Erlang/OTP 27+ +> iex> Mix.ensure_application!(:runtime_tools) # Not necessary on Erlang/OTP 27+ > iex> Mix.ensure_application!(:observer) > iex> :observer.start() > ``` > > If any of the calls above fail, here is what may have happened: some package managers default to installing a minimized Erlang without WX bindings for GUI support. In some package managers, you may be able to replace the headless Erlang with a more complete package (look for packages named `erlang` vs `erlang-nox` on Debian/Ubuntu/Arch). In others managers, you may need to install a separate `erlang-wx` (or similarly named) package. -> -> There are conversations to improve this experience in future releases. The above will open another Graphical User Interface that provides many panes to fully understand and navigate the runtime and your project. diff --git a/lib/ex_unit/lib/ex_unit/case.ex b/lib/ex_unit/lib/ex_unit/case.ex index 05976748de..abce477e8e 100644 --- a/lib/ex_unit/lib/ex_unit/case.ex +++ b/lib/ex_unit/lib/ex_unit/case.ex @@ -366,34 +366,52 @@ defmodule ExUnit.Case do end contents = - case contents do + case annotate_test(contents, __CALLER__) do [do: block] -> quote do unquote(block) :ok end - _ -> + contents -> quote do try(unquote(contents)) :ok end end - var = Macro.escape(var) - contents = Macro.escape(contents, unquote: true) %{module: mod, file: file, line: line} = __CALLER__ - quote bind_quoted: [ - var: var, - contents: contents, - message: message, - mod: mod, - file: file, - line: line - ] do - name = ExUnit.Case.register_test(mod, file, line, :test, message, []) - def unquote(name)(unquote(var)), do: unquote(contents) + name = + quote do + name = + ExUnit.Case.register_test( + unquote(mod), + unquote(file), + unquote(line), + :test, + unquote(message), + [] + ) + end + + def = + {:def, [], + [ + {{:unquote, [], [quote(do: name)]}, [], [var]}, + [do: contents] + ]} + + {:__block__, [], [name, def]} + end + + defp annotate_test(contents, caller) do + if Application.get_env(:ex_unit, :breakpoints, false) and Keyword.keyword?(contents) do + for {key, expr} <- contents do + {key, IEx.Pry.annotate_quoted(expr, true, caller)} + end + else + contents end end diff --git a/lib/iex/lib/iex/pry.ex b/lib/iex/lib/iex/pry.ex index a8df74ba51..373f240f78 100644 --- a/lib/iex/lib/iex/pry.ex +++ b/lib/iex/lib/iex/pry.ex @@ -86,7 +86,7 @@ defmodule IEx.Pry do end @doc false - def pry_with_next(next?, binding, opts_or_env) when is_boolean(next?) do + def __next__(next?, binding, opts_or_env) when is_boolean(next?) do next? and pry(binding, opts_or_env) == {:ok, true} end @@ -108,6 +108,63 @@ defmodule IEx.Pry do [] end + @doc """ + Annotate quoted expression with line-by-line `IEx.Pry` debugging steps. + + It expected the `quoted` expression to annotate, a `condition` that controls + if pry should run or not (usually is simply the boolean `true`), and the + caller macro environment. + """ + @doc since: "1.17.0" + @spec annotate_quoted(Macro.t(), Macro.t(), Macro.Env.t()) :: Macro.t() + def annotate_quoted(quoted, condition, caller) do + prelude = + quote do + [ + env = unquote(Macro.escape(Macro.Env.prune_compile_info(caller))), + next? = unquote(condition) + ] + end + + next_pry = + fn line, _version, _binding -> + quote do + next? = IEx.Pry.__next__(next?, binding(), %{env | line: unquote(line)}) + end + end + + annotate_quoted(quoted, prelude, caller.line, 0, :ok, fn _, _ -> :ok end, next_pry) + end + + defp annotate_quoted(maybe_block, prelude, line, version, binding, next_binding, next_pry) + when is_list(prelude) do + exprs = + maybe_block + |> unwrap_block() + |> annotate_quoted(true, line, version, binding, {next_binding, next_pry}) + + {:__block__, [], prelude ++ exprs} + end + + defp annotate_quoted([expr | exprs], force?, line, version, binding, funs) do + {next_binding, next_pry} = funs + new_binding = next_binding.(expr, binding) + {min_line, max_line} = line_range(expr, line) + + if force? or min_line > line do + [ + next_pry.(min_line, version, binding), + expr | annotate_quoted(exprs, false, max_line, version + 1, new_binding, funs) + ] + else + [expr | annotate_quoted(exprs, false, max_line, version, new_binding, funs)] + end + end + + defp annotate_quoted([], _force?, _line, _version, _binding, _funs) do + [] + end + @doc """ Formats the location for `whereami/3` prying. @@ -473,7 +530,6 @@ defmodule IEx.Pry do defp instrument_clause({meta, args, guards, clause}, ref, case_pattern, opts) do arity = length(args) - exprs = unwrap_block(clause) # Have an extra binding per argument for case matching. case_vars = @@ -487,20 +543,23 @@ defmodule IEx.Pry do # Generate the take_over condition with the ETS lookup. # Remember this is expanded AST, so no aliases allowed, # no locals (such as the unary -) and so on. - initialize_next = + prelude = quote do - unquote(next_var(arity + 1)) = - case unquote(case_head) do - unquote(case_pattern) -> - :erlang."/="( - # :ets.update_counter(table, key, {pos, inc, threshold, reset}) - :ets.update_counter(unquote(@table), unquote(ref), unquote(update_op)), - unquote(-1) - ) - - _ -> - false - end + [ + unquote(next_var(arity + 1)) = unquote(opts), + unquote(next_var(arity + 2)) = + case unquote(case_head) do + unquote(case_pattern) -> + :erlang."/="( + # :ets.update_counter(table, key, {pos, inc, threshold, reset}) + :ets.update_counter(unquote(@table), unquote(ref), unquote(update_op)), + unquote(-1) + ) + + _ -> + false + end + ] end args = @@ -508,42 +567,25 @@ defmodule IEx.Pry do |> Enum.zip(args) |> Enum.map(fn {var, arg} -> {:=, [], [arg, var]} end) - # The variable we pass around will start after the arity, - # as we use the arity to instrument the clause. + version = arity + 2 binding = match_binding(args, %{}) line = Keyword.get(meta, :line, 1) - exprs = instrument_body(exprs, true, line, arity + 1, binding, opts) - - {meta, args, guards, {:__block__, meta, [initialize_next | exprs]}} - end - - defp instrument_body([expr | exprs], force?, line, version, binding, opts) do - next_binding = binding(expr, binding) - {min_line, max_line} = line_range(expr, line) - - if force? or min_line > line do - pry_var = next_var(version) - pry_binding = Map.to_list(binding) - pry_opts = [line: min_line] ++ opts - - pry = - quote do - unquote(next_var(version + 1)) = - :"Elixir.IEx.Pry".pry_with_next( - unquote(pry_var), - unquote(pry_binding), - unquote(pry_opts) - ) - end - - [pry, expr | instrument_body(exprs, false, max_line, version + 1, next_binding, opts)] - else - [expr | instrument_body(exprs, false, max_line, version, next_binding, opts)] - end - end + env_var = next_var(arity + 1) + + clause = + annotate_quoted(clause, prelude, line, version, binding, &next_binding/2, fn + line, version, binding -> + quote do + unquote(next_var(version + 1)) = + :"Elixir.IEx.Pry".__next__( + unquote(next_var(version)), + unquote(Map.to_list(binding)), + [{:line, unquote(line)} | unquote(env_var)] + ) + end + end) - defp instrument_body([], _force?, _line, _version, _binding, _opts) do - [] + {meta, args, guards, clause} end defp line_range(ast, line) do @@ -567,7 +609,7 @@ defmodule IEx.Pry do if min == :infinity, do: {line, max}, else: {min, max} end - defp binding(ast, binding) do + defp next_binding(ast, binding) do {_, binding} = Macro.prewalk(ast, binding, fn {:=, _, [left, _right]}, acc -> @@ -637,7 +679,7 @@ defmodule IEx.Pry do env = unquote(env_with_line_from_asts(first_ast_chunk)) - next? = IEx.Pry.pry_with_next(true, binding(), env) + next? = IEx.Pry.__next__(true, binding(), env) value = unquote(pipe_chunk_of_asts(first_ast_chunk)) IEx.Pry.__dbg_pipe_step__( @@ -656,7 +698,7 @@ defmodule IEx.Pry do quote do unquote(ast_acc) env = unquote(env_with_line_from_asts(asts_chunk)) - next? = IEx.Pry.pry_with_next(next?, binding(), env) + next? = IEx.Pry.__next__(next?, binding(), env) value = unquote(piped_asts) IEx.Pry.__dbg_pipe_step__( diff --git a/lib/iex/test/iex/pry_test.exs b/lib/iex/test/iex/pry_test.exs index dc1862e98f..3558f4c899 100644 --- a/lib/iex/test/iex/pry_test.exs +++ b/lib/iex/test/iex/pry_test.exs @@ -185,6 +185,76 @@ defmodule IEx.PryTest do end end + describe "annotate_quoted" do + def annotate_quoted(block, condition \\ true) do + {:__block__, meta, [{:=, _, [{:env, _, _}, _]} | tail]} = + block + |> Code.string_to_quoted!() + |> IEx.Pry.annotate_quoted(condition, %{__ENV__ | line: 1}) + + Macro.to_string({:__block__, meta, tail}) + end + + test "one expresion, one line" do + assert annotate_quoted(""" + x = 123 + y = 456 + x + y + """) == """ + next? = true + next? = IEx.Pry.__next__(next?, binding(), %{env | line: 1}) + x = 123 + next? = IEx.Pry.__next__(next?, binding(), %{env | line: 2}) + y = 456 + next? = IEx.Pry.__next__(next?, binding(), %{env | line: 3}) + x + y\ + """ + end + + test "one expression, multiple lines" do + assert annotate_quoted(""" + (x = 123) && + (y = 456) + x + y + """) == """ + next? = true + next? = IEx.Pry.__next__(next?, binding(), %{env | line: 1}) + (x = 123) && (y = 456) + next? = IEx.Pry.__next__(next?, binding(), %{env | line: 3}) + x + y\ + """ + end + + test "one line, multiple expressions" do + assert annotate_quoted(""" + x = 123; y = 456 + x + y + """) == """ + next? = true + next? = IEx.Pry.__next__(next?, binding(), %{env | line: 1}) + x = 123 + y = 456 + next? = IEx.Pry.__next__(next?, binding(), %{env | line: 2}) + x + y\ + """ + end + + test "multiple line, multiple expressions" do + assert annotate_quoted(""" + x = 123; y = + 456 + x + y + """) == """ + next? = true + next? = IEx.Pry.__next__(next?, binding(), %{env | line: 1}) + x = 123 + y = 456 + next? = IEx.Pry.__next__(next?, binding(), %{env | line: 3}) + x + y\ + """ + end + end + defp instrumented?(module) do module.module_info(:attributes)[:iex_pry] == [true] end diff --git a/lib/mix/lib/mix/tasks/test.ex b/lib/mix/lib/mix/tasks/test.ex index 2074388a36..d7d916abc5 100644 --- a/lib/mix/lib/mix/tasks/test.ex +++ b/lib/mix/lib/mix/tasks/test.ex @@ -109,6 +109,11 @@ defmodule Mix.Tasks.Test do * `--all-warnings` (`--no-all-warnings`) - prints all warnings, including previous compilations (default is true except on errors) + * `-b`, `--breakpoints` - (since v1.17.0) sets a breakpoint at the beginning + of every test. The debugger goes line-by-line and can access all variables + and imports (but not local functions). You can press `n` for the next line + and `c` for the next test. This automatically sets `--trace` + * `--color` - enables color in the output * `--cover` - runs coverage tool. See "Coverage" section below @@ -411,6 +416,7 @@ defmodule Mix.Tasks.Test do @switches [ all_warnings: :boolean, + breakpoints: :boolean, force: :boolean, color: :boolean, cover: :boolean, @@ -445,7 +451,7 @@ defmodule Mix.Tasks.Test do @impl true def run(args) do - {opts, files} = OptionParser.parse!(args, strict: @switches) + {opts, files} = OptionParser.parse!(args, strict: @switches, aliases: [b: :breakpoints]) if not Mix.Task.recursing?() do do_run(opts, args, files) @@ -545,6 +551,20 @@ defmodule Mix.Tasks.Test do # The test helper may change the Mix.shell(), so revert it whenever we raise and after suite shell = Mix.shell() + test_elixirc_options = project[:test_elixirc_options] || [] + + {test_elixirc_options, opts} = + cond do + not Keyword.get(opts, :breakpoints, false) -> + {test_elixirc_options, opts} + + IEx.started?() -> + {Keyword.put(test_elixirc_options, :debug_info, true), opts} + + true -> + Mix.shell().error("you must run \"iex -S mix test\" when using -b/--breakpoints") + {test_elixirc_options, Keyword.delete(opts, :breakpoints)} + end # Configure ExUnit now and then again so the task options override test_helper.exs {ex_unit_opts, allowed_files} = process_ex_unit_opts(opts) @@ -555,7 +575,6 @@ defmodule Mix.Tasks.Test do ExUnit.configure(merge_helper_opts(ex_unit_opts)) # Finally parse, require and load the files - test_elixirc_options = project[:test_elixirc_options] || [] test_files = if files != [], do: parse_file_paths(files), else: test_paths test_pattern = project[:test_pattern] || "*_test.exs" warn_test_pattern = project[:warn_test_pattern] || "*_test.ex" @@ -643,6 +662,7 @@ defmodule Mix.Tasks.Test do end @option_keys [ + :breakpoints, :trace, :max_cases, :max_failures, @@ -679,17 +699,12 @@ defmodule Mix.Tasks.Test do defp merge_helper_opts(opts) do # The only options that are additive from app env are the excludes - merge_opts(opts, :exclude) - end - - defp merge_opts(opts, key) do - value = List.wrap(Application.get_env(:ex_unit, key, [])) - Keyword.update(opts, key, value, &Enum.uniq(&1 ++ value)) + value = List.wrap(Application.get_env(:ex_unit, :exclude, [])) + Keyword.update(opts, :exclude, value, &Enum.uniq(&1 ++ value)) end defp default_opts(opts) do - # Set autorun to false because Mix - # automatically runs the test suite for us. + # Set autorun to false because Mix automatically runs the test suite for us. [autorun: false] ++ opts end From 0a0a96ece290feba778d70cc8b8155d4ce3a3ef2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 29 Feb 2024 14:57:31 +0100 Subject: [PATCH 0428/1886] Skip undefined warnings during bootstrap --- lib/ex_unit/lib/ex_unit/case.ex | 2 +- lib/mix/lib/mix/tasks/test.ex | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/ex_unit/lib/ex_unit/case.ex b/lib/ex_unit/lib/ex_unit/case.ex index abce477e8e..520a8e9e75 100644 --- a/lib/ex_unit/lib/ex_unit/case.ex +++ b/lib/ex_unit/lib/ex_unit/case.ex @@ -276,7 +276,7 @@ defmodule ExUnit.Case do """ @type env :: module() | Macro.Env.t() - + @compile {:no_warn_undefined, [IEx.Pry]} @reserved [:module, :file, :line, :test, :async, :registered, :describe] @doc false diff --git a/lib/mix/lib/mix/tasks/test.ex b/lib/mix/lib/mix/tasks/test.ex index d7d916abc5..a6b93ad685 100644 --- a/lib/mix/lib/mix/tasks/test.ex +++ b/lib/mix/lib/mix/tasks/test.ex @@ -3,7 +3,7 @@ defmodule Mix.Tasks.Test do alias Mix.Compilers.Test, as: CT - @compile {:no_warn_undefined, [ExUnit, ExUnit.Filters]} + @compile {:no_warn_undefined, [IEx, ExUnit, ExUnit.Filters]} @shortdoc "Runs a project's tests" @recursive true From 56768edb56c35955945243a57358bc53f7385cce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 29 Feb 2024 15:42:09 +0100 Subject: [PATCH 0429/1886] Always pass -noshell when starting iex, fix Erlang/OTP 25 --- bin/elixir | 4 ++-- bin/elixir.bat | 4 ++-- lib/iex/lib/iex/cli.ex | 15 +++++++++++---- 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/bin/elixir b/bin/elixir index 27c478e1ef..1a4eb0961e 100755 --- a/bin/elixir +++ b/bin/elixir @@ -218,7 +218,7 @@ SELF=$(readlink_f "$0") SCRIPT_PATH=$(dirname "$SELF") if [ "$OSTYPE" = "cygwin" ]; then SCRIPT_PATH=$(cygpath -m "$SCRIPT_PATH"); fi -if [ "$MODE" != "iex" ]; then ERL="-noshell -s elixir start_cli $ERL"; fi +if [ "$MODE" != "iex" ]; then ERL="-s elixir start_cli $ERL"; fi if [ "$OS" != "Windows_NT" ] && [ -z "$NO_COLOR" ]; then if test -t 1 -a -t 2; then ERL="-elixir ansi_enabled true $ERL"; fi @@ -229,7 +229,7 @@ fi ERTS_BIN= ERTS_BIN="$ERTS_BIN" -set -- "$ERTS_BIN$ERL_EXEC" -elixir_root "$SCRIPT_PATH"/../lib -pa "$SCRIPT_PATH"/../lib/elixir/ebin $ELIXIR_ERL_OPTIONS $ERL "$@" +set -- "$ERTS_BIN$ERL_EXEC" -noshell -elixir_root "$SCRIPT_PATH"/../lib -pa "$SCRIPT_PATH"/../lib/elixir/ebin $ELIXIR_ERL_OPTIONS $ERL "$@" if [ -n "$RUN_ERL_PIPE" ]; then ESCAPED="" diff --git a/bin/elixir.bat b/bin/elixir.bat index edea2ff928..905a402c19 100644 --- a/bin/elixir.bat +++ b/bin/elixir.bat @@ -162,10 +162,10 @@ if %errorlevel% == 0 ( set beforeExtra=-elixir ansi_enabled true !beforeExtra! ) if not defined useIEx ( - set beforeExtra=-noshell -s elixir start_cli !beforeExtra! + set beforeExtra=-s elixir start_cli !beforeExtra! ) -set beforeExtra=-elixir_root "!SCRIPT_PATH!..\lib" -pa "!SCRIPT_PATH!..\lib\elixir\ebin" !beforeExtra! +set beforeExtra=-noshell -elixir_root "!SCRIPT_PATH!..\lib" -pa "!SCRIPT_PATH!..\lib\elixir\ebin" !beforeExtra! if defined ELIXIR_CLI_DRY_RUN ( if defined useWerl ( diff --git a/lib/iex/lib/iex/cli.ex b/lib/iex/lib/iex/cli.ex index c9800d51c5..1a7b3989cb 100644 --- a/lib/iex/lib/iex/cli.ex +++ b/lib/iex/lib/iex/cli.ex @@ -17,10 +17,17 @@ defmodule IEx.CLI do :user.start() - # IEx.Broker is capable of considering all groups under user_drv but - # when we use :user.start(), we need to explicitly register it instead. - # If we don't register, pry doesn't work. - :iex.start([register: true] ++ options(), {:elixir, :start_cli, []}) + spawn(fn -> + :application.ensure_all_started(:iex) + + case :init.notify_when_started(self()) do + :started -> :ok + _ -> :init.wait_until_started() + end + + :ok = :io.setopts(binary: true, encoding: :unicode) + IEx.Server.run_from_shell([register: true] ++ options(), {:elixir, :start_cli, []}) + end) end end From 729115420ae094620a23230cbac254ad93eb4c2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 29 Feb 2024 17:05:22 +0100 Subject: [PATCH 0430/1886] Skip IEx eval smoke test on Windows and Erlang/OTP 26 (#13377) --- lib/elixir/test/elixir/kernel/cli_test.exs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/lib/elixir/test/elixir/kernel/cli_test.exs b/lib/elixir/test/elixir/kernel/cli_test.exs index 5d2b4cc2fd..fc61e01953 100644 --- a/lib/elixir/test/elixir/kernel/cli_test.exs +++ b/lib/elixir/test/elixir/kernel/cli_test.exs @@ -65,16 +65,19 @@ defmodule Kernel.CLITest do {output, 0} = System.cmd(elixir_executable(), ["--eval", "IO.puts :hello_world123"]) assert output =~ "hello_world123" - {output, 0} = - System.cmd(iex_executable(), ["--eval", "IO.puts :hello_world123; System.halt()"]) - - assert output =~ "hello_world123" - {output, 0} = System.cmd(elixir_executable(), ["-e", "IO.puts :hello_world123"]) assert output =~ "hello_world123" - {output, 0} = System.cmd(iex_executable(), ["-e", "IO.puts :hello_world123; System.halt()"]) - assert output =~ "hello_world123" + # TODO: remove this once we bump CI to 26.3 + unless windows?() and :erlang.system_info(:otp_release) == ~c"26" do + {output, 0} = + System.cmd(iex_executable(), ["--eval", "IO.puts :hello_world123; System.halt()"]) + + assert output =~ "hello_world123" + + {output, 0} = System.cmd(iex_executable(), ["-e", "IO.puts :hello_world123; System.halt()"]) + assert output =~ "hello_world123" + end end test "--version smoke test" do From 0bb305a92e4460a2db1f9efe341aaf70187fee17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonatan=20K=C5=82osko?= Date: Thu, 29 Feb 2024 19:19:12 +0100 Subject: [PATCH 0431/1886] Add environment variable for reusing Mix.install/2 installation (#13378) --- lib/iex/lib/iex/helpers.ex | 8 ++ lib/mix/lib/mix.ex | 74 ++++++++++++++++--- lib/mix/test/mix_test.exs | 146 +++++++++++++++++++++++++++++-------- 3 files changed, 185 insertions(+), 43 deletions(-) diff --git a/lib/iex/lib/iex/helpers.ex b/lib/iex/lib/iex/helpers.ex index 6dd7720c9d..2d922e0321 100644 --- a/lib/iex/lib/iex/helpers.ex +++ b/lib/iex/lib/iex/helpers.ex @@ -105,6 +105,14 @@ defmodule IEx.Helpers do Mix.installed?() -> Mix.in_install_project(fn -> + # TODO: remove this once Mix requires Hex with the fix from + # https://github.com/hexpm/hex/pull/1015 + # Context: Mix.install/1 starts :hex if necessary and stops + # it afterwards. Calling compile here may require hex to be + # started and that should happen automatically, but because + # of a bug it is not (fixed in the linked PR). + _ = Application.ensure_all_started(:hex) + do_recompile(options) # Just as with Mix.install/2 we clear all task invocations, # so that we can recompile the dependencies again next time diff --git a/lib/mix/lib/mix.ex b/lib/mix/lib/mix.ex index dfd7e244c3..ff2563334f 100644 --- a/lib/mix/lib/mix.ex +++ b/lib/mix/lib/mix.ex @@ -661,6 +661,13 @@ defmodule Mix do This function can only be called outside of a Mix project and only with the same dependencies in the given VM. + The `MIX_INSTALL_RESTORE_PROJECT_DIR` environment variable may be specified. + It should point to a previous installation directory, which can be obtained + with `Mix.install_project_dir/0` (after calling `Mix.install/2`). Using a + restore dir may speed up the installation, since matching dependencies do + not need be refetched nor recompiled. This environment variable is ignored + if `:force` is enabled. + ## Options * `:force` - if `true`, runs with empty install cache. This is useful when you want @@ -845,14 +852,14 @@ defmodule Mix do Application.put_all_env(config, persistent: true) System.put_env(system_env) - install_dir = install_dir(id) + install_project_dir = install_project_dir(id) if Keyword.fetch!(opts, :verbose) do - Mix.shell().info("Mix.install/2 using #{install_dir}") + Mix.shell().info("Mix.install/2 using #{install_project_dir}") end if force? do - File.rm_rf!(install_dir) + File.rm_rf!(install_project_dir) end dynamic_config = [ @@ -865,21 +872,28 @@ defmodule Mix do started_apps = Application.started_applications() :ok = Mix.ProjectStack.push(@mix_install_project, config, "nofile") - build_dir = Path.join(install_dir, "_build") + build_dir = Path.join(install_project_dir, "_build") external_lockfile = expand_path(opts[:lockfile], deps, :lockfile, "mix.lock") try do first_build? = not File.dir?(build_dir) - File.mkdir_p!(install_dir) - File.cd!(install_dir, fn -> + restore_dir = System.get_env("MIX_INSTALL_RESTORE_PROJECT_DIR") + + if first_build? and restore_dir != nil and not force? do + File.cp_r(restore_dir, install_project_dir) + end + + File.mkdir_p!(install_project_dir) + + File.cd!(install_project_dir, fn -> if config_path do Mix.Task.rerun("loadconfig") end cond do external_lockfile -> - md5_path = Path.join(install_dir, "merge.lock.md5") + md5_path = Path.join(install_project_dir, "merge.lock.md5") old_md5 = case File.read(md5_path) do @@ -890,7 +904,7 @@ defmodule Mix do new_md5 = external_lockfile |> File.read!() |> :erlang.md5() if old_md5 != new_md5 do - lockfile = Path.join(install_dir, "mix.lock") + lockfile = Path.join(install_project_dir, "mix.lock") old_lock = Mix.Dep.Lock.read(lockfile) new_lock = Mix.Dep.Lock.read(external_lockfile) Mix.Dep.Lock.write(Map.merge(old_lock, new_lock), file: lockfile) @@ -928,6 +942,10 @@ defmodule Mix do end end + if restore_dir do + remove_leftover_deps(install_project_dir) + end + Mix.State.put(:installed, {id, dynamic_config}) :ok after @@ -965,7 +983,29 @@ defmodule Mix do Path.join(app_dir, relative_path) end - defp install_dir(cache_id) do + defp remove_leftover_deps(install_project_dir) do + build_lib_dir = Path.join([install_project_dir, "_build", "dev", "lib"]) + deps_dir = Path.join(install_project_dir, "deps") + + deps = File.ls!(build_lib_dir) + + loaded_deps = + for {app, _description, _version} <- Application.loaded_applications(), + into: MapSet.new(), + do: Atom.to_string(app) + + # We want to keep :mix_install, but it has no application + loaded_deps = MapSet.put(loaded_deps, "mix_install") + + for dep <- deps, not MapSet.member?(loaded_deps, dep) do + build_path = Path.join(build_lib_dir, dep) + File.rm_rf(build_path) + dep_path = Path.join(deps_dir, dep) + File.rm_rf(dep_path) + end + end + + defp install_project_dir(cache_id) do install_root = System.get_env("MIX_INSTALL_DIR") || Path.join(Mix.Utils.mix_cache(), "installs") @@ -996,9 +1036,9 @@ defmodule Mix do {id, dynamic_config} -> config = install_project_config(dynamic_config) - install_dir = install_dir(id) + install_project_dir = install_project_dir(id) - File.cd!(install_dir, fn -> + File.cd!(install_project_dir, fn -> :ok = Mix.ProjectStack.push(@mix_install_project, config, "nofile") try do @@ -1013,6 +1053,18 @@ defmodule Mix do end end + @doc """ + Returns the directory where the current `Mix.install/2` project + resides. + """ + @spec install_project_dir() :: Path.t() + def install_project_dir() do + case Mix.State.get(:installed) do + {id, _dynamic_config} -> install_project_dir(id) + nil -> nil + end + end + @doc """ Returns whether `Mix.install/2` was called in the current node. """ diff --git a/lib/mix/test/mix_test.exs b/lib/mix/test/mix_test.exs index 618d4cba67..266533fcdb 100644 --- a/lib/mix/test/mix_test.exs +++ b/lib/mix/test/mix_test.exs @@ -263,30 +263,26 @@ defmodule MixTest do [ {:git_repo, git: fixture_path("git_repo")} ], - lockfile: lockfile, - verbose: true + lockfile: lockfile ) assert_received {:mix_shell, :info, ["* Getting git_repo " <> _]} - assert_received {:mix_shell, :info, ["Mix.install/2 using " <> install_dir]} - assert File.read!(Path.join(install_dir, "mix.lock")) =~ rev - after - purge([GitRepo, GitRepo.MixProject]) + + install_project_dir = Mix.install_project_dir() + assert File.read!(Path.join(install_project_dir, "mix.lock")) =~ rev end test ":lockfile merging", %{tmp_dir: tmp_dir} do [rev1, rev2 | _] = get_git_repo_revs("git_repo") - Mix.install( - [ - {:git_repo, git: fixture_path("git_repo")} - ], - verbose: true - ) + Mix.install([ + {:git_repo, git: fixture_path("git_repo")} + ]) assert_received {:mix_shell, :info, ["* Getting git_repo " <> _]} - assert_received {:mix_shell, :info, ["Mix.install/2 using " <> install_dir]} - assert File.read!(Path.join(install_dir, "mix.lock")) =~ rev1 + + install_project_dir = Mix.install_project_dir() + assert File.read!(Path.join(install_project_dir, "mix.lock")) =~ rev1 Mix.Project.push(GitApp) lockfile = Path.join(tmp_dir, "lock") @@ -300,9 +296,7 @@ defmodule MixTest do lockfile: lockfile ) - assert File.read!(Path.join(install_dir, "mix.lock")) =~ rev1 - after - purge([GitRepo, GitRepo.MixProject]) + assert File.read!(Path.join(install_project_dir, "mix.lock")) =~ rev1 end test ":lockfile with application name", %{tmp_dir: tmp_dir} do @@ -318,15 +312,12 @@ defmodule MixTest do {:install_test, path: Path.join(tmp_dir, "install_test")}, {:git_repo, git: fixture_path("git_repo")} ], - lockfile: :install_test, - verbose: true + lockfile: :install_test ) assert_received {:mix_shell, :info, ["* Getting git_repo " <> _]} - assert_received {:mix_shell, :info, ["Mix.install/2 using " <> install_dir]} - assert File.read!(Path.join(install_dir, "mix.lock")) =~ rev - after - purge([GitRepo, GitRepo.MixProject]) + install_project_dir = Mix.install_project_dir() + assert File.read!(Path.join(install_project_dir, "mix.lock")) =~ rev end test ":lockfile that does not exist" do @@ -335,6 +326,73 @@ defmodule MixTest do end end + test "restore dir", %{tmp_dir: tmp_dir} do + with_cleanup(fn -> + Mix.install([ + {:git_repo, git: fixture_path("git_repo")} + ]) + + assert_received {:mix_shell, :info, ["* Getting git_repo " <> _]} + assert_received {:mix_shell, :info, ["==> git_repo"]} + assert_received {:mix_shell, :info, ["Compiling 1 file (.ex)"]} + assert_received {:mix_shell, :info, ["Generated git_repo app"]} + refute_received _ + + install_project_dir = Mix.install_project_dir() + build_lib_path = Path.join([install_project_dir, "_build", "dev", "lib"]) + deps_path = Path.join([install_project_dir, "deps"]) + + assert File.ls!(build_lib_path) |> Enum.sort() == ["git_repo", "mix_install"] + assert File.ls!(deps_path) == ["git_repo"] + + System.put_env("MIX_INSTALL_RESTORE_PROJECT_DIR", install_project_dir) + end) + + # Adding a dependency + + with_cleanup(fn -> + Mix.install([ + {:git_repo, git: fixture_path("git_repo")}, + {:install_test, path: Path.join(tmp_dir, "install_test")} + ]) + + assert_received {:mix_shell, :info, ["==> install_test"]} + assert_received {:mix_shell, :info, ["Compiling 2 files (.ex)"]} + assert_received {:mix_shell, :info, ["Generated install_test app"]} + refute_received _ + + install_project_dir = Mix.install_project_dir() + build_lib_path = Path.join([install_project_dir, "_build", "dev", "lib"]) + deps_path = Path.join([install_project_dir, "deps"]) + + assert File.ls!(build_lib_path) |> Enum.sort() == + ["git_repo", "install_test", "mix_install"] + + assert File.ls!(deps_path) == ["git_repo"] + + System.put_env("MIX_INSTALL_RESTORE_PROJECT_DIR", install_project_dir) + end) + + # Removing a dependency + + with_cleanup(fn -> + Mix.install([ + {:install_test, path: Path.join(tmp_dir, "install_test")} + ]) + + refute_received _ + + install_project_dir = Mix.install_project_dir() + build_lib_path = Path.join([install_project_dir, "_build", "dev", "lib"]) + deps_path = Path.join([install_project_dir, "deps"]) + + assert File.ls!(build_lib_path) |> Enum.sort() == ["install_test", "mix_install"] + assert File.ls!(deps_path) == [] + end) + after + System.delete_env("MIX_INSTALL_RESTORE_PROJECT_DIR") + end + test "installed?", %{tmp_dir: tmp_dir} do refute Mix.installed?() @@ -380,15 +438,7 @@ defmodule MixTest do on_exit(fn -> :code.set_path(path) - purge([InstallTest, InstallTest.MixProject, InstallTest.Protocol]) - - ExUnit.CaptureLog.capture_log(fn -> - Application.stop(:git_repo) - Application.unload(:git_repo) - - Application.stop(:install_test) - Application.unload(:install_test) - end) + cleanup_deps() end) Mix.State.put(:installed, nil) @@ -424,5 +474,37 @@ defmodule MixTest do [tmp_dir: tmp_dir] end + + defp with_cleanup(fun) do + path = :code.get_path() + + try do + fun.() + after + :code.set_path(path) + cleanup_deps() + + Mix.State.clear_cache() + Mix.State.put(:installed, nil) + end + end + + defp cleanup_deps() do + purge([ + GitRepo, + GitRepo.MixProject, + InstallTest, + InstallTest.MixProject, + InstallTest.Protocol + ]) + + ExUnit.CaptureLog.capture_log(fn -> + Application.stop(:git_repo) + Application.unload(:git_repo) + + Application.stop(:install_test) + Application.unload(:install_test) + end) + end end end From 91f6522eac20d6ac24c244c9a19e8b216f5439f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 29 Feb 2024 20:30:48 +0100 Subject: [PATCH 0432/1886] Add @doc since to install_project_dir --- lib/mix/lib/mix.ex | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/lib/mix/lib/mix.ex b/lib/mix/lib/mix.ex index ff2563334f..18b86927a2 100644 --- a/lib/mix/lib/mix.ex +++ b/lib/mix/lib/mix.ex @@ -352,8 +352,6 @@ defmodule Mix do * `MIX_INSTALL_DIR` - (since v1.12.0) specifies directory where `Mix.install/2` keeps install cache - * `MIX_INSTALL_FORCE` - (since v1.13.0) runs `Mix.install/2` with empty install cache - * `MIX_PATH` - appends extra code paths * `MIX_PROFILE` - a list of comma-separated Mix tasks to profile the time spent on @@ -661,13 +659,6 @@ defmodule Mix do This function can only be called outside of a Mix project and only with the same dependencies in the given VM. - The `MIX_INSTALL_RESTORE_PROJECT_DIR` environment variable may be specified. - It should point to a previous installation directory, which can be obtained - with `Mix.install_project_dir/0` (after calling `Mix.install/2`). Using a - restore dir may speed up the installation, since matching dependencies do - not need be refetched nor recompiled. This environment variable is ignored - if `:force` is enabled. - ## Options * `:force` - if `true`, runs with empty install cache. This is useful when you want @@ -783,6 +774,22 @@ defmodule Mix do The contents inside `defmodule` will only be expanded and executed after `Mix.install/2` runs, which means that any struct, macros, and imports will be correctly handled. + + ## Environment variables + + The `MIX_INSTALL_DIR` environment variable configures the directory that + caches all `Mix.install/2`. It defaults to the "mix/install" folder in the + default user cache of your operating system. + + The `MIX_INSTALL_FORCE` is available since Elixir v1.13.0 and forces + `Mix.install/2` to discard any previously cached entry of the current install. + + The `MIX_INSTALL_RESTORE_PROJECT_DIR` environment variable may be specified + since Elixir v1.16.2. It should point to a previous installation directory, + which can be obtained with `Mix.install_project_dir/0` (after calling `Mix.install/2`). + Using a restore dir may speed up the installation, since matching dependencies + do not need be refetched nor recompiled. This environment variable is ignored + if `:force` is enabled. """ @doc since: "1.12.0" def install(deps, opts \\ []) @@ -1057,6 +1064,7 @@ defmodule Mix do Returns the directory where the current `Mix.install/2` project resides. """ + @doc since: "1.16.2" @spec install_project_dir() :: Path.t() def install_project_dir() do case Mix.State.get(:installed) do From 270170dbd77a432ea7fbc2db3a0ddb21ced222c7 Mon Sep 17 00:00:00 2001 From: Nicolas Ferraro Date: Thu, 29 Feb 2024 16:39:15 -0300 Subject: [PATCH 0433/1886] Update ExUnit.Callbacks docs to reference `start_link_supervised!` (#13379) --- lib/ex_unit/lib/ex_unit/callbacks.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/ex_unit/lib/ex_unit/callbacks.ex b/lib/ex_unit/lib/ex_unit/callbacks.ex index 6637784e41..4761afa33b 100644 --- a/lib/ex_unit/lib/ex_unit/callbacks.ex +++ b/lib/ex_unit/lib/ex_unit/callbacks.ex @@ -3,8 +3,8 @@ defmodule ExUnit.Callbacks do Defines ExUnit callbacks. This module defines the `setup/1`, `setup/2`, `setup_all/1`, and - `setup_all/2` callbacks, as well as the `on_exit/2`, `start_supervised/2` - and `stop_supervised/1` functions. + `setup_all/2` callbacks, as well as the `on_exit/2`, `start_supervised/2`, + `stop_supervised/1` and `start_link_supervised!/2` functions. The setup callbacks may be used to define [test fixtures](https://en.wikipedia.org/wiki/Test_fixture#Software) and run any initialization code which help bring the system into a known From fafd35a5c51ed6317ee14b93e26967a223704c95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 29 Feb 2024 20:43:30 +0100 Subject: [PATCH 0434/1886] Add an example to fetch installation directory --- lib/mix/lib/mix.ex | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/mix/lib/mix.ex b/lib/mix/lib/mix.ex index 18b86927a2..8763a33b7b 100644 --- a/lib/mix/lib/mix.ex +++ b/lib/mix/lib/mix.ex @@ -779,7 +779,11 @@ defmodule Mix do The `MIX_INSTALL_DIR` environment variable configures the directory that caches all `Mix.install/2`. It defaults to the "mix/install" folder in the - default user cache of your operating system. + default user cache of your operating system. You can use `install_project_dir/0` + to access the directory of an existing install (alongside other installs): + + iex> Mix.install([]) + iex> Mix.install_project_dir() The `MIX_INSTALL_FORCE` is available since Elixir v1.13.0 and forces `Mix.install/2` to discard any previously cached entry of the current install. From a257c5fd176e2a29fc6c18a6473ebc2715c13db0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonatan=20K=C5=82osko?= Date: Fri, 1 Mar 2024 09:15:01 +0100 Subject: [PATCH 0435/1886] Fix Mix.install_project_dir/0 spec (#13381) --- lib/mix/lib/mix.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mix/lib/mix.ex b/lib/mix/lib/mix.ex index 8763a33b7b..41e855bdb6 100644 --- a/lib/mix/lib/mix.ex +++ b/lib/mix/lib/mix.ex @@ -1069,7 +1069,7 @@ defmodule Mix do resides. """ @doc since: "1.16.2" - @spec install_project_dir() :: Path.t() + @spec install_project_dir() :: Path.t() | nil def install_project_dir() do case Mix.State.get(:installed) do {id, _dynamic_config} -> install_project_dir(id) From d5e9ad301b6fb534cab0de9da670f9a0152de783 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonatan=20K=C5=82osko?= Date: Fri, 1 Mar 2024 09:33:14 +0100 Subject: [PATCH 0436/1886] Remove consolidated when restoring Mix.install/2 dir (#13382) --- lib/mix/lib/mix.ex | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/lib/mix/lib/mix.ex b/lib/mix/lib/mix.ex index 41e855bdb6..be6aa06c54 100644 --- a/lib/mix/lib/mix.ex +++ b/lib/mix/lib/mix.ex @@ -377,6 +377,8 @@ defmodule Mix do """ @mix_install_project __MODULE__.InstallProject + @mix_install_app :mix_install + @mix_install_app_string Atom.to_string(@mix_install_app) use Application @@ -893,6 +895,7 @@ defmodule Mix do if first_build? and restore_dir != nil and not force? do File.cp_r(restore_dir, install_project_dir) + remove_dep(install_project_dir, @mix_install_app_string) end File.mkdir_p!(install_project_dir) @@ -996,7 +999,6 @@ defmodule Mix do defp remove_leftover_deps(install_project_dir) do build_lib_dir = Path.join([install_project_dir, "_build", "dev", "lib"]) - deps_dir = Path.join(install_project_dir, "deps") deps = File.ls!(build_lib_dir) @@ -1006,16 +1008,23 @@ defmodule Mix do do: Atom.to_string(app) # We want to keep :mix_install, but it has no application - loaded_deps = MapSet.put(loaded_deps, "mix_install") + loaded_deps = MapSet.put(loaded_deps, @mix_install_app_string) for dep <- deps, not MapSet.member?(loaded_deps, dep) do - build_path = Path.join(build_lib_dir, dep) - File.rm_rf(build_path) - dep_path = Path.join(deps_dir, dep) - File.rm_rf(dep_path) + remove_dep(install_project_dir, dep) end end + defp remove_dep(install_project_dir, dep) do + build_lib_dir = Path.join([install_project_dir, "_build", "dev", "lib"]) + deps_dir = Path.join(install_project_dir, "deps") + + build_path = Path.join(build_lib_dir, dep) + File.rm_rf(build_path) + dep_path = Path.join(deps_dir, dep) + File.rm_rf(dep_path) + end + defp install_project_dir(cache_id) do install_root = System.get_env("MIX_INSTALL_DIR") || @@ -1033,7 +1042,7 @@ defmodule Mix do build_path: "_build", lockfile: "mix.lock", deps_path: "deps", - app: :mix_install, + app: @mix_install_app, erlc_paths: [], elixirc_paths: [], compilers: [], From 103faafcd092375810c22173f4e74571431e8370 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 1 Mar 2024 11:17:34 +0100 Subject: [PATCH 0437/1886] Discard mermaid fenced blocks from ansi docs --- lib/elixir/lib/io/ansi/docs.ex | 15 ++++++++++----- lib/elixir/test/elixir/io/ansi/docs_test.exs | 7 +++++-- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/lib/elixir/lib/io/ansi/docs.ex b/lib/elixir/lib/io/ansi/docs.ex index d0be03e380..d76a48d13f 100644 --- a/lib/elixir/lib/io/ansi/docs.ex +++ b/lib/elixir/lib/io/ansi/docs.ex @@ -176,12 +176,17 @@ defmodule IO.ANSI.Docs do process_code(rest, [line], indent, options) end - defp process(["```" <> _line | rest], text, indent, options) do - process_fenced_code_block(rest, text, indent, options, _delimiter = "```") + defp process(["```mermaid" <> _line | rest], text, indent, options) do + write_text(text, indent, options) + + rest + |> Enum.drop_while(&(&1 != "```")) + |> Enum.drop(1) + |> process([], indent, options) end - defp process(["~~~" <> _line | rest], text, indent, options) do - process_fenced_code_block(rest, text, indent, options, _delimiter = "~~~") + defp process(["```" <> _line | rest], text, indent, options) do + process_fenced_code_block(rest, text, indent, options, _delimiter = "```") end defp process(["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

=C* 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

S;enYEdLAwnMqYqE&y`0aImq|?wGlfe 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

)osq%y)8WnJ5 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%0A^(( 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