|
| 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 |
0 commit comments