From f0c4a811c21b361c1ac9168392d23bbf7df2c820 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Thu, 5 Sep 2024 14:49:56 +0200 Subject: [PATCH] use env from metadata builder for eval and completions --- apps/debug_adapter/lib/debug_adapter/code.ex | 137 ++++++++++++++ .../debug_adapter/lib/debug_adapter/server.ex | 176 +++++++++++++----- 2 files changed, 265 insertions(+), 48 deletions(-) create mode 100644 apps/debug_adapter/lib/debug_adapter/code.ex diff --git a/apps/debug_adapter/lib/debug_adapter/code.ex b/apps/debug_adapter/lib/debug_adapter/code.ex new file mode 100644 index 000000000..15cf6452e --- /dev/null +++ b/apps/debug_adapter/lib/debug_adapter/code.ex @@ -0,0 +1,137 @@ +defmodule ElixirLS.DebugAdapter.Code do + if Version.match?(System.version(), ">= 1.14.0-dev") do + defdelegate env_for_eval(env), to: Code + else + def env_for_eval(%{lexical_tracker: pid} = env) do + new_env = %{ + env + | context: nil, + context_modules: [], + macro_aliases: [], + versioned_vars: %{} + } + + if is_pid(pid) do + if Process.alive?(pid) do + new_env + else + IO.warn(""" + an __ENV__ with outdated compilation information was given to eval, \ + call Macro.Env.prune_compile_info/1 to prune it + """) + + %{new_env | lexical_tracker: nil, tracers: []} + end + else + %{new_env | tracers: []} + end + end + + def env_for_eval(opts) when is_list(opts) do + env = elixir_env.new() + + line = + case Keyword.get(opts, :line) do + line_opt when is_integer(line_opt) -> line_opt + nil -> Map.get(env, :line) + end + + file = + case Keyword.get(opts, :file) do + file_opt when is_binary(file_opt) -> file_opt + nil -> Map.get(env, :file) + end + + module = + case Keyword.get(opts, :module) do + module_opt when is_atom(module_opt) -> module_opt + nil -> nil + end + + fa = + case Keyword.get(opts, :function) do + {function, arity} when is_atom(function) and is_integer(arity) -> {function, arity} + nil -> nil + end + + temp_tracers = + case Keyword.get(opts, :tracers) do + tracers_opt when is_list(tracers_opt) -> tracers_opt + nil -> [] + end + + aliases = + case Keyword.get(opts, :aliases) do + aliases_opt when is_list(aliases_opt) -> + IO.warn(":aliases option in eval is deprecated") + aliases_opt + + nil -> + Map.get(env, :aliases) + end + + requires = + case Keyword.get(opts, :requires) do + requires_opt when is_list(requires_opt) -> + IO.warn(":requires option in eval is deprecated") + MapSet.new(requires_opt) + + nil -> + Map.get(env, :requires) + end + + functions = + case Keyword.get(opts, :functions) do + functions_opt when is_list(functions_opt) -> + IO.warn(":functions option in eval is deprecated") + functions_opt + + nil -> + Map.get(env, :functions) + end + + macros = + case Keyword.get(opts, :macros) do + macros_opt when is_list(macros_opt) -> + IO.warn(":macros option in eval is deprecated") + macros_opt + + nil -> + Map.get(env, :macros) + end + + {lexical_tracker, tracers} = + case Keyword.get(opts, :lexical_tracker) do + pid when is_pid(pid) -> + IO.warn(":lexical_tracker option in eval is deprecated") + + if Process.alive?(pid) do + {pid, temp_tracers} + else + {nil, []} + end + + nil -> + IO.warn(":lexical_tracker option in eval is deprecated") + {nil, []} + + _ -> + {nil, temp_tracers} + end + + %{ + env + | file: file, + module: module, + function: fa, + tracers: tracers, + macros: macros, + functions: functions, + lexical_tracker: lexical_tracker, + requires: requires, + aliases: aliases, + line: line + } + end + end +end diff --git a/apps/debug_adapter/lib/debug_adapter/server.ex b/apps/debug_adapter/lib/debug_adapter/server.ex index 5498c9026..a5786d428 100644 --- a/apps/debug_adapter/lib/debug_adapter/server.ex +++ b/apps/debug_adapter/lib/debug_adapter/server.ex @@ -249,7 +249,7 @@ defmodule ElixirLS.DebugAdapter.Server do first_frame | bindings: Map.new(binding), dbg_frame?: true, - dbg_env: Code.env_for_eval(env), + dbg_env: env, module: env.module, function: env.function, file: env.file, @@ -276,7 +276,7 @@ defmodule ElixirLS.DebugAdapter.Server do messages: [], bindings: Map.new(binding), dbg_frame?: true, - dbg_env: Code.env_for_eval(env), + dbg_env: env, line: env.line || 1 } @@ -294,11 +294,7 @@ defmodule ElixirLS.DebugAdapter.Server do messages: [], bindings: %{}, dbg_frame?: true, - dbg_env: - Code.env_for_eval( - file: file, - line: line - ), + dbg_env: nil, line: line } end @@ -1335,7 +1331,8 @@ defmodule ElixirLS.DebugAdapter.Server do end async_fn = fn -> - {binding, env_for_eval} = binding_and_env(state.paused_processes, args["frameId"]) + frame = frame_for_eval(state.paused_processes, args["frameId"]) + {_metadata, _env, binding, env_for_eval} = binding_and_env(state.paused_processes, frame) value = evaluate_code_expression(expr, binding, env_for_eval) child_type = Variables.child_type(value) @@ -1449,23 +1446,18 @@ defmodule ElixirLS.DebugAdapter.Server do column = Utils.dap_character_to_elixir(line, column) prefix = String.slice(line, 0, column) - {binding, _env_for_eval} = - binding_and_env(state.paused_processes, args["arguments"]["frameId"]) + frame = frame_for_eval(state.paused_processes, args["arguments"]["frameId"]) - vars = - binding - |> Enum.map(fn {name, value} -> - %ElixirSense.Core.State.VarInfo{ - name: name, - type: ElixirSense.Core.Binding.from_var(value) - } - end) + {metadata, env, _binding, _env_for_eval} = binding_and_env(state.paused_processes, frame) - env = %ElixirSense.Core.State.Env{vars: vars} - metadata = %ElixirSense.Core.Metadata{} + cursor_position = + case frame do + nil -> {1, 1} + frame -> {frame.line, 1} + end results = - ElixirLS.Utils.CompletionEngine.complete(prefix, env, metadata, {1, 1}) + ElixirLS.Utils.CompletionEngine.complete(prefix, env, metadata, cursor_position) |> Enum.map(&ElixirLS.DebugAdapter.Completions.map/1) %{"targets" => results} @@ -1609,7 +1601,9 @@ defmodule ElixirLS.DebugAdapter.Server do defp evaluate_code_expression(expr, binding, env_or_opts) do try do - {term, _bindings} = Code.eval_string(expr, binding, env_or_opts) + # TODO use Code.env_for_eval when we require elixir 1.14 + env = ElixirLS.DebugAdapter.Code.env_for_eval(env_or_opts) + {term, _bindings} = Code.eval_string(expr, binding, env) term catch kind, error -> @@ -1628,6 +1622,22 @@ defmodule ElixirLS.DebugAdapter.Server do end end + defp frame_for_eval(_paused_processes, nil), do: nil + + defp frame_for_eval(paused_processes, frame_id) do + case find_frame(paused_processes, frame_id) do + {_pid, %Frame{} = frame} -> + frame + + _ -> + raise ServerError, + message: "argumentError", + format: "Unable to find frame {frameId}", + variables: %{"frameId" => frame_id}, + send_telemetry: false + end + end + # for null frameId DAP spec suggest to return variables in the global scope # there is no global scope in erlang/elixir so instead we flat map all variables # from all paused processes and evaluator @@ -1647,35 +1657,38 @@ defmodule ElixirLS.DebugAdapter.Server do Binding.to_elixir_variable_names(bindings) end) - {binding, []} + {%ElixirSense.Core.Metadata{}, + update_env_vars_from_binding(%ElixirSense.Core.State.Env{}, binding), binding, []} end - defp binding_and_env(paused_processes, frame_id) do - case find_frame(paused_processes, frame_id) do - {_pid, %Frame{bindings: bindings, dbg_frame?: dbg_frame?} = frame} when is_map(bindings) -> - if dbg_frame? do - {bindings |> Enum.to_list(), frame.dbg_env} - else - {Binding.to_elixir_variable_names(bindings), - [ - file: frame.file, - line: frame.line - ]} - end + defp binding_and_env( + _paused_processes, + %Frame{bindings: bindings, dbg_frame?: dbg_frame?} = frame + ) + when is_map(bindings) do + {metadata, env, macro_env} = parse_file(frame.file, frame.line) - {_pid, %Frame{} = frame} -> - {[], - [ - file: frame.file, - line: frame.line - ]} - - _ -> - raise ServerError, - message: "argumentError", - format: "Unable to find frame {frameId}", - variables: %{"frameId" => frame_id}, - send_telemetry: false + if dbg_frame? do + if frame.dbg_env do + # we are evaluating an expression in dbg breakpoint - take dbg macro env as env for eval + # we have exact elixir bindings here + env = ElixirSense.Core.State.Env.update_from_macro_env(env, frame.dbg_env) + binding = bindings |> Enum.to_list() + {metadata, update_env_vars_from_binding(env, binding), binding, frame.dbg_env} + else + # we are evaluating an expression in dbg breakpoint but frame is upper in the stacktrace + # we don't have any bindings here + # take env from metadata builder as env for eval + {metadata, env, [], macro_env} + end + else + # we are evaluating an expression in OTP debugger breakpoint + # take env from metadata builder as env for eval + # bindings come from OTP debugger + # unfortunately there is no way to select right versions of variables in binding + # translation in :elixir_erl_var.translate is one way + binding = Binding.to_elixir_variable_names(bindings) + {metadata, update_env_vars_from_binding(env, binding), binding, macro_env} end end @@ -2597,6 +2610,16 @@ defmodule ElixirLS.DebugAdapter.Server do status not in [:exit, :no_conn], into: %{}, do: {pid, {function, status, info}} + rescue + e in MatchError -> + if Exception.message(e) =~ ":already_started" do + # workaround for a crash in tests (probably caused by race conditions) + # ** (MatchError) no match of right hand side value: {:error, {:already_started, #PID<0.385.0>}} + # (debugger 5.3.4) dbg_iserver.erl:75: :dbg_iserver.safe_call/1 + %{} + else + reraise(e, __STACKTRACE__) + end end defp update_threads(state = %__MODULE__{}, snapshot_by_pid \\ get_snapshot_by_pid()) do @@ -2889,4 +2912,61 @@ defmodule ElixirLS.DebugAdapter.Server do GenServer.call(parent, {:request_finished, packet, start_time, result}, :infinity) end) end + + defp parse_file(file, line) do + try do + if String.ends_with?(file, ".ex") or String.ends_with?(file, ".exs") do + code = File.read!(file) + buffer_file_metadata = ElixirSense.Core.Parser.parse_string(code, false, true, {line, 1}) + + env = ElixirSense.Core.Metadata.get_env(buffer_file_metadata, {line, 1}) + # TODO env_for_eval? + # should we clear versioned_vars? + {buffer_file_metadata, env, ElixirSense.Core.State.Env.to_macro_env(env, file, line)} + else + # do not try to parse non elixir files + {%ElixirSense.Core.Metadata{}, %ElixirSense.Core.State.Env{}, [file: file, line: line]} + end + rescue + error -> + {payload, stacktrace} = Exception.blame(:error, error, __STACKTRACE__) + message = Exception.format(:error, payload, stacktrace) + + Output.debugger_console( + "Unable to parse file #{file}: #{message}; Using stub evaluator environment." + ) + + {%ElixirSense.Core.Metadata{}, %ElixirSense.Core.State.Env{}, [file: file, line: line]} + end + end + + defp update_env_vars_from_binding(env, binding) do + env_vars = env.vars |> Map.new(&{&1.name, &1}) + env_var_names = env_vars |> Map.keys() + binding_var_names = binding |> Keyword.keys() + + vars = + for var_name <- Enum.uniq(env_var_names ++ binding_var_names) do + case {Keyword.fetch(binding, var_name), Map.fetch(env_vars, var_name)} do + {{:ok, binding_value}, {:ok, env_var}} -> + # var both in env and in binding - prefer type from binding + type = ElixirSense.Core.Binding.from_var(binding_value) + %ElixirSense.Core.State.VarInfo{env_var | type: type} + + {_, {:ok, env_var}} -> + # var only in env - keep it, binding may not have everything + env_var + + {{:ok, binding_value}, _} -> + # var only in binding - add var to env + %ElixirSense.Core.State.VarInfo{ + name: var_name, + type: ElixirSense.Core.Binding.from_var(binding_value) + } + end + |> IO.inspect() + end + + %{env | vars: vars} + end end