Skip to content

Commit edb4308

Browse files
committed
Add extract_function/4 based on AST modification with Sourceror.Zipper
1 parent 4895dc1 commit edb4308

File tree

2 files changed

+424
-0
lines changed

2 files changed

+424
-0
lines changed
Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
defmodule ElixirLS.LanguageServer.Experimental.CodeMod.ExtractFunction do
2+
@moduledoc """
3+
Elixir refactoring functions.
4+
"""
5+
6+
alias Sourceror.Zipper, as: Z
7+
8+
@doc """
9+
Return zipper containing AST with extracted function.
10+
"""
11+
def extract_function(zipper, start_line, end_line, function_name) do
12+
{quoted, acc} = extract_lines(zipper, start_line, end_line, function_name)
13+
zipper = Z.zip(quoted)
14+
15+
declares = vars_declared(function_name, [], acc.lines) |> Enum.uniq()
16+
used = vars_used(function_name, [], acc.lines) |> Enum.uniq()
17+
args = Enum.map(used -- declares, fn var -> {var, [], nil} end)
18+
returns = declares |> Enum.filter(&(&1 in acc.vars))
19+
{zipper, extracted} = return_declared(zipper, returns, function_name, args, acc.lines)
20+
21+
enclosing = acc.def
22+
23+
zipper
24+
|> top_find(fn
25+
{:def, _meta, [{^enclosing, _, _}, _]} -> true
26+
_ -> false
27+
end)
28+
|> Z.insert_right(extracted)
29+
|> fix_block()
30+
|> Z.root()
31+
end
32+
33+
@doc """
34+
Return zipper containing AST for lines in the range from-to.
35+
"""
36+
def extract_lines(zipper, start_line, end_line, replace_with \\ nil) do
37+
remove_range(zipper, start_line, end_line, %{
38+
lines: [],
39+
def: nil,
40+
def_end: nil,
41+
vars: [],
42+
replace_with: replace_with
43+
})
44+
end
45+
46+
defp next_remove_range(zipper, from, to, acc) do
47+
if next = Z.next(zipper) do
48+
remove_range(next, from, to, acc)
49+
else
50+
# return zipper with lines removed
51+
{
52+
elem(Z.top(zipper), 0),
53+
%{acc | lines: Enum.reverse(acc.lines), vars: Enum.reverse(acc.vars)}
54+
}
55+
end
56+
end
57+
58+
defp remove_range({{:def, meta, [{marker, _, _}, _]}, _list} = zipper, from, to, acc) do
59+
acc =
60+
if meta[:line] < from do
61+
x = put_in(acc.def, marker)
62+
put_in(x.def_end, meta[:end][:line])
63+
else
64+
acc
65+
end
66+
67+
next_remove_range(zipper, from, to, acc)
68+
end
69+
70+
defp remove_range({{marker, meta, children}, _list} = zipper, from, to, acc) do
71+
if meta[:line] < from || meta[:line] > to || marker == :__block__ do
72+
next_remove_range(
73+
zipper,
74+
from,
75+
to,
76+
if meta[:line] > to && meta[:line] < acc.def_end && is_atom(marker) && is_nil(children) do
77+
put_in(acc.vars, [marker | acc.vars] |> Enum.uniq())
78+
else
79+
acc
80+
end
81+
)
82+
else
83+
acc = put_in(acc.lines, [Z.node(zipper) | acc.lines])
84+
85+
if is_nil(acc.replace_with) do
86+
zipper
87+
|> Z.remove()
88+
|> next_remove_range(from, to, acc)
89+
else
90+
function_name = acc.replace_with
91+
acc = put_in(acc.replace_with, nil)
92+
93+
zipper
94+
|> Z.replace({function_name, [], []})
95+
|> next_remove_range(from, to, acc)
96+
end
97+
end
98+
end
99+
100+
defp remove_range(zipper, from, to, acc) do
101+
next_remove_range(zipper, from, to, acc)
102+
end
103+
104+
defp vars_declared(function_name, args, lines) do
105+
function_name
106+
|> new_function(args, lines)
107+
|> Z.zip()
108+
|> vars_declared(%{vars: []})
109+
end
110+
111+
defp vars_declared(nil, acc) do
112+
Enum.reverse(acc.vars)
113+
end
114+
115+
defp vars_declared({{:=, _, [{var, _, nil}, _]}, _rest} = zipper, acc) when is_atom(var) do
116+
zipper
117+
|> Z.next()
118+
|> vars_declared(put_in(acc.vars, [var | acc.vars]))
119+
end
120+
121+
defp vars_declared(zipper, acc) do
122+
zipper
123+
|> Z.next()
124+
|> vars_declared(acc)
125+
end
126+
127+
defp vars_used(function_name, args, lines) do
128+
function_name
129+
|> new_function(args, lines)
130+
|> Z.zip()
131+
|> vars_used(%{vars: []})
132+
end
133+
134+
defp vars_used(nil, acc) do
135+
Enum.reverse(acc.vars)
136+
end
137+
138+
defp vars_used({{marker, _meta, nil}, _rest} = zipper, acc) when is_atom(marker) do
139+
zipper
140+
|> Z.next()
141+
|> vars_used(put_in(acc.vars, [marker | acc.vars]))
142+
end
143+
144+
defp vars_used(zipper, acc) do
145+
zipper
146+
|> Z.next()
147+
|> vars_used(acc)
148+
end
149+
150+
defp return_declared(zipper, nil = _declares, function_name, args, lines) do
151+
{zipper, new_function(function_name, args, lines)}
152+
end
153+
154+
defp return_declared(zipper, [var], function_name, args, lines) when is_atom(var) do
155+
zipper =
156+
zipper
157+
|> top_find(fn
158+
{^function_name, [], []} -> true
159+
_ -> false
160+
end)
161+
|> Z.replace({:=, [], [{var, [], nil}, {function_name, [], args}]})
162+
163+
{zipper, new_function(function_name, args, Enum.concat(lines, [{var, [], nil}]))}
164+
end
165+
166+
defp return_declared(zipper, declares, function_name, args, lines) when is_list(declares) do
167+
declares = Enum.reduce(declares, {}, fn var, acc -> Tuple.append(acc, {var, [], nil}) end)
168+
169+
zipper =
170+
zipper
171+
|> top_find(fn
172+
{^function_name, [], []} -> true
173+
_ -> false
174+
end)
175+
|> Z.replace(
176+
{:=, [],
177+
[
178+
{:__block__, [],
179+
[
180+
declares
181+
]},
182+
{function_name, [], args}
183+
]}
184+
)
185+
186+
{zipper,
187+
new_function(
188+
function_name,
189+
args,
190+
Enum.concat(lines, [
191+
{:__block__, [],
192+
[
193+
declares
194+
]}
195+
])
196+
)}
197+
end
198+
199+
defp new_function(function_name, args, lines) do
200+
{:def, [do: [], end: []],
201+
[
202+
{function_name, [], args},
203+
[
204+
{
205+
{:__block__, [], [:do]},
206+
{:__block__, [], lines}
207+
}
208+
]
209+
]}
210+
end
211+
212+
defp fix_block(zipper) do
213+
zipper
214+
|> top_find(fn
215+
{:{}, [], _children} -> true
216+
_ -> false
217+
end)
218+
|> case do
219+
nil ->
220+
zipper
221+
222+
{{:{}, [], [block | defs]}, meta} ->
223+
{
224+
{
225+
block,
226+
{:__block__, [], defs}
227+
},
228+
meta
229+
}
230+
end
231+
end
232+
233+
defp top_find(zipper, function) do
234+
zipper
235+
|> Z.top()
236+
|> Z.find(function)
237+
end
238+
end

0 commit comments

Comments
 (0)