From 04d3010b4c004022782b70af02dcab263b2039f3 Mon Sep 17 00:00:00 2001 From: Mitchell Hanberg Date: Wed, 17 Apr 2024 10:07:56 -0400 Subject: [PATCH] fix(completions): imports inside blocks that generate functions (#423) The `test/2` macro is an example of this Closes #420 --- lib/next_ls.ex | 1 + priv/monkey/_next_ls_private_compiler.ex | 74 ++++++++++-------- test/next_ls/completions_test.exs | 95 +++++++++++++++++++++++- 3 files changed, 134 insertions(+), 36 deletions(-) diff --git a/lib/next_ls.ex b/lib/next_ls.ex index 5eafcfd1..4538afe4 100644 --- a/lib/next_ls.ex +++ b/lib/next_ls.ex @@ -646,6 +646,7 @@ defmodule NextLS do |> then(fn {:ok, ast} -> ast {:error, ast, _} -> ast + {:error, :no_fuel_remaining} -> nil end) {:ok, {_, _, _, macro_env}} = Runtime.expand(runtime, ast, Path.basename(uri)) diff --git a/priv/monkey/_next_ls_private_compiler.ex b/priv/monkey/_next_ls_private_compiler.ex index 06177a2e..da8dafb1 100644 --- a/priv/monkey/_next_ls_private_compiler.ex +++ b/priv/monkey/_next_ls_private_compiler.ex @@ -1217,16 +1217,14 @@ if Version.match?(System.version(), ">= 1.17.0-dev") do {arg, state, env} = expand(arg, state, env) {opts, state, env} = expand_directive_opts(opts, state, env) - case arg do - {:__aliases__, _, _} -> - # An actual compiler would raise if the alias fails. - case Macro.Env.define_alias(env, meta, arg, [trace: false] ++ opts) do - {:ok, env} -> {arg, state, env} - {:error, _} -> {arg, state, env} - end - - _ -> - {node, state, env} + if is_atom(arg) do + # An actual compiler would raise if the alias fails. + case Macro.Env.define_alias(env, meta, arg, [trace: false] ++ opts) do + {:ok, env} -> {arg, state, env} + {:error, _} -> {arg, state, env} + end + else + {node, state, env} end end @@ -1234,16 +1232,14 @@ if Version.match?(System.version(), ">= 1.17.0-dev") do {arg, state, env} = expand(arg, state, env) {opts, state, env} = expand_directive_opts(opts, state, env) - case arg do - {:__aliases__, _, _} -> - # An actual compiler would raise if the module is not defined or if the require fails. - case Macro.Env.define_require(env, meta, arg, [trace: false] ++ opts) do - {:ok, env} -> {arg, state, env} - {:error, _} -> {arg, state, env} - end - - _ -> - {node, state, env} + if is_atom(arg) do + # An actual compiler would raise if the module is not defined or if the require fails. + case Macro.Env.define_require(env, meta, arg, [trace: false] ++ opts) do + {:ok, env} -> {arg, state, env} + {:error, _} -> {arg, state, env} + end + else + {node, state, env} end end @@ -1251,18 +1247,16 @@ if Version.match?(System.version(), ">= 1.17.0-dev") do {arg, state, env} = expand(arg, state, env) {opts, state, env} = expand_directive_opts(opts, state, env) - case arg do - {:__aliases__, _, _} -> - # An actual compiler would raise if the module is not defined or if the import fails. - with true <- is_atom(arg) and Code.ensure_loaded?(arg), - {:ok, env} <- Macro.Env.define_import(env, meta, arg, [trace: false] ++ opts) do - {arg, state, env} - else - _ -> {arg, state, env} - end - - _ -> - {node, state, env} + if is_atom(arg) do + # An actual compiler would raise if the module is not defined or if the import fails. + with true <- is_atom(arg) and Code.ensure_loaded?(arg), + {:ok, env} <- Macro.Env.define_import(env, meta, arg, [trace: false] ++ opts) do + {arg, state, env} + else + _ -> {arg, state, env} + end + else + {node, state, env} end end @@ -1444,6 +1438,20 @@ if Version.match?(System.version(), ">= 1.17.0-dev") do {Enum.reverse(blocks), put_in(state.functions, functions), env} end + defp expand_macro(_meta, Kernel, type, [{_name, _, params}, blocks], _callback, state, env) + when type in [:def, :defp] and is_list(params) and is_list(blocks) do + {blocks, state} = + for {type, block} <- blocks, reduce: {[], state} do + {acc, state} -> + {res, state, _env} = expand(block, state, env) + {[{type, res} | acc], state} + end + + arity = length(List.wrap(params)) + + {Enum.reverse(blocks), state, env} + end + defp expand_macro(meta, Kernel, :@, [{name, _, p}] = args, callback, state, env) when is_list(p) do state = update_in(state.attrs, &[to_string(name) | &1]) expand_macro_callback(meta, Kernel, :@, args, callback, state, env) @@ -1527,7 +1535,7 @@ if Version.match?(System.version(), ">= 1.17.0-dev") do {Enum.reverse(acc), state, env} end - defp expand_list([h | t], state, env, acc) do + defp expand_list([h | t] = list, state, env, acc) do {h, state, env} = expand(h, state, env) expand_list(t, state, env, [h | acc]) end diff --git a/test/next_ls/completions_test.exs b/test/next_ls/completions_test.exs index 7a50b743..88481f17 100644 --- a/test/next_ls/completions_test.exs +++ b/test/next_ls/completions_test.exs @@ -5,6 +5,20 @@ defmodule NextLS.CompletionsTest do import GenLSP.Test import NextLS.Support.Utils + defmacrop assert_match({:in, _, [left, right]}) do + quote do + assert Enum.any?(unquote(right), fn x -> + match?(unquote(left), x) + end), + """ + failed to find a match inside of list + + left: #{unquote(Macro.to_string(left))} + right: #{inspect(unquote(right), pretty: true)} + """ + end + end + @moduletag tmp_dir: true, root_paths: ["my_proj"] setup %{tmp_dir: tmp_dir} do @@ -34,7 +48,7 @@ defmodule NextLS.CompletionsTest do baz = Path.join(cwd, "my_proj/lib/baz.ex") File.write!(baz, """ - defmodule Foo.Bar.Baz do + defmodule Foo.Bing.Baz do def run() do :ok end @@ -361,12 +375,47 @@ defmodule NextLS.CompletionsTest do ] end + test "aliases in document", %{client: client, foo: foo} do + uri = uri(foo) + + did_open(client, foo, """ + defmodule Foo do + alias Foo.Bing + + def run() do + B + end + end + """) + + request client, %{ + method: "textDocument/completion", + id: 2, + jsonrpc: "2.0", + params: %{ + textDocument: %{ + uri: uri + }, + position: %{ + line: 4, + character: 5 + } + } + } + + assert_result 2, results + + assert_match( + %{"data" => _, "documentation" => _, "insertText" => "Bing", "kind" => 9, "label" => "Bing"} in results + ) + end + test "inside alias special form", %{client: client, foo: foo} do uri = uri(foo) did_open(client, foo, """ defmodule Foo do - alias Foo.Bar. + alias Foo.Bing. def run() do :ok @@ -390,7 +439,47 @@ defmodule NextLS.CompletionsTest do } assert_result 2, [ - %{"data" => _, "documentation" => _, "insertText" => "Baz", "kind" => 9, "label" => "Baz"} + %{"data" => _, "documentation" => _, "insertText" => "Bing", "kind" => 9, "label" => "Bing"} ] end + + test "import functions appear", %{client: client, foo: foo} do + uri = uri(foo) + + did_open(client, foo, """ + defmodule Foo do + use ExUnit.Case + import ExUnit.CaptureLog + + test "foo" do + cap + end + end + """) + + request client, %{ + method: "textDocument/completion", + id: 2, + jsonrpc: "2.0", + params: %{ + textDocument: %{ + uri: uri + }, + position: %{ + line: 5, + character: 7 + } + } + } + + assert_result 2, results + + assert_match( + %{"data" => _, "documentation" => _, "insertText" => "capture_log", "kind" => 3, "label" => "capture_log/1"} in results + ) + + assert_match( + %{"data" => _, "documentation" => _, "insertText" => "capture_log", "kind" => 3, "label" => "capture_log/2"} in results + ) + end end