Skip to content

Commit

Permalink
improve graceful handling of defs with unquote fragments
Browse files Browse the repository at this point in the history
  • Loading branch information
lukaszsamson committed Jul 19, 2024
1 parent 24a0fad commit 8cdbd94
Show file tree
Hide file tree
Showing 2 changed files with 151 additions and 106 deletions.
66 changes: 35 additions & 31 deletions lib/elixir_sense/core/compiler.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1621,56 +1621,52 @@ defmodule ElixirSense.Core.Compiler do
)
when module != nil and
def_kind in [:def, :defp, :defmacro, :defmacrop, :defguard, :defguardp] do
# dbg(call)
# dbg(expr)
# dbg(def_kind)

%{vars: vars, unused: unused} = state

# unquoted_call = :elixir_quote.has_unquotes(call)
# unquoted_expr = :elixir_quote.has_unquotes(expr)
# TODO expand the call and expression.
# TODO store mod_fun_to_pos
line = Keyword.fetch!(meta, :line)

unquoted_call = __MODULE__.Quote.has_unquotes(call)
unquoted_expr = __MODULE__.Quote.has_unquotes(expr)
has_unquotes = unquoted_call or unquoted_expr

{call, expr} =
if unquoted_expr or unquoted_call do
{call, expr} = __MODULE__.Quote.escape({call, expr}, :none, true)

try do
# TODO binding?
{{call, expr}, _} = Code.eval_quoted({call, expr}, [], env)
{call, expr}
rescue
_ -> raise "unable to eval #{inspect({call, expr})}"
end
else
{call, expr}
end
# if there are unquote fragments in either call or body elixir escapes both and evaluates
# if unquoted_expr or unquoted_call, do: __MODULE__.Quote.escape({call, expr}, :none, true)
# instead we try to expand the call and body ignoring the unquotes
#

{name_and_args, guards} = __MODULE__.Utils.extract_guards(call)

# 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} =
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}
_ -> raise "invalid_def #{inspect(name_and_args)}"
{n, m, a} when is_atom(a) -> {:__unknown__, m, []}

Check warning on line 1645 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 "n" is unused (if the variable is not meant to be used, prefix it with an underscore)

Check warning on line 1645 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 "n" is unused (if the variable is not meant to be used, prefix it with an underscore)

Check warning on line 1645 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 "n" is unused (if the variable is not meant to be used, prefix it with an underscore)

Check warning on line 1645 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 "n" is unused (if the variable is not meant to be used, prefix it with an underscore)

Check warning on line 1645 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 "n" is unused (if the variable is not meant to be used, prefix it with an underscore)

Check warning on line 1645 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 "n" is unused (if the variable is not meant to be used, prefix it with an underscore)

Check warning on line 1645 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 "n" is unused (if the variable is not meant to be used, prefix it with an underscore)

Check warning on line 1645 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 "n" is unused (if the variable is not meant to be used, prefix it with an underscore)

Check warning on line 1645 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 "n" is unused (if the variable is not meant to be used, prefix it with an underscore)

Check warning on line 1645 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 "n" is unused (if the variable is not meant to be used, prefix it with an underscore)

Check warning on line 1645 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 "n" is unused (if the variable is not meant to be used, prefix it with an underscore)

Check warning on line 1645 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 "n" is unused (if the variable is not meant to be used, prefix it with an underscore)

Check warning on line 1645 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 "n" is unused (if the variable is not meant to be used, prefix it with an underscore)

Check warning on line 1645 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 "n" is unused (if the variable is not meant to be used, prefix it with an underscore)

Check warning on line 1645 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 "n" is unused (if the variable is not meant to be used, prefix it with an underscore)

Check warning on line 1645 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 "n" is unused (if the variable is not meant to be used, prefix it with an underscore)

Check warning on line 1645 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 "n" is unused (if the variable is not meant to be used, prefix it with an underscore)

Check warning on line 1645 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 "n" is unused (if the variable is not meant to be used, prefix it with an underscore)

Check warning on line 1645 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 "n" is unused (if the variable is not meant to be used, prefix it with an underscore)

Check warning on line 1645 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 "n" is unused (if the variable is not meant to be used, prefix it with an underscore)

Check warning on line 1645 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 "n" is unused (if the variable is not meant to be used, prefix it with an underscore)

Check warning on line 1645 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 "n" is unused (if the variable is not meant to be used, prefix it with an underscore)
{n, m, a} when is_list(a) -> {:__unknown__, m, a}

Check warning on line 1646 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 "n" is unused (if the variable is not meant to be used, prefix it with an underscore)

Check warning on line 1646 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 "n" is unused (if the variable is not meant to be used, prefix it with an underscore)

Check warning on line 1646 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 "n" is unused (if the variable is not meant to be used, prefix it with an underscore)

Check warning on line 1646 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 "n" is unused (if the variable is not meant to be used, prefix it with an underscore)

Check warning on line 1646 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 "n" is unused (if the variable is not meant to be used, prefix it with an underscore)

Check warning on line 1646 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 "n" is unused (if the variable is not meant to be used, prefix it with an underscore)

Check warning on line 1646 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 "n" is unused (if the variable is not meant to be used, prefix it with an underscore)

Check warning on line 1646 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 "n" is unused (if the variable is not meant to be used, prefix it with an underscore)

Check warning on line 1646 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 "n" is unused (if the variable is not meant to be used, prefix it with an underscore)

Check warning on line 1646 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 "n" is unused (if the variable is not meant to be used, prefix it with an underscore)

Check warning on line 1646 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 "n" is unused (if the variable is not meant to be used, prefix it with an underscore)

Check warning on line 1646 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 "n" is unused (if the variable is not meant to be used, prefix it with an underscore)

Check warning on line 1646 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 "n" is unused (if the variable is not meant to be used, prefix it with an underscore)

Check warning on line 1646 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 "n" is unused (if the variable is not meant to be used, prefix it with an underscore)

Check warning on line 1646 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 "n" is unused (if the variable is not meant to be used, prefix it with an underscore)

Check warning on line 1646 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 "n" is unused (if the variable is not meant to be used, prefix it with an underscore)

Check warning on line 1646 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 "n" is unused (if the variable is not meant to be used, prefix it with an underscore)

Check warning on line 1646 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 "n" is unused (if the variable is not meant to be used, prefix it with an underscore)

Check warning on line 1646 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 "n" is unused (if the variable is not meant to be used, prefix it with an underscore)

Check warning on line 1646 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 "n" is unused (if the variable is not meant to be used, prefix it with an underscore)

Check warning on line 1646 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 "n" is unused (if the variable is not meant to be used, prefix it with an underscore)

Check warning on line 1646 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 "n" is unused (if the variable is not meant to be used, prefix it with an underscore)
_ -> {:__unknown__, [], []}
end

arity = length(args)

# based on :elixir_def.env_for_expansion
state =
%{
state
| vars: {%{}, false},
unused: 0,
caller: def_kind in [:defmacro, :defmacrop, :defguard, :defguardp]
}
|> new_func_vars_scope
unless has_unquotes do
# module vars are not accessible in def body
%{
state
| vars: {%{}, false},
unused: 0,
caller: def_kind in [:defmacro, :defmacrop, :defguard, :defguardp]
}
|> new_func_vars_scope()
else
# make module variables accessible if there are unquote fragments in def body
%{
state
| caller: def_kind in [:defmacro, :defmacrop, :defguard, :defguardp]
}
end

env_for_expand = %{env | function: {name, arity}}

Expand Down Expand Up @@ -1716,7 +1712,15 @@ defmodule ElixirSense.Core.Compiler do
%{state | vars: vars, unused: unused, caller: false}
|> maybe_move_vars_to_outer_scope
|> remove_vars_scope
|> remove_func_vars_scope

state =
unless has_unquotes do
# restore module vars
remove_func_vars_scope(state)
else
# no need to do anything
state
end

# result of def expansion is fa tuple
{{name, arity}, state, env}
Expand Down
191 changes: 116 additions & 75 deletions test/elixir_sense/core/metadata_builder_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -578,25 +578,23 @@ defmodule ElixirSense.Core.MetadataBuilderTest do
assert [] = state |> get_line_vars(3)
end

if Version.match?(System.version(), ">= 1.17.0-dev") and @compiler do
test "for bitstring" do
state =
"""
for <<r::8, g::8, b::8 <- pixels>> do
record_env()
end
test "for bitstring" do
state =
"""
for <<r::8, g::8, b::8 <- pixels>> do
record_env()
"""
|> string_to_state
end
record_env()
"""
|> string_to_state

assert Map.keys(state.lines_to_env[2].versioned_vars) == [{:b, nil}, {:g, nil}, {:r, nil}]
assert Map.keys(state.lines_to_env[2].versioned_vars) == [{:b, nil}, {:g, nil}, {:r, nil}]

assert [
%VarInfo{name: :b, positions: [{1, 19}]},
%VarInfo{name: :g, positions: [{1, 13}]},
%VarInfo{name: :r, positions: [{1, 7}]}
] = state |> get_line_vars(2)
end
assert [
%VarInfo{name: :b, positions: [{1, 19}]},
%VarInfo{name: :g, positions: [{1, 13}]},
%VarInfo{name: :r, positions: [{1, 7}]}
] = state |> get_line_vars(2)
end

test "for assignment" do
Expand Down Expand Up @@ -917,6 +915,32 @@ defmodule ElixirSense.Core.MetadataBuilderTest do
] = state |> get_line_vars(8)
end

test "in unquote fragment" do
state =
"""
defmodule MyModuleWithFuns do
kv = [foo: 1, bar: 2] |> IO.inspect
Enum.each(kv, fn {k, v} ->
@spec unquote(k)() :: unquote(v)
def unquote(k)() do
unquote(v)
record_env()
end
end)
end
"""
|> string_to_state

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

# TODO we are not tracking usages in typespec
assert [
%VarInfo{name: :k, positions: [{3, 21}]},
%VarInfo{name: :kv, positions: [{2, 3}, {3, 13}]},
%VarInfo{name: :v, positions: [{3, 24}, {6, 15}]}
] = state |> get_line_vars(7)
end

test "in capture" do
state =
"""
Expand Down Expand Up @@ -2514,7 +2538,7 @@ defmodule ElixirSense.Core.MetadataBuilderTest do
"""
|> string_to_state

vars = state |> get_line_vars(6)
# vars = state |> get_line_vars(6)

# TODO wtf
# assert %VarInfo{
Expand Down Expand Up @@ -5656,52 +5680,44 @@ defmodule ElixirSense.Core.MetadataBuilderTest do
} = state.mods_funs_to_positions
end

if @expand_eval do
test "registers defs with unquote fragments" do
state =
"""
defmodule MyModuleWithFuns do
def unquote(:foo)(), do: :ok
def bar(), do: unquote(:ok)
def baz(unquote(:abc)), do: unquote(:abc)
end
"""
|> string_to_state
test "registers defs with unquote fragments in body" do
state =
"""
defmodule MyModuleWithFuns do
kv = [foo: 1]
Enum.each(kv, fn {k, v} ->
def foo(), do: unquote(v)
end)
end
"""
|> string_to_state

assert %{
{MyModuleWithFuns, :foo, 0} => %ModFunInfo{
params: [[]]
},
{MyModuleWithFuns, :bar, 0} => %ModFunInfo{
params: [[]]
},
{MyModuleWithFuns, :baz, 1} => %ModFunInfo{
params: [[:abc]]
}
} = state.mods_funs_to_positions
end
assert %{
{MyModuleWithFuns, :foo, 0} => %ModFunInfo{
params: [[]]
}
} = state.mods_funs_to_positions
end

test "registers defs with unquote fragments with binding" do
state =
"""
defmodule MyModuleWithFuns do
kv = [foo: 1, bar: 2] |> IO.inspect
Enum.each(kv, fn {k, v} ->
def unquote(k)(), do: unquote(v)
end)
end
"""
|> string_to_state
test "registers unknown for defs with unquote fragments in call" do
state =
"""
defmodule MyModuleWithFuns do
kv = [foo: 1, bar: 2]
Enum.each(kv, fn {k, v} ->
def unquote(k)(), do: 123
end)
end
"""
|> string_to_state

assert %{
{MyModuleWithFuns, :foo, 0} => %ModFunInfo{
params: [[]]
},
{MyModuleWithFuns, :bar, 0} => %ModFunInfo{
params: [[]]
}
} = state.mods_funs_to_positions
end
assert Map.keys(state.mods_funs_to_positions) == [
{MyModuleWithFuns, :__info__, 1},
{MyModuleWithFuns, :__unknown__, 0},
{MyModuleWithFuns, :module_info, 0},
{MyModuleWithFuns, :module_info, 1},
{MyModuleWithFuns, nil, nil}
]
end

test "registers builtin functions for protocols" do
Expand Down Expand Up @@ -6338,10 +6354,6 @@ defmodule ElixirSense.Core.MetadataBuilderTest do
end

describe "calls" do
defp sort_calls(calls) do
calls |> Enum.map(fn {k, v} -> {k, Enum.sort(v)} end) |> Map.new()
end

test "registers calls with __MODULE__" do
state =
"""
Expand Down Expand Up @@ -7204,6 +7216,46 @@ defmodule ElixirSense.Core.MetadataBuilderTest do
} = state.types
end

test "registers types with unquote fragments in body" do
state =
"""
defmodule My do
kv = [foo: 1]
Enum.each(kv, fn {k, v} ->
@type foo :: unquote(v)
end)
end
"""
|> string_to_state

assert %{
{My, :foo, 0} => %ElixirSense.Core.State.TypeInfo{
args: [[]],
kind: :type,
name: :foo,
positions: [{4, 5}],
end_positions: [{4, 28}],
generated: [false],
specs: ["@type foo() :: unquote(v())"]
}
} = state.types
end

test "skips types with unquote fragments in call" do
state =
"""
defmodule My do
kv = [foo: 1]
Enum.each(kv, fn {k, v} ->
@type unquote(k)() :: 123
end)
end
"""
|> string_to_state

assert %{} == state.types
end

test "protocol exports type t" do
state =
"""
Expand Down Expand Up @@ -8160,17 +8212,6 @@ defmodule ElixirSense.Core.MetadataBuilderTest do
|> MetadataBuilder.build()
end

defp get_scope_vars(state, line) do
case state.lines_to_env[line] do
nil ->
[]

env ->
state.vars_info_per_scope_id[env.scope_id]
end
|> Enum.sort()
end

defp get_line_vars(state, line) do
case state.lines_to_env[line] do
nil ->
Expand Down

0 comments on commit 8cdbd94

Please sign in to comment.