Skip to content

Commit 7477856

Browse files
authored
contract translator extracted to separate module (elixir-editors#462)
tests added fix %{optional(atom)=>any} being translated to %{} - should be map do not translate %Struct{} to Struct.t if Struct.t does not exist added tweaks for fun(), list() and struct() Fixes elixir-editors#425
1 parent 00765ba commit 7477856

File tree

3 files changed

+274
-97
lines changed

3 files changed

+274
-97
lines changed

apps/language_server/lib/language_server/providers/code_lens/type_spec.ex

Lines changed: 1 addition & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -12,103 +12,7 @@ defmodule ElixirLS.LanguageServer.Providers.CodeLens.TypeSpec do
1212

1313
alias ElixirLS.LanguageServer.Providers.CodeLens
1414
alias ElixirLS.LanguageServer.{Server, SourceFile}
15-
alias Erl2ex.Convert.{Context, ErlForms}
16-
alias Erl2ex.Pipeline.{Parse, ModuleData, ExSpec}
17-
18-
defmodule ContractTranslator do
19-
def translate_contract(fun, contract, is_macro) do
20-
# FIXME: Private module
21-
{[%ExSpec{specs: [spec]} | _], _} =
22-
"-spec foo#{contract}."
23-
# FIXME: Private module
24-
|> Parse.string()
25-
|> hd()
26-
|> elem(0)
27-
# FIXME: Private module
28-
|> ErlForms.conv_form(%Context{
29-
in_type_expr: true,
30-
# FIXME: Private module
31-
module_data: %ModuleData{}
32-
})
33-
34-
spec
35-
|> Macro.postwalk(&tweak_specs/1)
36-
|> drop_macro_env(is_macro)
37-
|> Macro.to_string()
38-
|> String.replace("()", "")
39-
|> Code.format_string!(line_length: :infinity)
40-
|> IO.iodata_to_binary()
41-
|> String.replace_prefix("foo", to_string(fun))
42-
end
43-
44-
defp tweak_specs({:list, _meta, args}) do
45-
case args do
46-
[{:{}, _, [{:atom, _, []}, {wild, _, _}]}] when wild in [:_, :any] -> quote do: keyword()
47-
list -> list
48-
end
49-
end
50-
51-
defp tweak_specs({:nonempty_list, _meta, args}) do
52-
case args do
53-
[{:any, _, []}] -> quote do: [...]
54-
_ -> args ++ quote do: [...]
55-
end
56-
end
57-
58-
defp tweak_specs({:%{}, _meta, fields}) do
59-
fields =
60-
Enum.map(fields, fn
61-
{:map_field_exact, _, [key, value]} -> {key, value}
62-
{key, value} -> quote do: {optional(unquote(key)), unquote(value)}
63-
field -> field
64-
end)
65-
|> Enum.reject(&match?({{:optional, _, [{:any, _, []}]}, {:any, _, []}}, &1))
66-
67-
fields
68-
|> Enum.find_value(fn
69-
{:__struct__, struct_type} when is_atom(struct_type) -> struct_type
70-
_ -> nil
71-
end)
72-
|> case do
73-
nil -> {:%{}, [], fields}
74-
struct_type -> {{:., [], [struct_type, :t]}, [], []}
75-
end
76-
end
77-
78-
# Undo conversion of _ to any() when inside binary spec
79-
defp tweak_specs({:<<>>, _, children}) do
80-
children =
81-
Macro.postwalk(children, fn
82-
{:any, _, []} -> quote do: _
83-
other -> other
84-
end)
85-
86-
{:<<>>, [], children}
87-
end
88-
89-
defp tweak_specs({:_, _, _}) do
90-
quote do: any()
91-
end
92-
93-
defp tweak_specs({:when, [], [spec, substitutions]}) do
94-
substitutions = Enum.reject(substitutions, &match?({:_, {:any, _, []}}, &1))
95-
96-
case substitutions do
97-
[] -> spec
98-
_ -> {:when, [], [spec, substitutions]}
99-
end
100-
end
101-
102-
defp tweak_specs(node) do
103-
node
104-
end
105-
106-
defp drop_macro_env(ast, false), do: ast
107-
108-
defp drop_macro_env({:"::", [], [{:foo, [], [_env | rest]}, res]}, true) do
109-
{:"::", [], [{:foo, [], rest}, res]}
110-
end
111-
end
15+
alias ElixirLS.LanguageServer.Providers.CodeLens.TypeSpec.ContractTranslator
11216

11317
def code_lens(server_instance_id, uri, text) do
11418
resp =
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
defmodule ElixirLS.LanguageServer.Providers.CodeLens.TypeSpec.ContractTranslator do
2+
@moduledoc false
3+
alias Erl2ex.Convert.{Context, ErlForms}
4+
alias Erl2ex.Pipeline.{Parse, ModuleData, ExSpec}
5+
6+
def translate_contract(fun, contract, is_macro) do
7+
# FIXME: Private module
8+
{[%ExSpec{specs: [spec]} | _], _} =
9+
"-spec foo#{contract}."
10+
# FIXME: Private module
11+
|> Parse.string()
12+
|> hd()
13+
|> elem(0)
14+
# FIXME: Private module
15+
|> ErlForms.conv_form(%Context{
16+
in_type_expr: true,
17+
# FIXME: Private module
18+
module_data: %ModuleData{}
19+
})
20+
21+
spec
22+
|> Macro.postwalk(&tweak_specs/1)
23+
|> drop_macro_env(is_macro)
24+
|> Macro.to_string()
25+
|> String.replace("()", "")
26+
|> Code.format_string!(line_length: :infinity)
27+
|> IO.iodata_to_binary()
28+
|> String.replace_prefix("foo", to_string(fun))
29+
end
30+
31+
defp tweak_specs({:list, _meta, args}) do
32+
case args do
33+
[{:{}, _, [{:atom, _, []}, {wild, _, _}]}] when wild in [:_, :any] -> quote do: keyword()
34+
[{:{}, _, [{:atom, _, []}, other]}] -> quote do: keyword(unquote(other))
35+
[{:any, _, []}] -> quote do: list()
36+
list -> list
37+
end
38+
end
39+
40+
defp tweak_specs({:nonempty_list, _meta, args}) do
41+
case args do
42+
[{:any, _, []}] -> quote do: [...]
43+
_ -> args ++ quote do: [...]
44+
end
45+
end
46+
47+
defp tweak_specs({:%{}, _meta, fields}) do
48+
fields =
49+
Enum.map(fields, fn
50+
{:map_field_exact, _, [key, value]} -> {key, value}
51+
{key, value} -> quote do: {optional(unquote(key)), unquote(value)}
52+
field -> field
53+
end)
54+
55+
translate_map(fields)
56+
end
57+
58+
# Undo conversion of _ to any() when inside binary spec
59+
defp tweak_specs({:<<>>, _, children}) do
60+
children =
61+
Macro.postwalk(children, fn
62+
{:any, _, []} -> quote do: _
63+
other -> other
64+
end)
65+
66+
{:<<>>, [], children}
67+
end
68+
69+
defp tweak_specs({:_, _, _}) do
70+
quote do: any()
71+
end
72+
73+
defp tweak_specs({:when, [], [spec, substitutions]}) do
74+
substitutions = Enum.reject(substitutions, &match?({:_, {:any, _, []}}, &1))
75+
76+
case substitutions do
77+
[] -> spec
78+
_ -> {:when, [], [spec, substitutions]}
79+
end
80+
end
81+
82+
defp tweak_specs([{:->, _, [[{:..., _, _}], {:any, _, []}]}]) do
83+
quote do: fun()
84+
end
85+
86+
defp tweak_specs(node) do
87+
node
88+
end
89+
90+
defp drop_macro_env(ast, false), do: ast
91+
92+
defp drop_macro_env({:"::", [], [{:foo, [], [_env | rest]}, res]}, true) do
93+
{:"::", [], [{:foo, [], rest}, res]}
94+
end
95+
96+
defp translate_map([
97+
{:__struct__, {:atom, _, []}},
98+
{{:optional, _, [{:atom, _, []}]}, {:any, _, []}}
99+
]) do
100+
quote do: struct()
101+
end
102+
103+
defp translate_map([
104+
{{:optional, _, [{:any, _, []}]}, {:any, _, []}}
105+
]) do
106+
quote do: map()
107+
end
108+
109+
defp translate_map(fields) do
110+
struct_type =
111+
fields
112+
|> Enum.find_value(fn
113+
{:__struct__, struct_type} when is_atom(struct_type) -> struct_type
114+
_ -> nil
115+
end)
116+
117+
translate_map(struct_type, fields)
118+
end
119+
120+
defp translate_map(nil, fields) do
121+
{:%{}, [], fields}
122+
end
123+
124+
defp translate_map(struct_type, fields) do
125+
struct_type_spec_exists =
126+
ElixirSense.Core.Normalized.Typespec.get_types(struct_type)
127+
|> Enum.any?(&match?({kind, {:t, _, []}} when kind in [:type, :opaque], &1))
128+
129+
if struct_type_spec_exists do
130+
# struct_type.t/0 public/opaque type exists, assume it's a struct
131+
{{:., [], [struct_type, :t]}, [], []}
132+
else
133+
# translate map AST to struct AST
134+
fields = fields |> Enum.reject(&match?({:__struct__, _}, &1))
135+
map = {:%{}, [], fields}
136+
{:%, [], [struct_type, map]}
137+
end
138+
end
139+
end
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
defmodule ElixirLS.LanguageServer.Providers.CodeLens.TypeSpec.ContractTranslatorTest do
2+
use ExUnit.Case, async: true
3+
alias ElixirLS.LanguageServer.Providers.CodeLens.TypeSpec.ContractTranslator
4+
5+
test "translate struct when struct.t type exists" do
6+
contract = '() -> \#{\'__struct__\':=\'Elixir.DateTime\'}'
7+
assert "foo :: DateTime.t()" == ContractTranslator.translate_contract(:foo, contract, false)
8+
end
9+
10+
test "don't translate struct when struct.t type does not exist" do
11+
contract = '() -> \#{\'__struct__\':=\'Elixir.SomeOtherStruct\'}'
12+
13+
assert "foo :: %SomeOtherStruct{}" ==
14+
ContractTranslator.translate_contract(:foo, contract, false)
15+
end
16+
17+
test "struct" do
18+
contract = '() -> \#{\'__struct__\':=atom(), atom()=>any()}'
19+
assert "foo :: struct" == ContractTranslator.translate_contract(:foo, contract, false)
20+
end
21+
22+
test "drop macro env argument" do
23+
contract = '(any(), integer()) -> integer()'
24+
25+
assert "foo(any, integer) :: integer" ==
26+
ContractTranslator.translate_contract(:foo, contract, false)
27+
28+
assert "foo(integer) :: integer" ==
29+
ContractTranslator.translate_contract(:foo, contract, true)
30+
end
31+
32+
test "atom :ok" do
33+
contract = '(any()) -> ok'
34+
assert "foo(any) :: :ok" == ContractTranslator.translate_contract(:foo, contract, false)
35+
end
36+
37+
test "atom true" do
38+
contract = '(any()) -> true'
39+
assert "foo(any) :: true" == ContractTranslator.translate_contract(:foo, contract, false)
40+
end
41+
42+
test "atom _ substitution" do
43+
contract = '(_) -> false'
44+
assert "foo(any) :: false" == ContractTranslator.translate_contract(:foo, contract, false)
45+
end
46+
47+
test "do not drop when substitutions" do
48+
contract = '(X) -> atom() when X :: any()'
49+
50+
assert "foo(x) :: atom when x: any" ==
51+
ContractTranslator.translate_contract(:foo, contract, false)
52+
end
53+
54+
test "keyword" do
55+
contract = '(any()) -> list({atom(), any()})'
56+
assert "foo(any) :: keyword" == ContractTranslator.translate_contract(:foo, contract, false)
57+
58+
contract = '(any()) -> list({atom(), _})'
59+
assert "foo(any) :: keyword" == ContractTranslator.translate_contract(:foo, contract, false)
60+
end
61+
62+
test "keyword(t)" do
63+
contract = '(any()) -> list({atom(), integer()})'
64+
65+
assert "foo(any) :: keyword(integer)" ==
66+
ContractTranslator.translate_contract(:foo, contract, false)
67+
end
68+
69+
test "[type]" do
70+
contract = '(any()) -> list(atom())'
71+
assert "foo(any) :: [atom]" == ContractTranslator.translate_contract(:foo, contract, false)
72+
end
73+
74+
test "list" do
75+
contract = '(any()) -> list(any())'
76+
assert "foo(any) :: list" == ContractTranslator.translate_contract(:foo, contract, false)
77+
end
78+
79+
test "empty list" do
80+
contract = '(any()) -> []'
81+
assert "foo(any) :: []" == ContractTranslator.translate_contract(:foo, contract, false)
82+
end
83+
84+
test "[...]" do
85+
contract = '(any()) -> nonempty_list(any())'
86+
assert "foo(any) :: [...]" == ContractTranslator.translate_contract(:foo, contract, false)
87+
88+
contract = '(any()) -> nonempty_list(_)'
89+
assert "foo(any) :: [...]" == ContractTranslator.translate_contract(:foo, contract, false)
90+
end
91+
92+
test "[type, ...]" do
93+
contract = '(any()) -> nonempty_list(atom())'
94+
95+
assert "foo(any) :: [atom, ...]" ==
96+
ContractTranslator.translate_contract(:foo, contract, false)
97+
end
98+
99+
test "undoes conversion of :_ to any inside bitstring" do
100+
contract = '(any()) -> <<_:2, _:_*3>>'
101+
102+
assert "foo(any) :: <<_::2, _::_*3>>" ==
103+
ContractTranslator.translate_contract(:foo, contract, false)
104+
end
105+
106+
test "function" do
107+
contract = '(any()) -> fun((...) -> ok)'
108+
109+
assert "foo(any) :: (... -> :ok)" ==
110+
ContractTranslator.translate_contract(:foo, contract, false)
111+
end
112+
113+
test "fun" do
114+
contract = '(any()) -> fun((...) -> any())'
115+
assert "foo(any) :: fun" == ContractTranslator.translate_contract(:foo, contract, false)
116+
end
117+
118+
test "empty map" do
119+
contract = '(any()) -> \#{}'
120+
assert "foo(any) :: %{}" == ContractTranslator.translate_contract(:foo, contract, false)
121+
end
122+
123+
test "map" do
124+
contract = '(any()) -> \#{any()=>any()}'
125+
assert "foo(any) :: map" == ContractTranslator.translate_contract(:foo, contract, false)
126+
end
127+
128+
test "map with fields" do
129+
contract = '(any()) -> \#{integer()=>any(), 1:=atom(), abc:=4}'
130+
131+
assert "foo(any) :: %{optional(integer) => any, 1 => atom, :abc => 4}" ==
132+
ContractTranslator.translate_contract(:foo, contract, false)
133+
end
134+
end

0 commit comments

Comments
 (0)