Skip to content

Commit 26a4fc5

Browse files
committed
Add support for the @optional_callbacks behaviour attribute (#4705)
1 parent 56686c7 commit 26a4fc5

File tree

5 files changed

+44
-12
lines changed

5 files changed

+44
-12
lines changed

lib/elixir/lib/kernel.ex

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2381,13 +2381,14 @@ defmodule Kernel do
23812381
raise ArgumentError, "expected 0 or 1 argument for @#{name}, got: #{length(args)}"
23822382
end
23832383

2384-
defp typespec(:type), do: :deftype
2385-
defp typespec(:typep), do: :deftypep
2386-
defp typespec(:opaque), do: :defopaque
2387-
defp typespec(:spec), do: :defspec
2388-
defp typespec(:callback), do: :defcallback
2389-
defp typespec(:macrocallback), do: :defmacrocallback
2390-
defp typespec(_), do: false
2384+
defp typespec(:type), do: :deftype
2385+
defp typespec(:typep), do: :deftypep
2386+
defp typespec(:opaque), do: :defopaque
2387+
defp typespec(:spec), do: :defspec
2388+
defp typespec(:callback), do: :defcallback
2389+
defp typespec(:macrocallback), do: :defmacrocallback
2390+
defp typespec(:optional_callbacks), do: :defoptional_callbacks
2391+
defp typespec(_), do: false
23912392

23922393
@doc """
23932394
Returns the binding for the given context as a keyword list.

lib/elixir/lib/kernel/typespec.ex

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,12 @@ defmodule Kernel.Typespec do
9292
end
9393
end
9494

95+
defmacro defoptional_callbacks(callbacks) do
96+
quote do
97+
Module.store_typespec(__ENV__.module, :optional_callbacks, {__ENV__.line, unquote(callbacks)})
98+
end
99+
end
100+
95101
@doc """
96102
Defines a `type`, `typep` or `opaque` by receiving a typespec expression.
97103
"""

lib/elixir/lib/module.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1105,8 +1105,8 @@ defmodule Module do
11051105
defp preprocess_attribute(:on_definition, atom) when is_atom(atom),
11061106
do: {atom, :__on_definition__}
11071107

1108-
defp preprocess_attribute(key, _value) when key in [:type, :typep, :export_type, :opaque, :callback, :macrocallback] do
1109-
raise ArgumentError, "attributes type, typep, export_type, opaque, callback and macrocallback " <>
1108+
defp preprocess_attribute(key, _value) when key in [:type, :typep, :export_type, :opaque, :callback, :macrocallback, :optional_callbacks] do
1109+
raise ArgumentError, "attributes type, typep, export_type, opaque, callback, macrocallback, and optional_callbacks " <>
11101110
"must be set directly via the @ notation"
11111111
end
11121112

lib/elixir/src/elixir_module.erl

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,8 @@ build(Line, File, Module, Docs, Lexical) ->
159159

160160
Attributes = [behaviour, on_load, compile, external_resource, dialyzer],
161161
ets:insert(Data, {?acc_attr, [before_compile, after_compile, on_definition, derive,
162-
spec, type, typep, opaque, callback, macrocallback | Attributes]}),
162+
spec, type, typep, opaque, callback, macrocallback,
163+
optional_callbacks | Attributes]}),
163164
ets:insert(Data, {?persisted_attr, [vsn | Attributes]}),
164165
ets:insert(Data, {?lexical_attr, Lexical}),
165166

@@ -285,7 +286,8 @@ specs_form(Data, Defmacro, Defmacrop, Unreachable, Forms) ->
285286
Specs3 = lists:filter(fun({{_Kind, NameArity, _Spec}, _Line}) ->
286287
not lists:member(NameArity, Unreachable)
287288
end, Specs2),
288-
specs_attributes(Forms, Specs3);
289+
optional_callbacks_attributes(get_typespec(Data, optional_callbacks), Specs3) ++
290+
specs_attributes(Forms, Specs3);
289291
true ->
290292
Forms
291293
end.
@@ -322,6 +324,22 @@ spec_for_macro({type, Line, 'fun', [{type, _, product, Args} | T]}) ->
322324

323325
spec_for_macro(Else) -> Else.
324326

327+
optional_callbacks_attributes(OptionalCallbacksTypespec, Specs) ->
328+
% We only take the specs of the callbacks (which are both callbacks and
329+
% macrocallbacks).
330+
Callbacks = [NameArity || {{callback, NameArity, _Spec}, _Line} <- Specs],
331+
[{attribute, Line, optional_callbacks, macroify_callback_names(NamesArities, Callbacks)} ||
332+
{Line, NamesArities} <- OptionalCallbacksTypespec].
333+
334+
macroify_callback_names(NamesArities, Callbacks) ->
335+
lists:map(fun({Name, Arity} = NameArity) ->
336+
MacroNameArity = {elixir_utils:macro_name(Name), Arity + 1},
337+
case lists:member(MacroNameArity, Callbacks) of
338+
true -> MacroNameArity;
339+
false -> NameArity
340+
end
341+
end, NamesArities).
342+
325343
%% Loads the form into the code server.
326344

327345
compile_opts(Module) ->

lib/elixir/test/elixir/kernel/typespec_test.exs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -659,11 +659,18 @@ defmodule Kernel.TypespecTest do
659659
@callback orr(atom | integer) :: atom
660660
@callback literal(123, {atom}, :atom, [integer], true) :: atom
661661
@macrocallback last(integer) :: Macro.t
662+
@macrocallback lastlast() :: atom
663+
@optional_callbacks bar: 2, lastlast: 0
662664
end
663665

664666
test "callbacks" do
665667
assert Sample.behaviour_info(:callbacks) ==
666-
[first: 1, guarded: 1, "MACRO-last": 2, literal: 5, orr: 1, foo: 2, bar: 2]
668+
["MACRO-lastlast": 1, first: 1, guarded: 1, "MACRO-last": 2, literal: 5, orr: 1, foo: 2, bar: 2]
669+
end
670+
671+
test "optional callbacks" do
672+
assert Sample.behaviour_info(:optional_callbacks) ==
673+
[bar: 2, "MACRO-lastlast": 1]
667674
end
668675

669676
test "default is not supported" do

0 commit comments

Comments
 (0)