Skip to content

Commit

Permalink
handle unquote_slicing in args and correctly expand defaults in defde…
Browse files Browse the repository at this point in the history
…legate
  • Loading branch information
lukaszsamson committed Sep 26, 2024
1 parent 521bd24 commit c5c0437
Show file tree
Hide file tree
Showing 3 changed files with 118 additions and 17 deletions.
83 changes: 78 additions & 5 deletions lib/elixir_sense/core/compiler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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} =

Check warning on line 864 in lib/elixir_sense/core/compiler.ex

View workflow job for this annotation

GitHub Actions / mix test (Elixir 1.15.x | Erlang/OTP 24.x)

variable "env_for_expand" is unused (there is a variable with the same name in the context, use the pin operator (^) to match on it or prefix this variable with underscore if it is not meant to be used)

Check warning on line 864 in lib/elixir_sense/core/compiler.ex

View workflow job for this annotation

GitHub Actions / mix test (Elixir 1.15.x | Erlang/OTP 25.x)

variable "env_for_expand" is unused (there is a variable with the same name in the context, use the pin operator (^) to match on it or prefix this variable with underscore if it is not meant to be used)

Check warning on line 864 in lib/elixir_sense/core/compiler.ex

View workflow job for this annotation

GitHub Actions / mix test (Elixir 1.14.x | Erlang/OTP 25.x)

variable "env_for_expand" is unused (there is a variable with the same name in the context, use the pin operator (^) to match on it or prefix this variable with underscore if it is not meant to be used)

Check warning on line 864 in lib/elixir_sense/core/compiler.ex

View workflow job for this annotation

GitHub Actions / mix test (Elixir 1.14.x | Erlang/OTP 24.x)

variable "env_for_expand" is unused (there is a variable with the same name in the context, use the pin operator (^) to match on it or prefix this variable with underscore if it is not meant to be used)

Check warning on line 864 in lib/elixir_sense/core/compiler.ex

View workflow job for this annotation

GitHub Actions / mix test (Elixir 1.13.x | Erlang/OTP 25.x)

variable "env_for_expand" is unused (there is a variable with the same name in the context, use the pin operator (^) to match on it or prefix this variable with underscore if it is not meant to be used)

Check warning on line 864 in lib/elixir_sense/core/compiler.ex

View workflow job for this annotation

GitHub Actions / mix test (Elixir 1.15.x | Erlang/OTP 26.x)

variable "env_for_expand" is unused (there is a variable with the same name in the context, use the pin operator (^) to match on it or prefix this variable with underscore if it is not meant to be used)

Check warning on line 864 in lib/elixir_sense/core/compiler.ex

View workflow job for this annotation

GitHub Actions / mix test (Elixir 1.13.x | Erlang/OTP 24.x)

variable "env_for_expand" is unused (there is a variable with the same name in the context, use the pin operator (^) to match on it or prefix this variable with underscore if it is not meant to be used)

Check warning on line 864 in lib/elixir_sense/core/compiler.ex

View workflow job for this annotation

GitHub Actions / mix test (Elixir 1.14.x | Erlang/OTP 26.x)

variable "env_for_expand" is unused (there is a variable with the same name in the context, use the pin operator (^) to match on it or prefix this variable with underscore if it is not meant to be used)

Check warning on line 864 in lib/elixir_sense/core/compiler.ex

View workflow job for this annotation

GitHub Actions / mix test (Elixir 1.13.x | Erlang/OTP 22.x)

variable "env_for_expand" is unused (there is a variable with the same name in the context, use the pin operator (^) to match on it or prefix this variable with underscore if it is not meant to be used)

Check warning on line 864 in lib/elixir_sense/core/compiler.ex

View workflow job for this annotation

GitHub Actions / mix test (Elixir 1.14.x | Erlang/OTP 23.x)

variable "env_for_expand" is unused (there is a variable with the same name in the context, use the pin operator (^) to match on it or prefix this variable with underscore if it is not meant to be used)

Check warning on line 864 in lib/elixir_sense/core/compiler.ex

View workflow job for this annotation

GitHub Actions / mix test (Elixir 1.13.x | Erlang/OTP 23.x)

variable "env_for_expand" is unused (there is a variable with the same name in the context, use the pin operator (^) to match on it or prefix this variable with underscore if it is not meant to be used)

Check warning on line 864 in lib/elixir_sense/core/compiler.ex

View workflow job for this annotation

GitHub Actions / mix test windows (Elixir 1.13.x | Erlang/OTP 24.x)

variable "env_for_expand" is unused (there is a variable with the same name in the context, use the pin operator (^) to match on it or prefix this variable with underscore if it is not meant to be used)

Check warning on line 864 in lib/elixir_sense/core/compiler.ex

View workflow job for this annotation

GitHub Actions / mix test windows (Elixir 1.13.x | Erlang/OTP 22.x)

variable "env_for_expand" is unused (there is a variable with the same name in the context, use the pin operator (^) to match on it or prefix this variable with underscore if it is not meant to be used)

Check warning on line 864 in lib/elixir_sense/core/compiler.ex

View workflow job for this annotation

GitHub Actions / mix test windows (Elixir 1.13.x | Erlang/OTP 25.x)

variable "env_for_expand" is unused (there is a variable with the same name in the context, use the pin operator (^) to match on it or prefix this variable with underscore if it is not meant to be used)

Check warning on line 864 in lib/elixir_sense/core/compiler.ex

View workflow job for this annotation

GitHub Actions / mix test windows (Elixir 1.14.x | Erlang/OTP 23.x)

variable "env_for_expand" is unused (there is a variable with the same name in the context, use the pin operator (^) to match on it or prefix this variable with underscore if it is not meant to be used)

Check warning on line 864 in lib/elixir_sense/core/compiler.ex

View workflow job for this annotation

GitHub Actions / mix test windows (Elixir 1.14.x | Erlang/OTP 25.x)

variable "env_for_expand" is unused (there is a variable with the same name in the context, use the pin operator (^) to match on it or prefix this variable with underscore if it is not meant to be used)

Check warning on line 864 in lib/elixir_sense/core/compiler.ex

View workflow job for this annotation

GitHub Actions / mix test windows (Elixir 1.14.x | Erlang/OTP 26.x)

variable "env_for_expand" is unused (there is a variable with the same name in the context, use the pin operator (^) to match on it or prefix this variable with underscore if it is not meant to be used)

Check warning on line 864 in lib/elixir_sense/core/compiler.ex

View workflow job for this annotation

GitHub Actions / mix test windows (Elixir 1.14.x | Erlang/OTP 24.x)

variable "env_for_expand" is unused (there is a variable with the same name in the context, use the pin operator (^) to match on it or prefix this variable with underscore if it is not meant to be used)

Check warning on line 864 in lib/elixir_sense/core/compiler.ex

View workflow job for this annotation

GitHub Actions / mix test windows (Elixir 1.13.x | Erlang/OTP 23.x)

variable "env_for_expand" is unused (there is a variable with the same name in the context, use the pin operator (^) to match on it or prefix this variable with underscore if it is not meant to be used)

Check warning on line 864 in lib/elixir_sense/core/compiler.ex

View workflow job for this annotation

GitHub Actions / mix test windows (Elixir 1.15.x | Erlang/OTP 25.x)

variable "env_for_expand" is unused (there is a variable with the same name in the context, use the pin operator (^) to match on it or prefix this variable with underscore if it is not meant to be used)

Check warning on line 864 in lib/elixir_sense/core/compiler.ex

View workflow job for this annotation

GitHub Actions / mix test windows (Elixir 1.15.x | Erlang/OTP 26.x)

variable "env_for_expand" is unused (there is a variable with the same name in the context, use the pin operator (^) to match on it or prefix this variable with underscore if it is not meant to be used)

Check warning on line 864 in lib/elixir_sense/core/compiler.ex

View workflow job for this annotation

GitHub Actions / mix test windows (Elixir 1.15.x | Erlang/OTP 24.x)

variable "env_for_expand" is unused (there is a variable with the same name in the context, use the pin operator (^) to match on it or prefix this variable with underscore if it is not meant to be used)
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(
Expand Down Expand Up @@ -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
Expand Down
13 changes: 7 additions & 6 deletions lib/elixir_sense/core/compiler/typespec.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
39 changes: 33 additions & 6 deletions test/elixir_sense/core/metadata_builder_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -985,19 +985,39 @@ 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
assert [
%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
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit c5c0437

Please sign in to comment.