Skip to content

Commit c09245b

Browse files
authored
Merge pull request #20 from esl/types-elixir-pp
Pretty print Elixir types
2 parents dd1467b + 33e9fc1 commit c09245b

File tree

9 files changed

+290
-72
lines changed

9 files changed

+290
-72
lines changed

lib/gradient/elixir_type.ex

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,23 @@ defmodule Gradient.ElixirType do
22
@moduledoc """
33
Module to format types.
44
5-
TODO records
6-
FIXME add tests
5+
Seems that:
6+
- record type
7+
- constrained function type
8+
are not used by Elixir so the pp support has not been added.
79
"""
810

11+
@type abstract_type() :: Gradient.Types.abstract_type()
12+
913
@doc """
1014
Take type and prepare a pretty string representation.
1115
"""
12-
@spec pretty_print(tuple()) :: String.t()
13-
def pretty_print({:remote_type, _, [{:atom, _, mod}, {:atom, _, type}, args]}) do
16+
@spec pretty_print(abstract_type()) :: String.t()
17+
def pretty_print({:remote_type, _, [{:atom, _, mod}, {:atom, _, name}, args]}) do
1418
args_str = Enum.map(args, &pretty_print(&1)) |> Enum.join(", ")
15-
type_str = Atom.to_string(type)
19+
name_str = Atom.to_string(name)
1620
mod_str = parse_module(mod)
17-
mod_str <> type_str <> "(#{args_str})"
21+
mod_str <> name_str <> "(#{args_str})"
1822
end
1923

2024
def pretty_print({:user_type, _, type, args}) do
@@ -24,7 +28,7 @@ defmodule Gradient.ElixirType do
2428
end
2529

2630
def pretty_print({:ann_type, _, [var_name, var_type]}) do
27-
pretty_print(var_name) <> pretty_print(var_type)
31+
pretty_print(var_name) <> " :: " <> pretty_print(var_type)
2832
end
2933

3034
def pretty_print({:type, _, :map, :any}) do
@@ -37,14 +41,18 @@ defmodule Gradient.ElixirType do
3741
end
3842

3943
def pretty_print({:op, _, op, type}) do
40-
Atom.to_string(op) <> pretty_print(type)
44+
Atom.to_string(op) <> " " <> pretty_print(type)
4145
end
4246

4347
def pretty_print({:op, _, op, left_type, right_type}) do
4448
operator = " " <> Atom.to_string(op) <> " "
4549
pretty_print(left_type) <> operator <> pretty_print(right_type)
4650
end
4751

52+
def pretty_print({:type, _, :fun, []}) do
53+
"fun()"
54+
end
55+
4856
def pretty_print({:type, _, :fun, [{:type, _, :product, arg_types}, res_type]}) do
4957
args = Enum.map(arg_types, &pretty_print(&1)) |> Enum.join(", ")
5058
res = pretty_print(res_type)
@@ -56,6 +64,10 @@ defmodule Gradient.ElixirType do
5664
"(... -> " <> res <> ")"
5765
end
5866

67+
def pretty_print({:type, _, :range, [low, high]}) do
68+
pretty_print(low) <> ".." <> pretty_print(high)
69+
end
70+
5971
def pretty_print({:type, _, :tuple, :any}) do
6072
"tuple()"
6173
end
@@ -65,11 +77,7 @@ defmodule Gradient.ElixirType do
6577
"{" <> elements_str <> "}"
6678
end
6779

68-
def pretty_print({:atom, _, nil}) do
69-
"nil"
70-
end
71-
72-
def pretty_print({:atom, _, val}) when val in [true, false] do
80+
def pretty_print({:atom, _, val}) when val in [nil, true, false] do
7381
Atom.to_string(val)
7482
end
7583

@@ -86,6 +94,7 @@ defmodule Gradient.ElixirType do
8694
end
8795

8896
def pretty_print({:type, _, nil, []}) do
97+
# The empty list type [] cannot be distinguished from the predefined type nil()
8998
"[]"
9099
end
91100

@@ -102,6 +111,10 @@ defmodule Gradient.ElixirType do
102111
Atom.to_string(type) <> "(#{args_str})"
103112
end
104113

114+
def pretty_print({:var, _, t}) do
115+
Atom.to_string(t)
116+
end
117+
105118
def pretty_print(type) do
106119
"#{inspect(type)}"
107120
end

lib/gradient/types.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
defmodule Gradient.Types do
22
@type token :: tuple()
33
@type tokens :: [tuple()]
4+
@type abstract_type :: :erl_parse.abstract_type()
45
@type form ::
56
:erl_parse.abstract_clause()
67
| :erl_parse.abstract_expr()
2.41 KB
Binary file not shown.
700 Bytes
Binary file not shown.

test/examples/type/record.ex

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
defmodule RecordEx do
2+
require Record
3+
Record.defrecord(:user, name: "john", age: 25)
4+
5+
@type user :: record(:user, name: String.t(), age: integer)
6+
7+
@spec ret_wrong_record() :: user()
8+
def ret_wrong_record(), do: :ok
9+
10+
@spec ret_wrong_record2() :: user()
11+
def ret_wrong_record2(), do: user(name: 12)
12+
end

test/examples/type/wrong_ret.ex

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,26 @@ defmodule WrongRet do
2626
@spec ret_out_of_range_int() :: 1..10
2727
def ret_out_of_range_int, do: 12
2828

29+
@spec ret_wrong_float() :: float()
30+
def ret_wrong_float() do
31+
1
32+
end
33+
34+
@spec ret_wrong_float2() :: float()
35+
def ret_wrong_float2() do
36+
nil
37+
end
38+
39+
@spec ret_wrong_char() :: char()
40+
def ret_wrong_char() do
41+
'Ala ma kota'
42+
end
43+
44+
@spec ret_wrong_char2() :: ?o
45+
def ret_wrong_char2() do
46+
nil
47+
end
48+
2949
@spec ret_wrong_boolean() :: boolean()
3050
def ret_wrong_boolean, do: :ok
3151

test/gradient/elixir_fmt_test.exs

Lines changed: 124 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ defmodule Gradient.ElixirFmtTest do
99
@example_module_path "test/examples/simple_app.ex"
1010

1111
setup_all config do
12-
load_wrong_ret_error_examples(config)
12+
config
13+
|> load_wrong_ret_error_examples()
14+
|> load_record_type_example()
1315
end
1416

1517
test "try_highlight_in_context/2" do
@@ -25,109 +27,152 @@ defmodule Gradient.ElixirFmtTest do
2527
end
2628

2729
describe "types format" do
28-
test "return integer() instead atom()", %{wrong_ret_errors: errors} do
29-
msg = format_error_to_binary(errors.ret_wrong_atom)
30+
test "return integer() instead of atom()", %{wrong_ret_errors: errors} do
31+
[expected, actual] = format_error_to_binary(errors.ret_wrong_atom)
3032

31-
assert String.contains?(msg, "atom()")
32-
assert String.contains?(msg, "1")
33+
assert String.contains?(expected, "atom()")
34+
assert String.contains?(actual, "1")
3335
end
3436

35-
test "return tuple() instead atom()", %{wrong_ret_errors: errors} do
36-
msg = format_error_to_binary(errors.ret_wrong_atom2)
37+
test "return tuple() instead of atom()", %{wrong_ret_errors: errors} do
38+
[expected, actual] = format_error_to_binary(errors.ret_wrong_atom2)
3739

38-
assert String.contains?(msg, "atom()")
39-
assert String.contains?(msg, "{:ok, []}")
40+
assert String.contains?(expected, "atom()")
41+
assert String.contains?(actual, "{:ok, []}")
4042
end
4143

42-
test "return map() instead atom()", %{wrong_ret_errors: errors} do
43-
msg = format_error_to_binary(errors.ret_wrong_atom3)
44+
test "return map() instead of atom()", %{wrong_ret_errors: errors} do
45+
[expected, actual] = format_error_to_binary(errors.ret_wrong_atom3)
4446

45-
assert String.contains?(msg, "atom()")
46-
assert String.contains?(msg, "%{required(:a) => 1}")
47+
assert String.contains?(expected, "atom()")
48+
assert String.contains?(actual, "%{required(:a) => 1}")
4749
end
4850

49-
test "return float() instead integer()", %{wrong_ret_errors: errors} do
50-
msg = format_error_to_binary(errors.ret_wrong_integer)
51+
test "return float() instead of integer()", %{wrong_ret_errors: errors} do
52+
[expected, actual] = format_error_to_binary(errors.ret_wrong_integer)
5153

52-
assert String.contains?(msg, "integer()")
53-
assert String.contains?(msg, "1.0")
54+
assert String.contains?(expected, "integer()")
55+
assert String.contains?(actual, "float()")
5456
end
5557

56-
test "return atom() instead integer()", %{wrong_ret_errors: errors} do
57-
msg = format_error_to_binary(errors.ret_wrong_integer2)
58+
test "return atom() instead of integer()", %{wrong_ret_errors: errors} do
59+
[expected, actual] = format_error_to_binary(errors.ret_wrong_integer2)
5860

59-
assert String.contains?(msg, "integer()")
60-
assert String.contains?(msg, ":ok")
61+
assert String.contains?(expected, "integer()")
62+
assert String.contains?(actual, ":ok")
6163
end
6264

63-
test "return boolean() instead integer()", %{wrong_ret_errors: errors} do
64-
msg = format_error_to_binary(errors.ret_wrong_integer3)
65+
test "return boolean() instead of integer()", %{wrong_ret_errors: errors} do
66+
[expected, actual] = format_error_to_binary(errors.ret_wrong_integer3)
6567

66-
assert String.contains?(msg, "integer()")
67-
assert String.contains?(msg, "true")
68+
assert String.contains?(expected, "integer()")
69+
assert String.contains?(actual, "true")
6870
end
6971

70-
test "return list() instead integer()", %{wrong_ret_errors: errors} do
71-
msg = format_error_to_binary(errors.ret_wrong_integer4)
72+
test "return list() instead of integer()", %{wrong_ret_errors: errors} do
73+
[expected, actual] = format_error_to_binary(errors.ret_wrong_integer4)
7274

73-
assert String.contains?(msg, "integer()")
74-
assert String.contains?(msg, "nonempty_list()")
75+
assert String.contains?(expected, "integer()")
76+
assert String.contains?(actual, "nonempty_list()")
7577
end
7678

7779
test "return integer() out of the range()", %{wrong_ret_errors: errors} do
78-
msg = format_error_to_binary(errors.ret_out_of_range_int)
80+
[expected, actual] = format_error_to_binary(errors.ret_out_of_range_int)
7981

80-
assert String.contains?(msg, "range(1, 10)")
81-
assert String.contains?(msg, "12")
82+
assert String.contains?(expected, "1..10")
83+
assert String.contains?(actual, "12")
8284
end
8385

84-
test "return atom() instead boolean()", %{wrong_ret_errors: errors} do
85-
msg = format_error_to_binary(errors.ret_wrong_boolean)
86+
test "return integer() instead of float()", %{wrong_ret_errors: errors} do
87+
[expected, actual] = format_error_to_binary(errors.ret_wrong_float)
8688

87-
assert String.contains?(msg, "boolean()")
88-
assert String.contains?(msg, ":ok")
89+
assert String.contains?(expected, "float()")
90+
assert String.contains?(actual, "1")
8991
end
9092

91-
test "return binary() instead boolean()", %{wrong_ret_errors: errors} do
92-
msg = format_error_to_binary(errors.ret_wrong_boolean2)
93+
test "return nil() instead of float()", %{wrong_ret_errors: errors} do
94+
[expected, actual] = format_error_to_binary(errors.ret_wrong_float2)
9395

94-
assert String.contains?(msg, "boolean()")
95-
assert String.contains?(msg, "binary()")
96+
assert String.contains?(expected, "float()")
97+
assert String.contains?(actual, "nil")
9698
end
9799

98-
test "return integer() instead boolean()", %{wrong_ret_errors: errors} do
99-
msg = format_error_to_binary(errors.ret_wrong_boolean3)
100+
test "return charlist() instead of char()", %{wrong_ret_errors: errors} do
101+
[expected, actual] = format_error_to_binary(errors.ret_wrong_char)
100102

101-
assert String.contains?(msg, "boolean()")
102-
assert String.contains?(msg, "1")
103+
assert String.contains?(expected, "char()")
104+
assert String.contains?(actual, "nonempty_list()")
103105
end
104106

105-
test "return keyword() instead boolean()", %{wrong_ret_errors: errors} do
106-
msg = format_error_to_binary(errors.ret_wrong_boolean4)
107+
test "return nil() instead of char()", %{wrong_ret_errors: errors} do
108+
[expected, actual] = format_error_to_binary(errors.ret_wrong_char2)
107109

108-
assert String.contains?(msg, "boolean()")
109-
assert String.contains?(msg, "nonempty_list()")
110+
# unfortunately char is represented as {:integer, 0, _}
111+
assert String.contains?(expected, "111")
112+
assert String.contains?(actual, "nil")
110113
end
111114

112-
test "return list() instead keyword()", %{wrong_ret_errors: errors} do
113-
msg = format_error_to_binary(errors.ret_wrong_keyword)
115+
test "return atom() instead of boolean()", %{wrong_ret_errors: errors} do
116+
[expected, actual] = format_error_to_binary(errors.ret_wrong_boolean)
114117

115-
assert String.contains?(msg, "{atom(), any()}")
116-
assert String.contains?(msg, "1")
118+
assert String.contains?(expected, "boolean()")
119+
assert String.contains?(actual, ":ok")
117120
end
118121

119-
test "return tuple() instead map()", %{wrong_ret_errors: errors} do
120-
msg = format_error_to_binary(errors.ret_wrong_map)
122+
test "return binary() instead of boolean()", %{wrong_ret_errors: errors} do
123+
[expected, actual] = format_error_to_binary(errors.ret_wrong_boolean2)
121124

122-
assert String.contains?(msg, "map()")
123-
assert String.contains?(msg, "{:a, 1, 2}")
125+
assert String.contains?(expected, "boolean()")
126+
assert String.contains?(actual, "binary()")
127+
end
128+
129+
test "return integer() instead of boolean()", %{wrong_ret_errors: errors} do
130+
[expected, actual] = format_error_to_binary(errors.ret_wrong_boolean3)
131+
132+
assert String.contains?(expected, "boolean()")
133+
assert String.contains?(actual, "1")
134+
end
135+
136+
test "return keyword() instead of boolean()", %{wrong_ret_errors: errors} do
137+
[expected, actual] = format_error_to_binary(errors.ret_wrong_boolean4)
138+
139+
assert String.contains?(expected, "boolean()")
140+
assert String.contains?(actual, "nonempty_list()")
141+
end
142+
143+
test "return list() instead of keyword()", %{wrong_ret_errors: errors} do
144+
[expected, actual] = format_error_to_binary(errors.ret_wrong_keyword)
145+
146+
assert String.contains?(expected, "{atom(), any()}")
147+
assert String.contains?(actual, "1")
148+
end
149+
150+
test "return tuple() instead of map()", %{wrong_ret_errors: errors} do
151+
[expected, actual] = format_error_to_binary(errors.ret_wrong_map)
152+
153+
assert String.contains?(expected, "map()")
154+
assert String.contains?(actual, "{:a, 1, 2}")
124155
end
125156

126157
test "return lambda with wrong returned type", %{wrong_ret_errors: errors} do
127-
msg = format_error_to_binary(errors.ret_wrong_fun)
158+
[expected, actual] = format_error_to_binary(errors.ret_wrong_fun)
159+
160+
assert String.contains?(expected, "atom()")
161+
assert String.contains?(actual, "12")
162+
end
128163

129-
assert String.contains?(msg, "atom()")
130-
assert String.contains?(msg, "12")
164+
test "return atom() instead of record()", %{record_type_errors: errors} do
165+
[expected, actual] = format_error_to_binary(errors.ret_wrong_record)
166+
167+
assert String.contains?(expected, "user()")
168+
assert String.contains?(actual, ":ok")
169+
end
170+
171+
test "return wrong record value type", %{record_type_errors: errors} do
172+
[expected, actual] = format_error_to_binary(errors.ret_wrong_record2)
173+
174+
assert String.contains?(expected, "String.t()")
175+
assert String.contains?(actual, "12")
131176
end
132177
end
133178

@@ -165,6 +210,26 @@ defmodule Gradient.ElixirFmtTest do
165210
error
166211
|> ElixirFmt.format_error(opts)
167212
|> :erlang.iolist_to_binary()
213+
|> String.split("have type")
214+
|> List.last()
215+
|> String.split("but it has type")
216+
end
217+
218+
@spec load_record_type_example(map()) :: map()
219+
defp load_record_type_example(config) do
220+
{_tokens, ast} = load("/type/Elixir.RecordEx.beam", "/type/record.ex")
221+
222+
{errors, forms} = type_check_file(ast, [])
223+
224+
names =
225+
get_function_names_from_ast(forms)
226+
|> Enum.drop(3)
227+
228+
errors_map =
229+
Enum.zip(names, errors)
230+
|> Map.new()
231+
232+
Map.put(config, :record_type_errors, errors_map)
168233
end
169234

170235
@spec load_wrong_ret_error_examples(map()) :: map()

0 commit comments

Comments
 (0)