Current section

Files

Jump to
igniter lib igniter code common.ex
Raw

lib/igniter/code/common.ex

defmodule Igniter.Code.Common do
@moduledoc """
General purpose utilities for working with `Sourceror.Zipper`.
"""
require Logger
alias Sourceror.Zipper
@doc """
Moves to the next node that matches the predicate.
"""
@spec move_to(Zipper.t(), (Zipper.t() -> boolean())) :: {:ok, Zipper.t()} | :error
def move_to(zipper, pred) do
if pred.(zipper) do
{:ok, zipper}
else
case Zipper.next(zipper) do
nil ->
:error
next ->
move_to(next, pred)
end
end
end
@doc """
Returns a list of `zippers` to each `node` that satisfies the `predicate` function, or
an empty list if none are found.
The optional second parameters specifies the `direction`, defaults to
`:next`.
"""
@spec find_all(Zipper.t(), predicate :: (Zipper.t() -> boolean())) :: [Zipper.t()]
def find_all(%Zipper{} = zipper, predicate) when is_function(predicate, 1) do
do_find_all(zipper, predicate, [])
end
defp do_find_all(nil, _predicate, buffer), do: Enum.reverse(buffer)
defp do_find_all(%Zipper{} = zipper, predicate, buffer) do
if predicate.(zipper) do
zipper |> Zipper.next() |> do_find_all(predicate, [zipper | buffer])
else
zipper |> Zipper.next() |> do_find_all(predicate, buffer)
end
end
@doc false
def find_prev(%Zipper{} = zipper, pred) when is_function(pred, 1) do
case move_left(zipper, pred) do
{:ok, zipper} ->
{:ok, zipper}
:error ->
case move_upwards(zipper, 1) do
{:ok, zipper} -> find_prev(zipper, pred)
:error -> :error
end
end
end
@doc """
Moves a zipper upwards.
If the second argument is a predicate function, it will be called on the zipper and then
move upwards until the predicate returns `true`.
If the second argument is a non-negative integer, it will move upwards that many times if
possible, returning `:error` otherwise.
"""
@spec move_upwards(Zipper.t(), non_neg_integer() | (Zipper.t() -> boolean())) ::
{:ok, Zipper.t()} | :error
def move_upwards(zipper, pred_or_n)
def move_upwards(%Zipper{} = zipper, pred) when is_function(pred, 1) do
if pred.(zipper) do
{:ok, zipper}
else
case move_upwards(zipper, 1) do
:error -> :error
{:ok, next} -> move_upwards(next, pred)
end
end
end
def move_upwards(%Zipper{} = zipper, n) when is_integer(n) and n >= 0 do
do_nth_upwards(zipper, n)
end
defp do_nth_upwards(nil, _), do: :error
defp do_nth_upwards(zipper, 0), do: {:ok, zipper}
defp do_nth_upwards(zipper, n), do: zipper |> Zipper.up() |> do_nth_upwards(n - 1)
@doc """
Moves to the last node before the node that matches the predicate, going upwards.
"""
@spec move_upwards_until(Zipper.t(), (Zipper.t() -> boolean())) :: {:ok, Zipper.t()} | :error
def move_upwards_until(zipper, pred) do
if pred.(zipper) do
{:ok, Zipper.down(zipper) || zipper}
else
case move_upwards(zipper, 1) do
:error -> :error
{:ok, next} -> move_upwards(next, pred)
end
end
end
@doc """
Removes any nodes matching the provided pattern, until there are no matches left.
"""
@spec remove(Zipper.t(), (Zipper.t() -> boolean)) :: Zipper.t()
def remove(zipper, pred) do
case move_to(zipper, pred) do
{:ok, zipper} ->
zipper
|> Zipper.remove()
|> remove(pred)
:error ->
zipper
end
end
@doc """
Moves to the next zipper that matches the predicate.
"""
@spec move_to(Zipper.t(), (Zipper.t() -> boolean())) :: {:ok, Zipper.t()} | :error
def move_to_zipper(zipper, pred) do
if pred.(zipper) do
{:ok, zipper}
else
if next = Zipper.next(zipper) do
move_to_zipper(next, pred)
else
:error
end
end
end
@doc """
Returns `true` if the current node matches the given pattern.
## Examples:
```elixir
list_zipper =
"[1, 2, 3]"
|> Sourceror.parse_string!()
|> Sourceror.Zipper.zip()
Common.node_matches_pattern?(list_zipper, value when is_list(value)) # true
```
"""
defmacro node_matches_pattern?(zipper, pattern) do
quote do
ast =
unquote(zipper)
|> Igniter.Code.Common.maybe_move_to_single_child_block()
|> Zipper.node()
match?(unquote(pattern), ast)
end
end
@doc """
Moves to the next node that matches the given pattern.
"""
defmacro move_to_pattern(zipper, pattern) do
quote do
case Sourceror.Zipper.find(unquote(zipper), fn
unquote(pattern) ->
true
_ ->
false
end) do
nil -> :error
value -> {:ok, value}
end
end
end
@doc """
Expands a literal value using the env at the cursor, if possible
"""
@spec expand_literal(Zipper.t()) :: {:ok, any()} | :error
def expand_literal(zipper) do
zipper = maybe_move_to_single_child_block(zipper)
if quoted_literal?(zipper) do
{v, _} = Code.eval_quoted(zipper.node)
{:ok, v}
else
case current_env(zipper) do
{:ok, env} ->
expanded = Macro.expand_literals(zipper.node, env)
if Macro.quoted_literal?(expanded) do
{:ok, expanded}
else
:error
end
_ ->
:error
end
end
end
@doc """
Adds the provided code to the zipper.
## Options
- `:placement` - `:after` | `:before`. Determines if the code goes `:after` or `:before` the current node. Defaults to `:after`.
- `:expand_env?` - boolean. Whether or not to read the current file to use it's aliases for added code. Defaults to `false`.
## Example:
```elixir
existing_zipper = \"\"\"
IO.inspect("Hello, world!")
\"\"\"
|> Sourceror.parse_string!()
|> Sourceror.Zipper.zip()
new_code = \"\"\"
IO.inspect("Goodbye, world!")
\"\"\"
existing_zipper
|> Igniter.Common.add_code(new_code)
|> Sourceror.Zipper.root()
|> Sourceror.to_string()
```
Which will produce
```elixir
\"\"\"
IO.inspect("Hello, world!")
IO.inspect("Goodbye, world!")
\"\"\"
```
"""
@spec add_code(Zipper.t(), String.t() | Macro.t(), [opt]) :: Zipper.t()
when opt: {:placement, :after | :before} | {:expand_env?, boolean()}
def add_code(zipper, new_code, opts \\ [])
def add_code(zipper, new_code, placement) when is_atom(placement) do
Logger.warning("""
Passing an atom as the third argument to `Igniter.Code.Common.add_code/3` is deprecated in favor of an options list.
Instead of:
Igniter.Code.Common.add_code(zipper, new_code, #{inspect(placement)})
You should now write:
Igniter.Code.Common.add_code(zipper, new_code, placement: #{inspect(placement)})
""")
add_code(zipper, new_code, placement: placement)
end
def add_code(zipper, new_code, opts) when is_binary(new_code) do
code = Sourceror.parse_string!(new_code)
add_code(zipper, code, opts)
end
def add_code(zipper, new_code, opts) do
do_add_code(zipper, new_code, opts)
end
defp do_add_code(zipper, new_code, opts) do
expand_env? = Keyword.get(opts, :expand_env?, true)
placement = Keyword.get(opts, :placement, :after)
new_code =
if expand_env? do
use_aliases(new_code, zipper)
else
new_code
end
upwards = Zipper.up(zipper)
super_upwards =
if !upwards && zipper.supertree do
Zipper.up(zipper.supertree)
end
cond do
upwards && extendable_block?(upwards.node) ->
{:__block__, upwards_meta, upwards_code} = upwards.node
index = Enum.count(zipper.path.left || [])
{to_insert, new_meta} =
if extendable_block?(new_code) do
{:__block__, new_meta, new_code} = new_code
{new_code, new_meta}
else
{[new_code], []}
end
{head, tail} =
if placement == :after do
Enum.split(upwards_code, index + 1)
else
Enum.split(upwards_code, index)
end
Zipper.replace(
upwards,
{:__block__, combine_comments(upwards_meta, new_meta, placement),
head ++ to_insert ++ tail}
)
super_upwards && extendable_block?(super_upwards.node) ->
{:__block__, upwards_meta, upwards_code} = super_upwards.node
index = Enum.count(zipper.supertree.path.left || [])
{to_insert, new_meta} =
if extendable_block?(new_code) do
{:__block__, new_meta, new_code} = new_code
{new_code, new_meta}
else
{[new_code], []}
end
{head, tail} =
if placement == :after do
Enum.split(upwards_code, index + 1)
else
Enum.split(upwards_code, index)
end
new_super_upwards =
Zipper.replace(
super_upwards,
{:__block__, combine_comments(upwards_meta, new_meta, placement),
head ++ to_insert ++ tail}
)
if placement == :after do
%{
zipper
| supertree: %{
zipper.supertree
| path: %{
zipper.supertree.path
| parent: new_super_upwards,
right: to_insert ++ zipper.supertree.path.right
}
}
}
else
%{
zipper
| supertree: %{
zipper.supertree
| path: %{
zipper.supertree.path
| parent: new_super_upwards,
left: zipper.supertree.path.right ++ to_insert
}
}
}
end
true ->
if extendable_block?(zipper.node) && extendable_block?(new_code) do
{:__block__, meta, stuff} = zipper.node
{:__block__, new_meta, new_stuff} = new_code
new_stuff =
if placement == :after do
stuff ++ new_stuff
else
new_stuff ++ stuff
end
Zipper.replace(
zipper,
{:__block__, combine_comments(meta, new_meta, placement), new_stuff}
)
else
if extendable_block?(zipper.node) do
{:__block__, meta, stuff} = zipper.node
new_stuff =
if placement == :after do
stuff ++ [new_code]
else
[new_code] ++ stuff
end
Zipper.replace(zipper, {:__block__, meta, new_stuff})
else
{code, meta} =
if extendable_block?(new_code) do
{:__block__, meta, new_stuff} = new_code
if placement == :after do
{[zipper.node] ++ new_stuff, meta}
else
{new_stuff ++ [zipper.node], meta}
end
else
if placement == :after do
{[zipper.node, new_code], []}
else
{[new_code, zipper.node], []}
end
end
Zipper.replace(zipper, {:__block__, meta, code})
end
end
end
end
@doc """
Updates all nodes matching the given predicate with the given function.
Recurses until the predicate no longer returns false
"""
@spec update_all_matches(
Zipper.t(),
(Zipper.t() -> boolean()),
(Zipper.t() ->
{:ok, Zipper.t() | {:code, term()}}
| {:warning | :error, term()})
) ::
{:ok, Zipper.t()} | {:warning | :error, term()}
def update_all_matches(zipper, pred, fun) do
# we check for a single match before traversing as an optimization
case move_to(zipper, pred) do
:error ->
{:ok, zipper}
{:ok, _} ->
Zipper.traverse(zipper, false, fn zipper, acc ->
if pred.(zipper) do
case within(zipper, fun) do
{:code, new_code} ->
{replace_code(zipper, new_code), true}
{:ok, ^zipper} ->
{zipper, acc}
{:ok, zipper} ->
{zipper, true}
{:halt_depth, zipper} ->
{zipper, acc}
other ->
throw({:other_res, other})
end
else
{zipper, acc}
end
end)
|> case do
{zipper, false} ->
{:ok, zipper}
{zipper, true} ->
update_all_matches(zipper, pred, fun)
end
end
catch
{:other_res, v} ->
v
end
@doc """
Removes all nodes matching the given predicate with the given function.
Recurses until the predicate no longer returns false
"""
@spec remove_all_matches(
Zipper.t(),
(Zipper.t() -> boolean())
) ::
Zipper.t()
def remove_all_matches(zipper, pred) do
# we check for a single match before traversing as an optimization
case move_to(zipper, pred) do
:error ->
zipper
{:ok, _} ->
Zipper.traverse(zipper, fn zipper ->
if pred.(zipper) do
Zipper.remove(zipper)
else
zipper
end
end)
end
catch
{:other_res, v} ->
v
end
@doc """
Replaces code with new code.
"""
def replace_code(%Zipper{} = zipper, code) when is_binary(code) do
replace_code(zipper, Sourceror.parse_string!(code))
end
def replace_code(%Zipper{} = zipper, new_code) do
new_code = use_aliases(new_code, zipper)
if extendable_block?(new_code) and in_extendable_block?(zipper) do
at_supertree_root? = Zipper.up(zipper) == nil and !!zipper.supertree
zipper = if at_supertree_root?, do: supertree(zipper), else: zipper
{:__block__, meta, new_inner_code} = new_code
if Enum.empty?(meta[:trailing_comments] || []) || Enum.empty?(meta[:leading_comments] || []) do
{new_code_last, new_inner_code} = List.pop_at(new_inner_code, -1)
zipper =
new_inner_code
# We insert to the left and explicitly replace the node with new_code_last
# so that the last node retains the metadata of the node we're replacing.
# That metadata includes things like the number of trailing newlines.
|> Enum.reduce(zipper, &Zipper.insert_left(&2, &1))
|> replace_node(new_code_last)
{:ok, zipper} = move_left(zipper, length(new_inner_code))
if at_supertree_root?, do: Zipper.subtree(zipper), else: zipper
else
replace_node(zipper, new_code)
end
else
replace_node(zipper, new_code)
end
end
defp replace_node(zipper, new_code) do
new_code =
with true <- multi_element_pipe?(zipper),
{call, meta, [first_arg | rest]} when call != :|> <- new_code,
{:ok, rewritten} <- rewrite_pipe(call, meta, first_arg, rest) do
rewritten
else
_ ->
new_code
end
case {zipper.node, new_code} do
{{_, meta, _}, {new_call, new_meta, new_children}} ->
Zipper.replace(zipper, {new_call, combine_comments(meta, new_meta), new_children})
{_node, _new_node} ->
Zipper.replace(zipper, new_code)
end
end
defp combine_comments(meta, new_meta, placement \\ :after) do
if placement == :after do
meta
|> Keyword.update(
:leading_comments,
new_meta[:leading_comments] || [],
&(&1 ++ (new_meta[:leading_comments] || []))
)
|> Keyword.update(
:trailing_comments,
new_meta[:trailing_comments] || [],
&((new_meta[:trailing_comments] || []) ++ &1)
)
|> ensure_unique_comments()
else
meta
|> Keyword.update(
:leading_comments,
new_meta[:leading_comments] || [],
&((new_meta[:leading_comments] || []) ++ &1)
)
|> Keyword.update(
:trailing_comments,
new_meta[:trailing_comments] || [],
&(&1 ++ (new_meta[:trailing_comments] || []))
)
|> ensure_unique_comments()
end
end
defp ensure_unique_comments(meta) do
meta
|> Keyword.update!(:leading_comments, &Enum.uniq/1)
|> Keyword.update!(:trailing_comments, &Enum.uniq/1)
end
defp multi_element_pipe?(%{node: {:|>, _, _}} = zipper) do
up = Zipper.up(zipper)
down = Zipper.down(zipper)
match?(%{node: {:|>, _, _}}, up) || match?(%{node: {:|>, _, _}}, down)
end
defp multi_element_pipe?(_), do: false
defp rewrite_pipe(:!, meta, first_arg, []) do
{:ok,
{:|>, meta, [first_arg, {{:., [], [{:__aliases__, [alias: false], [:Kernel]}, :!]}, [], []}]}}
end
defp rewrite_pipe(function, meta, first_arg, rest) when is_atom(function) do
if Regex.match?(~r/^[a-z_][a-zA-Z0-9_?!]*$/, to_string(function)) do
{:ok, {:|>, meta, [first_arg, {function, [], rest}]}}
else
:error
end
end
def extendable_block?(%Zipper{node: node}), do: extendable_block?(node)
def extendable_block?({:__block__, meta, contents}) when is_list(contents) do
!meta[:token] && !meta[:format] && !meta[:delimiter]
end
def extendable_block?(_), do: false
defp in_extendable_block?(%Zipper{} = zipper) do
case Zipper.up(zipper) do
%Zipper{} = zipper -> extendable_block?(zipper)
nil -> in_extendable_block?(zipper.supertree)
end
end
defp in_extendable_block?(nil), do: false
@doc """
Replaces full module names in `new_code` with any aliases for that
module found in the `current_code` environment.
"""
def use_aliases(new_code, current_code) do
case current_env(current_code) do
{:ok, env} ->
Macro.prewalk(new_code, fn
{:__aliases__, _, parts} = node ->
case use_alias(env, parts) do
{:alias, new_parts} ->
{:__aliases__, [], new_parts}
_ ->
node
end
{{:., _, [{:__aliases__, _, split}, name]}, _, args} = node ->
if imported?(env, split, name, Enum.count(args)) do
{name, [], args}
else
node
end
{{:., _, [{:__aliases__, _, split}, {name, _, context}]}, _, args} = node
when is_atom(context) ->
if imported?(env, split, name, Enum.count(args)) do
{name, [], args}
else
node
end
{:|>, _,
[
arg,
{{:., _, [{:__aliases__, _, split}, name]}, _, args}
]} = node ->
if imported?(env, split, name, Enum.count(args) + 1) do
{:|>, [], [arg, {name, [], args}]}
else
node
end
{:|>, _,
[
arg,
{{:., _, [{:__aliases__, _, split}, {name, _, context}]}, _, args}
]} = node
when is_atom(context) ->
if imported?(env, split, name, Enum.count(args) + 1) do
{:|>, [], [arg, {name, [], args}]}
else
node
end
node ->
node
end)
_ ->
new_code
end
end
defp imported?(env, split, name, arity) do
mod = Module.concat(split)
Enum.any?(env.functions, fn {imported_mod, funs} ->
mod == imported_mod &&
Enum.any?(funs, fn {fun_name, fun_arity} ->
fun_name == name and fun_arity == arity
end)
end)
end
defp use_alias(env, parts) do
env.aliases
|> Enum.filter(fn {_as, fqn} ->
fqn_split = Enum.map(Module.split(fqn), &String.to_atom/1)
List.starts_with?(parts, fqn_split)
end)
|> Enum.sort_by(fn {_as, fqn} ->
fqn
|> Module.split()
|> Enum.count()
end)
|> Enum.reverse()
|> Enum.at(0)
|> case do
nil ->
:error
{as, fqn} ->
to_drop =
fqn
|> Module.split()
|> Enum.count()
after_as =
Enum.drop(parts, to_drop)
as
|> Module.split()
|> Enum.map(&String.to_atom/1)
|> Enum.concat(after_as)
|> then(&{:alias, &1})
end
end
@doc """
Moves to a do block for the current call.
For example, at a node like:
```elixir
foo do
10
end
```
You would get a zipper back at `10`.
"""
@spec move_to_do_block(Zipper.t()) :: {:ok, Zipper.t()} | :error
def move_to_do_block(zipper) do
case move_to_pattern(zipper, {{:__block__, _, [:do]}, _}) do
:error ->
:error
{:ok, zipper} ->
zipper
|> Zipper.down()
|> case do
nil ->
:error
zipper ->
{:ok,
zipper
|> Zipper.rightmost()}
end
end
end
@doc """
Enters a block with a single child, and moves to that child,
or returns the zipper unmodified
"""
@spec maybe_move_to_single_child_block(Zipper.t()) :: Zipper.t()
def maybe_move_to_single_child_block(nil), do: nil
def maybe_move_to_single_child_block(zipper) do
if single_child_block?(zipper) do
zipper
|> Zipper.down()
|> case do
nil ->
zipper
zipper ->
maybe_move_to_single_child_block(zipper)
end
else
zipper
end
end
@spec single_child_block?(Zipper.t()) :: boolean()
def single_child_block?(zipper) do
case zipper.node do
{:__block__, _, [_]} = block ->
extendable_block?(block)
_ ->
false
end
end
@doc """
Enters a block, and moves to the first child, or returns the zipper unmodified.
"""
@spec maybe_move_to_block(Zipper.t()) :: Zipper.t()
def maybe_move_to_block(nil), do: nil
def maybe_move_to_block(zipper) do
case zipper.node do
{:__block__, _, _} ->
zipper
|> Zipper.down()
|> case do
nil ->
zipper
zipper ->
zipper
end
_ ->
zipper
end
end
@deprecated "Use `move_right/2` instead, passing an integer as the second argument."
@spec nth_right(Zipper.t(), non_neg_integer()) :: {:ok, Zipper.t()} | :error
def nth_right(zipper, n) do
do_nth_move(zipper, n, &Zipper.right/1)
end
@doc """
Moves to the cursor that matches the provided pattern or one of the provided patterns, in the current scope.
See `move_to_cursor/2` for an example of a pattern
"""
@spec move_to_cursor_match_in_scope(Zipper.t(), String.t() | [String.t()]) ::
{:ok, Zipper.t()} | :error
def move_to_cursor_match_in_scope(zipper, patterns) when is_list(patterns) do
Enum.find_value(patterns, :error, fn pattern ->
case move_to_cursor_match_in_scope(zipper, pattern) do
{:ok, value} -> {:ok, value}
_ -> nil
end
end)
end
def move_to_cursor_match_in_scope(zipper, pattern) do
pattern =
case pattern do
pattern when is_binary(pattern) ->
pattern
|> Sourceror.parse_string!()
|> Zipper.zip()
%Zipper{} = pattern ->
pattern
end
case move_to_cursor(zipper, pattern) do
:error ->
move_right(zipper, fn zipper ->
match?({:ok, _}, move_to_cursor(zipper, pattern))
end)
|> case do
{:ok, zipper} ->
move_to_cursor(zipper, pattern)
_ ->
:error
end
{:ok, zipper} ->
{:ok, zipper}
end
end
@doc """
Moves the zipper all the way to the right, potentially entering a single value block.
"""
@spec rightmost(Zipper.t()) :: Zipper.t()
def rightmost(%Zipper{} = zipper) do
zipper
|> Zipper.rightmost()
|> maybe_move_to_single_child_block()
end
@doc """
Moves a zipper to the left.
If the second argument is a predicate function, it will be called on the zipper and then
move leftwards until the predicate returns `true`. This function will automatically enter
and exit blocks.
If the second argument is a non-negative integer, it will move left that many times if
possible, returning `:error` otherwise.
"""
@spec move_left(Zipper.t(), non_neg_integer() | (Zipper.t() -> boolean())) ::
{:ok, Zipper.t()} | :error
def move_left(zipper, pred_or_n)
def move_left(%Zipper{} = zipper, pred) when is_function(pred, 1) do
do_move(zipper, pred, &Zipper.left/1)
end
def move_left(%Zipper{} = zipper, n) when is_integer(n) and n >= 0 do
do_nth_move(zipper, n, &Zipper.left/1)
end
@doc """
Moves a zipper to the right.
If the second argument is a predicate function, it will be called on the zipper and then
move rightwards until the predicate returns `true`. This function will automatically enter
and exit blocks.
If the second argument is a non-negative integer, it will move right that many times if
possible, returning `:error` otherwise.
"""
@spec move_right(Zipper.t(), non_neg_integer() | (Zipper.t() -> boolean())) ::
{:ok, Zipper.t()} | :error
def move_right(zipper, pred_or_n)
def move_right(%Zipper{} = zipper, pred) when is_function(pred, 1) do
do_move(zipper, pred, &Zipper.right/1)
end
def move_right(%Zipper{} = zipper, n) when is_integer(n) and n >= 0 do
do_nth_move(zipper, n, &Zipper.right/1)
end
defp do_move(%Zipper{} = zipper, pred, move)
when is_function(pred, 1) and is_function(move, 1) do
zipper_in_single_child_block = maybe_move_to_single_child_block(zipper)
cond do
pred.(zipper) ->
{:ok, zipper}
pred.(zipper_in_single_child_block) ->
{:ok, zipper_in_single_child_block}
extendable_block?(zipper) && length(elem(zipper.node, 2)) > 1 ->
zipper
|> Zipper.down()
|> case do
nil ->
case move.(zipper) do
nil ->
:error
zipper ->
do_move(zipper, pred, move)
end
zipper ->
case do_move(zipper, pred, move) do
{:ok, zipper} ->
{:ok, zipper}
:error ->
case move.(zipper) do
nil ->
:error
zipper ->
do_move(zipper, pred, move)
end
end
end
true ->
case move.(zipper) do
nil ->
:error
zipper ->
do_move(zipper, pred, move)
end
end
end
defp do_nth_move(zipper, count, move)
defp do_nth_move(nil, _, _), do: :error
defp do_nth_move(zipper, 0, _), do: {:ok, zipper}
defp do_nth_move(zipper, n, move), do: zipper |> move.() |> do_nth_move(n - 1, move)
@doc """
Moves nextwards (depth-first), until the provided predicate returns `true`.
Returns `:error` if the end is reached without finding a match.
"""
@spec move_next(Zipper.t(), (Zipper.t() -> boolean)) :: {:ok, Zipper.t()} | :error
def move_next(%Zipper{} = zipper, pred) do
if pred.(zipper) do
{:ok, zipper}
else
case Zipper.next(zipper) do
nil ->
:error
zipper ->
move_next(zipper, pred)
end
end
end
@doc """
Matches and moves to the location of a `__cursor__` in provided source code.
Use `__cursor__()` to match a cursor in the provided source code. Use `__` to skip any code at a point.
For example:
```elixir
zipper =
\"\"\"
if true do
10
end
\"\"\"
|> Sourceror.Zipper.zip()
pattern =
\"\"\"
if __ do
__cursor__()
end
\"\"\"
zipper
|> Igniter.Code.Common.move_to_cursor(pattern)
|> Zipper.node()
# => 10
```
"""
@spec move_to_cursor(Zipper.t(), Zipper.t() | String.t()) :: {:ok, Zipper.t()} | :error
def move_to_cursor(zipper, pattern) do
case Zipper.move_to_cursor(zipper, pattern) do
nil -> :error
zipper -> {:ok, zipper}
end
end
@doc """
Expands the environment at the current zipper position and returns the
expanded environment. Currently used for properly working with aliases.
"""
def current_env(zipper) do
Process.put(:elixir_code_diagnostics, {[], false})
zipper
|> do_add_code({:__cursor__, [], []}, placement: :after, expand_env?: false)
|> Zipper.topmost_root()
|> Sourceror.to_string()
|> String.split("__cursor__()", parts: 2)
|> List.first()
|> Spitfire.container_cursor_to_quoted()
|> then(fn {:ok, ast} ->
ast
end)
|> Spitfire.Env.expand("file.ex")
|> then(fn {_ast, _final_state, _final_env, cursor_env} ->
{:ok, struct(Macro.Env, cursor_env)}
end)
rescue
e ->
{:error, e}
after
Process.delete(:elixir_code_diagnostics)
end
@doc """
Runs the function `fun` on the subtree of the currently focused `node` and
returns the updated `zipper`.
`fun` must return {:ok, zipper} or `:error`, which may be positioned at the top of the subtree.
"""
def within(%Zipper{} = top_zipper, fun) when is_function(fun, 1) do
top_zipper
|> Zipper.subtree()
|> fun.()
|> case do
{:ok, %Zipper{} = zipper} ->
{:ok, Zipper.supertree(zipper)}
other ->
other
end
end
@spec nodes_equal?(Zipper.t() | Macro.t(), Macro.t()) :: boolean
def nodes_equal?(%Zipper{} = left, right) do
with zipper when not is_nil(zipper) <- Zipper.up(left),
{:defmodule, _, [{:__aliases__, _, parts}, _]} <- Zipper.node(zipper),
{:ok, env} <- current_env(zipper),
true <- nodes_equal?({:__aliases__, [], [Module.concat([env.module | parts])]}, right) do
true
else
_ ->
left
|> expand_aliases()
|> Zipper.node()
|> nodes_equal?(right)
end
end
def nodes_equal?(_left, %Zipper{}) do
raise ArgumentError, "right side of `nodes_equal?` must not be a zipper"
end
def nodes_equal?(v, v), do: true
def nodes_equal?(l, r) do
equal_vals?(l, r)
end
defp equal_vals?(same, same), do: true
defp equal_vals?({:__block__, _, [value]}, value) do
true
end
defp equal_vals?(value, {:__block__, _, [value]}) do
true
end
defp equal_vals?(
{:sigil_r, _, [{:<<>>, _, [left_contents]}, left_flags]},
{:sigil_r, _, [{:<<>>, _, [right_contents]}, right_flags]}
) do
equal_vals?(left_contents, right_contents) and equal_vals?(left_flags, right_flags)
end
defp equal_vals?(left, right) do
cond do
extendable_block?(left) ->
case left do
{:__block__, _, [left]} ->
nodes_equal?(left, right)
_ ->
equal_modules?(left, right)
end
extendable_block?(right) ->
case right do
{:__block__, _, [right]} ->
nodes_equal?(left, right)
_ ->
equal_modules?(left, right)
end
is_list(left) and is_list(right) ->
length(left) == length(right) and
Enum.all?(Enum.zip(left, right), fn {l, r} -> nodes_equal?(l, r) end)
true ->
equal_modules?(left, right)
end
end
@spec expand_alias(Zipper.t()) :: Zipper.t()
def expand_alias(zipper) do
case zipper.node do
{:__aliases__, _, parts} ->
case current_env(zipper) do
{:ok, env} ->
case do_expand_alias(env, [], parts) do
{:alias, value} ->
Zipper.replace(
zipper,
{:__aliases__, [], Enum.map(Module.split(value), &String.to_atom/1)}
)
_ ->
zipper
end
_ ->
zipper
end
_ ->
zipper
end
rescue
_ ->
zipper
end
if Code.ensure_loaded?(Macro.Env) && function_exported?(Macro.Env, :expand_alias, 3) do
defp do_expand_alias(env, meta, parts) do
Macro.Env.expand_alias(env, meta, parts)
end
else
defp do_expand_alias(_env, _meta, _parts) do
:error
end
end
@spec expand_aliases(Zipper.t()) :: Zipper.t()
def expand_aliases(zipper) do
Zipper.traverse(zipper, &expand_alias/1)
end
# aliases will confuse this, but that is a later problem :)
# probably the best thing we can do here is a pre-processing alias replacement pass?
# or I guess we'll have to pass the igniter in which tracks alias sources? Hard to say.
defp equal_modules?({:__aliases__, _, mod}, {:__aliases__, _, mod}), do: true
defp equal_modules?({:__aliases__, _, mod}, right) when is_atom(right) do
Module.concat(mod) == right
end
defp equal_modules?(left, {:__aliases__, _, mod}) when is_atom(left) do
Module.concat(mod) == left
end
defp equal_modules?(_left, _right) do
false
end
@compile {:inline, into: 2}
defp into(%Zipper{path: nil} = zipper, %Zipper{path: path, supertree: supertree}),
do: %{zipper | path: path, supertree: supertree}
defp supertree(%Zipper{supertree: supertree} = zipper) when not is_nil(supertree) do
zipper
|> Zipper.top()
|> into(supertree)
end
defp supertree(_), do: nil
defp quoted_literal?(%Zipper{} = zipper) do
quoted_literal?(zipper.node)
end
defp quoted_literal?(node) do
node
|> Sourceror.to_string()
|> Code.string_to_quoted()
|> case do
{:ok, quoted} -> Macro.quoted_literal?(quoted)
_ -> false
end
end
end