improvement: Add Igniter.Libs.Phoenix for working with Phoenix

improvement: deprecate duplicate `Igniter.Code.Module.move_to_use` function
improvement: `Igniter.Project.Config.configures?/4` that takes a config file
improvement: Add `Igniter.Util.Warning` for formatting code in warnings
This commit is contained in:
Zach Daniel 2024-07-15 14:06:25 -04:00
parent 29387cd2f2
commit d00dd671c7
7 changed files with 270 additions and 16 deletions

View file

@ -70,7 +70,11 @@ defmodule Igniter.Code.Function do
end end
@doc "Moves to a function call by the given name and arity, matching the given predicate, in the current scope" @doc "Moves to a function call by the given name and arity, matching the given predicate, in the current scope"
@spec move_to_function_call_in_current_scope(Zipper.t(), atom, non_neg_integer()) :: @spec move_to_function_call_in_current_scope(
Zipper.t(),
atom,
non_neg_integer() | list(non_neg_integer())
) ::
{:ok, Zipper.t()} | :error {:ok, Zipper.t()} | :error
def move_to_function_call_in_current_scope(zipper, name, arity, predicate \\ fn _ -> true end) def move_to_function_call_in_current_scope(zipper, name, arity, predicate \\ fn _ -> true end)

View file

@ -474,7 +474,7 @@ defmodule Igniter.Code.Module do
def move_to_module_using(zipper, module) do def move_to_module_using(zipper, module) do
with {:ok, mod_zipper} <- move_to_defmodule(zipper), with {:ok, mod_zipper} <- move_to_defmodule(zipper),
{:ok, mod_zipper} <- Igniter.Code.Common.move_to_do_block(mod_zipper), {:ok, mod_zipper} <- Igniter.Code.Common.move_to_do_block(mod_zipper),
{:ok, _} <- move_to_using(mod_zipper, module) do {:ok, _} <- move_to_use(mod_zipper, module) do
{:ok, mod_zipper} {:ok, mod_zipper}
else else
_ -> _ ->
@ -482,20 +482,19 @@ defmodule Igniter.Code.Module do
end end
end end
def move_to_using(zipper, using) do @deprecated "Use `move_to_use/2` instead."
Igniter.Code.Function.move_to_function_call_in_current_scope( def move_to_using(zipper, module), do: move_to_use(zipper, module)
zipper,
:use,
[1, 2],
fn zipper ->
with {:ok, actual_using} <- Igniter.Code.Function.move_to_nth_argument(zipper, 0) do
Igniter.Code.Common.nodes_equal?(actual_using, using)
end
end
)
end
@doc "Moves the zipper to the `use` statement for a provided module." @doc "Moves the zipper to the `use` statement for a provided module."
def move_to_use(zipper, [module]), do: move_to_use(zipper, module)
def move_to_use(zipper, [module | rest]) do
case move_to_use(zipper, module) do
{:ok, zipper} -> {:ok, zipper}
_ -> move_to_use(zipper, rest)
end
end
def move_to_use(zipper, module) do def move_to_use(zipper, module) do
Igniter.Code.Function.move_to_function_call_in_current_scope(zipper, :use, [1, 2], fn call -> Igniter.Code.Function.move_to_function_call_in_current_scope(zipper, :use, [1, 2], fn call ->
Igniter.Code.Function.argument_matches_predicate?( Igniter.Code.Function.argument_matches_predicate?(

208
lib/igniter/libs/phoenix.ex Normal file
View file

@ -0,0 +1,208 @@
defmodule Igniter.Libs.Phoenix do
@moduledoc "Codemods & utilities for working with Phoenix"
def add_scope(igniter, route, contents, opts \\ []) do
{igniter, router} =
case Keyword.fetch(opts, :router) do
{:ok, router} ->
{igniter, router}
:error ->
select_router(igniter)
end
scope_code =
case Keyword.fetch(opts, :arg2) do
{:ok, arg2} ->
contents = Sourceror.parse_string!(contents)
quote do
scope unquote(route), unquote(arg2) do
unquote(contents)
end
end
|> Sourceror.to_string()
_ ->
"""
scope #{inspect(route)} do
#{contents}
end
"""
end
if router do
Igniter.Code.Module.find_and_update_module!(igniter, router, fn zipper ->
case move_to_scope_location(zipper) do
{:ok, zipper, append_or_prepend} ->
{:ok, Igniter.Code.Common.add_code(zipper, scope_code, append_or_prepend)}
:error ->
{:warning,
Igniter.Util.Warning.formatted_warning(
"Could not add a scope for #{inspect(route)} to your router. Please add it manually.",
scope_code
)}
end
end)
else
Igniter.add_warning(
igniter,
Igniter.Util.Warning.formatted_warning(
"Could not add a scope for #{inspect(route)} to your router. Please add it manually.",
scope_code
)
)
end
end
def add_pipeline(igniter, name, contents, opts \\ []) do
{igniter, router} =
case Keyword.fetch(opts, :router) do
{:ok, router} ->
{igniter, router}
:error ->
select_router(igniter)
end
pipeline_code = """
pipeline #{inspect(name)} do
#{contents}
end
"""
if router do
Igniter.Code.Module.find_and_update_module!(igniter, router, fn zipper ->
Igniter.Code.Function.move_to_function_call(zipper, :pipeline, 2, fn zipper ->
Igniter.Code.Function.argument_matches_predicate?(
zipper,
0,
&Igniter.Code.Common.nodes_equal?(&1, name)
)
end)
|> case do
{:ok, _} ->
{:warning,
Igniter.Util.Warning.formatted_warning(
"The #{name} pipeline already exists in the router. Attempting to add scope: ",
pipeline_code
)}
_ ->
case move_to_pipeline_location(zipper) do
{:ok, zipper, append_or_prepend} ->
{:ok, Igniter.Code.Common.add_code(zipper, pipeline_code, append_or_prepend)}
:error ->
{:warning,
Igniter.Util.Warning.formatted_warning(
"Could not add the #{name} pipline to your router. Please add it manually.",
pipeline_code
)}
end
end
end)
else
Igniter.add_warning(
igniter,
Igniter.Util.Warning.formatted_warning(
"Could not add the #{name} pipline to your router. Please add it manually.",
pipeline_code
)
)
end
end
def select_router(igniter, label \\ "Which router should be modified?") do
case list_routers(igniter) do
{igniter, []} ->
{igniter, nil}
{igniter, [router]} ->
{igniter, router}
{igniter, routers} ->
{igniter, Owl.IO.select(routers, label: label, render_as: &inspect/1)}
end
end
def list_routers(igniter) do
Igniter.Code.Module.find_all_matching_modules(igniter, fn _mod, zipper ->
router_name =
Module.concat([to_string(Igniter.Code.Module.module_name_prefix()) <> "Web"])
with :error <-
Igniter.Code.Function.move_to_function_call(zipper, :use, 2, fn zipper ->
Igniter.Code.Function.argument_matches_predicate?(
zipper,
0,
&Igniter.Code.Common.nodes_equal?(&1, router_name)
) &&
Igniter.Code.Function.argument_matches_predicate?(
zipper,
1,
&Igniter.Code.Common.nodes_equal?(&1, :router)
)
end),
:error <- Igniter.Code.Module.move_to_use(zipper, Phoenix.Router) do
false
else
_ ->
true
end
end)
end
defp move_to_pipeline_location(zipper) do
with {:pipeline, :error} <-
{:pipeline,
Igniter.Code.Function.move_to_function_call_in_current_scope(zipper, :pipeline, 2)},
:error <-
Igniter.Code.Function.move_to_function_call_in_current_scope(zipper, :scope, [2, 3, 4]) do
case Igniter.Code.Module.move_to_use(zipper, Phoenix.Router) do
{:ok, zipper} -> {:ok, zipper, :after}
:error -> :error
end
else
{:pipeline, {:ok, zipper}} ->
{:ok, zipper, :after}
{:ok, zipper} ->
{:ok, zipper, :before}
end
end
defp move_to_scope_location(zipper) do
with :error <-
Igniter.Code.Function.move_to_function_call_in_current_scope(zipper, :scope, [2, 3, 4]),
{:pipeline, :error} <- {:pipeline, last_pipeline(zipper)} do
case Igniter.Code.Module.move_to_use(zipper, Phoenix.Router) do
{:ok, zipper} -> {:ok, zipper, :after}
:error -> :error
end
else
{:ok, zipper} ->
{:ok, zipper, :before}
{:pipeline, {:ok, zipper}} ->
{:ok, zipper, :after}
end
end
defp last_pipeline(zipper) do
case Igniter.Code.Function.move_to_function_call_in_current_scope(zipper, :pipeline, 2) do
{:ok, zipper} ->
with zipper when not is_nil(zipper) <- Sourceror.Zipper.right(zipper),
{:ok, zipper} <- last_pipeline(zipper) do
{:ok, zipper}
else
_ ->
{:ok, zipper}
end
:error ->
:error
end
end
end

View file

@ -225,8 +225,27 @@ defmodule Igniter.Project.Config do
end end
end end
@doc "Returns `true` if the given configuration path is set somewhere after the provided zipper." @doc "Returns `true` if the given configuration path is set somewhere after the provided zipper, or in the given configuration file."
@spec configures?(Zipper.t(), list(atom), atom()) :: boolean() @spec configures?(Igniter.t(), file :: String.t(), list(atom), atom()) :: boolean()
def configures?(igniter, file, path, app_name) do
file_path = Path.join("config", file)
igniter =
Igniter.include_existing_elixir_file(igniter, file_path, required?: false)
case Rewrite.source(igniter.rewrite, file_path) do
{:ok, source} ->
source
|> Rewrite.Source.get(:quoted)
|> Zipper.zip()
|> configures?(path, app_name)
_ ->
false
end
end
@spec configures?(zipper :: Zipper.t(), list(atom), atom()) :: boolean()
def configures?(zipper, config_path, app_name) do def configures?(zipper, config_path, app_name) do
if Enum.count(config_path) == 1 do if Enum.count(config_path) == 1 do
config_item = Enum.at(config_path, 0) config_item = Enum.at(config_path, 0)

View file

@ -0,0 +1,20 @@
defmodule Igniter.Util.Warning do
@moduledoc "Utilities for emitting well formatted warnings"
def warn_with_code_sample(igniter, message, code) do
Igniter.add_warning(igniter, formatted_warning(message, code))
end
def formatted_warning(message, code) do
formatted =
Code.format_string!(code)
|> IO.iodata_to_binary()
|> String.split("\n")
|> Enum.map_join("\n", &(" " <> &1))
"""
#{message}
#{formatted}
"""
end
end

View file

@ -91,6 +91,8 @@ defmodule Igniter.MixProject do
{:spitfire, "~> 0.1 and >= 0.1.3"}, {:spitfire, "~> 0.1 and >= 0.1.3"},
{:sourceror, "~> 1.4"}, {:sourceror, "~> 1.4"},
{:jason, "~> 1.4"}, {:jason, "~> 1.4"},
{:owl, "~> 0.9"},
{:ucwidth, "~> 0.2"},
# can't use spark because spark depends on this # can't use spark because spark depends on this
{:nimble_options, "~> 1.0"}, {:nimble_options, "~> 1.0"},
# Dev/Test dependencies # Dev/Test dependencies

View file

@ -24,10 +24,12 @@
"mix_test_watch": {:hex, :mix_test_watch, "1.2.0", "1f9acd9e1104f62f280e30fc2243ae5e6d8ddc2f7f4dc9bceb454b9a41c82b42", [:mix], [{:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "278dc955c20b3fb9a3168b5c2493c2e5cffad133548d307e0a50c7f2cfbf34f6"}, "mix_test_watch": {:hex, :mix_test_watch, "1.2.0", "1f9acd9e1104f62f280e30fc2243ae5e6d8ddc2f7f4dc9bceb454b9a41c82b42", [:mix], [{:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}], "hexpm", "278dc955c20b3fb9a3168b5c2493c2e5cffad133548d307e0a50c7f2cfbf34f6"},
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
"nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"}, "nimble_parsec": {:hex, :nimble_parsec, "1.4.0", "51f9b613ea62cfa97b25ccc2c1b4216e81df970acd8e16e8d1bdc58fef21370d", [:mix], [], "hexpm", "9c565862810fb383e9838c1dd2d7d2c437b3d13b267414ba6af33e50d2d1cf28"},
"owl": {:hex, :owl, "0.9.0", "9b33d64734bd51d3fc1d6ed01b12f8c2ed23e1fbf8c43658a6dfbff62578bd03", [:mix], [{:ucwidth, "~> 0.2", [hex: :ucwidth, repo: "hexpm", optional: true]}], "hexpm", "cd70b55327985f8f24d38cb7de5bf8a8d24040e1b49cca2345508f8119ce81fd"},
"rewrite": {:hex, :rewrite, "0.10.5", "6afadeae0b9d843b27ac6225e88e165884875e0aed333ef4ad3bf36f9c101bed", [:mix], [{:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "51cc347a4269ad3a1e7a2c4122dbac9198302b082f5615964358b4635ebf3d4f"}, "rewrite": {:hex, :rewrite, "0.10.5", "6afadeae0b9d843b27ac6225e88e165884875e0aed333ef4ad3bf36f9c101bed", [:mix], [{:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}], "hexpm", "51cc347a4269ad3a1e7a2c4122dbac9198302b082f5615964358b4635ebf3d4f"},
"sourceror": {:hex, :sourceror, "1.4.0", "be87319b1579191e25464005d465713079b3fd7124a3938a1e6cf4def39735a9", [:mix], [], "hexpm", "16751ca55e3895f2228938b703ad399b0b27acfe288eff6c0e629ed3e6ec0358"}, "sourceror": {:hex, :sourceror, "1.4.0", "be87319b1579191e25464005d465713079b3fd7124a3938a1e6cf4def39735a9", [:mix], [], "hexpm", "16751ca55e3895f2228938b703ad399b0b27acfe288eff6c0e629ed3e6ec0358"},
"spitfire": {:hex, :spitfire, "0.1.3", "7ea0f544005dfbe48e615ed90250c9a271bfe126914012023fd5e4b6b82b7ec7", [:mix], [], "hexpm", "d53b5107bcff526a05c5bb54c95e77b36834550affd5830c9f58760e8c543657"}, "spitfire": {:hex, :spitfire, "0.1.3", "7ea0f544005dfbe48e615ed90250c9a271bfe126914012023fd5e4b6b82b7ec7", [:mix], [], "hexpm", "d53b5107bcff526a05c5bb54c95e77b36834550affd5830c9f58760e8c543657"},
"statistex": {:hex, :statistex, "1.0.0", "f3dc93f3c0c6c92e5f291704cf62b99b553253d7969e9a5fa713e5481cd858a5", [:mix], [], "hexpm", "ff9d8bee7035028ab4742ff52fc80a2aa35cece833cf5319009b52f1b5a86c27"}, "statistex": {:hex, :statistex, "1.0.0", "f3dc93f3c0c6c92e5f291704cf62b99b553253d7969e9a5fa713e5481cd858a5", [:mix], [], "hexpm", "ff9d8bee7035028ab4742ff52fc80a2aa35cece833cf5319009b52f1b5a86c27"},
"ucwidth": {:hex, :ucwidth, "0.2.0", "1f0a440f541d895dff142275b96355f7e91e15bca525d4a0cc788ea51f0e3441", [:mix], [], "hexpm", "c1efd1798b8eeb11fb2bec3cafa3dd9c0c3647bee020543f0340b996177355bf"},
"yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"}, "yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"},
"yaml_elixir": {:hex, :yaml_elixir, "2.9.0", "9a256da867b37b8d2c1ffd5d9de373a4fda77a32a45b452f1708508ba7bbcb53", [:mix], [{:yamerl, "~> 0.10", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "0cb0e7d4c56f5e99a6253ed1a670ed0e39c13fc45a6da054033928607ac08dfc"}, "yaml_elixir": {:hex, :yaml_elixir, "2.9.0", "9a256da867b37b8d2c1ffd5d9de373a4fda77a32a45b452f1708508ba7bbcb53", [:mix], [{:yamerl, "~> 0.10", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "0cb0e7d4c56f5e99a6253ed1a670ed0e39c13fc45a6da054033928607ac08dfc"},
} }