changed CHANGELOG.md
 
@@ -5,6 +5,61 @@ they can and will change without that change being reflected in Styler's semanti
5
5
6
6
## main
7
7
8
+ ## 1.4
9
+
10
+ - A very nice change in alias lifting means Styler will make sure that your code is _using_ the aliases that it's specified.
11
+ - Shoutout to the smartrent folks for finding pipifying recursion issues
12
+ - Elixir 1.17 improvements and fixes
13
+ - Elixir 1.19-dev: delete struct updates
14
+
15
+ Read on for details.
16
+
17
+ ### Improvements
18
+
19
+ #### Alias Lifting
20
+
21
+ This release taught Styler to try just that little bit harder when doing alias lifting.
22
+
23
+ - general improvements around conflict detection, lifting in more correct places and fewer incorrect places (#193, h/t @jsw800)
24
+ - use knowledge of existing aliases to shorten invocations (#201, h/t me)
25
+
26
+ example:
27
+ alias A.B.C
28
+
29
+ A.B.C.foo()
30
+ A.B.C.bar()
31
+ A.B.C.baz()
32
+
33
+ becomes:
34
+ alias A.B.C
35
+
36
+ C.foo()
37
+ C.bar()
38
+ C.baz()
39
+
40
+ #### Struct Updates => Map Updates
41
+
42
+ 1.19 deprecates struct update syntax in favor of map update syntax.
43
+
44
+ ```elixir
45
+ # This
46
+ %Struct{x | y}
47
+ # Styles to this
48
+ %{x | y}
49
+ ```
50
+
51
+ **WARNING** Double check your diffs to make sure your variable is pattern matching against the same struct if you want to harness 1.19's type checking features. Apologies to folks who hoped Styler would do this step for you <3 (#199, h/t @SteffenDE)
52
+
53
+ #### Ex1.17+
54
+
55
+ - Replace `:timer.units(x)` with the new `to_timeout(unit: x)` for `hours|minutes|seconds` (This style is only applied if you're on 1.17+)
56
+
57
+ ### Fixes
58
+
59
+ - `pipes`: handle pipifying when the first arg is itself a pipe: `c(a |> b, d)` => `a |> b() |> c(d)` (#214, h/t @kybishop)
60
+ - `pipes`: handle pipifying nested functions `d(c(a |> b))` => `a |> b |> c() |> d` (#216, h/t @emkguts)
61
+ - `with`: fix a stabby `with` `, else: (_ -> :ok)` being rewritten to a case (#219, h/t @iamhassangm)
62
+
8
63
## 1.3.3
9
64
10
65
### Improvements
changed README.md
 
@@ -11,77 +11,18 @@ You can learn more about the history, purpose and implementation of Styler from
11
11
12
12
## Features
13
13
14
- ### AST Rewrites as part of `mix format`
14
+ Styler fixes a plethora of elixir style and optimization issues automatically as part of mix format.
15
15
16
- [See our Rewrites documentation on hexdocs](https://hexdocs.pm/styler/styles.html)
17
- Styler fixes a plethora of elixir style and optimization issues automatically as part of `mix format`. In addition to automating corrections for [many credo rules](docs/credo.md) (meaning you can turn them off to speed credo up), Styler:
16
+ [See Styler's documentation on Hex](https://hexdocs.pm/styler/styles.html) for the comprehensive list of its features.
18
17
19
- - [keeps a strict module layout](docs/module_directives.md#directive-organization)
20
- - alphabetizes module directives
21
- - [extracts repeated aliases](docs/module_directives.md#alias-lifting)
22
- - [makes your pipe chains pretty as can be](docs/pipes.md)
23
- - pipes and unpipes function calls based on the number of calls
24
- - optimizes standard library calls (`a |> Enum.map(m) |> Enum.into(Map.new)` => `Map.new(a, m)`)
25
- - replaces strings with sigils when the string has many escaped quotes
26
- - ... and so much more
18
+ The fastest way to see what all it can do you for you is to just try it out in your codebase... but here's a list of a few features to help you decide if you're interested in Styler:
27
19
28
- ### Maintain static list order via `# styler:sort`
29
-
30
- Styler can keep static values sorted for your team as part of its formatting pass. To instruct it to do so, replace any `# Please keep this list sorted!` notes you wrote to your teammates with `# styler:sort`.
31
-
32
- #### Examples
33
-
34
- ```elixir
35
- # styler:sort
36
- [:c, :a, :b]
37
-
38
- # styler:sort
39
- ~w(a list of words)
40
-
41
- # styler:sort
42
- @country_codes ~w(
43
- en_US
44
- po_PO
45
- fr_CA
46
- ja_JP
47
- )
48
-
49
- # styler:sort
50
- a_var =
51
- [
52
- Modules,
53
- In,
54
- A,
55
- List
56
- ]
57
- ```
58
-
59
- Would yield:
60
-
61
- ```elixir
62
- # styler:sort
63
- [:a, :b, :c]
64
-
65
- # styler:sort
66
- ~w(a list of words)
67
-
68
- # styler:sort
69
- @country_codes ~w(
70
- en_US
71
- fr_CA
72
- ja_JP
73
- po_PO
74
- )
75
-
76
- # styler:sort
77
- a_var =
78
- [
79
- A,
80
- In,
81
- List,
82
- Modules
83
- ]
84
- ```
20
+ - sorts and organizes `import`/`alias`/`require` and other [module directives](docs/module_directives.md)
21
+ - keeps lists, sigils, and even arbitrary code sorted with the `# styler:sort` [comment directive](docs/comment_directives.md)
22
+ - automatically creates aliases for repeatedly referenced modules names ([_"alias lifting"_](docs/module_directives.md#alias-lifting))
23
+ - optimizes pipe chains for [readability and performance](docs/pipes.md)
24
+ - rewrites strings as sigils when it results in fewer escapes
25
+ - auto-fixes [many credo rules](docs/credo.md), meaning you can spend less time fighting with CI
85
26
86
27
## Who is Styler for?
87
28
 
@@ -101,7 +42,7 @@ Add `:styler` as a dependency to your project's `mix.exs`:
101
42
```elixir
102
43
def deps do
103
44
[
104
- {:styler, "~> 1.2", only: [:dev, :test], runtime: false},
45
+ {:styler, "~> 1.4", only: [:dev, :test], runtime: false},
105
46
]
106
47
end
107
48
```
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.3">>}.
3
+ {<<"version">>,<<"1.4.0">>}.
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
 
@@ -53,10 +53,31 @@ defmodule Styler.Style.Blocks do
53
53
# to `case single_statement do success -> body; ...elses end`
54
54
def run({{:with, m, [{:<-, am, [success, single_statement]}, [body, elses]]}, zm}, ctx) do
55
55
{{:__block__, do_meta, [:do]}, body} = body
56
- {{:__block__, _else_meta, [:else]}, elses} = elses
56
+ {{:__block__, _, [:else]}, elses} = elses
57
+
58
+ elses =
59
+ case elses do
60
+ # unwrap a stab ala `, else: (_ -> :ok)`. these became literals in 1.17
61
+ {:__block__, _, [[{:->, _, _}] = stab]} -> stab
62
+ elses -> elses
63
+ end
64
+
65
+ # drops keyword formatting etc
66
+ do_meta = [line: do_meta[:line]]
57
67
clauses = [{{:__block__, am, [:do]}, [{:->, do_meta, [[success], body]} | elses]}]
68
+ end_line = Style.max_line(elses) + 1
69
+
70
+ # fun fact: i added the detailed meta just because i noticed it was missing while debugging something ...
71
+ # ... and it fixed the bug 🤷
72
+ case_meta = [
73
+ end_of_expression: [newlines: 1, line: end_line],
74
+ do: do_meta,
75
+ end: [line: end_line],
76
+ line: m[:line]
77
+ ]
78
+
58
79
# recurse in case this new case should be rewritten to a `if`, etc
59
- run({{:case, m, [single_statement, clauses]}, zm}, ctx)
80
+ run({{:case, case_meta, [single_statement, clauses]}, zm}, ctx)
60
81
end
61
82
62
83
# `with true <- x, do: bar` =>`if x, do: bar`
 
@@ -75,10 +96,6 @@ defmodule Styler.Style.Blocks do
75
96
{:cont, Zipper.replace(zipper, {:if, m, children}), ctx}
76
97
end
77
98
78
- def run({{:with, _, [[{{:__block__, _, [:do]}, body} | _]]}, _} = zipper, ctx) do
79
- {:cont, Zipper.replace(zipper, body), ctx}
80
- end
81
-
82
99
# Credo.Check.Refactor.WithClauses
83
100
def run({{:with, _, children}, _} = zipper, ctx) when is_list(children) do
84
101
do_block? = Enum.any?(children, &Style.do_block?/1)
changed lib/style/deprecations.ex
 
@@ -58,6 +58,17 @@ defmodule Styler.Style.Deprecations do
58
58
do: {:|>, m, [lhs, {f, fm, [lob, opts]}]}
59
59
end
60
60
61
+ if Version.match?(System.version(), ">= 1.17.0-dev") do
62
+ for {erl, ex} <- [hours: :hour, minutes: :minute, seconds: :second] do
63
+ defp style({{:., _, [{:__block__, _, [:timer]}, unquote(erl)]}, fm, [x]}),
64
+ do: {:to_timeout, fm, [[{{:__block__, [format: :keyword, line: fm[:line]], [unquote(ex)]}, x}]]}
65
+ end
66
+ end
67
+
68
+ # Struct update syntax is deprecated in 1.19
69
+ # `%Foo{x | y} => %{x | y}`
70
+ defp style({:%, _, [_struct, {:%{}, _, [{:|, _, _}]} = update]}), do: update
71
+
61
72
# For ranges where `start > stop`, you need to explicitly include the step
62
73
# Enum.slice(enumerable, 1..-2) => Enum.slice(enumerable, 1..-2//1)
63
74
# String.slice("elixir", 2..-1) => String.slice("elixir", 2..-1//1)
 
@@ -98,7 +109,7 @@ defmodule Styler.Style.Deprecations do
98
109
99
110
defp style(node), do: node
100
111
101
- defp rewrite_range_match({:.., dm, [first, {_, m, _} = last]}), do: {:"..//", dm, [first, last, {:_, m, nil}]}
112
+ defp rewrite_range_match({:.., dm, [first, {_, m, _} = last]}), do: {:..//, dm, [first, last, {:_, m, nil}]}
102
113
defp rewrite_range_match(x), do: x
103
114
104
115
defp add_step_to_date_range?(first, last) do
 
@@ -117,7 +128,7 @@ defmodule Styler.Style.Deprecations do
117
128
{:ok, stop} <- extract_value_from_range(last),
118
129
true <- start > stop do
119
130
step = {:__block__, [token: "1", line: lm[:line]], [1]}
120
- {:"..//", rm, [first, last, step]}
131
+ {:..//, rm, [first, last, step]}
121
132
else
122
133
_ -> range
123
134
end
changed lib/style/module_directives.ex
 
@@ -282,8 +282,7 @@ defmodule Styler.Style.ModuleDirectives do
282
282
# we can't use the dealias map built into state as that's what things look like before sorting
283
283
# now that we've sorted, it could be different!
284
284
dealiases = AliasEnv.define(aliases)
285
- excluded = dealiases |> Map.keys() |> Enum.into(Styler.Config.get(:lifting_excludes))
286
- liftable = find_liftable_aliases(requires ++ nondirectives, excluded)
285
+ liftable = find_liftable_aliases(requires ++ nondirectives, dealiases)
287
286
288
287
if Enum.any?(liftable) do
289
288
# This is a silly hack that helps comments stay put.
 
@@ -306,9 +305,15 @@ defmodule Styler.Style.ModuleDirectives do
306
305
end
307
306
end
308
307
309
- defp find_liftable_aliases(ast, excluded) do
308
+ defp find_liftable_aliases(ast, dealiases) do
309
+ excluded = dealiases |> Map.keys() |> Enum.into(Styler.Config.get(:lifting_excludes))
310
+
311
+ firsts = MapSet.new(dealiases, fn {_last, [first | _]} -> first end)
312
+
310
313
ast
311
314
|> Zipper.zip()
315
+ # we're reducing a datastructure that looks like
316
+ # %{last => {aliases, seen_before?} | :some_collision_probelm}
312
317
|> Zipper.reduce_while(%{}, fn
313
318
# we don't want to rewrite alias name `defx Aliases ... do` of these three keywords
314
319
{{defx, _, args}, _} = zipper, lifts when defx in ~w(defmodule defimpl defprotocol)a ->
 
@@ -329,19 +334,83 @@ defmodule Styler.Style.ModuleDirectives do
329
334
{{:quote, _, _}, _} = zipper, lifts ->
330
335
{:skip, zipper, lifts}
331
336
332
- {{:__aliases__, _, [_, _, _ | _] = aliases}, _} = zipper, lifts ->
337
+ {{:__aliases__, _, [first, _, _ | _] = aliases}, _} = zipper, lifts ->
333
338
last = List.last(aliases)
334
339
335
340
lifts =
336
- if last in excluded or not Enum.all?(aliases, &is_atom/1) do
337
- lifts
338
- else
339
- Map.update(lifts, last, {aliases, false}, fn
340
- {^aliases, _} -> {aliases, true}
341
- # if we have `Foo.Bar.Baz` and `Foo.Bar.Bop.Baz` both not aliased, we'll create a collision by lifting both
342
- # grouping by last alias lets us detect these collisions
343
- _ -> :collision_with_last
344
- end)
341
+ cond do
342
+ # this alias already exists, they just wrote it out fully and are leaving it up to us to shorten it down!
343
+ dealiases[last] == aliases ->
344
+ Map.put(lifts, last, {aliases, true})
345
+
346
+ last in excluded or Enum.any?(aliases, &(not is_atom(&1))) ->
347
+ lifts
348
+
349
+ # aliasing this would change the meaning of an existing alias
350
+ last > first and last in firsts ->
351
+ lifts
352
+
353
+ # We've seen this once before, time to mark it for lifting and do some bookkeeping for first-collisions
354
+ lifts[last] == {aliases, false} ->
355
+ lifts = Map.put(lifts, last, {aliases, true})
356
+
357
+ # Here's the bookkeeping for collisions with this alias's first module name...
358
+ case lifts[first] do
359
+ {:collision_with_first, claimants, colliders} ->
360
+ # release our claim on this collision
361
+ claimants = MapSet.delete(claimants, aliases)
362
+ empty? = Enum.empty?(claimants)
363
+
364
+ cond do
365
+ empty? and Enum.any?(colliders) ->
366
+ # no more claimants, try to promote a collider to be lifted
367
+ colliders = Enum.to_list(colliders)
368
+ # There's no longer a collision because the only claimant is being lifted.
369
+ # So, promote a claimant with these criteria
370
+ # - required: its first comes _after_ last, so we aren't promoting an alias that changes the meaning of the other alias we're doing
371
+ # - preferred: take a collider we know we want to lift (we've seen it multiple times)
372
+ lift =
373
+ Enum.reduce_while(colliders, :collision_with_first, fn
374
+ {[first | _], true} = liftable, _ when first > last ->
375
+ {:halt, liftable}
376
+
377
+ {[first | _], _false} = promotable, :collision_with_first when first > last ->
378
+ {:cont, promotable}
379
+
380
+ _, result ->
381
+ {:cont, result}
382
+ end)
383
+
384
+ Map.put(lifts, first, lift)
385
+
386
+ empty? ->
387
+ Map.delete(lifts, first)
388
+
389
+ true ->
390
+ Map.put(lifts, first, {:collision_with_first, claimants, colliders})
391
+ end
392
+
393
+ _ ->
394
+ lifts
395
+ end
396
+
397
+ true ->
398
+ lifts
399
+ |> Map.update(last, {aliases, false}, fn
400
+ # if something is claiming the atom we want, add ourselves to the list of colliders
401
+ {:collision_with_first, claimers, colliders} ->
402
+ {:collision_with_first, claimers, Map.update(colliders, aliases, false, fn _ -> true end)}
403
+
404
+ other ->
405
+ other
406
+ end)
407
+ |> Map.update(first, {:collision_with_first, MapSet.new([aliases]), %{}}, fn
408
+ {:collision_with_first, claimers, colliders} ->
409
+ {:collision_with_first, MapSet.put(claimers, aliases), colliders}
410
+
411
+ other ->
412
+ other
413
+ end)
345
414
end
346
415
347
416
{:skip, zipper, lifts}
 
@@ -354,6 +423,7 @@ defmodule Styler.Style.ModuleDirectives do
354
423
# C.foo()
355
424
#
356
425
# lifting A.B.C would create a collision with C.
426
+ # unlike the collision_with_first tuple book-keeping, there's no recovery here because we won't lift a < 3 length alias
357
427
{:skip, zipper, Map.put(lifts, first, :collision_with_first)}
358
428
359
429
zipper, lifts ->
changed lib/style/pipes.ex
 
@@ -95,7 +95,7 @@ defmodule Styler.Style.Pipes do
95
95
96
96
comments =
97
97
ctx.comments
98
- |> Style.displace_comments(lhs_line..(rhs_line - 1))
98
+ |> Style.displace_comments(lhs_line..(rhs_line - 1)//1)
99
99
|> Style.shift_comments(rhs_line..rhs_max_line, shift + 1)
100
100
101
101
{:cont, Zipper.replace(single_pipe_zipper, {fun, meta, [lhs | args]}), %{ctx | comments: comments}}
 
@@ -132,7 +132,7 @@ defmodule Styler.Style.Pipes do
132
132
133
133
# a(b |> c[, ...args])
134
134
# The first argument to a function-looking node is a pipe.
135
- # Maybe pipe the whole thing?
135
+ # Maybe pipify the whole thing?
136
136
def run({{f, m, [{:|>, _, _} = pipe | args]}, _} = zipper, ctx) do
137
137
parent =
138
138
case Zipper.up(zipper) do
 
@@ -168,7 +168,11 @@ defmodule Styler.Style.Pipes do
168
168
{:cont, zipper, ctx}
169
169
170
170
true ->
171
- {:cont, Zipper.replace(zipper, {:|>, m, [pipe, {f, m, args}]}), ctx}
171
+ zipper = Zipper.replace(zipper, {:|>, m, [pipe, {f, m, args}]})
172
+ # it's possible this is a nested function call `c(b(a |> b))`, so we should walk up the tree for de-nesting
173
+ zipper = Zipper.up(zipper) || zipper
174
+ # recursion ensures we get those nested function calls and any additional pipes
175
+ run(zipper, ctx)
172
176
end
173
177
end
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.3"
15
+ @version "1.4.0"
16
16
@url "https://github.com/adobe/elixir-styler"
17
17
18
18
def project do
 
@@ -70,6 +70,7 @@ defmodule Styler.MixProject do
70
70
"docs/control_flow_macros.md": [title: "Control Flow Macros (if, case, ...)"],
71
71
"docs/mix_configs.md": [title: "Mix Configs (config/*.exs)"],
72
72
"docs/module_directives.md": [title: "Module Directives (use, alias, ...)"],
73
+ "docs/comment_directives.md": [title: "Comment Directives (# styler:sort)"],
73
74
"docs/credo.md": [title: "Styler & Credo"],
74
75
"README.md": [title: "Styler"]
75
76
]