Current section

Files

Jump to
igniter lib igniter code function.ex
Raw

lib/igniter/code/function.ex

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), Keyword.t()) ::
{:ok, Zipper.t()} | :error
def move_to_defp(zipper, fun, arity, opts \\ []) do
opts = Keyword.put(opts, :target, Keyword.get(opts, :target, :inside))
do_move_to_def(zipper, fun, arity, :defp, opts)
end
@spec move_to_def(Zipper.t(), Keyword.t()) :: {:ok, Zipper.t()} | :error
def move_to_def(zipper, opts \\ []) do
opts = Keyword.put(opts, :target, Keyword.get(opts, :target, :at))
case move_to_function_call(zipper, :def, :any) do
{:ok, zipper} ->
move_to_target(zipper, Keyword.get(opts, :target, :inside))
:error ->
:error
end
end
@doc """
Moves the zipper to a function definition by the given name and arity. You may
also pass in a :target option to specify where in the function you want to
move to. By default it will move to the inside of the function.
The `:target` option can be one of the following:
- `:inside` - moves to the inside of the function
- `:before` - moves to before the function and takes into consideration the
attributes `@doc`, `@spec`, and `@impl` if they exist
- `:at` - moves to the function definition itself. Use this if you want to add
code directly before or directly after the function.
## Example - Moves before the function.
```elixir
zipper =
\"\"\"
defmodule Test do
@doc "hello"
@spec hello() :: :world
def hello() do
:world
end
end
\"\"\"
|> Sourceror.parse_string!()
|> Zipper.zip()
{:ok, zipper} = Igniter.Code.Function.move_to_function_and_attrs(zipper, :hello, 0)
zipper =
Igniter.Code.Common.add_code(
zipper,
\"\"\"
def world() do
:hello
end
\"\"\",
placement: :before
)
Igniter.Util.Debug.code_at_node(Zipper.topmost(zipper))
# defmodule Test do
# def world() do
# :hello
# end
#
# @doc "hello"
# @spec hello() :: :world
# def hello() do
# :world
# end
# end
```
"""
@spec move_to_def(Zipper.t(), fun :: atom, arity :: integer | list(integer) | :any, Keyword.t()) ::
{:ok, Zipper.t()} | :error
def move_to_def(zipper, fun, arity, opts \\ []) do
opts = Keyword.put(opts, :target, Keyword.get(opts, :target, :inside))
do_move_to_def(zipper, fun, arity, :def, opts)
end
defp do_move_to_def(zipper, fun, [arity], kind, opts) do
do_move_to_def(zipper, fun, arity, kind, opts)
end
defp do_move_to_def(zipper, fun, [arity | rest], kind, opts) do
case do_move_to_def(zipper, fun, arity, kind, opts) do
{:ok, zipper} -> {:ok, zipper}
:error -> do_move_to_def(zipper, fun, rest, kind, opts)
end
end
defp do_move_to_def(zipper, fun, arity, kind, opts) do
target = Keyword.get(opts, :target)
case Common.move_to(zipper, fn zipper ->
case Zipper.node(zipper) do
# Match the standard function definition
{^kind, _, [{^fun, _, args}, _body]} when arity == :any or length(args) == arity ->
true
# Match a zero-arity function that is defined without parentheses
{^kind, _, [{^fun, _, nil}, __body]} when arity == :any or arity == 0 ->
true
# Match a function with a guard clause
{^kind, _, [{:when, _, [{^fun, _, args}, _guard]}, _body]}
when arity == :any or length(args) == arity ->
true
# Probably not a common occurrence, but it is possible to have a
# function with a guard clause and no args
{^kind, _, [{:when, _, [{^fun, _, nil}, _guard]}, _body]}
when arity == :any or arity == 0 ->
true
_ ->
false
end
end) do
{:ok, zipper} ->
move_to_target(zipper, target)
:error ->
:error
end
end
defp move_to_target(zipper, target) do
case target do
:inside ->
Common.move_to_do_block(zipper)
:before ->
move_before_attrs(zipper)
:at ->
{:ok, zipper}
_ ->
:error
end
end
defp move_before_attrs(zipper) do
current_node = Zipper.node(zipper)
case Common.move_left(zipper, fn z ->
if z.path[:left] == [] || z.path[:left] == nil do
true
else
!match_function_or_attr?(z, current_node)
end
end) do
{:ok, zipper} = resp ->
# We need to match here to see if we are at the leftmost node and
# still match the function or attr. This happens when a function
# being matched on is the first thing in the module.
if match_function_or_attr?(zipper, current_node) do
resp
else
# Otherwise, we need to move right to the function or attr
Common.move_right(zipper, 1)
end
:error ->
:error
end
end
defp match_function_or_attr?(zipper, current_node) do
case Zipper.node(zipper) do
^current_node ->
true
{:@, _, [{:doc, _, _}]} ->
true
{:@, _, [{:spec, _, _}]} ->
true
{:@, _, [{:impl, _, _}]} ->
true
_ ->
false
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 | {atom, atom},
non_neg_integer() | list(non_neg_integer()) | :any
) ::
{: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 | {atom, atom},
:any | non_neg_integer() | list(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
If an `atom` is provided, it only matches functions in the form of `function(name)`.
If an `{module, atom}` is provided, it matches functions called on the given module,
taking into account any imports or aliases.
"""
@spec function_call?(Zipper.t(), atom | {module, atom}, arity :: integer | :any | list(integer)) ::
boolean()
def function_call?(zipper, name, arity \\ :any)
def function_call?(%Zipper{} = zipper, name, arity) when is_list(arity) do
Enum.any?(arity, &function_call?(zipper, name, &1))
end
def function_call?(%Zipper{} = zipper, name, arity) when is_atom(name) do
zipper
|> Common.maybe_move_to_single_child_block()
|> Zipper.node()
|> case do
{^name, _, args} when arity == :any or length(args) == arity ->
true
{{^name, _, context}, _, args}
when is_atom(context) and (arity == :any or length(args) == arity) ->
true
{:|>, _, [_, {{^name, _, context}, _, rest}]}
when is_atom(context) and (arity == :any or length(rest) == arity - 1) ->
true
{:|>, _, [_, {^name, _, rest}]}
when arity == :any or length(rest) == arity - 1 ->
true
_ ->
false
end
end
def function_call?(%Zipper{} = zipper, {module, name}, arity) when is_atom(name) do
node =
zipper
|> Common.maybe_move_to_single_child_block()
|> Zipper.node()
function_call_shape? =
case node do
{{:., _, [{:__aliases__, _, _} = alias, ^name]}, _, args}
when arity == :any or length(args) == arity ->
Common.nodes_equal?(Zipper.replace(zipper, alias), module)
{{:., _, [{:__aliases__, _, _} = alias, {^name, _, context}]}, _, args}
when is_atom(context) and (arity == :any or length(args) == arity) ->
Common.nodes_equal?(Zipper.replace(zipper, alias), module)
{:|>, _,
[
_,
{{:., _, [{:__aliases__, _, _} = alias, ^name]}, _, args}
]}
when arity == :any or length(args) == arity - 1 ->
Common.nodes_equal?(Zipper.replace(zipper, alias), module)
{:|>, _,
[
_,
{{:., _, [{:__aliases__, _, _} = alias, {^name, _, context}]}, _, args}
]}
when is_atom(context) and (arity == :any or length(args) == arity - 1) ->
Common.nodes_equal?(Zipper.replace(zipper, alias), module)
{{:., _, [^module, ^name]}, _, args}
when arity == :any or length(args) == arity ->
true
{{:., _, [^module, {^name, _, context}]}, _, args}
when is_atom(context) and (arity == :any or length(args) == arity) ->
true
{:|>, _,
[
_,
{{:., _, [^module, ^name]}, _, args}
]}
when arity == :any or length(args) == arity - 1 ->
true
{:|>, _,
[
_,
{{:., _, [^module, {^name, _, context}]}, _, args}
]}
when is_atom(context) and (arity == :any or length(args) == arity - 1) ->
true
{{:., _, [{:__block__, _, [^module]}, ^name]}, _, args}
when arity == :any or length(args) == arity ->
true
{{:., _, [{:__block__, _, [^module]}, {^name, _, context}]}, _, args}
when is_atom(context) and (arity == :any or length(args) == arity) ->
true
{:|>, _,
[
_,
{{:., _, [{:__block__, _, [^module]}, ^name]}, _, args}
]}
when arity == :any or length(args) == arity - 1 ->
true
{:|>, _,
[
_,
{{:., _, [{:__block__, _, [^module]}, {^name, _, context}]}, _, args}
]}
when is_atom(context) and (arity == :any or length(args) == arity - 1) ->
true
{^name, _, args} when is_list(args) and (arity == :any or length(args) == arity) ->
imported?(zipper, module, name, length(args))
{{^name, _, context}, _, args}
when is_atom(context) and (arity == :any or length(args) == arity) ->
imported?(zipper, module, name, length(args))
{:|>, _, [_, {{^name, _, context}, _, rest}]}
when is_atom(context) and (arity == :any or length(rest) == arity - 1) ->
imported?(zipper, module, name, length(rest) + 1)
{:|>, _, [_, {^name, _, rest}]}
when arity == :any or length(rest) == arity - 1 ->
imported?(zipper, module, name, length(rest) + 1)
_node ->
false
end
if function_call_shape? do
case Zipper.up(zipper) do
%{node: {:&, _, _}} ->
false
_ ->
true
end
else
false
end
end
@doc false
def imported?(zipper, module, name, arity) do
case Igniter.Code.Common.current_env(zipper) do
{:ok, env} ->
Enum.any?(env.functions ++ env.macros, fn {imported_module, funcs} ->
imported_module == module &&
Enum.any?(funcs, fn {imported_name, imported_arity} ->
name == imported_name && (arity == :any || imported_arity == arity)
end)
end)
_ ->
module == Kernel
end
end
@doc """
Returns true if the value is a function literal.
Examples:
- `fn x -> x end`
- `&(&1 + &2)`
- `&SomeMod.fun/2`
To refine the check, you can use `name` and `arity`.
## Names
- `:any` - matches any function literal, named or not
- `:any_named` - matches any named function literal
- `:anonymous` - matches any anonymous function literal
- `{module, name}` - matches a function literal with the given module and name
"""
@spec function?(
Zipper.t(),
name :: :any | :any_named | {module(), atom()} | :anonymous,
arity :: :any | non_neg_integer() | [non_neg_integer()]
) ::
boolean
def function?(zipper, name \\ :any, arity \\ :any)
def function?(zipper, name, arity) when is_list(arity) do
Enum.any?(arity, fn arity -> function?(zipper, name, arity) end)
end
def function?(%Zipper{}, name, _arity)
when is_atom(name) and name not in [:any, :any_named, :anonymous] do
raise ArgumentError,
"The name argument must be one of `:any`, `:any_named`, `:anonymous` or a `{module, name}` tuple."
end
def function?(%Zipper{} = zipper, :anonymous, arity) do
node =
zipper
|> Common.maybe_move_to_single_child_block()
|> Zipper.node()
case node do
{:&, _, [{{:., _, [{:__aliases__, _, _}, _]}, _, _}]} ->
false
{:&, _, [{:&, _, _}]} ->
arity == :any or arity == 1
{:&, _, [{name, _, _}]} when is_atom(name) ->
false
{:&, _, [body]} ->
arity == :any or count_captures(body) == arity
{:fn, _, [{:->, _, [[{:when, _, args}], _body]} | _]}
when arity == :any or length(args) == arity ->
true
{:fn, _, [{:->, _, [args, _body]} | _]} when arity == :any or length(args) == arity ->
true
{:fn, _, _} ->
true
_ ->
false
end
end
def function?(%Zipper{} = zipper, {module, name}, arity) do
node =
zipper
|> Common.maybe_move_to_single_child_block()
|> Zipper.node()
case node do
{:&, _, [{:/, _, [{^name, _, context}, actual_arity]}]}
when is_atom(context) and (arity == :any or actual_arity == arity) ->
imported?(zipper, module, name, actual_arity)
{:&, _, [{:/, _, [{^name, _, context}, {:__block__, _, [actual_arity]}]}]}
when is_atom(context) and (arity == :any or actual_arity == arity) ->
imported?(zipper, module, name, actual_arity)
{:&, _, [{:/, _, [^name, actual_arity]}]}
when arity == :any or actual_arity == arity ->
imported?(zipper, module, name, actual_arity)
{:&, _, [{:/, _, [^name, {:__block__, _, [actual_arity]}]}]}
when arity == :any or actual_arity == arity ->
imported?(zipper, module, name, actual_arity)
{:&, _,
[
{:/, _,
[
{{:., _, [{:__aliases__, _, _} = alias, ^name]}, _, _},
actual_arity
]}
]}
when arity == :any or actual_arity == arity ->
Common.nodes_equal?(Zipper.replace(zipper, alias), module)
{:&, _,
[
{:/, _,
[
{{:., _, [{:__aliases__, _, _} = alias, ^name]}, _, _},
{:__block__, _, [actual_arity]}
]}
]}
when arity == :any or actual_arity == arity ->
Common.nodes_equal?(Zipper.replace(zipper, alias), module)
{:&, _, [call]} ->
case call do
{{:., _, [{:__aliases__, _, _} = alias, ^name]}, _, args}
when arity == :any or length(args) == arity ->
Common.nodes_equal?(Zipper.replace(zipper, alias), module)
{{:., _, [{:__aliases__, _, _} = alias, {^name, _, context}]}, _, args}
when is_atom(context) and (arity == :any or length(args) == arity) ->
Common.nodes_equal?(Zipper.replace(zipper, alias), module)
{:|>, _,
[
_,
{{:., _, [{:__aliases__, _, _} = alias, ^name]}, _, args}
]}
when arity == :any or length(args) == arity - 1 ->
Common.nodes_equal?(Zipper.replace(zipper, alias), module)
{:|>, _,
[
_,
{{:., _, [{:__aliases__, _, _} = alias, {^name, _, context}]}, _, args}
]}
when is_atom(context) and (arity == :any or length(args) == arity - 1) ->
Common.nodes_equal?(Zipper.replace(zipper, alias), module)
{^name, _, args} when arity == :any or length(args) == arity ->
imported?(zipper, module, name, length(args))
{{^name, _, context}, _, args}
when is_atom(context) and (arity == :any or length(args) == arity) ->
imported?(zipper, module, name, length(args))
{:|>, _, [_, {{^name, _, context}, _, rest}]}
when is_atom(context) and (arity == :any or length(rest) == arity - 1) ->
imported?(zipper, module, name, length(rest) + 1)
{:|>, _, [_, {^name, _, rest}]}
when arity == :any or length(rest) == arity - 1 ->
imported?(zipper, module, name, length(rest) + 1)
_ ->
false
end
_ ->
false
end
end
def function?(%Zipper{} = zipper, :any, arity) do
function?(zipper, :any_named, arity) or function?(zipper, :anonymous, arity)
end
def function?(%Zipper{} = zipper, :any_named, arity) do
node =
zipper
|> Common.maybe_move_to_single_child_block()
|> Zipper.node()
case node do
{:&, _, [{:/, _, [{name, _, context}, actual_arity]}]}
when is_atom(name) and is_atom(context) and
(arity == :any or actual_arity == arity) ->
true
{:&, _, [{:/, _, [{name, _, context}, {:__block__, _, [actual_arity]}]}]}
when is_atom(name) and is_atom(context) and
(arity == :any or actual_arity == arity) ->
true
{:&, _, [{:/, _, [name, actual_arity]}]}
when is_atom(name) and (arity == :any or actual_arity == arity) ->
true
{:&, _, [{:/, _, [name, {:__block__, _, [actual_arity]}]}]}
when is_atom(name) and (arity == :any or actual_arity == arity) ->
true
{:&, _,
[
{:/, _,
[
{{:., _, [{:__aliases__, _, _}, name]}, _, _},
actual_arity
]}
]}
when is_atom(name) and (arity == :any or actual_arity == arity) ->
true
{:&, _,
[
{:/, _,
[
{{:., _, [{:__aliases__, _, _}, name]}, _, _},
{:__block__, _, [actual_arity]}
]}
]}
when is_atom(name) and (arity == :any or actual_arity == arity) ->
true
{:&, _, [call]} ->
case call do
{{:., _, [{:__aliases__, _, _}, name]}, _, args}
when is_atom(name) and (arity == :any or length(args) == arity) ->
true
{{:., _, [{:__aliases__, _, _}, {name, _, context}]}, _, args}
when is_atom(name) and is_atom(context) and (arity == :any or length(args) == arity) ->
true
{:|>, _,
[
_,
{{:., _, [{:__aliases__, _, _split}, name]}, _, args}
]}
when is_atom(name) or (arity == :any or length(args) == arity - 1) ->
true
{:|>, _,
[
_,
{{:., _, [{:__aliases__, _, _}, {name, _, context}]}, _, args}
]}
when is_atom(name) and is_atom(context) and (arity == :any or length(args) == arity - 1) ->
true
{name, _, args} when is_atom(name) and (arity == :any or length(args) == arity) ->
true
{{name, _, context}, _, args}
when is_atom(name) and is_atom(context) and
(arity == :any or length(args) == arity) ->
true
{:|>, _, [_, {name, _, context} | rest]}
when is_atom(name) and is_atom(context) and (arity == :any or length(rest) == arity - 1) ->
true
{:|>, _, [_, name | rest]}
when is_atom(name) and (arity == :any or length(rest) == arity - 1) ->
true
_ ->
false
end
_ ->
false
end
end
@doc """
Gets the name of a local function call.
Returns `:error` if the node is not a function call or cannot be determined.
"""
@spec get_local_function_call_name(Zipper.t()) :: {:ok, atom()} | :error
def get_local_function_call_name(%Zipper{} = zipper) do
case get_local_function_call(zipper) do
{:ok, {name, _arity}} -> {:ok, name}
:error -> :error
end
end
@doc """
Gets the name and arity of a local function call.
Returns `:error` if the node is not a function call or cannot be determined.
"""
@spec get_local_function_call(Zipper.t()) :: {:ok, {atom(), non_neg_integer()}} | :error
def get_local_function_call(%Zipper{} = zipper) do
zipper
|> Common.maybe_move_to_single_child_block()
|> Zipper.node()
|> case do
{:__block__, _, _} ->
:error
{:|>, _, [_, {{name, _, context}, _, args}]} when is_atom(context) and is_atom(name) ->
{:ok, {name, length(args) + 1}}
{:|>, _, [_, {name, _, args}]} when is_atom(name) ->
{:ok, {name, length(args) + 1}}
{name, _, args} when is_atom(name) ->
{:ok, {name, length(args)}}
{{name, _, context}, _, args} when is_atom(context) and is_atom(name) and is_list(args) ->
{:ok, {name, length(args)}}
_ ->
:error
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]}, _, _}
]}
when is_atom(name) ->
true
{:|>, _,
[
_,
{{:., _, [_, {name, _, context}]}, _, _args}
]}
when is_atom(name) and is_atom(context) ->
true
{:|>, _, [_, {{name, _, context}, _, _}]} when is_atom(context) and is_atom(name) ->
true
{:|>, _, [_, {name, _, _}]} when is_atom(name) ->
true
{name, _, _} when is_atom(name) ->
true
{{name, _, context}, _, _} when is_atom(context) and is_atom(name) ->
true
{{:., _, [_, name]}, _, _} when is_atom(name) ->
true
{{:., _, [_, {name, _, context}]}, _, _}
when is_atom(name) and is_atom(context) ->
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 | term())
) ::
{:ok, Zipper.t()} | :error | term()
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.move_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.move_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.move_right(index)
|> case do
:error ->
:error
{:ok, nth} ->
{:ok, nth}
end
end
end
end
else
offset =
case zipper.node do
{{:., _, _}, _, _args} ->
1
_ ->
0
end
zipper
|> Zipper.down()
|> case do
nil ->
:error
zipper ->
zipper
|> Common.move_right(index + offset)
|> 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()
|> Zipper.right()
|> argument_matches_predicate?(index - 1, func)
end
else
case Zipper.node(zipper) do
{{:., _, [_mod, name]}, _, args} when is_atom(name) and is_list(args) ->
zipper
|> Zipper.down()
|> Common.move_right(index + 1)
|> case do
:error ->
false
{:ok, zipper} ->
zipper
|> Common.maybe_move_to_single_child_block()
|> func.()
end
_ ->
zipper
|> Zipper.down()
|> case do
nil ->
false
zipper ->
zipper
|> Common.move_right(index)
|> case do
:error ->
false
{:ok, zipper} ->
zipper
|> Common.maybe_move_to_single_child_block()
|> func.()
end
end
end
end
else
false
end
end
defp pipeline?(zipper) do
case zipper.node do
{:|>, _, _} -> true
_ -> false
end
end
# Counts up all the arguments and generates new unique arguments for them.
# Works around the caveat that each usage of a unique `&n` variable must only
# be counted once.
defp count_captures(args) do
Macro.prewalk(args, [], fn
{:&, _, [v]} = ast, acc when is_integer(v) ->
{ast, [v | acc]}
ast, acc ->
{ast, acc}
end)
|> elem(1)
|> Enum.uniq()
|> Enum.count()
end
end