Skip to content

Commit f839aa6

Browse files
committed
module attributes: dont break references from use, moduledoc, etc
1 parent a68788c commit f839aa6

3 files changed

Lines changed: 70 additions & 81 deletions

File tree

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,20 @@ they can and will change without that change being reflected in Styler's semanti
99

1010
- `mix styler.inline_attrs`: Allow multiple file paths to be specified: `mix styler.inline_attrs <file1> [... additional files]`
1111

12+
#### Module Directive References
13+
14+
Module directives got smarter. Styler will no longer move module attributes below their references in `use` or `@moduledoc`s.
15+
16+
In other words, Styler will leave the following code untouched:
17+
18+
```elixir
19+
defmodule MyGreatLibrary do
20+
@library_options [...]
21+
@moduledoc make_pretty_docs(@library_options)
22+
use OptionsMagic, my_opts: @library_options
23+
end
24+
```
25+
1226
## 1.10.1
1327

1428
### Improvements

lib/style/module_directives.ex

Lines changed: 30 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,7 @@ defmodule Styler.Style.ModuleDirectives do
6060
require: [],
6161
nondirectives: [],
6262
alias_env: %{},
63-
attrs: MapSet.new(),
64-
attr_lifts: []
63+
attrs: MapSet.new()
6564
}
6665

6766
# module directives typically doesn't do anything until it sees a module (typical .ex file) or a directive (like a snippet)
@@ -73,7 +72,7 @@ defmodule Styler.Style.ModuleDirectives do
7372
def run(zipper, %{__MODULE__ => true} = ctx), do: do_run(zipper, ctx)
7473

7574
def run({node, nil} = zipper, ctx) do
76-
if interesting_zipper = Zipper.find(zipper, &interesting?/1) do
75+
if interesting_zipper = Zipper.find(zipper, &match?({x, _, _} when x in [:defmodule, :@ | @directives], &1)) do
7776
do_run(interesting_zipper, Map.put(ctx, __MODULE__, true))
7877
else
7978
# there's no defmodules or aliasy things - see if we can do some alias lifting?
@@ -103,9 +102,6 @@ defmodule Styler.Style.ModuleDirectives do
103102
end
104103
end
105104

106-
defp interesting?({x, _, _}) when x in [:defmodule, :@ | @directives], do: true
107-
defp interesting?(_), do: false
108-
109105
defp do_run({{:defmodule, _, children}, _} = zipper, ctx) do
110106
[name, [{{:__block__, do_meta, [:do]}, _body}]] = children
111107

@@ -198,28 +194,38 @@ defmodule Styler.Style.ModuleDirectives do
198194
# a dynamic module name, like `defmodule my_variable do ... end`
199195
defp moduledoc(_), do: nil
200196

201-
defp lift_module_attrs({node, _, _} = ast, %{attrs: attrs} = acc) do
197+
# pops module attributes this directive depends on out of `nondirectives`,
198+
# returning their definitions in a list alongside the updated accumulator
199+
#
200+
# naively grouping module attributes in nondirectives can break a common pattern where an attribute is referenced by
201+
# `use` or `@moduledoc` clauses, which then get moved above the attributes they reference.
202+
# this function finds and returns those dependencies, allowing the caller to keep them above the dependent directive
203+
defp split_depended_attributes({_, _, _} = directive, %{attrs: attrs, nondirectives: nondirectives} = acc) do
202204
if MapSet.size(attrs) == 0 do
203-
{ast, acc}
205+
{[], acc}
204206
else
205-
use? = node == :use
206-
207-
Macro.prewalk(ast, acc, fn
208-
{:@, m, [{attr, _, _} = var]} = ast, acc ->
207+
directive
208+
|> Macro.prewalk({[], acc}, fn
209+
{:@, _, [{attr, _, _}]} = ast, {prepends, acc} ->
209210
if attr in attrs do
210-
replacement =
211-
if use?,
212-
do: {:unquote, [closing: [line: m[:line]], line: m[:line]], [var]},
213-
else: var
211+
{definitions, nondirectives} = Enum.split_with(nondirectives, &match?({:@, _, [{^attr, _, _}]}, &1))
212+
acc = %{acc | nondirectives: nondirectives}
214213

215-
{replacement, %{acc | attr_lifts: [attr | acc.attr_lifts]}}
214+
recursed =
215+
Enum.reduce(definitions, {definitions ++ prepends, acc}, fn ast, {prepends, acc} ->
216+
{definitions, acc} = split_depended_attributes(ast, acc)
217+
{prepends ++ definitions, acc}
218+
end)
219+
220+
{ast, recursed}
216221
else
217-
{ast, acc}
222+
{ast, {prepends, acc}}
218223
end
219224

220225
ast, acc ->
221226
{ast, acc}
222227
end)
228+
|> elem(1)
223229
end
224230
end
225231

@@ -230,21 +236,22 @@ defmodule Styler.Style.ModuleDirectives do
230236
|> Enum.reduce(@env, fn
231237
{:@, _, [{attr_directive, _, _}]} = ast, acc when attr_directive in @attr_directives ->
232238
# attr_directives are moved above aliases, so we need to expand them
233-
{ast, acc} = acc.alias_env |> AliasEnv.expand_ast(ast) |> lift_module_attrs(acc)
234-
%{acc | attr_directive => [ast | acc[attr_directive]]}
239+
ast = AliasEnv.expand_ast(acc.alias_env, ast)
240+
{prepends, acc} = split_depended_attributes(ast, acc)
241+
%{acc | attr_directive => [ast | prepends ++ acc[attr_directive]]}
235242

236243
{:@, _, [{attr, _, _}]} = ast, acc ->
237244
%{acc | nondirectives: [ast | acc.nondirectives], attrs: MapSet.put(acc.attrs, attr)}
238245

239246
{directive, _, _} = ast, acc when directive in @directives ->
240-
{ast, acc} = lift_module_attrs(ast, acc)
247+
{prepends, acc} = split_depended_attributes(ast, acc)
241248
ast = expand(ast)
242-
# import and used get hoisted above aliases, so need to expand them
249+
# import and use get hoisted above aliases, so need to expand them
243250
ast = if directive in ~w(import use)a, do: AliasEnv.expand_ast(acc.alias_env, ast), else: ast
244251
alias_env = if directive == :alias, do: AliasEnv.define(acc.alias_env, ast), else: acc.alias_env
245252

246253
# the reverse accounts for `expand` putting things in reading order, whereas we're accumulating in reverse
247-
%{acc | directive => Enum.reverse(ast, acc[directive]), alias_env: alias_env}
254+
%{acc | directive => Enum.reverse(ast, prepends ++ acc[directive]), alias_env: alias_env}
248255

249256
ast, acc ->
250257
%{acc | nondirectives: [ast | acc.nondirectives]}
@@ -261,38 +268,6 @@ defmodule Styler.Style.ModuleDirectives do
261268
|> lift_aliases()
262269
|> apply_aliases()
263270

264-
# Not happy with it, but this does the work to move module attribute assignments above the module or quote or whatever
265-
# Given that it'll only be run once and not again, i'm okay with it being inefficient
266-
{acc, parent} =
267-
if Enum.any?(acc.attr_lifts) do
268-
lifts = acc.attr_lifts
269-
270-
nondirectives =
271-
Enum.map(acc.nondirectives, fn
272-
{:@, m, [{attr, am, _}]} = ast -> if attr in lifts, do: {:@, m, [{attr, am, [{attr, am, nil}]}]}, else: ast
273-
ast -> ast
274-
end)
275-
276-
assignments =
277-
Enum.flat_map(acc.nondirectives, fn
278-
{:@, m, [{attr, am, [val]}]} -> if attr in lifts, do: [{:=, m, [{attr, am, nil}, val]}], else: []
279-
_ -> []
280-
end)
281-
282-
{past, _} = parent
283-
284-
parent =
285-
parent
286-
|> Zipper.up()
287-
|> Style.find_nearest_block()
288-
|> Zipper.prepend_siblings(assignments)
289-
|> Zipper.find(&(&1 == past))
290-
291-
{%{acc | nondirectives: nondirectives}, parent}
292-
else
293-
{acc, parent}
294-
end
295-
296271
nondirectives = acc.nondirectives
297272

298273
directives =

test/style/module_directives_test.exs

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -592,48 +592,48 @@ defmodule Styler.Style.ModuleDirectivesTest do
592592
end
593593

594594
describe "module attribute lifting" do
595-
test "replaces uses in other attributes and `use` correctly" do
595+
test "maintains location when used in other spots" do
596+
assert_style("""
597+
defmodule MyGreatLibrary do
598+
@library_options [...]
599+
@moduledoc make_pretty_docs(@library_options)
600+
use OptionsMagic, my_opts: @library_options
601+
end
602+
""")
603+
end
604+
605+
test "interdepedent module attrs" do
596606
assert_style(
597607
"""
598608
defmodule MyGreatLibrary do
599-
@library_options [...]
609+
@foo :bar
610+
import Meow
611+
@library_options @foo
600612
@moduledoc make_pretty_docs(@library_options)
601613
use OptionsMagic, my_opts: @library_options
602614
end
603615
""",
604616
"""
605-
library_options = [...]
606-
607617
defmodule MyGreatLibrary do
608-
@moduledoc make_pretty_docs(library_options)
609-
use OptionsMagic, my_opts: unquote(library_options)
618+
@foo :bar
619+
@library_options @foo
620+
@moduledoc make_pretty_docs(@library_options)
621+
use OptionsMagic, my_opts: @library_options
610622
611-
@library_options library_options
623+
import Meow
612624
end
613625
"""
614626
)
615627
end
616628

617629
test "works with `quote`" do
618-
assert_style(
619-
"""
620-
quote do
621-
@library_options [...]
622-
@moduledoc make_pretty_docs(@library_options)
623-
use OptionsMagic, my_opts: @library_options
624-
end
625-
""",
626-
"""
627-
library_options = [...]
628-
629-
quote do
630-
@moduledoc make_pretty_docs(library_options)
631-
use OptionsMagic, my_opts: unquote(library_options)
632-
633-
@library_options library_options
634-
end
635-
"""
636-
)
630+
assert_style("""
631+
quote do
632+
@library_options [...]
633+
@moduledoc make_pretty_docs(@library_options)
634+
use OptionsMagic, my_opts: @library_options
635+
end
636+
""")
637637
end
638638
end
639639

0 commit comments

Comments
 (0)