Skip to content

Commit b30939c

Browse files
committed
diagnostics improvements
added diagnostic tags (deprecated and unused) added related information get mix compiler diagnostic info from details if present (elixir 1.16) fix diagnostic position in a few cases disable attribute value read in tracer as it breaks attribute warnings
1 parent 759d837 commit b30939c

File tree

2 files changed

+186
-56
lines changed

2 files changed

+186
-56
lines changed

apps/language_server/lib/language_server/diagnostics.ex

Lines changed: 182 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,21 @@ defmodule ElixirLS.LanguageServer.Diagnostics do
77

88
def normalize(diagnostics, root_path, mixfile) do
99
for %Mix.Task.Compiler.Diagnostic{} = diagnostic <- diagnostics do
10-
{type, file, position, stacktrace} =
11-
extract_message_info(diagnostic.message, root_path)
10+
case diagnostic |> dbg do
11+
%Mix.Task.Compiler.Diagnostic{details: payload = %_{line: _}, compiler_name: compiler_name} ->
12+
# remove stacktrace
13+
message = Exception.format_banner(:error, payload)
14+
compiler_name = if compiler_name == "Elixir", do: "ElixirLS", else: compiler_name
15+
%Mix.Task.Compiler.Diagnostic{diagnostic | message: message, compiler_name: compiler_name}
1216

13-
diagnostic
14-
|> maybe_update_file(file, mixfile)
15-
|> maybe_update_position(type, position, stacktrace)
17+
_ ->
18+
{type, file, position, stacktrace} =
19+
extract_message_info(diagnostic.message, root_path)
20+
21+
diagnostic
22+
|> maybe_update_file(file, mixfile)
23+
|> maybe_update_position(type, position, stacktrace)
24+
end
1625
end
1726
end
1827

@@ -175,47 +184,6 @@ defmodule ElixirLS.LanguageServer.Diagnostics do
175184
end)
176185
end
177186

178-
def publish_file_diagnostics(uri, uri_diagnostics, source_file, version) do
179-
diagnostics_json =
180-
for diagnostic <- uri_diagnostics do
181-
severity =
182-
case diagnostic.severity do
183-
:error -> 1
184-
:warning -> 2
185-
:information -> 3
186-
:hint -> 4
187-
end
188-
189-
message =
190-
case diagnostic.message do
191-
m when is_binary(m) -> m
192-
m when is_list(m) -> m |> Enum.join("\n")
193-
end
194-
195-
%{
196-
"message" => message,
197-
"severity" => severity,
198-
"range" => range(diagnostic.position, source_file),
199-
"source" => diagnostic.compiler_name
200-
}
201-
end
202-
|> Enum.sort_by(& &1["range"]["start"])
203-
204-
message = %{
205-
"uri" => uri,
206-
"diagnostics" => diagnostics_json
207-
}
208-
209-
message =
210-
if is_integer(version) do
211-
Map.put(message, "version", version)
212-
else
213-
message
214-
end
215-
216-
JsonRpc.notify("textDocument/publishDiagnostics", message)
217-
end
218-
219187
def mixfile_diagnostic({file, position, message}, severity) when not is_nil(file) do
220188
%Mix.Task.Compiler.Diagnostic{
221189
compiler_name: "ElixirLS",
@@ -242,15 +210,19 @@ defmodule ElixirLS.LanguageServer.Diagnostics do
242210
}
243211
end
244212

245-
def error_to_diagnostic(:error, %kind{} = payload, _stacktrace, path, project_dir)
246-
when kind in [EEx.SyntaxError, SyntaxError, TokenMissingError, MismatchedDelimiterError] do
213+
def error_to_diagnostic(:error, %_{line: _} = payload, _stacktrace, path, project_dir) do
247214
path = SourceFile.Path.absname(path, project_dir)
248215
message = Exception.format_banner(:error, payload)
249216

217+
position = case payload do
218+
%{line: line, column: column} -> {line, column}
219+
%{line: line} -> line
220+
end
221+
250222
%Mix.Task.Compiler.Diagnostic{
251223
compiler_name: "ElixirLS",
252224
file: path,
253-
position: {payload.line, payload.column},
225+
position: position,
254226
message: message,
255227
severity: :error,
256228
details: payload
@@ -303,6 +275,161 @@ defmodule ElixirLS.LanguageServer.Diagnostics do
303275
}
304276
end
305277

278+
def publish_file_diagnostics(uri, uri_diagnostics, source_file, version) do
279+
diagnostics_json =
280+
for diagnostic <- uri_diagnostics do
281+
severity =
282+
case diagnostic.severity do
283+
:error -> 1
284+
:warning -> 2
285+
:information -> 3
286+
:hint -> 4
287+
end
288+
289+
message =
290+
case diagnostic.message do
291+
m when is_binary(m) -> m
292+
m when is_list(m) -> m |> Enum.join("\n")
293+
end
294+
295+
%{
296+
"message" => message,
297+
"severity" => severity,
298+
"range" => range(diagnostic.position, source_file),
299+
"source" => diagnostic.compiler_name,
300+
"relatedInformation" => build_related_information(diagnostic, uri, source_file),
301+
"tags" => get_tags(diagnostic)
302+
}
303+
end
304+
|> Enum.sort_by(& &1["range"]["start"])
305+
306+
message = %{
307+
"uri" => uri,
308+
"diagnostics" => diagnostics_json
309+
}
310+
311+
message =
312+
if is_integer(version) do
313+
Map.put(message, "version", version)
314+
else
315+
message
316+
end
317+
318+
JsonRpc.notify("textDocument/publishDiagnostics", message)
319+
end
320+
321+
defp get_tags(diagnostic) do
322+
unused = if Regex.match?(~r/unused|no effect/u, diagnostic.message) do
323+
[1]
324+
else
325+
[]
326+
end
327+
deprecated = if Regex.match?(~r/deprecated/u, diagnostic.message) do
328+
[2]
329+
else
330+
[]
331+
end
332+
333+
unused ++ deprecated
334+
end
335+
336+
defp get_related_information_description(description, uri, source_file) do
337+
line = case Regex.run(
338+
~r/line (\d+)/u,
339+
description
340+
) do
341+
[_, line] -> String.to_integer(line)
342+
_ -> nil
343+
end
344+
345+
message = case String.split(description, "hint: ") do
346+
[_, hint] -> hint
347+
_ -> description
348+
end
349+
350+
if line do
351+
[
352+
%{
353+
"location" => %{
354+
"uri" => uri,
355+
"range" => range(line, source_file)
356+
},
357+
"message" => message
358+
}
359+
]
360+
else
361+
[]
362+
end
363+
end
364+
365+
defp get_related_information_message(message, uri, source_file) do
366+
line = case Regex.run(
367+
~r/line (\d+)/u,
368+
message
369+
) do
370+
[_, line] -> String.to_integer(line)
371+
_ -> nil
372+
end
373+
374+
if line do
375+
[
376+
%{
377+
"location" => %{
378+
"uri" => uri,
379+
"range" => range(line, source_file)
380+
},
381+
"message" => "related"
382+
}
383+
]
384+
else
385+
[]
386+
end
387+
end
388+
389+
defp build_related_information(diagnostic, uri, source_file) do
390+
case diagnostic.details do
391+
# for backwards compatibility with elixir < 1.16
392+
%kind{} = payload when kind == MismatchedDelimiterError ->
393+
[
394+
%{
395+
"location" => %{
396+
"uri" => uri,
397+
"range" => range({payload.line, payload.column - 1, payload.line, payload.column - 1 + String.length(to_string(payload.opening_delimiter))}, source_file)
398+
},
399+
"message" => "opening delimiter: #{payload.opening_delimiter}"
400+
},
401+
%{
402+
"location" => %{
403+
"uri" => uri,
404+
"range" => range({payload.end_line, payload.end_column - 1, payload.end_line, payload.end_column - 1 + String.length(to_string(payload.closing_delimiter))}, source_file)
405+
},
406+
"message" => "closing delimiter: #{payload.closing_delimiter}"
407+
}
408+
]
409+
%kind{end_line: end_line, opening_delimiter: opening_delimiter} = payload when kind == TokenMissingError and not is_nil(opening_delimiter) ->
410+
message = String.split(payload.description, "hint: ") |> hd
411+
[
412+
%{
413+
"location" => %{
414+
"uri" => uri,
415+
"range" => range({payload.line, payload.column - 1, payload.line, payload.column - 1 + String.length(to_string(payload.opening_delimiter))}, source_file)
416+
},
417+
"message" => "opening delimiter: #{payload.opening_delimiter}"
418+
},
419+
%{
420+
"location" => %{
421+
"uri" => uri,
422+
"range" => range(end_line, source_file)
423+
},
424+
"message" => message
425+
}
426+
] ++ get_related_information_description(payload.description, uri, source_file)
427+
%{description: description} ->
428+
get_related_information_description(description, uri, source_file)
429+
_ -> []
430+
end ++ get_related_information_message(diagnostic.message, uri, source_file)
431+
end
432+
306433
# for details see
307434
# https://hexdocs.pm/mix/1.13.4/Mix.Task.Compiler.Diagnostic.html#t:position/0
308435
# https://microsoft.github.io/language-server-protocol/specifications/specification-3-16/#diagnostic
@@ -351,11 +478,11 @@ defmodule ElixirLS.LanguageServer.Diagnostics do
351478
when not is_nil(source_file) do
352479
# some diagnostics are broken
353480
line_start = line_start || 1
354-
char_start = char_start || 1
481+
char_start = char_start || 0
355482
lines = SourceFile.lines(source_file)
356483
# elixir_position_to_lsp will handle positions outside file range
357484
{line_start_lsp, char_start_lsp} =
358-
SourceFile.elixir_position_to_lsp(lines, {line_start, char_start - 1})
485+
SourceFile.elixir_position_to_lsp(lines, {line_start, char_start + 1})
359486

360487
%{
361488
"start" => %{
@@ -375,18 +502,18 @@ defmodule ElixirLS.LanguageServer.Diagnostics do
375502
when not is_nil(source_file) do
376503
# some diagnostics are broken
377504
line_start = line_start || 1
378-
char_start = char_start || 1
505+
char_start = char_start || 0
379506

380507
line_end = line_end || 1
381-
char_end = char_end || 1
508+
char_end = char_end || 0
382509

383510
lines = SourceFile.lines(source_file)
384511
# elixir_position_to_lsp will handle positions outside file range
385512
{line_start_lsp, char_start_lsp} =
386-
SourceFile.elixir_position_to_lsp(lines, {line_start, char_start - 1})
513+
SourceFile.elixir_position_to_lsp(lines, {line_start, char_start + 1})
387514

388515
{line_end_lsp, char_end_lsp} =
389-
SourceFile.elixir_position_to_lsp(lines, {line_end, char_end - 1})
516+
SourceFile.elixir_position_to_lsp(lines, {line_end, char_end + 1})
390517

391518
%{
392519
"start" => %{

apps/language_server/lib/language_server/tracer.ex

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,10 @@ defmodule ElixirLS.LanguageServer.Tracer do
352352
attributes =
353353
if Version.match?(System.version(), ">= 1.13.0-dev") do
354354
for name <- apply(Module, :attributes_in, [module]) do
355-
{name, Module.get_attribute(module, name)}
355+
# reading attribute value here breaks unused attributes warnings
356+
# https://github.com/elixir-lang/elixir/issues/13168
357+
# {name, Module.get_attribute(module, name)}
358+
{name, nil}
356359
end
357360
else
358361
[]

0 commit comments

Comments
 (0)