Skip to content

Commit 76302b2

Browse files
committed
use version info when publishing diagnostics
store versions on build clear build diagnostics when build starts prefer parser diagnostics when file is updated
1 parent a5e484a commit 76302b2

File tree

3 files changed

+116
-69
lines changed

3 files changed

+116
-69
lines changed

apps/language_server/lib/language_server/diagnostics.ex

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -216,11 +216,11 @@ defmodule ElixirLS.LanguageServer.Diagnostics do
216216
JsonRpc.notify("textDocument/publishDiagnostics", message)
217217
end
218218

219-
def mixfile_diagnostic({file, line, message}, severity) when not is_nil(file) do
219+
def mixfile_diagnostic({file, position, message}, severity) when not is_nil(file) do
220220
%Mix.Task.Compiler.Diagnostic{
221221
compiler_name: "ElixirLS",
222222
file: file,
223-
position: line,
223+
position: position,
224224
message: message,
225225
severity: severity
226226
}
@@ -242,6 +242,20 @@ defmodule ElixirLS.LanguageServer.Diagnostics do
242242
}
243243
end
244244

245+
def error_to_diagnostic(:error, %kind{} = payload, _stacktrace, path, project_dir) when kind in [EEx.SyntaxError, SyntaxError, TokenMissingError, MismatchedDelimiterError] do
246+
path = SourceFile.Path.absname(path, project_dir)
247+
message = Exception.format_banner(:error, payload)
248+
249+
%Mix.Task.Compiler.Diagnostic{
250+
compiler_name: "ElixirLS",
251+
file: path,
252+
position: {payload.line, payload.column},
253+
message: message,
254+
severity: :error,
255+
details: payload
256+
}
257+
end
258+
245259
def error_to_diagnostic(kind, payload, stacktrace, path, project_dir) when not is_nil(path) do
246260
path = SourceFile.Path.absname(path, project_dir)
247261
message = Exception.format(kind, payload, stacktrace)
@@ -259,6 +273,7 @@ defmodule ElixirLS.LanguageServer.Diagnostics do
259273
%Mix.Task.Compiler.Diagnostic{
260274
compiler_name: "ElixirLS",
261275
file: path,
276+
# 0 means unknown
262277
position: line || 0,
263278
message: message,
264279
severity: :error,
@@ -292,6 +307,7 @@ defmodule ElixirLS.LanguageServer.Diagnostics do
292307
# https://microsoft.github.io/language-server-protocol/specifications/specification-3-16/#diagnostic
293308

294309
# position is a 1 based line number
310+
# 0 means unknown
295311
# we return a 0 length range at first non whitespace character in line
296312
defp range(line_start, source_file)
297313
when is_integer(line_start) and not is_nil(source_file) do
@@ -311,6 +327,7 @@ defmodule ElixirLS.LanguageServer.Diagnostics do
311327
{line_start - 1, SourceFile.elixir_character_to_lsp(line, start_idx)}
312328
end
313329
else
330+
# position unknown
314331
# return begin of the file
315332
{0, 0}
316333
end

apps/language_server/lib/language_server/parser.ex

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -180,8 +180,7 @@ defmodule ElixirLS.LanguageServer.Parser do
180180

181181
defp notify_diagnostics_updated(updated_files) do
182182
updated_files
183-
|> Enum.map(fn {_uri, %Context{diagnostics: diagnostics}} -> diagnostics end)
184-
|> List.flatten
183+
|> Map.new(fn {uri, %Context{diagnostics: diagnostics, parsed_version: version}} -> {uri, {version, diagnostics}} end)
185184
|> Server.parser_finished()
186185
end
187186

@@ -209,38 +208,44 @@ defmodule ElixirLS.LanguageServer.Parser do
209208
{:ok, ast}
210209
rescue
211210
e in [EEx.SyntaxError, SyntaxError, TokenMissingError, MismatchedDelimiterError] ->
212-
message = Exception.message(e)
211+
message = Exception.format_banner(:error, e)
213212

214213
diagnostic = %Mix.Task.Compiler.Diagnostic{
215214
compiler_name: "ElixirLS",
216215
file: file,
217216
position: {e.line, e.column},
218217
message: message,
219-
severity: :error
218+
severity: :error,
219+
details: e
220220
}
221221

222222
{:error, diagnostic}
223223

224-
e ->
225-
message = Exception.message(e)
224+
catch
225+
kind, err ->
226+
{payload, stacktrace} = Exception.blame(kind, err, __STACKTRACE__)
227+
228+
message = Exception.format(kind, payload, stacktrace)
226229

227230
diagnostic = %Mix.Task.Compiler.Diagnostic{
228231
compiler_name: "ElixirLS",
229232
file: file,
230-
position: {1, 1},
233+
# 0 means unknown
234+
position: 0,
231235
message: message,
232-
severity: :error
236+
severity: :error,
237+
details: payload
233238
}
234239

235240
# e.g. https://github.com/elixir-lang/elixir/issues/12926
236241
Logger.warning(
237242
"Unexpected parser error, please report it to elixir project https://github.com/elixir-lang/elixir/issues\n" <>
238-
Exception.format(:error, e, __STACKTRACE__)
243+
message
239244
)
240245

241246
JsonRpc.telemetry(
242247
"parser_error",
243-
%{"elixir_ls.parser_error" => Exception.format(:error, e, __STACKTRACE__)},
248+
%{"elixir_ls.parser_error" => message},
244249
%{}
245250
)
246251

@@ -250,7 +255,9 @@ defmodule ElixirLS.LanguageServer.Parser do
250255

251256
warning_diagnostics =
252257
raw_diagnostics
253-
|> Enum.map(&Diagnostics.code_diagnostic/1)
258+
|> Enum.map(fn raw ->
259+
Diagnostics.code_diagnostic(raw)
260+
end)
254261

255262
case result do
256263
{:ok, ast} -> {ast, warning_diagnostics}

apps/language_server/lib/language_server/server.ex

Lines changed: 79 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,9 @@ defmodule ElixirLS.LanguageServer.Server do
4949
:root_uri,
5050
:project_dir,
5151
:settings,
52-
parser_diagnostics: [],
52+
last_published_diagnostics_uris: [],
53+
parser_diagnostics: %{},
54+
document_versions_on_build: %{},
5355
build_diagnostics: [],
5456
dialyzer_diagnostics: [],
5557
needs_build?: false,
@@ -257,9 +259,8 @@ defmodule ElixirLS.LanguageServer.Server do
257259

258260
@impl GenServer
259261
def handle_cast({:parser_finished, diagnostics}, state = %__MODULE__{}) do
260-
old_diagnostics = current_diagnostics(state)
261262
state = %{state | parser_diagnostics: diagnostics}
262-
publish_diagnostics(state, old_diagnostics)
263+
|> publish_diagnostics()
263264

264265
{:noreply, state}
265266
end
@@ -1359,13 +1360,19 @@ defmodule ElixirLS.LanguageServer.Server do
13591360
end
13601361
end
13611362

1363+
document_versions_on_build = Map.new(state.source_files, fn {uri, source_file} -> {uri, source_file.version} end)
1364+
13621365
%__MODULE__{
13631366
state
13641367
| build_ref: build_ref,
13651368
needs_build?: false,
13661369
build_running?: true,
1367-
analysis_ready?: false
1370+
analysis_ready?: false,
1371+
build_diagnostics: [],
1372+
dialyzer_diagnostics: [],
1373+
document_versions_on_build: document_versions_on_build
13681374
}
1375+
|> publish_diagnostics()
13691376

13701377
true ->
13711378
# build already running, schedule another one
@@ -1407,38 +1414,32 @@ defmodule ElixirLS.LanguageServer.Server do
14071414
defp handle_build_result(status, diagnostics, state = %__MODULE__{}) do
14081415
do_sanity_check(state)
14091416

1410-
old_diagnostics = current_diagnostics(state)
1411-
1412-
state = put_in(state.build_diagnostics, diagnostics)
1413-
1414-
state =
1415-
cond do
1416-
state.needs_build? ->
1417-
state
1418-
1419-
status == :error or not dialyzer_enabled?(state) ->
1420-
put_in(state.dialyzer_diagnostics, [])
1421-
1422-
true ->
1423-
dialyze(state)
1424-
end
1417+
state = if state.needs_build? or status == :error or not dialyzer_enabled?(state) do
1418+
state
1419+
else
1420+
dialyze(state)
1421+
end
14251422

1426-
publish_diagnostics(state, old_diagnostics)
1423+
state = if state.needs_build? do
1424+
# do not publish diagnostics if we need another build
1425+
state
1426+
else
1427+
put_in(state.build_diagnostics, diagnostics)
1428+
|> publish_diagnostics()
1429+
end
14271430

14281431
%{state | full_build_done?: if(status == :ok, do: true, else: state.full_build_done?)}
14291432
end
14301433

14311434
defp handle_dialyzer_result(diagnostics, build_ref, state = %__MODULE__{}) do
1432-
old_diagnostics = current_diagnostics(state)
1433-
state = put_in(state.dialyzer_diagnostics, diagnostics)
1434-
1435-
publish_diagnostics(state, old_diagnostics)
1436-
14371435
# If these results were triggered by the most recent build and files are not dirty, then we know
14381436
# we're up to date and can release spec suggestions to the code lens provider
14391437
if build_ref == state.build_ref do
14401438
Logger.info("Dialyzer analysis is up to date")
14411439

1440+
state = put_in(state.dialyzer_diagnostics, diagnostics)
1441+
|> publish_diagnostics()
1442+
14421443
{dirty, not_dirty} =
14431444
state.awaiting_contracts
14441445
|> Enum.split_with(fn {_, uri} ->
@@ -1461,6 +1462,7 @@ defmodule ElixirLS.LanguageServer.Server do
14611462

14621463
%{state | analysis_ready?: true, awaiting_contracts: dirty}
14631464
else
1465+
# do not publish diagnostics if those results are not for the current build
14641466
state
14651467
end
14661468
end
@@ -1488,30 +1490,31 @@ defmodule ElixirLS.LanguageServer.Server do
14881490
end
14891491
end
14901492

1491-
defp current_diagnostics(state = %__MODULE__{}) do
1492-
state.build_diagnostics ++
1493-
state.dialyzer_diagnostics ++ state.parser_diagnostics
1494-
end
1495-
1496-
defp publish_diagnostics(state = %__MODULE__{project_dir: project_dir, source_files: source_files}, old_diagnostics) do
1497-
new_diagnostics = current_diagnostics(state)
1498-
# we need to publish diagnostics for all files in new_diagnostics
1499-
# to clear diagnostics we need to push empty sets for old_diagnostics
1500-
# in case we missed something or restarted and don't have old_diagnostics
1493+
defp publish_diagnostics(state = %__MODULE__{project_dir: project_dir, source_files: source_files, last_published_diagnostics_uris: last_published_diagnostics_uris}) do
1494+
# we need to publish diagnostics for all uris in current diagnostics
1495+
# to clear diagnostics we need to push empty sets for uris from last push
1496+
# in case we missed something or restarted and don't have old diagnostics uris
15011497
# we also push for all open files
1502-
uris_from_old_diagnostics =
1503-
old_diagnostics
1504-
|> Enum.reject(&is_nil(&1.file))
1505-
|> Enum.map(&(&1.file |> SourceFile.Path.to_uri(project_dir)))
15061498

1507-
new_diagnostics_by_uri =
1508-
new_diagnostics
1509-
|> Enum.reject(&is_nil(&1.file))
1510-
|> Enum.group_by(&(&1.file |> SourceFile.Path.to_uri(project_dir)))
1499+
uris_from_parser_diagnostics = Map.keys(state.parser_diagnostics)
1500+
1501+
filter_diagnostics_with_known_location = fn
1502+
%Mix.Task.Compiler.Diagnostic{file: file} when is_binary(file) ->
1503+
file != "nofile"
1504+
_ -> false
1505+
end
15111506

1507+
valid_build_and_dialyzer_diagnostics_by_uri = (state.build_diagnostics ++ state.dialyzer_diagnostics)
1508+
|> Enum.filter(filter_diagnostics_with_known_location)
1509+
|> Enum.group_by(fn
1510+
%Mix.Task.Compiler.Diagnostic{file: file} -> SourceFile.Path.to_uri(file, project_dir)
1511+
end)
1512+
1513+
uris_from_build_and_dialyzer_diagnostics = Map.keys(valid_build_and_dialyzer_diagnostics_by_uri)
1514+
15121515
invalid_diagnostics =
1513-
new_diagnostics
1514-
|> Enum.filter(&is_nil(&1.file))
1516+
(state.build_diagnostics ++ state.dialyzer_diagnostics)
1517+
|> Enum.reject(filter_diagnostics_with_known_location)
15151518

15161519
# TODO remove when we are sure diagnostic code is correct
15171520
if invalid_diagnostics != [] do
@@ -1528,21 +1531,39 @@ defmodule ElixirLS.LanguageServer.Server do
15281531
)
15291532
end
15301533

1531-
uris_from_new_diagnostics = Map.keys(new_diagnostics_by_uri)
1532-
15331534
uris_from_open_files = Map.keys(source_files)
15341535

15351536
uris_to_publish_diagnostics =
1536-
Enum.uniq(uris_from_old_diagnostics ++ uris_from_new_diagnostics ++ uris_from_open_files)
1537+
Enum.uniq(last_published_diagnostics_uris ++ uris_from_parser_diagnostics ++ uris_from_build_and_dialyzer_diagnostics ++ uris_from_open_files)
15371538

15381539
for uri <- uris_to_publish_diagnostics do
1539-
uri_diagnostics = Map.get(new_diagnostics_by_uri, uri, [])
1540-
# TODO store versions on compile/parse/dialyze?
1541-
version =
1542-
case source_files[uri] do
1543-
nil -> nil
1544-
file -> file.version
1545-
end
1540+
{parser_diagnostics_document_version, parser_diagnostics} = Map.get(state.parser_diagnostics, uri, {nil, []})
1541+
1542+
build_document_version = Map.get(state.document_versions_on_build, uri)
1543+
build_and_dialyzer_diagnostics = Map.get(valid_build_and_dialyzer_diagnostics_by_uri, uri, [])
1544+
1545+
{version, uri_diagnostics} = cond do
1546+
is_integer(parser_diagnostics_document_version) and is_integer(build_document_version) and parser_diagnostics_document_version > build_document_version ->
1547+
# document open and edited since build was triggered
1548+
# parser diagnostics are more recent, discard build and dialyzer
1549+
{parser_diagnostics_document_version, parser_diagnostics}
1550+
is_integer(parser_diagnostics_document_version) and is_integer(build_document_version) and parser_diagnostics_document_version == build_document_version ->
1551+
# document open and not edited since build was triggered
1552+
# parser build and dialyzer diagnostics are recent
1553+
# prefer them as they will likely contain the same warnings as parser ones
1554+
{build_document_version, if(build_and_dialyzer_diagnostics != [], do: build_and_dialyzer_diagnostics, else: parser_diagnostics)}
1555+
is_integer(parser_diagnostics_document_version) and is_nil(build_document_version) ->
1556+
# document open but was closed when build was triggered
1557+
# document could have been edited
1558+
# combine diagnostics
1559+
# they can be duplicated but it is not trivial to deduplicate
1560+
{parser_diagnostics_document_version, Enum.uniq(parser_diagnostics ++ build_and_dialyzer_diagnostics)}
1561+
true ->
1562+
# document closed
1563+
# don't emit parser diagnostics
1564+
# return build diagnostics with possibly nil version
1565+
{build_document_version, build_and_dialyzer_diagnostics}
1566+
end
15461567

15471568
Diagnostics.publish_file_diagnostics(
15481569
uri,
@@ -1551,8 +1572,10 @@ defmodule ElixirLS.LanguageServer.Server do
15511572
version
15521573
)
15531574

1554-
# TODO dump diagnostics to file
1575+
# TODO dump last_published_diagnostics_uris to file?
15551576
end
1577+
1578+
%{state | last_published_diagnostics_uris: uris_to_publish_diagnostics}
15561579
end
15571580

15581581
defp show_version_warnings do

0 commit comments

Comments
 (0)