Skip to content

Commit 9a01a18

Browse files
committed
fix replacing for aliased modules
1 parent 3b5d778 commit 9a01a18

File tree

3 files changed

+118
-19
lines changed

3 files changed

+118
-19
lines changed

apps/language_server/lib/language_server/experimental/code_mod/replace_remote_function.ex

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,26 @@ defmodule ElixirLS.LanguageServer.Experimental.CodeMod.ReplaceRemoteFunction do
44
alias ElixirLS.LanguageServer.Experimental.CodeMod.Text
55
alias ElixirLS.LanguageServer.Experimental.Protocol.Types.TextEdit
66

7-
@spec text_edits(String.t(), Ast.t(), [atom()], atom(), atom()) ::
7+
@spec text_edits(String.t(), Ast.t(), [[atom()]], atom(), atom()) ::
88
{:ok, [TextEdit.t()]} | :error
9-
def text_edits(original_text, ast, module_aliases, name, suggestion) do
9+
def text_edits(original_text, ast, possible_aliases, name, suggestion) do
1010
with {:ok, transformed} <-
11-
apply_transforms(original_text, ast, module_aliases, name, suggestion) do
11+
apply_transforms(original_text, ast, possible_aliases, name, suggestion) do
1212
{:ok, Diff.diff(original_text, transformed)}
1313
end
1414
end
1515

16-
defp apply_transforms(line_text, quoted_ast, module_aliases, name, suggestion) do
16+
defp apply_transforms(line_text, quoted_ast, possible_aliases, name, suggestion) do
1717
leading_indent = Text.leading_indent(line_text)
1818

1919
updated_ast =
2020
Macro.postwalk(quoted_ast, fn
21-
{:., function_meta, [{:__aliases__, module_meta, ^module_aliases}, ^name]} ->
22-
{:., function_meta, [{:__aliases__, module_meta, module_aliases}, suggestion]}
21+
{:., function_meta, [{:__aliases__, module_meta, module_alias}, ^name]} ->
22+
if module_alias in possible_aliases do
23+
{:., function_meta, [{:__aliases__, module_meta, module_alias}, suggestion]}
24+
else
25+
{:., function_meta, [{:__aliases__, module_meta, module_alias}, name]}
26+
end
2327

2428
other ->
2529
other

apps/language_server/lib/language_server/experimental/provider/code_action/replace_remote_function.ex

Lines changed: 45 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ defmodule ElixirLS.LanguageServer.Experimental.Provider.CodeAction.ReplaceRemote
1111
alias ElixirLS.LanguageServer.Experimental.Protocol.Types.TextEdit
1212
alias ElixirLS.LanguageServer.Experimental.Protocol.Types.Workspace
1313
alias ElixirLS.LanguageServer.Experimental.SourceFile
14+
alias ElixirSense.Core.Parser
1415

1516
@function_re ~r/(.*)\/(.*) is undefined or private. .*:\n(.*)/s
1617

@@ -24,9 +25,9 @@ defmodule ElixirLS.LanguageServer.Experimental.Provider.CodeAction.ReplaceRemote
2425
one_based_line = extract_start_line(diagnostic)
2526
suggestions = extract_suggestions(diagnostic.message)
2627

27-
with {:ok, module_aliases, name} <- extract_function(diagnostic.message),
28+
with {:ok, module_alias, name} <- extract_function(diagnostic.message),
2829
{:ok, replies} <-
29-
build_code_actions(source_file, one_based_line, module_aliases, name, suggestions) do
30+
build_code_actions(source_file, one_based_line, module_alias, name, suggestions) do
3031
replies
3132
else
3233
_ -> []
@@ -37,22 +38,22 @@ defmodule ElixirLS.LanguageServer.Experimental.Provider.CodeAction.ReplaceRemote
3738
defp extract_function(message) do
3839
case Regex.scan(@function_re, message) do
3940
[[_, full_name, _, _]] ->
40-
{module_aliases, name} = separate_module_from_name(full_name)
41-
{:ok, module_aliases, name}
41+
{module_alias, name} = separate_module_from_name(full_name)
42+
{:ok, module_alias, name}
4243

4344
_ ->
4445
:error
4546
end
4647
end
4748

4849
defp separate_module_from_name(full_name) do
49-
{name, module_aliases} =
50+
{name, module_alias} =
5051
full_name
5152
|> String.split(".")
5253
|> Enum.map(&String.to_atom/1)
5354
|> List.pop_at(-1)
5455

55-
{module_aliases, name}
56+
{module_alias, name}
5657
end
5758

5859
@suggestion_re ~r/\* .*\/[\d]+/
@@ -80,14 +81,16 @@ defmodule ElixirLS.LanguageServer.Experimental.Provider.CodeAction.ReplaceRemote
8081
defp build_code_actions(
8182
%SourceFile{} = source_file,
8283
one_based_line,
83-
module_aliases,
84+
module_alias,
8485
name,
8586
suggestions
8687
) do
8788
with {:ok, line_text} <- SourceFile.fetch_text_at(source_file, one_based_line),
8889
{:ok, line_ast} <- Ast.from(line_text),
90+
{:ok, possible_aliases} <-
91+
fetch_possible_aliases(source_file, one_based_line, module_alias),
8992
{:ok, edits_per_suggestion} <-
90-
text_edits_per_suggestion(line_text, line_ast, module_aliases, name, suggestions) do
93+
text_edits_per_suggestion(line_text, line_ast, possible_aliases, name, suggestions) do
9194
case edits_per_suggestion do
9295
[] ->
9396
:error
@@ -98,7 +101,7 @@ defmodule ElixirLS.LanguageServer.Experimental.Provider.CodeAction.ReplaceRemote
98101
text_edits = Enum.map(text_edits, &update_line(&1, one_based_line))
99102

100103
CodeActionResult.new(
101-
title: construct_title(module_aliases, suggestion),
104+
title: construct_title(module_alias, suggestion),
102105
kind: :quick_fix,
103106
edit: Workspace.Edit.new(changes: %{source_file.uri => text_edits})
104107
)
@@ -109,13 +112,42 @@ defmodule ElixirLS.LanguageServer.Experimental.Provider.CodeAction.ReplaceRemote
109112
end
110113
end
111114

112-
defp text_edits_per_suggestion(line_text, line_ast, module_aliases, name, suggestions) do
115+
# Extracted `ElixirSense.Core.State.Env` contains all reachable aliases as a list of tuples
116+
# `{alias, aliased}`. If `aliased` is a prefix of `module_alias`, the function to be replaced
117+
# may use the corresponding `alias`.
118+
defp fetch_possible_aliases(source_file, one_based_line, module_alias) do
119+
metadata =
120+
source_file
121+
|> SourceFile.to_string()
122+
|> Parser.parse_string(true, true, one_based_line)
123+
124+
case metadata.lines_to_env[one_based_line] do
125+
%ElixirSense.Core.State.Env{aliases: aliases} ->
126+
possible_aliases =
127+
Enum.flat_map(aliases, fn {_alias, aliased} ->
128+
aliased = aliased |> Module.split() |> Enum.map(&String.to_atom/1)
129+
130+
if aliased == Enum.take(module_alias, length(aliased)) do
131+
[Enum.drop(module_alias, length(aliased) - 1)]
132+
else
133+
[]
134+
end
135+
end)
136+
137+
{:ok, [module_alias | possible_aliases]}
138+
139+
_ ->
140+
:error
141+
end
142+
end
143+
144+
defp text_edits_per_suggestion(line_text, line_ast, possible_aliases, name, suggestions) do
113145
suggestions
114146
|> Enum.reduce_while([], fn suggestion, acc ->
115147
case CodeMod.ReplaceRemoteFunction.text_edits(
116148
line_text,
117149
line_ast,
118-
module_aliases,
150+
possible_aliases,
119151
name,
120152
suggestion
121153
) do
@@ -136,8 +168,8 @@ defmodule ElixirLS.LanguageServer.Experimental.Provider.CodeAction.ReplaceRemote
136168
|> put_in([:range, :end, :line], line_number - 1)
137169
end
138170

139-
defp construct_title(module_aliases, suggestion) do
140-
module_string = Enum.map_join(module_aliases, ".", &Atom.to_string/1)
171+
defp construct_title(module_alias, suggestion) do
172+
module_string = Enum.map_join(module_alias, ".", &Atom.to_string/1)
141173

142174
"Replace with #{module_string}.#{suggestion}"
143175
end

apps/language_server/test/experimental/provider/code_action/replace_remote_function_test.exs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ defmodule ElixirLS.LanguageServer.Experimental.Provider.CodeAction.ReplaceRemote
66
alias ElixirLS.LanguageServer.Experimental.Protocol.Types.CodeAction, as: CodeActionReply
77
alias ElixirLS.LanguageServer.Experimental.Protocol.Types.Diagnostic
88
alias ElixirLS.LanguageServer.Experimental.Protocol.Types.Range
9+
alias ElixirLS.LanguageServer.Experimental.Protocol.Types.Position
910
alias ElixirLS.LanguageServer.Experimental.Protocol.Types.TextEdit
1011
alias ElixirLS.LanguageServer.Experimental.Provider.CodeAction.ReplaceRemoteFunction
1112
alias ElixirLS.LanguageServer.Experimental.SourceFile
@@ -191,4 +192,66 @@ defmodule ElixirLS.LanguageServer.Experimental.Provider.CodeAction.ReplaceRemote
191192

192193
assert [] = apply(code_action)
193194
end
195+
196+
test "works with aliased modules" do
197+
diagnostic_message = """
198+
Example.A.B.my_fun/1 is undefined or private. Did you mean:
199+
200+
* my_func/1
201+
"""
202+
203+
code = ~S[
204+
defmodule Example do
205+
defmodule A.B do
206+
def my_func(a), do: a
207+
end
208+
209+
defmodule C do
210+
def my_fun(a), do: a
211+
end
212+
213+
defmodule D do
214+
alias Example.A
215+
alias Example.A.B
216+
alias Example.C
217+
def bar() do
218+
A.B.my_fun(42)
219+
C.my_fun(42) + B.my_fun(42)
220+
end
221+
end
222+
end
223+
]
224+
225+
# A.B.my_fun(42)
226+
{file_uri, code_action} =
227+
code_action(code, "/project/file.ex", 14, diagnostic_message: diagnostic_message)
228+
229+
assert [%CodeActionReply{edit: %{changes: %{^file_uri => edits}}}] = apply(code_action)
230+
231+
assert [
232+
%TextEdit{
233+
new_text: "c",
234+
range: %Range{
235+
end: %Position{character: 24, line: 14},
236+
start: %Position{character: 24, line: 14}
237+
}
238+
}
239+
] = edits
240+
241+
# B.my_fun(42)
242+
{file_uri, code_action} =
243+
code_action(code, "/project/file.ex", 15, diagnostic_message: diagnostic_message)
244+
245+
assert [%CodeActionReply{edit: %{changes: %{^file_uri => edits}}}] = apply(code_action)
246+
247+
assert [
248+
%TextEdit{
249+
new_text: "c",
250+
range: %Range{
251+
end: %Position{character: 37, line: 15},
252+
start: %Position{character: 37, line: 15}
253+
}
254+
}
255+
] = edits
256+
end
194257
end

0 commit comments

Comments
 (0)