igniter/lib/common.ex

581 lines
12 KiB
Elixir
Raw Normal View History

2024-05-28 15:30:41 +12:00
defmodule Igniter.Common do
alias Sourceror.Zipper
def find(zipper, direction \\ :next, pred) do
Zipper.find(zipper, direction, fn thing ->
try do
pred.(thing)
rescue
FunctionClauseError ->
false
end
end)
end
defmacro node_matches_pattern?(zipper, pattern) do
quote do
ast =
unquote(zipper)
|> Igniter.Common.maybe_enter_block()
|> Zipper.subtree()
|> Zipper.root()
match?(unquote(pattern), ast)
end
end
defmacro find_pattern(zipper, direction \\ :next, pattern) do
quote do
Sourceror.Zipper.find(unquote(zipper), unquote(direction), fn
unquote(pattern) ->
true
_ ->
false
end)
end
end
defmacro argument_matches_pattern?(zipper, index, pattern) do
quote do
Igniter.Common.argument_matches_predicate?(
unquote(zipper),
unquote(index),
&match?(unquote(pattern), &1)
)
end
end
def puts_code_at_node(zipper) do
zipper
|> Zipper.subtree()
|> Zipper.root()
|> Sourceror.to_string()
|> IO.puts()
zipper
end
def add_code(zipper, new_code) do
current_code =
zipper
|> Zipper.subtree()
|> Zipper.root()
|> IO.inspect()
case current_code do
{:__block__, block_meta, stuff} ->
Zipper.replace(zipper, {:__block__, block_meta, stuff ++ [new_code]})
code ->
Zipper.replace(zipper, {:__block__, [], [code, new_code]})
end
end
def put_in_keyword(zipper, path, value, updater \\ nil) do
updater = updater || fn _ -> value end
do_put_in_keyword(zipper, path, value, updater)
end
defp do_put_in_keyword(zipper, [key], value, updater) do
set_keyword_key(zipper, key, value, updater)
end
defp do_put_in_keyword(zipper, [key | rest], value, updater) do
if node_matches_pattern?(zipper, value when is_list(value)) do
case find_list_item(zipper, fn item ->
if is_tuple?(item) do
first_elem = tuple_elem(item, 0)
first_elem && node_matches_pattern?(first_elem, ^key)
end
end) do
nil ->
value = keywordify(rest, value)
prepend_to_list(
zipper,
{{:__block__, [format: :keyword], [key]}, {:__block__, [], [value]}}
)
zipper ->
zipper
|> tuple_elem(1)
|> case do
nil ->
nil
zipper ->
do_put_in_keyword(zipper, rest, value, updater)
end
end
end
end
def set_keyword_key(zipper, key, value, updater) do
if node_matches_pattern?(zipper, value when is_list(value)) do
case find_list_item(zipper, fn item ->
if is_tuple?(item) do
first_elem = tuple_elem(item, 0)
first_elem && node_matches_pattern?(first_elem, ^key)
end
end) do
nil ->
prepend_to_list(
zipper,
{{:__block__, [format: :keyword], [key]}, {:__block__, [], [value]}}
)
zipper ->
zipper
|> tuple_elem(1)
|> case do
nil ->
nil
zipper ->
updater.(zipper)
end
end
end
end
def find_function_call_in_current_scope(zipper, name, arity, predicate \\ fn _ -> true end) do
case Zipper.down(zipper) do
nil ->
nil
zipper ->
find_right(zipper, fn zipper ->
is_function_call(zipper, name, arity) && predicate.(zipper)
end)
end
end
def is_function_call(zipper, name, arity) do
zipper
|> Zipper.subtree()
|> Zipper.root()
|> case do
{^name, _, args} ->
Enum.count(args) == arity
{{^name, _, context}, _, args} when is_atom(context) ->
Enum.count(args) == arity
{:|>, _, [{^name, _, context} | rest]} when is_atom(context) ->
Enum.count(rest) == arity - 1
{:|>, _, [^name | rest]} ->
Enum.count(rest) == arity - 1
_ ->
false
end
end
def update_nth_argument(zipper, index, func) do
if is_pipeline?(zipper) do
if index == 0 do
zipper
|> Zipper.down()
|> case do
nil ->
nil
zipper ->
func.(zipper)
end
else
zipper
|> Zipper.down()
|> case do
nil ->
nil
zipper ->
zipper
|> Zipper.rightmost()
|> Zipper.down()
|> case do
nil ->
nil
zipper ->
zipper
|> nth_right(index)
|> case do
nil ->
nil
nth ->
func.(nth)
end
end
end
end
else
zipper
|> Zipper.down()
|> case do
nil ->
nil
zipper ->
zipper
|> nth_right(index)
|> case do
nil ->
nil
nth ->
func.(nth)
end
end
end
end
def argument_matches_predicate?(zipper, index, func) do
if is_pipeline?(zipper) do
if index == 0 do
zipper
|> Zipper.down()
|> case do
nil -> nil
zipper -> func.(zipper)
end
else
zipper
|> Zipper.down()
|> case do
nil ->
nil
zipper ->
zipper
|> Zipper.rightmost()
|> Zipper.down()
|> case do
nil ->
nil
zipper ->
zipper
|> nth_right(index - 1)
|> maybe_enter_block()
|> Zipper.subtree()
|> Zipper.root()
|> func.()
end
end
end
else
zipper
|> Zipper.down()
|> case do
nil ->
false
zipper ->
zipper
|> nth_right(index)
|> maybe_enter_block()
|> Zipper.subtree()
|> Zipper.root()
|> func.()
end
end
end
def is_pipeline?(zipper) do
zipper
|> Zipper.subtree()
|> Zipper.root()
|> case do
{:|>, _, _} -> true
_ -> false
end
end
def move_to_module_using(zipper, module) do
split_module =
module
|> Module.split()
|> Enum.map(&String.to_atom/1)
with zipper when not is_nil(zipper) <- find_pattern(zipper, {:defmodule, _, [_, _]}),
subtree <- Zipper.subtree(zipper),
subtree <- subtree |> Zipper.down() |> Zipper.rightmost(),
subtree <- remove_module_definitions(subtree),
found when not is_nil(found) <-
find(subtree, fn
{:use, _, [^module]} ->
true
{:use, _, [{:__aliases__, _, ^split_module}]} ->
true
end),
{:ok, zipper} <- move_to_do_block(zipper) do
{:ok, zipper}
else
_ ->
:error
end
end
# aliases will confuse this, but that is a later problem :)
def equal_modules?(zipper, module) do
root =
zipper
|> Zipper.subtree()
|> Zipper.root()
do_equal_modules?(root, module)
end
defp do_equal_modules?(left, left), do: true
defp do_equal_modules?({:__aliases__, _, mod}, {:__aliases__, _, mod}), do: true
defp do_equal_modules?({:__aliases__, _, mod}, right) when is_atom(right) do
Module.concat(mod) == right
end
defp do_equal_modules?(left, {:__aliases__, _, mod}) when is_atom(left) do
Module.concat(mod) == left
end
defp do_equal_modules?(_, _), do: false
def move_to_defp(zipper, fun, arity) do
case find_pattern(zipper, {:defp, _, [{^fun, _, args}, _]} when length(args) == arity) do
nil ->
if arity == 0 do
case find_pattern(zipper, {:defp, _, [{^fun, _, context}, _]} when is_atom(context)) do
nil ->
:error
zipper ->
move_to_do_block(zipper)
end
else
:error
end
zipper ->
move_to_do_block(zipper)
end
end
def move_to_do_block(zipper) do
case find_pattern(zipper, {{:__block__, _, [:do]}, _}) do
nil ->
:error
zipper ->
{:ok,
zipper
|> Zipper.down()
|> Zipper.rightmost()
|> maybe_enter_block()}
end
end
def maybe_enter_block(nil), do: nil
def maybe_enter_block(zipper) do
zipper
|> Zipper.subtree()
|> Zipper.root()
|> case do
{:__block__, _, [_]} ->
Zipper.down(zipper)
_ ->
zipper
end
end
def remove_module_definitions(zipper) do
Sourceror.Zipper.traverse(zipper, fn
{:defmodule, _, _} ->
nil
other ->
other
end)
end
def prepend_new_to_list(zipper, quoted, equality_pred \\ &default_equality_pred/2) do
zipper
|> find_list_item_index(fn value ->
equality_pred.(value, quoted)
end)
|> case do
nil ->
zipper
|> maybe_enter_block()
|> Zipper.insert_child(quoted)
_ ->
zipper
end
end
defp default_equality_pred(zipper, quoted) do
zipper
|> Zipper.subtree()
|> Zipper.root()
|> Kernel.==(quoted)
end
def prepend_to_list(zipper, quoted) do
zipper
|> maybe_enter_block()
|> Zipper.insert_child(quoted)
end
def remove_index(zipper, index) do
zipper
|> maybe_enter_block()
|> Zipper.down()
|> case do
nil ->
zipper
zipper ->
zipper
|> do_remove_index(index)
end
end
defp do_remove_index(zipper, 0) do
Zipper.remove(zipper)
end
defp do_remove_index(zipper, i) do
zipper
|> Zipper.right()
|> case do
nil ->
zipper
zipper ->
zipper
|> do_remove_index(i - 1)
end
end
defp nth_right(zipper, 0) do
zipper
end
defp nth_right(zipper, n) do
zipper
|> Zipper.right()
|> case do
nil ->
nil
zipper ->
nth_right(zipper, n - 1)
end
end
def find_list_item_index(zipper, pred) do
# go into first list item
zipper
|> maybe_enter_block()
|> Zipper.down()
|> case do
nil ->
nil
zipper ->
find_index_right(zipper, pred, 0)
end
end
def find_list_item(zipper, pred) do
# go into first list item
zipper
|> maybe_enter_block()
|> Zipper.down()
|> case do
nil ->
nil
zipper ->
find_right(zipper, pred)
end
end
def is_tuple?(item) do
item
|> Zipper.subtree()
|> Zipper.root()
|> case do
{:{}, _, _} -> true
{_, _} -> true
_ -> false
end
end
def tuple_elem(item, elem) do
item
|> maybe_enter_block()
|> Zipper.down()
|> go_right_n_times(elem)
|> maybe_enter_block()
end
defp go_right_n_times(zipper, 0), do: maybe_enter_block(zipper)
defp go_right_n_times(zipper, n) do
zipper
|> Zipper.right()
|> case do
nil -> nil
zipper -> go_right_n_times(zipper, n - 1)
end
end
defp find_index_right(zipper, pred, index) do
if pred.(maybe_enter_block(zipper)) do
index
else
case Zipper.right(zipper) do
nil ->
nil
zipper ->
zipper
|> find_index_right(pred, index + 1)
end
end
end
defp find_right(zipper, pred) do
if pred.(maybe_enter_block(zipper)) do
zipper
else
case Zipper.right(zipper) do
nil ->
nil
zipper ->
zipper
|> find_right(pred)
end
end
end
@doc false
def keywordify([], value) do
value
end
def keywordify([key | rest], value) do
[{key, keywordify(rest, value)}]
end
end