Skip to content

Commit

Permalink
Elixir 1.16 support (#1032)
Browse files Browse the repository at this point in the history
* fix warnings

* Mix.Dep.load_on_environment no longer exists on 1.16

* update elixir_sense api

* rescue MismatchedDelimiterError added in 1.16

* remove diagnostic message normalisation as it breaks 1.16 and 1.15 message format

* handle updated message format

* sanitize paths passed to wildcards

* fix dialyzer error

* format

* fix warning

* fix test

* fix flaky test

revert group leader change

* add missing alias

* fix race conditions during config reload

project reload wasn't under build lock and it could execute in parallel with build
this made the tests flaky

* format on 1.15

* use List.improper? when it makes sense

* bump elixir_sense

* make the sample more broken

* attempt to fix errors on <= 1.14

* run formatter
  • Loading branch information
lukaszsamson committed Nov 29, 2023
1 parent 2d6c4b8 commit 8c35947
Show file tree
Hide file tree
Showing 21 changed files with 320 additions and 246 deletions.
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.17.10
0.18.0
2 changes: 1 addition & 1 deletion apps/elixir_ls_debugger/lib/debugger/server.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1028,7 +1028,7 @@ defmodule ElixirLS.Debugger.Server do
_ -> -1
end

stack_frames = Enum.slice(paused_process.stack, start_frame..end_frame)
stack_frames = Enum.slice(paused_process.stack, start_frame..end_frame//1)
{state, frame_ids} = ensure_frame_ids(state, pid, stack_frames)

stack_frames_json =
Expand Down
25 changes: 9 additions & 16 deletions apps/elixir_ls_debugger/lib/debugger/variables.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,11 @@ defmodule ElixirLS.Debugger.Variables do
if Keyword.keyword?(var) do
:named
else
:indexed

try do
# this call will raise ArgumentError for improper list, no better way to check it
_ = length(var)
if List.improper?(var) do
# improper list has head and tail
:named
else
:indexed
rescue
ArgumentError ->
# improper list has head and tail
:named
end
end
end
Expand All @@ -48,7 +43,7 @@ defmodule ElixirLS.Debugger.Variables do
start = start || 0

try do
# this call will raise ArgumentError for improper list, no better way to check it
# this call will raise ArgumentError for improper list
max_count = length(var)
count = count || max_count

Expand Down Expand Up @@ -137,6 +132,7 @@ defmodule ElixirLS.Debugger.Variables do

def num_children(var) when is_list(var) do
try do
# this call will raise ArgumentError for improper list
length(var)
rescue
ArgumentError ->
Expand Down Expand Up @@ -202,13 +198,10 @@ defmodule ElixirLS.Debugger.Variables do
if Keyword.keyword?(var) and var != [] do
"keyword"
else
try do
# this call will raise ArgumentError for improper list, no better way to check it
_ = length(var)
if List.improper?(var) do
"improper list"
else
"list"
rescue
ArgumentError ->
"improper list"
end
end
end
Expand Down
2 changes: 2 additions & 0 deletions apps/elixir_ls_debugger/test/debugger_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ defmodule ElixirLS.Debugger.ServerTest do

setup do
{:ok, packet_capture} = ElixirLS.Utils.PacketCapture.start_link(self())
default_group_leader = Process.info(Process.whereis(ElixirLS.Debugger.Output))[:group_leader]
Process.group_leader(Process.whereis(ElixirLS.Debugger.Output), packet_capture)

{:ok, _} = start_supervised(BreakpointCondition)
{:ok, _} = start_supervised({ModuleInfoCache, %{}})
{:ok, server} = Server.start_link(name: Server)

on_exit(fn ->
Process.group_leader(Process.whereis(ElixirLS.Debugger.Output), default_group_leader)
for mod <- :int.interpreted(), do: :int.nn(mod)
:int.auto_attach(false)
:int.no_break()
Expand Down
262 changes: 150 additions & 112 deletions apps/language_server/lib/language_server/build.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,140 +4,171 @@ defmodule ElixirLS.LanguageServer.Build do
require Logger

def build(parent, root_path, opts) when is_binary(root_path) do
spawn_monitor(fn ->
with_build_lock(fn ->
{us, result} =
:timer.tc(fn ->
Logger.info("Starting build with MIX_ENV: #{Mix.env()} MIX_TARGET: #{Mix.target()}")

# read cache before cleaning up mix state in reload_project
cached_deps = read_cached_deps()
mixfile = SourceFile.Path.absname(MixfileHelpers.mix_exs(), root_path)

case reload_project(mixfile, root_path) do
{:ok, mixfile_diagnostics} ->
{deps_result, deps_raw_diagnostics} =
with_diagnostics([log: true], fn ->
try do
# this call can raise
current_deps = Mix.Dep.load_on_environment([])

purge_changed_deps(current_deps, cached_deps)

if Keyword.get(opts, :fetch_deps?) and current_deps != cached_deps do
fetch_deps(current_deps)
build_pid_reference =
spawn_monitor(fn ->
with_build_lock(fn ->
{us, result} =
:timer.tc(fn ->
Logger.info("Starting build with MIX_ENV: #{Mix.env()} MIX_TARGET: #{Mix.target()}")

# read cache before cleaning up mix state in reload_project
cached_deps = read_cached_deps()
mixfile = SourceFile.Path.absname(MixfileHelpers.mix_exs(), root_path)

case reload_project(mixfile, root_path) do
{:ok, mixfile_diagnostics} ->
{deps_result, deps_raw_diagnostics} =
with_diagnostics([log: true], fn ->
try do
# this call can raise
current_deps =
if Version.match?(System.version(), "< 1.16.0-dev") do
Mix.Dep.load_on_environment([])
else
Mix.Dep.Converger.converge([])
end

purge_changed_deps(current_deps, cached_deps)

if Keyword.get(opts, :fetch_deps?) and current_deps != cached_deps do
fetch_deps(current_deps)
end

state = %{
get: Mix.Project.get(),
# project_file: Mix.Project.project_file(),
config: Mix.Project.config(),
# config_files: Mix.Project.config_files(),
config_mtime: Mix.Project.config_mtime(),
umbrella?: Mix.Project.umbrella?(),
apps_paths: Mix.Project.apps_paths(),
# deps_path: Mix.Project.deps_path(),
# deps_apps: Mix.Project.deps_apps(),
# deps_scms: Mix.Project.deps_scms(),
deps_paths: Mix.Project.deps_paths(),
# build_path: Mix.Project.build_path(),
manifest_path: Mix.Project.manifest_path()
}

ElixirLS.LanguageServer.MixProject.store(state)

:ok
catch
kind, err ->
{payload, stacktrace} = Exception.blame(kind, err, __STACKTRACE__)
{:error, kind, payload, stacktrace}
end
end)

deps_diagnostics =
deps_raw_diagnostics
|> Enum.map(&Diagnostics.code_diagnostic/1)

case deps_result do
:ok ->
if Keyword.get(opts, :compile?) do
{status, compile_diagnostics} =
run_mix_compile(Keyword.get(opts, :force?, false))

compile_diagnostics =
Diagnostics.normalize(compile_diagnostics, root_path, mixfile)

Server.build_finished(
parent,
{status, mixfile_diagnostics ++ deps_diagnostics ++ compile_diagnostics}
)

:"mix_compile_#{status}"
else
Server.build_finished(
parent,
{:ok, mixfile_diagnostics ++ deps_diagnostics}
)

:mix_compile_disabled
end

state = %{
get: Mix.Project.get(),
# project_file: Mix.Project.project_file(),
config: Mix.Project.config(),
# config_files: Mix.Project.config_files(),
config_mtime: Mix.Project.config_mtime(),
umbrella?: Mix.Project.umbrella?(),
apps_paths: Mix.Project.apps_paths(),
# deps_path: Mix.Project.deps_path(),
# deps_apps: Mix.Project.deps_apps(),
# deps_scms: Mix.Project.deps_scms(),
deps_paths: Mix.Project.deps_paths(),
# build_path: Mix.Project.build_path(),
manifest_path: Mix.Project.manifest_path()
}

ElixirLS.LanguageServer.MixProject.store(state)

:ok
catch
kind, err ->
{payload, stacktrace} = Exception.blame(kind, err, __STACKTRACE__)
{:error, kind, payload, stacktrace}
end
end)

deps_diagnostics =
deps_raw_diagnostics
|> Enum.map(&Diagnostics.code_diagnostic/1)

case deps_result do
:ok ->
if Keyword.get(opts, :compile?) do
{status, compile_diagnostics} = run_mix_compile()

compile_diagnostics =
Diagnostics.normalize(compile_diagnostics, root_path, mixfile)

{:error, kind, err, stacktrace} ->
# TODO get path from exception message
Server.build_finished(
parent,
{status, mixfile_diagnostics ++ deps_diagnostics ++ compile_diagnostics}
{:error,
mixfile_diagnostics ++
deps_diagnostics ++
[
Diagnostics.error_to_diagnostic(
kind,
err,
stacktrace,
mixfile,
root_path
)
]}
)

:"mix_compile_#{status}"
else
Server.build_finished(
parent,
{:ok, mixfile_diagnostics ++ deps_diagnostics}
)
:deps_error
end

:mix_compile_disabled
end

{:error, kind, err, stacktrace} ->
# TODO get path from exception message
Server.build_finished(
parent,
{:error,
mixfile_diagnostics ++
deps_diagnostics ++
[
Diagnostics.error_to_diagnostic(
kind,
err,
stacktrace,
mixfile,
root_path
)
]}
)
{:error, mixfile_diagnostics} ->
Server.build_finished(parent, {:error, mixfile_diagnostics})
:mixfile_error

:deps_error
end
:no_mixfile ->
Server.build_finished(parent, {:no_mixfile, []})
:no_mixfile
end
end)

{:error, mixfile_diagnostics} ->
Server.build_finished(parent, {:error, mixfile_diagnostics})
:mixfile_error
if Keyword.get(opts, :compile?) do
Tracer.save()
Logger.info("Compile took #{div(us, 1000)} milliseconds")
else
Logger.info("Mix project load took #{div(us, 1000)} milliseconds")
end

:no_mixfile ->
Server.build_finished(parent, {:no_mixfile, []})
:no_mixfile
end
end)
JsonRpc.telemetry("build", %{"elixir_ls.build_result" => result}, %{
"elixir_ls.build_time" => div(us, 1000)
})
end)
end)

if Keyword.get(opts, :compile?) do
Tracer.save()
Logger.info("Compile took #{div(us, 1000)} milliseconds")
else
Logger.info("Mix project load took #{div(us, 1000)} milliseconds")
end
spawn(fn ->
Process.monitor(parent)
{build_process, _ref} = build_pid_reference
Process.monitor(build_process)

JsonRpc.telemetry("build", %{"elixir_ls.build_result" => result}, %{
"elixir_ls.build_time" => div(us, 1000)
})
end)
receive do
{:DOWN, _ref, _, ^build_process, _reason} ->
:ok

{:DOWN, _ref, _, ^parent, _reason} ->
Process.exit(build_process, :kill)
end
end)

build_pid_reference
end

def clean(clean_deps? \\ false) do
def clean(root_path, clean_deps? \\ false) when is_binary(root_path) do
with_build_lock(fn ->
Mix.Task.clear()
run_mix_clean(clean_deps?)
mixfile = SourceFile.Path.absname(MixfileHelpers.mix_exs(), root_path)

case reload_project(mixfile, root_path) do
{:ok, _} ->
Mix.Task.clear()
run_mix_clean(clean_deps?)

other ->
other
end
end)
end

def with_build_lock(func) do
:global.trans({__MODULE__, self()}, func)
end

def reload_project(mixfile, root_path) do
defp reload_project(mixfile, root_path) do
if File.exists?(mixfile) do
if module = Mix.Project.get() do
build_path = Mix.Project.config()[:build_path]
Expand Down Expand Up @@ -321,7 +352,7 @@ defmodule ElixirLS.LanguageServer.Build do
end
end

defp run_mix_compile do
defp run_mix_compile(force?) do
opts = [
"--return-errors",
"--ignore-module-conflict",
Expand All @@ -335,6 +366,13 @@ defmodule ElixirLS.LanguageServer.Build do
opts ++ ["--all-warnings"]
end

opts =
if force? do
opts ++ ["--force"]
else
opts
end

case Mix.Task.run("compile", opts) do
{status, diagnostics} when status in [:ok, :error, :noop] and is_list(diagnostics) ->
{status, diagnostics}
Expand Down
Loading

0 comments on commit 8c35947

Please sign in to comment.