Skip to content

Commit a0f0f75

Browse files
committed
Add Code.with_diagnostics/2, closes #12276
1 parent 77c95d5 commit a0f0f75

File tree

9 files changed

+293
-140
lines changed

9 files changed

+293
-140
lines changed

lib/elixir/lib/code.ex

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,21 @@ defmodule Code do
194194
"""
195195
@type binding :: [{atom() | tuple(), any}]
196196

197+
@typedoc """
198+
Diagnostics returned by the compiler and code evaluation.
199+
"""
200+
@type diagnostic(severity) :: %{
201+
file: Path.t(),
202+
severity: severity,
203+
message: String.t(),
204+
position: position,
205+
stacktrace: Exception.stacktrace()
206+
}
207+
208+
@typedoc "The line. 0 indicates no line."
209+
@type line() :: non_neg_integer()
210+
@type position() :: line() | {pos_integer(), column :: non_neg_integer}
211+
197212
@boolean_compiler_options [
198213
:docs,
199214
:debug_info,
@@ -533,6 +548,51 @@ defmodule Code do
533548
end)
534549
end
535550

551+
@doc """
552+
Executes the given `fun` and capture all diagnostics.
553+
554+
Diagnostics are warnings and errors emitted by the compiler
555+
and by functions such as `IO.warn/2`.
556+
557+
## Options
558+
559+
* `:log` - if the diagnostics should be logged as they happen.
560+
Defaults to `false`.
561+
562+
"""
563+
@spec with_diagnostics(keyword(), (-> result)) :: {result, [diagnostic(:warning | :error)]}
564+
when result: term()
565+
def with_diagnostics(opts \\ [], fun) do
566+
value = :erlang.get(:elixir_code_diagnostics)
567+
log = Keyword.get(opts, :log, false)
568+
:erlang.put(:elixir_code_diagnostics, {[], log})
569+
570+
try do
571+
result = fun.()
572+
{diagnostics, _log?} = :erlang.get(:elixir_code_diagnostics)
573+
{result, Enum.reverse(diagnostics)}
574+
after
575+
if value == :undefined do
576+
:erlang.erase(:elixir_code_diagnostics)
577+
else
578+
:erlang.put(:elixir_code_diagnostics, value)
579+
end
580+
end
581+
end
582+
583+
@doc """
584+
Prints a diagnostic into the standard error.
585+
586+
A diagnostic is either returned by `Kernel.ParallelCompiler`
587+
or by `Code.with_diagnostics/2`.
588+
"""
589+
@doc since: "1.15.0"
590+
@spec print_diagnostic(diagnostic(:warning | :error)) :: :ok
591+
def print_diagnostic(diagnostic) do
592+
:elixir_errors.print_diagnostic(diagnostic)
593+
:ok
594+
end
595+
536596
@doc ~S"""
537597
Formats the given code `string`.
538598
@@ -876,7 +936,7 @@ defmodule Code do
876936
warn_on_unnecessary_quotes: false,
877937
literal_encoder: &{:ok, {:__block__, &2, [&1]}},
878938
token_metadata: true,
879-
emit_warnings: false
939+
warnings: false
880940
] ++ opts
881941

882942
{forms, comments} = string_to_quoted_with_comments!(string, to_quoted_opts)
@@ -1688,7 +1748,7 @@ defmodule Code do
16881748
16891749
## Examples
16901750
1691-
iex> Code.ensure_loaded?(Atom)
1751+
iex> Code.ensure_loaded?(String)
16921752
true
16931753
16941754
"""

lib/elixir/lib/code/fragment.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1054,6 +1054,6 @@ defmodule Code.Fragment do
10541054
opts =
10551055
Keyword.take(opts, [:file, :line, :column, :columns, :token_metadata, :literal_encoder])
10561056

1057-
Code.string_to_quoted(fragment, [cursor_completion: true, emit_warnings: false] ++ opts)
1057+
Code.string_to_quoted(fragment, [cursor_completion: true, warnings: false] ++ opts)
10581058
end
10591059
end

lib/elixir/lib/kernel/parallel_compiler.ex

Lines changed: 10 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,14 @@ defmodule Kernel.ParallelCompiler do
33
A module responsible for compiling and requiring files in parallel.
44
"""
55

6-
@typedoc "The line. 0 indicates no line."
7-
@type line() :: non_neg_integer()
8-
@type position() :: line() | {pos_integer(), column :: non_neg_integer}
9-
10-
@type diagnostic(severity) :: %{
11-
file: Path.t(),
12-
severity: severity,
13-
message: String.t(),
14-
position: position,
15-
stacktrace: Exception.stacktrace()
16-
}
17-
186
@type info :: %{
19-
runtime_warnings: [diagnostic(:warning)],
20-
compile_warnings: [diagnostic(:warning)]
7+
runtime_warnings: [Code.diagnostic(:warning)],
8+
compile_warnings: [Code.diagnostic(:warning)]
219
}
2210

2311
# Deprecated types
24-
@type warning() :: {file :: Path.t(), position(), message :: String.t()}
25-
@type error() :: {file :: Path.t(), position(), message :: String.t()}
12+
@type warning() :: {file :: Path.t(), Code.position(), message :: String.t()}
13+
@type error() :: {file :: Path.t(), Code.position(), message :: String.t()}
2614

2715
@doc """
2816
Starts a task for parallel compilation.
@@ -71,7 +59,7 @@ defmodule Kernel.ParallelCompiler do
7159
resolved.
7260
7361
It returns `{:ok, modules, warnings}` or `{:error, errors, warnings}`
74-
by default but we recommend using `return_maps: true` so it returns
62+
by default but we recommend using `return_diagnostics: true` so it returns
7563
diagnostics as maps as well as a map of compilation information.
7664
The map has the shape of:
7765
@@ -115,14 +103,14 @@ defmodule Kernel.ParallelCompiler do
115103
116104
* `:beam_timestamp` - the modification timestamp to give all BEAM files
117105
118-
* `:return_maps` (since v1.15.0) - returns maps with information instead of
106+
* `:return_diagnostics` (since v1.15.0) - returns maps with information instead of
119107
a list of warnings and returns diagnostics as maps instead of tuples
120108
121109
"""
122110
@doc since: "1.6.0"
123111
@spec compile([Path.t()], keyword()) ::
124112
{:ok, [atom], [warning] | info()}
125-
| {:error, [error] | [diagnostic(:error)], [warning] | info()}
113+
| {:error, [error] | [Code.diagnostic(:error)], [warning] | info()}
126114
def compile(files, options \\ []) when is_list(options) do
127115
spawn_workers(files, :compile, options)
128116
end
@@ -135,7 +123,7 @@ defmodule Kernel.ParallelCompiler do
135123
@doc since: "1.6.0"
136124
@spec compile_to_path([Path.t()], Path.t(), keyword()) ::
137125
{:ok, [atom], [warning] | info()}
138-
| {:error, [error] | [diagnostic(:error)], [warning] | info()}
126+
| {:error, [error] | [Code.diagnostic(:error)], [warning] | info()}
139127
def compile_to_path(files, path, options \\ []) when is_binary(path) and is_list(options) do
140128
spawn_workers(files, {:compile, path}, options)
141129
end
@@ -147,7 +135,7 @@ defmodule Kernel.ParallelCompiler do
147135
automatically solved between files.
148136
149137
It returns `{:ok, modules, warnings}` or `{:error, errors, warnings}`
150-
by default but we recommend using `return_maps: true` so it returns
138+
by default but we recommend using `return_diagnostics: true` so it returns
151139
diagnostics as maps as well as a map of compilation information.
152140
The map has the shape of:
153141
@@ -172,14 +160,6 @@ defmodule Kernel.ParallelCompiler do
172160
spawn_workers(files, :require, options)
173161
end
174162

175-
@doc """
176-
Prints a diagnostic returned by the compiler into stderr.
177-
"""
178-
@doc since: "1.15.0"
179-
def print_diagnostic(diagnostic) do
180-
:elixir_errors.print_diagnostic(diagnostic)
181-
end
182-
183163
@doc false
184164
# TODO: Deprecate me on Elixir v1.19
185165
def print_warning({file, location, warning}) do
@@ -239,7 +219,7 @@ defmodule Kernel.ParallelCompiler do
239219
end
240220

241221
# TODO: Require this to be set from Elixir v1.19
242-
if Keyword.get(options, :return_maps, false) do
222+
if Keyword.get(options, :return_diagnostics, false) do
243223
{status, modules_or_errors, info}
244224
else
245225
to_tuples = &Enum.map(&1, fn diag -> {diag.file, diag.position, diag.message} end)

lib/elixir/lib/module/parallel_checker.ex

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ defmodule Module.ParallelChecker do
4848
@doc """
4949
Spawns a process that runs the parallel checker.
5050
"""
51-
def spawn({pid, checker}, module, info) do
51+
def spawn({pid, checker}, module, info, log?) do
5252
ref = make_ref()
5353

5454
spawned =
@@ -79,7 +79,7 @@ defmodule Module.ParallelChecker do
7979

8080
warnings =
8181
if module_map do
82-
check_module(module_map, {checker, ets})
82+
check_module(module_map, {checker, ets}, log?)
8383
else
8484
[]
8585
end
@@ -143,12 +143,23 @@ defmodule Module.ParallelChecker do
143143
"""
144144
@spec verify(pid(), [{module(), Path.t()}]) :: [warning()]
145145
def verify(checker, runtime_files) do
146+
value = :erlang.get(:elixir_code_diagnostics)
147+
log? = not match?({_, false}, value)
148+
146149
for {module, file} <- runtime_files do
147-
spawn({self(), checker}, module, file)
150+
spawn({self(), checker}, module, file, log?)
148151
end
149152

150153
count = :gen_server.call(checker, :start, :infinity)
151-
collect_results(count, [])
154+
diagnostics = collect_results(count, [])
155+
156+
case :erlang.get(:elixir_code_diagnostics) do
157+
:undefined -> :ok
158+
{tail, true} -> :erlang.put(:elixir_code_diagnostics, {diagnostics ++ tail, true})
159+
{tail, false} -> :erlang.put(:elixir_code_diagnostics, {diagnostics ++ tail, false})
160+
end
161+
162+
diagnostics
152163
end
153164

154165
defp collect_results(0, diagnostics) do
@@ -221,7 +232,7 @@ defmodule Module.ParallelChecker do
221232

222233
## Module checking
223234

224-
defp check_module(module_map, cache) do
235+
defp check_module(module_map, cache, log?) do
225236
%{
226237
module: module,
227238
file: file,
@@ -252,7 +263,7 @@ defmodule Module.ParallelChecker do
252263
|> Module.Types.warnings(file, definitions, no_warn_undefined, cache)
253264
|> Kernel.++(behaviour_warnings)
254265
|> group_warnings()
255-
|> emit_warnings()
266+
|> emit_warnings(log?)
256267

257268
module_map
258269
|> Map.get(:after_verify, [])
@@ -278,7 +289,7 @@ defmodule Module.ParallelChecker do
278289

279290
## Warning helpers
280291

281-
def group_warnings(warnings) do
292+
defp group_warnings(warnings) do
282293
warnings
283294
|> Enum.reduce(%{}, fn {module, warning, location}, acc ->
284295
locations = MapSet.new([location])
@@ -288,11 +299,11 @@ defmodule Module.ParallelChecker do
288299
|> Enum.sort()
289300
end
290301

291-
def emit_warnings(warnings) do
302+
defp emit_warnings(warnings, log?) do
292303
Enum.flat_map(warnings, fn {module, warning, locations} ->
293304
message = module.format_warning(warning)
294305
diagnostics = Enum.map(locations, &to_diagnostic(message, &1))
295-
:elixir_errors.print_warning([message, ?\n, format_stacktraces(diagnostics)])
306+
log? and :elixir_errors.print_warning([message, ?\n, format_stacktraces(diagnostics)])
296307
diagnostics
297308
end)
298309
end
@@ -317,7 +328,7 @@ defmodule Module.ParallelChecker do
317328
severity: :warning,
318329
file: file,
319330
position: line,
320-
message: message,
331+
message: IO.iodata_to_binary(message),
321332
stacktrace: [to_stacktrace(file, line, mfa)]
322333
}
323334
end

lib/elixir/src/elixir.erl

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -426,7 +426,7 @@ string_to_tokens(String, StartLine, StartColumn, File, Opts) when is_integer(Sta
426426
{ok, _Line, _Column, [], Tokens} ->
427427
{ok, Tokens};
428428
{ok, _Line, _Column, Warnings, Tokens} ->
429-
(lists:keyfind(emit_warnings, 1, Opts) /= {emit_warnings, false}) andalso
429+
(lists:keyfind(warnings, 1, Opts) /= {warnings, false}) andalso
430430
[elixir_errors:erl_warn(L, File, M) || {L, M} <- lists:reverse(Warnings)],
431431
{ok, Tokens};
432432
{error, {Line, Column, {ErrorPrefix, ErrorSuffix}, Token}, _Rest, _Warnings, _SoFar} ->
@@ -486,8 +486,8 @@ to_binary(Atom) when is_atom(Atom) -> atom_to_binary(Atom).
486486

487487
handle_parsing_opts(File, Opts) ->
488488
WarningFile =
489-
case lists:keyfind(emit_warnings, 1, Opts) of
490-
{emit_warnings, false} -> nil;
489+
case lists:keyfind(warnings, 1, Opts) of
490+
{warnings, false} -> nil;
491491
_ -> File
492492
end,
493493
LiteralEncoder =

lib/elixir/src/elixir_errors.erl

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ print_diagnostic(#{severity := Severity, message := Message, stacktrace := Stack
3333
[["\n ", 'Elixir.Exception':format_stacktrace_entry(E)] || E <- Stacktrace]
3434
end,
3535
io:put_chars(standard_error, [prefix(Severity), Message, Location, "\n\n"]),
36-
ok.
36+
Diagnostic.
3737

3838
emit_diagnostic(Severity, Position, File, Message, Stacktrace) ->
3939
Diagnostic = #{
@@ -44,7 +44,11 @@ emit_diagnostic(Severity, Position, File, Message, Stacktrace) ->
4444
stacktrace => Stacktrace
4545
},
4646

47-
print_diagnostic(Diagnostic),
47+
case get(elixir_code_diagnostics) of
48+
undefined -> print_diagnostic(Diagnostic);
49+
{Tail, true} -> put(elixir_code_diagnostics, {[print_diagnostic(Diagnostic) | Tail], true});
50+
{Tail, false} -> put(elixir_code_diagnostics, {[Diagnostic | Tail], false})
51+
end,
4852

4953
case get(elixir_compiler_info) of
5054
undefined -> ok;

lib/elixir/src/elixir_module.erl

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -506,13 +506,20 @@ beam_location(ModuleAsCharlist) ->
506506
checker_info() ->
507507
case get(elixir_checker_info) of
508508
undefined -> undefined;
509-
_ -> 'Elixir.Module.ParallelChecker':get()
509+
_ ->
510+
Log =
511+
case erlang:get(elixir_code_diagnostics) of
512+
{_, false} -> false;
513+
_ -> true
514+
end,
515+
516+
{'Elixir.Module.ParallelChecker':get(), Log}
510517
end.
511518

512519
spawn_parallel_checker(undefined, _Module, _ModuleMap) ->
513520
nil;
514-
spawn_parallel_checker(CheckerInfo, Module, ModuleMap) ->
515-
'Elixir.Module.ParallelChecker':spawn(CheckerInfo, Module, ModuleMap).
521+
spawn_parallel_checker({CheckerInfo, Log}, Module, ModuleMap) ->
522+
'Elixir.Module.ParallelChecker':spawn(CheckerInfo, Module, ModuleMap, Log).
516523

517524
make_module_available(Module, Binary) ->
518525
case get(elixir_module_binaries) of

0 commit comments

Comments
 (0)