Packages
spark
2.2.39
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
Current section
Files
lib/spark/igniter.ex
if Code.ensure_loaded?(Igniter) do
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 """
Gets an option at a given path within a DSL.
We will attempt to expand literals using the environment at the path
but this is only guaranteed to return the *AST* at that option, *not* necessarily a value.
Additionally, this only finds options set explicitly in the body of the resource, not by an extension.
"""
@spec get_option(Igniter.t(), module(), list(atom)) ::
{Igniter.t(), {:ok, Macro.t()} | :error}
def get_option(igniter, module, path) do
{body, [last]} = Enum.split(List.wrap(path), -1)
path = Enum.map(body, &{:section, &1}) ++ [{:option, last}]
find(igniter, module, fn _search_module, zipper ->
do_get_option(zipper, path)
end)
|> case do
{:ok, igniter, _module, value} -> {igniter, {:ok, value}}
{:error, igniter} -> {igniter, :error}
end
end
defp do_get_option(zipper, [{:option, name}]) do
with {:ok, zipper} <-
Igniter.Code.Function.move_to_function_call_in_current_scope(zipper, name, 1),
{:ok, zipper} <- Igniter.Code.Function.move_to_nth_argument(zipper, 0) do
zipper = Igniter.Code.Common.maybe_move_to_single_child_block(zipper)
case Igniter.Code.Common.expand_literal(zipper) do
{:ok, value} -> {:ok, value}
:error -> {:ok, zipper.node}
end
else
_ -> :error
end
end
defp do_get_option(zipper, [{:section, name} | rest]) 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_get_option(zipper, rest)
else
_ -> :error
end
end
@doc "Searches for a match to a zipper function inside a DSL and all of its fragments."
@spec find(Igniter.t(), module, (module(), Zipper.t() -> {:ok, value} | :error)) ::
{:ok, Igniter.t(), module(), value} | {:error, Igniter.t()}
when value: term()
def find(igniter, module, callback) do
{:ok, {igniter, _source, zipper}} = Igniter.Project.Module.find_module(igniter, module)
zipper =
case Igniter.Code.Common.move_to_do_block(zipper) do
{:ok, zipper} -> zipper
_ -> zipper
end
zipper
|> search_modules(module)
|> Enum.reduce_while({igniter, :error}, fn search_module, {igniter, :error} ->
{igniter, zipper} =
if search_module == module do
{igniter, {:ok, zipper}}
else
with {:ok, {igniter, _source, zipper}} <-
Igniter.Project.Module.find_module(igniter, search_module),
{:ok, zipper} <- Igniter.Code.Common.move_to_do_block(zipper) do
{igniter, {:ok, zipper}}
else
{:error, igniter} ->
{igniter, :error}
_ ->
{igniter, :error}
end
end
with {:ok, zipper} <- zipper,
{:ok, value} <- callback.(search_module, zipper) do
{:halt, {:ok, igniter, search_module, value}}
else
_ -> {:cont, {igniter, :error}}
end
end)
|> case do
{igniter, :error} -> {:error, igniter}
{:ok, igniter, search_module, value} -> {:ok, igniter, search_module, value}
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.Project.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.Project.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.Project.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.Project.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
# sobelow_skip ["RCE.CodeModule"]
defp search_modules(zipper, base) do
with {:ok, zipper} <-
Igniter.Code.Function.move_to_function_call_in_current_scope(
zipper,
:use,
2,
fn zipper ->
with {:ok, zipper} <- Igniter.Code.Function.move_to_nth_argument(zipper, 1),
{:ok, _} <- Igniter.Code.Keyword.get_key(zipper, :fragments) do
true
else
_ ->
false
end
end
),
{:ok, zipper} <- Igniter.Code.Function.move_to_nth_argument(zipper, 1),
{:ok, zipper} <- Igniter.Code.Keyword.get_key(zipper, :fragments) do
evaled =
try do
case Igniter.Code.Common.expand_literal(zipper) do
{:ok, value} -> value
:error -> Code.eval_quoted(zipper.node)
end
rescue
_e ->
[]
end
if is_list(evaled) do
Enum.uniq([base | Enum.filter(evaled, &is_atom/1)])
else
[base]
end
else
_ ->
[base]
end
end
end
end