Packages
igniter
0.2.2
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/code/common.ex
defmodule Igniter.Code.Common do
@moduledoc """
General purpose utilities for working with `Sourceror.Zipper`.
"""
alias Sourceror.Zipper
@doc """
Moves to the next node that matches the predicate.
"""
@spec move_to(Zipper.t(), (Zipper.tree() -> Zipper.t())) :: {:ok, Zipper.t()} | :error
def move_to(zipper, pred) do
Zipper.find(zipper, fn thing ->
try do
pred.(thing)
rescue
FunctionClauseError ->
false
end
end)
|> case do
nil ->
:error
zipper ->
{:ok, zipper}
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.subtree()
|> Zipper.root()
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 """
Adds the provided code to the zipper.
Use `placement` to determine if the code goes `:after` or `:before` the current node.
## 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(), :after | :before) :: Zipper.t()
def add_code(zipper, new_code, placement \\ :after)
def add_code(zipper, new_code, placement) when is_binary(new_code) do
code = Sourceror.parse_string!(new_code)
add_code(zipper, code, placement)
end
def add_code(zipper, new_code, placement) do
do_add_code(zipper, new_code, placement)
end
defp do_add_code(zipper, new_code, placement, expand_env? \\ true) do
current_code =
zipper
|> Zipper.subtree()
new_code =
if expand_env? do
use_aliases(new_code, current_code)
else
new_code
end
current_code = Zipper.root(current_code)
case current_code do
{:__block__, meta, stuff} ->
new_stuff =
if placement == :after do
stuff ++ [new_code]
else
[new_code | stuff]
end
Zipper.replace(zipper, {:__block__, meta, new_stuff})
code ->
zipper
|> highest_adjacent_block()
|> case do
nil ->
if placement == :after do
Zipper.replace(zipper, {:__block__, [], [code, new_code]})
else
Zipper.replace(zipper, {:__block__, [], [new_code, code]})
end
upwards ->
upwards
|> Zipper.subtree()
|> Zipper.root()
|> case do
{:__block__, meta, stuff} ->
new_stuff =
if placement == :after do
List.wrap(stuff) ++ [new_code]
else
case List.wrap(stuff) do
[first | rest] ->
[first, new_code | rest]
_ ->
[new_code | stuff]
end
end
Zipper.replace(upwards, {:__block__, meta, new_stuff})
_ ->
if placement == :after do
Zipper.replace(zipper, {:__block__, [], [code, new_code]})
else
Zipper.replace(zipper, {:__block__, [], [new_code, code]})
end
end
end
end
end
def replace_code(zipper, code) when is_binary(code) do
add_code(zipper, Sourceror.parse_string!(code))
end
def replace_code(zipper, code) do
current_code =
zipper
|> Zipper.subtree()
code = use_aliases(code, current_code)
Zipper.replace(zipper, code)
end
defp highest_adjacent_block(zipper) do
case Zipper.up(zipper) do
nil ->
nil
upwards ->
upwards
|> Zipper.node()
|> case do
{:__block__, _, _} ->
case highest_adjacent_block(upwards) do
nil -> upwards
zipper -> zipper
end
_ ->
nil
end
end
end
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
node ->
node
end)
_ ->
new_code
end
end
# sobelow_skip ["DOS.StringToAtom"]
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
zipper
|> Zipper.subtree()
|> Zipper.root()
|> case do
{:__block__, _, [_]} ->
zipper
|> Zipper.down()
|> case do
nil ->
zipper
zipper ->
maybe_move_to_single_child_block(zipper)
end
_ ->
zipper
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
zipper
|> Zipper.subtree()
|> Zipper.root()
|> case do
{:__block__, _, _} ->
zipper
|> Zipper.down()
|> case do
nil ->
zipper
zipper ->
zipper
end
_ ->
zipper
end
end
@doc "Moves the zipper right n times, returning `:error` if it can't move that many times."
@spec nth_right(Zipper.t(), non_neg_integer()) :: {:ok, Zipper.t()} | :error
def nth_right(zipper, 0) do
{:ok, zipper}
end
def nth_right(zipper, n) do
zipper
|> Zipper.right()
|> case do
nil ->
:error
zipper ->
nth_right(zipper, n - 1)
end
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_right(zipper, &move_to_cursor(&1, pattern)) do
:error ->
:error
{:ok, zipper} ->
move_to_cursor(zipper, pattern)
end
end
@doc """
Moves right in the zipper, until the provided predicate returns `true`.
Returns `:error` if the end is reached without finding a match.
"""
@spec move_right(Zipper.t(), (Zipper.t() -> boolean)) :: {:ok, Zipper.t()} | :error
def move_right(%Zipper{} = zipper, pred) do
zipper
|> maybe_move_to_block()
|> do_move_right(pred)
end
defp do_move_right(zipper, pred) do
zipper_in_block = maybe_move_to_single_child_block(zipper)
cond do
pred.(zipper_in_block) ->
{:ok, zipper_in_block}
pred.(zipper) ->
{:ok, zipper}
true ->
case Zipper.right(zipper) do
nil ->
:error
zipper ->
zipper
|> move_right(pred)
end
end
end
# keeping in mind that version returns `nil` on no match
@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.subtree()
|> Zipper.node()
# => 10
```
"""
@spec move_to_cursor(Zipper.t(), Zipper.t() | String.t()) :: {:ok, Zipper.t()} | :error
def move_to_cursor(%Zipper{} = zipper, pattern) when is_binary(pattern) do
pattern
|> Sourceror.parse_string!()
|> Zipper.zip()
|> then(&do_move_to_cursor(zipper, &1))
end
def move_to_cursor(%Zipper{} = zipper, %Zipper{} = pattern_zipper) do
do_move_to_cursor(zipper, pattern_zipper)
end
defp do_move_to_cursor(%Zipper{} = zipper, %Zipper{} = pattern_zipper) do
cond do
cursor?(pattern_zipper |> Zipper.subtree() |> Zipper.node()) ->
{:ok, zipper}
match_type = zippers_match(zipper, pattern_zipper) ->
move =
case match_type do
:skip -> &Zipper.skip/1
:next -> &Zipper.next/1
end
with zipper when not is_nil(zipper) <- move.(zipper),
pattern_zipper when not is_nil(pattern_zipper) <- move.(pattern_zipper) do
do_move_to_cursor(zipper, pattern_zipper)
end
true ->
:error
end
end
defp cursor?({:__cursor__, _, []}), do: true
defp cursor?(_other), do: false
defp zippers_match(zipper, pattern_zipper) do
zipper_node =
zipper
|> Zipper.subtree()
|> Zipper.node()
pattern_node =
pattern_zipper
|> Zipper.subtree()
|> Zipper.node()
case {zipper_node, pattern_node} do
{_, {:__, _, _}} ->
:skip
{{call, _, _}, {call, _, _}} ->
:next
{{_, _}, {_, _}} ->
:next
{same, same} ->
:next
{left, right} when is_list(left) and is_list(right) ->
:next
_ ->
false
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
zipper
|> do_add_code({:__cursor__, [], []}, :after, 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}
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
:error ->
:error
{:ok, zipper} ->
{:ok,
zipper
|> Zipper.top()
|> into(top_zipper)}
end
end
@spec nodes_equal?(Zipper.t() | Macro.t(), Macro.t()) :: boolean
def nodes_equal?(%Zipper{} = left, right) do
left
|> expand_aliases()
|> Zipper.subtree()
|> Zipper.node()
|> nodes_equal?(right)
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_modules?(l, r)
end
@spec expand_aliases(Zipper.t()) :: Zipper.t()
def expand_aliases(zipper) do
case current_env(zipper) do
{:ok, env} ->
Zipper.traverse(zipper, fn x ->
x
|> Zipper.subtree()
|> Zipper.node()
|> case do
{:__aliases__, _, parts} ->
case Macro.Env.expand_alias(env, [], parts) do
{:alias, value} ->
Zipper.replace(x, {:__aliases__, [], Module.split(value)})
_ ->
x
end
_ ->
x
end
end)
_ ->
zipper
end
rescue
_ ->
zipper
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}
end