improvement: Module.find_and_update_or_create_module
Some checks are pending
CI / ash-ci (push) Waiting to run

This commit is contained in:
Zach Daniel 2024-06-28 00:36:04 -04:00
parent 0dc28d3e8b
commit ccb0984314
6 changed files with 218 additions and 68 deletions

View file

@ -1,2 +1,2 @@
erlang 26.0.2
elixir 1.17.0
elixir 1.17.1

View file

@ -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, _} ->

View file

@ -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,11 +628,20 @@ defmodule Igniter.Code.Common do
@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)
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
@ -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} ->
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
end
_ ->
x
end
end)
rescue
_ ->
zipper

View file

@ -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

View file

@ -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,

View file

@ -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")
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
)
# config_file = Rewrite.source!(rewrite, "config/fake.exs")
contents =
rewrite
|> Rewrite.source!("lib/foo/bar.ex")
|> Rewrite.Source.get(:content)
# assert Source.from?(config_file, :string)
assert contents == """
defmodule Foo.Bar do
defmodule Baz do
30
end
end
"""
end
# assert Source.get(config_file, :content) == """
# import Config
# config :fake, foo: [bar: "baz"]
# """
# 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