Skip to content

Commit

Permalink
OTP 26 Incremental dialyzer (#1081)
Browse files Browse the repository at this point in the history
* fix typo

* PoC incremental dialyzer

* acquire build lock

trigger next analysis

* implement suggest contracts

* apply some improvements introduced since the initial PoC

* add error reporting

* add env

* update readme

* remove not needed erts app

* fix suggest contracts

* fix diagnostic emit

* we no longer consolidate protocols

* make old dialyzer tests pass

* add tests

* add OTP 26 dialyzer options
  • Loading branch information
lukaszsamson committed Mar 15, 2024
1 parent a6f5d8a commit 06178f5
Show file tree
Hide file tree
Showing 11 changed files with 886 additions and 117 deletions.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -306,8 +306,9 @@ With Dialyzer integration enabled, ElixirLS will build an index of symbols (modu
Below is a list of configuration options supported by the ElixirLS language server. Please refer to your editor's documentation to determine how to configure language servers.

<dl>
<dt>elixirLS.autoBuild</dt><dd>Trigger ElixirLS build when code is saved.</dd>
<dt>elixirLS.dialyzerEnabled</dt><dd>Run ElixirLS's rapid Dialyzer when code is saved.</dd>
<dt>elixirLS.autoBuild</dt><dd>Trigger ElixirLS build when code is saved</dd>
<dt>elixirLS.dialyzerEnabled</dt><dd>Run ElixirLS's rapid Dialyzer when code is saved</dd>
<dt>elixirLS.incrementalDialyzer</dt><dd>Use OTP incremental dialyzer (available on OTP 26+)</dd>
<dt>elixirLS.dialyzerWarnOpts</dt><dd>Dialyzer options to enable or disable warnings - See Dialyzer's documentation for options. Note that the <code>race_conditions</code> option is unsupported.</dd>
<dt>elixirLS.dialyzerFormat</dt><dd>Formatter to use for Dialyzer warnings</dd>
<dt>elixirLS.envVariables</dt><dd>Environment variables to use for compilation</dd>
Expand Down
22 changes: 11 additions & 11 deletions apps/language_server/lib/language_server/dialyzer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ defmodule ElixirLS.LanguageServer.Dialyzer do
deps_path: deps_path
}

trigger_analyze(state)
maybe_trigger_analyze(state)
else
state
end
Expand Down Expand Up @@ -268,8 +268,8 @@ defmodule ElixirLS.LanguageServer.Dialyzer do
}
end

defp trigger_analyze(%{analysis_pid: nil} = state), do: do_analyze(state)
defp trigger_analyze(state), do: state
defp maybe_trigger_analyze(%{analysis_pid: nil} = state), do: do_analyze(state)
defp maybe_trigger_analyze(state), do: state

defp update_stale(md5, removed_files, file_changes, timestamp, project_dir, build_path) do
prev_paths = Map.keys(md5) |> MapSet.new()
Expand Down Expand Up @@ -607,7 +607,7 @@ defmodule ElixirLS.LanguageServer.Dialyzer do
%Diagnostics{
compiler_name: "ElixirLS Dialyzer",
file: source_file,
position: normalize_postion(position),
position: normalize_position(position),
message: warning_message(data, warning_format),
severity: :warning,
details: data
Expand All @@ -617,25 +617,25 @@ defmodule ElixirLS.LanguageServer.Dialyzer do

# up until OTP 23 position was line :: non_negative_integer
# starting from OTP 24 it is erl_anno:location() :: line | {line, column}
defp normalize_postion({line, column}) when line > 0 do
def normalize_position({line, column}) when line > 0 do
{line, column}
end

# 0 means unknown line
defp normalize_postion(line) when line >= 0 do
def normalize_position(line) when line >= 0 do
line
end

defp normalize_postion(position) do
def normalize_position(position) do
Logger.warning(
"[ElixirLS Dialyzer] dialyzer returned warning with invalid position #{inspect(position)}"
)

0
end

defp warning_message({_, _, {warning_name, args}} = raw_warning, warning_format)
when warning_format in ["dialyxir_long", "dialyxir_short"] do
def warning_message({_, _, {warning_name, args}} = raw_warning, warning_format)
when warning_format in ["dialyxir_long", "dialyxir_short"] do
format_function =
case warning_format do
"dialyxir_long" -> :format_long
Expand All @@ -652,11 +652,11 @@ defmodule ElixirLS.LanguageServer.Dialyzer do
end
end

defp warning_message(raw_warning, "dialyzer") do
def warning_message(raw_warning, "dialyzer") do
dialyzer_raw_warning_message(raw_warning)
end

defp warning_message(raw_warning, warning_format) do
def warning_message(raw_warning, warning_format) do
Logger.info(
"[ElixirLS Dialyzer] Unrecognized dialyzerFormat setting: #{inspect(warning_format)}" <>
", falling back to \"dialyzer\""
Expand Down
72 changes: 50 additions & 22 deletions apps/language_server/lib/language_server/dialyzer/analyzer.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,33 +9,41 @@ defmodule ElixirLS.LanguageServer.Dialyzer.Analyzer do
# TODO remove this comment when OTP >= 25 is required

# default warns taken from
# https://github.com/erlang/otp/blob/4ed7957623e5ccbd420a09a506bd6bc9930fe93c/lib/dialyzer/src/dialyzer_options.erl#L34
# macros defined in https://github.com/erlang/otp/blob/4ed7957623e5ccbd420a09a506bd6bc9930fe93c/lib/dialyzer/src/dialyzer.hrl#L36
# as of OTP 25
# https://github.com/erlang/otp/blob/928d03e6da416208fce7b9a7dbbfbb4f25d26c37/lib/dialyzer/src/dialyzer_options.erl#L34
# macros defined in https://github.com/erlang/otp/blob/928d03e6da416208fce7b9a7dbbfbb4f25d26c37/lib/dialyzer/src/dialyzer.hrl#L36
# as of OTP 26
@default_warns [
:warn_behaviour,
:warn_bin_construction,
:warn_callgraph,
:warn_contract_range,
:warn_contract_syntax,
:warn_contract_types,
:warn_failing_call,
:warn_fun_app,
:warn_map_construction,
:warn_matching,
:warn_non_proper_list,
:warn_not_called,
:warn_opaque,
:warn_return_no_exit,
:warn_undefined_callbacks
]
:warn_behaviour,
:warn_bin_construction,
:warn_callgraph,
:warn_contract_range,
:warn_contract_syntax,
:warn_contract_types,
:warn_failing_call,
:warn_fun_app,
:warn_map_construction,
:warn_matching,
:warn_non_proper_list,
:warn_not_called,
:warn_opaque,
:warn_return_no_exit,
:warn_undefined_callbacks
] ++
(if String.to_integer(System.otp_release()) >= 26 do
[
# warn_unknown is enabled by default since OTP 26
:warn_unknown
]
else
[]
end)

@non_default_warns [
:warn_contract_not_equal,
:warn_contract_subtype,
:warn_contract_supertype,
:warn_return_only_exit,
:warn_umatched_return,
:warn_unknown
:warn_umatched_return
] ++
(if String.to_integer(System.otp_release()) >= 25 do
[
Expand All @@ -45,6 +53,17 @@ defmodule ElixirLS.LanguageServer.Dialyzer.Analyzer do
]
else
[]
end) ++
(if String.to_integer(System.otp_release()) >= 26 do
[
# OTP >= 26 options
:warn_overlapping_contract
]
else
[
# warn_unknown is enabled by default since OTP 26
:warn_unknown
]
end)
@log_cache_length 10

Expand Down Expand Up @@ -162,7 +181,16 @@ defmodule ElixirLS.LanguageServer.Dialyzer.Analyzer do
end

def matching_tags(warn_opts) do
:dialyzer_options.build_warnings(warn_opts, @default_warns)
default_warns =
unless :persistent_term.get(:language_server_test_mode, false) do
@default_warns
else
# do not include warn_unknown in tests
# we build small PLT and this results in lots of warnings
@default_warns -- [:warn_unknown]
end

:dialyzer_options.build_warnings(warn_opts, default_warns)
end

defp main_loop(%__MODULE__{backend_pid: backend_pid} = state) do
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,7 @@ defmodule ElixirLS.LanguageServer.Dialyzer.Manifest do
end
end

defp otp_vsn() do
def otp_vsn() do
major = :erlang.system_info(:otp_release) |> List.to_string()
vsn_file = Path.join([:code.root_dir(), "releases", major, "OTP_VERSION"])

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ defmodule ElixirLS.LanguageServer.Dialyzer.SuccessTypings do
|> :dialyzer_plt.all_modules()
|> :sets.to_list()

# TODO filter by apps?
for mod <- modules,
file = source(mod),
file in files,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
defmodule ElixirLS.LanguageServer.Dialyzer.Supervisor do
alias ElixirLS.LanguageServer.Dialyzer
alias ElixirLS.LanguageServer.{Dialyzer, DialyzerIncremental}
use Supervisor

def start_link(parent \\ self(), root_path) do
Expand All @@ -10,7 +10,8 @@ defmodule ElixirLS.LanguageServer.Dialyzer.Supervisor do
def init({parent, root_path}) do
Supervisor.init(
[
{Dialyzer, {parent, root_path}}
{Dialyzer, {parent, root_path}},
{DialyzerIncremental, {parent, root_path}}
],
strategy: :one_for_one
)
Expand Down
1 change: 1 addition & 0 deletions apps/language_server/lib/language_server/dialyzer/utils.ex
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ defmodule ElixirLS.LanguageServer.Dialyzer.Utils do
defp module_references(mod) do
try do
for form <- read_forms(mod),
# TODO does import create remote call?
{:call, _, {:remote, _, {:atom, _, module}, _}, _} <- form,
uniq: true,
do: module
Expand Down
Loading

0 comments on commit 06178f5

Please sign in to comment.