diff options
author | José Valim <jose.valim@dashbit.co> | 2023-05-10 12:32:25 +0200 |
---|---|---|
committer | José Valim <jose.valim@dashbit.co> | 2023-05-10 12:32:25 +0200 |
commit | 48e2efd2d9479ce6a79ae2badf91f24d87e71b88 (patch) | |
tree | 89ccbf2a85770613c2caff5509a0faa6b55407e3 | |
parent | fb1f1830b0eb5c2381c1fa14dba27431092152c8 (diff) | |
download | elixir-48e2efd2d9479ce6a79ae2badf91f24d87e71b88.tar.gz |
Return diagnostics from Kernel.ParallelCompiler
-rw-r--r-- | lib/elixir/lib/io.ex | 5 | ||||
-rw-r--r-- | lib/elixir/lib/kernel/parallel_compiler.ex | 88 | ||||
-rw-r--r-- | lib/elixir/lib/module/parallel_checker.ex | 33 | ||||
-rw-r--r-- | lib/elixir/src/elixir_errors.erl | 51 | ||||
-rw-r--r-- | lib/mix/lib/mix/compilers/elixir.ex | 30 |
5 files changed, 117 insertions, 90 deletions
diff --git a/lib/elixir/lib/io.ex b/lib/elixir/lib/io.ex index 351d8bc6f..a6ba705de 100644 --- a/lib/elixir/lib/io.ex +++ b/lib/elixir/lib/io.ex @@ -327,7 +327,7 @@ defmodule IO do def warn(message, []) do message = [to_chardata(message), ?\n] - :elixir_errors.print_warning(0, nil, message, message) + :elixir_errors.print_diagnostic(:warning, 0, nil, message, message) end def warn(message, %Macro.Env{} = env) do @@ -357,7 +357,8 @@ defmodule IO do line = opts[:line] file = opts[:file] - :elixir_errors.print_warning( + :elixir_errors.print_diagnostic( + :warning, line || 0, file && List.to_string(file), message, diff --git a/lib/elixir/lib/kernel/parallel_compiler.ex b/lib/elixir/lib/kernel/parallel_compiler.ex index db02906f3..7b8c88e0b 100644 --- a/lib/elixir/lib/kernel/parallel_compiler.ex +++ b/lib/elixir/lib/kernel/parallel_compiler.ex @@ -5,15 +5,24 @@ defmodule Kernel.ParallelCompiler do @typedoc "The line. 0 indicates no line." @type line() :: non_neg_integer() - @type location() :: line() | {pos_integer(), column :: non_neg_integer} - @type warning() :: {file :: Path.t(), location(), message :: String.t()} - @type error() :: {file :: Path.t(), location(), message :: String.t()} + @type position() :: line() | {pos_integer(), column :: non_neg_integer} + + @type diagnostic(severity) :: %{ + file: Path.t(), + severity: severity, + message: String.t(), + position: position + } @type info :: %{ - runtime_warnings: [warning], - compile_warnings: [warning] + runtime_warnings: [diagnostic(:warning)], + compile_warnings: [diagnostic(:warning)] } + # Deprecated types + @type warning() :: {file :: Path.t(), position(), message :: String.t()} + @type error() :: {file :: Path.t(), position(), message :: String.t()} + @doc """ Starts a task for parallel compilation. @@ -62,17 +71,14 @@ defmodule Kernel.ParallelCompiler do It returns `{:ok, modules, warnings}` or `{:error, errors, warnings}` by default but we recommend using `return_maps: true` so it returns - a map as third element instead of a list of warnings. The map has the - shape of: + diagnostics as maps as well as a map of compilation information. + The map has the shape of: %{ runtime_warnings: [warning], compile_warnings: [warning] } - Both errors and warnings are a list of three-element tuples containing - the file, line and the formatted error/warning. - ## Options * `:each_file` - for each file compiled, invokes the callback passing the @@ -109,12 +115,13 @@ defmodule Kernel.ParallelCompiler do * `:beam_timestamp` - the modification timestamp to give all BEAM files * `:return_maps` (since v1.15.0) - returns maps with information instead of - a list of warnings + a list of warnings and returns diagnostics as maps instead of tuples """ @doc since: "1.6.0" @spec compile([Path.t()], keyword()) :: - {:ok, [atom], [warning] | info()} | {:error, [error], [warning] | info()} + {:ok, [atom], [warning] | info()} + | {:error, [error] | [diagnostic(:error)], [warning] | info()} def compile(files, options \\ []) when is_list(options) do spawn_workers(files, :compile, options) end @@ -126,7 +133,8 @@ defmodule Kernel.ParallelCompiler do """ @doc since: "1.6.0" @spec compile_to_path([Path.t()], Path.t(), keyword()) :: - {:ok, [atom], [warning] | info()} | {:error, [error], [warning] | info()} + {:ok, [atom], [warning] | info()} + | {:error, [error] | [diagnostic(:error)], [warning] | info()} def compile_to_path(files, path, options \\ []) when is_binary(path) and is_list(options) do spawn_workers(files, {:compile, path}, options) end @@ -139,17 +147,14 @@ defmodule Kernel.ParallelCompiler do It returns `{:ok, modules, warnings}` or `{:error, errors, warnings}` by default but we recommend using `return_maps: true` so it returns - a map as third element instead of a list of warnings. The map has the - shape of: + diagnostics as maps as well as a map of compilation information. + The map has the shape of: %{ runtime_warnings: [warning], compile_warnings: [warning] } - Both errors and warnings are a list of three-element tuples containing - the file, line and the formatted error/warning. - ## Options * `:each_file` - for each file compiled, invokes the callback passing the @@ -167,12 +172,17 @@ defmodule Kernel.ParallelCompiler do end @doc """ - Prints a warning returned by the compiler. + Prints a diagnostic returned by the compiler into stderr. """ - @doc since: "1.13.0" - @spec print_warning(warning) :: :ok + @doc since: "1.15.0" + def print_diagnostic(%{file: file, position: position, message: message, severity: severity}) do + :elixir_errors.print_diagnostic_no_capture(severity, position, file, message) + end + + @doc false + # TODO: Deprecate me on Elixir v1.19 def print_warning({file, location, warning}) do - :elixir_errors.print_warning_no_diagnostic(location, file, warning) + :elixir_errors.print_diagnostic_no_capture(:warning, location, file, warning) end @doc false @@ -209,7 +219,8 @@ defmodule Kernel.ParallelCompiler do "Compilation failed due to warnings while using the --warnings-as-errors option" IO.puts(:stderr, message) - {:error, r_warnings ++ c_warnings, %{info | runtime_warnings: [], compile_warnings: []}} + errors = Enum.map(r_warnings ++ c_warnings, &Map.replace!(&1, :severity, :error)) + {:error, errors, %{info | runtime_warnings: [], compile_warnings: []}} {{:ok, outcome, info}, _} -> beam_timestamp = Keyword.get(options, :beam_timestamp) @@ -230,7 +241,12 @@ defmodule Kernel.ParallelCompiler do if Keyword.get(options, :return_maps, false) do {status, modules_or_errors, info} else - {status, modules_or_errors, info.runtime_warnings ++ info.compile_warnings} + to_tuples = &Enum.map(&1, fn diag -> {diag.file, diag.position, diag.message} end) + + modules_or_errors = + if status == :ok, do: modules_or_errors, else: to_tuples.(modules_or_errors) + + {status, modules_or_errors, to_tuples.(info.runtime_warnings ++ info.compile_warnings)} end end @@ -633,19 +649,13 @@ defmodule Kernel.ParallelCompiler do state = %{state | timer_ref: timer_ref} spawn_workers(queue, spawned, waiting, files, result, warnings, errors, state) - {:diagnostic, type, file, location, message} -> - file = file && Path.absname(file) - message = :unicode.characters_to_binary(message) + {:diagnostic, %{severity: :warning} = diagnostic} -> + warnings = [diagnostic | warnings] + wait_for_messages(queue, spawned, waiting, files, result, warnings, errors, state) - case type do - :warning -> - warnings = [{file, location, message} | warnings] - wait_for_messages(queue, spawned, waiting, files, result, warnings, errors, state) - - :error -> - errors = [{file, location, message} | errors] - wait_for_messages(queue, spawned, waiting, files, result, warnings, errors, state) - end + {:diagnostic, %{severity: :error} = diagnostic} -> + errors = [diagnostic | errors] + wait_for_messages(queue, spawned, waiting, files, result, warnings, errors, state) {:file_ok, child_pid, ref, file, lexical} -> state.each_file.(file, lexical) @@ -816,7 +826,9 @@ defmodule Kernel.ParallelCompiler do "and that the modules they reference exist and are correctly named\n" ) - for {file, _, description} <- deadlock, do: {Path.absname(file), nil, description} + for {file, _, description} <- deadlock do + %{severity: :error, file: Path.absname(file), position: nil, message: description} + end end defp terminate(files) do @@ -842,7 +854,7 @@ defmodule Kernel.ParallelCompiler do line = get_line(file, reason, stack) file = Path.absname(file) message = :unicode.characters_to_binary(Kernel.CLI.format_error(kind, reason, stack)) - {file, line || 0, message} + %{file: file, position: line || 0, message: message, severity: :error} end defp get_line(_file, %{line: line, column: column}, _stack) diff --git a/lib/elixir/lib/module/parallel_checker.ex b/lib/elixir/lib/module/parallel_checker.ex index 58a571a49..2e52a6b98 100644 --- a/lib/elixir/lib/module/parallel_checker.ex +++ b/lib/elixir/lib/module/parallel_checker.ex @@ -147,24 +147,21 @@ defmodule Module.ParallelChecker do spawn({self(), checker}, module, file) end - modules = :gen_server.call(checker, :start, :infinity) - collect_results(modules, []) + count = :gen_server.call(checker, :start, :infinity) + collect_results(count, []) end - defp collect_results([], warnings) do - warnings + defp collect_results(0, diagnostics) do + diagnostics end - defp collect_results(modules, warnings) do + defp collect_results(count, diagnostics) do receive do - {:diagnostic, _type, file, location, message} -> - file = file && Path.absname(file) - message = :unicode.characters_to_binary(message) - warning = {file, location, message} - collect_results(modules, [warning | warnings]) - - {__MODULE__, _module, new_warnings} -> - collect_results(tl(modules), new_warnings ++ warnings) + {:diagnostic, diagnostic} -> + collect_results(count, [diagnostic | diagnostics]) + + {__MODULE__, _module, new_diagnostics} -> + collect_results(count - 1, new_diagnostics ++ diagnostics) end end @@ -294,10 +291,14 @@ defmodule Module.ParallelChecker do def emit_warnings(warnings) do Enum.flat_map(warnings, fn {module, warning, locations} -> message = module.format_warning(warning) - :elixir_errors.print_warning_no_diagnostic([message, ?\n, format_locations(locations)]) + + :elixir_errors.print_diagnostic_no_capture( + :warning, + [message, ?\n, format_locations(locations)] + ) Enum.map(locations, fn {file, line, _mfa} -> - {file, line, message} + %{severity: :warning, file: file, position: line, message: message} end) end) end @@ -485,7 +486,7 @@ defmodule Module.ParallelChecker do end end - {:reply, modules, run_checkers(state)} + {:reply, length(modules), run_checkers(state)} end def handle_call(:ets, _from, state) do diff --git a/lib/elixir/src/elixir_errors.erl b/lib/elixir/src/elixir_errors.erl index d3f5b28ef..758fffb67 100644 --- a/lib/elixir/src/elixir_errors.erl +++ b/lib/elixir/src/elixir_errors.erl @@ -7,7 +7,7 @@ -export([compile_error/1, compile_error/3, parse_error/5]). -export([function_error/4, module_error/4, file_error/4]). -export([erl_warn/3, file_warn/4]). --export([print_warning/4, print_warning_no_diagnostic/1, print_warning_no_diagnostic/3]). +-export([print_diagnostic/5, print_diagnostic_no_capture/2, print_diagnostic_no_capture/4]). -include("elixir.hrl"). -type location() :: non_neg_integer() | {non_neg_integer(), non_neg_integer()}. @@ -17,21 +17,22 @@ erl_warn(none, File, Warning) -> erl_warn(0, File, Warning); erl_warn(Location, File, Warning) when is_binary(File) -> send_diagnostic(warning, Location, File, Warning), - print_warning_no_diagnostic(Location, File, Warning). + print_diagnostic_no_capture(warning, Location, File, Warning). --spec print_warning_no_diagnostic(location(), unicode:chardata(), unicode:chardata()) -> ok. -print_warning_no_diagnostic(Location, File, Warning) -> - print_warning_no_diagnostic([Warning, "\n ", file_format(Location, File), $\n]). +-spec print_diagnostic_no_capture(warning | error, location(), unicode:chardata(), unicode:chardata()) -> ok. +print_diagnostic_no_capture(Type, Location, File, Message) -> + print_diagnostic_no_capture(Type, [Message, "\n ", file_format(Location, File), $\n]). --spec print_warning_no_diagnostic(unicode:chardata()) -> ok. -print_warning_no_diagnostic(Message) -> - io:put_chars(standard_error, [warning_prefix(), Message, $\n]), +-spec print_diagnostic_no_capture(warning | error, unicode:chardata()) -> ok. +print_diagnostic_no_capture(Type, Message) -> + io:put_chars(standard_error, [prefix(Type), Message, $\n]), ok. --spec print_warning(location(), unicode:chardata() | nil, unicode:chardata(), unicode:chardata()) -> ok. -print_warning(Location, File, DiagMessage, PrintMessage) when is_binary(File) or (File == nil) -> - send_diagnostic(warning, Location, File, DiagMessage), - print_warning_no_diagnostic(PrintMessage). +-spec print_diagnostic(warning | error, location(), unicode:chardata() | nil, unicode:chardata(), unicode:chardata()) -> ok. +print_diagnostic(Type, Location, File, DiagMessage, PrintMessage) + when is_binary(File) or (File == nil) -> + send_diagnostic(Type, Location, File, DiagMessage), + print_diagnostic_no_capture(Type, PrintMessage). %% Compilation error/warn handling. @@ -44,8 +45,8 @@ file_warn(Meta, E, Module, Desc) when is_list(Meta) -> true -> ok; false -> {EnvLine, EnvFile, EnvLocation} = env_format(Meta, E), - Warning = Module:format_error(Desc), - print_warning(EnvLine, EnvFile, Warning, [Warning, "\n ", EnvLocation, $\n]) + Message = Module:format_error(Desc), + print_diagnostic(warning, EnvLine, EnvFile, Message, [Message, "\n ", EnvLocation, $\n]) end. -spec file_error(list(), binary() | #{file := binary(), _ => _}, module(), any()) -> no_return(). @@ -78,8 +79,7 @@ function_error(Meta, Env, Module, Desc) -> print_error(Meta, Env, Module, Desc) -> {EnvLine, EnvFile, EnvLocation} = env_format(Meta, Env), Message = Module:format_error(Desc), - send_diagnostic(error, EnvLine, EnvFile, Message), - io:put_chars(standard_error, [error_prefix(), Message, "\n ", EnvLocation, $\n, $\n]), + print_diagnostic(error, EnvLine, EnvFile, Message, [Message, "\n ", EnvLocation, $\n]), ok. %% Compilation error. @@ -210,20 +210,27 @@ snippet(InputString, Location, StartLine, StartColumn) -> %% Helpers -send_diagnostic(Type, Line, File, Message) -> +send_diagnostic(Severity, Position, File, Message) -> case get(elixir_compiler_info) of undefined -> ok; - {CompilerPid, _} -> CompilerPid ! {diagnostic, Type, File, Line, Message} + {CompilerPid, _} -> + Diagnostic = #{ + severity => Severity, + file => if File =:= nil -> nil; true -> filename:absname(File) end, + position => Position, + message => unicode:characters_to_binary(Message) + }, + + CompilerPid ! {diagnostic, Diagnostic} end, ok. -warning_prefix() -> +prefix(warning) -> case application:get_env(elixir, ansi_enabled, false) of true -> <<"\e[33mwarning: \e[0m">>; false -> <<"warning: ">> - end. - -error_prefix() -> + end; +prefix(error) -> case application:get_env(elixir, ansi_enabled, false) of true -> <<"\e[31merror: \e[0m">>; false -> <<"error: ">> diff --git a/lib/mix/lib/mix/compilers/elixir.ex b/lib/mix/lib/mix/compilers/elixir.ex index 8accc7cba..c4a48742a 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 18 + @manifest_vsn 19 @checkpoint_vsn 2 import Record @@ -190,14 +190,14 @@ 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, :warning)) + all_warnings = previous_warnings ++ Enum.map(info_warnings, &diagnostic/1) unless_previous_warnings_as_errors(previous_warnings, opts, {:ok, all_warnings}) {: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 - errors = Enum.map(errors, &diagnostic(&1, :error)) - warnings = Enum.map(r_warnings ++ c_warnings, &diagnostic(&1, :warning)) + errors = Enum.map(errors, &diagnostic/1) + warnings = Enum.map(r_warnings ++ c_warnings, &diagnostic/1) all_warnings = Keyword.get(opts, :all_warnings, true) {:error, previous_warnings(sources, all_warnings) ++ warnings ++ errors} after @@ -697,21 +697,27 @@ defmodule Mix.Compilers.Elixir do runtime_warnings: runtime_warnings ) <- sources, file = Path.absname(source), - {location, message} <- compile_warnings ++ runtime_warnings do - warning = {file, location, message} + {position, message} <- compile_warnings ++ runtime_warnings do + diagnostic = %Mix.Task.Compiler.Diagnostic{ + severity: :warning, + file: file, + position: position, + message: message, + compiler_name: "Elixir" + } if print? do Mix.shell().print_app() - Kernel.ParallelCompiler.print_warning(warning) + Kernel.ParallelCompiler.print_diagnostic(diagnostic) end - diagnostic(warning, :warning) + diagnostic end end defp apply_warnings(sources, %{runtime_warnings: r_warnings, compile_warnings: c_warnings}) do - runtime_group = Enum.group_by(r_warnings, &elem(&1, 0), &{elem(&1, 1), elem(&1, 2)}) - compile_group = Enum.group_by(c_warnings, &elem(&1, 0), &{elem(&1, 1), elem(&1, 2)}) + runtime_group = Enum.group_by(r_warnings, & &1.file, &{&1.position, &1.message}) + compile_group = Enum.group_by(c_warnings, & &1.file, &{&1.position, &1.message}) for source( source: source_path, @@ -727,10 +733,10 @@ defmodule Mix.Compilers.Elixir do end end - defp diagnostic({file, location, message}, severity) do + defp diagnostic(%{file: file, position: position, message: message, severity: severity}) do %Mix.Task.Compiler.Diagnostic{ file: file, - position: location, + position: position, message: message, severity: severity, compiler_name: "Elixir" |