Skip to content

Commit 7470e5d

Browse files
authored
Emit parser warnings and errors on type (#986)
* remove not needed monitor * emit diagnostics from parser on edit * react to closing and opening * only parse ex and exs * handle eex files * use with_diagnostics * use common error handling handle unexpected errors * fix tests * add tests * fix paths on windows * don't assert on error message * eex extension * don't assert warnings on elixir < 1.15.3 * remove addressed todos
1 parent 02d203d commit 7470e5d

File tree

4 files changed

+434
-42
lines changed

4 files changed

+434
-42
lines changed

apps/language_server/lib/language_server/server.ex

Lines changed: 141 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ defmodule ElixirLS.LanguageServer.Server do
4949
:root_uri,
5050
:project_dir,
5151
:settings,
52+
parse_file_refs: %{},
53+
parser_diagnostics: %{},
5254
build_diagnostics: [],
5355
dialyzer_diagnostics: [],
5456
needs_build?: false,
@@ -270,6 +272,47 @@ defmodule ElixirLS.LanguageServer.Server do
270272
{:noreply, state}
271273
end
272274

275+
@impl GenServer
276+
def handle_info(
277+
{:parse_file, uri},
278+
%__MODULE__{source_files: source_files} = state
279+
) do
280+
old_diagnostics =
281+
state.build_diagnostics ++
282+
state.dialyzer_diagnostics ++ List.flatten(Map.values(state.parser_diagnostics))
283+
284+
parser_diagnostics =
285+
case source_files[uri] do
286+
%SourceFile{} = source_file ->
287+
file = SourceFile.Path.from_uri(uri)
288+
case parse_file(source_file.text, file) do
289+
[] ->
290+
Map.delete(state.parser_diagnostics, uri)
291+
292+
diagnostics ->
293+
Map.put(state.parser_diagnostics, uri, diagnostics)
294+
end
295+
296+
nil ->
297+
Map.delete(state.parser_diagnostics, uri)
298+
end
299+
300+
state = %{
301+
state
302+
| parser_diagnostics: parser_diagnostics,
303+
parse_file_refs: Map.delete(state.parse_file_refs, uri)
304+
}
305+
306+
publish_diagnostics(
307+
state.build_diagnostics ++
308+
state.dialyzer_diagnostics ++ List.flatten(Map.values(state.parser_diagnostics)),
309+
old_diagnostics,
310+
state.source_files
311+
)
312+
313+
{:noreply, state}
314+
end
315+
273316
## Helpers
274317

275318
defp handle_notification(notification("initialized"), state = %__MODULE__{}) do
@@ -381,13 +424,9 @@ defmodule ElixirLS.LanguageServer.Server do
381424
else
382425
source_file = %SourceFile{text: text, version: version}
383426

384-
Diagnostics.publish_file_diagnostics(
385-
uri,
386-
state.build_diagnostics ++ state.dialyzer_diagnostics,
387-
source_file
388-
)
389-
390-
put_in(state.source_files[uri], source_file)
427+
state = put_in(state.source_files[uri], source_file)
428+
# parse handler will emit diagnostics for opened file
429+
trigger_parse(state, uri, 0)
391430
end
392431
end
393432

@@ -402,6 +441,9 @@ defmodule ElixirLS.LanguageServer.Server do
402441
else
403442
awaiting_contracts = reject_awaiting_contracts(state.awaiting_contracts, uri)
404443

444+
# parse handler will clean up diagnostics and refs for closed file
445+
state = trigger_parse(state, uri, 0)
446+
405447
%{
406448
state
407449
| source_files: Map.delete(state.source_files, uri),
@@ -421,10 +463,14 @@ defmodule ElixirLS.LanguageServer.Server do
421463

422464
state
423465
else
424-
update_in(state.source_files[uri], fn source_file ->
425-
%SourceFile{source_file | version: version, dirty?: true}
426-
|> SourceFile.apply_content_changes(content_changes)
427-
end)
466+
state =
467+
update_in(state.source_files[uri], fn source_file ->
468+
%SourceFile{source_file | version: version, dirty?: true}
469+
|> SourceFile.apply_content_changes(content_changes)
470+
end)
471+
472+
# trigger parse with debounce
473+
trigger_parse(state, uri, 300)
428474
end
429475
end
430476

@@ -1039,7 +1085,10 @@ defmodule ElixirLS.LanguageServer.Server do
10391085
end
10401086

10411087
defp handle_build_result(status, diagnostics, state = %__MODULE__{}) do
1042-
old_diagnostics = state.build_diagnostics ++ state.dialyzer_diagnostics
1088+
old_diagnostics =
1089+
state.build_diagnostics ++
1090+
state.dialyzer_diagnostics ++ List.flatten(Map.values(state.parser_diagnostics))
1091+
10431092
state = put_in(state.build_diagnostics, diagnostics)
10441093

10451094
state =
@@ -1055,7 +1104,8 @@ defmodule ElixirLS.LanguageServer.Server do
10551104
end
10561105

10571106
publish_diagnostics(
1058-
state.build_diagnostics ++ state.dialyzer_diagnostics,
1107+
state.build_diagnostics ++
1108+
state.dialyzer_diagnostics ++ List.flatten(Map.values(state.parser_diagnostics)),
10591109
old_diagnostics,
10601110
state.source_files
10611111
)
@@ -1432,4 +1482,82 @@ defmodule ElixirLS.LanguageServer.Server do
14321482
state
14331483
end
14341484
end
1485+
1486+
defp trigger_parse(state, uri, debounce_timeout) do
1487+
if String.ends_with?(uri, [".ex", ".exs", ".eex"]) do
1488+
update_in(state.parse_file_refs[uri], fn old_ref ->
1489+
if old_ref do
1490+
Process.cancel_timer(old_ref, info: false)
1491+
end
1492+
1493+
Process.send_after(self(), {:parse_file, uri}, debounce_timeout)
1494+
end)
1495+
else
1496+
state
1497+
end
1498+
end
1499+
1500+
defp parse_file(text, file) do
1501+
{result, raw_diagnostics} =
1502+
Build.with_diagnostics([log: false], fn ->
1503+
try do
1504+
parser_options = [
1505+
file: file,
1506+
columns: true
1507+
]
1508+
1509+
if String.ends_with?(file, ".eex") do
1510+
EEx.compile_string(text,
1511+
file: file,
1512+
parser_options: parser_options
1513+
)
1514+
else
1515+
Code.string_to_quoted!(text, parser_options)
1516+
end
1517+
1518+
:ok
1519+
rescue
1520+
e in [EEx.SyntaxError, SyntaxError, TokenMissingError] ->
1521+
message = Exception.message(e)
1522+
1523+
diagnostic = %Mix.Task.Compiler.Diagnostic{
1524+
compiler_name: "ElixirLS",
1525+
file: file,
1526+
position: {e.line, e.column},
1527+
message: message,
1528+
severity: :error
1529+
}
1530+
1531+
{:error, diagnostic}
1532+
1533+
e ->
1534+
message = Exception.message(e)
1535+
1536+
diagnostic = %Mix.Task.Compiler.Diagnostic{
1537+
compiler_name: "ElixirLS",
1538+
file: file,
1539+
position: {1, 1},
1540+
message: message,
1541+
severity: :error
1542+
}
1543+
1544+
# e.g. https://github.com/elixir-lang/elixir/issues/12926
1545+
Logger.warning(
1546+
"Unexpected parser error, please report it to elixir project https://github.com/elixir-lang/elixir/issues\n" <>
1547+
Exception.format(:error, e, __STACKTRACE__)
1548+
)
1549+
1550+
{:error, diagnostic}
1551+
end
1552+
end)
1553+
1554+
warning_diagnostics =
1555+
raw_diagnostics
1556+
|> Enum.map(&Diagnostics.code_diagnostic/1)
1557+
1558+
case result do
1559+
:ok -> warning_diagnostics
1560+
{:error, diagnostic} -> [diagnostic | warning_diagnostics]
1561+
end
1562+
end
14351563
end

apps/language_server/mix.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ defmodule ElixirLS.LanguageServer.Mixfile do
3131
end
3232

3333
def application do
34-
[mod: {ElixirLS.LanguageServer, []}, extra_applications: [:logger]]
34+
[mod: {ElixirLS.LanguageServer, []}, extra_applications: [:logger, :eex]]
3535
end
3636

3737
defp deps do

apps/language_server/test/dialyzer_test.exs

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,6 @@ defmodule ElixirLS.LanguageServer.DialyzerTest do
112112
@tag slow: true, fixture: true
113113
test "only analyzes the changed files", %{server: server} do
114114
in_fixture(__DIR__, "dialyzer", fn ->
115-
file_c = SourceFile.Path.to_uri(Path.absname("lib/c.ex"))
116-
117115
capture_log(fn ->
118116
initialize(server, %{"dialyzerEnabled" => true, "dialyzerFormat" => "dialyxir_long"})
119117

@@ -149,8 +147,6 @@ defmodule ElixirLS.LanguageServer.DialyzerTest do
149147
}),
150148
3_000
151149

152-
assert_receive publish_diagnostics_notif(^file_c, []), 20_000
153-
154150
assert_receive notification("window/logMessage", %{
155151
"message" => "[ElixirLS Dialyzer] Done writing manifest" <> _
156152
}),
@@ -376,9 +372,6 @@ defmodule ElixirLS.LanguageServer.DialyzerTest do
376372

377373
assert_receive notification("window/logMessage", %{"message" => "Compile took" <> _}), 5000
378374

379-
assert_receive notification("textDocument/publishDiagnostics", %{"diagnostics" => []}),
380-
30000
381-
382375
Process.sleep(2000)
383376

384377
v2_text = """

0 commit comments

Comments
 (0)