-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
344 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
# Used by "mix format" | ||
[ | ||
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"] | ||
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"], | ||
locals_without_parens: [test_formatting: 2] | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
defmodule Mix.Tasks.Es6Maps.Format do | ||
@shortdoc "Replaces all map keys with their shorthand form" | ||
@moduledoc """ | ||
Replaces all map keys with their shorthand form. | ||
```shell | ||
mix es6_maps.format 'lib/**/*.ex' 'test/**/*.exs' | ||
``` | ||
The arguments are expanded with `Path.wildcard(match_dot: true)`. | ||
The task manipulates the AST, not raw strings, so it's precise and will only change your code by: | ||
1. changing map keys into the shorthand form; | ||
2. reordering map keys so the shorthand form comes first; | ||
3. formatting the results with `mix format`. | ||
### Going back to old-style maps | ||
You can revert all of the ES6-style shorthand uses with the `--revert` format flag: | ||
```shell | ||
mix es6_maps.format --revert lib/myapp/myapp.ex | ||
``` | ||
### Reordering map keys | ||
When applicable, the formatting will reorder the keys to shorthand them, for example: | ||
```elixir | ||
%{hello: "world", foo: foo, bar: bar} = var | ||
``` | ||
will become: | ||
```elixir | ||
%{foo, bar, hello: "world"} = var | ||
``` | ||
## Options | ||
* `--revert` - Reverts the transformation. | ||
* `--locals-without-parens` - Specifies a list of locals that should not have parentheses. | ||
The format is `local_name/arity`, where `arity` can be an integer or `*`. This option can | ||
be given multiple times, and/or multiple values can be separated by commas. | ||
""" | ||
|
||
use Mix.Task | ||
|
||
@switches [revert: :boolean, locals_without_parens: :keep] | ||
|
||
@impl Mix.Task | ||
def run(all_args) do | ||
{opts, args} = OptionParser.parse!(all_args, strict: @switches) | ||
|
||
locals_without_parens = collect_locals_without_parens(opts) | ||
revert = Keyword.get(opts, :revert, false) | ||
opts = %{locals_without_parens: locals_without_parens, revert: revert} | ||
|
||
Enum.each(collect_paths(args), &format_file(&1, opts)) | ||
Mix.Tasks.Format.run(args) | ||
end | ||
|
||
defp collect_locals_without_parens(opts) do | ||
opts | ||
|> Keyword.get_values(:locals_without_parens) | ||
|> Enum.flat_map(&String.split(&1, ",")) | ||
|> Enum.map(fn local_str -> | ||
[fname_str, arity_str] = | ||
case String.split(local_str, "/", parts: 2) do | ||
[fname_str, arity_str] -> [fname_str, arity_str] | ||
_ -> raise ArgumentError, "invalid local: #{local_str}" | ||
end | ||
|
||
fname = String.to_atom(fname_str) | ||
arity = if arity_str == "*", do: :*, else: String.to_integer(arity_str) | ||
{fname, arity} | ||
end) | ||
end | ||
|
||
defp collect_paths(paths) do | ||
paths |> Enum.flat_map(&Path.wildcard(&1, match_dot: true)) |> Enum.filter(&File.regular?/1) | ||
end | ||
|
||
defp format_file(filepath, opts) do | ||
{quoted, comments} = | ||
filepath | ||
|> File.read!() | ||
|> Code.string_to_quoted_with_comments!( | ||
emit_warnings: false, | ||
literal_encoder: &{:ok, {:__block__, &2, [&1]}}, | ||
token_metadata: true, | ||
unescape: false, | ||
file: filepath | ||
) | ||
|
||
quoted | ||
|> Macro.postwalk(&format_map(&1, opts)) | ||
|> Code.quoted_to_algebra( | ||
comments: comments, | ||
escape: false, | ||
locals_without_parens: opts.locals_without_parens | ||
) | ||
|> Inspect.Algebra.format(:infinity) | ||
|> then(&File.write!(filepath, &1)) | ||
end | ||
|
||
defp format_map({:%{}, meta, [{:|, pipemeta, [lhs, elements]}]}, opts) do | ||
{_, _, mapped_elements} = format_map({:%{}, pipemeta, elements}, opts) | ||
{:%{}, meta, [{:|, pipemeta, [lhs, mapped_elements]}]} | ||
end | ||
|
||
defp format_map({:%{}, meta, elements}, %{revert: true}) do | ||
{:%{}, meta, | ||
Enum.map(elements, fn | ||
{key, _meta, context} = var when is_atom(context) -> {key, var} | ||
elem -> elem | ||
end)} | ||
end | ||
|
||
defp format_map({:%{}, meta, elements}, _opts) do | ||
{vars, key_vals} = | ||
Enum.reduce(elements, {[], []}, fn | ||
{{:__block__, _, [key]}, {key, _, ctx} = var}, {vars, key_vals} when is_atom(ctx) -> | ||
{[var | vars], key_vals} | ||
|
||
{_, _, ctx} = var, {vars, key_vals} when is_atom(ctx) -> | ||
{[var | vars], key_vals} | ||
|
||
key_val, {vars, key_vals} -> | ||
{vars, [key_val | key_vals]} | ||
end) | ||
|
||
{:%{}, meta, Enum.reverse(key_vals ++ vars)} | ||
end | ||
|
||
defp format_map(node, _opts), do: node | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
%{ | ||
"briefly": {:hex, :briefly, "0.5.1", "ee10d48da7f79ed2aebdc3e536d5f9a0c3e36ff76c0ad0d4254653a152b13a8a", [:mix], [], "hexpm", "bd684aa92ad8b7b4e0d92c31200993c4bc1469fc68cd6d5f15144041bd15cb57"}, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
defmodule Es6MapsTestTest do | ||
defmodule Es6MapsTest.Es6Maps do | ||
use ExUnit.Case | ||
|
||
defmodule MyStruct do | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
defmodule Es6MapsTest.Format do | ||
use ExUnit.Case | ||
|
||
import Es6MapsTest.Support.FormattingAssertions | ||
|
||
describe "map literal" do | ||
test_formatting "has its keys reformatted into shorthands", | ||
original: """ | ||
def test(var) do | ||
%{a: a, b: b, c: 1} = var | ||
var | ||
end | ||
""", | ||
formatted: """ | ||
def test(var) do | ||
%{a, b, c: 1} = var | ||
var | ||
end | ||
""" | ||
|
||
test_formatting "has its keys moved to the front when reformatting to shorthand", | ||
original: """ | ||
def test(var) do | ||
%{a: 1, b: 2, c: c, d: d} = var | ||
var | ||
end | ||
""", | ||
formatted: """ | ||
def test(var) do | ||
%{c, d, a: 1, b: 2} = var | ||
var | ||
end | ||
""", | ||
reverted: """ | ||
def test(var) do | ||
%{c: c, d: d, a: 1, b: 2} = var | ||
var | ||
end | ||
""" | ||
end | ||
|
||
describe "map update literal" do | ||
test_formatting "has its keys reformatted into shorthands", | ||
original: """ | ||
def test(var) do | ||
%{var | a: a, b: b, c: 1} | ||
end | ||
""", | ||
formatted: """ | ||
def test(var) do | ||
%{var | a, b, c: 1} | ||
end | ||
""" | ||
|
||
test_formatting "has its keys moved to the front when reformatting to shorthand", | ||
original: """ | ||
def test(var) do | ||
%{var | a: 1, b: 2, c: c, d: d} | ||
end | ||
""", | ||
formatted: """ | ||
def test(var) do | ||
%{var | c, d, a: 1, b: 2} | ||
end | ||
""", | ||
reverted: """ | ||
def test(var) do | ||
%{var | c: c, d: d, a: 1, b: 2} | ||
end | ||
""" | ||
end | ||
|
||
describe "struct literals" do | ||
test_formatting "has its keys reformatted into shorthands", | ||
original: """ | ||
def test(var) do | ||
%A.B.StructName{a: a, b: b, c: 1} = var | ||
var | ||
end | ||
""", | ||
formatted: """ | ||
def test(var) do | ||
%A.B.StructName{a, b, c: 1} = var | ||
var | ||
end | ||
""" | ||
|
||
test_formatting "has its keys moved to the front when reformatting to shorthand", | ||
original: """ | ||
def test(var) do | ||
%A.B.StructName{a: 1, b: 2, c: c, d: d} = var | ||
var | ||
end | ||
""", | ||
formatted: """ | ||
def test(var) do | ||
%A.B.StructName{c, d, a: 1, b: 2} = var | ||
var | ||
end | ||
""", | ||
reverted: """ | ||
def test(var) do | ||
%A.B.StructName{c: c, d: d, a: 1, b: 2} = var | ||
var | ||
end | ||
""" | ||
end | ||
|
||
describe "struct update literal" do | ||
test_formatting "has its keys reformatted into shorthands", | ||
original: """ | ||
def test(var) do | ||
%A.B.StructName{var | a: a, b: b, c: 1} | ||
end | ||
""", | ||
formatted: """ | ||
def test(var) do | ||
%A.B.StructName{var | a, b, c: 1} | ||
end | ||
""" | ||
|
||
test_formatting "has its keys moved to the front when reformatting to shorthand", | ||
original: """ | ||
def test(var) do | ||
%A.B.StructName{var | a: 1, b: 2, c: c, d: d} | ||
end | ||
""", | ||
formatted: """ | ||
def test(var) do | ||
%A.B.StructName{var | c, d, a: 1, b: 2} | ||
end | ||
""", | ||
reverted: """ | ||
def test(var) do | ||
%A.B.StructName{var | c: c, d: d, a: 1, b: 2} | ||
end | ||
""" | ||
end | ||
|
||
describe "original code" do | ||
test_formatting "heredoc strings newlines are preserved", | ||
original: ~s''' | ||
def test(var) do | ||
""" | ||
this is | ||
my heredoc | ||
""" | ||
end | ||
''' | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
defmodule Es6MapsTest.Support.FormattingAssertions do | ||
defmacro test_formatting(name, opts) do | ||
original = Keyword.fetch!(opts, :original) | ||
formatted = Keyword.get(opts, :formatted, original) | ||
reverted = Keyword.get(opts, :reverted, original) | ||
|
||
quote location: :keep do | ||
test unquote(name) do | ||
{:ok, path} = Briefly.create() | ||
File.write!(path, unquote(original)) | ||
|
||
Mix.Tasks.Es6Maps.Format.run([path]) | ||
assert File.read!(path) == String.trim(unquote(formatted)) | ||
|
||
Mix.Tasks.Es6Maps.Format.run([path, "--revert"]) | ||
assert File.read!(path) == String.trim(unquote(reverted || original)) | ||
end | ||
end | ||
end | ||
end |