mirror of
https://github.com/ash-project/igniter.git
synced 2024-09-19 21:12:54 +12:00
improvement: Module.find_and_update_or_create_module
Some checks are pending
CI / ash-ci (push) Waiting to run
Some checks are pending
CI / ash-ci (push) Waiting to run
This commit is contained in:
parent
0dc28d3e8b
commit
ccb0984314
6 changed files with 218 additions and 68 deletions
|
@ -1,2 +1,2 @@
|
|||
erlang 26.0.2
|
||||
elixir 1.17.0
|
||||
elixir 1.17.1
|
||||
|
|
|
@ -116,7 +116,7 @@ defmodule Igniter do
|
|||
glob =
|
||||
case glob do
|
||||
%GlobEx{} = glob -> glob
|
||||
string -> GlobEx.compile!(string)
|
||||
string -> GlobEx.compile!(Path.expand(string))
|
||||
end
|
||||
|
||||
igniter = include_glob(igniter, glob)
|
||||
|
@ -885,6 +885,7 @@ defmodule Igniter do
|
|||
end)
|
||||
end
|
||||
|
||||
# sobelow_skip ["RCE.CodeModule"]
|
||||
defp parse_igniter_config(igniter) do
|
||||
case Rewrite.source(igniter.rewrite, ".igniter.exs") do
|
||||
{:error, _} ->
|
||||
|
|
|
@ -7,7 +7,7 @@ defmodule Igniter.Code.Common do
|
|||
@doc """
|
||||
Moves to the next node that matches the predicate.
|
||||
"""
|
||||
@spec move_to(Zipper.t(), (Zipper.tree() -> Zipper.t())) :: {:ok, Zipper.t()} | :error
|
||||
@spec move_to(Zipper.t(), (Zipper.tree() -> boolean())) :: {:ok, Zipper.t()} | :error
|
||||
def move_to(zipper, pred) do
|
||||
Zipper.find(zipper, fn thing ->
|
||||
try do
|
||||
|
@ -26,6 +26,22 @@ defmodule Igniter.Code.Common do
|
|||
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.
|
||||
|
||||
|
@ -612,12 +628,21 @@ defmodule Igniter.Code.Common do
|
|||
|
||||
@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 |> Zipper.subtree() |> Zipper.node(),
|
||||
{:ok, env} <- current_env(zipper),
|
||||
true <- nodes_equal?({:__aliases__, [], [Module.concat([env.module | parts])]}, right) do
|
||||
true
|
||||
else
|
||||
_ ->
|
||||
left
|
||||
|> expand_aliases()
|
||||
|> Zipper.subtree()
|
||||
|> 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"
|
||||
|
@ -631,14 +656,14 @@ defmodule Igniter.Code.Common do
|
|||
|
||||
@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 current_env(zipper) do
|
||||
{:ok, env} ->
|
||||
case Macro.Env.expand_alias(env, [], parts) do
|
||||
{:alias, value} ->
|
||||
Zipper.replace(x, {:__aliases__, [], Module.split(value)})
|
||||
|
@ -650,11 +675,11 @@ defmodule Igniter.Code.Common do
|
|||
_ ->
|
||||
x
|
||||
end
|
||||
end)
|
||||
|
||||
_ ->
|
||||
zipper
|
||||
x
|
||||
end
|
||||
end)
|
||||
rescue
|
||||
_ ->
|
||||
zipper
|
||||
|
|
|
@ -4,6 +4,75 @@ defmodule Igniter.Code.Module do
|
|||
alias Igniter.Code.Common
|
||||
alias Sourceror.Zipper
|
||||
|
||||
@doc "Find or create module"
|
||||
def find_and_update_or_create_module(igniter, module_name, contents, updater) do
|
||||
igniter
|
||||
|> Igniter.include_glob("lib/**/*.ex")
|
||||
|> Map.get(:rewrite)
|
||||
|> Enum.find_value(fn source ->
|
||||
source
|
||||
|> Rewrite.Source.get(:quoted)
|
||||
|> Zipper.zip()
|
||||
|> Igniter.Code.Common.move_to_zipper(fn zipper ->
|
||||
with true <- Igniter.Code.Function.function_call?(zipper, :defmodule, 2),
|
||||
{:ok, inner_zipper} <- Igniter.Code.Function.move_to_nth_argument(zipper, 0),
|
||||
inner_zipper <- Igniter.Code.Common.expand_aliases(inner_zipper),
|
||||
true <-
|
||||
Igniter.Code.Common.nodes_equal?(
|
||||
inner_zipper,
|
||||
module_name
|
||||
) do
|
||||
{:ok, inner_zipper}
|
||||
else
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
end)
|
||||
|> case do
|
||||
{:ok, zipper} ->
|
||||
{source, zipper}
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
end)
|
||||
|> case do
|
||||
{source, zipper} ->
|
||||
case Common.move_to_do_block(zipper) do
|
||||
{:ok, zipper} ->
|
||||
case updater.(zipper) do
|
||||
{:ok, zipper} ->
|
||||
new_quoted =
|
||||
zipper
|
||||
|> Zipper.topmost()
|
||||
|> Zipper.node()
|
||||
|
||||
new_source = Rewrite.Source.update(source, :quoted, new_quoted)
|
||||
%{igniter | rewrite: Rewrite.update!(igniter.rewrite, new_source)}
|
||||
|
||||
{:error, error} ->
|
||||
Igniter.add_issue(igniter, error)
|
||||
|
||||
{:warning, error} ->
|
||||
Igniter.add_warning(igniter, error)
|
||||
end
|
||||
|
||||
_ ->
|
||||
igniter
|
||||
end
|
||||
|
||||
nil ->
|
||||
contents =
|
||||
"""
|
||||
defmodule #{inspect(module_name)} do
|
||||
#{contents}
|
||||
end
|
||||
"""
|
||||
|
||||
Igniter.create_new_elixir_file(igniter, proper_location(module_name), contents)
|
||||
end
|
||||
end
|
||||
|
||||
@doc "Given a suffix, returns a module name with the prefix of the current project."
|
||||
@spec module_name(String.t()) :: module()
|
||||
def module_name(suffix) do
|
||||
|
@ -18,9 +87,9 @@ defmodule Igniter.Code.Module do
|
|||
iex> Igniter.Code.Module.proper_location(MyApp.Hello)
|
||||
"lib/my_app/hello.ex"
|
||||
"""
|
||||
@spec proper_location(igniter :: Igniter.t() | nil, module()) :: Path.t()
|
||||
def proper_location(igniter \\ nil, module_name) do
|
||||
do_proper_location(igniter, module_name, :lib)
|
||||
@spec proper_location(module()) :: Path.t()
|
||||
def proper_location(module_name) do
|
||||
do_proper_location(module_name, :lib)
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
@ -35,9 +104,9 @@ defmodule Igniter.Code.Module do
|
|||
iex> Igniter.Code.Module.proper_test_location(MyApp.HelloTest)
|
||||
"test/my_app/hello_test.exs"
|
||||
"""
|
||||
@spec proper_test_location(igniter :: Igniter.t() | nil, module()) :: Path.t()
|
||||
def proper_test_location(igniter \\ nil, module_name) do
|
||||
do_proper_location(igniter, module_name, :test)
|
||||
@spec proper_test_location(module()) :: Path.t()
|
||||
def proper_test_location(module_name) do
|
||||
do_proper_location(module_name, :test)
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
@ -49,9 +118,9 @@ defmodule Igniter.Code.Module do
|
|||
iex> Igniter.Code.Module.proper_test_support_location(MyApp.DataCase)
|
||||
"test/support/data_case.ex"
|
||||
"""
|
||||
@spec proper_test_support_location(igniter :: Igniter.t() | nil, module()) :: Path.t()
|
||||
def proper_test_support_location(igniter \\ nil, module_name) do
|
||||
do_proper_location(igniter, module_name, :test_support)
|
||||
@spec proper_test_support_location(module()) :: Path.t()
|
||||
def proper_test_support_location(module_name) do
|
||||
do_proper_location(module_name, :test_support)
|
||||
end
|
||||
|
||||
@doc false
|
||||
|
@ -149,7 +218,7 @@ defmodule Igniter.Code.Module do
|
|||
split_from_path == split
|
||||
end
|
||||
|
||||
defp do_proper_location(igniter, module_name, kind) do
|
||||
defp do_proper_location(module_name, kind) do
|
||||
path =
|
||||
module_name
|
||||
|> Module.split()
|
||||
|
@ -174,35 +243,6 @@ defmodule Igniter.Code.Module do
|
|||
[_prefix | leading_rest] = leading
|
||||
Path.join(["test/support" | leading_rest] ++ ["#{last}.ex"])
|
||||
end
|
||||
|> apply_leaf_module_configuration(igniter)
|
||||
end
|
||||
|
||||
defp apply_leaf_module_configuration(path, nil), do: path
|
||||
|
||||
defp apply_leaf_module_configuration(path, igniter) do
|
||||
case Igniter.Project.IgniterConfig.get(igniter, :leaf_module_location) do
|
||||
:outside_folder ->
|
||||
path
|
||||
|
||||
:inside_folder ->
|
||||
path
|
||||
|> Path.split()
|
||||
|> Enum.reverse()
|
||||
|> Enum.split(2)
|
||||
|> case do
|
||||
{[filename, last_folder_name], rest} ->
|
||||
if Path.rootname(filename) == last_folder_name do
|
||||
[last_folder_name <> Path.extname(filename) | rest]
|
||||
|> Enum.reverse()
|
||||
|> Path.join()
|
||||
else
|
||||
path
|
||||
end
|
||||
|
||||
_ ->
|
||||
path
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def module?(zipper) do
|
||||
|
|
|
@ -54,7 +54,7 @@ defmodule Igniter.Project.IgniterConfig do
|
|||
unquote(config[:default])
|
||||
end
|
||||
|
||||
# TODO: when we have a way to comment ahead of a keyword item
|
||||
# when we have a way to comment ahead of a keyword item
|
||||
# we should comment the docs
|
||||
case Igniter.Code.Keyword.set_keyword_key(
|
||||
zipper,
|
||||
|
|
|
@ -20,17 +20,101 @@ defmodule Igniter.Code.ModuleTest do
|
|||
|
||||
assert "lib/foo/bar/bar.ex" in paths
|
||||
assert "lib/foo/bar/baz.ex" in paths
|
||||
end
|
||||
|
||||
# Igniter.Project.Config.configure(Igniter.new(), "fake.exs", :fake, [:foo, :bar], "baz")
|
||||
|
||||
# config_file = Rewrite.source!(rewrite, "config/fake.exs")
|
||||
|
||||
# assert Source.from?(config_file, :string)
|
||||
|
||||
# assert Source.get(config_file, :content) == """
|
||||
# import Config
|
||||
# config :fake, foo: [bar: "baz"]
|
||||
# """
|
||||
# end
|
||||
test "modules can be found anywhere across the project" do
|
||||
%{rewrite: rewrite} =
|
||||
Igniter.new()
|
||||
|> Igniter.create_new_elixir_file("lib/foo/bar.ex", """
|
||||
defmodule Foo.Bar do
|
||||
defmodule Baz do
|
||||
10
|
||||
end
|
||||
end
|
||||
""")
|
||||
|> Igniter.Code.Module.find_and_update_or_create_module(
|
||||
Foo.Bar.Baz,
|
||||
"""
|
||||
20
|
||||
""",
|
||||
fn zipper ->
|
||||
{:ok, Igniter.Code.Common.replace_code(zipper, 30)}
|
||||
end
|
||||
)
|
||||
|
||||
contents =
|
||||
rewrite
|
||||
|> Rewrite.source!("lib/foo/bar.ex")
|
||||
|> Rewrite.Source.get(:content)
|
||||
|
||||
assert contents == """
|
||||
defmodule Foo.Bar do
|
||||
defmodule Baz do
|
||||
30
|
||||
end
|
||||
end
|
||||
"""
|
||||
end
|
||||
|
||||
test "modules will be created if they do not exist, in the conventional place" do
|
||||
%{rewrite: rewrite} =
|
||||
Igniter.new()
|
||||
|> Igniter.create_new_elixir_file("lib/foo/bar.ex", """
|
||||
defmodule Foo.Bar do
|
||||
end
|
||||
""")
|
||||
|> Igniter.Code.Module.find_and_update_or_create_module(
|
||||
Foo.Bar.Baz,
|
||||
"""
|
||||
20
|
||||
""",
|
||||
fn zipper ->
|
||||
{:ok, Igniter.Code.Common.replace_code(zipper, 30)}
|
||||
end
|
||||
)
|
||||
|
||||
contents =
|
||||
rewrite
|
||||
|> Rewrite.source!("lib/foo/bar/baz.ex")
|
||||
|> Rewrite.Source.get(:content)
|
||||
|
||||
assert contents == """
|
||||
defmodule Foo.Bar.Baz do
|
||||
20
|
||||
end
|
||||
"""
|
||||
end
|
||||
|
||||
test "modules will be created if they do not exist, in the conventional place, which can be configured" do
|
||||
%{rewrite: rewrite} =
|
||||
Igniter.new()
|
||||
|> Igniter.assign(:igniter_exs,
|
||||
leaf_module_location: :inside_folder
|
||||
)
|
||||
|> Igniter.create_new_elixir_file("lib/foo/bar/something.ex", """
|
||||
defmodule Foo.Bar.Something do
|
||||
end
|
||||
""")
|
||||
|> Igniter.Code.Module.find_and_update_or_create_module(
|
||||
Foo.Bar,
|
||||
"""
|
||||
20
|
||||
""",
|
||||
fn zipper ->
|
||||
{:ok, Igniter.Code.Common.replace_code(zipper, 30)}
|
||||
end
|
||||
)
|
||||
|> Igniter.prepare_for_write()
|
||||
|
||||
contents =
|
||||
rewrite
|
||||
|> Rewrite.source!("lib/foo/bar/bar.ex")
|
||||
|> Rewrite.Source.get(:content)
|
||||
|
||||
assert contents == """
|
||||
defmodule Foo.Bar do
|
||||
20
|
||||
end
|
||||
"""
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue