Packages
igniter
0.3.28
0.8.2
0.8.1
0.8.0
0.7.9
0.7.8
0.7.7
0.7.6
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.30
0.6.29
0.6.28
0.6.27
0.6.26
0.6.25
0.6.24
0.6.23
0.6.22
0.6.21
0.6.20
0.6.19
0.6.18
0.6.17
0.6.16
0.6.15
0.6.14
0.6.13
0.6.12
0.6.11
0.6.10
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.52
0.5.51
0.5.50
0.5.49
0.5.48
0.5.47
0.5.46
0.5.45
0.5.44
0.5.43
0.5.42
0.5.41
0.5.40
0.5.39
0.5.38
0.5.37
0.5.36
0.5.35
0.5.34
0.5.33
0.5.32
0.5.31
0.5.30
0.5.29
0.5.28
0.5.27
0.5.26
0.5.25
0.5.24
0.5.23
0.5.22
0.5.21
0.5.20
0.5.19
0.5.18
0.5.17
0.5.16
0.5.15
0.5.14
0.5.13
0.5.12
0.5.11
0.5.10
0.5.9
0.5.8
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.8
0.4.7
0.4.6
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.78
0.3.77
0.3.76
0.3.75
0.3.74
0.3.73
0.3.72
0.3.71
0.3.70
0.3.69
0.3.68
0.3.67
0.3.66
0.3.65
0.3.64
0.3.63
0.3.62
0.3.61
0.3.60
0.3.59
0.3.58
0.3.57
0.3.56
0.3.55
0.3.54
0.3.53
0.3.52
0.3.51
0.3.50
0.3.49
0.3.48
0.3.47
0.3.46
0.3.45
0.3.44
0.3.43
0.3.42
0.3.41
0.3.40
0.3.39
0.3.38
0.3.37
0.3.36
0.3.35
0.3.34
0.3.33
0.3.32
0.3.31
0.3.30
0.3.29
0.3.28
0.3.27
0.3.26
0.3.25
0.3.24
0.3.23
0.3.22
0.3.21
0.3.20
0.3.19
0.3.18
0.3.17
0.3.16
0.3.15
0.3.14
0.3.13
0.3.12
0.3.11
0.3.10
0.3.9
0.3.8
0.3.7
0.3.6
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.13
0.2.12
0.2.11
0.2.10
0.2.9
0.2.8
0.2.7
0.2.6
0.2.5
0.2.4
0.2.3
0.2.2
0.2.1
0.2.0
0.1.8
0.1.7
0.1.6
0.1.5
0.1.4
0.1.3
0.1.2
0.1.1
0.1.0
A code generation and project patching framework
Current section
Files
Jump to
Current section
Files
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)) ::
{: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 | {atom, 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
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) :: boolean()
def function_call?(zipper, name, arity \\ :any)
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} ->
arity == :any || Enum.count(args) == arity
{{^name, _, context}, _, args} when is_atom(context) ->
arity == :any || Enum.count(args) == arity
{:|>, _, [{^name, _, context} | rest]} when is_atom(context) ->
arity == :any || Enum.count(rest) == arity - 1
{:|>, _, [^name | rest]} ->
arity == :any || Enum.count(rest) == arity - 1
_ ->
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()
|> Igniter.Code.Common.expand_aliases()
|> Zipper.node()
split = module |> Module.split() |> Enum.map(&String.to_atom/1)
case Igniter.Code.Common.current_env(zipper) do
{:ok, env} ->
imported? =
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 || Enum.count(imported_arity) == arity)
end)
end)
case node do
{{:., _, [{:__aliases__, _, ^split}, ^name]}, _, args} ->
arity == :any || Enum.count(args) == arity
{{:., _, [{:__aliases__, _, ^split}, {^name, _, context}]}, _, args}
when is_atom(context) ->
arity == :any || Enum.count(args) == arity
{:|>, _,
[
_,
{{:., _, [{:__aliases__, _, ^split}, ^name]}, _, args}
]} ->
arity == :any || Enum.count(args) == arity - 1
{:|>, _,
[
_,
{{:., _, [{:__aliases__, _, ^split}, {^name, _, context}]}, _, args}
]}
when is_atom(context) ->
arity == :any || Enum.count(args) == arity - 1
{^name, _, args} ->
imported? && (arity == :any || Enum.count(args) == arity)
{{^name, _, context}, _, args} when is_atom(context) ->
imported? && (arity == :any || Enum.count(args) == arity)
{:|>, _, [{^name, _, context} | rest]} when is_atom(context) ->
imported? && (arity == :any || Enum.count(rest) == arity - 1)
{:|>, _, [^name | rest]} ->
imported? && (arity == :any || Enum.count(rest) == arity - 1)
_ ->
false
end
_ ->
case node do
{{:., _, [{:__aliases__, _, ^split}, ^name]}, _, args} ->
arity == :any || Enum.count(args) == arity
{{:., _, [{:__aliases__, _, ^split}, {^name, _, context}]}, _, args}
when is_atom(context) ->
arity == :any || Enum.count(args) == arity
{:|>, _,
[
_,
{{:., _, [{:__aliases__, _, ^split}, ^name]}, _, args}
]} ->
arity == :any || Enum.count(args) == arity - 1
{:|>, _,
[
_,
{{:., _, [{:__aliases__, _, ^split}, {^name, _, context}]}, _, args}
]}
when is_atom(context) ->
arity == :any || Enum.count(args) == arity - 1
_ ->
false
end
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} | _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
{{:., _, [_, 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)
) ::
{: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
offset =
case zipper.node do
{{:., _, _}, _, _args} ->
1
_ ->
0
end
zipper
|> Zipper.down()
|> case do
nil ->
:error
zipper ->
zipper
|> Common.nth_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.nth_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.nth_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
end