|
| 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