Skip to content

Commit

Permalink
better type inference in map guards
Browse files Browse the repository at this point in the history
  • Loading branch information
lukaszsamson committed Sep 19, 2024
1 parent 332ed4a commit b117cae
Show file tree
Hide file tree
Showing 2 changed files with 76 additions and 5 deletions.
37 changes: 34 additions & 3 deletions lib/elixir_sense/core/type_inference/guard.ex
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,19 @@ defmodule ElixirSense.Core.TypeInference.Guard do
end)
end

# {{:., _, [target, key]}, _, []}
def type_information_from_guards({{:., _, [target, key]}, _, []}) when is_atom(key) do
case extract_var_type(target, {:map, [{key, {:atom, true}}], []}) do
nil -> %{}
{var, type} -> %{var => type}
end
end

# Standalone variable: func my_func(x) when x
def type_information_from_guards({var, meta, context}) when is_atom(var) and is_atom(context) do
case Keyword.fetch(meta, :version) do
{:ok, version} ->
%{{var, version} => :boolean}
%{{var, version} => {:atom, true}}

_ ->
%{}
Expand All @@ -94,8 +102,7 @@ defmodule ElixirSense.Core.TypeInference.Guard do
{{:., _dot_meta, [:erlang, fun]}, _call_meta, params}, acc
when is_atom(fun) and is_list(params) ->
with {type, binding} <- guard_predicate_type(fun, params),
{var, meta, context} when is_atom(var) and is_atom(context) <- binding,
{:ok, version} <- Keyword.fetch(meta, :version) do
{{var, version}, type} <- extract_var_type(binding, type) do
# If we found the predicate type, we can prematurely exit traversing the subtree
{nil, Map.put(acc, {var, version}, type)}
else
Expand All @@ -115,6 +122,22 @@ defmodule ElixirSense.Core.TypeInference.Guard do
acc
end

defp extract_var_type({var, meta, context}, type) when is_atom(var) and is_atom(context) do
case Keyword.fetch(meta, :version) do
{:ok, version} ->
{{var, version}, type}

_ ->
nil
end
end

defp extract_var_type({{:., _, [target, key]}, _, []}, type) when is_atom(key) do
extract_var_type(target, {:map, [{key, type}], []})
end

defp extract_var_type(_, _), do: nil

# TODO div and rem only work on first arg
defp guard_predicate_type(p, [first | _])
when p in [
Expand Down Expand Up @@ -221,11 +244,19 @@ defmodule ElixirSense.Core.TypeInference.Guard do
{type_of(value), lhs}
end

defp guard_predicate_type(p, [{{:., _, _}, _, []} = lhs, value]) when p in [:==, :===] do
{type_of(value), lhs}
end

defp guard_predicate_type(p, [value, {variable, _, context} = rhs])
when p in [:==, :===] and is_atom(variable) and is_atom(context) do
guard_predicate_type(p, [rhs, value])
end

defp guard_predicate_type(p, [value, {{:., _, _}, _, []} = rhs]) when p in [:==, :===] do
guard_predicate_type(p, [rhs, value])
end

defp guard_predicate_type(:is_map, [first | _]), do: {{:map, [], nil}, first}
defp guard_predicate_type(:is_non_struct_map, [first | _]), do: {{:map, [], nil}, first}
defp guard_predicate_type(:map_size, [first | _]), do: {{:map, [], nil}, first}
Expand Down
44 changes: 42 additions & 2 deletions test/elixir_sense/core/type_inference/guard_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ defmodule ElixirSense.Core.TypeInference.GuardTest do
test "infers type from naked var" do
guard_expr = quote(do: x) |> expand()
result = Guard.type_information_from_guards(guard_expr)
assert result == %{{:x, 0} => :boolean}
assert result == %{{:x, 0} => {:atom, true}}
end

# 1. Simple guards
Expand Down Expand Up @@ -270,7 +270,7 @@ defmodule ElixirSense.Core.TypeInference.GuardTest do
guard = quote(do: is_number(x) or is_atom(x) or (is_nil(x) or x)) |> expand()

result = Guard.type_information_from_guards(guard)
assert result == %{{:x, 0} => {:union, [:number, :atom, {:atom, nil}, :boolean]}}
assert result == %{{:x, 0} => {:union, [:number, :atom, {:atom, nil}, {:atom, true}]}}
end

test "handles nested when" do
Expand All @@ -280,4 +280,44 @@ defmodule ElixirSense.Core.TypeInference.GuardTest do
assert result == %{{:x, 0} => {:union, [:number, :binary]}}
end
end

describe "guard on map field" do
test "naked" do
guard = quote(do: x.foo) |> expand()

result = Guard.type_information_from_guards(guard)
assert result == %{{:x, 0} => {:map, [{:foo, {:atom, true}}], []}}
end

test "naked nested" do
guard = quote(do: x.foo.bar) |> expand()

result = Guard.type_information_from_guards(guard)
assert result == %{{:x, 0} => {:map, [{:foo, {:map, [{:bar, {:atom, true}}], []}}], []}}
end

test "simple" do
guard = quote(do: is_atom(x.foo)) |> expand()

result = Guard.type_information_from_guards(guard)
assert result == %{{:x, 0} => {:map, [{:foo, :atom}], []}}
end

test "nested" do
guard = quote(do: is_atom(x.foo.bar.baz)) |> expand()

result = Guard.type_information_from_guards(guard)

assert result == %{
{:x, 0} => {:map, [{:foo, {:map, [{:bar, {:map, [{:baz, :atom}], []}}], []}}], []}
}
end

test "with operator" do
guard = quote(do: x.foo == 1) |> expand()

result = Guard.type_information_from_guards(guard)
assert result == %{{:x, 0} => {:map, [{:foo, {:integer, 1}}], []}}
end
end
end

0 comments on commit b117cae

Please sign in to comment.