Packages

spark

1.1.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
spark lib spark formatter.ex
Raw

lib/spark/formatter.ex

defmodule Spark.Formatter do
@moduledoc """
Formats Spark modules.
Currently, it is very simple, and will only reorder the outermost sections according to some rules.
# Plugin
Include the plugin into your `.formatter.exs` like so `plugins: [Spark.Formatter]`.
If no configuration is provided, it will sort all top level DSL sections *alphabetically*.
# Section Order
To provide a custom section order, add configuration to your app, for example:
```elixir
config :spark, :formatter,
remove_parens?: true,
"Ash.Resource": [
section_order: [
:resource,
:postgres,
:attributes,
:relationships,
:aggregates,
:calculations
]
],
"MyApp.Resource": [
# Use this if you use a module that is not the spark DSL itself.
# For example, you might have a "base" that you use instead that sets some simple defaults.
# This tells us what the actual thing is so we know what extensions are included automatically.
type: Ash.Resource,
# Tell us what extensions might be added under the hood
extensions: [MyApp.ResourceExtension],
section_order: [...]
]
```
Any sections found that aren't in that list will be left in the order that they were in, the sections
in the list will be sorted "around" those sections. E.g the following list: `[:code_interface, :attributes]` can be interpreted as
"ensure that code_interface comes before attributes, and don't change the rest".
"""
@behaviour Mix.Tasks.Format
require Logger
def features(_opts) do
[extensions: [".ex", ".exs"]]
end
def format(contents, opts) do
config =
:spark
|> Application.get_env(:formatter, [])
|> Enum.map(fn
{key, value} when key in [:remove_parens?] ->
{key, value}
{key, value} ->
{Code.ensure_compiled!(Module.concat([key])), value}
end)
parse_result =
try do
{:ok, Sourceror.parse_string!(contents)}
rescue
_ ->
:error
end
case parse_result do
{:ok, parsed} ->
parsed
|> format_resources(opts, config)
|> then(fn patches ->
Sourceror.patch_string(contents, patches)
end)
|> Code.format_string!(opts_without_plugin(opts))
|> then(fn iodata ->
[iodata, ?\n]
end)
|> IO.iodata_to_binary()
:error ->
contents
end
end
defp format_resources(parsed, opts, config) do
{_, patches} =
prewalk(parsed, [], false, fn
{:defmodule, _, [_, [{{:__block__, _, [:do]}, {:__block__, _, body}}]]} = expr,
patches,
false ->
case get_extensions(body, config) do
{:ok, extensions, type, using} ->
replacement = format_resource(body, extensions, config, type, using)
patches =
body
|> Enum.zip(replacement)
|> Enum.reduce(patches, fn {body_section, replacement_section}, patches ->
if body_section == replacement_section do
patches
else
[
%{
range: Sourceror.get_range(body_section, include_comments: true),
change: Sourceror.to_string(replacement_section, opts)
}
| patches
]
end
end)
{expr, patches, true}
_ ->
{expr, patches, true}
end
expr, patches, branch_acc ->
{expr, patches, branch_acc}
end)
patches
end
@doc """
Copy of `Macro.prewalk/2` w/ a branch accumulator
"""
def prewalk(ast, fun) when is_function(fun, 1) do
elem(prewalk(ast, nil, nil, fn x, nil, nil -> {fun.(x), nil} end), 0)
end
@doc """
Copy of `Macro.prewalk/3` w/ a branch accumulator
"""
def prewalk(ast, acc, branch_acc, fun) when is_function(fun, 3) do
traverse(ast, acc, branch_acc, fun, fn x, a -> {x, a} end)
end
@doc """
A copy of the corresponding `Macro.traverse` function that has a separate accumulator that only goes *down* each branch, only for `pre`
"""
def traverse(ast, acc, branch_acc, pre, post)
when is_function(pre, 3) and is_function(post, 2) do
{ast, acc, branch_acc} = pre.(ast, acc, branch_acc)
do_traverse(ast, acc, branch_acc, pre, post)
end
defp do_traverse({form, meta, args}, acc, branch_acc, pre, post) when is_atom(form) do
{args, acc} = do_traverse_args(args, acc, branch_acc, pre, post)
post.({form, meta, args}, acc)
end
defp do_traverse({form, meta, args}, acc, branch_acc, pre, post) do
{form, acc, branch_acc} = pre.(form, acc, branch_acc)
{form, acc} = do_traverse(form, acc, branch_acc, pre, post)
{args, acc} = do_traverse_args(args, acc, branch_acc, pre, post)
post.({form, meta, args}, acc)
end
defp do_traverse({left, right}, acc, branch_acc, pre, post) do
{left, acc, left_branch_acc} = pre.(left, acc, branch_acc)
{left, acc} = do_traverse(left, acc, left_branch_acc, pre, post)
{right, acc, right_branch_acc} = pre.(right, acc, branch_acc)
{right, acc} = do_traverse(right, acc, right_branch_acc, pre, post)
post.({left, right}, acc)
end
defp do_traverse(list, acc, branch_acc, pre, post) when is_list(list) do
{list, acc} = do_traverse_args(list, acc, branch_acc, pre, post)
post.(list, acc)
end
defp do_traverse(x, acc, _branch_acc, _pre, post) do
post.(x, acc)
end
defp do_traverse_args(args, acc, _branch_acc, _pre, _post) when is_atom(args) do
{args, acc}
end
defp do_traverse_args(args, acc, branch_acc, pre, post) when is_list(args) do
:lists.mapfoldl(
fn x, acc ->
{x, acc, branch_acc} = pre.(x, acc, branch_acc)
do_traverse(x, acc, branch_acc, pre, post)
end,
acc,
args
)
end
defp format_resource(body, extensions, config, _type, using) do
sections =
extensions
|> Enum.flat_map(fn extension ->
Enum.map(extension.sections(), fn section ->
{extension, section}
end)
end)
|> sort_sections(config[using][:section_order])
section_names = Enum.map(sections, fn {_, section} -> section.name end)
{section_exprs, non_section_exprs} =
body
|> Enum.with_index()
|> Enum.split_with(fn {{name, _, _}, _index} ->
name in section_names
end)
new_sections =
section_exprs
|> Enum.sort_by(fn {{name, _, _}, _} ->
Enum.find_index(section_names, &(&1 == name))
end)
new_section_indexes =
section_exprs
|> Enum.map(&elem(&1, 1))
|> Enum.sort()
new_sections =
Enum.zip_with(new_sections, new_section_indexes, fn {new_section, _}, index ->
{new_section, index}
end)
non_section_exprs
|> Enum.concat(new_sections)
|> Enum.sort_by(&elem(&1, 1))
|> Enum.map(&elem(&1, 0))
|> then(fn sections ->
if config[:remove_parens?] do
de_paren(sections, Enum.flat_map(extensions, & &1.sections), extensions)
else
sections
end
end)
end
defp de_paren(actual_sections, dsl_sections, extensions) do
actual_sections
|> Enum.map(fn
{name, meta, body} ->
case Enum.find(dsl_sections, &(&1.name == name)) do
nil ->
{name, meta, body}
section ->
{name, meta, de_paren_section(body, section, extensions)}
end
other ->
other
end)
end
defp de_paren_section(body, section, extensions) do
builders = all_entity_builders([section], extensions)
Macro.prewalk(body, fn
{func, meta, body} = node when is_atom(func) ->
count = Enum.count(List.wrap(body))
if builders[func] in [count, count - 1] && Enum.count(List.wrap(body)) &&
Keyword.keyword?(meta) &&
meta[:closing] do
{func, Keyword.delete(meta, :closing), body}
else
node
end
node ->
node
end)
end
defp sort_sections(sections, nil), do: sections
defp sort_sections(sections, section_order) do
{ordered, unordered} =
sections
|> Enum.with_index()
|> Enum.split_with(fn {{_, section}, _} ->
section.name in section_order
end)
reordered =
ordered
|> Enum.map(&elem(&1, 0))
|> Enum.sort_by(fn {_, section} ->
Enum.find_index(section_order, &(&1 == section.name))
end)
Enum.reduce(unordered, reordered, fn {{extension, section}, i}, acc ->
List.insert_at(acc, i, {extension, section})
end)
end
defp get_extensions(body, config) do
Enum.find_value(body, :error, fn
{:use, _, using} ->
[using, opts] =
case Spark.Dsl.Extension.expand_alias(using, __ENV__) do
[using] ->
[using, []]
[using, opts] ->
[using, opts]
end
if Keyword.has_key?(config, using) do
type = config[using][:type] || using
{:ok, parse_extensions(opts, config, type), type, using}
end
_ ->
nil
end)
end
defp parse_extensions(blocks, config, type) do
blocks
|> Enum.flat_map(fn {{:__block__, _, _}, extensions} ->
extensions
|> case do
{:__block__, _, [extensions]} ->
extensions
extension when is_atom(extension) ->
extension
_ ->
[]
end
|> List.wrap()
|> Enum.flat_map(fn extension ->
case Code.ensure_compiled(extension) do
{:module, module} ->
if Spark.implements_behaviour?(module, Spark.Dsl.Extension) do
[module]
else
[]
end
_ ->
[]
end
end)
end)
|> Enum.concat(config[:extensions] || [])
|> Enum.concat(type.default_extensions() || [])
end
defp opts_without_plugin(opts) do
Keyword.update(opts, :plugins, [], &(&1 -- [__MODULE__]))
end
@doc false
def all_entity_builders(sections, extensions, path \\ []) do
Enum.flat_map(sections, fn section ->
Enum.concat([
all_entity_option_builders(section),
all_patch_entity_builders(extensions, section, path),
section_option_builders(section),
section_entity_builders(section, extensions, path)
])
end)
|> Enum.uniq()
|> Enum.sort()
end
defp all_patch_entity_builders(extensions, section, path) do
match_path = path ++ [section.name]
extensions
|> Enum.flat_map(& &1.dsl_patches())
|> tap(fn patches ->
Enum.map(patches, & &1.section_path)
end)
|> Enum.filter(fn
%Spark.Dsl.Patch.AddEntity{section_path: ^match_path} ->
true
_ ->
false
end)
|> Enum.map(& &1.entity)
|> Enum.flat_map(&entity_option_builders/1)
end
defp section_entity_builders(section, extensions, path) do
match_path = path ++ [section.name]
mixed_in_entities =
extensions
|> Enum.flat_map(& &1.dsl_patches())
|> Enum.filter(fn
%Spark.Dsl.Patch.AddEntity{section_path: ^match_path} ->
true
_ ->
false
end)
|> Enum.map(& &1.entity)
Enum.flat_map(section.entities ++ mixed_in_entities, fn entity ->
entity_builders(entity)
end) ++ all_entity_builders(section.sections, extensions, path ++ [section.name])
end
defp entity_builders(entity) do
arg_count = Enum.count(entity.args)
non_optional_arg_count = Enum.count(entity.args, &is_atom/1)
non_optional_arg_count..arg_count
|> Enum.flat_map(&[{entity.name, &1}, {entity.name, &1 + 1}])
|> Enum.concat(flat_map_nested_entities(entity, &entity_builders/1))
end
defp all_entity_option_builders(section) do
Enum.flat_map(section.entities, fn entity ->
entity_option_builders(entity)
end)
end
defp entity_option_builders(entity) do
entity_args_to_drop = Spark.Dsl.Entity.arg_names(entity)
entity.schema
|> Keyword.drop(entity_args_to_drop)
|> Enum.map(fn {key, _schema} ->
{key, 1}
end)
|> Kernel.++(flat_map_nested_entities(entity, &entity_option_builders/1))
end
defp section_option_builders(section) do
Enum.map(section.schema, fn {key, _} ->
{key, 1}
end)
end
defp flat_map_nested_entities(entity, mapper) do
Enum.flat_map(entity.entities, fn {_, nested_entities} ->
nested_entities
|> List.wrap()
|> Enum.flat_map(fn nested_entity ->
mapper.(nested_entity)
end)
end)
end
end