Packages

spark

2.2.20
2.7.2 2.7.1 2.7.0 2.6.1 2.6.0 2.5.0 2.4.1 2.4.0 2.3.14 2.3.13 2.3.12 2.3.11 2.3.10 2.3.9 2.3.8 2.3.7 2.3.6 2.3.5 2.3.4 2.3.3 2.3.2 2.3.1 2.3.0 2.2.69 2.2.68 2.2.67 2.2.66 2.2.65 2.2.64 2.2.63 2.2.62 2.2.61 2.2.60 2.2.59 2.2.58 2.2.57 2.2.56 2.2.55 2.2.54 2.2.53 2.2.52 2.2.51 2.2.50 2.2.49 2.2.48 2.2.47 2.2.46 2.2.45 2.2.44 2.2.43 2.2.42 2.2.41 2.2.40 2.2.39 2.2.38 2.2.37 2.2.36 2.2.35 2.2.34 2.2.33 2.2.32 2.2.31 2.2.30 2.2.29 2.2.28 2.2.27 2.2.26 2.2.25 2.2.24 2.2.23 2.2.22 2.2.21 2.2.20 2.2.19 2.2.18 2.2.17 2.2.16 2.2.15 2.2.14 2.2.13 2.2.12 2.2.11 2.2.10 2.2.9 2.2.8 2.2.7 2.2.6 2.2.5 2.2.4 2.2.3 2.2.2 2.2.1 2.2.0 2.1.24 2.1.23 2.1.22 2.1.21 2.1.20 2.1.19 2.1.18 2.1.17 2.1.16 2.1.15 2.1.14 2.1.13 2.1.12 2.1.11 2.1.10 2.1.9 2.1.8 2.1.7 2.1.6 2.1.5 2.1.4 2.1.3 2.1.2 2.1.1 2.1.0 2.0.1 2.0.0 1.1.55 1.1.54 1.1.53 1.1.52 1.1.51 1.1.50 1.1.49 1.1.48 1.1.47 1.1.46 1.1.45 1.1.44 1.1.43 1.1.42 1.1.41 1.1.40 1.1.39 1.1.38 1.1.37 1.1.36 1.1.35 1.1.34 1.1.32 1.1.31 1.1.30 1.1.29 1.1.28 1.1.27 1.1.26 1.1.25 1.1.24 1.1.22 1.1.21 1.1.20 1.1.19 1.1.18 1.1.17 1.1.16 1.1.15 1.1.14 retired 1.1.13 1.1.12 1.1.11 1.1.10 1.1.9 1.1.8 1.1.7 1.1.6 1.1.5 1.1.4 1.1.3 1.1.2 1.1.1 1.1.0 1.0.9 1.0.8 1.0.7 1.0.6 1.0.5 1.0.4 1.0.3 1.0.2 1.0.1 1.0.0 0.4.12 0.4.11 0.4.10 0.4.9 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.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.18 0.2.17 0.2.16 0.2.15 0.2.14 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.29 0.1.28 0.1.27 0.1.26 0.1.25 0.1.24 0.1.23 0.1.22 0.1.21 0.1.20 0.1.19 0.1.18 0.1.17 0.1.16 0.1.15 0.1.14 0.1.13 0.1.12 0.1.11 0.1.10 0.1.9 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

Generic tooling for building DSLs

Current section

Files

Jump to
spark lib spark igniter.ex
Raw

lib/spark/igniter.ex

defmodule Spark.Igniter do
@moduledoc """
Helpers for patching Spark DSLs.
"""
require Igniter.Code.Common
alias Sourceror.Zipper
@doc "Prepends a new section or list of sections to the section order in a formatter configuration."
def prepend_to_section_order(igniter, type, sections) do
sections = List.wrap(sections)
Igniter.Project.Config.configure(
igniter,
"config.exs",
:spark,
[:formatter, type, :section_order],
sections,
updater: fn zipper ->
sections
|> Enum.reverse()
|> Enum.reduce({:ok, zipper}, fn section, {:ok, zipper} ->
case Igniter.Code.List.prepend_new_to_list(zipper, section) do
{:ok, zipper} -> {:ok, zipper}
:error -> {:ok, zipper}
end
end)
end
)
end
@doc "Sets an option at a given path within in a DSL."
@spec set_option(
Igniter.t(),
module,
dsl_path :: list(atom),
value :: term(),
(Zipper.t() -> {:ok, Zipper.t()} | {:error, term | list(term)} | :error)
) :: Igniter.t()
def set_option(igniter, module, path, value, updater \\ &{:ok, &1}) do
{body, [last]} = Enum.split(List.wrap(path), -1)
path = Enum.map(body, &{:section, &1}) ++ [{:option, last}]
update_dsl(igniter, module, path, value, updater)
end
def update_dsl(igniter, module, path, value, func) do
Igniter.Code.Module.find_and_update_module!(igniter, module, fn zipper ->
do_update_dsl(zipper, path, value, func)
end)
end
defp do_update_dsl(zipper, [], value, fun) do
if Igniter.Code.Common.node_matches_pattern?(zipper, v when is_nil(v)) do
{:ok, Sourceror.Zipper.replace(zipper, value)}
else
fun.(zipper)
end
end
defp do_update_dsl(zipper, [{:section, name} | rest], value, fun) do
with {:ok, zipper} <-
Igniter.Code.Function.move_to_function_call_in_current_scope(zipper, name, 1),
{:ok, zipper} <- Igniter.Code.Common.move_to_do_block(zipper) do
do_update_dsl(zipper, rest, value, fun)
else
_ ->
zipper =
Igniter.Code.Common.add_code(zipper, """
#{name} do
end
""")
with {:ok, zipper} <-
Igniter.Code.Function.move_to_function_call_in_current_scope(zipper, name, 1),
{:ok, zipper} <- Igniter.Code.Common.move_to_do_block(zipper) do
do_update_dsl(zipper, rest, value, fun)
else
_ ->
{:ok, zipper}
end
end
end
defp do_update_dsl(zipper, [{:option, name}], value, fun) do
case Igniter.Code.Function.move_to_function_call_in_current_scope(zipper, name, 1) do
{:ok, zipper} ->
Igniter.Code.Function.update_nth_argument(zipper, 0, fun)
_ ->
added_zipper = Igniter.Code.Common.add_code(zipper, {name, [], [value]})
case Igniter.Code.Function.move_to_function_call_in_current_scope(added_zipper, name, 1) do
{:ok, zipper} ->
{:ok, zipper}
_ ->
{:ok, zipper}
end
end
end
defp do_update_dsl(_zipper, [{:option, name} | _], _value, _fun) do
raise ArgumentError,
"{:option, #{inspect(name)}} was found as a non-leaf node in a path to update. Options must be the last item in the list."
end
@doc "Removes an extension from a DSL module."
@spec remove_extension(Igniter.t(), module(), module(), atom(), module(), boolean()) ::
Igniter.t()
def remove_extension(igniter, module, type, key, extension, singleton? \\ false) do
Igniter.Code.Module.find_and_update_module!(igniter, module, fn zipper ->
with {:ok, zipper} <- Igniter.Code.Module.move_to_use(zipper, type) do
if Igniter.Code.Common.node_matches_pattern?(zipper, {_, _, [_]}) do
:error
else
if singleton? do
case Igniter.Code.Keyword.remove_keyword_key(zipper, key) do
{:ok, zipper} ->
{:ok, zipper}
_ ->
:error
end
else
Igniter.Code.Keyword.put_in_keyword(zipper, [key], [], fn zipper ->
Igniter.Code.List.remove_from_list(zipper, fn nested_zipper ->
Igniter.Code.Common.nodes_equal?(nested_zipper, extension)
end)
end)
end
end
end
|> case do
:error ->
{:ok, zipper}
{:ok, zipper} ->
do_remove_constructors(zipper, type, extension)
end
end)
end
defp do_remove_constructors(zipper, type, extension) do
constructors_to_remove = extension_constructors(extension)
zipper = Zipper.topmost(zipper)
{:ok, zipper} = Igniter.Code.Module.move_to_module_using(zipper, type)
remove_constructors(zipper, constructors_to_remove)
end
@doc "Returns `{igniter, true}` if the module has the extension, or `{igniter, false}` otherwise."
@spec has_extension(Igniter.t(), module(), module(), atom(), module()) ::
{Igniter.t(), boolean()}
def has_extension(igniter, module, type, key, extension) do
case Igniter.Code.Module.find_module(igniter, module) do
{:ok, {igniter, _source, zipper}} ->
with {:ok, zipper} <- Igniter.Code.Module.move_to_use(zipper, type),
{:ok, zipper} <- Igniter.Code.Function.move_to_nth_argument(zipper, 1),
{:ok, zipper} <- Igniter.Code.Keyword.get_key(zipper, key) do
match? =
if Igniter.Code.List.list?(zipper) do
match?(
{:ok, _},
Igniter.Code.List.move_to_list_item(
zipper,
&Igniter.Code.Common.nodes_equal?(&1, extension)
)
)
else
Igniter.Code.Common.nodes_equal?(zipper, extension)
end
{igniter, match?}
else
_ ->
{igniter, false}
end
{:error, igniter} ->
{igniter, false}
end
end
# sobelow_skip ["DOS.StringToAtom"]
@doc "Adds an extension to a DSL module."
@spec add_extension(Igniter.t(), module(), module(), atom(), module(), boolean()) :: Igniter.t()
def add_extension(igniter, module, type, key, extension, singleton? \\ false) do
extension = {:__aliases__, [], Enum.map(Module.split(extension), &String.to_atom/1)}
Igniter.Code.Module.find_and_update_module!(igniter, module, fn zipper ->
case Igniter.Code.Module.move_to_use(zipper, type) do
{:ok, zipper} ->
if Igniter.Code.Common.node_matches_pattern?(zipper, {_, _, [_]}) do
value =
if singleton? do
extension
else
[extension]
end
Igniter.Code.Function.append_argument(zipper, [{key, value}])
else
remove_extension =
if singleton? do
with {:ok, arg_zipper} <- Igniter.Code.Function.move_to_nth_argument(zipper, 1),
{:ok, value_zipper} <- Igniter.Code.Keyword.get_key(arg_zipper, key),
module_zipper <- Igniter.Code.Common.expand_aliases(value_zipper),
{:__aliases__, _, parts} <- Zipper.node(module_zipper) do
{:ok, Module.concat(parts)}
else
_ ->
:error
end
else
:error
end
Igniter.Code.Function.update_nth_argument(zipper, 1, fn zipper ->
if singleton? do
case Igniter.Code.Keyword.put_in_keyword(zipper, [key], extension, fn x ->
{:ok, Igniter.Code.Common.replace_code(x, extension)}
end) do
{:ok, zipper} ->
{:ok, zipper}
_ ->
{:ok, zipper}
end
else
Igniter.Code.Keyword.put_in_keyword(zipper, [key], [extension], fn zipper ->
Igniter.Code.List.prepend_new_to_list(
zipper,
extension
)
end)
end
end)
|> case do
{:ok, zipper} ->
case remove_extension do
{:ok, module} ->
if Code.ensure_loaded?(module) &&
Spark.implements_behaviour?(module, Spark.Dsl.Extension) do
do_remove_constructors(zipper, type, module)
else
{:ok, zipper}
end
:error ->
{:ok, zipper}
end
:error ->
:error
end
end
_ ->
{:ok, zipper}
end
end)
end
defp remove_constructors(zipper, []), do: {:ok, zipper}
defp remove_constructors(zipper, [{name, arity} | rest] = all) do
Igniter.Code.Common.within(zipper, fn zipper ->
case Igniter.Code.Function.move_to_function_call_in_current_scope(zipper, name, arity) do
{:ok, zipper} ->
{:ok, Zipper.remove(zipper)}
:error ->
:error
end
end)
|> case do
:error ->
remove_constructors(zipper, rest)
{:ok, zipper} ->
remove_constructors(zipper, all)
end
end
defp extension_constructors(module) do
module.sections()
|> Enum.flat_map(fn section ->
if section.top_level? do
nested_entities =
Enum.flat_map(section.entities, fn entity ->
total_count = Enum.count(entity.args)
optional_count = Enum.count(entity.args, &(not is_atom(&1)))
required_count = total_count - optional_count
Enum.map(required_count..total_count, fn i ->
{entity.name, i}
end)
end)
nested_sections =
Enum.map(section.sections, fn section ->
{section.name, 1}
end)
nested_entities ++ nested_sections
else
[{section.name, 1}]
end
end)
end
end