diff --git a/.formatter.exs b/.formatter.exs index f9bb2b1f0f2..b09e896884e 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/.github/workflows/ci.yml b/.github/workflows/ci.yml index afe1f6b4b55..4a35c6ad3f9 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,12 @@ 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: "24.3" + - otp_version: "24.0" - otp_version: master development: true - otp_version: maint @@ -77,7 +77,7 @@ jobs: name: Windows Server 2019, Erlang/OTP ${{ matrix.otp_version }} strategy: matrix: - otp_version: ['24', '25', '26'] + otp_version: ["24", "25", "26.0"] runs-on: windows-2019 steps: - name: Configure Git diff --git a/CHANGELOG.md b/CHANGELOG.md index d5e1d9b99e8..31930daa959 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,21 @@ For mismatched delimiters, it now shows both delimiters: └─ lib/my_app.ex:1:18 ``` +For unclosed delimiters, it now shows where the unclosed delimiter starts: + +``` +** (TokenMissingError) token missing on lib/my_app: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 ")") + │ + └─ lib/my_app:8:23 +``` + Errors and warnings diagnostics also include code snippets. When possible, we will show precise spans, such as on undefined variables: ``` @@ -51,19 +66,91 @@ 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. +A huge thank you to Vinícius Müller 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. +Elixir's Getting Started guides have 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. +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 examples of potential anti-patterns, 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.16.3 (2024-05-21) + +### 1. Bug fixes + +#### Elixir + + * [bin/elixir] Properly handle the `--dbg` flag in Elixir's CLI + * [Code.Formatter] Add brackets around keyword lists when formatting the left-hand side of `when` + * [Kernel] Only infer size in pinned variable in binary strings when needed + * [System] Add a note that arguments are unsafe when invoking .bat/.com scripts on Windows via `System.cmd/3` + * [Port] Add a note that arguments are unsafe when invoking .bat/.com scripts on Windows + * [URI] Ensure `:undefined` fields are properly converted to `nil` when invoking Erlang's API + +#### Logger + + * [Logger] Ensure translators are persisted across logger restarts + +#### Mix + + * [mix compile] Ensure compile paths are accessible during compilation + +## v1.16.2 (2024-03-10) + +### 1. Enhancements + +#### Elixir + + * [Code] Emit `:defmodule` tracing event on module definition + +#### Mix + + * [Mix] Add `Mix.install_project_dir/0` + * [Mix] Add environment variable for reusing `Mix.install/2` installation + * [Mix.SCM] Add `Mix.SCM.delete/1` + +### 2. Bug fixes + +#### Elixir + + * [Code] Fix charlist formatting issue when a single-quoted charlist escapes a double-quote character + * [Path] Fix regression on how `Path.relative_to/2` dealt with "." as input + +#### IEx + + * [IEx.Helpers] Discard mermaid fenced blocks from ansi docs + +#### ExUnit + + * [ExUnit] Properly compared pinned values when building diffs + +## v1.16.1 (2024-01-31) + +### 1. Bug fixes + +#### Elixir + + * [Code] Fix `Code.quoted_to_algebra/2` for operator with :do key as operand + * [Kernel.ParallelCompiler] Do not crash parallel compiler when it receives diagnostics from additional code evaluation + * [Kernel.ParallelCompiler] Always log errors at the end of compilation + * [String] Fix `String.capitalize/1` with a single codepoint + +#### IEx + + * [IEx] Fix autocompletion of function signatures on Erlang/OTP 26 + * [IEx] Do not assume `$HOME` is set + +#### Mix + + * [mix deps.compile] Handle compilation of rebar3 dependencies when rebar3 is on a path with spaces on Unix + * [mix test] Properly resolve relative paths when running tests from individual files + * [mix test] Properly resolve Windows paths when running tests from individual files + +## v1.16.0 (2023-12-22) ### 1. Enhancements @@ -73,9 +160,14 @@ Finally, we have started enriching our documentation with [Mermaid.js](https://m #### Elixir + * [Code] Add `:emit_warnings` for `Code.string_to_quoted/2` * [Code] Automatically include columns in parsing options * [Code] Introduce `MismatchedDelimiterError` for handling mismatched delimiter exceptions * [Code.Fragment] Handle anonymous calls in fragments + * [Code.Formatter] Trim trailing whitespace on heredocs with `\r\n` + * [File] Add `:offset` option to `File.stream!/2` + * [Kernel] Auto infer size of matched variable in bitstrings + * [Kernel] Preserve column information when translating typespecs * [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 @@ -83,34 +175,57 @@ Finally, we have started enriching our documentation with [Mermaid.js](https://m * [Module] Add support for `@nifs` annotation from Erlang/OTP 25 * [Module] Add support for missing `@dialyzer` configuration * [String] Update to Unicode 15.1.0 + * [String] Add `String.replace_invalid/2` * [Task] Add `:limit` option to `Task.yield_many/2` +#### Logger + + * [Logger] Add `Logger.levels/0` + #### Mix * [mix] Add `MIX_PROFILE` to profile a list of comma separated tasks + * [mix archive.install] Support `--sparse` option + * [mix compile.app] Warn if both `:applications` and `:extra_applications` are used + * [mix compile.elixir] Pass original exception down to diagnostic `:details` when possible * [mix compile.elixir] Optimize scenario where there are thousands of files in `lib/` and one of them is changed + * [mix deps.clean] Emit a warning instead of crashing when a dependency cannot be removed + * [mix escript.build] Escripts now strip .beam files by default, which leads to smaller escripts. However, if you are using escripts to access Elixir docs or compile Elixir code, documentation and deprecation metadata is no longer available. Set `strip_beams: false` in your escript configuration in your `mix.exs` to keep all metadata + * [mix escript.install] Support `--sparse` option + * [mix release] Include `include/` directory in releases * [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] Keep quotes for atom keys in formatter * [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] Re-enabled compiler optimizations for top level functions in scripts (disabled in v1.14.0 but shouldn't impact most programs) * [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 + * [Macro] Address exception on `Macro.to_string/1` for certain ASTs + * [Path] Lazily evaluate `File.cwd!/0` in `Path.expand/1` and `Path.absname/1` * [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 +#### IEx + + * [IEx.Pry] Fix prying functions with only literals in their body + #### Mix - * [Mix] Ensure files with duplicate modules are recompiled whenever any of the files change + * [mix archive.install] Restore code paths after `mix archive.install` + * [mix compile] Ensure files with duplicate modules are recompiled whenever any of the files change + * [mix compile] Update Mix compiler diagnostics documentation and typespecs to match the Elixir compiler behaviour where both lines and columns start from one (before it inaccurately said that columns started from zero) + * [mix escript.install] Restore code paths after `mix escript.install` ### 3. Soft deprecations (no warnings emitted) @@ -120,6 +235,10 @@ Finally, we have started enriching our documentation with [Mermaid.js](https://m * [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` +#### Mix + + * [mix compile] Returning a four-element tuple as a position in `Mix.Task.Compiler.Diagnostic` + ### 4. Hard deprecations #### Elixir diff --git a/Makefile b/Makefile index 1dd0b18fdc2..d1725414637 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ PREFIX ?= /usr/local TEST_FILES ?= "*_test.exs" SHARE_PREFIX ?= $(PREFIX)/share MAN_PREFIX ?= $(SHARE_PREFIX)/man -CANONICAL := main/ +# CANONICAL := main/ ELIXIRC := bin/elixirc --ignore-module-conflict $(ELIXIRC_OPTS) ERLC := erlc -I lib/elixir/include ERL_MAKE := if [ -n "$(ERLC_OPTS)" ]; then ERL_COMPILER_OPTIONS=$(ERLC_OPTS) erl -make; else erl -make; fi @@ -189,7 +189,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/SECURITY.md b/SECURITY.md index 69252d3ad7c..be691f8303a 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -6,12 +6,11 @@ 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.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 1f0d2f33519..b8ae5a5b4d6 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.16.0-dev +1.16.3 \ No newline at end of file diff --git a/bin/elixir b/bin/elixir index cb0a84650b6..72ae34eb32e 100755 --- a/bin/elixir +++ b/bin/elixir @@ -1,7 +1,7 @@ #!/bin/sh set -e -ELIXIR_VERSION=1.16.0-dev +ELIXIR_VERSION=1.16.3 if [ $# -eq 0 ] || { [ $# -eq 1 ] && { [ "$1" = "--help" ] || [ "$1" = "-h" ]; }; }; then cat <&2 @@ -112,10 +112,10 @@ while [ $I -le $LENGTH ]; do C=1 MODE="iex" ;; - -v|--no-halt|--dbg) + -v|--no-halt) C=1 ;; - -e|-r|-pr|-pa|-pz|--eval|--remsh|--dot-iex) + -e|-r|-pr|-pa|-pz|--eval|--remsh|--dot-iex|--dbg) C=2 ;; --rpc-eval) diff --git a/bin/elixir.bat b/bin/elixir.bat index a222cbcc65e..d98cf550d5e 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.16.3 setlocal enabledelayedexpansion if ""%1""=="""" if ""%2""=="""" goto documentation @@ -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 diff --git a/lib/eex/test/eex_test.exs b/lib/eex/test/eex_test.exs index 2e60d90354b..8436bd65b3b 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/lib/code.ex b/lib/elixir/lib/code.ex index 09099667a5c..e8ce423a1b3 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 @@ -196,20 +200,44 @@ 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 + the line and column the diagnostic ends. + + 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(:file) => Path.t(), + required(:source) => Path.t() | nil, + 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) => {line :: pos_integer(), column :: pos_integer()} | nil, + optional(:details) => term(), 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, @@ -962,10 +990,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 +1153,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` @@ -2057,7 +2083,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/formatter.ex b/lib/elixir/lib/code/formatter.ex index eb1d0dd773e..3a159c3f79d 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 @@ -299,7 +300,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 @@ -309,7 +310,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 @@ -368,13 +369,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 -> @@ -520,14 +522,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, "\"", "\":") @@ -1956,6 +1959,14 @@ defmodule Code.Formatter do # fn a, b, c when d -> e end defp clause_args_to_algebra([{:when, meta, args}], state) do {args, right} = split_last(args) + + # If there are any keywords, wrap them in lists + args = + Enum.map(args, fn + [_ | _] = keyword -> {:__block__, [], [keyword]} + other -> other + end) + left = {{:special, :clause_args}, meta, [args]} binary_op_to_algebra(:when, "when", meta, left, right, :no_parens_arg, state) end @@ -2409,19 +2420,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/lib/code/fragment.ex b/lib/elixir/lib/code/fragment.ex index ace8d467661..5ad6894f9f1 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 @@ -1107,6 +1091,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/lib/code/normalizer.ex b/lib/elixir/lib/code/normalizer.ex index 03c8a706769..7abded155b3 100644 --- a/lib/elixir/lib/code/normalizer.ex +++ b/lib/elixir/lib/code/normalizer.ex @@ -349,19 +349,25 @@ defmodule Code.Normalizer do meta end + last = List.last(args) + cond do - Keyword.has_key?(meta, :do) or match?([{{:__block__, _, [:do]}, _} | _], List.last(args)) -> + 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 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]] 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, []) @@ -382,10 +388,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/lib/code/typespec.ex b/lib/elixir/lib/code/typespec.ex index 7cda6db78b0..017b2f05dcc 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 @@ -419,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/config.ex b/lib/elixir/lib/config.ex index cc39e43baa5..abde56cb1d4 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`. diff --git a/lib/elixir/lib/enum.ex b/lib/elixir/lib/enum.ex index e34d665ca4a..7b7127d60dd 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 @@ -3793,9 +3793,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]} @@ -4051,7 +4048,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/exception.ex b/lib/elixir/lib/exception.ex index 68bb06bc6a4..6575743c889 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 """ @@ -793,6 +807,194 @@ defmodule Exception do col -> format_file_line_column(Keyword.get(opts, :file), Keyword.get(opts, :line), col, " ") end end + + @doc false + def format_delimiter(delimiter) do + if delimiter |> Atom.to_string() |> String.contains?(["\"", "'"]), + do: delimiter, + else: ~s("#{delimiter}") + end + + @doc false + def format_snippet( + {start_line, _start_column} = start_pos, + {end_line, end_column} = end_pos, + 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) + + relevant_lines = + if end_line - start_line < 5 do + line_range(lines, start_pos, end_pos, padding, max_digits, start_message, end_message) + else + trimmed_inbetween_lines( + lines, + start_pos, + end_pos, + padding, + max_digits, + start_message, + end_message + ) + end + + """ + #{padding}#{red("error:")} #{pad_message(description, padding)} + #{padding}│ + #{relevant_lines} + #{padding}│ + #{padding}└─ #{Path.relative_to_cwd(file)}:#{end_line}:#{end_column}\ + """ + end + + def format_snippet( + {start_line, start_column}, + {end_line, end_column}, + 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) + formatted_line = [line_padding(end_line, max_digits), to_string(end_line), " │ ", hd(lines)] + + mismatched_closing_line = + [ + n_spaces(start_column - 1), + red("│"), + format_end_message(end_column - start_column, end_message) + ] + + unclosed_delimiter_line = + [padding, " │ ", format_start_message(start_column, start_message)] + + below_line = [padding, " │ ", mismatched_closing_line, "\n", unclosed_delimiter_line] + + """ + #{padding}#{red("error:")} #{pad_message(description, padding)} + #{padding}│ + #{formatted_line} + #{below_line} + #{padding}│ + #{padding}└─ #{Path.relative_to_cwd(file)}:#{end_line}:#{end_column}\ + """ + end + + defp line_padding(line_number, max_digits) do + line_digits = digits(line_number) + + spacing = + if line_digits == 1 do + max(2, max_digits) + else + max_digits - line_digits + 1 + end + + n_spaces(spacing) + end + + defp n_spaces(n), do: String.duplicate(" ", n) + + defp digits(number, acc \\ 1) + defp digits(number, acc) when number < 10, do: acc + defp digits(number, acc), do: digits(div(number, 10), acc + 1) + + defp trimmed_inbetween_lines( + lines, + {start_line, start_column}, + {end_line, end_column}, + padding, + max_digits, + start_message, + end_message + ) do + start_padding = line_padding(start_line, max_digits) + end_padding = line_padding(end_line, max_digits) + first_line = hd(lines) + last_line = List.last(lines) + + """ + #{start_padding}#{start_line} │ #{first_line} + #{padding}│ #{format_start_message(start_column, start_message)} + ... + #{end_padding}#{end_line} │ #{last_line} + #{padding}│ #{format_end_message(end_column, end_message)}\ + """ + end + + defp line_range( + lines, + {start_line, start_column}, + {end_line, end_column}, + padding, + max_digits, + start_message, + end_message + ) do + Enum.zip_with(lines, start_line..end_line, fn line, line_number -> + line_padding = line_padding(line_number, max_digits) + + cond do + line_number == start_line -> + [ + line_padding, + to_string(line_number), + " │ ", + line, + "\n", + padding, + " │ ", + format_start_message(start_column, start_message) + ] + + line_number == end_line -> + [ + line_padding, + to_string(line_number), + " │ ", + line, + "\n", + padding, + " │ ", + format_end_message(end_column, end_message) + ] + + true -> + [line_padding, to_string(line_number), " │ ", line] + end + end) + |> Enum.intersperse("\n") + end + + defp format_end_message(end_column, message), + do: [ + n_spaces(end_column - 1), + red(message) + ] + + 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}") + + defp red(string) do + if IO.ANSI.enabled?() do + [IO.ANSI.red(), string, IO.ANSI.reset()] + else + string + end + end end # Some exceptions implement "message/1" instead of "exception/1" mostly @@ -944,11 +1146,24 @@ defmodule MismatchedDelimiterError do An exception raised when a mismatched delimiter is found when parsing code. For example: - - `[1, 2, 3}` - - `fn a -> )` - """ - @max_lines_shown 5 + * `[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 [ :file, @@ -958,6 +1173,7 @@ defmodule MismatchedDelimiterError do :end_column, :opening_delimiter, :closing_delimiter, + :expected_delimiter, :snippet, description: "mismatched delimiter error" ] @@ -969,204 +1185,30 @@ defmodule MismatchedDelimiterError do end_line: end_line, end_column: end_column, 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 = :elixir_tokenizer.terminator(opening_delimiter) + expected_delimiter = Exception.format_delimiter(expected_delimiter) - snippet = format_snippet(start_pos, end_pos, description, file, lines, expected_delimiter) - format_message(file, end_line, end_column, snippet) - end + start_message = "└ unclosed delimiter" + end_message = ~s/└ mismatched closing delimiter (expected #{expected_delimiter})/ - defp format_snippet( - {start_line, _start_column} = start_pos, - {end_line, end_column} = end_pos, - description, - file, - lines, - expected_delimiter - ) - 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 - line_range(lines, start_pos, end_pos, padding, max_digits, expected_delimiter) - else - trimmed_inbetween_lines( - lines, - start_pos, - end_pos, - padding, - max_digits, - expected_delimiter - ) - end - - """ - #{padding}#{red("error:")} #{pad_message(description, padding)} - #{padding}│ - #{relevant_lines} - #{padding}│ - #{padding}└─ #{Path.relative_to_cwd(file)}:#{end_line}:#{end_column}\ - """ - end - - defp format_snippet( - {start_line, start_column}, - {end_line, end_column}, - description, - file, - lines, - expected_delimiter - ) - when start_line == end_line 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) - formatted_line = [line_padding(end_line, max_digits), to_string(end_line), " │ ", line] - - mismatched_closing_line = - [ - n_spaces(start_column - 1), - red("│"), - mismatched_closing_delimiter(end_column - start_column, expected_delimiter) - ] - - unclosed_delimiter_line = - [padding, " │ ", unclosed_delimiter(start_column)] - - below_line = [padding, " │ ", mismatched_closing_line, "\n", unclosed_delimiter_line] - - """ - #{padding}#{red("error:")} #{pad_message(description, padding)} - #{padding}│ - #{formatted_line} - #{below_line} - #{padding}│ - #{padding}└─ #{Path.relative_to_cwd(file)}:#{end_line}:#{end_column}\ - """ - end - - defp line_padding(line_number, max_digits) do - line_digits = digits(line_number) - - spacing = - if line_digits == 1 do - max(2, max_digits) - else - max_digits - line_digits + 1 - end - - n_spaces(spacing) - end - - defp n_spaces(n), do: String.duplicate(" ", n) - - defp digits(number, acc \\ 1) - defp digits(number, acc) when number < 10, do: acc - defp digits(number, acc), do: digits(div(number, 10), acc + 1) - - defp trimmed_inbetween_lines( - lines, - {start_line, start_column}, - {end_line, end_column}, - 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) - - """ - #{start_padding}#{start_line} │ #{first_line} - #{padding}│ #{unclosed_delimiter(start_column)} - ... - #{end_padding}#{end_line} │ #{last_line} - #{padding}│ #{mismatched_closing_delimiter(end_column, expected_delimiter)}\ - """ - end - - defp line_range( - lines, - {start_line, start_column}, - {end_line, end_column}, - padding, - max_digits, - expected_delimiter - ) do - start_line = start_line - 1 - end_line = end_line - 1 - - lines - |> Enum.slice(start_line..end_line) - |> 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 - - line_padding = line_padding(line_number, max_digits) - - cond do - line_number == start_line -> - [ - line_padding, - to_string(line_number), - " │ ", - line, - "\n", - padding, - " │ ", - unclosed_delimiter(start_column) - ] - - line_number == end_line -> - [ - line_padding, - to_string(line_number), - " │ ", - line, - "\n", - padding, - " │ ", - mismatched_closing_delimiter(end_column, expected_delimiter) - ] - - true -> - [line_padding, to_string(line_number), " │ ", line] - end - end) - |> Enum.intersperse("\n") - end - - defp mismatched_closing_delimiter(end_column, expected_closing_delimiter), - do: [ - n_spaces(end_column - 1), - red(~s/└ mismatched closing delimiter (expected "#{expected_closing_delimiter}")/) - ] - - defp unclosed_delimiter(start_column), - do: [n_spaces(start_column - 1), red("└ unclosed delimiter")] - - defp pad_message(message, padding), do: String.replace(message, "\n", "\n #{padding}") + snippet = + Exception.format_snippet( + start_pos, + end_pos, + description, + file, + lines, + start_message, + end_message + ) - defp red(string) do - if IO.ANSI.enabled?() do - [IO.ANSI.red(), string, IO.ANSI.reset()] - else - string - end + format_message(file, end_line, end_column, snippet) end defp format_message(file, line, column, message) do @@ -1183,8 +1225,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 """ @@ -1233,17 +1276,24 @@ 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 [ :file, :line, - :snippet, :column, + :end_line, + :end_column, + :snippet, :opening_delimiter, + :expected_delimiter, description: "expression is incomplete" ] @@ -1252,14 +1302,40 @@ defmodule TokenMissingError do file: file, line: line, column: column, + end_line: end_line, description: description, + expected_delimiter: expected_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 + {trimmed, [last_line | _] = reversed_lines} = + snippet + |> String.split("\n") + |> Enum.reverse() + |> Enum.split_while(&(&1 == "")) + + end_line = end_line - length(trimmed) + end_column = String.length(last_line) + 1 + + start_pos = {line, column} + end_pos = {end_line, end_column} + expected_delimiter = Exception.format_delimiter(expected_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, + description, + file, + Enum.reverse(reversed_lines), + start_message, + end_message + ) - format_message(file, line, column, snippet) + format_message(file, end_line, end_column, snippet) end @impl true @@ -1267,13 +1343,13 @@ 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 format_message(file, line, column, message) do diff --git a/lib/elixir/lib/file.ex b/lib/elixir/lib/file.ex index f9f928146f9..df1664dd658 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}} @@ -1748,18 +1751,20 @@ 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 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. 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 # 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) diff --git a/lib/elixir/lib/file/stream.ex b/lib/elixir/lib/file/stream.ex index cd23f2b0b54..4633ca1f1c2 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/lib/float.ex b/lib/elixir/lib/float.ex index 49862329e6f..b0bf7697172 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). @@ -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 -> minus_zero() + :half_up when sign === 1 -> minus_zero() _ -> 0.0 end @@ -394,6 +405,9 @@ defmodule Float do boundary = den <<< 52 cond do + num == 0 and sign == 1 -> + minus_zero() + num == 0 -> 0.0 @@ -408,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/lib/gen_server.ex b/lib/elixir/lib/gen_server.ex index 79549a9c730..a2884ebbdbb 100644 --- a/lib/elixir/lib/gen_server.ex +++ b/lib/elixir/lib/gen_server.ex @@ -9,14 +9,10 @@ defmodule GenServer do 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 + graph BT + C(Client #3) ~~~ B(Client #2) ~~~ A(Client #1) + A & B & C -->|request| GenServer + GenServer -.->|reply| A & B & C ``` ## Example @@ -475,7 +471,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/inspect.ex b/lib/elixir/lib/inspect.ex index d5753b8d844..92dc6765564 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/inspect/algebra.ex b/lib/elixir/lib/inspect/algebra.ex index cfd790f77f0..5e213b37798 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`. diff --git a/lib/elixir/lib/io.ex b/lib/elixir/lib/io.ex index d5d0788b6d3..42b3a03025b 100644 --- a/lib/elixir/lib/io.ex +++ b/lib/elixir/lib/io.ex @@ -308,17 +308,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 @@ -337,15 +340,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/lib/io/ansi/docs.ex b/lib/elixir/lib/io/ansi/docs.ex index d0be03e3805..d76a48d13f9 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(["