Packages
igniter
0.5.17
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/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