Skip to content

Commit

Permalink
better support for variable tracking in unquote fragments
Browse files Browse the repository at this point in the history
  • Loading branch information
lukaszsamson committed Sep 23, 2024
1 parent b4bab8d commit 521bd24
Show file tree
Hide file tree
Showing 4 changed files with 93 additions and 18 deletions.
42 changes: 33 additions & 9 deletions lib/elixir_sense/core/compiler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -831,12 +831,19 @@ defmodule ElixirSense.Core.Compiler do
funs
|> List.wrap()
|> Enum.reduce(state, fn fun, state ->
fun =
{fun, state} =
if __MODULE__.Quote.has_unquotes(fun) do
# dynamic defdelegate - replace unquote expression with fake call
{:__unknown__, [], []}
case fun do
{{:unquote, _, unquote_args}, meta, args} ->
{_, state, _} = expand(unquote_args, state, env)
{{:__unknown__, meta, args}, state}

_ ->
{{:__unknown__, [], []}, state}
end
else
fun
{fun, state}
end

{name, args, as, as_args} = Kernel.Utils.defdelegate_each(fun, opts)
Expand Down Expand Up @@ -1625,13 +1632,30 @@ defmodule ElixirSense.Core.Compiler do

# elixir raises here if def is invalid, we try to continue with unknown
# especially, we return unknown for calls with unquote fragments
{name, _meta_1, args} =
{{name, _meta_1, args}, state} =
case name_and_args do
{n, m, a} when is_atom(n) and is_atom(a) -> {n, m, []}
{n, m, a} when is_atom(n) and is_list(a) -> {n, m, a}
{_n, m, a} when is_atom(a) -> {:__unknown__, m, []}
{_n, m, a} when is_list(a) -> {:__unknown__, m, a}
_ -> {:__unknown__, [], []}
{n, m, a} when is_atom(n) and is_atom(a) ->
{{n, m, []}, state}

{n, m, a} when is_atom(n) and is_list(a) ->
{{n, m, a}, state}

{{:unquote, _, unquote_args}, m, a} when is_atom(a) ->
{_, state, _} = expand(unquote_args, state, env)
{{:__unknown__, m, []}, state}

{{:unquote, _, unquote_args}, m, a} when is_list(a) ->
{_, state, _} = expand(unquote_args, state, env)
{{:__unknown__, m, a}, state}

{_n, m, a} when is_atom(a) ->
{{:__unknown__, m, []}, state}

{_n, m, a} when is_list(a) ->
{{:__unknown__, m, a}, state}

_ ->
{{:__unknown__, [], []}, state}
end

arity = length(args)
Expand Down
26 changes: 25 additions & 1 deletion lib/elixir_sense/core/compiler/typespec.ex
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,18 @@ defmodule ElixirSense.Core.Compiler.Typespec do

defp do_expand_spec(other, guard, guard_meta, state, env) do
case other do
{:"::", meta, [{{:unquote, _, unquote_args}, meta1, call_args}, definition]} ->
# replace unquote fragment and try to expand args to find variables
{_, state, env} = ElixirExpand.expand(unquote_args, state, env)

do_expand_spec(
{:"::", meta, [{:__unknown__, meta1, call_args}, definition]},
guard,
guard_meta,
state,
env
)

{name, meta, args} when is_atom(name) and name != :"::" ->
# invalid or incomplete spec
# try to wrap in :: expression
Expand Down Expand Up @@ -169,7 +181,8 @@ defmodule ElixirSense.Core.Compiler.Typespec do
end
end

defp do_expand_type({:"::", meta, [{name, name_meta, args}, definition]}, state, env) do
defp do_expand_type({:"::", meta, [{name, name_meta, args}, definition]}, state, env)
when is_atom(name) and name != :"::" do
args =
if is_atom(args) do
[]
Expand All @@ -196,6 +209,11 @@ defmodule ElixirSense.Core.Compiler.Typespec do

defp do_expand_type(other, state, env) do
case other do
{:"::", meta, [{{:unquote, _, unquote_args}, meta1, call_args}, definition]} ->
# replace unquote fragment and try to expand args to find variables
{_, state, env} = ElixirExpand.expand(unquote_args, state, env)
do_expand_type({:"::", meta, [{:__unknown__, meta1, call_args}, definition]}, state, env)

{name, meta, args} when is_atom(name) and name != :"::" ->
# invalid or incomplete type
# try to wrap in :: expression
Expand Down Expand Up @@ -514,6 +532,12 @@ defmodule ElixirSense.Core.Compiler.Typespec do
{{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
35 changes: 27 additions & 8 deletions test/elixir_sense/core/metadata_builder_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -978,6 +978,8 @@ defmodule ElixirSense.Core.MetadataBuilderTest do
kv = [foo: 1, bar: 2] |> IO.inspect
Enum.each(kv, fn {k, v} ->
@spec unquote(k)() :: unquote(v)
@type unquote(k)() :: unquote(v)
defdelegate unquote(k)(), to: Foo
def unquote(k)() do
unquote(v)
record_env()
Expand All @@ -989,12 +991,13 @@ defmodule ElixirSense.Core.MetadataBuilderTest do

assert Map.keys(state.lines_to_env[7].versioned_vars) == [{:k, nil}, {:kv, nil}, {:v, nil}]

# TODO we are not tracking usages in typespec
# TODO should we handle unquote_slicing in arg list?
# TODO defquard on 1.18
assert [
%VarInfo{name: :k, positions: [{3, 21}]},
%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}, {6, 15}]}
] = state |> get_line_vars(7)
%VarInfo{name: :v, positions: [{3, 24}, {4, 35}, {5, 35}, {8, 15}]}
] = state |> get_line_vars(8)
end

test "in capture" do
Expand Down Expand Up @@ -1186,6 +1189,22 @@ defmodule ElixirSense.Core.MetadataBuilderTest do
] = state |> get_line_vars(3)
end

test "variables hygiene" do
state =
"""
defmodule MyModule do
import ElixirSenseExample.Math
def func do
squared(5)
IO.puts ""
end
end
"""
|> string_to_state

assert [] == state |> get_line_vars(5)
end

test "variables are added to environment" do
state =
"""
Expand Down Expand Up @@ -5678,7 +5697,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do
specs: [
"@spec __protocol__(:impls) :: :not_consolidated | {:consolidated, list(module())}",
"@spec __protocol__(:consolidated?) :: boolean()",
"@spec __protocol__(:functions) :: unquote(Protocol.__functions_spec__(@__functions__()))",
"@spec __protocol__(:functions) :: :__unknown__",
"@spec __protocol__(:module) :: Proto"
]
},
Expand Down Expand Up @@ -6221,7 +6240,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do
|> string_to_state

assert %{
{MyModuleWithFuns, :__unknown__, 0} => %ModFunInfo{
{MyModuleWithFuns, :__unknown__, 1} => %ModFunInfo{
target: {List, :flatten},
type: :defdelegate
}
Expand Down Expand Up @@ -7964,7 +7983,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do
positions: [{4, 5}],
end_positions: _,
generated: [false],
specs: ["@type foo() :: unquote(v())"]
specs: ["@type foo() :: :__unknown__"]
}
} = state.types
end
Expand All @@ -7985,7 +8004,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do
{My, :__unknown__, 0} => %ElixirSense.Core.State.TypeInfo{
name: :__unknown__,
args: [[]],
specs: ["@type unquote(k)() :: 123"],
specs: ["@type __unknown__() :: 123"],
kind: :type,
positions: [{4, 5}],
end_positions: [{4, 30}],
Expand Down
8 changes: 8 additions & 0 deletions test/support/macro_hygiene.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
defmodule ElixirSenseExample.Math do
defmacro squared(x) do
quote do
x = unquote(x)
x * x
end
end
end

0 comments on commit 521bd24

Please sign in to comment.