Skip to content

Commit 3ee9d5d

Browse files
author
José Valim
committed
Merge pull request #2521 from ericmj/record-typespecs
Add typespec syntax for records
2 parents 69b78da + d86ba0e commit 3ee9d5d

File tree

6 files changed

+113
-14
lines changed

6 files changed

+113
-14
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/macro.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -867,7 +867,7 @@ defmodule Macro do
867867
end
868868

869869
expand = :elixir_dispatch.expand_import(meta, {atom, length(args)}, args,
870-
env, extra)
870+
env, extra, true)
871871

872872
case expand do
873873
{:ok, receiver, quoted} ->

lib/elixir/lib/record.ex

Lines changed: 24 additions & 2 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 """
@@ -195,7 +209,15 @@ defmodule Record do
195209
Keyword.keyword?(args) ->
196210
create(atom, fields, args, caller)
197211
true ->
198-
quote do: Record.__keyword__(unquote(atom), unquote(fields), unquote(args))
212+
case Macro.expand(args, caller) do
213+
{:{}, _, [^atom|list]} when length(list) == length(fields) ->
214+
record = List.to_tuple([atom|list])
215+
Macro.escape(Record.__keyword__(atom, fields, record))
216+
{^atom, arg} when length(fields) == 1 ->
217+
Macro.escape(Record.__keyword__(atom, fields, {atom, arg}))
218+
_ ->
219+
quote do: Record.__keyword__(unquote(atom), unquote(fields), unquote(args))
220+
end
199221
end
200222
end
201223

@@ -291,7 +313,7 @@ defmodule Record do
291313
[_tag|values] = Tuple.to_list(record)
292314
join_keyword(fields, values, [])
293315
else
294-
msg = "expected argument to be a literal atom, literal keyword or a #{atom}() record, got runtime: #{inspect record}"
316+
msg = "expected argument to be a literal atom, literal keyword or a #{inspect atom} record, got runtime: #{inspect record}"
295317
raise ArgumentError, msg
296318
end
297319
end

lib/elixir/src/elixir_dispatch.erl

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
-module(elixir_dispatch).
55
-export([dispatch_import/5, dispatch_require/6,
66
require_function/5, import_function/4,
7-
expand_import/5, expand_require/5,
7+
expand_import/6, expand_require/5,
88
default_functions/0, default_macros/0, default_requires/0,
99
find_import/4, format_error/1]).
1010
-include("elixir.hrl").
@@ -88,7 +88,7 @@ remote_function(Meta, Receiver, Name, Arity, E) ->
8888

8989
dispatch_import(Meta, Name, Args, E, Callback) ->
9090
Arity = length(Args),
91-
case expand_import(Meta, {Name, Arity}, Args, E, []) of
91+
case expand_import(Meta, {Name, Arity}, Args, E, [], false) of
9292
{ok, Receiver, Quoted} ->
9393
expand_quoted(Meta, Receiver, Name, Arity, Quoted, E);
9494
{ok, Receiver, NewName, NewArgs} ->
@@ -115,12 +115,12 @@ dispatch_require(_Meta, Receiver, Name, Args, _E, Callback) ->
115115

116116
%% Macros expansion
117117

118-
expand_import(Meta, {Name, Arity} = Tuple, Args, E, Extra) ->
119-
Module = ?m(E, module),
120-
Dispatch = find_dispatch(Meta, Tuple, Extra, E),
121-
Function = ?m(E, function),
122-
Local = (Function /= nil) andalso (Function /= Tuple) andalso
123-
elixir_locals:macro_for(Module, Name, Arity),
118+
expand_import(Meta, {Name, Arity} = Tuple, Args, E, Extra, External) ->
119+
Module = ?m(E, module),
120+
Dispatch = find_dispatch(Meta, Tuple, Extra, E),
121+
Function = ?m(E, function),
122+
AllowLocals = External orelse ((Function /= nil) andalso (Function /= Tuple)),
123+
Local = AllowLocals andalso elixir_locals:macro_for(Module, Name, Arity),
124124

125125
case Dispatch of
126126
%% In case it is an import, we dispatch the import.

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 :: []

lib/elixir/test/elixir/record_test.exs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,9 @@ defmodule RecordTest do
8181

8282
record = user()
8383
assert user(record) == [name: "José", age: 25]
84+
assert user(user()) == [name: "José", age: 25]
8485

85-
msg = "expected argument to be a literal atom, literal keyword or a file_info() record, " <>
86+
msg = "expected argument to be a literal atom, literal keyword or a :file_info record, " <>
8687
"got runtime: {RecordTest, \"José\", 25}"
8788
assert_raise ArgumentError, msg, fn ->
8889
file_info(record)

0 commit comments

Comments
 (0)