Skip to content

Commit 112ffc6

Browse files
author
José Valim
committed
Ensure no dialyzer warnings after protocol consolidation, closes #7107
1 parent 9941745 commit 112ffc6

File tree

3 files changed

+59
-49
lines changed

3 files changed

+59
-49
lines changed

lib/elixir/lib/protocol.ex

Lines changed: 36 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -366,24 +366,20 @@ defmodule Protocol do
366366
abstract_types = :erl_parse.abstract(:lists.usort(types))
367367

368368
clauses =
369-
:lists.map(
370-
fn
371-
{:clause, l, [{:atom, _, :consolidated?}], [], [{:atom, _, _}]} ->
372-
{:clause, l, [{:atom, 0, :consolidated?}], [], [{:atom, 0, true}]}
369+
Enum.map(clauses, fn
370+
{:clause, l, [{:atom, _, :consolidated?}], [], [{:atom, _, _}]} ->
371+
{:clause, l, [{:atom, 0, :consolidated?}], [], [{:atom, 0, true}]}
373372

374-
{:clause, l, [{:atom, _, :impls}], [], [{:atom, _, _}]} ->
375-
tuple = {:tuple, 0, [{:atom, 0, :consolidated}, abstract_types]}
376-
{:clause, l, [{:atom, 0, :impls}], [], [tuple]}
373+
{:clause, l, [{:atom, _, :impls}], [], [{:atom, _, _}]} ->
374+
tuple = {:tuple, 0, [{:atom, 0, :consolidated}, abstract_types]}
375+
{:clause, l, [{:atom, 0, :impls}], [], [tuple]}
377376

378-
{:clause, _, _, _, _} = c ->
379-
c
380-
end,
381-
clauses
382-
)
377+
{:clause, _, _, _, _} = c ->
378+
c
379+
end)
383380

384-
change_impl_for(tail, protocol, types, structs, true, [
385-
{:function, line, :__protocol__, 1, clauses} | acc
386-
])
381+
acc = [{:function, line, :__protocol__, 1, clauses} | acc]
382+
change_impl_for(tail, protocol, types, structs, true, acc)
387383
end
388384

389385
defp change_impl_for(
@@ -404,9 +400,8 @@ defmodule Protocol do
404400
clauses =
405401
[struct_clause_for(line) | clauses] ++ [fallback_clause_for(fallback, protocol, line)]
406402

407-
change_impl_for(tail, protocol, types, structs, protocol?, [
408-
{:function, line, :impl_for, 1, clauses} | acc
409-
])
403+
acc = [{:function, line, :impl_for, 1, clauses} | acc]
404+
change_impl_for(tail, protocol, types, structs, protocol?, acc)
410405
end
411406

412407
defp change_impl_for(
@@ -549,6 +544,13 @@ defmodule Protocol do
549544

550545
defp after_defprotocol do
551546
quote bind_quoted: [builtin: __builtin__()] do
547+
any_impl_for =
548+
if @fallback_to_any do
549+
quote do: unquote(__MODULE__.Any).__impl__(:target)
550+
else
551+
nil
552+
end
553+
552554
@doc false
553555
@spec impl_for(term) :: atom | nil
554556
Kernel.def(impl_for(data))
@@ -567,9 +569,10 @@ defmodule Protocol do
567569
target = Module.concat(__MODULE__, mod)
568570

569571
Kernel.def impl_for(data) when :erlang.unquote(guard)(data) do
570-
case impl_for?(unquote(target)) do
572+
case Code.ensure_compiled?(unquote(target)) and
573+
function_exported?(unquote(target), :__impl__, 1) do
571574
true -> unquote(target).__impl__(:target)
572-
false -> any_impl_for()
575+
false -> unquote(any_impl_for)
573576
end
574577
end
575578
end,
@@ -583,39 +586,33 @@ defmodule Protocol do
583586
# since it relies on Dialyzer not being smart enough to conclude that all
584587
# opaque types will get the any_impl_for/0 implementation.
585588
Kernel.def impl_for(_) do
586-
any_impl_for()
589+
unquote(any_impl_for)
587590
end
588591

589592
@doc false
590-
@spec impl_for!(term) :: atom | no_return
591-
Kernel.def impl_for!(data) do
592-
impl_for(data) || raise(Protocol.UndefinedError, protocol: __MODULE__, value: data)
593-
end
594-
595-
# Internal handler for Any
596-
if @fallback_to_any do
597-
Kernel.defp(any_impl_for(), do: __MODULE__.Any.__impl__(:target))
593+
@spec impl_for!(term) :: atom
594+
if any_impl_for do
595+
Kernel.def impl_for!(data) do
596+
impl_for(data)
597+
end
598598
else
599-
Kernel.defp(any_impl_for(), do: nil)
599+
Kernel.def impl_for!(data) do
600+
impl_for(data) || raise(Protocol.UndefinedError, protocol: __MODULE__, value: data)
601+
end
600602
end
601603

602604
# Internal handler for Structs
603605
Kernel.defp struct_impl_for(struct) do
604606
target = Module.concat(__MODULE__, struct)
605607

606-
case impl_for?(target) do
608+
case Code.ensure_compiled?(target) and function_exported?(target, :__impl__, 1) do
607609
true -> target.__impl__(:target)
608-
false -> any_impl_for()
610+
false -> unquote(any_impl_for)
609611
end
610612
end
611613

612-
# Check if compilation is available internally
613-
Kernel.defp impl_for?(target) do
614-
Code.ensure_compiled?(target) and function_exported?(target, :__impl__, 1)
615-
end
616-
617-
# Inline any and struct implementations
618-
@compile {:inline, any_impl_for: 0, struct_impl_for: 1, impl_for?: 1}
614+
# Inline struct implementation for performance
615+
@compile {:inline, struct_impl_for: 1}
619616

620617
unless Kernel.Typespec.defines_type?(__MODULE__, :t, 0) do
621618
@type t :: term

lib/elixir/test/elixir/fixtures/dialyzer/protocol_opaque.ex

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ defmodule Dialyzer.ProtocolOpaque do
66
end
77

88
defprotocol Dialyzer.ProtocolOpaque.Entity do
9+
@fallback_to_any true
910
def speak(entity)
1011
end
1112

@@ -20,3 +21,9 @@ defmodule Dialyzer.ProtocolOpaque.Duck do
2021
def speak(%Dialyzer.ProtocolOpaque.Duck{}), do: "Quack!"
2122
end
2223
end
24+
25+
defimpl Dialyzer.ProtocolOpaque.Entity, for: Any do
26+
def speak(_any) do
27+
"I can be anything"
28+
end
29+
end

lib/elixir/test/elixir/kernel/dialyzer_test.exs

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ defmodule Kernel.DialyzerTest do
3636
# Set up a per-test temporary directory, so we can run these with async: true.
3737
# We use the test's line number as the directory name, so they won't conflict.
3838
dir =
39-
context[:base_dir]
40-
|> Path.join("line#{context[:line]}")
39+
context.base_dir
40+
|> Path.join("line#{context.line}")
4141
|> String.to_charlist()
4242

4343
File.mkdir_p!(dir)
@@ -47,10 +47,8 @@ defmodule Kernel.DialyzerTest do
4747
|> Path.join("plt")
4848
|> String.to_charlist()
4949

50-
File.cp!(context[:base_plt], plt)
51-
50+
File.cp!(context.base_plt, plt)
5251
dialyzer = [analysis_type: :succ_typings, check_plt: false, files_rec: [dir], plts: [plt]]
53-
5452
{:ok, [outdir: dir, dialyzer: dialyzer]}
5553
end
5654

@@ -88,9 +86,17 @@ defmodule Kernel.DialyzerTest do
8886
end
8987

9088
test "no warnings on protocol calls with opaque types", context do
91-
copy_beam!(context, Dialyzer.ProtocolOpaque)
92-
copy_beam!(context, Dialyzer.ProtocolOpaque.Entity)
93-
copy_beam!(context, Dialyzer.ProtocolOpaque.Duck)
89+
alias Dialyzer.ProtocolOpaque
90+
91+
copy_beam!(context, ProtocolOpaque)
92+
copy_beam!(context, ProtocolOpaque.Entity)
93+
copy_beam!(context, ProtocolOpaque.Duck)
94+
assert_dialyze_no_warnings!(context)
95+
96+
# Also ensure no warnings after consolidation.
97+
Code.prepend_path(context.base_dir)
98+
{:ok, binary} = Protocol.consolidate(ProtocolOpaque.Entity, [ProtocolOpaque.Duck, Any])
99+
File.write!(Path.join(context.outdir, "#{ProtocolOpaque.Entity}.beam"), binary)
94100
assert_dialyze_no_warnings!(context)
95101
end
96102

@@ -118,11 +124,11 @@ defmodule Kernel.DialyzerTest do
118124

119125
defp copy_beam!(context, module) do
120126
name = "#{module}.beam"
121-
File.cp!(Path.join(context[:base_dir], name), Path.join(context[:outdir], name))
127+
File.cp!(Path.join(context.base_dir, name), Path.join(context.outdir, name))
122128
end
123129

124130
defp assert_dialyze_no_warnings!(context) do
125-
case dialyzer_run(context[:dialyzer]) do
131+
case dialyzer_run(context.dialyzer) do
126132
[] ->
127133
:ok
128134

0 commit comments

Comments
 (0)