Skip to content

Commit

Permalink
make sure structs are expanded until fixpoint
Browse files Browse the repository at this point in the history
fix crash when completing var bound to call result being a struct
  • Loading branch information
lukaszsamson committed Nov 11, 2023
1 parent ed74f77 commit 8e212ae
Show file tree
Hide file tree
Showing 5 changed files with 69 additions and 39 deletions.
28 changes: 16 additions & 12 deletions lib/elixir_sense/core/binding.ex
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,20 @@ defmodule ElixirSense.Core.Binding do
end

def expand(%Binding{} = env, expanded, stack \\ []) do
unless expanded in stack do
do_expand(env, expanded, [expanded | stack])
res =
unless expanded in stack do
do_expand(env, expanded, [expanded | stack])
end

case res do
{:struct, _, _, _} ->
do_expand(env, res, [res | stack])

{:map, _, _} ->
do_expand(env, res, [res | stack])

_ ->
res
end
end

Expand Down Expand Up @@ -98,14 +110,6 @@ defmodule ElixirSense.Core.Binding do
expand(env, type, stack)
end

def do_expand(
_env,
{:struct, _fields, module, _updated_struct} = s,
_stack
)
when is_atom(module) and not is_nil(module),
do: s

def do_expand(
%Binding{structs: structs} = env,
{:struct, fields, module, updated_struct},
Expand Down Expand Up @@ -133,7 +137,7 @@ defmodule ElixirSense.Core.Binding do
{fields, module} =
get_struct_fields(env, get_fields_from(expanded) |> Keyword.merge(fields), module)

{:struct, fields, module, nil}
{:struct, fields, if(module != nil, do: {:atom, module}), nil}
else
:none
end
Expand Down Expand Up @@ -1375,7 +1379,7 @@ defmodule ElixirSense.Core.Binding do
{key, from_var(value)}
end

{:struct, fields |> Keyword.put(:__struct__, {:atom, type}), type, nil}
{:struct, fields |> Keyword.put(:__struct__, {:atom, type}), {:atom, type}, nil}
end

def from_var(map) when is_map(map) do
Expand Down
7 changes: 5 additions & 2 deletions lib/elixir_sense/providers/suggestion/complete.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1216,15 +1216,18 @@ defmodule ElixirSense.Providers.Suggestion.Complete do
defp match_map_fields(fields, hint, type, %State.Env{} = _env, %Metadata{} = metadata) do
{subtype, origin, types} =
case type do
{:struct, mod} when is_atom(mod) ->
{:struct, {:atom, mod}} ->
types =
Reducers.Struct.get_field_types(
metadata,
mod,
true
)

{:struct_field, if(mod, do: inspect(mod)), types}
{:struct_field, inspect(mod), types}

{:struct, nil} ->
{:struct_field, nil, %{}}

:map ->
{:map_key, nil, %{}}
Expand Down
7 changes: 5 additions & 2 deletions lib/elixir_sense/providers/suggestion/reducers/struct.ex
Original file line number Diff line number Diff line change
Expand Up @@ -107,10 +107,13 @@ defmodule ElixirSense.Providers.Suggestion.Reducers.Struct do
defp expand_map_field_access(metadata, fields, hint, type, fields_so_far) do
{subtype, origin, types} =
case type do
{:struct, mod} ->
{:struct, {:atom, mod}} ->
types = get_field_types(metadata, mod, true)

{:struct_field, if(mod, do: inspect(mod)), types}
{:struct_field, inspect(mod), types}

{:struct, nil} ->
{:struct_field, nil, %{}}

:map ->
{:map_key, nil, %{}}
Expand Down
46 changes: 23 additions & 23 deletions test/elixir_sense/core/binding_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -97,15 +97,15 @@ defmodule ElixirSense.Core.BindingTest do
__struct__: {:atom, ElixirSenseExample.ModuleWithTypedStruct},
other: nil,
typed_field: nil
], ElixirSenseExample.ModuleWithTypedStruct,
], {:atom, ElixirSenseExample.ModuleWithTypedStruct},
nil} ==
Binding.expand(
@env,
{:struct, [], {:atom, ElixirSenseExample.ModuleWithTypedStruct}, nil}
)
end

test "introspection module not a stuct" do
test "introspection module not a struct" do
assert :none ==
Binding.expand(@env, {:struct, [], {:atom, ElixirSenseExample.EmptyModule}, nil})
end
Expand All @@ -116,7 +116,7 @@ defmodule ElixirSense.Core.BindingTest do
__struct__: {:atom, ElixirSenseExample.ModuleWithTypedStruct},
other: {:atom, :a},
typed_field: {:atom, :b}
], ElixirSenseExample.ModuleWithTypedStruct,
], {:atom, ElixirSenseExample.ModuleWithTypedStruct},
nil} ==
Binding.expand(
@env,
Expand All @@ -133,7 +133,7 @@ defmodule ElixirSense.Core.BindingTest do
__struct__: {:atom, ElixirSenseExample.ModuleWithTypedStruct},
other: {:atom, :a},
typed_field: {:atom, :b}
], ElixirSenseExample.ModuleWithTypedStruct,
], {:atom, ElixirSenseExample.ModuleWithTypedStruct},
nil} ==
Binding.expand(
@env,
Expand All @@ -149,7 +149,7 @@ defmodule ElixirSense.Core.BindingTest do
__struct__: {:atom, ElixirSenseExample.ModuleWithTypedStruct},
other: nil,
typed_field: nil
], ElixirSenseExample.ModuleWithTypedStruct,
], {:atom, ElixirSenseExample.ModuleWithTypedStruct},
nil} ==
Binding.expand(
@env
Expand All @@ -175,7 +175,7 @@ defmodule ElixirSense.Core.BindingTest do
end

test "metadata struct" do
assert {:struct, [__struct__: {:atom, MyMod}, abc: nil], MyMod, nil} ==
assert {:struct, [__struct__: {:atom, MyMod}, abc: nil], {:atom, MyMod}, nil} ==
Binding.expand(
@env
|> Map.merge(%{
Expand Down Expand Up @@ -832,7 +832,7 @@ defmodule ElixirSense.Core.BindingTest do
[
__struct__: {:atom, ElixirSenseExample.FunctionsWithReturnSpec},
abc: {:map, [key: {:atom, nil}], nil}
], ElixirSenseExample.FunctionsWithReturnSpec,
], {:atom, ElixirSenseExample.FunctionsWithReturnSpec},
nil} ==
Binding.expand(
@env
Expand All @@ -849,7 +849,7 @@ defmodule ElixirSense.Core.BindingTest do
test "remote call fun with spec remote t expanding to struct" do
assert {:struct,
[__struct__: {:atom, ElixirSenseExample.FunctionsWithReturnSpec.Remote}, abc: nil],
ElixirSenseExample.FunctionsWithReturnSpec.Remote,
{:atom, ElixirSenseExample.FunctionsWithReturnSpec.Remote},
nil} ==
Binding.expand(
@env
Expand All @@ -866,7 +866,7 @@ defmodule ElixirSense.Core.BindingTest do
test "remote call fun with spec struct" do
assert {:struct,
[__struct__: {:atom, ElixirSenseExample.FunctionsWithReturnSpec}, abc: nil],
ElixirSenseExample.FunctionsWithReturnSpec,
{:atom, ElixirSenseExample.FunctionsWithReturnSpec},
nil} ==
Binding.expand(
@env
Expand Down Expand Up @@ -1041,7 +1041,7 @@ defmodule ElixirSense.Core.BindingTest do
end

test "local call metadata fun returning struct" do
assert {:struct, [{:__struct__, {:atom, MyMod}}, {:abc, nil}], MyMod, nil} ==
assert {:struct, [{:__struct__, {:atom, MyMod}}, {:abc, nil}], {:atom, MyMod}, nil} ==
Binding.expand(
@env
|> Map.merge(%{
Expand Down Expand Up @@ -1074,7 +1074,7 @@ defmodule ElixirSense.Core.BindingTest do
end

test "local call metadata fun returning local type expanding to struct" do
assert {:struct, [{:__struct__, {:atom, MyMod}}, {:abc, nil}], MyMod, nil} ==
assert {:struct, [{:__struct__, {:atom, MyMod}}, {:abc, nil}], {:atom, MyMod}, nil} ==
Binding.expand(
@env
|> Map.merge(%{
Expand Down Expand Up @@ -1115,7 +1115,7 @@ defmodule ElixirSense.Core.BindingTest do
end

test "local call metadata fun returning local type expanding to private type" do
assert {:struct, [{:__struct__, {:atom, MyMod}}, {:abc, nil}], MyMod, nil} ==
assert {:struct, [{:__struct__, {:atom, MyMod}}, {:abc, nil}], {:atom, MyMod}, nil} ==
Binding.expand(
@env
|> Map.merge(%{
Expand Down Expand Up @@ -1158,7 +1158,7 @@ defmodule ElixirSense.Core.BindingTest do
end

test "remote call metadata public fun returning local type expanding to struct" do
assert {:struct, [{:__struct__, {:atom, MyMod}}, {:abc, nil}], MyMod, nil} ==
assert {:struct, [{:__struct__, {:atom, MyMod}}, {:abc, nil}], {:atom, MyMod}, nil} ==
Binding.expand(
@env
|> Map.merge(%{
Expand Down Expand Up @@ -1323,7 +1323,7 @@ defmodule ElixirSense.Core.BindingTest do
{:variable, :ref}
)

assert {:struct, [{:__struct__, {:atom, MyMod}}, {:abc, nil}], MyMod, nil} ==
assert {:struct, [{:__struct__, {:atom, MyMod}}, {:abc, nil}], {:atom, MyMod}, nil} ==
Binding.expand(
env
|> Map.put(:variables, [
Expand All @@ -1332,7 +1332,7 @@ defmodule ElixirSense.Core.BindingTest do
{:variable, :ref}
)

assert {:struct, [{:__struct__, {:atom, MyMod}}, {:abc, nil}], MyMod, nil} ==
assert {:struct, [{:__struct__, {:atom, MyMod}}, {:abc, nil}], {:atom, MyMod}, nil} ==
Binding.expand(
env
|> Map.put(:variables, [
Expand All @@ -1341,7 +1341,7 @@ defmodule ElixirSense.Core.BindingTest do
{:variable, :ref}
)

assert {:struct, [{:__struct__, {:atom, MyMod}}, {:abc, nil}], MyMod, nil} ==
assert {:struct, [{:__struct__, {:atom, MyMod}}, {:abc, nil}], {:atom, MyMod}, nil} ==
Binding.expand(
env
|> Map.put(:variables, [
Expand Down Expand Up @@ -1869,7 +1869,7 @@ defmodule ElixirSense.Core.BindingTest do
test "intersection" do
assert {:struct,
[{:__struct__, {:atom, State}}, {:abc, nil}, {:formatted, {:variable, :formatted}}],
State,
{:atom, State},
nil} ==
Binding.expand(
@env
Expand Down Expand Up @@ -1956,7 +1956,7 @@ defmodule ElixirSense.Core.BindingTest do
{:__struct__, {:atom, State}},
{:abc, {:atom, X}},
{:formatted, {:variable, :formatted}}
], State,
], {:atom, State},
nil} ==
Binding.expand(
@env
Expand All @@ -1979,7 +1979,7 @@ defmodule ElixirSense.Core.BindingTest do
{:__struct__, {:atom, State}},
{:abc, {:atom, X}},
{:formatted, {:variable, :formatted}}
], State,
], {:atom, State},
nil} ==
Binding.expand(
@env
Expand Down Expand Up @@ -2042,7 +2042,7 @@ defmodule ElixirSense.Core.BindingTest do
{:__struct__, {:atom, State}},
{:abc, {:atom, X}},
{:formatted, {:variable, :formatted}}
], State,
], {:atom, State},
nil} ==
Binding.expand(
@env
Expand All @@ -2065,7 +2065,7 @@ defmodule ElixirSense.Core.BindingTest do
{:__struct__, {:atom, State}},
{:abc, {:atom, X}},
{:formatted, {:variable, :formatted}}
], State,
], {:atom, State},
nil} ==
Binding.expand(
@env
Expand All @@ -2090,7 +2090,7 @@ defmodule ElixirSense.Core.BindingTest do
{:__struct__, {:atom, State}},
{:abc, {:atom, X}},
{:formatted, {:variable, :formatted}}
], State,
], {:atom, State},
nil} ==
Binding.expand(
@env
Expand Down Expand Up @@ -2163,7 +2163,7 @@ defmodule ElixirSense.Core.BindingTest do
assert_is_stable(
Binding.from_var(%{__struct__: BindingTest.Some, asd: 123}),
{:struct, [{:__struct__, {:atom, BindingTest.Some}}, {:asd, {:integer, 123}}],
BindingTest.Some, nil}
{:atom, BindingTest.Some}, nil}
)
end
end
Expand Down
20 changes: 20 additions & 0 deletions test/elixir_sense/providers/suggestion/complete_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -589,6 +589,14 @@ defmodule ElixirSense.Providers.Suggestion.CompleteTest do
%VarInfo{
name: :var,
type: {:variable, :struct}
},
%VarInfo{
name: :yyyy,
type: {:map, [date: {:struct, [], {:atom, DateTime}, nil}], []}
},
%VarInfo{
name: :xxxx,
type: {:call, {:atom, Map}, :fetch!, [{:variable, :yyyy}, {:atom, :date}]}
}
]
}
Expand Down Expand Up @@ -654,6 +662,18 @@ defmodule ElixirSense.Providers.Suggestion.CompleteTest do
type_spec: "Calendar.hour()"
}
]

assert expand(~c"xxxx.h", env, metadata) ==
[
%{
call?: true,
name: "hour",
origin: "DateTime",
subtype: :struct_field,
type: :field,
type_spec: "Calendar.hour()"
}
]
end

test "map atom key completion is supported on attributes" do
Expand Down

0 comments on commit 8e212ae

Please sign in to comment.