Packages
styler
1.0.0
1.11.0
1.10.1
1.10.0
1.9.1
1.9.0
1.8.0
1.7.0
1.6.0
1.5.1
1.5.0
1.4.2
1.4.1
1.4.0
1.3.3
1.3.2
1.3.1
1.3.0
1.2.1
1.2.0
1.1.2
1.1.1
1.1.0
1.0.0
1.0.0-rc.2
1.0.0-rc.1
1.0.0-rc.0
1.0.0-alpha.0
0.11.9
0.11.8
0.11.7
0.11.6
0.11.5
0.11.4
0.11.3
0.11.2
0.11.1
0.11.0
0.10.5
0.10.4
retired
0.10.3
0.10.2
0.10.1
0.10.0
0.9.7
0.9.6
0.9.5
0.9.4
0.9.3
0.9.2
retired
0.9.1
retired
0.9.0
0.8.5
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.14
0.7.13
0.7.12
0.7.11
0.7.10
0.7.9
0.7.8
0.7.7
0.7.6
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.1
0.6.0
0.5.2
0.5.1
0.5.0
0.4.1
0.4.0
0.3.1
0.3.0
0.2.0
0.1.1
0.1.0
A code-style enforcer that will just FIFY instead of complaining
Current section
Files
Jump to
Current section
Files
lib/style/configs.ex
# Copyright 2024 Adobe. All rights reserved.
# This file is licensed to you under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. You may obtain a copy
# of the License at http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software distributed under
# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
# OF ANY KIND, either express or implied. See the License for the specific language
# governing permissions and limitations under the License.
defmodule Styler.Style.Configs do
@moduledoc """
Orders `Config.config/2,3` stanzas in configuration files.
- ordering is done only within immediate-sibling config statements
- assignments are moved above the configuration blocks
- any non `config/2,3` or assignment (`=/2`) calls mark the end of a sorting block.
this is support having conditional blocks (`if/case/cond`) and `import_config` stanzas between blocks
### Breakages
If you configure the same values multiple times, Styler may swap their orders
**Before**
line 04: config :foo, bar: :zab
line 40: config :foo, bar: :baz
# Application.fetch_env!(:foo)[:bar] => :baz
**After**
line 04: config :foo, bar: :baz
line 05: config :foo, bar: :zab
# Application.fetch_env!(:foo)[:bar] => :zab
**Fix**
The reason Styler sorts configuration is to help you noticed these duplicated configuration stanzas.
Delete the duplicative/erroneous stanza and life will be good.
"""
alias Styler.Style
def run({{:import, _, [{:__aliases__, _, [:Config]}]}, _} = zipper, %{config?: true} = ctx) do
{:skip, zipper, Map.put(ctx, :mix_config?, true)}
end
def run({{:config, cfm, [_, _ | _]} = config, zm}, %{mix_config?: true, comments: comments} = ctx) do
# all of these list are reversed due to the reduce
{configs, assignments, rest} = accumulate(zm.r, [], [])
# @TODO
# okay so comments between nodes that we moved.......
# lets just push them out of the way (???). so
# 1. figure out first/last possible lines we're talking about here
# 2. only pass comments in that range off
# 3. split those comments into "moved, didn't move"
# 4. for any "didn't move" comments... move them to the top?
#
# also, should i just do a scan of the configs ++ assignments, and see if any of them have lines out of order,
# and decide from there whether or not i want to do set_lines
configs =
[config | configs]
|> Enum.group_by(fn
{:config, _, [{:__block__, _, [app]} | _]} -> app
{:config, _, [arg | _]} -> Style.without_meta(arg)
end)
|> Enum.sort()
|> Enum.flat_map(fn {_app, configs} ->
configs
|> Enum.sort_by(&Style.without_meta/1)
|> Style.reset_newlines()
end)
nodes =
assignments
|> Enum.reverse()
|> Style.reset_newlines()
|> Enum.concat(configs)
# `set_lines` performs better than `fix_line_numbers` for a large number of nodes moving, as it moves their comments with them
# however, it will also move any comments not associated with a node, causing wildly unpredictable sad times!
# so i'm trying to guess which change will be less damaging.
# moving >=3 nodes hints that this is an initial run, where `set_lines` definitely outperforms.
{nodes, comments} =
case change_count(nodes) do
0 ->
{nodes, comments}
n when n < 3 ->
{Style.fix_line_numbers(nodes, List.last(rest)), comments}
_ ->
# after running, this block should take up the same # of lines that it did before
# the first node of `rest` is greater than the highest line in configs, assignments
# config line is the first line to be used as part of this block
# that will change when we consider preceding comments
{node_comments, _} = comments_for_node(config, comments)
first_line = min(List.last(node_comments)[:line] || cfm[:line], cfm[:line])
set_lines(nodes, comments, first_line)
end
[config | left_siblings] = Enum.reverse(nodes, zm.l)
{:skip, {config, %{zm | l: left_siblings, r: rest}}, %{ctx | comments: comments}}
end
def run(zipper, %{config?: true} = ctx) do
{:cont, zipper, ctx}
end
def run(zipper, %{file: file} = ctx) do
if file =~ ~r|config/.*\.exs| or file =~ ~r|rel/overlays/.*\.exs| do
# @TODO have this run forward to `import Config`, then run forward from there until we find `config` itself. no need for multi function head
{:cont, zipper, Map.put(ctx, :config?, true)}
else
{:halt, zipper, ctx}
end
end
defp change_count(nodes, n \\ 0)
defp change_count([{_, am, _}, {_, bm, _} = b | tail], n) do
n = if am[:line] > bm[:line], do: n + 1, else: n
change_count([b | tail], n)
end
defp change_count(_, n), do: n
defp set_lines(nodes, comments, first_line) do
{nodes, comments, node_comments} = set_lines(nodes, comments, first_line, [], [])
# @TODO if there are dangling comments between the nodes min/max, push them somewhere?
# likewise deal with conflicting line comments?
{nodes, Enum.sort_by(comments ++ node_comments, & &1.line)}
end
def set_lines([], comments, _, node_acc, c_acc), do: {Enum.reverse(node_acc), comments, c_acc}
def set_lines([{_, meta, _} = node | nodes], comments, start_line, n_acc, c_acc) do
line = meta[:line]
last_line = meta[:end_of_expression][:line] || Style.max_line(node)
{node, node_comments, comments} =
if start_line == line do
{node, [], comments}
else
{mine, comments} = comments_for_lines(comments, line, last_line)
line_with_comments = (List.first(mine)[:line] || line) - (List.first(mine)[:previous_eol_count] || 1) + 1
if line_with_comments == start_line do
{node, mine, comments}
else
shift = start_line - line_with_comments
node = Style.shift_line(node, shift)
mine = Enum.map(mine, &%{&1 | line: &1.line + shift})
{node, mine, comments}
end
end
{_, meta, _} = node
# @TODO what about comments that were free floating between blocks? i'm just ignoring them and maybe always will...
# kind of just want to shove them to the end though, so that they don't interrupt existing stanzas.
# i think that's accomplishable by doing a final call above that finds all comments in the comments list that weren't moved
# and which are in the range of start..finish and sets their lines to finish!
last_line = meta[:end_of_expression][:line] || Style.max_line(node)
last_line = (meta[:end_of_expression][:newlines] || 1) + last_line
set_lines(nodes, comments, last_line, [node | n_acc], node_comments ++ c_acc)
end
defp comments_for_node({_, m, _} = node, comments) do
last_line = m[:end_of_expression][:line] || Style.max_line(node)
comments_for_lines(comments, m[:line], last_line)
end
defp comments_for_lines(comments, start_line, last_line) do
comments
|> Enum.reverse()
|> comments_for_lines(start_line, last_line, [], [])
end
defp comments_for_lines(reversed_comments, start, last, match, acc)
defp comments_for_lines([], _, _, match, acc), do: {Enum.reverse(match), acc}
defp comments_for_lines([%{line: line} = comment | rev_comments], start, last, match, acc) do
cond do
line > last -> comments_for_lines(rev_comments, start, last, match, [comment | acc])
line >= start -> comments_for_lines(rev_comments, start, last, [comment | match], acc)
# @TODO bug: match line looks like `x = :foo # comment for x`
# could account for that by pre-running the formatter on config files :/
line == start - 1 -> comments_for_lines(rev_comments, start - 1, last, [comment | match], acc)
true -> {match, Enum.reverse(rev_comments, [comment | acc])}
end
end
defp accumulate([{:config, _, [_, _ | _]} = c | siblings], cs, as), do: accumulate(siblings, [c | cs], as)
defp accumulate([{:=, _, [_lhs, _rhs]} = a | siblings], cs, as), do: accumulate(siblings, cs, [a | as])
defp accumulate(rest, configs, assignments), do: {configs, assignments, rest}
end