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 end
@doc "Moves to a function call by the given name and arity, matching the given predicate, in the current or lower scope" @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 {:ok, Zipper.t()} | :error
def move_to_function_call(zipper, name, arity, predicate \\ fn _ -> true end) def move_to_function_call(zipper, name, arity, predicate \\ fn _ -> true end)
@ -135,51 +135,124 @@ defmodule Igniter.Code.Function do
end end
end end
@doc "Returns `true` if the node is a function call of the given name and arity" @doc """
@spec function_call?(Zipper.t(), atom, non_neg_integer()) :: boolean() Returns `true` if the node is a function call of the given name
def function_call?(%Zipper{} = zipper, name, arity) do
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 zipper
|> Common.maybe_move_to_single_child_block() |> Common.maybe_move_to_single_child_block()
|> Zipper.node() |> Zipper.node()
|> case do |> case do
{^name, _, args} -> {^name, _, args} ->
Enum.count(args) == arity arity == :any || Enum.count(args) == arity
{{^name, _, context}, _, args} when is_atom(context) -> {{^name, _, context}, _, args} when is_atom(context) ->
Enum.count(args) == arity arity == :any || Enum.count(args) == arity
{:|>, _, [{^name, _, context} | rest]} when is_atom(context) -> {:|>, _, [{^name, _, context} | rest]} when is_atom(context) ->
Enum.count(rest) == arity - 1 arity == :any || Enum.count(rest) == arity - 1
{:|>, _, [^name | rest]} -> {:|>, _, [^name | rest]} ->
Enum.count(rest) == arity - 1 arity == :any || Enum.count(rest) == arity - 1
_ -> _ ->
false false
end end
end end
@doc "Returns `true` if the node is a function call of the given name" def function_call?(%Zipper{} = zipper, {module, name}, arity) when is_atom(name) do
@spec function_call?(Zipper.t(), atom) :: boolean() node =
def function_call?(%Zipper{} = zipper, name) do zipper
zipper |> Common.maybe_move_to_single_child_block()
|> Common.maybe_move_to_single_child_block() |> Igniter.Code.Common.expand_aliases()
|> Zipper.node() |> Zipper.node()
|> case do
{^name, _, _} ->
true
{{^name, _, context}, _, _} when is_atom(context) -> split = module |> Module.split() |> Enum.map(&String.to_atom/1)
true
{:|>, _, [{^name, _, context} | _rest]} when is_atom(context) -> case Igniter.Code.Common.current_env(zipper) do
true {: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]} -> case node do
true {{:., _, [{:__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
end end
@ -190,6 +263,22 @@ defmodule Igniter.Code.Function do
|> Common.maybe_move_to_single_child_block() |> Common.maybe_move_to_single_child_block()
|> Zipper.node() |> Zipper.node()
|> case do |> 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) -> {:|>, _, [{name, _, context} | _rest]} when is_atom(context) and is_atom(name) ->
true true
@ -202,6 +291,13 @@ defmodule Igniter.Code.Function do
{{name, _, context}, _, _} when is_atom(context) and is_atom(name) -> {{name, _, context}, _, _} when is_atom(context) and is_atom(name) ->
true true
{{:., _, [_, name]}, _, _} when is_atom(name) ->
true
{{:., _, [_, {name, _, context}]}, _, _}
when is_atom(name) and is_atom(context) ->
true
_ -> _ ->
false false
end end
@ -404,43 +500,15 @@ defmodule Igniter.Code.Function do
else else
zipper zipper
|> Zipper.down() |> Zipper.down()
|> case do |> Zipper.right()
nil -> |> argument_matches_predicate?(index - 1, func)
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 end
else else
zipper case Zipper.node(zipper) do
|> Zipper.down() {{:., _, [_mod, name]}, _, args} when is_atom(name) and is_list(args) ->
|> case do
nil ->
false
zipper ->
zipper zipper
|> Common.nth_right(index) |> Zipper.down()
|> Common.nth_right(index + 1)
|> case do |> case do
:error -> :error ->
false false
@ -450,6 +518,27 @@ defmodule Igniter.Code.Function do
|> Common.maybe_move_to_single_child_block() |> Common.maybe_move_to_single_child_block()
|> func.() |> func.()
end 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
end end
else 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 end
end end
@ -254,14 +254,26 @@ defmodule Igniter.Mix.Task do
def extract_positional_args(argv, got_argv, positional) do def extract_positional_args(argv, got_argv, positional) do
case OptionParser.next(argv, switches: []) do case OptionParser.next(argv, switches: []) do
{:ok, key, value, rest} -> {:ok, _key, _value, rest} ->
extract_positional_args(rest, got_argv ++ [{key, value}], positional) extract_positional_args(
rest,
got_argv ++ [Enum.at(argv, 0), Enum.at(argv, 1)],
positional
)
{:invalid, key, value, rest} -> {:invalid, _key, _value, rest} ->
extract_positional_args(rest, got_argv ++ [{key, value}], positional) extract_positional_args(
rest,
got_argv ++ [Enum.at(argv, 0), Enum.at(argv, 1)],
positional
)
{:undefined, key, value, rest} -> {:undefined, _key, _value, rest} ->
extract_positional_args(rest, got_argv ++ [{key, value}], positional) extract_positional_args(
rest,
got_argv ++ [Enum.at(argv, 0), Enum.at(argv, 1)],
positional
)
{:error, rest} -> {:error, rest} ->
[first | rest] = rest [first | rest] = rest