Current section

Files

Jump to
cinder lib mix tasks cinder.upgrade.ex
Raw

lib/mix/tasks/cinder.upgrade.ex

if Code.ensure_loaded?(Igniter) do
defmodule Mix.Tasks.Cinder.Upgrade do
@moduledoc """
Tasks for automatic migration of your code between various versions of Cinder.
"""
use Igniter.Mix.Task
@theme_key_renames %{
filter_boolean_container_class: :filter_radio_group_container_class,
filter_boolean_option_class: :filter_radio_group_option_class,
filter_boolean_radio_class: :filter_radio_group_radio_class,
filter_boolean_label_class: :filter_radio_group_label_class
}
@impl Igniter.Mix.Task
def info(_argv, _composing_task) do
%Igniter.Mix.Task.Info{
group: :cinder,
adds_deps: [],
installs: [],
positional: [:from, :to],
composes: [],
schema: [],
defaults: [],
aliases: [],
required: []
}
end
@impl Igniter.Mix.Task
def igniter(igniter) do
positional = igniter.args.positional
options = igniter.args.options
upgrades =
%{
"0.9.0" => [&flatten_theme_component_blocks/2],
"0.10.0" => [&rename_boolean_theme_keys/2]
}
Igniter.Upgrades.run(igniter, positional.from, positional.to, upgrades,
custom_opts: options
)
end
# --- 0.9.0: Flatten component/2 blocks to flat set calls ---
@doc false
def flatten_theme_component_blocks(igniter, _opts) do
Igniter.update_all_elixir_files(igniter, fn zipper ->
with {:ok, _zipper} <- Igniter.Code.Module.move_to_module_using(zipper, [Cinder.Theme]) do
# Work on the source text directly since this is a structural transform
source_string = zipper |> Sourceror.Zipper.topmost_root() |> Sourceror.to_string()
new_string = transform_component_blocks(source_string)
if new_string == source_string do
{:ok, zipper}
else
case Sourceror.parse_string(new_string) do
{:ok, new_ast} ->
new_zipper = Sourceror.Zipper.zip(new_ast)
{:ok, new_zipper}
{:error, _} ->
{:ok, zipper}
end
end
else
_ -> {:ok, zipper}
end
end)
end
@doc false
def transform_component_blocks(source_string) do
case Sourceror.parse_string(source_string) do
{:ok, ast} ->
patches = collect_component_patches(ast)
if patches == [] do
source_string
else
source_string
|> Sourceror.patch_string(patches)
|> Code.format_string!(locals_without_parens: [set: 2, extends: 1])
|> IO.iodata_to_binary()
end
{:error, _} ->
source_string
end
end
defp collect_component_patches(ast) do
{_ast, patches} =
Macro.prewalk(ast, [], fn
# Pattern for Sourceror AST: do block is stored as {{:__block__, _, [:do]}, body}
{:component, _meta, [module_ast, [{{:__block__, _, [:do]}, body}]]} = node, acc ->
extract_and_patch_component(node, module_ast, body, acc)
# Pattern for standard AST: do block is stored as {:do, body}
{:component, _meta, [module_ast, [do: body]]} = node, acc ->
extract_and_patch_component(node, module_ast, body, acc)
node, acc ->
{node, acc}
end)
Enum.reverse(patches)
end
defp extract_and_patch_component(node, module_ast, body, acc) do
comment = module_to_comment(module_ast)
set_calls =
case body do
{:__block__, _, calls} -> calls
single_call -> [single_call]
end
replacement_text = build_replacement_text(comment, set_calls)
range = Sourceror.get_range(node)
if range do
patch = %{range: range, change: replacement_text}
{node, [patch | acc]}
else
{node, acc}
end
end
@doc false
def module_to_comment(module_ast) do
case module_ast do
{:__aliases__, _, parts} ->
parts |> List.last() |> to_string()
atom when is_atom(atom) ->
atom |> Module.split() |> List.last()
_ ->
"Component"
end
end
defp build_replacement_text(comment, set_calls) do
set_call_strings =
Enum.map(set_calls, fn call ->
Sourceror.to_string(call)
end)
case set_call_strings do
[] ->
"# #{comment}"
calls ->
"# #{comment}\n" <> Enum.join(calls, "\n")
end
end
# --- 0.10.0: Rename filter_boolean_* theme keys to filter_radio_group_* ---
@doc false
def rename_boolean_theme_keys(igniter, _opts) do
Igniter.update_all_elixir_files(igniter, fn zipper ->
with {:ok, zipper} <- Igniter.Code.Module.move_to_module_using(zipper, [Cinder.Theme]) do
zipper = Sourceror.Zipper.top(zipper)
zipper =
Enum.reduce(@theme_key_renames, zipper, fn {old_key, new_key}, zipper ->
rename_set_calls(zipper, old_key, new_key)
end)
{:ok, zipper}
else
_ -> {:ok, zipper}
end
end)
end
defp rename_set_calls(zipper, old_key, new_key) do
zipper
|> Igniter.Code.Common.find_all(fn z ->
match?(
{:set, _, [{:__block__, _, [^old_key]}, _]},
Sourceror.Zipper.node(z)
)
end)
|> Enum.reduce(zipper, fn _found_zipper, acc_zipper ->
case Igniter.Code.Common.move_to(acc_zipper, fn z ->
match?(
{:set, _, [{:__block__, _, [^old_key]}, _]},
Sourceror.Zipper.node(z)
)
end) do
{:ok, found} ->
{:set, meta, [{:__block__, key_meta, [^old_key]}, value]} =
Sourceror.Zipper.node(found)
new_node = {:set, meta, [{:__block__, key_meta, [new_key]}, value]}
Sourceror.Zipper.replace(found, new_node) |> Sourceror.Zipper.top()
_ ->
acc_zipper
end
end)
end
end
else
defmodule Mix.Tasks.Cinder.Upgrade do
@moduledoc false
use Mix.Task
@impl Mix.Task
def run(_argv) do
Mix.shell().error("""
The task 'cinder.upgrade' requires igniter. Please install igniter and try again.
For more information, see: https://hexdocs.pm/igniter/readme.html#installation
""")
exit({:shutdown, 1})
end
end
end