diff --git a/lib/elixir_sense/core/compiler.ex b/lib/elixir_sense/core/compiler.ex index e57411d5..8657e327 100644 --- a/lib/elixir_sense/core/compiler.ex +++ b/lib/elixir_sense/core/compiler.ex @@ -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) @@ -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) diff --git a/lib/elixir_sense/core/compiler/typespec.ex b/lib/elixir_sense/core/compiler/typespec.ex index 8b5c99f4..16b5715d 100644 --- a/lib/elixir_sense/core/compiler/typespec.ex +++ b/lib/elixir_sense/core/compiler/typespec.ex @@ -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 @@ -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 [] @@ -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 @@ -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 diff --git a/test/elixir_sense/core/metadata_builder_test.exs b/test/elixir_sense/core/metadata_builder_test.exs index 008f213e..4d05813a 100644 --- a/test/elixir_sense/core/metadata_builder_test.exs +++ b/test/elixir_sense/core/metadata_builder_test.exs @@ -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() @@ -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 @@ -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 = """ @@ -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" ] }, @@ -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 } @@ -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 @@ -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}], diff --git a/test/support/macro_hygiene.ex b/test/support/macro_hygiene.ex new file mode 100644 index 00000000..13a5b71b --- /dev/null +++ b/test/support/macro_hygiene.ex @@ -0,0 +1,8 @@ +defmodule ElixirSenseExample.Math do + defmacro squared(x) do + quote do + x = unquote(x) + x * x + end + end +end