Skip to content

Commit dad7f7b

Browse files
author
José Valim
committed
Allow keywords in typespecs
1 parent a6b81bf commit dad7f7b

File tree

3 files changed

+53
-8
lines changed

3 files changed

+53
-8
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
* enhancements
44
* [Kernel] Add `String.start_with?` and `String.end_with?`
5+
* [Typespec] Allow keywords, e.g. `[foo: integer, bar: boolean | module]`, in typespecs
56

67
* bug fix
78
* [Dict] `Enum.to_list` and `Dict.to_list` now return the same results for dicts

lib/elixir/lib/kernel/typespec.ex

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -422,6 +422,17 @@ defmodule Kernel.Typespec do
422422
{ :{}, [line: line], args }
423423
end
424424

425+
defp typespec_to_ast({ :type, _line, :list, [arg] }) do
426+
case unpack_typespec_kw(arg, []) do
427+
{ :ok, ast } -> ast
428+
:error -> [typespec_to_ast(arg)]
429+
end
430+
end
431+
432+
defp typespec_to_ast({ :type, _line, :list, args }) do
433+
lc arg inlist args, do: typespec_to_ast(arg)
434+
end
435+
425436
defp typespec_to_ast({ :type, line, :binary, [arg1, arg2] }) do
426437
[arg1, arg2] = lc arg inlist [arg1, arg2], do: typespec_to_ast(arg)
427438
cond do
@@ -473,7 +484,7 @@ defmodule Kernel.Typespec do
473484
{ var, line, nil }
474485
end
475486

476-
# special shortcut(s)
487+
# Special shortcut(s)
477488
defp typespec_to_ast({ :remote_type, line, [{:atom, _, :elixir}, {:atom, _, :char_list}, []] }) do
478489
typespec_to_ast({:type, line, :char_list, []})
479490
end
@@ -482,7 +493,6 @@ defmodule Kernel.Typespec do
482493
typespec_to_ast({:type, line, :as_boolean, [arg]})
483494
end
484495

485-
486496
defp typespec_to_ast({ :remote_type, line, [mod, name, args] }) do
487497
args = lc arg inlist args, do: typespec_to_ast(arg)
488498
dot = { :., [line: line], [typespec_to_ast(mod), typespec_to_ast(name)] }
@@ -524,7 +534,7 @@ defmodule Kernel.Typespec do
524534

525535
# Handle unions
526536
defp typespec({ :|, meta, [_,_] } = exprs, vars, caller) do
527-
exprs = :lists.reverse(collect_union(exprs))
537+
exprs = Enum.reverse(collect_union(exprs))
528538
union = lc e inlist exprs, do: typespec(e, vars, caller)
529539
{ :type, line(meta), :union, union }
530540
end
@@ -558,7 +568,6 @@ defmodule Kernel.Typespec do
558568
end
559569

560570
# Handle funs
561-
562571
defp typespec({:->, meta, [{[{:fun, _, arguments}], return}]}, vars, caller) when is_list(arguments) do
563572
typespec({:->, meta, [{arguments, return}]}, vars, caller)
564573
end
@@ -609,7 +618,6 @@ defmodule Kernel.Typespec do
609618
end
610619

611620
# Handle blocks
612-
613621
defp typespec({:__block__, _meta, [arg]}, vars, caller) do
614622
typespec(arg, vars, caller)
615623
end
@@ -665,8 +673,11 @@ defmodule Kernel.Typespec do
665673
typespec({ :nonempty_list, [], [spec] }, vars, caller)
666674
end
667675

668-
defp typespec(l, _, _) when is_list(l) do
669-
raise ArgumentError, message: "Unexpected list #{inspect l}"
676+
defp typespec([h|t] = l, vars, caller) do
677+
union = Enum.reduce(t, validate_kw(h, l), fn(x, acc) ->
678+
{ :|, [], [acc, validate_kw(x, l)] }
679+
end)
680+
typespec({ :list, [], [union] }, vars, caller)
670681
end
671682

672683
defp typespec(t, vars, caller) when is_tuple(t) do
@@ -684,6 +695,11 @@ defmodule Kernel.Typespec do
684695
defp collect_union({ :|, _, [a, b] }), do: [b|collect_union(a)]
685696
defp collect_union(v), do: [v]
686697

698+
defp validate_kw({ key, _ } = t, _) when is_atom(key), do: t
699+
defp validate_kw(_, original) do
700+
raise ArgumentError, message: "unexpected list #{inspect original} in typespec"
701+
end
702+
687703
defp fn_args(meta, args, return, vars, caller) do
688704
case [fn_args(meta, args, vars, caller), typespec(return, vars, caller)] do
689705
[{:type,_,:any},{:type,_,:any,[]}] -> []
@@ -703,4 +719,19 @@ defmodule Kernel.Typespec do
703719
defp variable({name, meta, _}) do
704720
{:var, line(meta), name}
705721
end
722+
723+
defp unpack_typespec_kw({ :type, _, :union, [
724+
next,
725+
{ :type, _, :tuple, [{ :atom, _, atom }, type] }
726+
] }, acc) do
727+
unpack_typespec_kw(next, [{atom,typespec_to_ast(type)}|acc])
728+
end
729+
730+
defp unpack_typespec_kw({ :type, _, :tuple, [{ :atom, _, atom }, type] }, acc) do
731+
{ :ok, [{atom,typespec_to_ast(type)}|acc] }
732+
end
733+
734+
defp unpack_typespec_kw(_, _acc) do
735+
:error
736+
end
706737
end

lib/elixir/test/elixir/typespec_test.exs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,18 @@ defmodule Typespec.TypeTest do
214214
[{:atom, _, Range}, {:type, _, :integer, []}, {:type, _, :any, []}]}, []} = spec
215215
end
216216

217+
test "@type with keywords" do
218+
spec = test_module do
219+
@type mytype :: [first: integer, last: integer]
220+
end
221+
assert {:mytype,{:type,_,:list,[
222+
{:type,_,:union,[
223+
{:type,_,:tuple,[{:atom,_,:first},{:type,_,:integer,[]}]},
224+
{:type,_,:tuple,[{:atom,_,:last},{:type,_,:integer,[]}]}
225+
]}
226+
]}, []} = spec
227+
end
228+
217229
test "@type with parameters" do
218230
{spec1, spec2, spec3} = test_module do
219231
t1 = @type mytype(x) :: x
@@ -339,7 +351,7 @@ defmodule Typespec.TypeTest do
339351
(quote do: @type imm_type_1() :: 1),
340352
(quote do: @type imm_type_2() :: :atom),
341353
(quote do: @type simple_type() :: integer()),
342-
(quote do: @type param_type(p) :: list(p)),
354+
(quote do: @type param_type(p) :: [p]),
343355
(quote do: @type union_type() :: integer() | binary() | boolean()),
344356
(quote do: @type binary_type1() :: <<_ :: _ * 8>>),
345357
(quote do: @type binary_type2() :: <<_ :: 3 * 8>>),
@@ -350,6 +362,7 @@ defmodule Typespec.TypeTest do
350362
(quote do: @type ab() :: as_boolean(term())),
351363
(quote do: @type vaf() :: (... -> any())),
352364
(quote do: @type rng() :: 1 .. 10),
365+
(quote do: @type opts() :: [first: integer(), last: integer()]),
353366
]
354367

355368
types = test_module do

0 commit comments

Comments
 (0)