igniter/lib/code/function.ex

372 lines
8.8 KiB
Elixir
Raw Normal View History

2024-06-13 10:22:08 +12:00
defmodule Igniter.Code.Function do
@moduledoc """
Utilities for working with functions.
"""
2024-06-13 11:16:03 +12:00
2024-06-13 10:22:08 +12:00
alias Igniter.Code.Common
2024-06-13 11:16:03 +12:00
alias Sourceror.Zipper
2024-06-13 10:22:08 +12:00
@doc "Returns `true` if the argument at the provided index exists and matches the provided pattern"
defmacro argument_matches_pattern?(zipper, index, pattern) do
quote do
Igniter.Code.Function.argument_matches_predicate?(
unquote(zipper),
unquote(index),
fn zipper ->
code_at_node =
zipper
|> Sourceror.Zipper.subtree()
|> Sourceror.Zipper.root()
match?(unquote(pattern), code_at_node)
end
)
end
end
@doc "Moves to a function call by the given name and arity, matching the given predicate, in the current scope"
@spec move_to_function_call_in_current_scope(Zipper.t(), atom, non_neg_integer()) ::
{:ok, Zipper.t()} | :error
def move_to_function_call_in_current_scope(zipper, name, arity, predicate \\ fn _ -> true end)
def move_to_function_call_in_current_scope(zipper, name, [arity | arities], predicate) do
2024-06-13 10:22:08 +12:00
case move_to_function_call_in_current_scope(zipper, name, arity, predicate) do
:error ->
move_to_function_call_in_current_scope(zipper, name, arities, predicate)
{:ok, zipper} ->
{:ok, zipper}
end
end
def move_to_function_call_in_current_scope(_, _, [], _) do
:error
end
def move_to_function_call_in_current_scope(%Zipper{} = zipper, name, arity, predicate) do
zipper
|> Common.maybe_move_to_block()
|> Common.move_right(fn zipper ->
function_call?(zipper, name, arity) && predicate.(zipper)
end)
end
@doc "Returns `true` if the node is a function call of the given name and arity"
@spec function_call?(Zipper.t(), atom, non_neg_integer()) :: boolean()
def function_call?(%Zipper{} = zipper, name, arity) do
zipper
|> Common.maybe_move_to_block()
|> 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
2024-06-13 12:11:41 +12:00
@doc "Returns `true` if the node is a function call of the given name"
2024-06-13 10:22:08 +12:00
@spec function_call?(Zipper.t(), atom) :: boolean()
def function_call?(%Zipper{} = zipper, name) do
zipper
|> Common.maybe_move_to_block()
|> Zipper.subtree()
|> Zipper.root()
|> case do
{^name, _, _} ->
true
{{^name, _, context}, _, _} when is_atom(context) ->
true
{:|>, _, [{^name, _, context} | _rest]} when is_atom(context) ->
true
{:|>, _, [^name | _rest]} ->
true
_ ->
false
end
end
2024-06-13 12:11:41 +12:00
@doc "Returns `true` if the node is a function call"
2024-06-13 10:22:08 +12:00
@spec function_call?(Zipper.t()) :: boolean()
def function_call?(%Zipper{} = zipper) do
zipper
|> Common.maybe_move_to_block()
|> Zipper.subtree()
|> Zipper.root()
|> case do
2024-06-13 11:16:03 +12:00
{:|>, _, [{name, _, context} | _rest]} when is_atom(context) and is_atom(name) ->
2024-06-13 10:22:08 +12:00
true
2024-06-13 11:16:03 +12:00
{:|>, _, [name | _rest]} when is_atom(name) ->
2024-06-13 10:22:08 +12:00
true
2024-06-13 11:16:03 +12:00
{name, _, _} when is_atom(name) ->
2024-06-13 10:22:08 +12:00
true
2024-06-13 11:16:03 +12:00
{{name, _, context}, _, _} when is_atom(context) and is_atom(name) ->
2024-06-13 10:22:08 +12:00
true
_ ->
false
end
end
@doc "Updates the `nth` argument of a function call, leaving the zipper at the function call's node."
@spec update_nth_argument(
Zipper.t(),
non_neg_integer(),
(Zipper.t() ->
{:ok, Zipper.t()} | :error)
) ::
{:ok, Zipper.t()} | :error
2024-06-13 10:22:08 +12:00
def update_nth_argument(zipper, index, func) do
Common.within(zipper, fn zipper ->
if pipeline?(zipper) do
if index == 0 do
zipper
|> Zipper.down()
|> case do
nil ->
:error
2024-06-13 10:22:08 +12:00
zipper ->
func.(zipper)
end
else
zipper
|> Zipper.down()
|> case do
nil ->
:error
zipper ->
zipper
|> Zipper.rightmost()
|> Zipper.down()
|> case do
nil ->
:error
zipper ->
zipper
|> Common.nth_right(index)
|> case do
:error ->
:error
{:ok, nth} ->
func.(nth)
end
end
end
2024-06-13 10:22:08 +12:00
end
else
zipper
|> Zipper.down()
|> case do
nil ->
:error
zipper ->
zipper
|> Common.nth_right(index)
2024-06-13 10:22:08 +12:00
|> case do
:error ->
2024-06-13 10:22:08 +12:00
:error
{:ok, nth} ->
func.(nth)
2024-06-13 10:22:08 +12:00
end
end
end
end)
end
2024-06-13 10:22:08 +12:00
@doc "Moves to the `nth` argument of a function call."
@spec move_to_nth_argument(
Zipper.t(),
non_neg_integer()
) ::
{:ok, Zipper.t()} | :error
def move_to_nth_argument(zipper, index) do
if function_call?(zipper) do
if pipeline?(zipper) do
if index == 0 do
2024-06-13 10:22:08 +12:00
zipper
|> Zipper.down()
2024-06-13 10:22:08 +12:00
|> case do
nil ->
2024-06-13 10:22:08 +12:00
:error
zipper ->
{:ok, zipper}
2024-06-13 10:22:08 +12:00
end
else
zipper
|> Zipper.down()
|> case do
nil ->
:error
2024-06-13 10:22:08 +12:00
zipper ->
zipper
|> Zipper.rightmost()
|> Zipper.down()
|> case do
nil ->
:error
zipper ->
zipper
|> Common.nth_right(index)
|> case do
:error ->
:error
{:ok, nth} ->
{:ok, nth}
end
end
end
2024-06-13 10:22:08 +12:00
end
else
zipper
|> Zipper.down()
|> case do
nil ->
:error
zipper ->
zipper
|> Common.nth_right(index)
2024-06-13 10:22:08 +12:00
|> case do
:error ->
2024-06-13 10:22:08 +12:00
:error
{:ok, nth} ->
{:ok, nth}
2024-06-13 10:22:08 +12:00
end
end
end
else
:error
2024-06-13 10:22:08 +12:00
end
end
@doc "Appends an argument to a function call, leaving the zipper at the function call's node."
@spec append_argument(Zipper.t(), any()) :: {:ok, Zipper.t()} | :error
2024-06-13 10:22:08 +12:00
def append_argument(zipper, value) do
if function_call?(zipper) do
if pipeline?(zipper) do
2024-06-13 10:22:08 +12:00
zipper
|> Zipper.down()
|> case do
nil ->
:error
zipper ->
{:ok, Zipper.append_child(zipper, value)}
end
else
{:ok, Zipper.append_child(zipper, value)}
end
else
:error
end
end
@doc "Returns true if the argument at the given index matches the provided predicate"
@spec argument_matches_predicate?(Zipper.t(), non_neg_integer(), (Zipper.t() -> boolean)) ::
boolean()
def argument_matches_predicate?(zipper, index, func) do
if function_call?(zipper) do
if 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
|> Common.nth_right(index - 1)
|> case do
:error ->
false
{:ok, zipper} ->
zipper
|> Common.maybe_move_to_block()
|> func.()
end
end
end
2024-06-13 10:22:08 +12:00
end
else
zipper
|> Zipper.down()
|> case do
nil ->
false
2024-06-13 10:22:08 +12:00
zipper ->
zipper
|> Common.nth_right(index)
2024-06-13 10:22:08 +12:00
|> case do
:error ->
false
2024-06-13 10:22:08 +12:00
{:ok, zipper} ->
2024-06-13 10:22:08 +12:00
zipper
|> Common.maybe_move_to_block()
|> func.()
2024-06-13 10:22:08 +12:00
end
end
end
else
:error
2024-06-13 10:22:08 +12:00
end
end
defp pipeline?(zipper) do
2024-06-13 10:22:08 +12:00
zipper
|> Zipper.subtree()
|> Zipper.root()
|> case do
{:|>, _, _} -> true
_ -> false
end
end
end