Skip to content

Commit a4bbb96

Browse files
committed
Include a hint for defimpl type checking
1 parent 0f597b7 commit a4bbb96

File tree

4 files changed

+46
-12
lines changed

4 files changed

+46
-12
lines changed

lib/elixir/lib/module/types.ex

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@ defmodule Module.Types do
4949
finder =
5050
fn fun_arity ->
5151
case :lists.keyfind(fun_arity, 1, defs) do
52-
{_, kind, _, _} = clause ->
53-
{infer_mode(kind, infer_signatures?), clause, default_domain(fun_arity, impl)}
52+
{_, kind, _, _} = def ->
53+
default_domain(infer_mode(kind, infer_signatures?), def, fun_arity, impl)
5454

5555
false ->
5656
false
@@ -82,7 +82,7 @@ defmodule Module.Types do
8282
{types, context} ->
8383
# Optimized version of finder, since we already the definition
8484
finder = fn _ ->
85-
{infer_mode(kind, infer_signatures?), def, default_domain(fun_arity, impl)}
85+
default_domain(infer_mode(kind, infer_signatures?), def, fun_arity, impl)
8686
end
8787

8888
{_kind, inferred, context} = local_handler(meta, fun_arity, stack, context, finder)
@@ -146,12 +146,24 @@ defmodule Module.Types do
146146
end
147147
end
148148

149-
defp default_domain({_, arity} = fun_arity, impl) do
149+
defp default_domain(mode, def, {_, arity} = fun_arity, impl) do
150150
with {for, callbacks} <- impl,
151151
true <- fun_arity in callbacks do
152-
[Descr.dynamic(Module.Types.Of.impl(for)) | List.duplicate(Descr.dynamic(), arity - 1)]
152+
args = [
153+
Descr.dynamic(Module.Types.Of.impl(for))
154+
| List.duplicate(Descr.dynamic(), arity - 1)
155+
]
156+
157+
{fun_arity, kind, meta, clauses} = def
158+
159+
clauses =
160+
for {meta, args, guards, body} <- clauses do
161+
{[type_check: {:impl, for}] ++ meta, args, guards, body}
162+
end
163+
164+
{mode, {fun_arity, kind, meta, clauses}, args}
153165
else
154-
_ -> List.duplicate(Descr.dynamic(), arity)
166+
_ -> {mode, def, List.duplicate(Descr.dynamic(), arity)}
155167
end
156168
end
157169

@@ -208,7 +220,7 @@ defmodule Module.Types do
208220

209221
finder = fn fun_arity ->
210222
case :lists.keyfind(fun_arity, 1, defs) do
211-
{_, _, _, _} = clause -> {:dynamic, clause, default_domain(fun_arity, impl)}
223+
{_, _, _, _} = def -> default_domain(:dynamic, def, fun_arity, impl)
212224
false -> false
213225
end
214226
end
@@ -219,7 +231,7 @@ defmodule Module.Types do
219231
context =
220232
Enum.reduce(defs, context(), fn {fun_arity, _kind, meta, _clauses} = def, context ->
221233
# Optimized version of finder, since we already the definition
222-
finder = fn _ -> {:dynamic, def, default_domain(fun_arity, impl)} end
234+
finder = fn _ -> default_domain(:dynamic, def, fun_arity, impl) end
223235
{_kind, _inferred, context} = local_handler(meta, fun_arity, stack, context, finder)
224236
context
225237
end)

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,20 @@ defmodule Module.Types.Helpers do
132132
the union (which may be none)
133133
"""
134134

135+
{:impl, for} ->
136+
# Get the type without dynamic for better pretty printing
137+
type =
138+
for
139+
|> Module.Types.Of.impl()
140+
|> Module.Types.Descr.dynamic()
141+
|> Map.fetch!(:dynamic)
142+
|> Module.Types.Descr.to_quoted_string(collapse_structs: true)
143+
144+
"""
145+
146+
#{hint()} defimpl for #{inspect(for)} requires its callbacks to match exclusively on #{type}
147+
"""
148+
135149
:empty_domain ->
136150
"""
137151

lib/elixir/lib/module/types/pattern.ex

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -259,12 +259,12 @@ defmodule Module.Types.Pattern do
259259
defp badpattern_error(expr, index, tag, stack, context) do
260260
meta =
261261
if meta = get_meta(expr) do
262-
meta ++ Keyword.take(stack.meta, [:generated, :line])
262+
meta ++ Keyword.take(stack.meta, [:generated, :line, :type_check])
263263
else
264264
stack.meta
265265
end
266266

267-
error(__MODULE__, {:badpattern, expr, index, tag, context}, meta, stack, context)
267+
error(__MODULE__, {:badpattern, meta, expr, index, tag, context}, meta, stack, context)
268268
end
269269

270270
defp of_pattern_intersect(tree, expected, expr, index, tag, stack, context) do
@@ -803,13 +803,19 @@ defmodule Module.Types.Pattern do
803803
#
804804
# The match pattern ones have the whole expression instead
805805
# of a single pattern.
806-
def format_diagnostic({:badpattern, pattern_or_expr, index, tag, context}) do
806+
def format_diagnostic({:badpattern, meta, pattern_or_expr, index, tag, context}) do
807807
{to_trace, message} = badpattern(tag, pattern_or_expr, index)
808808
traces = collect_traces(to_trace, context)
809809

810+
hints =
811+
case Keyword.get(meta, :type_check) do
812+
{:impl, _} = impl -> [impl]
813+
_ -> []
814+
end
815+
810816
%{
811817
details: %{typing_traces: traces},
812-
message: IO.iodata_to_binary([message, format_traces(traces)])
818+
message: IO.iodata_to_binary([message, format_traces(traces), format_hints(hints)])
813819
}
814820
end
815821

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,8 @@ defmodule Module.Types.IntegrationTest do
479479
480480
dynamic(%Range{})
481481
482+
hint: defimpl for Range requires its callbacks to match exclusively on %Range{}
483+
482484
typing violation found at:
483485
484486
6 │ def itself(nil), do: nil

0 commit comments

Comments
 (0)