defmodule Igniter.Code.Function do @moduledoc """ Utilities for working with functions. """ require Igniter.Code.Common alias Igniter.Code.Common alias Sourceror.Zipper @doc """ Returns `true` if the argument at the provided index exists and matches the provided pattern Note: to check for argument equality, use `argument_equals?/3` instead. """ defmacro argument_matches_pattern?(zipper, index, pattern) do quote do Igniter.Code.Function.argument_matches_predicate?( unquote(zipper), unquote(index), fn zipper -> match?(unquote(pattern), zipper.node) end ) end end @spec move_to_defp(Zipper.t(), fun :: atom, arity :: integer | list(integer)) :: {:ok, Zipper.t()} | :error def move_to_defp(zipper, fun, arity) do do_move_to_def(zipper, fun, arity, :defp) end @spec move_to_def(Zipper.t(), fun :: atom, arity :: integer | list(integer)) :: {:ok, Zipper.t()} | :error def move_to_def(zipper, fun, arity) do do_move_to_def(zipper, fun, arity, :def) end defp do_move_to_def(zipper, fun, [arity], kind) do do_move_to_def(zipper, fun, arity, kind) end defp do_move_to_def(zipper, fun, [arity | rest], kind) do case do_move_to_def(zipper, fun, arity, kind) do {:ok, zipper} -> {:ok, zipper} :error -> do_move_to_def(zipper, fun, rest, kind) end end defp do_move_to_def(zipper, fun, arity, kind) do case Common.move_to_pattern( zipper, {^kind, _, [{^fun, _, args}, _]} when length(args) == arity ) do :error -> if arity == 0 do case Common.move_to_pattern( zipper, {^kind, _, [{^fun, _, context}, _]} when is_atom(context) ) do :error -> :error {:ok, zipper} -> Common.move_to_do_block(zipper) end else :error end {:ok, zipper} -> Common.move_to_do_block(zipper) 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() | list(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 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 if function_call?(zipper, name, arity) && predicate.(zipper) do {:ok, zipper} else Common.move_right(zipper, fn zipper -> function_call?(zipper, name, arity) && predicate.(zipper) end) end end @doc "Moves to a function call by the given name and arity, matching the given predicate, in the current or lower scope" @spec move_to_function_call(Zipper.t(), atom, non_neg_integer()) :: {:ok, Zipper.t()} | :error def move_to_function_call(zipper, name, arity, predicate \\ fn _ -> true end) def move_to_function_call(zipper, name, [arity | arities], predicate) do case move_to_function_call(zipper, name, arity, predicate) do :error -> move_to_function_call(zipper, name, arities, predicate) {:ok, zipper} -> {:ok, zipper} end end def move_to_function_call(_, _, [], _) do :error end def move_to_function_call(%Zipper{} = zipper, name, arity, predicate) do if function_call?(zipper, name, arity) && predicate.(zipper) do {:ok, zipper} else Common.move_next(zipper, fn zipper -> function_call?(zipper, name, arity) && predicate.(zipper) end) 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_single_child_block() |> Zipper.node() |> 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 @doc "Returns `true` if the node is a function call of the given name" @spec function_call?(Zipper.t(), atom) :: boolean() def function_call?(%Zipper{} = zipper, name) do zipper |> Common.maybe_move_to_single_child_block() |> Zipper.node() |> 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 @doc "Returns `true` if the node is a function call" @spec function_call?(Zipper.t()) :: boolean() def function_call?(%Zipper{} = zipper) do zipper |> Common.maybe_move_to_single_child_block() |> Zipper.node() |> case do {:|>, _, [{name, _, context} | _rest]} when is_atom(context) and is_atom(name) -> true {:|>, _, [name | _rest]} when is_atom(name) -> true {name, _, _} when is_atom(name) -> true {{name, _, context}, _, _} when is_atom(context) and is_atom(name) -> 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 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 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 end else zipper |> Zipper.down() |> case do nil -> :error zipper -> zipper |> Common.nth_right(index) |> case do :error -> :error {:ok, nth} -> func.(nth) end end end end) end @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 zipper |> Zipper.down() |> case do nil -> :error zipper -> {:ok, 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} -> {:ok, nth} end end end end else zipper |> Zipper.down() |> case do nil -> :error zipper -> zipper |> Common.nth_right(index) |> case do :error -> :error {:ok, nth} -> {:ok, nth} end end end else :error 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 def append_argument(zipper, value) do if function_call?(zipper) do if pipeline?(zipper) do 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 """ Checks if the provided function call (in a Zipper) has an argument that equals `term` at `index`. """ @spec argument_equals?(Zipper.t(), integer(), any()) :: boolean() def argument_equals?(zipper, index, term) do if function_call?(zipper) do Igniter.Code.Function.argument_matches_predicate?( zipper, index, &Igniter.Code.Common.nodes_equal?(&1, term) ) else false 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_single_child_block() |> func.() end end end end else zipper |> Zipper.down() |> case do nil -> false zipper -> zipper |> Common.nth_right(index) |> case do :error -> false {:ok, zipper} -> zipper |> Common.maybe_move_to_single_child_block() |> func.() end end end else :error end end defp pipeline?(zipper) do case zipper.node do {:|>, _, _} -> true _ -> false end end end