Skip to content

Commit 207e51b

Browse files
committed
Prove hints for empty domains from unions
1 parent 13f0eef commit 207e51b

File tree

3 files changed

+65
-22
lines changed

3 files changed

+65
-22
lines changed

lib/elixir/lib/module/types/apply.ex

Lines changed: 19 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -832,18 +832,10 @@ defmodule Module.Types.Apply do
832832
## Diagnostics
833833

834834
def format_diagnostic({{:badapply, reason}, args_types, fun_type, expr, context}) do
835-
traces =
836-
case reason do
837-
# Include arguments in traces in case of badarg
838-
{:badarg, _} -> collect_traces(expr, context)
839-
# Otherwise just the fun
840-
_ -> collect_traces(elem(expr, 0), context)
841-
end
842-
843-
message =
835+
{message, to_trace, hints} =
844836
case reason do
845837
{:badarg, domain} ->
846-
"""
838+
message = """
847839
incompatible types given on function application:
848840
849841
#{expr_to_string(expr) |> indent(4)}
@@ -857,16 +849,15 @@ defmodule Module.Types.Apply do
857849
#{to_quoted_string(fun_type) |> indent(4)}
858850
"""
859851

860-
:badarg ->
861-
"""
862-
expected a #{length(args_types)}-arity function on call:
863-
864-
#{expr_to_string(expr) |> indent(4)}
865-
866-
but got type:
852+
hints =
853+
cond do
854+
not empty?(args_to_domain(domain)) -> []
855+
match?({:or, _, _}, to_quoted(fun_type)) -> [:empty_union_domain]
856+
true -> [:empty_domain]
857+
end
867858

868-
#{to_quoted_string(fun_type) |> indent(4)}
869-
"""
859+
# When there is an argument error, we trace the arguments
860+
{message, elem(expr, 2), hints}
870861

871862
{:badarity, arities} ->
872863
info =
@@ -875,7 +866,7 @@ defmodule Module.Types.Apply do
875866
_ -> "function with arities #{Enum.join(arities, ",")}"
876867
end
877868

878-
"""
869+
message = """
879870
expected a #{length(args_types)}-arity function on call:
880871
881872
#{expr_to_string(expr) |> indent(4)}
@@ -885,8 +876,10 @@ defmodule Module.Types.Apply do
885876
#{to_quoted_string(fun_type) |> indent(4)}
886877
"""
887878

879+
{message, elem(expr, 0), []}
880+
888881
:badfun ->
889-
"""
882+
message = """
890883
expected a #{length(args_types)}-arity function on call:
891884
892885
#{expr_to_string(expr) |> indent(4)}
@@ -895,11 +888,15 @@ defmodule Module.Types.Apply do
895888
896889
#{to_quoted_string(fun_type) |> indent(4)}
897890
"""
891+
892+
{message, elem(expr, 0), []}
898893
end
899894

895+
traces = collect_traces(to_trace, context)
896+
900897
%{
901898
details: %{typing_traces: traces},
902-
message: IO.iodata_to_binary([message, format_traces(traces)])
899+
message: IO.iodata_to_binary([message, format_traces(traces), format_hints(hints)])
903900
}
904901
end
905902

lib/elixir/lib/module/types/helpers.ex

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,22 @@ defmodule Module.Types.Helpers do
122122
If you are trying to access an exception's :message key, either specify the \
123123
exception names or use `Exception.message/1`.
124124
"""
125+
126+
:empty_union_domain ->
127+
"""
128+
129+
#{hint()} the function has an empty domain and therefore cannot be applied to \
130+
any argument. This may happen when you have a union of functions, which means \
131+
the only valid argument to said function are types that satisfy all sides of \
132+
the union (which may be none)
133+
"""
134+
135+
:empty_domain ->
136+
"""
137+
138+
#{hint()} the function has an empty domain and therefore cannot be applied to \
139+
any argument
140+
"""
125141
end)
126142
end
127143

lib/elixir/test/elixir/module/types/expr_test.exs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,36 @@ defmodule Module.Types.ExprTest do
184184
185185
(binary() -> integer())
186186
"""
187+
188+
assert typeerror!(
189+
[x],
190+
(if x do
191+
&String.to_integer/1
192+
else
193+
&List.to_integer/1
194+
end).(:foo)
195+
)
196+
|> strip_ansi() == ~l"""
197+
incompatible types given on function application:
198+
199+
(if x do
200+
&String.to_integer/1
201+
else
202+
&List.to_integer/1
203+
end).(:foo)
204+
205+
given types:
206+
207+
:foo
208+
209+
but function has type:
210+
211+
(binary() -> integer()) or (non_empty_list(integer()) -> integer())
212+
213+
hint: the function has an empty domain and therefore cannot be applied to any argument. \
214+
This may happen when you have a union of functions, which means the only valid argument \
215+
to said function are types that satisfy all sides of the union (which may be none)
216+
"""
187217
end
188218
end
189219

0 commit comments

Comments
 (0)