changed CHANGELOG.md
 
@@ -2,8 +2,42 @@
2
2
3
3
**Note** Styler's only public API is its usage as a formatter plugin. While you're welcome to play with its internals,
4
4
they can and will change without that change being reflected in Styler's semantic version.
5
+
5
6
## main
6
7
8
+ ## 1.3.3
9
+
10
+ ### Improvements
11
+
12
+ - `with do: body` and variations with no arrows in the head will be rewritten to just `body`
13
+ - `# styler:sort` will sort arbitrary ast nodes within a `do end` block:
14
+
15
+ Given:
16
+ # styler:sort
17
+ my_macro "some arg" do
18
+ another_macro :q
19
+ another_macro :w
20
+ another_macro :e
21
+ another_macro :r
22
+ another_macro :t
23
+ another_macro :y
24
+ end
25
+
26
+ We get
27
+ # styler:sort
28
+ my_macro "some arg" do
29
+ another_macro :e
30
+ another_macro :q
31
+ another_macro :r
32
+ another_macro :t
33
+ another_macro :w
34
+ another_macro :y
35
+ end
36
+
37
+ ### Fixes
38
+
39
+ - fix a bug in comment-movement when multiple `# styler:sort` directives are added to a file at the same time
40
+
7
41
## 1.3.2
8
42
9
43
### Improvements
changed hex_metadata.config
 
@@ -1,6 +1,6 @@
1
1
{<<"links">>,[{<<"GitHub">>,<<"https://github.com/adobe/elixir-styler">>}]}.
2
2
{<<"name">>,<<"styler">>}.
3
- {<<"version">>,<<"1.3.2">>}.
3
+ {<<"version">>,<<"1.3.3">>}.
4
4
{<<"description">>,
5
5
<<"A code-style enforcer that will just FIFY instead of complaining">>}.
6
6
{<<"elixir">>,<<"~> 1.15">>}.
changed lib/style/blocks.ex
 
@@ -75,103 +75,32 @@ defmodule Styler.Style.Blocks do
75
75
{:cont, Zipper.replace(zipper, {:if, m, children}), ctx}
76
76
end
77
77
78
+ def run({{:with, _, [[{{:__block__, _, [:do]}, body} | _]]}, _} = zipper, ctx) do
79
+ {:cont, Zipper.replace(zipper, body), ctx}
80
+ end
81
+
78
82
# Credo.Check.Refactor.WithClauses
79
- def run({{:with, with_meta, children}, _} = zipper, ctx) when is_list(children) do
80
- # a std lib `with` block will have at least one left arrow and a `do` body. anything else we skip ¯\_(ツ)_/¯
81
- arrow_or_match? = &(left_arrow?(&1) || match?({:=, _, _}, &1))
83
+ def run({{:with, _, children}, _} = zipper, ctx) when is_list(children) do
84
+ do_block? = Enum.any?(children, &Style.do_block?/1)
85
+ arrow_or_match? = Enum.any?(children, &(left_arrow?(&1) || match?({:=, _, _}, &1)))
82
86
83
- if Enum.any?(children, arrow_or_match?) and Enum.any?(children, &Style.do_block?/1) do
84
- {preroll, children} =
85
- children
86
- |> Enum.map(fn
87
- # `_ <- rhs` => `rhs`
88
- {:<-, _, [{:_, _, _}, rhs]} -> rhs
89
- # `lhs <- rhs` => `lhs = rhs`
90
- {:<-, m, [{atom, _, nil} = lhs, rhs]} when is_atom(atom) -> {:=, m, [lhs, rhs]}
91
- child -> child
92
- end)
93
- |> Enum.split_while(&(not left_arrow?(&1)))
87
+ cond do
88
+ # we can style this!
89
+ do_block? and arrow_or_match? ->
90
+ style_with_statement(zipper, ctx)
94
91
95
- # after rewriting `x <- y()` to `x = y()` there are no more arrows.
96
- # this never should've been a with statement at all! we can just replace it with assignments
97
- if Enum.empty?(children) do
98
- {:cont, replace_with_statement(zipper, preroll), ctx}
99
- else
100
- [[{{_, do_meta, _} = do_block, do_body} | elses] | reversed_clauses] = Enum.reverse(children)
101
- {postroll, reversed_clauses} = Enum.split_while(reversed_clauses, &(not left_arrow?(&1)))
102
- [{:<-, final_clause_meta, [lhs, rhs]} = _final_clause | rest] = reversed_clauses
92
+ # `with (head_statements) do: x (else ...)`
93
+ do_block? ->
94
+ # head statements can be the empty list, if it matters
95
+ {head_statements, [[{{:__block__, _, [:do]}, body} | _]]} = Enum.split_while(children, &(not Style.do_block?(&1)))
96
+ [first | rest] = head_statements ++ [body]
97
+ # replace this `with` statement with its headers + body
98
+ zipper = zipper |> Zipper.replace(first) |> Zipper.insert_siblings(rest)
99
+ {:cont, zipper, ctx}
103
100
104
- # drop singleton identity else clauses like `else foo -> foo end`
105
- elses =
106
- case elses do
107
- [{{_, _, [:else]}, [{:->, _, [[left], right]}]}] -> if nodes_equivalent?(left, right), do: [], else: elses
108
- _ -> elses
109
- end
110
-
111
- {reversed_clauses, do_body} =
112
- cond do
113
- # Put the postroll into the body
114
- Enum.any?(postroll) ->
115
- {node, do_body_meta, do_children} = do_body
116
- do_children = if node == :__block__, do: do_children, else: [do_body]
117
- do_body = {:__block__, Keyword.take(do_body_meta, [:line]), Enum.reverse(postroll, do_children)}
118
- {reversed_clauses, do_body}
119
-
120
- # Credo.Check.Refactor.RedundantWithClauseResult
121
- Enum.empty?(elses) and nodes_equivalent?(lhs, do_body) ->
122
- {rest, rhs}
123
-
124
- # no change
125
- true ->
126
- {reversed_clauses, do_body}
127
- end
128
-
129
- do_line = do_meta[:line]
130
- final_clause_line = final_clause_meta[:line]
131
-
132
- do_line =
133
- cond do
134
- do_meta[:format] == :keyword && final_clause_line + 1 >= do_line -> do_line
135
- do_meta[:format] == :keyword -> final_clause_line + 1
136
- true -> final_clause_line
137
- end
138
-
139
- do_block = Macro.update_meta(do_block, &Keyword.put(&1, :line, do_line))
140
- # disable keyword `, do:` since there will be multiple statements in the body
141
- with_meta =
142
- if Enum.any?(postroll),
143
- do: Keyword.merge(with_meta, do: [line: with_meta[:line]], end: [line: Style.max_line(children) + 1]),
144
- else: with_meta
145
-
146
- with_children = Enum.reverse(reversed_clauses, [[{do_block, do_body} | elses]])
147
- zipper = Zipper.replace(zipper, {:with, with_meta, with_children})
148
-
149
- cond do
150
- # oops! RedundantWithClauseResult removed the final arrow in this. no more need for a with statement!
151
- Enum.empty?(reversed_clauses) ->
152
- {:cont, replace_with_statement(zipper, preroll ++ with_children), ctx}
153
-
154
- # recurse if the # of `<-` have changed (this `with` could now be eligible for a `case` rewrite)
155
- Enum.any?(preroll) ->
156
- # put the preroll before the with statement in either a block we create or the existing parent block
157
- zipper
158
- |> Style.find_nearest_block()
159
- |> Zipper.prepend_siblings(preroll)
160
- |> run(ctx)
161
-
162
- # the # of `<-` canged, so we should have another look at this with statement
163
- Enum.any?(postroll) ->
164
- run(zipper, ctx)
165
-
166
- true ->
167
- # of clauess didn't change, so don't reecurse or we'll loop FOREEEVEERR
168
- {:cont, zipper, ctx}
169
- end
170
- end
171
- else
172
- # maybe this isn't a with statement - could be a function named `with`
173
- # or it's just a with statement with no arrows, but that's too saddening to imagine
174
- {:cont, zipper, ctx}
101
+ # maybe this isn't a with statement - could be a function named `with` or something.
102
+ true ->
103
+ {:cont, zipper, ctx}
175
104
end
176
105
end
177
106
 
@@ -217,6 +146,97 @@ defmodule Styler.Style.Blocks do
217
146
218
147
def run(zipper, ctx), do: {:cont, zipper, ctx}
219
148
149
+ # with statements can do _a lot_, so this beast of a function likewise does a lot.
150
+ defp style_with_statement({{:with, with_meta, children}, _} = zipper, ctx) do
151
+ {preroll, children} =
152
+ children
153
+ |> Enum.map(fn
154
+ # `_ <- rhs` => `rhs`
155
+ {:<-, _, [{:_, _, _}, rhs]} -> rhs
156
+ # `lhs <- rhs` => `lhs = rhs`
157
+ {:<-, m, [{atom, _, nil} = lhs, rhs]} when is_atom(atom) -> {:=, m, [lhs, rhs]}
158
+ child -> child
159
+ end)
160
+ |> Enum.split_while(&(not left_arrow?(&1)))
161
+
162
+ # after rewriting `x <- y()` to `x = y()` there are no more arrows.
163
+ # this never should've been a with statement at all! we can just replace it with assignments
164
+ if Enum.empty?(children) do
165
+ {:cont, replace_with_statement(zipper, preroll), ctx}
166
+ else
167
+ [[{{_, do_meta, _} = do_block, do_body} | elses] | reversed_clauses] = Enum.reverse(children)
168
+ {postroll, reversed_clauses} = Enum.split_while(reversed_clauses, &(not left_arrow?(&1)))
169
+ [{:<-, final_clause_meta, [lhs, rhs]} = _final_clause | rest] = reversed_clauses
170
+
171
+ # drop singleton identity else clauses like `else foo -> foo end`
172
+ elses =
173
+ case elses do
174
+ [{{_, _, [:else]}, [{:->, _, [[left], right]}]}] -> if nodes_equivalent?(left, right), do: [], else: elses
175
+ _ -> elses
176
+ end
177
+
178
+ {reversed_clauses, do_body} =
179
+ cond do
180
+ # Put the postroll into the body
181
+ Enum.any?(postroll) ->
182
+ {node, do_body_meta, do_children} = do_body
183
+ do_children = if node == :__block__, do: do_children, else: [do_body]
184
+ do_body = {:__block__, Keyword.take(do_body_meta, [:line]), Enum.reverse(postroll, do_children)}
185
+ {reversed_clauses, do_body}
186
+
187
+ # Credo.Check.Refactor.RedundantWithClauseResult
188
+ Enum.empty?(elses) and nodes_equivalent?(lhs, do_body) ->
189
+ {rest, rhs}
190
+
191
+ # no change
192
+ true ->
193
+ {reversed_clauses, do_body}
194
+ end
195
+
196
+ do_line = do_meta[:line]
197
+ final_clause_line = final_clause_meta[:line]
198
+
199
+ do_line =
200
+ cond do
201
+ do_meta[:format] == :keyword && final_clause_line + 1 >= do_line -> do_line
202
+ do_meta[:format] == :keyword -> final_clause_line + 1
203
+ true -> final_clause_line
204
+ end
205
+
206
+ do_block = Macro.update_meta(do_block, &Keyword.put(&1, :line, do_line))
207
+ # disable keyword `, do:` since there will be multiple statements in the body
208
+ with_meta =
209
+ if Enum.any?(postroll),
210
+ do: Keyword.merge(with_meta, do: [line: with_meta[:line]], end: [line: Style.max_line(children) + 1]),
211
+ else: with_meta
212
+
213
+ with_children = Enum.reverse(reversed_clauses, [[{do_block, do_body} | elses]])
214
+ zipper = Zipper.replace(zipper, {:with, with_meta, with_children})
215
+
216
+ cond do
217
+ # oops! RedundantWithClauseResult removed the final arrow in this. no more need for a with statement!
218
+ Enum.empty?(reversed_clauses) ->
219
+ {:cont, replace_with_statement(zipper, preroll ++ with_children), ctx}
220
+
221
+ # recurse if the # of `<-` have changed (this `with` could now be eligible for a `case` rewrite)
222
+ Enum.any?(preroll) ->
223
+ # put the preroll before the with statement in either a block we create or the existing parent block
224
+ zipper
225
+ |> Style.find_nearest_block()
226
+ |> Zipper.prepend_siblings(preroll)
227
+ |> run(ctx)
228
+
229
+ # the # of `<-` canged, so we should have another look at this with statement
230
+ Enum.any?(postroll) ->
231
+ run(zipper, ctx)
232
+
233
+ true ->
234
+ # of clauess didn't change, so don't reecurse or we'll loop FOREEEVEERR
235
+ {:cont, zipper, ctx}
236
+ end
237
+ end
238
+ end
239
+
220
240
# `with a <- b(), c <- d(), do: :ok, else: (_ -> :error)`
221
241
# =>
222
242
# `a = b(); c = d(); :ok`
changed lib/style/comment_directives.ex
 
@@ -33,8 +33,7 @@ defmodule Styler.Style.CommentDirectives do
33
33
end)
34
34
35
35
if found do
36
- {node, _} = found
37
- {sorted, comments} = sort(node, ctx.comments)
36
+ {sorted, comments} = found |> Zipper.node() |> sort(comments)
38
37
{Zipper.replace(found, sorted), comments}
39
38
else
40
39
{zipper, comments}
 
@@ -107,5 +106,23 @@ defmodule Styler.Style.CommentDirectives do
107
106
{{key, value}, comments}
108
107
end
109
108
109
+ # sorts arbitrary ast nodes within a `do end` list
110
+ defp sort({f, m, args} = node, comments) do
111
+ if m[:do] && m[:end] && match?([{{:__block__, _, [:do]}, {:__block__, _, _}}], List.last(args)) do
112
+ {[{{:__block__, m1, [:do]}, {:__block__, m2, nodes}}], args} = List.pop_at(args, -1)
113
+
114
+ {nodes, comments} =
115
+ nodes
116
+ |> Enum.sort_by(&Macro.to_string/1)
117
+ |> Style.order_line_meta_and_comments(comments, m[:line])
118
+
119
+ args = List.insert_at(args, -1, [{{:__block__, m1, [:do]}, {:__block__, m2, nodes}}])
120
+
121
+ {{f, m, args}, comments}
122
+ else
123
+ {node, comments}
124
+ end
125
+ end
126
+
110
127
defp sort(x, comments), do: {x, comments}
111
128
end
changed lib/zipper.ex
 
@@ -172,18 +172,18 @@ defmodule Styler.Zipper do
172
172
top level.
173
173
"""
174
174
@spec insert_left(zipper, tree) :: zipper
175
- def insert_left({_, nil}, _), do: raise(ArgumentError, message: "Can't insert siblings at the top level.")
176
- def insert_left({tree, meta}, child), do: {tree, %{meta | l: [child | meta.l]}}
175
+ def insert_left(zipper, child), do: prepend_siblings(zipper, [child])
177
176
178
177
@doc """
179
178
Inserts many siblings to the left.
179
+ If the node is at the top of the tree, builds a new root `:__block__` while maintaining focus on the current node.
180
180
181
181
Equivalent to
182
182
183
183
Enum.reduce(siblings, zipper, &Zipper.insert_left(&2, &1))
184
184
"""
185
185
@spec prepend_siblings(zipper, [tree]) :: zipper
186
- def prepend_siblings({_, nil}, _), do: raise(ArgumentError, message: "Can't insert siblings at the top level.")
186
+ def prepend_siblings({node, nil}, siblings), do: {:__block__, [], siblings ++ [node]} |> zip() |> down() |> rightmost()
187
187
def prepend_siblings({tree, meta}, siblings), do: {tree, %{meta | l: Enum.reverse(siblings, meta.l)}}
188
188
189
189
@doc """
 
@@ -192,18 +192,18 @@ defmodule Styler.Zipper do
192
192
top level.
193
193
"""
194
194
@spec insert_right(zipper, tree) :: zipper
195
- def insert_right({_, nil}, _), do: raise(ArgumentError, message: "Can't insert siblings at the top level.")
196
- def insert_right({tree, meta}, child), do: {tree, %{meta | r: [child | meta.r]}}
195
+ def insert_right(zipper, child), do: insert_siblings(zipper, [child])
197
196
198
197
@doc """
199
198
Inserts many siblings to the right.
199
+ If the node is at the top of the tree, builds a new root `:__block__` while maintaining focus on the current node.
200
200
201
201
Equivalent to
202
202
203
203
Enum.reduce(siblings, zipper, &Zipper.insert_right(&2, &1))
204
204
"""
205
205
@spec insert_siblings(zipper, [tree]) :: zipper
206
- def insert_siblings({_, nil}, _), do: raise(ArgumentError, message: "Can't insert siblings at the top level.")
206
+ def insert_siblings({node, nil}, siblings), do: {:__block__, [], [node | siblings]} |> zip() |> down()
207
207
def insert_siblings({tree, meta}, siblings), do: {tree, %{meta | r: siblings ++ meta.r}}
208
208
209
209
@doc """
changed mix.exs
 
@@ -12,7 +12,7 @@ defmodule Styler.MixProject do
12
12
use Mix.Project
13
13
14
14
# Don't forget to bump the README when doing non-patch version changes
15
- @version "1.3.2"
15
+ @version "1.3.3"
16
16
@url "https://github.com/adobe/elixir-styler"
17
17
18
18
def project do