From 1cae2e8b26df02c8368c9f20bb2e8b6b3e705b59 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Fri, 13 May 2022 17:37:14 +0200 Subject: [PATCH 01/12] Initial implementation of tracer migrated to tracer for references added building of module and def database --- apps/language_server/lib/language_server.ex | 3 +- .../lib/language_server/build.ex | 3 +- .../lib/language_server/cli.ex | 5 + .../language_server/providers/references.ex | 3 +- .../lib/language_server/server.ex | 15 +- .../lib/language_server/tracer.ex | 285 ++++++++++++++++++ .../test/providers/references_test.exs | 10 +- mix.lock | 2 +- 8 files changed, 320 insertions(+), 6 deletions(-) create mode 100644 apps/language_server/lib/language_server/tracer.ex diff --git a/apps/language_server/lib/language_server.ex b/apps/language_server/lib/language_server.ex index f913823ef..36dfd96c1 100644 --- a/apps/language_server/lib/language_server.ex +++ b/apps/language_server/lib/language_server.ex @@ -9,7 +9,8 @@ defmodule ElixirLS.LanguageServer do children = [ {ElixirLS.LanguageServer.Server, ElixirLS.LanguageServer.Server}, {ElixirLS.LanguageServer.JsonRpc, name: ElixirLS.LanguageServer.JsonRpc}, - {ElixirLS.LanguageServer.Providers.WorkspaceSymbols, []} + {ElixirLS.LanguageServer.Providers.WorkspaceSymbols, []}, + {ElixirLS.LanguageServer.Tracer, []} ] opts = [strategy: :one_for_one, name: ElixirLS.LanguageServer.Supervisor, max_restarts: 0] diff --git a/apps/language_server/lib/language_server/build.ex b/apps/language_server/lib/language_server/build.ex index b400019f2..03b55e63e 100644 --- a/apps/language_server/lib/language_server/build.ex +++ b/apps/language_server/lib/language_server/build.ex @@ -1,5 +1,5 @@ defmodule ElixirLS.LanguageServer.Build do - alias ElixirLS.LanguageServer.{Server, JsonRpc, Diagnostics} + alias ElixirLS.LanguageServer.{Server, JsonRpc, Diagnostics, Tracer} alias ElixirLS.Utils.MixfileHelpers def build(parent, root_path, opts) when is_binary(root_path) do @@ -40,6 +40,7 @@ defmodule ElixirLS.LanguageServer.Build do end end) + Tracer.save() JsonRpc.log_message(:info, "Compile took #{div(us, 1000)} milliseconds") end) end) diff --git a/apps/language_server/lib/language_server/cli.ex b/apps/language_server/lib/language_server/cli.ex index d66815a33..001f67b13 100644 --- a/apps/language_server/lib/language_server/cli.ex +++ b/apps/language_server/lib/language_server/cli.ex @@ -6,6 +6,11 @@ defmodule ElixirLS.LanguageServer.CLI do WireProtocol.intercept_output(&JsonRpc.print/1, &JsonRpc.print_err/1) Launch.start_mix() + Code.put_compiler_option(:tracers, [ + ElixirLS.LanguageServer.Tracer + ]) + Code.put_compiler_option(:parser_options, [columns: true, token_metadata: true]) + start_language_server() IO.puts("Started ElixirLS v#{Launch.language_server_version()}") diff --git a/apps/language_server/lib/language_server/providers/references.ex b/apps/language_server/lib/language_server/providers/references.ex index 7188ac608..c242d2de8 100644 --- a/apps/language_server/lib/language_server/providers/references.ex +++ b/apps/language_server/lib/language_server/providers/references.ex @@ -17,7 +17,8 @@ defmodule ElixirLS.LanguageServer.Providers.References do {line, character} = SourceFile.lsp_position_to_elixir(text, {line, character}) Build.with_build_lock(fn -> - ElixirSense.references(text, line, character) + trace = ElixirLS.LanguageServer.Tracer.get_trace() + ElixirSense.references(text, line, character, trace) |> Enum.map(fn elixir_sense_reference -> elixir_sense_reference |> build_reference(uri, text) diff --git a/apps/language_server/lib/language_server/server.ex b/apps/language_server/lib/language_server/server.ex index 37abaddac..1c5cfa3e1 100644 --- a/apps/language_server/lib/language_server/server.ex +++ b/apps/language_server/lib/language_server/server.ex @@ -35,6 +35,7 @@ defmodule ElixirLS.LanguageServer.Server do } alias ElixirLS.Utils.Launch + alias ElixirLS.LanguageServer.Tracer use Protocol @@ -57,6 +58,7 @@ defmodule ElixirLS.LanguageServer.Server do source_files: %{}, awaiting_contracts: [], supports_dynamic: false, + mix_project?: false, no_mixfile_warned?: false ] @@ -415,6 +417,12 @@ defmodule ElixirLS.LanguageServer.Server do state.source_files[uri].dirty?) end) + # TODO remove uniq when duplicated subscriptions from vscode plugin are fixed + for change <- changes, change["type"] == 3, uniq: true do + path = SourceFile.path_from_uri(change["uri"]) + ElixirLS.LanguageServer.Tracer.notify_file_deleted(path) + end + source_files = changes |> Enum.reduce(state.source_files, fn @@ -447,6 +455,7 @@ defmodule ElixirLS.LanguageServer.Server do state = %{state | source_files: source_files} + # TODO remove uniq when duplicated subscriptions from vscode plugin are fixed changes |> Enum.map(& &1["uri"]) |> Enum.uniq() @@ -1099,6 +1108,7 @@ defmodule ElixirLS.LanguageServer.Server do |> add_watched_extensions(additional_watched_extensions) state = create_gitignore(state) + Tracer.set_project_dir(state.project_dir) trigger_build(%{state | settings: settings}) end @@ -1211,7 +1221,10 @@ defmodule ElixirLS.LanguageServer.Server do is_nil(prev_project_dir) -> File.cd!(project_dir) - %{state | project_dir: File.cwd!()} + %{state | + project_dir: File.cwd!(), + mix_project?: File.exists?("mix.exs") + } prev_project_dir != project_dir -> JsonRpc.show_message( diff --git a/apps/language_server/lib/language_server/tracer.ex b/apps/language_server/lib/language_server/tracer.ex new file mode 100644 index 000000000..64d3d9536 --- /dev/null +++ b/apps/language_server/lib/language_server/tracer.ex @@ -0,0 +1,285 @@ +defmodule ElixirLS.LanguageServer.Tracer do + @moduledoc """ + """ + use GenServer + require Logger + + @tables ~w(modules calls)a + + for table <- @tables do + defp table_name(unquote(table)) do + :"#{__MODULE__}:#{unquote(table)}" + end + end + + def start_link(args) do + GenServer.start_link(__MODULE__, args, name: __MODULE__) + end + + def set_project_dir(project_dir) do + GenServer.call(__MODULE__, {:set_project_dir, project_dir}) + end + + defp get_project_dir() do + case Process.get(:elixir_ls_project_dir) do + nil -> + project_dir = GenServer.call(__MODULE__, :get_project_dir) + Process.put(:elixir_ls_project_dir, project_dir) + project_dir + project_dir -> project_dir + end + end + + def notify_file_deleted(file) do + delete_modules_by_file(file) + delete_calls_by_file(file) + end + + @impl true + def init(_args) do + for table <- @tables do + table_name = table_name(table) + :ets.new(table_name, [:named_table, :public, read_concurrency: true, write_concurrency: true]) + end + {:ok, %{project_dir: nil}} + end + + @impl true + def handle_call({:set_project_dir, project_dir}, _from, state) do + maybe_close_tables(state) + + if project_dir != nil do + {us, _} = :timer.tc(fn -> + for table <- @tables do + init_table(table, project_dir) + end + end) + Logger.info "Loaded DETS databases in #{div(us, 1000)}ms" + end + + {:reply, :ok, %{state | project_dir: project_dir}} + end + + def handle_call(:get_project_dir, _from, %{project_dir: project_dir} = state) do + {:reply, project_dir, state} + end + + @impl true + def terminate(_reason, state) do + maybe_close_tables(state) + end + + defp maybe_close_tables(%{project_dir: nil}), do: :ok + defp maybe_close_tables(%{project_dir: project_dir}) do + for table <- @tables do + close_table(table, project_dir) + end + :ok + end + + defp dets_path(project_dir, table) do + Path.join([project_dir, ".elixir_ls", "#{table}.dets"]) + end + + def init_table(table, project_dir) do + table_name = table_name(table) + path = dets_path(project_dir, table) + {:ok, _} = :dets.open_file(table_name, [ + file: path |> String.to_charlist, + auto_save: 60_000 + ]) + case :dets.to_ets(table_name, table_name) do + ^table_name -> :ok + {:error, reason} -> + Logger.error("Unable to load DETS #{path}, #{inspect(reason)}") + end + end + + def close_table(table, project_dir) do + path = dets_path(project_dir, table) + # TODO sync ets + case :dets.close(table) do + :ok -> :ok + {:error, reason} -> + Logger.error("Unable to close DETS #{path}, #{inspect(reason)}") + end + :ets.delete(table) + end + + defp modules_by_file_matchspec(file, return) do + [ + {{:"$1", :"$2"}, + [ + { + :andalso, + {:andalso, {:==, {:map_get, :file, :"$2"}, file}} + } + ], [return]} + ] + end + + def get_modules_by_file(file) do + ms = modules_by_file_matchspec(file, :"$_") + # ms = :ets.fun2ms(fn {_, map} when :erlang.map_get(:file, map) == file -> map end) + + :ets.select(table_name(:modules), ms) + end + + def delete_modules_by_file(file) do + ms = modules_by_file_matchspec(file, true) + # ms = :ets.fun2ms(fn {_, map} when :erlang.map_get(:file, map) == file -> true end) + + :ets.select_delete(table_name(:modules), ms) + end + + def trace(:start, %Macro.Env{} = env) do + delete_modules_by_file(env.file) + delete_calls_by_file(env.file) + :ok + end + + # def trace(:stop, %Macro.Env{} = env) do + # :ok + # end + + def trace({:on_module, _, _}, %Macro.Env{} = env) do + info = build_module_info(env.module, env.file, env.line) + :ets.insert(table_name(:modules), {env.module, info}) + :ok + end + + def trace({kind, meta, module, name, arity}, env) when kind in [:imported_function, :imported_macro, :remote_function, :remote_macro] do + register_call(meta, module, name, arity, env) + end + + # TODO def trace({kind, meta, name, arity}, env) when kind in [:local_function, :local_macro] + + def trace(_trace, _env) do + :ok + end + + defp build_module_info(module, file, line) do + defs = for {name, arity} <- Module.definitions_in(module) do + def_info = Module.get_definition(module, {name, arity}) + {{name, arity}, build_def_info(def_info)} + end + + attributes = for name <- Module.attributes_in(module) do + {name, Module.get_attribute(module, name)} + end + + %{ + defs: defs, + attributes: attributes, + file: file, + line: line + } + end + + defp build_def_info({:v1, def_kind, meta_1, clauses}) do + clauses = for {meta_2, arguments, guards, _body} <- clauses do + %{ + arguments: arguments, + guards: guards, + meta: meta_2 + } + end + %{ + kind: def_kind, + clauses: clauses, + meta: meta_1 + } + end + + defp register_call(meta, module, name, arity, env) do + if in_project_sources?(env.file) do + do_register_call(meta, module, name, arity, env) + end + :ok + end + + defp do_register_call(meta, module, name, arity, env) do + callee = {module, name, arity} + call = %{ + callee: callee, + file: env.file, + line: meta[:line], + column: meta[:column] + } + updated_calls = case :ets.lookup(table_name(:calls), callee) do + [{_callee, callee_calls}] when is_map(callee_calls) -> + file_calle_calls = case callee_calls[env.file] do + nil -> [call] + file_calle_calls -> [call | file_calle_calls] + end + Map.put(callee_calls, env.file, file_calle_calls) + [] -> %{env.file => [call]} + end + + :ets.insert(table_name(:calls), {callee, updated_calls}) + end + + def get_trace do + :ets.tab2list(table_name(:calls)) + |> Map.new(fn {callee, calls_by_file} -> + calls = calls_by_file |> Map.values |> List.flatten + {callee, calls} + end) + end + + def save do + for table <- @tables do + table_name = table_name(table) + with :ok <- :dets.from_ets(table_name, table_name), + :ok <- :dets.sync(table_name) do + :ok + else + {:error, reason} -> + Logger.error("Unable to sync DETS #{table_name}, #{inspect(reason)}") + end + end + end + + defp in_project_sources?(path) do + project_dir = get_project_dir() + + topmost_path_segment = path + |> Path.relative_to(project_dir) + |> Path.split + |> hd + + topmost_path_segment != "deps" + end + + defp calls_by_file_matchspec(file, return) do + [ + {{:"$1", :"$2"}, + [ + { + :andalso, + {:andalso, {:is_list, {:map_get, file, :"$2"}}} + } + ], [return]} + ] + end + + def get_calls_by_file(file) do + ms = calls_by_file_matchspec(file, :"$_") + + :ets.select(table_name(:calls), ms) + end + + def delete_calls_by_file(file) do + ms = calls_by_file_matchspec(file, :"$_") + table_name = table_name(:calls) + + for {callee, calls_by_file} <- :ets.select(table_name(:calls), ms) do + calls_by_file = calls_by_file |> Map.delete(file) + if calls_by_file == %{} do + :ets.delete(table_name, callee) + else + :ets.insert(table_name, {callee, calls_by_file}) + end + end + end +end diff --git a/apps/language_server/test/providers/references_test.exs b/apps/language_server/test/providers/references_test.exs index 334a9e55b..90d495967 100644 --- a/apps/language_server/test/providers/references_test.exs +++ b/apps/language_server/test/providers/references_test.exs @@ -4,9 +4,17 @@ defmodule ElixirLS.LanguageServer.Providers.ReferencesTest do alias ElixirLS.LanguageServer.Providers.References alias ElixirLS.LanguageServer.SourceFile alias ElixirLS.LanguageServer.Test.FixtureHelpers + alias ElixirLS.LanguageServer.Tracer require ElixirLS.Test.TextLoc - test "finds references to a function" do + setup context do + {:ok, _pid} = Tracer.start_link([]) + Tracer.set_project_dir(FixtureHelpers.get_path("")) + Code.compile_file(FixtureHelpers.get_path("references_b.ex")) + {:ok, context} + end + + test "finds references to a variable" do file_path = FixtureHelpers.get_path("references_b.ex") text = File.read!(file_path) uri = SourceFile.path_to_uri(file_path) diff --git a/mix.lock b/mix.lock index 30f8eac22..869f92dd8 100644 --- a/mix.lock +++ b/mix.lock @@ -1,7 +1,7 @@ %{ "dialyxir": {:hex, :dialyxir, "1.1.0", "c5aab0d6e71e5522e77beff7ba9e08f8e02bad90dfbeffae60eaf0cb47e29488", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "07ea8e49c45f15264ebe6d5b93799d4dd56a44036cf42d0ad9c960bc266c0b9a"}, "docsh": {:hex, :docsh, "0.7.2", "f893d5317a0e14269dd7fe79cf95fb6b9ba23513da0480ec6e77c73221cae4f2", [:rebar3], [{:providers, "1.8.1", [hex: :providers, repo: "hexpm", optional: false]}], "hexpm", "4e7db461bb07540d2bc3d366b8513f0197712d0495bb85744f367d3815076134"}, - "elixir_sense": {:git, "https://github.com/elixir-lsp/elixir_sense.git", "08bac2a5a0ad867908d77a7087eac756bf7cce43", []}, + "elixir_sense": {:git, "https://github.com/elixir-lsp/elixir_sense.git", "12bdf7ac9971a9f1cb278b66364f912d63af4c0f", []}, "erl2ex": {:git, "https://github.com/dazuma/erl2ex.git", "244c2d9ed5805ef4855a491d8616b8842fef7ca4", []}, "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, "getopt": {:hex, :getopt, "1.0.1", "c73a9fa687b217f2ff79f68a3b637711bb1936e712b521d8ce466b29cbf7808a", [:rebar3], [], "hexpm", "53e1ab83b9ceb65c9672d3e7a35b8092e9bdc9b3ee80721471a161c10c59959c"}, From cf4124a12c2b33fb8eefc11e014b745e351f5087 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Thu, 1 Sep 2022 10:29:35 +0200 Subject: [PATCH 02/12] server test fixed --- .../lib/language_server/build.ex | 7 ++++++ .../lib/language_server/cli.ex | 6 ++--- .../lib/language_server/server.ex | 7 +++--- .../lib/language_server/tracer.ex | 24 ++++++++++++------- apps/language_server/test/server_test.exs | 14 +++++++++-- 5 files changed, 41 insertions(+), 17 deletions(-) diff --git a/apps/language_server/lib/language_server/build.ex b/apps/language_server/lib/language_server/build.ex index 03b55e63e..dc7971985 100644 --- a/apps/language_server/lib/language_server/build.ex +++ b/apps/language_server/lib/language_server/build.ex @@ -214,4 +214,11 @@ defmodule ElixirLS.LanguageServer.Build do :ok end + + def set_compiler_options() do + Code.put_compiler_option(:tracers, [ + Tracer + ]) + Code.put_compiler_option(:parser_options, [columns: true, token_metadata: true]) + end end diff --git a/apps/language_server/lib/language_server/cli.ex b/apps/language_server/lib/language_server/cli.ex index 001f67b13..f747e5841 100644 --- a/apps/language_server/lib/language_server/cli.ex +++ b/apps/language_server/lib/language_server/cli.ex @@ -1,15 +1,13 @@ defmodule ElixirLS.LanguageServer.CLI do alias ElixirLS.Utils.{WireProtocol, Launch} alias ElixirLS.LanguageServer.JsonRpc + alias ElixirLS.LanguageServer.Build def main do WireProtocol.intercept_output(&JsonRpc.print/1, &JsonRpc.print_err/1) Launch.start_mix() - Code.put_compiler_option(:tracers, [ - ElixirLS.LanguageServer.Tracer - ]) - Code.put_compiler_option(:parser_options, [columns: true, token_metadata: true]) + Build.set_compiler_options() start_language_server() diff --git a/apps/language_server/lib/language_server/server.ex b/apps/language_server/lib/language_server/server.ex index 1c5cfa3e1..8e261261a 100644 --- a/apps/language_server/lib/language_server/server.ex +++ b/apps/language_server/lib/language_server/server.ex @@ -418,9 +418,10 @@ defmodule ElixirLS.LanguageServer.Server do end) # TODO remove uniq when duplicated subscriptions from vscode plugin are fixed - for change <- changes, change["type"] == 3, uniq: true do - path = SourceFile.path_from_uri(change["uri"]) - ElixirLS.LanguageServer.Tracer.notify_file_deleted(path) + deleted_paths = for change <- changes, change["type"] == 3, uniq: true, do: + SourceFile.path_from_uri(change["uri"]) + for path <- deleted_paths do + Tracer.notify_file_deleted(path) end source_files = diff --git a/apps/language_server/lib/language_server/tracer.ex b/apps/language_server/lib/language_server/tracer.ex index 64d3d9536..32a0a7839 100644 --- a/apps/language_server/lib/language_server/tracer.ex +++ b/apps/language_server/lib/language_server/tracer.ex @@ -47,6 +47,10 @@ defmodule ElixirLS.LanguageServer.Tracer do @impl true def handle_call({:set_project_dir, project_dir}, _from, state) do maybe_close_tables(state) + for table <- @tables do + table_name = table_name(table) + :ets.delete_all_objects(table_name) + end if project_dir != nil do {us, _} = :timer.tc(fn -> @@ -98,12 +102,12 @@ defmodule ElixirLS.LanguageServer.Tracer do def close_table(table, project_dir) do path = dets_path(project_dir, table) # TODO sync ets - case :dets.close(table) do + case :dets.close(table_name(table)) do :ok -> :ok {:error, reason} -> Logger.error("Unable to close DETS #{path}, #{inspect(reason)}") end - :ets.delete(table) + end defp modules_by_file_matchspec(file, return) do @@ -243,12 +247,16 @@ defmodule ElixirLS.LanguageServer.Tracer do defp in_project_sources?(path) do project_dir = get_project_dir() - topmost_path_segment = path - |> Path.relative_to(project_dir) - |> Path.split - |> hd - - topmost_path_segment != "deps" + if project_dir != nil do + topmost_path_segment = path + |> Path.relative_to(project_dir) + |> Path.split + |> hd + + topmost_path_segment != "deps" + else + false + end end defp calls_by_file_matchspec(file, return) do diff --git a/apps/language_server/test/server_test.exs b/apps/language_server/test/server_test.exs index c9faf3b0e..11a9c8a52 100644 --- a/apps/language_server/test/server_test.exs +++ b/apps/language_server/test/server_test.exs @@ -1,5 +1,5 @@ defmodule ElixirLS.LanguageServer.ServerTest do - alias ElixirLS.LanguageServer.{Server, Protocol, SourceFile} + alias ElixirLS.LanguageServer.{Server, Protocol, SourceFile, Tracer, Build} alias ElixirLS.Utils.PacketCapture alias ElixirLS.LanguageServer.Test.FixtureHelpers use ElixirLS.Utils.MixTest.Case, async: false @@ -231,8 +231,9 @@ defmodule ElixirLS.LanguageServer.ServerTest do setup context do unless context[:skip_server] do server = ElixirLS.LanguageServer.Test.ServerTestHelpers.start_server() + {:ok, tracer} = Tracer.start_link([]) - {:ok, %{server: server}} + {:ok, %{server: server, tracer: tracer}} else :ok end @@ -1100,7 +1101,10 @@ defmodule ElixirLS.LanguageServer.ServerTest do text = File.read!(file_path) reference_uri = SourceFile.path_to_uri("lib/a.ex") + Build.set_compiler_options() + initialize(server) + wait_until_compiled(server) Server.receive_packet(server, did_open(file_uri, "elixir", 1, text)) Server.receive_packet( @@ -1129,7 +1133,12 @@ defmodule ElixirLS.LanguageServer.ServerTest do text = File.read!(file_path) reference_uri = SourceFile.path_to_uri("apps/app1/lib/app1.ex") + Build.set_compiler_options() + + for change <- text, uniq: true, do: :ok + initialize(server) + wait_until_compiled(server) Server.receive_packet(server, did_open(file_uri, "elixir", 1, text)) Server.receive_packet( @@ -1157,6 +1166,7 @@ defmodule ElixirLS.LanguageServer.ServerTest do # First to compile the applications and build the cache. # Second time to see if loads modules with_new_server(fn server -> + Tracer.start_link([]) initialize(server) wait_until_compiled(server) end) From 2cb5d7295b872d92b5f7e505206f4018ca95da05 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sat, 3 Sep 2022 11:01:05 +0200 Subject: [PATCH 03/12] fix dialyzer tests --- apps/language_server/lib/language_server/server.ex | 4 +++- apps/language_server/test/dialyzer_test.exs | 4 +++- apps/language_server/test/server_test.exs | 2 -- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/apps/language_server/lib/language_server/server.ex b/apps/language_server/lib/language_server/server.ex index 8e261261a..d23455ce5 100644 --- a/apps/language_server/lib/language_server/server.ex +++ b/apps/language_server/lib/language_server/server.ex @@ -1053,7 +1053,9 @@ defmodule ElixirLS.LanguageServer.Server do text {:error, reason} -> - IO.warn("Couldn't read file #{file}: #{inspect(reason)}") + if reason != :enoent do + IO.warn("Couldn't read file #{file}: #{inspect(reason)}") + end nil end end diff --git a/apps/language_server/test/dialyzer_test.exs b/apps/language_server/test/dialyzer_test.exs index fa4d516af..45db08c30 100644 --- a/apps/language_server/test/dialyzer_test.exs +++ b/apps/language_server/test/dialyzer_test.exs @@ -1,7 +1,7 @@ defmodule ElixirLS.LanguageServer.DialyzerTest do # TODO: Test loading and saving manifest - alias ElixirLS.LanguageServer.{Dialyzer, Server, Protocol, SourceFile, JsonRpc} + alias ElixirLS.LanguageServer.{Dialyzer, Server, Protocol, SourceFile, JsonRpc, Tracer, Build} import ExUnit.CaptureLog use ElixirLS.Utils.MixTest.Case, async: false use Protocol @@ -10,10 +10,12 @@ defmodule ElixirLS.LanguageServer.DialyzerTest do # This will generate a large PLT file and will take a long time, so we need to make sure that # Mix.Utils.home() is in the saved build artifacts for any automated testing Dialyzer.Manifest.load_elixir_plt() + Build.set_compiler_options() {:ok, %{}} end setup do + Tracer.start_link([]) server = ElixirLS.LanguageServer.Test.ServerTestHelpers.start_server() {:ok, %{server: server}} diff --git a/apps/language_server/test/server_test.exs b/apps/language_server/test/server_test.exs index 11a9c8a52..152212e7d 100644 --- a/apps/language_server/test/server_test.exs +++ b/apps/language_server/test/server_test.exs @@ -1135,8 +1135,6 @@ defmodule ElixirLS.LanguageServer.ServerTest do Build.set_compiler_options() - for change <- text, uniq: true, do: :ok - initialize(server) wait_until_compiled(server) Server.receive_packet(server, did_open(file_uri, "elixir", 1, text)) From f667e5ec1625970e1a1e26cd74d1741d824e0596 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sat, 3 Sep 2022 11:44:08 +0200 Subject: [PATCH 04/12] fix references tests --- apps/language_server/lib/language_server/build.ex | 14 ++++++++++---- .../language_server/lib/language_server/tracer.ex | 3 ++- .../test/providers/references_test.exs | 15 ++++++++++----- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/apps/language_server/lib/language_server/build.ex b/apps/language_server/lib/language_server/build.ex index dc7971985..28d8775ff 100644 --- a/apps/language_server/lib/language_server/build.ex +++ b/apps/language_server/lib/language_server/build.ex @@ -215,10 +215,16 @@ defmodule ElixirLS.LanguageServer.Build do :ok end - def set_compiler_options() do - Code.put_compiler_option(:tracers, [ - Tracer + def set_compiler_options(options \\ [], parser_options \\ []) do + parser_options = parser_options |> Keyword.merge([ + columns: true, + token_metadata: true ]) - Code.put_compiler_option(:parser_options, [columns: true, token_metadata: true]) + options = options |> Keyword.merge([ + tracers: [Tracer], + parser_options: parser_options + ]) + + Code.compiler_options(options) end end diff --git a/apps/language_server/lib/language_server/tracer.ex b/apps/language_server/lib/language_server/tracer.ex index 32a0a7839..25fc94aac 100644 --- a/apps/language_server/lib/language_server/tracer.ex +++ b/apps/language_server/lib/language_server/tracer.ex @@ -158,7 +158,8 @@ defmodule ElixirLS.LanguageServer.Tracer do # TODO def trace({kind, meta, name, arity}, env) when kind in [:local_function, :local_macro] - def trace(_trace, _env) do + def trace(trace, _env) do + IO.inspect(trace, label: "skipped") :ok end diff --git a/apps/language_server/test/providers/references_test.exs b/apps/language_server/test/providers/references_test.exs index 90d495967..0a7879023 100644 --- a/apps/language_server/test/providers/references_test.exs +++ b/apps/language_server/test/providers/references_test.exs @@ -5,24 +5,29 @@ defmodule ElixirLS.LanguageServer.Providers.ReferencesTest do alias ElixirLS.LanguageServer.SourceFile alias ElixirLS.LanguageServer.Test.FixtureHelpers alias ElixirLS.LanguageServer.Tracer + alias ElixirLS.LanguageServer.Build require ElixirLS.Test.TextLoc - setup context do - {:ok, _pid} = Tracer.start_link([]) + setup_all context do + Tracer.start_link([]) Tracer.set_project_dir(FixtureHelpers.get_path("")) + Build.set_compiler_options(ignore_module_conflict: true) Code.compile_file(FixtureHelpers.get_path("references_b.ex")) + Code.compile_file(FixtureHelpers.get_path("uses_macro_a.ex")) + Code.compile_file(FixtureHelpers.get_path("macro_a.ex")) + Code.compile_file(FixtureHelpers.get_path("references_a.ex")) {:ok, context} end - test "finds references to a variable" do + test "finds references to a function" do file_path = FixtureHelpers.get_path("references_b.ex") text = File.read!(file_path) uri = SourceFile.path_to_uri(file_path) - {line, char} = {2, 8} + {line, char} = {1, 8} ElixirLS.Test.TextLoc.annotate_assert(file_path, line, char, """ - some_var = 42 + def b_fun do ^ """) From c68707b9f36e61fc7a6001304c9c90b123abe77d Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sat, 3 Sep 2022 12:17:27 +0200 Subject: [PATCH 05/12] add tracing of local calls --- .../lib/language_server/tracer.ex | 7 +- .../test/providers/references_test.exs | 92 +++++++++++++------ .../test/support/fixtures/macro_a.ex | 1 + .../test/support/fixtures/references_a.ex | 5 - .../test/support/fixtures/references_b.ex | 8 -- .../support/fixtures/references_imported.ex | 13 +++ .../support/fixtures/references_referenced.ex | 26 ++++++ .../support/fixtures/references_remote.ex | 13 +++ 8 files changed, 124 insertions(+), 41 deletions(-) delete mode 100644 apps/language_server/test/support/fixtures/references_a.ex delete mode 100644 apps/language_server/test/support/fixtures/references_b.ex create mode 100644 apps/language_server/test/support/fixtures/references_imported.ex create mode 100644 apps/language_server/test/support/fixtures/references_referenced.ex create mode 100644 apps/language_server/test/support/fixtures/references_remote.ex diff --git a/apps/language_server/lib/language_server/tracer.ex b/apps/language_server/lib/language_server/tracer.ex index 25fc94aac..65a3b93a8 100644 --- a/apps/language_server/lib/language_server/tracer.ex +++ b/apps/language_server/lib/language_server/tracer.ex @@ -142,6 +142,7 @@ defmodule ElixirLS.LanguageServer.Tracer do :ok end + # TODO sync on stop? # def trace(:stop, %Macro.Env{} = env) do # :ok # end @@ -152,11 +153,13 @@ defmodule ElixirLS.LanguageServer.Tracer do :ok end - def trace({kind, meta, module, name, arity}, env) when kind in [:imported_function, :imported_macro, :remote_function, :remote_macro] do + def trace({kind, meta, module, name, arity}, %Macro.Env{} = env) when kind in [:imported_function, :imported_macro, :remote_function, :remote_macro] do register_call(meta, module, name, arity, env) end - # TODO def trace({kind, meta, name, arity}, env) when kind in [:local_function, :local_macro] + def trace({kind, meta, name, arity}, %Macro.Env{} = env) when kind in [:local_function, :local_macro] do + register_call(meta, env.module, name, arity, env) + end def trace(trace, _env) do IO.inspect(trace, label: "skipped") diff --git a/apps/language_server/test/providers/references_test.exs b/apps/language_server/test/providers/references_test.exs index 0a7879023..737b3119d 100644 --- a/apps/language_server/test/providers/references_test.exs +++ b/apps/language_server/test/providers/references_test.exs @@ -9,18 +9,20 @@ defmodule ElixirLS.LanguageServer.Providers.ReferencesTest do require ElixirLS.Test.TextLoc setup_all context do + File.rm_rf!(FixtureHelpers.get_path(".elixir_ls/calls.dets")) Tracer.start_link([]) Tracer.set_project_dir(FixtureHelpers.get_path("")) Build.set_compiler_options(ignore_module_conflict: true) - Code.compile_file(FixtureHelpers.get_path("references_b.ex")) + Code.compile_file(FixtureHelpers.get_path("references_referenced.ex")) + Code.compile_file(FixtureHelpers.get_path("references_imported.ex")) + Code.compile_file(FixtureHelpers.get_path("references_remote.ex")) Code.compile_file(FixtureHelpers.get_path("uses_macro_a.ex")) Code.compile_file(FixtureHelpers.get_path("macro_a.ex")) - Code.compile_file(FixtureHelpers.get_path("references_a.ex")) {:ok, context} end - test "finds references to a function" do - file_path = FixtureHelpers.get_path("references_b.ex") + test "finds local, remote and imported references to a function" do + file_path = FixtureHelpers.get_path("references_referenced.ex") text = File.read!(file_path) uri = SourceFile.path_to_uri(file_path) @@ -31,28 +33,35 @@ defmodule ElixirLS.LanguageServer.Providers.ReferencesTest do ^ """) - ElixirLS.Utils.TestUtils.assert_match_list( - References.references(text, uri, line, char, true), - [ - %{ - "range" => %{ - "start" => %{"line" => 2, "character" => 4}, - "end" => %{"line" => 2, "character" => 12} - }, - "uri" => uri - }, - %{ - "range" => %{ - "start" => %{"line" => 4, "character" => 12}, - "end" => %{"line" => 4, "character" => 20} - }, - "uri" => uri - } - ] - ) + list = References.references(text, uri, line, char, true) + + assert length(list) == 3 + assert Enum.any?(list, & &1["uri"] |> String.ends_with?("references_remote.ex")) + assert Enum.any?(list, & &1["uri"] |> String.ends_with?("references_imported.ex")) + assert Enum.any?(list, & &1["uri"] |> String.ends_with?("references_referenced.ex")) + end + + test "finds local, remote and imported references to a macro" do + file_path = FixtureHelpers.get_path("references_referenced.ex") + text = File.read!(file_path) + uri = SourceFile.path_to_uri(file_path) + + {line, char} = {8, 12} + + ElixirLS.Test.TextLoc.annotate_assert(file_path, line, char, """ + defmacro macro_unless(clause, do: expression) do + ^ + """) + + list = References.references(text, uri, line, char, true) + + assert length(list) == 3 + assert Enum.any?(list, & &1["uri"] |> String.ends_with?("references_remote.ex")) + assert Enum.any?(list, & &1["uri"] |> String.ends_with?("references_imported.ex")) + assert Enum.any?(list, & &1["uri"] |> String.ends_with?("references_referenced.ex")) end - test "cannot find a references to a macro generated function call" do + test "find a references to a macro generated function call" do file_path = FixtureHelpers.get_path("uses_macro_a.ex") text = File.read!(file_path) uri = SourceFile.path_to_uri(file_path) @@ -63,7 +72,9 @@ defmodule ElixirLS.LanguageServer.Providers.ReferencesTest do ^ """) - assert References.references(text, uri, line, char, true) == [] + assert References.references(text, uri, line, char, true) == [ + %{"range" => %{"end" => %{"character" => 16, "line" => 6}, "start" => %{"character" => 4, "line" => 6}}, "uri" => uri} + ] end test "finds a references to a macro imported function call" do @@ -89,7 +100,7 @@ defmodule ElixirLS.LanguageServer.Providers.ReferencesTest do end test "finds references to a variable" do - file_path = FixtureHelpers.get_path("references_b.ex") + file_path = FixtureHelpers.get_path("references_referenced.ex") text = File.read!(file_path) uri = SourceFile.path_to_uri(file_path) {line, char} = {4, 14} @@ -116,4 +127,33 @@ defmodule ElixirLS.LanguageServer.Providers.ReferencesTest do } ] end + + test "finds references to an attribute" do + file_path = FixtureHelpers.get_path("references_referenced.ex") + text = File.read!(file_path) + uri = SourceFile.path_to_uri(file_path) + {line, char} = {20, 5} + + ElixirLS.Test.TextLoc.annotate_assert(file_path, line, char, """ + @some \"123\" + ^ + """) + + assert References.references(text, uri, line, char, true) == [ + %{ + "range" => %{ + "end" => %{"character" => 7, "line" => 20}, + "start" => %{"character" => 2, "line" => 20} + }, + "uri" => uri + }, + %{ + "range" => %{ + "end" => %{"character" => 9, "line" => 23}, + "start" => %{"character" => 4, "line" => 23} + }, + "uri" => uri + } + ] + end end diff --git a/apps/language_server/test/support/fixtures/macro_a.ex b/apps/language_server/test/support/fixtures/macro_a.ex index 882d0f8c6..d366c84a3 100644 --- a/apps/language_server/test/support/fixtures/macro_a.ex +++ b/apps/language_server/test/support/fixtures/macro_a.ex @@ -6,6 +6,7 @@ defmodule ElixirLS.Test.MacroA do def macro_a_func do :ok end + end end diff --git a/apps/language_server/test/support/fixtures/references_a.ex b/apps/language_server/test/support/fixtures/references_a.ex deleted file mode 100644 index c9087d4d6..000000000 --- a/apps/language_server/test/support/fixtures/references_a.ex +++ /dev/null @@ -1,5 +0,0 @@ -defmodule ElixirLS.Test.ReferencesA do - def a_fun do - ElixirLS.Test.ReferencesB.b_fun() - end -end diff --git a/apps/language_server/test/support/fixtures/references_b.ex b/apps/language_server/test/support/fixtures/references_b.ex deleted file mode 100644 index 0bbb1f3c6..000000000 --- a/apps/language_server/test/support/fixtures/references_b.ex +++ /dev/null @@ -1,8 +0,0 @@ -defmodule ElixirLS.Test.ReferencesB do - def b_fun do - some_var = 42 - - IO.puts(some_var + 1) - :ok - end -end diff --git a/apps/language_server/test/support/fixtures/references_imported.ex b/apps/language_server/test/support/fixtures/references_imported.ex new file mode 100644 index 000000000..8e5e73d8b --- /dev/null +++ b/apps/language_server/test/support/fixtures/references_imported.ex @@ -0,0 +1,13 @@ +defmodule ElixirLS.Test.ReferencesImported do + import ElixirLS.Test.ReferencesReferenced + + def a_fun do + b_fun() + end + + def b_fun(a) do + macro_unless a do + :ok + end + end +end diff --git a/apps/language_server/test/support/fixtures/references_referenced.ex b/apps/language_server/test/support/fixtures/references_referenced.ex new file mode 100644 index 000000000..72745d41f --- /dev/null +++ b/apps/language_server/test/support/fixtures/references_referenced.ex @@ -0,0 +1,26 @@ +defmodule ElixirLS.Test.ReferencesReferenced do + def b_fun do + some_var = 42 + + IO.puts(some_var + 1) + :ok + end + + defmacro macro_unless(clause, do: expression) do + quote do + if(!unquote(clause), do: unquote(expression)) + end + end + + def local(a) do + macro_unless a do + b_fun + end + end + + @some "123" + + def use_attribute do + @some + end +end diff --git a/apps/language_server/test/support/fixtures/references_remote.ex b/apps/language_server/test/support/fixtures/references_remote.ex new file mode 100644 index 000000000..1f78c46ed --- /dev/null +++ b/apps/language_server/test/support/fixtures/references_remote.ex @@ -0,0 +1,13 @@ +defmodule ElixirLS.Test.ReferencesRemote do + require ElixirLS.Test.ReferencesReferenced, as: ReferencesReferenced + + def a_fun do + ReferencesReferenced.b_fun() + end + + def b_fun(a) do + ReferencesReferenced.macro_unless a do + :ok + end + end +end From 58c7e55ea99f09c7036cb26362706805cc6b898f Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sat, 3 Sep 2022 12:44:43 +0200 Subject: [PATCH 06/12] fix and extend definition tests --- .../lib/language_server/build.ex | 21 +- .../language_server/providers/references.ex | 1 + .../lib/language_server/server.ex | 9 +- .../lib/language_server/tracer.ex | 129 ++++++++----- .../test/providers/definition_test.exs | 180 +++++++++++++++++- .../test/providers/references_test.exs | 44 +++-- .../test/support/fixtures/macro_a.ex | 1 - .../support/fixtures/references_imported.ex | 8 +- .../support/fixtures/references_referenced.ex | 24 ++- .../support/fixtures/references_remote.ex | 8 +- 10 files changed, 325 insertions(+), 100 deletions(-) diff --git a/apps/language_server/lib/language_server/build.ex b/apps/language_server/lib/language_server/build.ex index 28d8775ff..622910dde 100644 --- a/apps/language_server/lib/language_server/build.ex +++ b/apps/language_server/lib/language_server/build.ex @@ -216,14 +216,19 @@ defmodule ElixirLS.LanguageServer.Build do end def set_compiler_options(options \\ [], parser_options \\ []) do - parser_options = parser_options |> Keyword.merge([ - columns: true, - token_metadata: true - ]) - options = options |> Keyword.merge([ - tracers: [Tracer], - parser_options: parser_options - ]) + parser_options = + parser_options + |> Keyword.merge( + columns: true, + token_metadata: true + ) + + options = + options + |> Keyword.merge( + tracers: [Tracer], + parser_options: parser_options + ) Code.compiler_options(options) end diff --git a/apps/language_server/lib/language_server/providers/references.ex b/apps/language_server/lib/language_server/providers/references.ex index c242d2de8..2ca5ae513 100644 --- a/apps/language_server/lib/language_server/providers/references.ex +++ b/apps/language_server/lib/language_server/providers/references.ex @@ -18,6 +18,7 @@ defmodule ElixirLS.LanguageServer.Providers.References do Build.with_build_lock(fn -> trace = ElixirLS.LanguageServer.Tracer.get_trace() + ElixirSense.references(text, line, character, trace) |> Enum.map(fn elixir_sense_reference -> elixir_sense_reference diff --git a/apps/language_server/lib/language_server/server.ex b/apps/language_server/lib/language_server/server.ex index d23455ce5..63a467536 100644 --- a/apps/language_server/lib/language_server/server.ex +++ b/apps/language_server/lib/language_server/server.ex @@ -418,8 +418,12 @@ defmodule ElixirLS.LanguageServer.Server do end) # TODO remove uniq when duplicated subscriptions from vscode plugin are fixed - deleted_paths = for change <- changes, change["type"] == 3, uniq: true, do: - SourceFile.path_from_uri(change["uri"]) + deleted_paths = + for change <- changes, + change["type"] == 3, + uniq: true, + do: SourceFile.path_from_uri(change["uri"]) + for path <- deleted_paths do Tracer.notify_file_deleted(path) end @@ -1056,6 +1060,7 @@ defmodule ElixirLS.LanguageServer.Server do if reason != :enoent do IO.warn("Couldn't read file #{file}: #{inspect(reason)}") end + nil end end diff --git a/apps/language_server/lib/language_server/tracer.ex b/apps/language_server/lib/language_server/tracer.ex index 65a3b93a8..0c9970a7b 100644 --- a/apps/language_server/lib/language_server/tracer.ex +++ b/apps/language_server/lib/language_server/tracer.ex @@ -26,7 +26,9 @@ defmodule ElixirLS.LanguageServer.Tracer do project_dir = GenServer.call(__MODULE__, :get_project_dir) Process.put(:elixir_ls_project_dir, project_dir) project_dir - project_dir -> project_dir + + project_dir -> + project_dir end end @@ -39,26 +41,36 @@ defmodule ElixirLS.LanguageServer.Tracer do def init(_args) do for table <- @tables do table_name = table_name(table) - :ets.new(table_name, [:named_table, :public, read_concurrency: true, write_concurrency: true]) + + :ets.new(table_name, [ + :named_table, + :public, + read_concurrency: true, + write_concurrency: true + ]) end + {:ok, %{project_dir: nil}} end @impl true def handle_call({:set_project_dir, project_dir}, _from, state) do maybe_close_tables(state) + for table <- @tables do table_name = table_name(table) :ets.delete_all_objects(table_name) end if project_dir != nil do - {us, _} = :timer.tc(fn -> - for table <- @tables do - init_table(table, project_dir) - end - end) - Logger.info "Loaded DETS databases in #{div(us, 1000)}ms" + {us, _} = + :timer.tc(fn -> + for table <- @tables do + init_table(table, project_dir) + end + end) + + Logger.info("Loaded DETS databases in #{div(us, 1000)}ms") end {:reply, :ok, %{state | project_dir: project_dir}} @@ -74,10 +86,12 @@ defmodule ElixirLS.LanguageServer.Tracer do end defp maybe_close_tables(%{project_dir: nil}), do: :ok + defp maybe_close_tables(%{project_dir: project_dir}) do for table <- @tables do close_table(table, project_dir) end + :ok end @@ -88,12 +102,17 @@ defmodule ElixirLS.LanguageServer.Tracer do def init_table(table, project_dir) do table_name = table_name(table) path = dets_path(project_dir, table) - {:ok, _} = :dets.open_file(table_name, [ - file: path |> String.to_charlist, - auto_save: 60_000 - ]) + + {:ok, _} = + :dets.open_file(table_name, + file: path |> String.to_charlist(), + auto_save: 60_000 + ) + case :dets.to_ets(table_name, table_name) do - ^table_name -> :ok + ^table_name -> + :ok + {:error, reason} -> Logger.error("Unable to load DETS #{path}, #{inspect(reason)}") end @@ -103,11 +122,12 @@ defmodule ElixirLS.LanguageServer.Tracer do path = dets_path(project_dir, table) # TODO sync ets case :dets.close(table_name(table)) do - :ok -> :ok + :ok -> + :ok + {:error, reason} -> Logger.error("Unable to close DETS #{path}, #{inspect(reason)}") end - end defp modules_by_file_matchspec(file, return) do @@ -153,11 +173,13 @@ defmodule ElixirLS.LanguageServer.Tracer do :ok end - def trace({kind, meta, module, name, arity}, %Macro.Env{} = env) when kind in [:imported_function, :imported_macro, :remote_function, :remote_macro] do + def trace({kind, meta, module, name, arity}, %Macro.Env{} = env) + when kind in [:imported_function, :imported_macro, :remote_function, :remote_macro] do register_call(meta, module, name, arity, env) end - def trace({kind, meta, name, arity}, %Macro.Env{} = env) when kind in [:local_function, :local_macro] do + def trace({kind, meta, name, arity}, %Macro.Env{} = env) + when kind in [:local_function, :local_macro] do register_call(meta, env.module, name, arity, env) end @@ -167,14 +189,16 @@ defmodule ElixirLS.LanguageServer.Tracer do end defp build_module_info(module, file, line) do - defs = for {name, arity} <- Module.definitions_in(module) do - def_info = Module.get_definition(module, {name, arity}) - {{name, arity}, build_def_info(def_info)} - end + defs = + for {name, arity} <- Module.definitions_in(module) do + def_info = Module.get_definition(module, {name, arity}) + {{name, arity}, build_def_info(def_info)} + end - attributes = for name <- Module.attributes_in(module) do - {name, Module.get_attribute(module, name)} - end + attributes = + for name <- Module.attributes_in(module) do + {name, Module.get_attribute(module, name)} + end %{ defs: defs, @@ -185,13 +209,15 @@ defmodule ElixirLS.LanguageServer.Tracer do end defp build_def_info({:v1, def_kind, meta_1, clauses}) do - clauses = for {meta_2, arguments, guards, _body} <- clauses do - %{ - arguments: arguments, - guards: guards, - meta: meta_2 - } - end + clauses = + for {meta_2, arguments, guards, _body} <- clauses do + %{ + arguments: arguments, + guards: guards, + meta: meta_2 + } + end + %{ kind: def_kind, clauses: clauses, @@ -203,26 +229,34 @@ defmodule ElixirLS.LanguageServer.Tracer do if in_project_sources?(env.file) do do_register_call(meta, module, name, arity, env) end + :ok end defp do_register_call(meta, module, name, arity, env) do callee = {module, name, arity} + call = %{ callee: callee, file: env.file, line: meta[:line], column: meta[:column] } - updated_calls = case :ets.lookup(table_name(:calls), callee) do - [{_callee, callee_calls}] when is_map(callee_calls) -> - file_calle_calls = case callee_calls[env.file] do - nil -> [call] - file_calle_calls -> [call | file_calle_calls] - end - Map.put(callee_calls, env.file, file_calle_calls) - [] -> %{env.file => [call]} - end + + updated_calls = + case :ets.lookup(table_name(:calls), callee) do + [{_callee, callee_calls}] when is_map(callee_calls) -> + file_calle_calls = + case callee_calls[env.file] do + nil -> [call] + file_calle_calls -> [call | file_calle_calls] + end + + Map.put(callee_calls, env.file, file_calle_calls) + + [] -> + %{env.file => [call]} + end :ets.insert(table_name(:calls), {callee, updated_calls}) end @@ -230,7 +264,7 @@ defmodule ElixirLS.LanguageServer.Tracer do def get_trace do :ets.tab2list(table_name(:calls)) |> Map.new(fn {callee, calls_by_file} -> - calls = calls_by_file |> Map.values |> List.flatten + calls = calls_by_file |> Map.values() |> List.flatten() {callee, calls} end) end @@ -238,8 +272,9 @@ defmodule ElixirLS.LanguageServer.Tracer do def save do for table <- @tables do table_name = table_name(table) + with :ok <- :dets.from_ets(table_name, table_name), - :ok <- :dets.sync(table_name) do + :ok <- :dets.sync(table_name) do :ok else {:error, reason} -> @@ -252,10 +287,11 @@ defmodule ElixirLS.LanguageServer.Tracer do project_dir = get_project_dir() if project_dir != nil do - topmost_path_segment = path - |> Path.relative_to(project_dir) - |> Path.split - |> hd + topmost_path_segment = + path + |> Path.relative_to(project_dir) + |> Path.split() + |> hd topmost_path_segment != "deps" else @@ -287,6 +323,7 @@ defmodule ElixirLS.LanguageServer.Tracer do for {callee, calls_by_file} <- :ets.select(table_name(:calls), ms) do calls_by_file = calls_by_file |> Map.delete(file) + if calls_by_file == %{} do :ets.delete(table_name, callee) else diff --git a/apps/language_server/test/providers/definition_test.exs b/apps/language_server/test/providers/definition_test.exs index f5933fe26..4f4f927de 100644 --- a/apps/language_server/test/providers/definition_test.exs +++ b/apps/language_server/test/providers/definition_test.exs @@ -7,19 +7,19 @@ defmodule ElixirLS.LanguageServer.Providers.DefinitionTest do alias ElixirLS.LanguageServer.Test.FixtureHelpers require ElixirLS.Test.TextLoc - test "find definition" do - file_path = FixtureHelpers.get_path("references_a.ex") + test "find definition remote function call" do + file_path = FixtureHelpers.get_path("references_remote.ex") text = File.read!(file_path) uri = SourceFile.path_to_uri(file_path) - b_file_path = FixtureHelpers.get_path("references_b.ex") + b_file_path = FixtureHelpers.get_path("references_referenced.ex") b_uri = SourceFile.path_to_uri(b_file_path) - {line, char} = {2, 30} + {line, char} = {4, 28} ElixirLS.Test.TextLoc.annotate_assert(file_path, line, char, """ - ElixirLS.Test.ReferencesB.b_fun() - ^ + ReferencesReferenced.referenced_fun() + ^ """) assert {:ok, %Location{uri: ^b_uri, range: range}} = @@ -30,4 +30,172 @@ defmodule ElixirLS.LanguageServer.Providers.DefinitionTest do "end" => %{"line" => 1, "character" => 6} } end + + test "find definition remote macro call" do + file_path = FixtureHelpers.get_path("references_remote.ex") + text = File.read!(file_path) + uri = SourceFile.path_to_uri(file_path) + + b_file_path = FixtureHelpers.get_path("references_referenced.ex") + b_uri = SourceFile.path_to_uri(b_file_path) + + {line, char} = {8, 28} + + ElixirLS.Test.TextLoc.annotate_assert(file_path, line, char, """ + ReferencesReferenced.referenced_macro a do + ^ + """) + + assert {:ok, %Location{uri: ^b_uri, range: range}} = + Definition.definition(uri, text, line, char) + + assert range == %{ + "start" => %{"line" => 8, "character" => 11}, + "end" => %{"line" => 8, "character" => 11} + } + end + + test "find definition imported function call" do + file_path = FixtureHelpers.get_path("references_imported.ex") + text = File.read!(file_path) + uri = SourceFile.path_to_uri(file_path) + + b_file_path = FixtureHelpers.get_path("references_referenced.ex") + b_uri = SourceFile.path_to_uri(b_file_path) + + {line, char} = {4, 5} + + ElixirLS.Test.TextLoc.annotate_assert(file_path, line, char, """ + referenced_fun() + ^ + """) + + assert {:ok, %Location{uri: ^b_uri, range: range}} = + Definition.definition(uri, text, line, char) + + assert range == %{ + "start" => %{"line" => 1, "character" => 6}, + "end" => %{"line" => 1, "character" => 6} + } + end + + test "find definition imported macro call" do + file_path = FixtureHelpers.get_path("references_imported.ex") + text = File.read!(file_path) + uri = SourceFile.path_to_uri(file_path) + + b_file_path = FixtureHelpers.get_path("references_referenced.ex") + b_uri = SourceFile.path_to_uri(b_file_path) + + {line, char} = {8, 5} + + ElixirLS.Test.TextLoc.annotate_assert(file_path, line, char, """ + referenced_macro a do + ^ + """) + + assert {:ok, %Location{uri: ^b_uri, range: range}} = + Definition.definition(uri, text, line, char) + + assert range == %{ + "start" => %{"line" => 8, "character" => 11}, + "end" => %{"line" => 8, "character" => 11} + } + end + + test "find definition local function call" do + file_path = FixtureHelpers.get_path("references_referenced.ex") + text = File.read!(file_path) + uri = SourceFile.path_to_uri(file_path) + + b_file_path = FixtureHelpers.get_path("references_referenced.ex") + b_uri = SourceFile.path_to_uri(b_file_path) + + {line, char} = {15, 5} + + ElixirLS.Test.TextLoc.annotate_assert(file_path, line, char, """ + referenced_fun + ^ + """) + + assert {:ok, %Location{uri: ^b_uri, range: range}} = + Definition.definition(uri, text, line, char) + + assert range == %{ + "start" => %{"line" => 1, "character" => 6}, + "end" => %{"line" => 1, "character" => 6} + } + end + + test "find definition local macro call" do + file_path = FixtureHelpers.get_path("references_referenced.ex") + text = File.read!(file_path) + uri = SourceFile.path_to_uri(file_path) + + b_file_path = FixtureHelpers.get_path("references_referenced.ex") + b_uri = SourceFile.path_to_uri(b_file_path) + + {line, char} = {19, 5} + + ElixirLS.Test.TextLoc.annotate_assert(file_path, line, char, """ + referenced_macro a do + ^ + """) + + assert {:ok, %Location{uri: ^b_uri, range: range}} = + Definition.definition(uri, text, line, char) + + assert range == %{ + "start" => %{"line" => 8, "character" => 11}, + "end" => %{"line" => 8, "character" => 11} + } + end + + test "find definition variable" do + file_path = FixtureHelpers.get_path("references_referenced.ex") + text = File.read!(file_path) + uri = SourceFile.path_to_uri(file_path) + + b_file_path = FixtureHelpers.get_path("references_referenced.ex") + b_uri = SourceFile.path_to_uri(b_file_path) + + {line, char} = {4, 13} + + ElixirLS.Test.TextLoc.annotate_assert(file_path, line, char, """ + IO.puts(referenced_variable + 1) + ^ + """) + + assert {:ok, %Location{uri: ^b_uri, range: range}} = + Definition.definition(uri, text, line, char) + + assert range == %{ + "start" => %{"line" => 2, "character" => 4}, + "end" => %{"line" => 2, "character" => 4} + } + end + + test "find definition attribute" do + file_path = FixtureHelpers.get_path("references_referenced.ex") + text = File.read!(file_path) + uri = SourceFile.path_to_uri(file_path) + + b_file_path = FixtureHelpers.get_path("references_referenced.ex") + b_uri = SourceFile.path_to_uri(b_file_path) + + {line, char} = {27, 5} + + ElixirLS.Test.TextLoc.annotate_assert(file_path, line, char, """ + @referenced_attribute + ^ + """) + + assert {:ok, %Location{uri: ^b_uri, range: range}} = + Definition.definition(uri, text, line, char) + + assert range == %{ + "start" => %{"line" => 24, "character" => 2}, + "end" => %{"line" => 24, "character" => 2} + } + end end diff --git a/apps/language_server/test/providers/references_test.exs b/apps/language_server/test/providers/references_test.exs index 737b3119d..592c7dba7 100644 --- a/apps/language_server/test/providers/references_test.exs +++ b/apps/language_server/test/providers/references_test.exs @@ -29,16 +29,16 @@ defmodule ElixirLS.LanguageServer.Providers.ReferencesTest do {line, char} = {1, 8} ElixirLS.Test.TextLoc.annotate_assert(file_path, line, char, """ - def b_fun do + def referenced_fun do ^ """) list = References.references(text, uri, line, char, true) assert length(list) == 3 - assert Enum.any?(list, & &1["uri"] |> String.ends_with?("references_remote.ex")) - assert Enum.any?(list, & &1["uri"] |> String.ends_with?("references_imported.ex")) - assert Enum.any?(list, & &1["uri"] |> String.ends_with?("references_referenced.ex")) + assert Enum.any?(list, &(&1["uri"] |> String.ends_with?("references_remote.ex"))) + assert Enum.any?(list, &(&1["uri"] |> String.ends_with?("references_imported.ex"))) + assert Enum.any?(list, &(&1["uri"] |> String.ends_with?("references_referenced.ex"))) end test "finds local, remote and imported references to a macro" do @@ -49,16 +49,16 @@ defmodule ElixirLS.LanguageServer.Providers.ReferencesTest do {line, char} = {8, 12} ElixirLS.Test.TextLoc.annotate_assert(file_path, line, char, """ - defmacro macro_unless(clause, do: expression) do + defmacro referenced_macro(clause, do: expression) do ^ """) list = References.references(text, uri, line, char, true) assert length(list) == 3 - assert Enum.any?(list, & &1["uri"] |> String.ends_with?("references_remote.ex")) - assert Enum.any?(list, & &1["uri"] |> String.ends_with?("references_imported.ex")) - assert Enum.any?(list, & &1["uri"] |> String.ends_with?("references_referenced.ex")) + assert Enum.any?(list, &(&1["uri"] |> String.ends_with?("references_remote.ex"))) + assert Enum.any?(list, &(&1["uri"] |> String.ends_with?("references_imported.ex"))) + assert Enum.any?(list, &(&1["uri"] |> String.ends_with?("references_referenced.ex"))) end test "find a references to a macro generated function call" do @@ -73,8 +73,14 @@ defmodule ElixirLS.LanguageServer.Providers.ReferencesTest do """) assert References.references(text, uri, line, char, true) == [ - %{"range" => %{"end" => %{"character" => 16, "line" => 6}, "start" => %{"character" => 4, "line" => 6}}, "uri" => uri} - ] + %{ + "range" => %{ + "end" => %{"character" => 16, "line" => 6}, + "start" => %{"character" => 4, "line" => 6} + }, + "uri" => uri + } + ] end test "finds a references to a macro imported function call" do @@ -106,21 +112,21 @@ defmodule ElixirLS.LanguageServer.Providers.ReferencesTest do {line, char} = {4, 14} ElixirLS.Test.TextLoc.annotate_assert(file_path, line, char, """ - IO.puts(some_var + 1) + IO.puts(referenced_variable + 1) ^ """) assert References.references(text, uri, line, char, true) == [ %{ "range" => %{ - "end" => %{"character" => 12, "line" => 2}, + "end" => %{"character" => 23, "line" => 2}, "start" => %{"character" => 4, "line" => 2} }, "uri" => uri }, %{ "range" => %{ - "end" => %{"character" => 20, "line" => 4}, + "end" => %{"character" => 31, "line" => 4}, "start" => %{"character" => 12, "line" => 4} }, "uri" => uri @@ -132,25 +138,25 @@ defmodule ElixirLS.LanguageServer.Providers.ReferencesTest do file_path = FixtureHelpers.get_path("references_referenced.ex") text = File.read!(file_path) uri = SourceFile.path_to_uri(file_path) - {line, char} = {20, 5} + {line, char} = {24, 5} ElixirLS.Test.TextLoc.annotate_assert(file_path, line, char, """ - @some \"123\" + @referenced_attribute \"123\" ^ """) assert References.references(text, uri, line, char, true) == [ %{ "range" => %{ - "end" => %{"character" => 7, "line" => 20}, - "start" => %{"character" => 2, "line" => 20} + "end" => %{"character" => 23, "line" => 24}, + "start" => %{"character" => 2, "line" => 24} }, "uri" => uri }, %{ "range" => %{ - "end" => %{"character" => 9, "line" => 23}, - "start" => %{"character" => 4, "line" => 23} + "end" => %{"character" => 25, "line" => 27}, + "start" => %{"character" => 4, "line" => 27} }, "uri" => uri } diff --git a/apps/language_server/test/support/fixtures/macro_a.ex b/apps/language_server/test/support/fixtures/macro_a.ex index d366c84a3..882d0f8c6 100644 --- a/apps/language_server/test/support/fixtures/macro_a.ex +++ b/apps/language_server/test/support/fixtures/macro_a.ex @@ -6,7 +6,6 @@ defmodule ElixirLS.Test.MacroA do def macro_a_func do :ok end - end end diff --git a/apps/language_server/test/support/fixtures/references_imported.ex b/apps/language_server/test/support/fixtures/references_imported.ex index 8e5e73d8b..ae20fd4c8 100644 --- a/apps/language_server/test/support/fixtures/references_imported.ex +++ b/apps/language_server/test/support/fixtures/references_imported.ex @@ -1,12 +1,12 @@ defmodule ElixirLS.Test.ReferencesImported do import ElixirLS.Test.ReferencesReferenced - def a_fun do - b_fun() + def uses_fun do + referenced_fun() end - def b_fun(a) do - macro_unless a do + def uses_macro(a) do + referenced_macro a do :ok end end diff --git a/apps/language_server/test/support/fixtures/references_referenced.ex b/apps/language_server/test/support/fixtures/references_referenced.ex index 72745d41f..80da429d2 100644 --- a/apps/language_server/test/support/fixtures/references_referenced.ex +++ b/apps/language_server/test/support/fixtures/references_referenced.ex @@ -1,26 +1,30 @@ defmodule ElixirLS.Test.ReferencesReferenced do - def b_fun do - some_var = 42 + def referenced_fun do + referenced_variable = 42 - IO.puts(some_var + 1) + IO.puts(referenced_variable + 1) :ok end - defmacro macro_unless(clause, do: expression) do + defmacro referenced_macro(clause, do: expression) do quote do if(!unquote(clause), do: unquote(expression)) end end - def local(a) do - macro_unless a do - b_fun + def uses_fun(a) do + referenced_fun + end + + def uses_macro(a) do + referenced_macro a do + :ok end end - @some "123" + @referenced_attribute "123" - def use_attribute do - @some + def uses_attribute do + @referenced_attribute end end diff --git a/apps/language_server/test/support/fixtures/references_remote.ex b/apps/language_server/test/support/fixtures/references_remote.ex index 1f78c46ed..409d21201 100644 --- a/apps/language_server/test/support/fixtures/references_remote.ex +++ b/apps/language_server/test/support/fixtures/references_remote.ex @@ -1,12 +1,12 @@ defmodule ElixirLS.Test.ReferencesRemote do require ElixirLS.Test.ReferencesReferenced, as: ReferencesReferenced - def a_fun do - ReferencesReferenced.b_fun() + def uses_fun do + ReferencesReferenced.referenced_fun() end - def b_fun(a) do - ReferencesReferenced.macro_unless a do + def uses_macro(a) do + ReferencesReferenced.referenced_macro a do :ok end end From 13bbce3c6326525e69c4aa1e7c48cb2a8a88986f Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sat, 3 Sep 2022 22:56:06 +0200 Subject: [PATCH 07/12] make suite synchronous --- apps/language_server/test/providers/references_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/language_server/test/providers/references_test.exs b/apps/language_server/test/providers/references_test.exs index 592c7dba7..6e7282e9e 100644 --- a/apps/language_server/test/providers/references_test.exs +++ b/apps/language_server/test/providers/references_test.exs @@ -1,5 +1,5 @@ defmodule ElixirLS.LanguageServer.Providers.ReferencesTest do - use ExUnit.Case, async: true + use ExUnit.Case, async: false alias ElixirLS.LanguageServer.Providers.References alias ElixirLS.LanguageServer.SourceFile From ae444b1e983c2ed0267e3c091789c042d2412293 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Fri, 9 Sep 2022 08:43:50 +0200 Subject: [PATCH 08/12] add test coverage to tracer --- apps/language_server/test/tracer_test.exs | 209 ++++++++++++++++++++++ 1 file changed, 209 insertions(+) create mode 100644 apps/language_server/test/tracer_test.exs diff --git a/apps/language_server/test/tracer_test.exs b/apps/language_server/test/tracer_test.exs new file mode 100644 index 000000000..4840cfee9 --- /dev/null +++ b/apps/language_server/test/tracer_test.exs @@ -0,0 +1,209 @@ +defmodule ElixirLS.LanguageServer.TracerTest do + use ExUnit.Case, async: false + alias ElixirLS.LanguageServer.Tracer + alias ElixirLS.LanguageServer.Test.FixtureHelpers + + setup context do + File.rm_rf!(FixtureHelpers.get_path(".elixir_ls/calls.dets")) + File.rm_rf!(FixtureHelpers.get_path(".elixir_ls/modules.dets")) + {:ok, _pid} = Tracer.start_link([]) + + {:ok, context} + end + + test "project dir is nil" do + assert GenServer.call(Tracer, :get_project_dir) == nil + end + + test "set project dir" do + project_path = FixtureHelpers.get_path("") + + Tracer.set_project_dir(project_path) + + assert GenServer.call(Tracer, :get_project_dir) == project_path + end + + test "saves DETS" do + Tracer.set_project_dir(FixtureHelpers.get_path("")) + + Tracer.save() + + assert File.exists?(FixtureHelpers.get_path(".elixir_ls/calls.dets")) + assert File.exists?(FixtureHelpers.get_path(".elixir_ls/modules.dets")) + end + + describe "call trace" do + setup context do + Tracer.set_project_dir(FixtureHelpers.get_path("")) + + {:ok, context} + end + + defp sorted_calls() do + :ets.tab2list(:"#{Tracer}:calls") |> Enum.sort() + end + + test "trace is empty" do + assert sorted_calls() == [] + end + + test "registers calls same function different files" do + Tracer.trace( + {:remote_function, [line: 12, column: 2], CalledModule, :called, 1}, + %Macro.Env{ + module: CallingModule, + file: "calling_module.ex" + } + ) + + Tracer.trace( + {:remote_function, [line: 13, column: 3], CalledModule, :called, 1}, + %Macro.Env{ + module: OtherCallingModule, + file: "other_calling_module.ex" + } + ) + + assert [ + {{CalledModule, :called, 1}, + %{ + "calling_module.ex" => [ + %{ + callee: {CalledModule, :called, 1}, + column: 2, + file: "calling_module.ex", + line: 12 + } + ], + "other_calling_module.ex" => [ + %{ + callee: {CalledModule, :called, 1}, + column: 3, + file: "other_calling_module.ex", + line: 13 + } + ] + }} + ] == sorted_calls() + end + + test "registers calls same function in one file" do + Tracer.trace( + {:remote_function, [line: 12, column: 2], CalledModule, :called, 1}, + %Macro.Env{ + module: CallingModule, + file: "calling_module.ex" + } + ) + + Tracer.trace( + {:remote_function, [line: 13, column: 3], CalledModule, :called, 1}, + %Macro.Env{ + module: CallingModule, + file: "calling_module.ex" + } + ) + + assert [ + {{CalledModule, :called, 1}, + %{ + "calling_module.ex" => [ + %{ + callee: {CalledModule, :called, 1}, + column: 3, + file: "calling_module.ex", + line: 13 + }, + %{ + callee: {CalledModule, :called, 1}, + column: 2, + file: "calling_module.ex", + line: 12 + } + ] + }} + ] == sorted_calls() + end + + test "registers calls different functions" do + Tracer.trace( + {:remote_function, [line: 12, column: 2], CalledModule, :called, 1}, + %Macro.Env{ + module: CallingModule, + file: "calling_module.ex" + } + ) + + Tracer.trace( + {:remote_function, [line: 13, column: 3], CalledModule, :other_called, 1}, + %Macro.Env{ + module: OtherCallingModule, + file: "other_calling_module.ex" + } + ) + + assert [ + {{CalledModule, :called, 1}, + %{ + "calling_module.ex" => [ + %{ + callee: {CalledModule, :called, 1}, + column: 2, + file: "calling_module.ex", + line: 12 + } + ] + }}, + {{CalledModule, :other_called, 1}, + %{ + "other_calling_module.ex" => [ + %{ + callee: {CalledModule, :other_called, 1}, + column: 3, + file: "other_calling_module.ex", + line: 13 + } + ] + }} + ] == sorted_calls() + end + + test "deletes calls by file" do + Tracer.trace( + {:remote_function, [line: 12, column: 2], CalledModule, :called, 1}, + %Macro.Env{ + module: CallingModule, + file: "calling_module.ex" + } + ) + + Tracer.trace( + {:remote_function, [line: 13, column: 3], CalledModule, :called, 1}, + %Macro.Env{ + module: OtherCallingModule, + file: "other_calling_module.ex" + } + ) + + Tracer.delete_calls_by_file("other_calling_module.ex") + + assert [ + {{CalledModule, :called, 1}, + %{ + "calling_module.ex" => [ + %{ + callee: {CalledModule, :called, 1}, + column: 2, + file: "calling_module.ex", + line: 12 + } + ] + }} + ] == sorted_calls() + + Tracer.delete_calls_by_file("calling_module.ex") + + assert [] == sorted_calls() + end + end +end From 33d745d66c2abba50dc59efdfc31be01918e0cec Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Sat, 10 Sep 2022 19:17:18 +0200 Subject: [PATCH 09/12] address todos --- .../lib/language_server/tracer.ex | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/apps/language_server/lib/language_server/tracer.ex b/apps/language_server/lib/language_server/tracer.ex index 0c9970a7b..00cd5baeb 100644 --- a/apps/language_server/lib/language_server/tracer.ex +++ b/apps/language_server/lib/language_server/tracer.ex @@ -120,8 +120,10 @@ defmodule ElixirLS.LanguageServer.Tracer do def close_table(table, project_dir) do path = dets_path(project_dir, table) - # TODO sync ets - case :dets.close(table_name(table)) do + table_name = table_name(table) + sync(table_name) + + case :dets.close(table_name) do :ok -> :ok @@ -162,11 +164,6 @@ defmodule ElixirLS.LanguageServer.Tracer do :ok end - # TODO sync on stop? - # def trace(:stop, %Macro.Env{} = env) do - # :ok - # end - def trace({:on_module, _, _}, %Macro.Env{} = env) do info = build_module_info(env.module, env.file, env.line) :ets.insert(table_name(:modules), {env.module, info}) @@ -273,13 +270,17 @@ defmodule ElixirLS.LanguageServer.Tracer do for table <- @tables do table_name = table_name(table) - with :ok <- :dets.from_ets(table_name, table_name), - :ok <- :dets.sync(table_name) do - :ok - else - {:error, reason} -> - Logger.error("Unable to sync DETS #{table_name}, #{inspect(reason)}") - end + sync(table_name) + end + end + + defp sync(table_name) do + with :ok <- :dets.from_ets(table_name, table_name), + :ok <- :dets.sync(table_name) do + :ok + else + {:error, reason} -> + Logger.error("Unable to sync DETS #{table_name}, #{inspect(reason)}") end end From 85c1b40df4215ecb416cb83f5b36376fbafeb0eb Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Wed, 21 Sep 2022 21:13:24 +0200 Subject: [PATCH 10/12] fix warnings --- apps/language_server/lib/language_server/tracer.ex | 4 ++-- .../test/support/fixtures/references_referenced.ex | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/language_server/lib/language_server/tracer.ex b/apps/language_server/lib/language_server/tracer.ex index 00cd5baeb..dc2d1d618 100644 --- a/apps/language_server/lib/language_server/tracer.ex +++ b/apps/language_server/lib/language_server/tracer.ex @@ -180,8 +180,8 @@ defmodule ElixirLS.LanguageServer.Tracer do register_call(meta, env.module, name, arity, env) end - def trace(trace, _env) do - IO.inspect(trace, label: "skipped") + def trace(_trace, _env) do + # IO.inspect(trace, label: "skipped") :ok end diff --git a/apps/language_server/test/support/fixtures/references_referenced.ex b/apps/language_server/test/support/fixtures/references_referenced.ex index 80da429d2..7f422cdc0 100644 --- a/apps/language_server/test/support/fixtures/references_referenced.ex +++ b/apps/language_server/test/support/fixtures/references_referenced.ex @@ -12,8 +12,8 @@ defmodule ElixirLS.Test.ReferencesReferenced do end end - def uses_fun(a) do - referenced_fun + def uses_fun(_a) do + referenced_fun() end def uses_macro(a) do From ef8eab84913ebb65af7500eb76efc8fa9fb2c2ad Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Wed, 21 Sep 2022 21:33:50 +0200 Subject: [PATCH 11/12] fix tests --- apps/language_server/test/fixtures/.elixir_ls/.gitkeep | 0 apps/language_server/test/providers/definition_test.exs | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 apps/language_server/test/fixtures/.elixir_ls/.gitkeep diff --git a/apps/language_server/test/fixtures/.elixir_ls/.gitkeep b/apps/language_server/test/fixtures/.elixir_ls/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/apps/language_server/test/providers/definition_test.exs b/apps/language_server/test/providers/definition_test.exs index 4f4f927de..16ef598f8 100644 --- a/apps/language_server/test/providers/definition_test.exs +++ b/apps/language_server/test/providers/definition_test.exs @@ -114,7 +114,7 @@ defmodule ElixirLS.LanguageServer.Providers.DefinitionTest do {line, char} = {15, 5} ElixirLS.Test.TextLoc.annotate_assert(file_path, line, char, """ - referenced_fun + referenced_fun() ^ """) From cdff5116def7eeb2c028638d656783e108e4e0f5 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Wed, 21 Sep 2022 22:28:27 +0200 Subject: [PATCH 12/12] make tests more predictable --- .../lib/language_server/server.ex | 5 +---- apps/language_server/test/dialyzer_test.exs | 8 +++++++- .../providers/execute_command/mix_clean_test.exs | 3 ++- .../test/providers/references_test.exs | 16 +++++++++++++++- apps/language_server/test/server_test.exs | 6 +++++- .../{ => support}/fixtures/.elixir_ls/.gitkeep | 0 6 files changed, 30 insertions(+), 8 deletions(-) rename apps/language_server/test/{ => support}/fixtures/.elixir_ls/.gitkeep (100%) diff --git a/apps/language_server/lib/language_server/server.ex b/apps/language_server/lib/language_server/server.ex index 63a467536..d5fef5c7d 100644 --- a/apps/language_server/lib/language_server/server.ex +++ b/apps/language_server/lib/language_server/server.ex @@ -1229,10 +1229,7 @@ defmodule ElixirLS.LanguageServer.Server do is_nil(prev_project_dir) -> File.cd!(project_dir) - %{state | - project_dir: File.cwd!(), - mix_project?: File.exists?("mix.exs") - } + %{state | project_dir: File.cwd!(), mix_project?: File.exists?("mix.exs")} prev_project_dir != project_dir -> JsonRpc.show_message( diff --git a/apps/language_server/test/dialyzer_test.exs b/apps/language_server/test/dialyzer_test.exs index 45db08c30..2dfc944dd 100644 --- a/apps/language_server/test/dialyzer_test.exs +++ b/apps/language_server/test/dialyzer_test.exs @@ -10,12 +10,18 @@ defmodule ElixirLS.LanguageServer.DialyzerTest do # This will generate a large PLT file and will take a long time, so we need to make sure that # Mix.Utils.home() is in the saved build artifacts for any automated testing Dialyzer.Manifest.load_elixir_plt() + compiler_options = Code.compiler_options() Build.set_compiler_options() + + on_exit(fn -> + Code.compiler_options(compiler_options) + end) + {:ok, %{}} end setup do - Tracer.start_link([]) + {:ok, _} = Tracer.start_link([]) server = ElixirLS.LanguageServer.Test.ServerTestHelpers.start_server() {:ok, %{server: server}} diff --git a/apps/language_server/test/providers/execute_command/mix_clean_test.exs b/apps/language_server/test/providers/execute_command/mix_clean_test.exs index f9e55464d..a82cfe529 100644 --- a/apps/language_server/test/providers/execute_command/mix_clean_test.exs +++ b/apps/language_server/test/providers/execute_command/mix_clean_test.exs @@ -1,9 +1,10 @@ defmodule ElixirLS.LanguageServer.Providers.ExecuteCommand.MixCleanTest do - alias ElixirLS.LanguageServer.{Server, Protocol, SourceFile} + alias ElixirLS.LanguageServer.{Server, Protocol, SourceFile, Tracer} use ElixirLS.Utils.MixTest.Case, async: false use Protocol setup do + {:ok, _} = Tracer.start_link([]) server = ElixirLS.LanguageServer.Test.ServerTestHelpers.start_server() {:ok, %{server: server}} diff --git a/apps/language_server/test/providers/references_test.exs b/apps/language_server/test/providers/references_test.exs index 6e7282e9e..3882f4bc9 100644 --- a/apps/language_server/test/providers/references_test.exs +++ b/apps/language_server/test/providers/references_test.exs @@ -10,9 +10,23 @@ defmodule ElixirLS.LanguageServer.Providers.ReferencesTest do setup_all context do File.rm_rf!(FixtureHelpers.get_path(".elixir_ls/calls.dets")) - Tracer.start_link([]) + {:ok, pid} = Tracer.start_link([]) Tracer.set_project_dir(FixtureHelpers.get_path("")) + + compiler_options = Code.compiler_options() Build.set_compiler_options(ignore_module_conflict: true) + + on_exit(fn -> + Code.compiler_options(compiler_options) + Process.monitor(pid) + Process.unlink(pid) + GenServer.stop(pid) + + receive do + {:DOWN, _, _, _, _} -> :ok + end + end) + Code.compile_file(FixtureHelpers.get_path("references_referenced.ex")) Code.compile_file(FixtureHelpers.get_path("references_imported.ex")) Code.compile_file(FixtureHelpers.get_path("references_remote.ex")) diff --git a/apps/language_server/test/server_test.exs b/apps/language_server/test/server_test.exs index 152212e7d..e3d5284c4 100644 --- a/apps/language_server/test/server_test.exs +++ b/apps/language_server/test/server_test.exs @@ -1123,6 +1123,8 @@ defmodule ElixirLS.LanguageServer.ServerTest do wait_until_compiled(server) end) + after + Code.put_compiler_option(:tracers, []) end @tag :fixture @@ -1155,6 +1157,8 @@ defmodule ElixirLS.LanguageServer.ServerTest do wait_until_compiled(server) end) + after + Code.put_compiler_option(:tracers, []) end @tag fixture: true, skip_server: true @@ -1164,7 +1168,7 @@ defmodule ElixirLS.LanguageServer.ServerTest do # First to compile the applications and build the cache. # Second time to see if loads modules with_new_server(fn server -> - Tracer.start_link([]) + {:ok, _pid} = Tracer.start_link([]) initialize(server) wait_until_compiled(server) end) diff --git a/apps/language_server/test/fixtures/.elixir_ls/.gitkeep b/apps/language_server/test/support/fixtures/.elixir_ls/.gitkeep similarity index 100% rename from apps/language_server/test/fixtures/.elixir_ls/.gitkeep rename to apps/language_server/test/support/fixtures/.elixir_ls/.gitkeep