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
|
]
|