Skip to content

Commit e38ae98

Browse files
fertapricjosevalim
authored andcommitted
Allow typespec attrs to be used as module attrs (#8327)
Port of: c190f09
1 parent 71a537b commit e38ae98

File tree

5 files changed

+194
-30
lines changed

5 files changed

+194
-30
lines changed

lib/elixir/lib/kernel/typespec.ex

Lines changed: 38 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -71,24 +71,24 @@ defmodule Kernel.Typespec do
7171
type_to_signature(expr) == signature
7272
end
7373

74-
:lists.any(finder, get_typespec(bag, :type))
74+
:lists.any(finder, get_typespecs(bag, [:type, :opaque, :typep]))
7575
end
7676

7777
def spec_to_callback(module, {name, arity} = signature)
7878
when is_atom(module) and is_atom(name) and arity in 0..255 do
7979
{_set, bag} = :elixir_module.data_tables(module)
8080

81-
filter = fn {expr, _} = object ->
81+
filter = fn {:spec, expr, pos} ->
8282
if spec_to_signature(expr) == signature do
83-
:ets.delete_object(bag, {:spec, object})
84-
store_typespec(bag, :callback, object)
83+
delete_typespec(bag, :spec, expr, pos)
84+
store_typespec(bag, :callback, expr, pos)
8585
true
8686
else
8787
false
8888
end
8989
end
9090

91-
:lists.filter(filter, get_typespec(bag, :spec)) != []
91+
:lists.filter(filter, get_typespecs(bag, :spec)) != []
9292
end
9393

9494
## Typespec definition and storage
@@ -100,7 +100,7 @@ defmodule Kernel.Typespec do
100100
"""
101101
def deftypespec(:spec, expr, _line, _file, module, pos) do
102102
{_set, bag} = :elixir_module.data_tables(module)
103-
store_typespec(bag, :spec, {expr, pos})
103+
store_typespec(bag, :spec, expr, pos)
104104
end
105105

106106
def deftypespec(kind, expr, line, _file, module, pos)
@@ -116,7 +116,7 @@ defmodule Kernel.Typespec do
116116
:error
117117
end
118118

119-
store_typespec(bag, kind, {expr, pos})
119+
store_typespec(bag, kind, expr, pos)
120120
end
121121

122122
def deftypespec(kind, expr, line, file, module, pos)
@@ -143,17 +143,34 @@ defmodule Kernel.Typespec do
143143
:error
144144
end
145145

146-
store_typespec(bag, :type, {kind, expr, pos})
146+
store_typespec(bag, kind, expr, pos)
147147
end
148148

149-
defp get_typespec(bag, key) do
150-
:ets.lookup_element(bag, key, 2)
149+
defp get_typespecs(bag, keys) when is_list(keys) do
150+
:lists.flatmap(&get_typespecs(bag, &1), keys)
151+
end
152+
153+
defp get_typespecs(bag, key) do
154+
:ets.lookup_element(bag, {:accumulate, key}, 2)
151155
catch
152156
:error, :badarg -> []
153157
end
154158

155-
defp store_typespec(bag, key, value) do
156-
:ets.insert(bag, {key, value})
159+
defp take_typespecs(bag, keys) when is_list(keys) do
160+
:lists.flatmap(&take_typespecs(bag, &1), keys)
161+
end
162+
163+
defp take_typespecs(bag, key) do
164+
:lists.map(&elem(&1, 1), :ets.take(bag, {:accumulate, key}))
165+
end
166+
167+
defp store_typespec(bag, key, expr, pos) do
168+
:ets.insert(bag, {{:accumulate, key}, {key, expr, pos}})
169+
:ok
170+
end
171+
172+
defp delete_typespec(bag, key, expr, pos) do
173+
:ets.delete_object(bag, {{:accumulate, key}, {key, expr, pos}})
157174
:ok
158175
end
159176

@@ -192,19 +209,15 @@ defmodule Kernel.Typespec do
192209

193210
@doc false
194211
def translate_typespecs_for_module(_set, bag) do
195-
types = Enum.map(take_typespec(bag, :type), &translate_type/1)
196-
specs = Enum.map(take_typespec(bag, :spec), &translate_spec/1)
197-
callbacks = Enum.map(take_typespec(bag, :callback), &translate_spec/1)
198-
macrocallbacks = Enum.map(take_typespec(bag, :macrocallback), &translate_spec/1)
199-
optional_callbacks = List.flatten(get_typespec(bag, {:accumulate, :optional_callbacks}))
212+
types = Enum.map(take_typespecs(bag, [:type, :opaque, :typep]), &translate_type/1)
213+
specs = Enum.map(take_typespecs(bag, :spec), &translate_spec/1)
214+
callbacks = Enum.map(take_typespecs(bag, :callback), &translate_spec/1)
215+
macrocallbacks = Enum.map(take_typespecs(bag, :macrocallback), &translate_spec/1)
216+
optional_callbacks = List.flatten(get_typespecs(bag, :optional_callbacks))
200217
{types, specs, callbacks, macrocallbacks, optional_callbacks}
201218
end
202219

203-
defp take_typespec(bag, key) do
204-
:ets.take(bag, key)
205-
end
206-
207-
defp translate_type({_, {kind, {:::, _, [{name, _, args}, definition]}, pos}})
220+
defp translate_type({kind, {:::, _, [{name, _, args}, definition]}, pos})
208221
when is_atom(name) and name != ::: do
209222
caller = :elixir_locals.get_cached_env(pos)
210223

@@ -247,7 +260,7 @@ defmodule Kernel.Typespec do
247260
{kind, {name, arity}, caller.line, type, export}
248261
end
249262

250-
defp translate_type({_, {_kind, other, pos}}) do
263+
defp translate_type({_kind, other, pos}) do
251264
caller = :elixir_locals.get_cached_env(pos)
252265
type_spec = Macro.to_string(other)
253266
compile_error(caller, "invalid type specification: #{type_spec}")
@@ -259,12 +272,12 @@ defmodule Kernel.Typespec do
259272

260273
defp valid_variable_ast?(_), do: false
261274

262-
defp translate_spec({kind, {{:when, _meta, [spec, guard]}, pos}}) do
275+
defp translate_spec({kind, {:when, _meta, [spec, guard]}, pos}) do
263276
caller = :elixir_locals.get_cached_env(pos)
264277
translate_spec(kind, spec, guard, caller)
265278
end
266279

267-
defp translate_spec({kind, {spec, pos}}) do
280+
defp translate_spec({kind, spec, pos}) do
268281
caller = :elixir_locals.get_cached_env(pos)
269282
translate_spec(kind, spec, [], caller)
270283
end

lib/elixir/lib/module.ex

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -466,7 +466,7 @@ defmodule Module do
466466
of the corresponding setting in `Code.compiler_options/1`
467467
468468
* `@compile {:inline, some_fun: 2, other_fun: 3}` - inlines the given
469-
name/arity pairs. Inlining is applied locally, calls from another
469+
name/arity pairs. Inlining is applied locally, calls from another
470470
module are not affected by this option
471471
472472
* `@compile {:autoload, false}` - disables automatic loading of
@@ -1864,9 +1864,9 @@ defmodule Module do
18641864
do: {atom, :__on_definition__}
18651865

18661866
defp preprocess_attribute(key, _value)
1867-
when key in [:type, :typep, :export_type, :opaque, :callback, :macrocallback] do
1867+
when key in [:type, :typep, :export_type, :opaque, :spec, :callback, :macrocallback] do
18681868
raise ArgumentError,
1869-
"attributes type, typep, export_type, opaque, callback, and macrocallback" <>
1869+
"attributes type, typep, export_type, opaque, spec, callback, and macrocallback " <>
18701870
"must be set directly via the @ notation"
18711871
end
18721872

lib/elixir/src/elixir_module.erl

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ build(Line, File, Module, Lexical) ->
211211

212212
%% In the bag table we store:
213213
%%
214-
%% * {{accumulate, Attribute}, ...}
214+
%% * {{accumulate, Attribute}, ...} (includes typespecs)
215215
%% * {attributes, ...}
216216
%% * {impls, ...}
217217
%% * {deprecated, ...}
@@ -221,7 +221,6 @@ build(Line, File, Module, Lexical) ->
221221
%% * {{clauses, Tuple}, ...} (from elixir_def)
222222
%% * {reattach, ...} (from elixir_local)
223223
%% * {{local, Tuple}, ...} (from elixir_local)
224-
%% * {spec, ...}, {type, ...}, {callback, ...}, {macrocallback, ...}
225224
%%
226225
DataBag = ets:new(Module, [duplicate_bag, public]),
227226

@@ -238,6 +237,12 @@ build(Line, File, Module, Lexical) ->
238237
{dialyzer, [], accumulate},
239238
{external_resource, [], accumulate},
240239
{on_definition, [], accumulate},
240+
{type, [], accumulate},
241+
{opaque, [], accumulate},
242+
{typep, [], accumulate},
243+
{spec, [], accumulate},
244+
{callback, [], accumulate},
245+
{macrocallback, [], accumulate},
241246
{on_load, [], accumulate},
242247
{optional_callbacks, [], accumulate},
243248

lib/elixir/test/elixir/kernel/errors_test.exs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -503,6 +503,22 @@ defmodule Kernel.ErrorsTest do
503503
end
504504
end
505505

506+
test "typespec attributes set via Module.put_attribute/4" do
507+
message =
508+
"attributes type, typep, export_type, opaque, spec, callback, and macrocallback " <>
509+
"must be set directly via the @ notation"
510+
511+
for kind <- [:type, :typep, :opaque, :spec, :callback, :macrocallback] do
512+
assert_eval_raise ArgumentError,
513+
message,
514+
"""
515+
defmodule PutTypespecAttribute do
516+
Module.put_attribute(__MODULE__, #{inspect(kind)}, {})
517+
end
518+
"""
519+
end
520+
end
521+
506522
test "invalid struct field value" do
507523
msg = ~r"invalid value for struct field baz, cannot escape "
508524

lib/elixir/test/elixir/typespec_test.exs

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -747,6 +747,136 @@ defmodule TypespecTest do
747747
assert [{:atom, _, :is_subtype}, [{:var, _, :y}, {:var, _, :x}]] = constraint_type
748748
end
749749

750+
test "@type, @opaque, and @typep as module attributes" do
751+
defmodule TypeModuleAttributes do
752+
@type type1 :: boolean
753+
@opaque opaque1 :: boolean
754+
@typep typep1 :: boolean
755+
756+
def type1, do: @type
757+
def opaque1, do: @opaque
758+
def typep1, do: @typep
759+
760+
@type type2 :: atom
761+
@type type3 :: pid
762+
@opaque opaque2 :: atom
763+
@opaque opaque3 :: pid
764+
@typep typep2 :: atom
765+
766+
def type2, do: @type
767+
def opaque2, do: @opaque
768+
def typep2, do: @typep
769+
770+
# Avoid unused warnings
771+
@spec foo(typep1) :: typep2
772+
def foo(_x), do: :ok
773+
end
774+
775+
assert [
776+
{:type, {:::, _, [{:type1, _, _}, {:boolean, _, _}]},
777+
{TypespecTest.TypeModuleAttributes, _}}
778+
] = TypeModuleAttributes.type1()
779+
780+
assert [
781+
{:type, {:::, _, [{:type3, _, _}, {:pid, _, _}]},
782+
{TypespecTest.TypeModuleAttributes, _}},
783+
{:type, {:::, _, [{:type2, _, _}, {:atom, _, _}]},
784+
{TypespecTest.TypeModuleAttributes, _}},
785+
{:type, {:::, _, [{:type1, _, _}, {:boolean, _, _}]},
786+
{TypespecTest.TypeModuleAttributes, _}}
787+
] = TypeModuleAttributes.type2()
788+
789+
assert [
790+
{:opaque, {:::, _, [{:opaque1, _, _}, {:boolean, _, _}]},
791+
{TypespecTest.TypeModuleAttributes, _}}
792+
] = TypeModuleAttributes.opaque1()
793+
794+
assert [
795+
{:opaque, {:::, _, [{:opaque3, _, _}, {:pid, _, _}]},
796+
{TypespecTest.TypeModuleAttributes, _}},
797+
{:opaque, {:::, _, [{:opaque2, _, _}, {:atom, _, _}]},
798+
{TypespecTest.TypeModuleAttributes, _}},
799+
{:opaque, {:::, _, [{:opaque1, _, _}, {:boolean, _, _}]},
800+
{TypespecTest.TypeModuleAttributes, _}}
801+
] = TypeModuleAttributes.opaque2()
802+
803+
assert [
804+
{:typep, {:::, _, [{:typep1, _, _}, {:boolean, _, _}]},
805+
{TypespecTest.TypeModuleAttributes, _}}
806+
] = TypeModuleAttributes.typep1()
807+
808+
assert [
809+
{:typep, {:::, _, [{:typep2, _, _}, {:atom, _, _}]},
810+
{TypespecTest.TypeModuleAttributes, _}},
811+
{:typep, {:::, _, [{:typep1, _, _}, {:boolean, _, _}]},
812+
{TypespecTest.TypeModuleAttributes, _}}
813+
] = TypeModuleAttributes.typep2()
814+
after
815+
:code.delete(TypeModuleAttributes)
816+
:code.purge(TypeModuleAttributes)
817+
end
818+
819+
test "@spec, @callback, and @macrocallback as module attributes" do
820+
defmodule SpecModuleAttributes do
821+
@callback callback1 :: integer
822+
@macrocallback macrocallback1 :: integer
823+
824+
@spec spec1 :: boolean
825+
def spec1, do: @spec
826+
827+
@callback callback2 :: boolean
828+
@macrocallback macrocallback2 :: boolean
829+
830+
@spec spec2 :: atom
831+
def spec2, do: @spec
832+
833+
@spec spec3 :: pid
834+
def spec3, do: :ok
835+
def spec4, do: @spec
836+
837+
def callback, do: @callback
838+
def macrocallback, do: @macrocallback
839+
end
840+
841+
assert [
842+
{:spec, {:::, _, [{:spec1, _, _}, {:boolean, _, _}]},
843+
{TypespecTest.SpecModuleAttributes, _}}
844+
] = SpecModuleAttributes.spec1()
845+
846+
assert [
847+
{:spec, {:::, _, [{:spec2, _, _}, {:atom, _, _}]},
848+
{TypespecTest.SpecModuleAttributes, _}},
849+
{:spec, {:::, _, [{:spec1, _, _}, {:boolean, _, _}]},
850+
{TypespecTest.SpecModuleAttributes, _}}
851+
] = SpecModuleAttributes.spec2()
852+
853+
assert [
854+
{:spec, {:::, _, [{:spec3, _, _}, {:pid, _, _}]},
855+
{TypespecTest.SpecModuleAttributes, _}},
856+
{:spec, {:::, _, [{:spec2, _, _}, {:atom, _, _}]},
857+
{TypespecTest.SpecModuleAttributes, _}},
858+
{:spec, {:::, _, [{:spec1, _, _}, {:boolean, _, _}]},
859+
{TypespecTest.SpecModuleAttributes, _}}
860+
] = SpecModuleAttributes.spec4()
861+
862+
assert [
863+
{:callback, {:::, _, [{:callback2, _, _}, {:boolean, _, _}]},
864+
{TypespecTest.SpecModuleAttributes, _}},
865+
{:callback, {:::, _, [{:callback1, _, _}, {:integer, _, _}]},
866+
{TypespecTest.SpecModuleAttributes, _}}
867+
] = SpecModuleAttributes.callback()
868+
869+
assert [
870+
{:macrocallback, {:::, _, [{:macrocallback2, _, _}, {:boolean, _, _}]},
871+
{TypespecTest.SpecModuleAttributes, _}},
872+
{:macrocallback, {:::, _, [{:macrocallback1, _, _}, {:integer, _, _}]},
873+
{TypespecTest.SpecModuleAttributes, _}}
874+
] = SpecModuleAttributes.macrocallback()
875+
after
876+
:code.delete(SpecModuleAttributes)
877+
:code.purge(SpecModuleAttributes)
878+
end
879+
750880
test "@callback(callback)" do
751881
bytecode =
752882
test_module do

0 commit comments

Comments
 (0)