Skip to content

Commit bd49269

Browse files
author
Étienne Lévesque
committed
Properly identify test modules
1 parent fd1793e commit bd49269

File tree

3 files changed

+219
-180
lines changed

3 files changed

+219
-180
lines changed

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

Lines changed: 4 additions & 180 deletions
Original file line numberDiff line numberDiff line change
@@ -10,189 +10,13 @@ defmodule ElixirLS.LanguageServer.Providers.CodeLens do
1010
disable this feature.
1111
"""
1212

13-
alias ElixirLS.LanguageServer.{Server, SourceFile}
14-
alias ElixirSense.Core.Parser
15-
alias ElixirSense.Core.State
16-
alias Erl2ex.Convert.{Context, ErlForms}
17-
alias Erl2ex.Pipeline.{Parse, ModuleData, ExSpec}
13+
alias ElixirLS.LanguageServer.Providers.CodeLens
1814
import ElixirLS.LanguageServer.Protocol
1915

20-
defmodule ContractTranslator do
21-
def translate_contract(fun, contract, is_macro) do
22-
# FIXME: Private module
23-
{[%ExSpec{specs: [spec]} | _], _} =
24-
"-spec foo#{contract}."
25-
# FIXME: Private module
26-
|> Parse.string()
27-
|> hd()
28-
|> elem(0)
29-
# FIXME: Private module
30-
|> ErlForms.conv_form(%Context{
31-
in_type_expr: true,
32-
# FIXME: Private module
33-
module_data: %ModuleData{}
34-
})
16+
def spec_code_lens(server_instance_id, uri, text),
17+
do: CodeLens.Spec.code_lens(server_instance_id, uri, text)
3518

36-
spec
37-
|> Macro.postwalk(&tweak_specs/1)
38-
|> drop_macro_env(is_macro)
39-
|> Macro.to_string()
40-
|> String.replace("()", "")
41-
|> Code.format_string!(line_length: :infinity)
42-
|> IO.iodata_to_binary()
43-
|> String.replace_prefix("foo", to_string(fun))
44-
end
45-
46-
defp tweak_specs({:list, _meta, args}) do
47-
case args do
48-
[{:{}, _, [{:atom, _, []}, {wild, _, _}]}] when wild in [:_, :any] -> quote do: keyword()
49-
list -> list
50-
end
51-
end
52-
53-
defp tweak_specs({:nonempty_list, _meta, args}) do
54-
case args do
55-
[{:any, _, []}] -> quote do: [...]
56-
_ -> args ++ quote do: [...]
57-
end
58-
end
59-
60-
defp tweak_specs({:%{}, _meta, fields}) do
61-
fields =
62-
Enum.map(fields, fn
63-
{:map_field_exact, _, [key, value]} -> {key, value}
64-
{key, value} -> quote do: {optional(unquote(key)), unquote(value)}
65-
field -> field
66-
end)
67-
|> Enum.reject(&match?({{:optional, _, [{:any, _, []}]}, {:any, _, []}}, &1))
68-
69-
fields
70-
|> Enum.find_value(fn
71-
{:__struct__, struct_type} when is_atom(struct_type) -> struct_type
72-
_ -> nil
73-
end)
74-
|> case do
75-
nil -> {:%{}, [], fields}
76-
struct_type -> {{:., [], [struct_type, :t]}, [], []}
77-
end
78-
end
79-
80-
# Undo conversion of _ to any() when inside binary spec
81-
defp tweak_specs({:<<>>, _, children}) do
82-
children =
83-
Macro.postwalk(children, fn
84-
{:any, _, []} -> quote do: _
85-
other -> other
86-
end)
87-
88-
{:<<>>, [], children}
89-
end
90-
91-
defp tweak_specs({:_, _, _}) do
92-
quote do: any()
93-
end
94-
95-
defp tweak_specs({:when, [], [spec, substitutions]}) do
96-
substitutions = Enum.reject(substitutions, &match?({:_, {:any, _, []}}, &1))
97-
98-
case substitutions do
99-
[] -> spec
100-
_ -> {:when, [], [spec, substitutions]}
101-
end
102-
end
103-
104-
defp tweak_specs(node) do
105-
node
106-
end
107-
108-
defp drop_macro_env(ast, false), do: ast
109-
110-
defp drop_macro_env({:"::", [], [{:foo, [], [_env | rest]}, res]}, true) do
111-
{:"::", [], [{:foo, [], rest}, res]}
112-
end
113-
end
114-
115-
def spec_code_lens(server_instance_id, uri, text) do
116-
resp =
117-
for {_, line, {mod, fun, arity}, contract, is_macro} <- Server.suggest_contracts(uri),
118-
SourceFile.function_def_on_line?(text, line, fun),
119-
spec = ContractTranslator.translate_contract(fun, contract, is_macro) do
120-
build_code_lens(
121-
line,
122-
"@spec #{spec}",
123-
"spec:#{server_instance_id}",
124-
%{
125-
"uri" => uri,
126-
"mod" => to_string(mod),
127-
"fun" => to_string(fun),
128-
"arity" => arity,
129-
"spec" => spec,
130-
"line" => line
131-
}
132-
)
133-
end
134-
135-
{:ok, resp}
136-
end
137-
138-
def test_code_lens(uri, src) do
139-
file_path = SourceFile.path_from_uri(uri)
140-
141-
if imports?(src, ExUnit.Case) do
142-
test_calls = calls_to(src, :test)
143-
describe_calls = calls_to(src, :describe)
144-
145-
calls_lenses =
146-
for {line, _col} <- test_calls ++ describe_calls do
147-
test_filter = "#{file_path}:#{line}"
148-
149-
build_code_lens(line, "Run test", "elixir.test.run", test_filter)
150-
end
151-
152-
file_lens = build_code_lens(1, "Run test", "elixir.test.run", file_path)
153-
154-
{:ok, [file_lens | calls_lenses]}
155-
end
156-
end
157-
158-
@spec imports?(String.t(), [atom()] | atom()) :: boolean()
159-
defp imports?(buffer, modules) do
160-
buffer_file_metadata =
161-
buffer
162-
|> Parser.parse_string(true, true, 1)
163-
164-
imports_set =
165-
buffer_file_metadata.lines_to_env
166-
|> get_imports()
167-
|> MapSet.new()
168-
169-
modules
170-
|> List.wrap()
171-
|> MapSet.new()
172-
|> MapSet.subset?(imports_set)
173-
end
174-
175-
defp get_imports(lines_to_env) do
176-
%State.Env{imports: imports} =
177-
lines_to_env
178-
|> Enum.max_by(fn {k, _v} -> k end)
179-
|> elem(1)
180-
181-
imports
182-
end
183-
184-
@spec calls_to(String.t(), atom() | {atom(), integer()}) :: [{pos_integer(), pos_integer()}]
185-
defp calls_to(buffer, function) do
186-
buffer_file_metadata =
187-
buffer
188-
|> Parser.parse_string(true, true, 1)
189-
190-
buffer_file_metadata.calls
191-
|> Enum.map(fn {_k, v} -> v end)
192-
|> List.flatten()
193-
|> Enum.filter(fn call_info -> call_info.func == function end)
194-
|> Enum.map(fn call -> call.position end)
195-
end
19+
def test_code_lens(uri, text), do: CodeLens.Test.code_lens(uri, text)
19620

19721
def build_code_lens(line, title, command, argument) do
19822
%{
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
defmodule ElixirLS.LanguageServer.Providers.CodeLens.Spec do
2+
alias ElixirLS.LanguageServer.Providers.CodeLens
3+
alias ElixirLS.LanguageServer.{Server, SourceFile}
4+
alias Erl2ex.Convert.{Context, ErlForms}
5+
alias Erl2ex.Pipeline.{Parse, ModuleData, ExSpec}
6+
7+
defmodule ContractTranslator do
8+
def translate_contract(fun, contract, is_macro) do
9+
# FIXME: Private module
10+
{[%ExSpec{specs: [spec]} | _], _} =
11+
"-spec foo#{contract}."
12+
# FIXME: Private module
13+
|> Parse.string()
14+
|> hd()
15+
|> elem(0)
16+
# FIXME: Private module
17+
|> ErlForms.conv_form(%Context{
18+
in_type_expr: true,
19+
# FIXME: Private module
20+
module_data: %ModuleData{}
21+
})
22+
23+
spec
24+
|> Macro.postwalk(&tweak_specs/1)
25+
|> drop_macro_env(is_macro)
26+
|> Macro.to_string()
27+
|> String.replace("()", "")
28+
|> Code.format_string!(line_length: :infinity)
29+
|> IO.iodata_to_binary()
30+
|> String.replace_prefix("foo", to_string(fun))
31+
end
32+
33+
defp tweak_specs({:list, _meta, args}) do
34+
case args do
35+
[{:{}, _, [{:atom, _, []}, {wild, _, _}]}] when wild in [:_, :any] -> quote do: keyword()
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+
|> Enum.reject(&match?({{:optional, _, [{:any, _, []}]}, {:any, _, []}}, &1))
55+
56+
fields
57+
|> Enum.find_value(fn
58+
{:__struct__, struct_type} when is_atom(struct_type) -> struct_type
59+
_ -> nil
60+
end)
61+
|> case do
62+
nil -> {:%{}, [], fields}
63+
struct_type -> {{:., [], [struct_type, :t]}, [], []}
64+
end
65+
end
66+
67+
# Undo conversion of _ to any() when inside binary spec
68+
defp tweak_specs({:<<>>, _, children}) do
69+
children =
70+
Macro.postwalk(children, fn
71+
{:any, _, []} -> quote do: _
72+
other -> other
73+
end)
74+
75+
{:<<>>, [], children}
76+
end
77+
78+
defp tweak_specs({:_, _, _}) do
79+
quote do: any()
80+
end
81+
82+
defp tweak_specs({:when, [], [spec, substitutions]}) do
83+
substitutions = Enum.reject(substitutions, &match?({:_, {:any, _, []}}, &1))
84+
85+
case substitutions do
86+
[] -> spec
87+
_ -> {:when, [], [spec, substitutions]}
88+
end
89+
end
90+
91+
defp tweak_specs(node) do
92+
node
93+
end
94+
95+
defp drop_macro_env(ast, false), do: ast
96+
97+
defp drop_macro_env({:"::", [], [{:foo, [], [_env | rest]}, res]}, true) do
98+
{:"::", [], [{:foo, [], rest}, res]}
99+
end
100+
end
101+
102+
def code_lens(server_instance_id, uri, text) do
103+
resp =
104+
for {_, line, {mod, fun, arity}, contract, is_macro} <- Server.suggest_contracts(uri),
105+
SourceFile.function_def_on_line?(text, line, fun),
106+
spec = ContractTranslator.translate_contract(fun, contract, is_macro) do
107+
CodeLens.build_code_lens(
108+
line,
109+
"@spec #{spec}",
110+
"spec:#{server_instance_id}",
111+
%{
112+
"uri" => uri,
113+
"mod" => to_string(mod),
114+
"fun" => to_string(fun),
115+
"arity" => arity,
116+
"spec" => spec,
117+
"line" => line
118+
}
119+
)
120+
end
121+
122+
{:ok, resp}
123+
end
124+
end

0 commit comments

Comments
 (0)