fix: detect more function call formats

fix: properly extract arguments when parsing positional args
This commit is contained in:
Zach Daniel 2024-07-30 15:30:29 -04:00
parent 8a6eebd6bd
commit 7ef6465da5
2 changed files with 166 additions and 65 deletions

View file

@ -107,7 +107,7 @@ defmodule Igniter.Code.Function do
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()) ::
@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)
@ -135,51 +135,124 @@ defmodule Igniter.Code.Function do
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
@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} ->
Enum.count(args) == arity
arity == :any || Enum.count(args) == arity
{{^name, _, context}, _, args} when is_atom(context) ->
Enum.count(args) == arity
arity == :any || Enum.count(args) == arity
{:|>, _, [{^name, _, context} | rest]} when is_atom(context) ->
Enum.count(rest) == arity - 1
arity == :any || Enum.count(rest) == arity - 1
{:|>, _, [^name | rest]} ->
Enum.count(rest) == arity - 1
arity == :any || 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
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()
{{^name, _, context}, _, _} when is_atom(context) ->
true
split = module |> Module.split() |> Enum.map(&String.to_atom/1)
{:|>, _, [{^name, _, context} | _rest]} when is_atom(context) ->
true
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)
{:|>, _, [^name | _rest]} ->
true
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
_ ->
false
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
@ -190,6 +263,22 @@ defmodule Igniter.Code.Function do
|> 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
@ -202,6 +291,13 @@ defmodule Igniter.Code.Function do
{{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
@ -404,43 +500,15 @@ defmodule Igniter.Code.Function do
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
|> Zipper.right()
|> argument_matches_predicate?(index - 1, func)
end
else
zipper
|> Zipper.down()
|> case do
nil ->
false
zipper ->
case Zipper.node(zipper) do
{{:., _, [_mod, name]}, _, args} when is_atom(name) and is_list(args) ->
zipper
|> Common.nth_right(index)
|> Zipper.down()
|> Common.nth_right(index + 1)
|> case do
:error ->
false
@ -450,6 +518,27 @@ defmodule Igniter.Code.Function do
|> 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

View file

@ -194,7 +194,7 @@ defmodule Igniter.Mix.Task do
"""
_ ->
{Igniter.Mix.Task.add_default_values(Map.new(got), desired), positional}
{Igniter.Mix.Task.add_default_values(Map.new(got), desired), argv}
end
end
end
@ -254,14 +254,26 @@ defmodule Igniter.Mix.Task do
def extract_positional_args(argv, got_argv, positional) do
case OptionParser.next(argv, switches: []) do
{:ok, key, value, rest} ->
extract_positional_args(rest, got_argv ++ [{key, value}], positional)
{:ok, _key, _value, rest} ->
extract_positional_args(
rest,
got_argv ++ [Enum.at(argv, 0), Enum.at(argv, 1)],
positional
)
{:invalid, key, value, rest} ->
extract_positional_args(rest, got_argv ++ [{key, value}], positional)
{:invalid, _key, _value, rest} ->
extract_positional_args(
rest,
got_argv ++ [Enum.at(argv, 0), Enum.at(argv, 1)],
positional
)
{:undefined, key, value, rest} ->
extract_positional_args(rest, got_argv ++ [{key, value}], positional)
{:undefined, _key, _value, rest} ->
extract_positional_args(
rest,
got_argv ++ [Enum.at(argv, 0), Enum.at(argv, 1)],
positional
)
{:error, rest} ->
[first | rest] = rest