Skip to content

Commit d86ba0e

Browse files
committed
Add typespec syntax for records
1 parent c295ee5 commit d86ba0e

File tree

3 files changed

+92
-2
lines changed

3 files changed

+92
-2
lines changed

lib/elixir/lib/kernel/typespec.ex

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,11 @@ defmodule Kernel.Typespec do
7777
| %{Keyword}
7878
| %{Pairs}
7979
80-
Tuple :: tuple # a tuple of any size
81-
| {} # empty tuple
80+
Tuple :: tuple # a tuple of any size
81+
| {} # empty tuple
8282
| {TList}
83+
| record(Atom) # record (see Record)
84+
| record(Atom, Keyword)
8385
8486
Keyword :: ElixirAtom: Type
8587
| ElixirAtom: Type, Keyword
@@ -851,6 +853,34 @@ defmodule Kernel.Typespec do
851853
typespec({:%{}, meta, fields}, vars, caller)
852854
end
853855

856+
# Handle records
857+
defp typespec({:record, meta, [atom]}, vars, caller) do
858+
typespec({:record, meta, [atom, []]}, vars, caller)
859+
end
860+
861+
defp typespec({:record, meta, [atom, fields]}, vars, caller) do
862+
case Macro.expand({atom, [], [{atom, [], []}]}, caller) do
863+
keyword when is_list(keyword) ->
864+
keyword =
865+
:lists.map(fn {field, _} ->
866+
{field, quote do: term()}
867+
end, keyword)
868+
869+
:lists.foreach(fn {field, _} ->
870+
unless Keyword.has_key?(keyword, field) do
871+
compile_error(caller, "undefined field #{field} on record #{inspect atom}")
872+
end
873+
end, fields)
874+
875+
fields = Keyword.merge(keyword, fields)
876+
types = Keyword.values(fields)
877+
878+
typespec({:{}, meta, [atom|types]}, vars, caller)
879+
_ ->
880+
compile_error(caller, "unknown record #{inspect atom}")
881+
end
882+
end
883+
854884
# Handle ranges
855885
defp typespec({:.., meta, args}, vars, caller) do
856886
typespec({:range, meta, args}, vars, caller)

lib/elixir/lib/record.ex

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,20 @@ defmodule Record do
2020
The macros `defrecord/3` and `defrecordp/3` can be used to create
2121
records while `extract/2` can be used to extract records from Erlang
2222
files.
23+
24+
## Types
25+
26+
Types can be defined for tuples with the `record/2` macro (only available
27+
in typespecs). Like with the generated record macros it will expand to
28+
a tuple.
29+
30+
defmodule MyModule do
31+
require Record
32+
Record.defrecord :user name: "José", age: 25
33+
34+
@type user :: record(:user, name: String.t, age: integer)
35+
# expands to: `@type user :: {:user, String.t, integer}`
36+
end
2337
"""
2438

2539
@doc """

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

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,52 @@ defmodule Kernel.TypespecTest do
214214
end
215215
end
216216

217+
test "@type with public record" do
218+
module = test_module do
219+
require Record
220+
Record.defrecord :timestamp, [date: 1, time: 2]
221+
@type mytype :: record(:timestamp, time: :foo)
222+
end
223+
224+
assert [type: {:mytype,
225+
{:type, _, :tuple, [
226+
{:atom, 0, :timestamp}, {:atom, 0, :foo}, {:type, 0, :term, []}
227+
]},
228+
[]}] = types(module)
229+
end
230+
231+
test "@type with private record" do
232+
module = test_module do
233+
require Record
234+
Record.defrecordp :timestamp, [date: 1, time: 2]
235+
@type mytype :: record(:timestamp, time: :foo)
236+
end
237+
238+
assert [type: {:mytype,
239+
{:type, _, :tuple, [
240+
{:atom, 0, :timestamp}, {:atom, 0, :foo}, {:type, 0, :term, []}
241+
]},
242+
[]}] = types(module)
243+
end
244+
245+
test "@type with undefined record" do
246+
assert_raise CompileError, ~r"unknown record :this_record_does_not_exist", fn ->
247+
test_module do
248+
@type mytype :: record(:this_record_does_not_exist, [])
249+
end
250+
end
251+
end
252+
253+
test "@type with a record with undefined field" do
254+
assert_raise CompileError, ~r"undefined field no_field on record :timestamp", fn ->
255+
test_module do
256+
require Record
257+
Record.defrecord :timestamp, [date: 1, time: 2]
258+
@type mytype :: record(:timestamp, no_field: :foo)
259+
end
260+
end
261+
end
262+
217263
test "@type with list shortcuts" do
218264
module = test_module do
219265
@type mytype :: []

0 commit comments

Comments
 (0)