Skip to content

Commit 8f921f1

Browse files
authored
Add a mix format plugin. (#4)
1 parent fcc91d1 commit 8f921f1

File tree

7 files changed

+250
-165
lines changed

7 files changed

+250
-165
lines changed

README.md

+39-13
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,17 @@ This feature will be immediately familiar to JavaScript and Rust developers, and
1717

1818
### Is there any runtime overhead?
1919

20-
No; the shorthand map keys compile down to exactly the same bytecode as the "old-style" maps.
20+
No; the shorthand map keys compile down to exactly the same bytecode as the "vanilla-style" maps.
2121

2222
## Installation
2323

2424
The package can be installed by adding `es6_maps` to your list of dependencies and compilers in `mix.exs`:
2525

2626
```elixir
27+
# mix.exs
28+
2729
def project do
2830
[
29-
app: :testme,
30-
version: "0.1.0",
3131
compilers: [:es6_maps | Mix.compilers()],
3232
deps: deps()
3333
]
@@ -94,28 +94,54 @@ iex> hello
9494

9595
## Converting existing code to use ES6-style maps
9696

97-
`es6_maps` includes a formatting task that will convert your existing map & struct literals into the shorthand style:
97+
`es6_maps` includes a formatting plugin that will convert your existing map and struct literals into the shorthand style.
98+
Add the plugin to `.formatter.exs`, then call `mix format` to reformat your code:
9899

99-
```shell
100-
mix es6_maps.format 'lib/**/*.ex' 'test/**/*.exs'
100+
```elixir
101+
# .formatter.exs
102+
[
103+
plugins: [Es6Maps.Formatter],
104+
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
105+
]
101106
```
102107

103-
The formatting task manipulates the AST, not raw strings, so it's precise and will only change your code by:
108+
The plugin manipulates the AST, not raw strings, so it's precise and will only change your code by:
104109

105110
1. changing map keys into the shorthand form;
106111
2. reordering map keys so the shorthand form comes first;
107-
3. formatting the results with `mix format`.
112+
3. formatting the results like `mix format` would.
113+
114+
### Reverting to the vanilla-style maps
108115

109-
See `mix help es6_maps.format` for more options and information.
116+
The formatting plugin can also be used to revert all of the ES6-style map shorthand uses back to the "vanilla" style.
117+
Set the `es6_maps: [map_style: :vanilla]` option in `.formatter.exs`, then call `mix format` to reformat your code:
110118

111-
### Going back to old-style maps
119+
```elixir
120+
# .formatter.exs
121+
[
122+
plugins: [Es6Maps.Formatter],
123+
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"],
124+
es6_maps: [map_style: :vanilla]
125+
]
126+
```
112127

113-
You can revert all of the ES6-style shorthand uses with the `--revert` format flag:
128+
### Formatting pragmas
114129

115-
```shell
116-
mix es6_maps.format --revert lib/myapp/myapp.ex
130+
The plugin supports pragmas in the comments to control the formatting.
131+
The pragma must be in the form `# es6_maps: [map_style: :es6]` and can be placed anywhere in the file.
132+
The `map_style` option can be set to `:es6` to convert to shorthand form or `:vanilla` to revert to the vanilla-style maps.
133+
The pragma takes effect only on the line following the comment.
134+
135+
For example in the code below, the first map will be formatted to the shorthand form, while the second map will be left as is:
136+
137+
```elixir
138+
%{foo, bar: 1} = var
139+
# es6_maps: [map_style: :vanilla]
140+
%{hello: hello, foo: foo, bar: 1} = var
117141
```
118142

143+
`es6_maps: [map_style: :vanilla]` option in `.formatter.exs` can be combined with `# es6_maps: [map_style: :es6]` comment pragmas.
144+
119145
## How does it work?
120146

121147
`es6_maps` replaces in runtime the Elixir compiler's `elixir_map` module.

lib/es6_maps/formatter.ex

+152
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
defmodule Es6Maps.Formatter do
2+
@moduledoc """
3+
Replaces all map keys with their shorthand form.
4+
5+
Add the plugin to `.formatter.exs`, then call `mix format` to reformat your code:
6+
7+
```elixir
8+
# .formatter.exs
9+
[
10+
plugins: [Es6Maps.Formatter],
11+
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
12+
]
13+
```
14+
15+
The plugin manipulates the AST, not raw strings, so it's precise and will only change your code by:
16+
17+
1. changing map keys into the shorthand form;
18+
2. reordering map keys so the shorthand form comes first;
19+
3. formatting the results like `mix format` would.
20+
21+
### Reverting to the vanilla-style maps
22+
23+
The formatting plugin can also be used to revert all of the ES6-style map shorthand uses back to the "vanilla" style.
24+
Set the `es6_maps: [map_style: :vanilla]` option in `.formatter.exs`, then call `mix format` to reformat your code:
25+
26+
```elixir
27+
# .formatter.exs
28+
[
29+
plugins: [Es6Maps.Formatter],
30+
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"],
31+
es6_maps: [map_style: :vanilla]
32+
]
33+
```
34+
35+
### Formatting pragmas
36+
37+
The plugin supports pragmas in the comments to control the formatting.
38+
The pragma must be in the form `# es6_maps: [map_style: :vanilla]` and can be placed anywhere in the file.
39+
The `map_style` option can be set to `:es6` to convert to shorthand form or `:vanilla` to revert to the vanilla-style maps.
40+
The pragma takes effect only on the line following the comment.
41+
42+
For example in the code below, the first map will be formatted to the shorthand form, while the second map will be left as is:
43+
44+
```elixir
45+
%{foo, bar: 1} = var
46+
# es6_maps: [map_style: :vanilla]
47+
%{hello: hello, foo: foo, bar: 1} = var
48+
```
49+
50+
`es6_maps: [map_style: :vanilla]` option in `.formatter.exs` can be combined with `# es6_maps: [map_style: :es6]` comment pragmas.
51+
52+
## Options
53+
* `es6_maps`:
54+
* `map_style` - `:es6` to convert to shorthand form, `:vanilla` to revert to the vanilla-style maps.
55+
* all other options of mix format, such as `line_length`, are supported and passed down to formatting functions.
56+
"""
57+
58+
@behaviour Mix.Tasks.Format
59+
60+
@impl Mix.Tasks.Format
61+
def features(_opts), do: [sigils: [], extensions: [".ex", ".exs"]]
62+
63+
def format(contents), do: format(contents, [])
64+
65+
@impl Mix.Tasks.Format
66+
def format(contents, opts) do
67+
line_length = Keyword.get(opts, :line_length, 98)
68+
69+
{quoted, comments} =
70+
Code.string_to_quoted_with_comments!(
71+
contents,
72+
Keyword.merge(
73+
[
74+
unescape: false,
75+
literal_encoder: &{:ok, {:__block__, &2, [&1]}},
76+
token_metadata: true,
77+
emit_warnings: false
78+
],
79+
opts
80+
)
81+
)
82+
83+
pragmas = comments_to_pragmas(comments)
84+
85+
quoted
86+
|> Macro.postwalk(&format_map(&1, pragmas, opts))
87+
|> Code.Formatter.to_algebra(Keyword.merge([comments: comments], opts))
88+
|> Inspect.Algebra.format(line_length)
89+
|> case do
90+
[] -> ""
91+
text -> IO.iodata_to_binary([text, ?\n])
92+
end
93+
end
94+
95+
defp comments_to_pragmas(comments) do
96+
comments
97+
|> Enum.filter(&String.starts_with?(&1.text, "# es6_maps: "))
98+
|> Map.new(fn comment ->
99+
{settings, _} =
100+
comment.text
101+
|> String.replace_prefix("# ", "[")
102+
|> String.replace_suffix("", "]")
103+
|> Code.eval_string()
104+
105+
{comment.line + 1, settings}
106+
end)
107+
end
108+
109+
defp format_map({:%{}, meta, [{:|, pipemeta, [lhs, elements]}]}, pragmas, opts) do
110+
{_, _, mapped_elements} = format_map({:%{}, meta, elements}, pragmas, opts)
111+
{:%{}, meta, [{:|, pipemeta, [lhs, mapped_elements]}]}
112+
end
113+
114+
defp format_map({:%{}, meta, _elements} = map, pragmas, opts) do
115+
opts = Config.Reader.merge(opts, Map.get(pragmas, meta[:line], []))
116+
117+
case Kernel.get_in(opts, [:es6_maps, :map_style]) || :es6 do
118+
:es6 -> format_map_es6(map)
119+
:vanilla -> format_map_vanilla(map)
120+
other -> raise ArgumentError, "invalid map_style: #{inspect(other)}"
121+
end
122+
end
123+
124+
defp format_map(node, _pragmas, _opts), do: node
125+
126+
defp format_map_vanilla({:%{}, meta, elements}) do
127+
{:%{}, meta,
128+
Enum.map(elements, fn
129+
{key, meta, context} = var when is_atom(context) ->
130+
{{:__block__, [format: :keyword] ++ meta, [key]}, var}
131+
132+
elem ->
133+
elem
134+
end)}
135+
end
136+
137+
defp format_map_es6({:%{}, meta, elements}) do
138+
{vars, key_vals} =
139+
Enum.reduce(elements, {[], []}, fn
140+
{{:__block__, _, [key]}, {key, _, ctx} = var}, {vars, key_vals} when is_atom(ctx) ->
141+
{[var | vars], key_vals}
142+
143+
{_, _, ctx} = var, {vars, key_vals} when is_atom(ctx) ->
144+
{[var | vars], key_vals}
145+
146+
key_val, {vars, key_vals} ->
147+
{vars, [key_val | key_vals]}
148+
end)
149+
150+
{:%{}, meta, Enum.reverse(key_vals ++ vars)}
151+
end
152+
end

lib/mix/tasks/es6_maps/format.ex

-137
This file was deleted.

test/es6_maps_test/mix.exs

+1-2
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,7 @@ defmodule Es6MapsTest.MixProject do
2121

2222
defp deps do
2323
[
24-
{:es6_maps, path: "../..", runtime: false},
25-
{:briefly, "~> 0.5.0", only: :test}
24+
{:es6_maps, path: "../..", runtime: false}
2625
]
2726
end
2827

test/es6_maps_test/mix.lock

-3
This file was deleted.

0 commit comments

Comments
 (0)