Skip to content

Commit 30764fe

Browse files
authored
Add type checking of protocol implementations (#14115)
1 parent ecb4613 commit 30764fe

File tree

9 files changed

+299
-134
lines changed

9 files changed

+299
-134
lines changed

lib/elixir/lib/exception.ex

Lines changed: 0 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -2547,61 +2547,3 @@ defmodule ErlangError do
25472547
defp nth(3), do: "3rd"
25482548
defp nth(n), do: "#{n}th"
25492549
end
2550-
2551-
defmodule Inspect.Error do
2552-
@moduledoc """
2553-
Raised when a struct cannot be inspected.
2554-
"""
2555-
@enforce_keys [:exception_module, :exception_message, :stacktrace, :inspected_struct]
2556-
defexception @enforce_keys
2557-
2558-
@impl true
2559-
def exception(arguments) when is_list(arguments) do
2560-
exception = Keyword.fetch!(arguments, :exception)
2561-
exception_module = exception.__struct__
2562-
exception_message = Exception.message(exception) |> String.trim_trailing("\n")
2563-
stacktrace = Keyword.fetch!(arguments, :stacktrace)
2564-
inspected_struct = Keyword.fetch!(arguments, :inspected_struct)
2565-
2566-
%Inspect.Error{
2567-
exception_module: exception_module,
2568-
exception_message: exception_message,
2569-
stacktrace: stacktrace,
2570-
inspected_struct: inspected_struct
2571-
}
2572-
end
2573-
2574-
@impl true
2575-
def message(%__MODULE__{
2576-
exception_module: exception_module,
2577-
exception_message: exception_message,
2578-
inspected_struct: inspected_struct
2579-
}) do
2580-
~s'''
2581-
got #{inspect(exception_module)} with message:
2582-
2583-
"""
2584-
#{pad(exception_message, 4)}
2585-
"""
2586-
2587-
while inspecting:
2588-
2589-
#{pad(inspected_struct, 4)}
2590-
'''
2591-
end
2592-
2593-
@doc false
2594-
def pad(message, padding_length)
2595-
when is_binary(message) and is_integer(padding_length) and padding_length >= 0 do
2596-
padding = String.duplicate(" ", padding_length)
2597-
2598-
message
2599-
|> String.split("\n")
2600-
|> Enum.map(fn
2601-
"" -> "\n"
2602-
line -> [padding, line, ?\n]
2603-
end)
2604-
|> IO.iodata_to_binary()
2605-
|> String.trim_trailing("\n")
2606-
end
2607-
end

lib/elixir/lib/inspect.ex

Lines changed: 0 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -577,36 +577,6 @@ defimpl Inspect, for: Function do
577577
end
578578
end
579579

580-
defimpl Inspect, for: Inspect.Error do
581-
@impl true
582-
def inspect(%{stacktrace: stacktrace} = inspect_error, _opts) do
583-
message = Exception.message(inspect_error)
584-
format_output(message, stacktrace)
585-
end
586-
587-
defp format_output(message, [_ | _] = stacktrace) do
588-
stacktrace = Exception.format_stacktrace(stacktrace)
589-
590-
"""
591-
#Inspect.Error<
592-
#{Inspect.Error.pad(message, 2)}
593-
594-
Stacktrace:
595-
596-
#{stacktrace}
597-
>\
598-
"""
599-
end
600-
601-
defp format_output(message, []) do
602-
"""
603-
#Inspect.Error<
604-
#{Inspect.Error.pad(message, 2)}
605-
>\
606-
"""
607-
end
608-
end
609-
610580
defimpl Inspect, for: PID do
611581
def inspect(pid, _opts) do
612582
"#PID" <> IO.iodata_to_binary(:erlang.pid_to_list(pid))

lib/elixir/lib/inspect/error.ex

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
defmodule Inspect.Error do
2+
@moduledoc """
3+
Raised when a struct cannot be inspected.
4+
"""
5+
@enforce_keys [:exception_module, :exception_message, :stacktrace, :inspected_struct]
6+
defexception @enforce_keys
7+
8+
@impl true
9+
def exception(arguments) when is_list(arguments) do
10+
exception = Keyword.fetch!(arguments, :exception)
11+
exception_module = exception.__struct__
12+
exception_message = Exception.message(exception) |> String.trim_trailing("\n")
13+
stacktrace = Keyword.fetch!(arguments, :stacktrace)
14+
inspected_struct = Keyword.fetch!(arguments, :inspected_struct)
15+
16+
%Inspect.Error{
17+
exception_module: exception_module,
18+
exception_message: exception_message,
19+
stacktrace: stacktrace,
20+
inspected_struct: inspected_struct
21+
}
22+
end
23+
24+
@impl true
25+
def message(%__MODULE__{
26+
exception_module: exception_module,
27+
exception_message: exception_message,
28+
inspected_struct: inspected_struct
29+
}) do
30+
~s'''
31+
got #{inspect(exception_module)} with message:
32+
33+
"""
34+
#{pad(exception_message, 4)}
35+
"""
36+
37+
while inspecting:
38+
39+
#{pad(inspected_struct, 4)}
40+
'''
41+
end
42+
43+
@doc false
44+
def pad(message, padding_length)
45+
when is_binary(message) and is_integer(padding_length) and padding_length >= 0 do
46+
padding = String.duplicate(" ", padding_length)
47+
48+
message
49+
|> String.split("\n")
50+
|> Enum.map(fn
51+
"" -> "\n"
52+
line -> [padding, line, ?\n]
53+
end)
54+
|> IO.iodata_to_binary()
55+
|> String.trim_trailing("\n")
56+
end
57+
end
58+
59+
defimpl Inspect, for: Inspect.Error do
60+
@impl true
61+
def inspect(%{stacktrace: stacktrace} = inspect_error, _opts) do
62+
message = Exception.message(inspect_error)
63+
format_output(message, stacktrace)
64+
end
65+
66+
defp format_output(message, [_ | _] = stacktrace) do
67+
stacktrace = Exception.format_stacktrace(stacktrace)
68+
69+
"""
70+
#Inspect.Error<
71+
#{Inspect.Error.pad(message, 2)}
72+
73+
Stacktrace:
74+
75+
#{stacktrace}
76+
>\
77+
"""
78+
end
79+
80+
defp format_output(message, []) do
81+
"""
82+
#Inspect.Error<
83+
#{Inspect.Error.pad(message, 2)}
84+
>\
85+
"""
86+
end
87+
end

lib/elixir/lib/module/parallel_checker.ex

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,17 @@ defmodule Module.ParallelChecker do
4747
@doc """
4848
Spawns a process that runs the parallel checker.
4949
"""
50-
def spawn({pid, {checker, table}}, module, module_map, log?) do
51-
inner_spawn(pid, checker, table, module, cache_from_module_map(table, module_map), log?)
50+
def spawn({pid, {checker, table}}, module, module_map, beam_location, log?) do
51+
# Protocols may have been consolidated. So if we know their beam location,
52+
# we discard their module map on purpose and start from file.
53+
info =
54+
if beam_location != [] and List.keymember?(module_map.attributes, :__protocol__, 0) do
55+
List.to_string(beam_location)
56+
else
57+
cache_from_module_map(table, module_map)
58+
end
59+
60+
inner_spawn(pid, checker, table, module, info, log?)
5261
end
5362

5463
defp inner_spawn(pid, checker, table, module, info, log?) do
@@ -221,7 +230,7 @@ defmodule Module.ParallelChecker do
221230
## Module checking
222231

223232
defp check_module(module_tuple, cache, log?) do
224-
{module, file, line, definitions, no_warn_undefined, behaviours, impls, after_verify} =
233+
{module, file, line, definitions, no_warn_undefined, behaviours, impls, attrs, after_verify} =
225234
module_tuple
226235

227236
behaviour_warnings =
@@ -236,7 +245,7 @@ defmodule Module.ParallelChecker do
236245

237246
diagnostics =
238247
module
239-
|> Module.Types.warnings(file, definitions, no_warn_undefined, cache)
248+
|> Module.Types.warnings(file, attrs, definitions, no_warn_undefined, cache)
240249
|> Kernel.++(behaviour_warnings)
241250
|> group_warnings()
242251
|> emit_warnings(file, log?)
@@ -273,7 +282,10 @@ defmodule Module.ParallelChecker do
273282
|> extract_no_warn_undefined()
274283
|> merge_compiler_no_warn_undefined()
275284

276-
{module, file, line, definitions, no_warn_undefined, behaviours, impls, after_verify}
285+
attributes = Keyword.take(attributes, [:__protocol__, :__impl__])
286+
287+
{module, file, line, definitions, no_warn_undefined, behaviours, impls, attributes,
288+
after_verify}
277289
end
278290

279291
defp extract_no_warn_undefined(compile_opts) do

lib/elixir/lib/module/types.ex

Lines changed: 56 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -30,17 +30,26 @@ defmodule Module.Types do
3030
@modes [:static, :dynamic, :infer, :traversal]
3131

3232
# These functions are not inferred because they are added/managed by the compiler
33-
@no_infer [__protocol__: 1, behaviour_info: 1]
33+
@no_infer [behaviour_info: 1]
3434

3535
@doc false
36-
def infer(module, file, defs, private, used_private, env, {_, cache}) do
37-
infer_signatures? = :elixir_config.get(:infer_signatures) and cache != nil
36+
def infer(module, file, attrs, defs, private, used_private, env, {_, cache}) do
37+
# We don't care about inferring signatures for protocols,
38+
# those will be replaced anyway. There is also nothing to
39+
# infer if there is no cache system, we only do traversals.
40+
infer_signatures? =
41+
:elixir_config.get(:infer_signatures) and cache != nil and not protocol?(attrs)
42+
43+
impl = impl_for(attrs)
3844

3945
finder =
4046
fn fun_arity ->
4147
case :lists.keyfind(fun_arity, 1, defs) do
42-
{_, kind, _, _} = clause -> {infer_mode(kind, infer_signatures?), clause}
43-
false -> false
48+
{_, kind, _, _} = clause ->
49+
{infer_mode(kind, infer_signatures?), clause, default_domain(fun_arity, impl)}
50+
51+
false ->
52+
false
4453
end
4554
end
4655

@@ -67,7 +76,11 @@ defmodule Module.Types do
6776
kind in [:def, :defmacro],
6877
reduce: {[], context()} do
6978
{types, context} ->
70-
finder = fn _ -> {infer_mode(kind, infer_signatures?), def} end
79+
# Optimized version of finder, since we already the definition
80+
finder = fn _ ->
81+
{infer_mode(kind, infer_signatures?), def, default_domain(fun_arity, impl)}
82+
end
83+
7184
{_kind, inferred, context} = local_handler(meta, fun_arity, stack, context, finder)
7285

7386
if infer_signatures? and kind == :def and fun_arity not in @no_infer do
@@ -111,6 +124,33 @@ defmodule Module.Types do
111124
if infer_signatures? and kind in [:def, :defp], do: :infer, else: :traversal
112125
end
113126

127+
defp protocol?(attrs) do
128+
List.keymember?(attrs, :__protocol__, 0)
129+
end
130+
131+
defp impl_for(attrs) do
132+
case List.keyfind(attrs, :__impl__, 0) do
133+
{:__impl__, [protocol: protocol, for: for]} ->
134+
if Code.ensure_loaded?(protocol) and function_exported?(protocol, :behaviour_info, 1) do
135+
{for, protocol.behaviour_info(:callbacks)}
136+
else
137+
nil
138+
end
139+
140+
_ ->
141+
nil
142+
end
143+
end
144+
145+
defp default_domain({_, arity} = fun_arity, impl) do
146+
with {for, callbacks} <- impl,
147+
true <- fun_arity in callbacks do
148+
[Module.Types.Of.impl(for) | List.duplicate(Descr.dynamic(), arity - 1)]
149+
else
150+
_ -> List.duplicate(Descr.dynamic(), arity)
151+
end
152+
end
153+
114154
defp undefined_function!(reason, meta, {fun, arity}, stack, env) do
115155
env = %{env | function: stack.function, file: stack.file}
116156
tuple = {reason, {fun, arity}, stack.module}
@@ -159,10 +199,12 @@ defmodule Module.Types do
159199
end
160200

161201
@doc false
162-
def warnings(module, file, defs, no_warn_undefined, cache) do
202+
def warnings(module, file, attrs, defs, no_warn_undefined, cache) do
203+
impl = impl_for(attrs)
204+
163205
finder = fn fun_arity ->
164206
case :lists.keyfind(fun_arity, 1, defs) do
165-
{_, _, _, _} = clause -> {:dynamic, clause}
207+
{_, _, _, _} = clause -> {:dynamic, clause, default_domain(fun_arity, impl)}
166208
false -> false
167209
end
168210
end
@@ -172,7 +214,8 @@ defmodule Module.Types do
172214

173215
context =
174216
Enum.reduce(defs, context(), fn {fun_arity, _kind, meta, _clauses} = def, context ->
175-
finder = fn _ -> {:dynamic, def} end
217+
# Optimized version of finder, since we already the definition
218+
finder = fn _ -> {:dynamic, def, default_domain(fun_arity, impl)} end
176219
{_kind, _inferred, context} = local_handler(meta, fun_arity, stack, context, finder)
177220
context
178221
end)
@@ -211,11 +254,11 @@ defmodule Module.Types do
211254

212255
local_sigs ->
213256
case finder.(fun_arity) do
214-
{mode, {fun_arity, kind, meta, clauses}} ->
257+
{mode, {fun_arity, kind, meta, clauses}, expected} ->
215258
context = put_in(context.local_sigs, Map.put(local_sigs, fun_arity, kind))
216259

217260
{inferred, mapping, context} =
218-
local_handler(fun_arity, kind, meta, clauses, mode, stack, context)
261+
local_handler(fun_arity, kind, meta, clauses, expected, mode, stack, context)
219262

220263
context =
221264
update_in(context.local_sigs, &Map.put(&1, fun_arity, {kind, inferred, mapping}))
@@ -228,8 +271,8 @@ defmodule Module.Types do
228271
end
229272
end
230273

231-
defp local_handler({fun, arity} = fun_arity, kind, meta, clauses, mode, stack, context) do
232-
expected = List.duplicate(Descr.dynamic(), arity)
274+
defp local_handler(fun_arity, kind, meta, clauses, expected, mode, stack, context) do
275+
{fun, _arity} = fun_arity
233276
stack = stack |> fresh_stack(mode, fun_arity) |> with_file_meta(meta)
234277

235278
{_, _, mapping, clauses_types, clauses_context} =

0 commit comments

Comments
 (0)