Skip to content

Commit 9442858

Browse files
committed
Avoid compile warnings when implementing an empty protocol
Implementing a protocol creates a module that implements the behaviour given by the protocol. A protocol that defines no functions by extension defines no callbacks, and so won't be recognized as a behaviour by the Erlang compiler. This means that Elixir will log a warning when implementing such a protocol, because behaviour_info/1 won't be defined on it, and so Elixir also won't recognize it as a behaviour. Erlang offers no mechanism to explicitly denote a module as defining a behaviour, but in this case we know that every protocol implementation must define an impl/1 function. By adding a callback that describes this function to all protocols, the warning can be avoided.
1 parent b401d15 commit 9442858

File tree

2 files changed

+31
-5
lines changed

2 files changed

+31
-5
lines changed

lib/elixir/lib/protocol.ex

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -764,7 +764,7 @@ defmodule Protocol do
764764
end
765765

766766
# TODO: Convert the following warnings into errors in future Elixir versions
767-
def __before_compile__(env) do
767+
defmacro __before_compile__(env) do
768768
# Callbacks
769769
callback_metas = callback_metas(env.module, :callback)
770770
callbacks = :maps.keys(callback_metas)
@@ -806,6 +806,16 @@ defmodule Protocol do
806806
nil
807807
)
808808
end
809+
810+
quote do
811+
# Register the __impl__/1 callback to ensure the protocol is a behaviour
812+
# Without this, a protocol that defines no functions won't be detected
813+
# as a behaviour by the Erlang compiler, and any implementations of that
814+
# protocol will log a warning.
815+
@callback __impl__(:foo) :: module()
816+
@callback __impl__(:target) :: module()
817+
@callback __impl__(:protocol) :: module()
818+
end
809819
end
810820

811821
defp after_defprotocol do

lib/elixir/test/elixir/protocol_test.exs

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ Code.require_file("test_helper.exs", __DIR__)
33
defmodule ProtocolTest do
44
use ExUnit.Case, async: true
55

6+
defp capture_err(fun) do
7+
ExUnit.CaptureIO.capture_io(:stderr, fun)
8+
end
9+
610
doctest Protocol
711

812
{_, _, sample_binary, _} =
@@ -143,11 +147,23 @@ defmodule ProtocolTest do
143147
end
144148

145149
test "protocol defines callbacks" do
146-
assert [{:type, 13, :fun, args}] = get_callbacks(@sample_binary, :ok, 1)
147-
assert args == [{:type, 13, :product, [{:user_type, 13, :t, []}]}, {:type, 13, :boolean, []}]
150+
assert [{:type, 17, :fun, args}] = get_callbacks(@sample_binary, :ok, 1)
151+
assert args == [{:type, 17, :product, [{:user_type, 17, :t, []}]}, {:type, 17, :boolean, []}]
152+
153+
assert [{:type, 27, :fun, args}] = get_callbacks(@with_any_binary, :ok, 1)
154+
assert args == [{:type, 27, :product, [{:user_type, 27, :t, []}]}, {:type, 27, :term, []}]
155+
end
156+
157+
test "protocol with no functions is a behaviour" do
158+
refute capture_err(fn ->
159+
Code.eval_string("""
160+
defprotocol ProtoNoFunctions do
161+
end
148162
149-
assert [{:type, 23, :fun, args}] = get_callbacks(@with_any_binary, :ok, 1)
150-
assert args == [{:type, 23, :product, [{:user_type, 23, :t, []}]}, {:type, 23, :term, []}]
163+
defimpl ProtoNoFunctions, for: Integer do
164+
end
165+
""")
166+
end) =~ "module ProtoNoFunctions is not a behaviour"
151167
end
152168

153169
test "protocol defines functions and attributes" do

0 commit comments

Comments
 (0)