Skip to content

Commit

Permalink
Warn if a protocol has no definition, closes elixir-lang#11588
Browse files Browse the repository at this point in the history
  • Loading branch information
josevalim committed Apr 26, 2022
1 parent 808e955 commit 27c3a4a
Show file tree
Hide file tree
Showing 3 changed files with 86 additions and 81 deletions.
13 changes: 10 additions & 3 deletions lib/elixir/lib/protocol.ex
Original file line number Diff line number Diff line change
Expand Up @@ -763,13 +763,20 @@ defmodule Protocol do
IO.warn(message, stacktrace)
end

# TODO: Convert the following warnings into errors in future Elixir versions
def __before_compile__(env) do
# Callbacks
callback_metas = callback_metas(env.module, :callback)
callbacks = :maps.keys(callback_metas)
functions = Module.get_attribute(env.module, :__functions__)

if functions == [] do
warn(
"protocols must define at least one function, but none was defined",
env,
nil
)
end

# TODO: Convert the following warnings into errors in future Elixir versions
:lists.map(
fn {name, arity} = fa ->
warn(
Expand Down Expand Up @@ -799,7 +806,7 @@ defmodule Protocol do
# Optional Callbacks
optional_callbacks = Module.get_attribute(env.module, :optional_callbacks)

if length(optional_callbacks) > 0 do
if optional_callbacks != [] do
warn(
"cannot define @optional_callbacks inside protocol, all of the protocol definitions are required",
env,
Expand Down
60 changes: 0 additions & 60 deletions lib/elixir/test/elixir/kernel/warning_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -1224,66 +1224,6 @@ defmodule Kernel.WarningTest do
purge([Sample1, Sample1.Atom])
end

test "warn when callbacks and friends are defined inside a protocol" do
message =
capture_err(fn ->
Code.eval_string(~S"""
defprotocol SampleWithCallbacks do
@spec with_specs(any(), keyword()) :: tuple()
def with_specs(term, options \\ [])
@spec with_specs_and_when(any(), opts) :: tuple() when opts: keyword
def with_specs_and_when(term, options \\ [])
def without_specs(term, options \\ [])
@callback foo :: {:ok, term}
@callback foo(term) :: {:ok, term}
@callback foo(term, keyword) :: {:ok, term, keyword}
@callback foo_when :: {:ok, x} when x: term
@callback foo_when(x) :: {:ok, x} when x: term
@callback foo_when(x, opts) :: {:ok, x, opts} when x: term, opts: keyword
@macrocallback bar(term) :: {:ok, term}
@macrocallback bar(term, keyword) :: {:ok, term, keyword}
@optional_callbacks [foo: 1, foo: 2]
@optional_callbacks [without_specs: 2]
end
""")
end)

assert message =~
"cannot define @callback foo/0 inside protocol, use def/1 to outline your protocol definition\n nofile:1"

assert message =~
"cannot define @callback foo/1 inside protocol, use def/1 to outline your protocol definition\n nofile:1"

assert message =~
"cannot define @callback foo/2 inside protocol, use def/1 to outline your protocol definition\n nofile:1"

assert message =~
"cannot define @callback foo_when/0 inside protocol, use def/1 to outline your protocol definition\n nofile:1"

assert message =~
"cannot define @callback foo_when/1 inside protocol, use def/1 to outline your protocol definition\n nofile:1"

assert message =~
"cannot define @callback foo_when/2 inside protocol, use def/1 to outline your protocol definition\n nofile:1"

assert message =~
"cannot define @macrocallback bar/1 inside protocol, use def/1 to outline your protocol definition\n nofile:1"

assert message =~
"cannot define @macrocallback bar/2 inside protocol, use def/1 to outline your protocol definition\n nofile:1"

assert message =~
"cannot define @optional_callbacks inside protocol, all of the protocol definitions are required\n nofile:1"
after
purge([SampleWithCallbacks])
end

test "overridden def name" do
assert capture_err(fn ->
Code.eval_string("""
Expand Down
94 changes: 76 additions & 18 deletions lib/elixir/test/elixir/protocol_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -267,27 +267,84 @@ defmodule ProtocolTest do
String.to_charlist(__ENV__.file)
end

test "cannot derive without any implementation" do
assert_raise ArgumentError,
~r"could not load module #{inspect(Sample.Any)} due to reason :nofile, cannot derive #{inspect(Sample)}",
fn ->
defmodule NotCompiled do
@derive [Sample]
defstruct hello: :world
end
end
describe "warnings" do
import ExUnit.CaptureIO

test "with no definitions" do
assert capture_io(:stderr, fn ->
defprotocol SampleWithNoDefinitions do
end
end) =~ "protocols must define at least one function, but none was defined"
end

test "when @callbacks and friends are defined inside a protocol" do
message =
capture_io(:stderr, fn ->
defprotocol SampleWithCallbacks do
@spec with_specs(any(), keyword()) :: tuple()
def with_specs(term, options \\ [])

@spec with_specs_and_when(any(), opts) :: tuple() when opts: keyword
def with_specs_and_when(term, options \\ [])

def without_specs(term, options \\ [])

@callback foo :: {:ok, term}
@callback foo(term) :: {:ok, term}
@callback foo(term, keyword) :: {:ok, term, keyword}

@callback foo_when :: {:ok, x} when x: term
@callback foo_when(x) :: {:ok, x} when x: term
@callback foo_when(x, opts) :: {:ok, x, opts} when x: term, opts: keyword

@macrocallback bar(term) :: {:ok, term}
@macrocallback bar(term, keyword) :: {:ok, term, keyword}

@optional_callbacks [foo: 1, foo: 2]
@optional_callbacks [without_specs: 2]
end
end)

assert message =~
"cannot define @callback foo/0 inside protocol, use def/1 to outline your protocol definition"

assert message =~
"cannot define @callback foo/1 inside protocol, use def/1 to outline your protocol definition"

assert message =~
"cannot define @callback foo/2 inside protocol, use def/1 to outline your protocol definition"

assert message =~
"cannot define @callback foo_when/0 inside protocol, use def/1 to outline your protocol definition"

assert message =~
"cannot define @callback foo_when/1 inside protocol, use def/1 to outline your protocol definition"

assert message =~
"cannot define @callback foo_when/2 inside protocol, use def/1 to outline your protocol definition"

assert message =~
"cannot define @macrocallback bar/1 inside protocol, use def/1 to outline your protocol definition"

assert message =~
"cannot define @macrocallback bar/2 inside protocol, use def/1 to outline your protocol definition"

assert message =~
"cannot define @optional_callbacks inside protocol, all of the protocol definitions are required"
end
end

test "malformed @callback raises with CompileError" do
assert_raise CompileError,
"nofile:2: type specification missing return type: foo(term)",
fn ->
Code.eval_string("""
defprotocol WithMalformedCallback do
@callback foo(term)
describe "errors" do
test "cannot derive without any implementation" do
assert_raise ArgumentError,
~r"could not load module #{inspect(Sample.Any)} due to reason :nofile, cannot derive #{inspect(Sample)}",
fn ->
defmodule NotCompiled do
@derive [Sample]
defstruct hello: :world
end
end
""")
end
end
end
end

Expand All @@ -299,6 +356,7 @@ defmodule Protocol.DebugInfoTest do

{:module, _, binary, _} =
defprotocol DebugInfoProto do
def example(info)
end

assert {:ok, {DebugInfoProto, [debug_info: debug_info]}} =
Expand Down

0 comments on commit 27c3a4a

Please sign in to comment.