From c5c0437f6fff9a6c9f77e7211b825ffa4b15ba68 Mon Sep 17 00:00:00 2001 From: Lukasz Samson Date: Thu, 26 Sep 2024 22:16:40 +0200 Subject: [PATCH] handle unquote_slicing in args and correctly expand defaults in defdelegate --- lib/elixir_sense/core/compiler.ex | 83 +++++++++++++++++-- lib/elixir_sense/core/compiler/typespec.ex | 13 +-- .../core/metadata_builder_test.exs | 39 +++++++-- 3 files changed, 118 insertions(+), 17 deletions(-) diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index 8657e327..7c7aa41e 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -831,24 +831,61 @@ defmodule ElixirSense.Core.Compiler do funs |> List.wrap() |> Enum.reduce(state, fn fun, state -> - {fun, state} = + state_orig = state + + {fun, state, has_unquotes} = if __MODULE__.Quote.has_unquotes(fun) do + state = new_vars_scope(state) # dynamic defdelegate - replace unquote expression with fake call case fun do {{:unquote, _, unquote_args}, meta, args} -> {_, state, _} = expand(unquote_args, state, env) - {{:__unknown__, meta, args}, state} + {{:__unknown__, meta, args}, state, true} _ -> - {{:__unknown__, [], []}, state} + {fun, state, true} end else - {fun, state} + state = new_func_vars_scope(state) + {fun, state, false} end - {name, args, as, as_args} = Kernel.Utils.defdelegate_each(fun, opts) + {name, args, as, as_args} = __MODULE__.Utils.defdelegate_each(fun, opts) arity = length(args) + # no need to reset versioned_vars - we never update it + env_for_expand = %{env | function: {name, arity}} + + # expand defaults and pass args without defaults to expand_args + {args_no_defaults, args, state} = + expand_defaults(args, state, %{env_for_expand | context: nil}, [], []) + + # based on :elixir_clauses.def + {e_args_no_defaults, state, env_for_expand} = + expand_args(args_no_defaults, %{state | prematch: {%{}, 0, :none}}, %{ + env_for_expand + | context: :match + }) + + args = + Enum.zip(args, e_args_no_defaults) + |> Enum.map(fn + {{:"\\\\", meta, [_, expanded_default]}, expanded_arg} -> + {:"\\\\", meta, [expanded_arg, expanded_default]} + + {_, expanded_arg} -> + expanded_arg + end) + + state = + unless has_unquotes do + # restore module vars + remove_func_vars_scope(state, state_orig) + else + # remove scope + remove_vars_scope(state, state_orig) + end + state |> add_current_env_to_line(meta, %{env | context: nil, function: {name, arity}}) |> add_func_to_index( @@ -2558,6 +2595,42 @@ defmodule ElixirSense.Core.Compiler do result end + + def defdelegate_each(fun, opts) when is_list(opts) do + # TODO: Remove on v2.0 + append_first? = Keyword.get(opts, :append_first, false) + + {name, args} = + case fun do + {:when, _, [_left, right]} -> + raise ArgumentError, + "guards are not allowed in defdelegate/2, got: when #{Macro.to_string(right)}" + + _ -> + case Macro.decompose_call(fun) do + {_, _} = pair -> pair + _ -> raise ArgumentError, "invalid syntax in defdelegate #{Macro.to_string(fun)}" + end + end + + as = Keyword.get(opts, :as, name) + as_args = build_as_args(args, append_first?) + + {name, args, as, as_args} + end + + defp build_as_args(args, append_first?) do + as_args = :lists.map(&build_as_arg/1, args) + + case append_first? do + true -> tl(as_args) ++ [hd(as_args)] + false -> as_args + end + end + + # elixir validates arg + defp build_as_arg({:\\, _, [arg, _default_arg]}), do: arg + defp build_as_arg(arg), do: arg end defmodule Clauses do diff --git a/lib/elixir_sense/core/compiler/typespec.ex b/lib/elixir_sense/core/compiler/typespec.ex index 16b5715d..6e48a66a 100644 --- a/lib/elixir_sense/core/compiler/typespec.ex +++ b/lib/elixir_sense/core/compiler/typespec.ex @@ -526,18 +526,19 @@ defmodule ElixirSense.Core.Compiler.Typespec do end end + # handle unquote fragment + defp typespec({key, _, args}, _vars, caller, state) + when is_list(args) and key in [:unquote, :unquote_splicing] do + {_, state, _env} = ElixirExpand.expand(args, state, caller) + {:__unknown__, state} + end + # Handle local calls defp typespec({name, meta, args}, :disabled, caller, state) when is_atom(name) do {args, state} = :lists.mapfoldl(&typespec(&1, :disabled, caller, &2), state, args) {{name, meta, args}, state} end - # handle unquote fragment - defp typespec({:unquote, _, args}, _vars, caller, state) when is_list(args) do - {_, state, _env} = ElixirExpand.expand(args, state, caller) - {:__unknown__, state} - end - defp typespec({name, meta, args}, vars, caller, state) when is_atom(name) do {args, state} = :lists.mapfoldl(&typespec(&1, vars, caller, &2), state, args) # elixir raises if type is not defined diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index 4d05813a..2a90604d 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -985,11 +985,19 @@ defmodule ElixirSense.Core.MetadataBuilderTest do record_env() end end) + + keys = [{:foo, [], nil}, {:bar, [], nil}] + @spec foo_splicing(unquote_splicing(keys)) :: :ok + @type foo_splicing(unquote_splicing(keys)) :: :ok + defdelegate foo_splicing(unquote_splicing(keys)), to: Foo + def foo_splicing(unquote_splicing(keys)) do + record_env() + end end """ |> string_to_state - assert Map.keys(state.lines_to_env[7].versioned_vars) == [{:k, nil}, {:kv, nil}, {:v, nil}] + assert Map.keys(state.lines_to_env[9].versioned_vars) == [{:k, nil}, {:kv, nil}, {:v, nil}] # TODO should we handle unquote_slicing in arg list? # TODO defquard on 1.18 @@ -997,7 +1005,19 @@ defmodule ElixirSense.Core.MetadataBuilderTest do %VarInfo{name: :k, positions: [{3, 21}, {4, 19}, {5, 19}, {6, 25}, {7, 17}]}, %VarInfo{name: :kv, positions: [{2, 3}, {3, 13}]}, %VarInfo{name: :v, positions: [{3, 24}, {4, 35}, {5, 35}, {8, 15}]} - ] = state |> get_line_vars(8) + ] = state |> get_line_vars(9) + + assert Map.keys(state.lines_to_env[18].versioned_vars) == [keys: nil, kv: nil] + + # TODO should we handle unquote_slicing in arg list? + # TODO defquard on 1.18 + assert [ + %VarInfo{ + name: :keys, + positions: [{13, 3}, {14, 39}, {15, 39}, {16, 45}, {17, 37}] + }, + %VarInfo{name: :kv, positions: [{2, 3}, {3, 13}]} + ] = state |> get_line_vars(18) end test "in capture" do @@ -6197,34 +6217,41 @@ defmodule ElixirSense.Core.MetadataBuilderTest do defdelegate func_delegated_erlang(par), to: :erlang_module defdelegate func_delegated_as(par), to: __MODULE__.Sub, as: :my_func defdelegate func_delegated_alias(par), to: E + defdelegate func_delegated_defaults(par \\\\ 123), to: E end """ |> string_to_state assert %{ {MyModuleWithFuns, :func_delegated, 1} => %ModFunInfo{ - params: [[{:par, [line: 3, column: 30], nil}]], + params: [[{:par, _, nil}]], positions: [{3, 3}], target: {OtherModule, :func_delegated}, type: :defdelegate }, {MyModuleWithFuns, :func_delegated_alias, 1} => %ModFunInfo{ - params: [[{:par, [line: 6, column: 36], nil}]], + params: [[{:par, _, nil}]], positions: [{6, 3}], target: {Enum, :func_delegated_alias}, type: :defdelegate }, {MyModuleWithFuns, :func_delegated_as, 1} => %ModFunInfo{ - params: [[{:par, [line: 5, column: 33], nil}]], + params: [[{:par, _, nil}]], positions: [{5, 3}], target: {MyModuleWithFuns.Sub, :my_func}, type: :defdelegate }, {MyModuleWithFuns, :func_delegated_erlang, 1} => %ModFunInfo{ - params: [[{:par, [line: 4, column: 37], nil}]], + params: [[{:par, _, nil}]], positions: [{4, 3}], target: {:erlang_module, :func_delegated_erlang}, type: :defdelegate + }, + {MyModuleWithFuns, :func_delegated_defaults, 1} => %ModFunInfo{ + params: [[{:\\, _, [{:par, _, nil}, 123]}]], + positions: [{7, 3}], + target: {Enum, :func_delegated_defaults}, + type: :defdelegate } } = state.mods_funs_to_positions end