From 8c3594736f93b976a6cdd5e39a88f51cb4c6c8b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Samson?= Date: Wed, 29 Nov 2023 10:11:04 +0100 Subject: [PATCH] Elixir 1.16 support (#1032) * 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 --- VERSION | 2 +- .../elixir_ls_debugger/lib/debugger/server.ex | 2 +- .../lib/debugger/variables.ex | 25 +- .../elixir_ls_debugger/test/debugger_test.exs | 2 + .../lib/language_server/build.ex | 262 ++++++++++-------- .../lib/language_server/diagnostics.ex | 68 ++--- .../lib/language_server/dialyzer.ex | 6 +- .../lib/language_server/dialyzer/manifest.ex | 6 +- .../providers/code_lens/test.ex | 2 +- .../language_server/providers/completion.ex | 4 +- .../providers/document_symbols.ex | 5 +- .../providers/execute_command/mix_clean.ex | 4 +- .../providers/on_type_formatting.ex | 2 +- .../lib/language_server/server.ex | 45 ++- .../lib/language_server/source_file/path.ex | 19 +- .../lib/language_server/tracer.ex | 7 +- .../language_server/test/diagnostics_test.exs | 79 +++--- .../test/providers/document_symbols_test.exs | 6 +- apps/language_server/test/server_test.exs | 16 +- dep_versions.exs | 2 +- mix.lock | 2 +- 21 files changed, 320 insertions(+), 246 deletions(-) diff --git a/VERSION b/VERSION index 52eacacfb..66333910a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.17.10 +0.18.0 diff --git a/apps/elixir_ls_debugger/lib/debugger/server.ex b/apps/elixir_ls_debugger/lib/debugger/server.ex index ef1abe88b..e0aee1adf 100644 --- a/apps/elixir_ls_debugger/lib/debugger/server.ex +++ b/apps/elixir_ls_debugger/lib/debugger/server.ex @@ -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 = diff --git a/apps/elixir_ls_debugger/lib/debugger/variables.ex b/apps/elixir_ls_debugger/lib/debugger/variables.ex index a06b994f4..c3c1fbd22 100644 --- a/apps/elixir_ls_debugger/lib/debugger/variables.ex +++ b/apps/elixir_ls_debugger/lib/debugger/variables.ex @@ -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 @@ -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 @@ -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 -> @@ -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 diff --git a/apps/elixir_ls_debugger/test/debugger_test.exs b/apps/elixir_ls_debugger/test/debugger_test.exs index 9d175b134..c31afdd8f 100644 --- a/apps/elixir_ls_debugger/test/debugger_test.exs +++ b/apps/elixir_ls_debugger/test/debugger_test.exs @@ -12,6 +12,7 @@ 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) @@ -19,6 +20,7 @@ defmodule ElixirLS.Debugger.ServerTest do {: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() diff --git a/apps/language_server/lib/language_server/build.ex b/apps/language_server/lib/language_server/build.ex index 49fde3130..b387b5d72 100644 --- a/apps/language_server/lib/language_server/build.ex +++ b/apps/language_server/lib/language_server/build.ex @@ -4,132 +4,163 @@ 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 @@ -137,7 +168,7 @@ defmodule ElixirLS.LanguageServer.Build 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] @@ -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", @@ -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} diff --git a/apps/language_server/lib/language_server/diagnostics.ex b/apps/language_server/lib/language_server/diagnostics.ex index 5b7ea1c20..d0669cc66 100644 --- a/apps/language_server/lib/language_server/diagnostics.ex +++ b/apps/language_server/lib/language_server/diagnostics.ex @@ -2,12 +2,11 @@ defmodule ElixirLS.LanguageServer.Diagnostics do alias ElixirLS.LanguageServer.{SourceFile, JsonRpc} def normalize(diagnostics, root_path, mixfile) do - for diagnostic <- diagnostics do - {type, file, position, description, stacktrace} = + for %Mix.Task.Compiler.Diagnostic{} = diagnostic <- diagnostics do + {type, file, position, stacktrace} = extract_message_info(diagnostic.message, root_path) diagnostic - |> update_message(type, description, stacktrace) |> maybe_update_file(file, mixfile) |> maybe_update_position(type, position, stacktrace) end @@ -26,32 +25,9 @@ defmodule ElixirLS.LanguageServer.Diagnostics do stacktrace = reversed_stacktrace |> Enum.map(&String.trim/1) |> Enum.reverse() {type, message_without_type} = split_type_and_message(message) - {file, position, description} = split_file_and_description(message_without_type, root_path) + {file, position} = get_file_and_position(message_without_type, root_path) - {type, file, position, description, stacktrace} - end - - defp update_message(diagnostic, type, description, stacktrace) do - description = - if type do - "(#{type}) #{description}" - else - description - end - - message = - if stacktrace != [] do - stacktrace = - stacktrace - |> Enum.map_join("\n", &" │ #{&1}") - |> String.trim_trailing() - - description <> "\n\n" <> "Stacktrace:\n" <> stacktrace - else - description - end - - Map.put(diagnostic, :message, message) + {type, file, position, stacktrace} end defp maybe_update_file(diagnostic, path, mixfile) do @@ -104,8 +80,18 @@ defmodule ElixirLS.LanguageServer.Diagnostics do end end - defp split_file_and_description(message, root_path) do - with {file, line, column, description} <- get_message_parts(message), + defp get_file_and_position(message, root_path) do + # this regex won't match filenames with spaces but in elixir 1.16 errors we can't be sure where + # the file name starts e.g. + # invalid syntax found on lib/template.eex:2:5: + file_position = + case Regex.run(~r/([^\s:]+):(\d+)(:(\d+))?/su, message) do + [_, file, line] -> {file, line, ""} + [_, file, line, _, column] -> {file, line, column} + _ -> nil + end + + with {file, line, column} <- file_position, {:ok, path} <- file_path(file, root_path) do line = String.to_integer(line) @@ -116,17 +102,10 @@ defmodule ElixirLS.LanguageServer.Diagnostics do true -> {line, String.to_integer(column)} end - {path, position, description} + {path, position} else _ -> - {nil, nil, message} - end - end - - defp get_message_parts(message) do - case Regex.run(~r/^(.*?):(\d+)(:(\d+))?: (.*)/su, message) do - [_, file, line, _, column, description] -> {file, line, column, description} - _ -> nil + {nil, nil} end end @@ -141,7 +120,14 @@ defmodule ElixirLS.LanguageServer.Diagnostics do end defp file_path_in_umbrella(file, root_path) do - case [root_path, "apps", "*", file] |> Path.join() |> Path.wildcard() do + case [ + SourceFile.Path.escape_for_wildcard(root_path), + "apps", + "*", + SourceFile.Path.escape_for_wildcard(file) + ] + |> Path.join() + |> Path.wildcard() do [] -> {:error, :file_not_found} @@ -164,7 +150,7 @@ defmodule ElixirLS.LanguageServer.Diagnostics do defp extract_line_from_missing_hint(message) do case Regex.run( - ~r/HINT: it looks like the .+ on line (\d+) does not have a matching /u, + ~r/starting at line (\d+)\)/u, message ) do [_, line] -> String.to_integer(line) diff --git a/apps/language_server/lib/language_server/dialyzer.ex b/apps/language_server/lib/language_server/dialyzer.ex index 5d80f888f..3c222a328 100644 --- a/apps/language_server/lib/language_server/dialyzer.ex +++ b/apps/language_server/lib/language_server/dialyzer.ex @@ -452,7 +452,11 @@ defmodule ElixirLS.LanguageServer.Dialyzer do end temp_modules = - for file <- Path.wildcard(temp_file_path(root_path, "**/*.beam")), into: %{} do + for file <- + Path.wildcard( + temp_file_path(SourceFile.Path.escape_for_wildcard(root_path), "**/*.beam") + ), + into: %{} do {String.to_atom(Path.basename(file, ".beam")), to_charlist(file)} end diff --git a/apps/language_server/lib/language_server/dialyzer/manifest.ex b/apps/language_server/lib/language_server/dialyzer/manifest.ex index eed0be0a3..c93913d8f 100644 --- a/apps/language_server/lib/language_server/dialyzer/manifest.ex +++ b/apps/language_server/lib/language_server/dialyzer/manifest.ex @@ -1,5 +1,5 @@ defmodule ElixirLS.LanguageServer.Dialyzer.Manifest do - alias ElixirLS.LanguageServer.{Dialyzer, Dialyzer.Utils, JsonRpc} + alias ElixirLS.LanguageServer.{Dialyzer, Dialyzer.Utils, JsonRpc, SourceFile} import Record import Dialyzer.Utils require Logger @@ -166,7 +166,9 @@ defmodule ElixirLS.LanguageServer.Dialyzer.Manifest do modules_to_paths = for app <- @erlang_apps ++ @elixir_apps, - path <- Path.join([Application.app_dir(app), "**/*.beam"]) |> Path.wildcard(), + path <- + Path.join([SourceFile.Path.escape_for_wildcard(Application.app_dir(app)), "**/*.beam"]) + |> Path.wildcard(), into: %{}, do: {pathname_to_module(path), path |> String.to_charlist()} diff --git a/apps/language_server/lib/language_server/providers/code_lens/test.ex b/apps/language_server/lib/language_server/providers/code_lens/test.ex index 2475d0b7b..2344f4e60 100644 --- a/apps/language_server/lib/language_server/providers/code_lens/test.ex +++ b/apps/language_server/lib/language_server/providers/code_lens/test.ex @@ -158,7 +158,7 @@ defmodule ElixirLS.LanguageServer.Providers.CodeLens.Test do defp parse_source(text) do buffer_file_metadata = text - |> Parser.parse_string(true, true, 1) + |> Parser.parse_string(true, true, {1, 1}) if buffer_file_metadata.error != nil do {:error, buffer_file_metadata} diff --git a/apps/language_server/lib/language_server/providers/completion.ex b/apps/language_server/lib/language_server/providers/completion.ex index e6f1263ab..cbf8d2ae2 100644 --- a/apps/language_server/lib/language_server/providers/completion.ex +++ b/apps/language_server/lib/language_server/providers/completion.ex @@ -102,12 +102,12 @@ defmodule ElixirLS.LanguageServer.Providers.Completion do character = SourceFile.lsp_character_to_elixir(line_text, character) text_before_cursor = String.slice(line_text, 0, character - 1) - text_after_cursor = String.slice(line_text, (character - 1)..-1) + text_after_cursor = String.slice(line_text, (character - 1)..-1//1) prefix = get_prefix(text_before_cursor) # Can we use ElixirSense.Providers.Suggestion? ElixirSense.suggestions/3 - metadata = ElixirSense.Core.Parser.parse_string(text, true, true, line) + metadata = ElixirSense.Core.Parser.parse_string(text, true, true, {line, character}) env = ElixirSense.Core.Metadata.get_env(metadata, {line, character}) diff --git a/apps/language_server/lib/language_server/providers/document_symbols.ex b/apps/language_server/lib/language_server/providers/document_symbols.ex index 4b15d2261..b5fad67e3 100644 --- a/apps/language_server/lib/language_server/providers/document_symbols.ex +++ b/apps/language_server/lib/language_server/providers/document_symbols.ex @@ -49,7 +49,10 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbols do end defp list_symbols(src) do - case ElixirSense.string_to_quoted(src, 1, @max_parser_errors, line: 1, token_metadata: true) do + case ElixirSense.string_to_quoted(src, {1, 1}, @max_parser_errors, + line: 1, + token_metadata: true + ) do {:ok, quoted_form} -> {:ok, extract_modules(quoted_form) |> Enum.reject(&is_nil/1)} {:error, _error} -> {:error, :compilation_error} end diff --git a/apps/language_server/lib/language_server/providers/execute_command/mix_clean.ex b/apps/language_server/lib/language_server/providers/execute_command/mix_clean.ex index 838889ab0..570950374 100644 --- a/apps/language_server/lib/language_server/providers/execute_command/mix_clean.ex +++ b/apps/language_server/lib/language_server/providers/execute_command/mix_clean.ex @@ -2,8 +2,8 @@ defmodule ElixirLS.LanguageServer.Providers.ExecuteCommand.MixClean do @behaviour ElixirLS.LanguageServer.Providers.ExecuteCommand @impl ElixirLS.LanguageServer.Providers.ExecuteCommand - def execute([clean_deps?], _state) do - case ElixirLS.LanguageServer.Build.clean(clean_deps?) do + def execute([clean_deps?], state) do + case ElixirLS.LanguageServer.Build.clean(state.project_dir, clean_deps?) do :ok -> {:ok, %{}} {:error, reason} -> {:error, :server_error, "Mix clean failed: #{inspect(reason)}", true} end diff --git a/apps/language_server/lib/language_server/providers/on_type_formatting.ex b/apps/language_server/lib/language_server/providers/on_type_formatting.ex index 1c7d54b18..8f0315478 100644 --- a/apps/language_server/lib/language_server/providers/on_type_formatting.ex +++ b/apps/language_server/lib/language_server/providers/on_type_formatting.ex @@ -24,7 +24,7 @@ defmodule ElixirLS.LanguageServer.Providers.OnTypeFormatting do # Use contents and indentation of the next line to help us guess whether to insert an "end" indentation_suggests_edit? = if Enum.at(prev_tokens, -1) == "do" or "fn" in prev_tokens do - next_line = Enum.find(Enum.slice(lines, (line + 1)..-1), "", &(!blank?(&1))) + next_line = Enum.find(Enum.slice(lines, (line + 1)..-1//1), "", &(!blank?(&1))) next_tokens = tokens(next_line) next_indentation_length = String.length(indentation(next_line)) diff --git a/apps/language_server/lib/language_server/server.ex b/apps/language_server/lib/language_server/server.ex index cc6969f56..cd7e9db0f 100644 --- a/apps/language_server/lib/language_server/server.ex +++ b/apps/language_server/lib/language_server/server.ex @@ -54,6 +54,7 @@ defmodule ElixirLS.LanguageServer.Server do build_diagnostics: [], dialyzer_diagnostics: [], needs_build?: false, + full_build_done?: false, build_running?: false, analysis_ready?: false, received_shutdown?: false, @@ -364,7 +365,13 @@ defmodule ElixirLS.LanguageServer.Server do handle_build_result(:error, [Diagnostics.exception_to_diagnostic(reason, path)], state) end - state = if state.needs_build?, do: trigger_build(state), else: state + state = + if state.needs_build? do + trigger_build(state) + else + state + end + {:noreply, state} end @@ -738,7 +745,11 @@ defmodule ElixirLS.LanguageServer.Server do |> Enum.map(& &1["uri"]) |> WorkspaceSymbols.notify_uris_modified() - if needs_build, do: trigger_build(state), else: state + if needs_build do + trigger_build(state) + else + state + end end defp handle_notification(did_change_watched_files(_changes), state = %__MODULE__{}) do @@ -1327,7 +1338,9 @@ defmodule ElixirLS.LanguageServer.Server do # Build - defp trigger_build(state = %__MODULE__{project_dir: project_dir}) do + defp trigger_build( + state = %__MODULE__{project_dir: project_dir, full_build_done?: full_build_done?} + ) do cond do not is_binary(project_dir) -> state @@ -1335,7 +1348,8 @@ defmodule ElixirLS.LanguageServer.Server do not state.build_running? -> opts = [ fetch_deps?: Map.get(state.settings || %{}, "fetchDeps", false), - compile?: Map.get(state.settings || %{}, "autoBuild", true) + compile?: Map.get(state.settings || %{}, "autoBuild", true), + force?: not full_build_done? ] {_pid, build_ref} = @@ -1449,7 +1463,7 @@ defmodule ElixirLS.LanguageServer.Server do state.project_dir ) - state + %{state | full_build_done?: if(status == :ok, do: true, else: state.full_build_done?)} end defp handle_dialyzer_result(diagnostics, build_ref, state = %__MODULE__{}) do @@ -1679,7 +1693,7 @@ defmodule ElixirLS.LanguageServer.Server do add_watched_extensions(state.server_instance_id, additional_watched_extensions) - maybe_rebuild(state) + # maybe_rebuild(state) state = create_gitignore(state) if state.mix_project? do @@ -2128,21 +2142,22 @@ defmodule ElixirLS.LanguageServer.Server do case File.cwd() do {:ok, cwd} -> if SourceFile.Path.absname(cwd) == SourceFile.Path.absname(project_dir) do - mixfile = SourceFile.Path.absname(MixfileHelpers.mix_exs()) - try do - case Build.reload_project(mixfile, project_dir) do - {:ok, _} -> - Build.clean(true) + case Build.clean(project_dir) do + :ok -> + :ok + + e -> + Logger.warning( + "Unable to clean project, databases may not be up to date: #{inspect(e)}" + ) - _ -> - # TODO emit diagnostics here? :ok end rescue e -> message = - "Unable to reload project: #{Exception.message(e)}" + "Unable to reload project: #{Exception.format(:error, e, __STACKTRACE__)}" Logger.error(message) @@ -2274,7 +2289,7 @@ defmodule ElixirLS.LanguageServer.Server do :ok rescue - e in [EEx.SyntaxError, SyntaxError, TokenMissingError] -> + e in [EEx.SyntaxError, SyntaxError, TokenMissingError, MismatchedDelimiterError] -> message = Exception.message(e) diagnostic = %Mix.Task.Compiler.Diagnostic{ diff --git a/apps/language_server/lib/language_server/source_file/path.ex b/apps/language_server/lib/language_server/source_file/path.ex index 7fe9cccb0..73cf0dec0 100644 --- a/apps/language_server/lib/language_server/source_file/path.ex +++ b/apps/language_server/lib/language_server/source_file/path.ex @@ -161,6 +161,23 @@ defmodule ElixirLS.LanguageServer.SourceFile.Path do end end + def escape_for_wildcard(path) when is_list(path), do: escape_for_wildcard(to_string(path)) + + def escape_for_wildcard(path) when is_binary(path) do + # Path.wildcard expects universal separators even on windows + # escape all special chars + path + |> convert_separators_to_universal() + |> String.replace("\\", "\\\\") + |> String.replace("?", "\\?") + |> String.replace("*", "\\*") + |> String.replace("{", "\\{") + |> String.replace("}", "\\}") + |> String.replace("[", "\\[") + |> String.replace("]", "\\]") + |> String.replace(",", "\\,") + end + # the functions below are copied from elixir project # https://github.com/lukaszsamson/elixir/blob/bf3e2fd3ad78235bda059b80994a90d9a4184353/lib/elixir/lib/path.ex # with applied https://github.com/elixir-lang/elixir/pull/13061 @@ -189,7 +206,7 @@ defmodule ElixirLS.LanguageServer.SourceFile.Path do absname(path, &File.cwd!/0) end - @spec absname(t, t) :: binary + @spec absname(t, t | (-> t)) :: binary def absname(path, relative_to) do path = IO.chardata_to_string(path) diff --git a/apps/language_server/lib/language_server/tracer.ex b/apps/language_server/lib/language_server/tracer.ex index d7a09eb41..6794c34e0 100644 --- a/apps/language_server/lib/language_server/tracer.ex +++ b/apps/language_server/lib/language_server/tracer.ex @@ -3,6 +3,7 @@ defmodule ElixirLS.LanguageServer.Tracer do """ use GenServer alias ElixirLS.LanguageServer.JsonRpc + alias ElixirLS.LanguageServer.SourceFile require Logger @version 3 @@ -490,7 +491,9 @@ defmodule ElixirLS.LanguageServer.Tracer do {version, ""} <- Integer.parse(text) do version else - _ -> nil + other -> + IO.warn("Manifest: #{inspect(other)}") + nil end end @@ -500,7 +503,7 @@ defmodule ElixirLS.LanguageServer.Tracer do def clean_dets(project_dir) do for path <- - Path.join([project_dir, ".elixir_ls/*.dets"]) + Path.join([SourceFile.Path.escape_for_wildcard(project_dir), ".elixir_ls/*.dets"]) |> Path.wildcard(), do: File.rm_rf!(path) end diff --git a/apps/language_server/test/diagnostics_test.exs b/apps/language_server/test/diagnostics_test.exs index 90a4144f4..8b9295055 100644 --- a/apps/language_server/test/diagnostics_test.exs +++ b/apps/language_server/test/diagnostics_test.exs @@ -19,22 +19,9 @@ defmodule ElixirLS.LanguageServer.DiagnosticsTest do lib/my_app/my_module.ex:10: MyApp.MyModule.render/1 """ - [diagnostic | _] = + [_diagnostic | _] = [build_diagnostic(message, file, position)] |> Diagnostics.normalize(root_path, Path.join(root_path, "mix.exs")) - - assert diagnostic.message == """ - (CompileError) some message - - Hint: Some hint - - Stacktrace: - │ (elixir 1.10.1) lib/macro.ex:304: Macro.pipe/3 - │ (stdlib 3.7.1) lists.erl:1263: :lists.foldl/3 - │ (elixir 1.10.1) expanding macro: Kernel.|>/2 - │ expanding macro: SomeModule.sigil_L/2 - │ lib/my_app/my_module.ex:10: MyApp.MyModule.render/1\ - """ end test "update file and position if file is present in the message" do @@ -51,14 +38,33 @@ defmodule ElixirLS.LanguageServer.DiagnosticsTest do [build_diagnostic(message, file, position)] |> Diagnostics.normalize(root_path, Path.join(root_path, "mix.exs")) - assert diagnostic.message == """ - (CompileError) some message + assert diagnostic.position == 3 + end - Stacktrace: - │ lib/my_app/my_module.ex:10: MyApp.MyModule.render/1\ - """ + test "update file and position if file is present in the message - 1.16 format" do + root_path = Path.join(__DIR__, "fixtures/build_errors_on_external_resource") + file = Path.join(root_path, "lib/has_error.ex") + position = 2 - assert diagnostic.position == 3 + message = """ + ** (SyntaxError) invalid syntax found on lib/template.eex:2:5: + error: syntax error before: ',' + │ + 2 │ , + │ ^ + │ + └─ lib/template.eex:2:5 + (eex 1.16.0-rc.0) lib/eex/compiler.ex:332: EEx.Compiler.generate_buffer/4 + lib/has_error.ex:2: (module) + (elixir 1.16.0-rc.0) lib/kernel/parallel_compiler.ex:428: anonymous fn/5 in Kernel.ParallelCompiler.spawn_workers/8 + """ + + [diagnostic | _] = + [build_diagnostic(message, file, position)] + |> Diagnostics.normalize(root_path, Path.join(root_path, "mix.exs")) + + assert diagnostic.position == {2, 5} + assert diagnostic.file == Path.join(root_path, "lib/template.eex") end test "update file and position with column if file is present in the message" do @@ -75,13 +81,6 @@ defmodule ElixirLS.LanguageServer.DiagnosticsTest do [build_diagnostic(message, file, position)] |> Diagnostics.normalize(root_path, Path.join(root_path, "mix.exs")) - assert diagnostic.message == """ - (CompileError) some message - - Stacktrace: - │ lib/my_app/my_module.ex:10: MyApp.MyModule.render/1\ - """ - assert diagnostic.position == {3, 5} end @@ -100,7 +99,6 @@ defmodule ElixirLS.LanguageServer.DiagnosticsTest do [build_diagnostic(message, file, position)] |> Diagnostics.normalize(root_path, Path.join(root_path, "mix.exs")) - assert diagnostic.message =~ "(CompileError) some message" assert diagnostic.file =~ "umbrella/apps/app2/lib/app2.ex" assert diagnostic.position == 5 end @@ -119,13 +117,6 @@ defmodule ElixirLS.LanguageServer.DiagnosticsTest do [build_diagnostic(message, file, position)] |> Diagnostics.normalize(root_path, Path.join(root_path, "mix.exs")) - assert diagnostic.message == """ - (CompileError) lib/non_existing.ex:3: some message - - Stacktrace: - │ lib/my_app/my_module.ex:10: MyApp.MyModule.render/1\ - """ - assert diagnostic.position == 2 end @@ -170,6 +161,24 @@ defmodule ElixirLS.LanguageServer.DiagnosticsTest do (elixir 1.12.1) lib/kernel/parallel_compiler.ex:319: anonymous fn/4 in Kernel.ParallelCompiler.spawn_workers/7 """ + [diagnostic | _] = + [build_diagnostic(message, file, position)] + |> Diagnostics.normalize(root_path, Path.join(root_path, "mix.exs")) + + assert diagnostic.position == 1 + end + + test "if position is nil and error is TokenMissingError, try to retrieve from the hint - 1.16 format" do + root_path = Path.join(__DIR__, "fixtures/token_missing_error") + file = Path.join(root_path, "lib/has_error.ex") + position = nil + + message = """ + ** (TokenMissingError) token missing on lib/has_error.ex:16:1: + error: missing terminator: end (for "fn" starting at line 6) + └─ lib/has_error.ex:16:1 + """ + [diagnostic | _] = [build_diagnostic(message, file, position)] |> Diagnostics.normalize(root_path, Path.join(root_path, "mix.exs")) diff --git a/apps/language_server/test/providers/document_symbols_test.exs b/apps/language_server/test/providers/document_symbols_test.exs index bf7f1c3cf..c9c188b73 100644 --- a/apps/language_server/test/providers/document_symbols_test.exs +++ b/apps/language_server/test/providers/document_symbols_test.exs @@ -2458,15 +2458,15 @@ defmodule ElixirLS.LanguageServer.Providers.DocumentSymbolsTest do uri = "file:///project/test.exs" text = """ - defmodule A do + defmodule aA do def hello do Hello.hi( end end """ - assert {:error, :server_error, message, false} = DocumentSymbols.symbols(uri, text, true) - assert String.contains?(message, "Cannot parse source file") + assert {:error, :server_error, "Cannot parse source file", false} = + DocumentSymbols.symbols(uri, text, true) end test "returns def and defp as a prefix" do diff --git a/apps/language_server/test/server_test.exs b/apps/language_server/test/server_test.exs index 2defe2256..3f94cc956 100644 --- a/apps/language_server/test/server_test.exs +++ b/apps/language_server/test/server_test.exs @@ -1661,15 +1661,15 @@ defmodule ElixirLS.LanguageServer.ServerTest do "diagnostics" => [ %{ "message" => - "(CompileError) lib/has_error.ex: cannot compile module" <> _, + "** (CompileError) lib/has_error.ex: cannot compile module" <> _, "range" => %{"end" => %{"line" => 0}, "start" => %{"line" => 0}}, "severity" => 1 }, %{ "message" => "undefined function does_not_exist/0" <> _, "range" => %{ - "end" => %{"character" => 4, "line" => 3}, - "start" => %{"character" => 4, "line" => 3} + "end" => %{"character" => _, "line" => 3}, + "start" => %{"character" => _, "line" => 3} }, "severity" => 1, "source" => "Elixir" @@ -1682,7 +1682,9 @@ defmodule ElixirLS.LanguageServer.ServerTest do "uri" => ^error_file, "diagnostics" => [ %{ - "message" => "(CompileError) undefined function does_not_exist" <> _, + "message" => + "** (CompileError) lib/has_error.ex:4: undefined function does_not_exist" <> + _, "range" => %{"end" => %{"line" => 3}, "start" => %{"line" => 3}}, "severity" => 1 } @@ -1706,8 +1708,8 @@ defmodule ElixirLS.LanguageServer.ServerTest do "uri" => ^error_file, "diagnostics" => [ %{ - "message" => "(TokenMissingError) missing terminator: end" <> _, - "range" => %{"end" => %{"line" => 5}, "start" => %{"line" => 5}}, + "message" => "** (TokenMissingError)" <> _, + "range" => %{"end" => %{"line" => _}, "start" => %{"line" => _}}, "severity" => 1 } ] @@ -1729,7 +1731,7 @@ defmodule ElixirLS.LanguageServer.ServerTest do "uri" => ^error_file, "diagnostics" => [ %{ - "message" => "(SyntaxError) syntax error before: ','" <> _, + "message" => "** (SyntaxError)" <> _, "range" => %{"end" => %{"line" => 1}, "start" => %{"line" => 1}}, "severity" => 1 } diff --git a/dep_versions.exs b/dep_versions.exs index b5baf22de..3b642e3dd 100644 --- a/dep_versions.exs +++ b/dep_versions.exs @@ -1,5 +1,5 @@ [ - elixir_sense: "16d28f78e5702678394523c6aa17486931740402", + elixir_sense: "02c101d03c0b5a81379b3905e7baa6e685c0fe99", dialyxir_vendored: "d50dcd7101c6ebd37b57b7ee4a7888d8cb634782", jason_v: "c81537e2a5e1acacb915cf339fe400357e3c2aaa", erl2ex_vendored: "073ac6b9a44282e718b6050c7b27cedf9217a12a", diff --git a/mix.lock b/mix.lock index 43fd32463..3deb41b96 100644 --- a/mix.lock +++ b/mix.lock @@ -2,7 +2,7 @@ "benchee": {:hex, :benchee, "1.1.0", "f3a43817209a92a1fade36ef36b86e1052627fd8934a8b937ac9ab3a76c43062", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.0", [hex: :statistex, repo: "hexpm", optional: false]}], "hexpm", "7da57d545003165a012b587077f6ba90b89210fd88074ce3c60ce239eb5e6d93"}, "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"}, "dialyxir_vendored": {:git, "https://github.com/elixir-lsp/dialyxir.git", "d50dcd7101c6ebd37b57b7ee4a7888d8cb634782", [ref: "d50dcd7101c6ebd37b57b7ee4a7888d8cb634782"]}, - "elixir_sense": {:git, "https://github.com/elixir-lsp/elixir_sense.git", "16d28f78e5702678394523c6aa17486931740402", [ref: "16d28f78e5702678394523c6aa17486931740402"]}, + "elixir_sense": {:git, "https://github.com/elixir-lsp/elixir_sense.git", "02c101d03c0b5a81379b3905e7baa6e685c0fe99", [ref: "02c101d03c0b5a81379b3905e7baa6e685c0fe99"]}, "erl2ex_vendored": {:git, "https://github.com/elixir-lsp/erl2ex.git", "073ac6b9a44282e718b6050c7b27cedf9217a12a", [ref: "073ac6b9a44282e718b6050c7b27cedf9217a12a"]}, "erlex_vendored": {:git, "https://github.com/elixir-lsp/erlex.git", "82db0e82ee4896491bc26dec99f5d795f03ab9f4", [ref: "82db0e82ee4896491bc26dec99f5d795f03ab9f4"]}, "jason_v": {:git, "https://github.com/elixir-lsp/jason.git", "c81537e2a5e1acacb915cf339fe400357e3c2aaa", [ref: "c81537e2a5e1acacb915cf339fe400357e3c2aaa"]},