Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b63f8f541e | ||
|
|
babbae7e47 | ||
|
|
0f0d4c0f6e | ||
|
|
5ee492af55 | ||
|
|
25e480f3d0 | ||
|
|
2dc6773d6c | ||
|
|
ff103b19b9 | ||
|
|
d99a16256b | ||
|
|
1601b309b8 | ||
|
|
421a77d508 | ||
|
|
cc9e9b29a7 | ||
|
|
67f7c6ba5b | ||
|
|
c99a17b8cd |
@@ -128,6 +128,30 @@ Now any application can use your formatter as follows:
|
||||
|
||||
Finally, the `Code` module has also been augmented with two functions: `Code.string_to_quoted_with_comments/2` and `Code.quoted_to_algebra/2`. Those functions allow someone to retrieve the Elixir AST with their original source code comments, and then convert this AST to formatted code. In other words, those functions provide a wrapper around the Elixir Code Formatter, supporting developers who wish to create tools that directly manipulate and custom format Elixir source code.
|
||||
|
||||
## v1.13.3 (2022-02-09)
|
||||
|
||||
### 1. Enhancements
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix format] Supply file and line to formatter plugins
|
||||
* [mix format] Support embedded Elixir expressions inside formatter plugins
|
||||
|
||||
### 2. Bug fixes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Code] Fix duplicate bindings causing errors during evaluation
|
||||
* [Kernel] Make sure signatures stored in the Documentation chunk does not contain newlines
|
||||
* [Kernel] Fix infinite loop when compiling guards with recursive map access
|
||||
* [Macro] Fix error on `Macro.to_string/1` when the plain alias `Elixir` is given
|
||||
* [String] Fix error for certain codepoint combinations in `String.split_at/2`
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix compile] Recompile project files when exports from dependencies change
|
||||
* [mix test] Fix total coverage always showing in red even when above the threshold
|
||||
|
||||
## v1.13.2 (2022-01-13)
|
||||
|
||||
### 1. Enhancements
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
ELIXIR_VERSION=1.13.2
|
||||
ELIXIR_VERSION=1.13.3
|
||||
|
||||
if [ $# -eq 0 ] || { [ $# -eq 1 ] && { [ "$1" = "--help" ] || [ "$1" = "-h" ]; }; }; then
|
||||
cat <<USAGE >&2
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
@if defined ELIXIR_CLI_ECHO (@echo on) else (@echo off)
|
||||
|
||||
set ELIXIR_VERSION=1.13.2
|
||||
set ELIXIR_VERSION=1.13.3
|
||||
|
||||
setlocal enabledelayedexpansion
|
||||
if ""%1""=="""" if ""%2""=="""" goto documentation
|
||||
|
||||
@@ -177,6 +177,7 @@ defmodule Code.Formatter do
|
||||
defp state(comments, opts) do
|
||||
force_do_end_blocks = Keyword.get(opts, :force_do_end_blocks, false)
|
||||
locals_without_parens = Keyword.get(opts, :locals_without_parens, [])
|
||||
file = Keyword.get(opts, :file, nil)
|
||||
sigils = Keyword.get(opts, :sigils, [])
|
||||
|
||||
sigils =
|
||||
@@ -199,7 +200,8 @@ defmodule Code.Formatter do
|
||||
operand_nesting: 2,
|
||||
skip_eol: false,
|
||||
comments: comments,
|
||||
sigils: sigils
|
||||
sigils: sigils,
|
||||
file: file
|
||||
}
|
||||
end
|
||||
|
||||
@@ -1320,7 +1322,14 @@ defmodule Code.Formatter do
|
||||
entries =
|
||||
case state.sigils do
|
||||
%{^name => callback} ->
|
||||
case callback.(hd(entries), sigil: List.to_atom([name]), modifiers: modifiers) do
|
||||
metadata = [
|
||||
file: state.file,
|
||||
line: meta[:line],
|
||||
sigil: List.to_atom([name]),
|
||||
modifiers: modifiers
|
||||
]
|
||||
|
||||
case callback.(hd(entries), metadata) do
|
||||
binary when is_binary(binary) ->
|
||||
[binary]
|
||||
|
||||
|
||||
@@ -251,12 +251,16 @@ defmodule Code.Normalizer do
|
||||
|
||||
if is_atom(literal) and Code.Identifier.classify(literal) == :alias and
|
||||
is_nil(meta[:delimiter]) do
|
||||
"Elixir." <> segments = Atom.to_string(literal)
|
||||
|
||||
segments =
|
||||
segments
|
||||
|> String.split(".")
|
||||
|> Enum.map(&String.to_atom/1)
|
||||
case Atom.to_string(literal) do
|
||||
"Elixir" ->
|
||||
[:"Elixir"]
|
||||
|
||||
"Elixir." <> segments ->
|
||||
segments
|
||||
|> String.split(".")
|
||||
|> Enum.map(&String.to_atom/1)
|
||||
end
|
||||
|
||||
{:__aliases__, meta, segments}
|
||||
else
|
||||
|
||||
+18
-1
@@ -214,7 +214,24 @@ defmodule Map do
|
||||
|
||||
"""
|
||||
@spec new(Enumerable.t(), (term -> {key, value})) :: map
|
||||
def new(enumerable, transform) when is_function(transform, 1) do
|
||||
def new(enumerable, transform)
|
||||
def new(%_{} = enumerable, transform), do: new_from_enum(enumerable, transform)
|
||||
def new(%{} = map, transform), do: new_from_map(map, transform)
|
||||
def new(enumerable, transform), do: new_from_enum(enumerable, transform)
|
||||
|
||||
defp new_from_map(map, transform) when is_function(transform, 1) do
|
||||
iter = :maps.iterator(map)
|
||||
next = :maps.next(iter)
|
||||
:maps.from_list(do_map(next, transform))
|
||||
end
|
||||
|
||||
defp do_map(:none, _fun), do: []
|
||||
|
||||
defp do_map({key, value, iter}, transform) do
|
||||
[transform.({key, value}) | do_map(:maps.next(iter), transform)]
|
||||
end
|
||||
|
||||
defp new_from_enum(enumerable, transform) when is_function(transform, 1) do
|
||||
enumerable
|
||||
|> Enum.map(transform)
|
||||
|> :maps.from_list()
|
||||
|
||||
@@ -403,10 +403,8 @@ defmodule Module.Types.Pattern do
|
||||
# bar, {:ok, baz}
|
||||
# bar, {:ok, bat}
|
||||
|
||||
expanded_args =
|
||||
args
|
||||
|> Enum.map(&flatten_union(&1, context))
|
||||
|> cartesian_product()
|
||||
flatten_args = Enum.map(args, &flatten_union(&1, context))
|
||||
cartesian_args = cartesian_product(flatten_args)
|
||||
|
||||
# Remove clauses that do not match the expected type
|
||||
# Ignore type variables in parameters by changing them to dynamic
|
||||
@@ -425,11 +423,11 @@ defmodule Module.Types.Pattern do
|
||||
# the type contexts from unifying argument and parameter to
|
||||
# infer type variables in arguments
|
||||
result =
|
||||
flat_map_ok(expanded_args, fn expanded_args ->
|
||||
flat_map_ok(cartesian_args, fn cartesian_args ->
|
||||
result =
|
||||
Enum.flat_map(clauses, fn {params, return} ->
|
||||
result =
|
||||
map_ok(Enum.zip(expanded_args, params), fn {arg, param} ->
|
||||
map_ok(Enum.zip(cartesian_args, params), fn {arg, param} ->
|
||||
case unify(arg, param, stack, context) do
|
||||
{:ok, _type, context} -> {:ok, context}
|
||||
{:error, reason} -> {:error, reason}
|
||||
@@ -453,7 +451,13 @@ defmodule Module.Types.Pattern do
|
||||
{:ok, returns_contexts} ->
|
||||
{success_returns, contexts} = Enum.unzip(returns_contexts)
|
||||
contexts = Enum.concat(contexts)
|
||||
indexes = Enum.uniq(Enum.flat_map(args, &collect_var_indexes_from_type/1))
|
||||
|
||||
indexes =
|
||||
for types <- flatten_args,
|
||||
type <- types,
|
||||
index <- collect_var_indexes_from_type(type),
|
||||
do: index,
|
||||
uniq: true
|
||||
|
||||
# Build unions from collected type contexts to unify with
|
||||
# type variables from arguments
|
||||
|
||||
@@ -2723,12 +2723,16 @@ defmodule String do
|
||||
graphemes_and_length: 1,
|
||||
reverse_characters_to_binary: 1}
|
||||
|
||||
defp byte_size_remaining_at(binary, 0) do
|
||||
byte_size(binary)
|
||||
defp byte_size_unicode(binary) when is_binary(binary), do: byte_size(binary)
|
||||
defp byte_size_unicode([head]), do: byte_size_unicode(head)
|
||||
defp byte_size_unicode([head | tail]), do: byte_size_unicode(head) + byte_size_unicode(tail)
|
||||
|
||||
defp byte_size_remaining_at(unicode, 0) do
|
||||
byte_size_unicode(unicode)
|
||||
end
|
||||
|
||||
defp byte_size_remaining_at(binary, n) do
|
||||
case :unicode_util.gc(binary) do
|
||||
defp byte_size_remaining_at(unicode, n) do
|
||||
case :unicode_util.gc(unicode) do
|
||||
[_] -> 0
|
||||
[_ | rest] -> byte_size_remaining_at(rest, n - 1)
|
||||
[] -> 0
|
||||
|
||||
@@ -254,7 +254,7 @@ eval_quoted(Tree, Binding, #{line := Line} = E) ->
|
||||
eval_forms(Tree, Binding, Opts) when is_list(Opts) ->
|
||||
eval_forms(Tree, Binding, env_for_eval(Opts));
|
||||
eval_forms(Tree, RawBinding, OrigE) ->
|
||||
{Vars, Binding} = normalize_binding(RawBinding, [], [], 0),
|
||||
{Vars, Binding} = normalize_binding(RawBinding, #{}, [], 0),
|
||||
E = elixir_env:with_vars(OrigE, Vars),
|
||||
{_, S} = elixir_env:env_to_erl(E),
|
||||
{Erl, NewErlS, NewExS, NewE} = quoted_to_erl(Tree, E, S),
|
||||
@@ -275,12 +275,19 @@ eval_forms(Tree, RawBinding, OrigE) ->
|
||||
{Value, elixir_erl_var:dump_binding(NewBinding, NewExS, NewErlS), NewE}
|
||||
end.
|
||||
|
||||
normalize_binding([{Key, Value} | Binding], Vars, Acc, I) when is_atom(Key) ->
|
||||
normalize_binding(Binding, [{{Key, nil}, I} | Vars], [{{Key, nil}, Value} | Acc], I + 1);
|
||||
normalize_binding([{Pair, Value} | Binding], Vars, Acc, I) ->
|
||||
normalize_binding(Binding, [{Pair, I} | Vars], [{Pair, Value} | Acc], I + 1);
|
||||
normalize_binding([], Vars, Acc, _I) ->
|
||||
{maps:from_list(Vars), Acc}.
|
||||
normalize_binding([Binding | NextBindings], VarsMap, Normalized, Counter) ->
|
||||
{Pair, Value} = normalize_pair(Binding),
|
||||
case VarsMap of
|
||||
#{Pair := _} ->
|
||||
normalize_binding(NextBindings, VarsMap, [{Pair, Value} | Normalized], Counter);
|
||||
#{} ->
|
||||
normalize_binding(NextBindings, VarsMap#{Pair => Counter}, [{Pair, Value} | Normalized], Counter + 1)
|
||||
end;
|
||||
normalize_binding([], VarsMap, Normalized, _Counter) ->
|
||||
{VarsMap, Normalized}.
|
||||
|
||||
normalize_pair({Key, Value}) when is_atom(Key) -> {{Key, nil}, Value};
|
||||
normalize_pair({Pair, Value}) -> {Pair, Value}.
|
||||
|
||||
recur_eval([Expr | Exprs], Binding, Env) ->
|
||||
{value, Value, NewBinding} =
|
||||
|
||||
@@ -552,7 +552,9 @@ signature_to_binary(Module, '__struct__', []) ->
|
||||
<<"%", ('Elixir.Kernel':inspect(Module))/binary, "{}">>;
|
||||
|
||||
signature_to_binary(_, Name, Signature) ->
|
||||
'Elixir.Macro':to_string({Name, [{closing, []}], Signature}).
|
||||
Quoted = {Name, [{closing, []}], Signature},
|
||||
Doc = 'Elixir.Inspect.Algebra':format('Elixir.Code':quoted_to_algebra(Quoted), infinity),
|
||||
'Elixir.IO':iodata_to_binary(Doc).
|
||||
|
||||
checker_chunk(#{definitions := Definitions, deprecated := Deprecated, is_behaviour := IsBehaviour}) ->
|
||||
DeprecatedMap = maps:from_list(Deprecated),
|
||||
|
||||
@@ -126,7 +126,7 @@ defmodule Code.Formatter.GeneralTest do
|
||||
"""
|
||||
|
||||
formatter = fn content, opts ->
|
||||
assert opts == [sigil: :W, modifiers: []]
|
||||
assert opts == [file: nil, line: 1, sigil: :W, modifiers: []]
|
||||
content |> String.split(~r/ +/) |> Enum.join(" ")
|
||||
end
|
||||
|
||||
@@ -141,7 +141,7 @@ defmodule Code.Formatter.GeneralTest do
|
||||
"""
|
||||
|
||||
formatter = fn content, opts ->
|
||||
assert opts == [sigil: :W, modifiers: 'abc']
|
||||
assert opts == [file: nil, line: 1, sigil: :W, modifiers: 'abc']
|
||||
content |> String.split(~r/ +/) |> Enum.join(" ")
|
||||
end
|
||||
|
||||
@@ -162,7 +162,7 @@ defmodule Code.Formatter.GeneralTest do
|
||||
"""
|
||||
|
||||
formatter = fn content, opts ->
|
||||
assert opts == [sigil: :W, modifiers: []]
|
||||
assert opts == [file: nil, line: 1, sigil: :W, modifiers: []]
|
||||
content |> String.split(~r/ +/) |> Enum.join(" ")
|
||||
end
|
||||
|
||||
@@ -189,7 +189,7 @@ defmodule Code.Formatter.GeneralTest do
|
||||
"""
|
||||
|
||||
formatter = fn content, opts ->
|
||||
assert opts == [sigil: :W, modifiers: 'abc']
|
||||
assert opts == [file: nil, line: 2, sigil: :W, modifiers: 'abc']
|
||||
content |> String.split(~r/ +/) |> Enum.join("\n")
|
||||
end
|
||||
|
||||
|
||||
@@ -115,6 +115,7 @@ defmodule Code.Normalizer.FormatterASTTest do
|
||||
|
||||
test "does not reformat aliases" do
|
||||
assert_same ~S[:"Elixir.String"]
|
||||
assert_same ~S[:"Elixir"]
|
||||
end
|
||||
|
||||
test "quoted operators" do
|
||||
|
||||
@@ -620,6 +620,7 @@ defmodule Code.Normalizer.QuotedASTTest do
|
||||
assert quoted_to_string({:__block__, [], [:"a\nb\tc"]}, escape: false) == ~s/:"a\nb\tc"/
|
||||
assert quoted_to_string({:__block__, [], [:"a\nb\tc"]}) == ~S/:"a\nb\tc"/
|
||||
|
||||
assert quoted_to_string(quote(do: :"Elixir")) == "Elixir"
|
||||
assert quoted_to_string(quote(do: :"Elixir.Foo")) == "Foo"
|
||||
assert quoted_to_string(quote(do: :"Elixir.Foo.Bar")) == "Foo.Bar"
|
||||
assert quoted_to_string(quote(do: :"Elixir.foobar")) == ~S/:"Elixir.foobar"/
|
||||
|
||||
@@ -52,6 +52,9 @@ defmodule CodeTest do
|
||||
# The order of which values win is not guaranteed, but it should evaluate successfully.
|
||||
assert Code.eval_string("b = String.Chars.to_string(a)", a: 0, a: 1) ==
|
||||
{"1", [{:b, "1"}, {:a, 1}]}
|
||||
|
||||
assert Code.eval_string("b = String.Chars.to_string(a)", a: 0, a: 1, c: 2) ==
|
||||
{"1", [{:c, 2}, {:b, "1"}, {:a, 1}]}
|
||||
end
|
||||
|
||||
test "with many options" do
|
||||
|
||||
@@ -119,6 +119,14 @@ defmodule Kernel.DocsTest do
|
||||
|
||||
def two_good_names(first, :ok), do: first
|
||||
def two_good_names(second, :error), do: second
|
||||
|
||||
def really_long_signature(
|
||||
really_long_var_named_one,
|
||||
really_long_var_named_two,
|
||||
really_long_var_named_three
|
||||
) do
|
||||
{really_long_var_named_one, really_long_var_named_two, really_long_var_named_three}
|
||||
end
|
||||
end
|
||||
)
|
||||
|
||||
@@ -128,6 +136,7 @@ defmodule Kernel.DocsTest do
|
||||
assert [
|
||||
arg_names,
|
||||
only_underscore,
|
||||
really_long_signature,
|
||||
two_good_names,
|
||||
with_defaults,
|
||||
with_map_and_default,
|
||||
@@ -141,6 +150,12 @@ defmodule Kernel.DocsTest do
|
||||
# only_underscore/1
|
||||
assert {{:only_underscore, 1}, ["only_underscore(_)"]} = only_underscore
|
||||
|
||||
# really_long_signature/3
|
||||
assert {{:really_long_signature, 3},
|
||||
[
|
||||
"really_long_signature(really_long_var_named_one, really_long_var_named_two, really_long_var_named_three)"
|
||||
]} = really_long_signature
|
||||
|
||||
# two_good_names/2
|
||||
assert {{:two_good_names, 2}, ["two_good_names(first, atom)"]} = two_good_names
|
||||
|
||||
|
||||
@@ -521,6 +521,9 @@ defmodule Kernel.QuoteTest.AliasHygieneTest do
|
||||
assert {:__aliases__, [alias: false], [:Foo, :Bar]} = quote(do: Foo.Bar)
|
||||
assert {:__aliases__, [alias: false], [:Dict, :Bar]} = quote(do: Dict.Bar)
|
||||
assert {:__aliases__, [alias: Dict.Bar], [:SuperDict, :Bar]} = quote(do: SuperDict.Bar)
|
||||
|
||||
# Edge-case
|
||||
assert {:__aliases__, _, [Elixir]} = quote(do: Elixir)
|
||||
end
|
||||
|
||||
test "expand aliases" do
|
||||
|
||||
@@ -326,6 +326,8 @@ defmodule MacroTest do
|
||||
end
|
||||
|
||||
test "aliases call" do
|
||||
assert macro_to_string(quote(do: Elixir)) == "Elixir"
|
||||
assert macro_to_string(quote(do: Foo)) == "Foo"
|
||||
assert macro_to_string(quote(do: Foo.Bar.baz(1, 2, 3))) == "Foo.Bar.baz(1, 2, 3)"
|
||||
assert macro_to_string(quote(do: Foo.Bar.baz([1, 2, 3]))) == "Foo.Bar.baz([1, 2, 3])"
|
||||
assert macro_to_string(quote(do: Foo.bar(<<>>, []))) == "Foo.bar(<<>>, [])"
|
||||
|
||||
@@ -687,5 +687,18 @@ defmodule Module.Types.TypesTest do
|
||||
end
|
||||
) == :none
|
||||
end
|
||||
|
||||
test "no-recursion on guards with map fields" do
|
||||
assert warning(
|
||||
[assigns],
|
||||
(
|
||||
variable_enum = assigns.variable_enum
|
||||
|
||||
case true do
|
||||
_ when variable_enum != nil -> assigns.variable_enum
|
||||
end
|
||||
)
|
||||
) == :none
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -131,6 +131,11 @@ defmodule StringTest do
|
||||
end
|
||||
end
|
||||
|
||||
test "split_at/2 with invalid guard" do
|
||||
assert String.split_at(<<?a, 195, 10, ?a>>, 2) == {<<?a, 195>>, <<10, ?a>>}
|
||||
assert String.split_at(<<107, 205, 135, 184>>, 1) == {<<107, 205, 135>>, <<184>>}
|
||||
end
|
||||
|
||||
test "upcase/1" do
|
||||
assert String.upcase("123 abcd 456 efg hij ( %$#) kl mnop @ qrst = -_ uvwxyz") ==
|
||||
"123 ABCD 456 EFG HIJ ( %$#) KL MNOP @ QRST = -_ UVWXYZ"
|
||||
|
||||
@@ -2,6 +2,7 @@ defmodule Mix.Compilers.Elixir do
|
||||
@moduledoc false
|
||||
|
||||
@manifest_vsn 13
|
||||
@checkpoint_vsn 2
|
||||
|
||||
import Record
|
||||
|
||||
@@ -102,7 +103,7 @@ defmodule Mix.Compilers.Elixir do
|
||||
{false, stale, old_lock, old_config}
|
||||
end
|
||||
|
||||
{stale_local_mods, stale_local_exports, all_local_exports} =
|
||||
{stale_modules, stale_exports, all_local_exports} =
|
||||
stale_local_deps(manifest, stale, modified, all_local_exports)
|
||||
|
||||
prev_paths = for source(source: source) <- all_sources, do: source
|
||||
@@ -120,8 +121,8 @@ defmodule Mix.Compilers.Elixir do
|
||||
all_modules,
|
||||
all_sources,
|
||||
removed,
|
||||
stale_local_mods,
|
||||
Map.merge(stale_local_exports, removed_modules),
|
||||
Map.merge(stale_modules, removed_modules),
|
||||
Map.merge(stale_exports, removed_modules),
|
||||
dest
|
||||
)
|
||||
end
|
||||
@@ -180,7 +181,7 @@ defmodule Mix.Compilers.Elixir do
|
||||
delete_compiler_info()
|
||||
end
|
||||
else
|
||||
# We need to return ok if deps_changed? or stale_local_mods changed,
|
||||
# We need to return ok if deps_changed? or stale_modules changed,
|
||||
# even if no code was compiled, because we need to propagate the changed
|
||||
# status to compile.protocols. This will be the case whenever:
|
||||
#
|
||||
@@ -193,7 +194,7 @@ defmodule Mix.Compilers.Elixir do
|
||||
# will only compute the diff with current protocols. In fact, there is no
|
||||
# need to reconsolidate if an Erlang file changes and it doesn't trigger
|
||||
# any other change, but the diff check should be reasonably fast anyway.
|
||||
status = if removed != [] or deps_changed? or stale_local_mods != %{}, do: :ok, else: :noop
|
||||
status = if removed != [] or deps_changed? or stale_modules != %{}, do: :ok, else: :noop
|
||||
|
||||
# If nothing changed but there is one more recent mtime, bump the manifest
|
||||
if status != :noop or Enum.any?(Map.values(sources_stats), &(elem(&1, 0) > modified)) do
|
||||
@@ -283,8 +284,8 @@ defmodule Mix.Compilers.Elixir do
|
||||
all_modules,
|
||||
all_sources,
|
||||
removed,
|
||||
stale_local_mods,
|
||||
stale_local_exports,
|
||||
stale_modules,
|
||||
stale_exports,
|
||||
dest
|
||||
) do
|
||||
# TODO: Use :maps.from_keys/2 on Erlang/OTP 24+
|
||||
@@ -294,13 +295,17 @@ defmodule Mix.Compilers.Elixir do
|
||||
into: %{},
|
||||
do: {module, []}
|
||||
|
||||
{checkpoint_stale, checkpoint_modules} = parse_checkpoint(manifest)
|
||||
modules_to_recompile = Map.merge(checkpoint_modules, modules_to_recompile)
|
||||
stale_local_mods = Map.merge(checkpoint_stale, stale_local_mods)
|
||||
{checkpoint_stale_modules, checkpoint_stale_exports, checkpoint_modules} =
|
||||
parse_checkpoint(manifest)
|
||||
|
||||
if map_size(stale_local_mods) != map_size(checkpoint_stale) or
|
||||
modules_to_recompile = Map.merge(checkpoint_modules, modules_to_recompile)
|
||||
stale_modules = Map.merge(checkpoint_stale_modules, stale_modules)
|
||||
stale_exports = Map.merge(checkpoint_stale_exports, stale_exports)
|
||||
|
||||
if map_size(stale_modules) != map_size(checkpoint_stale_modules) or
|
||||
map_size(stale_exports) != map_size(checkpoint_stale_exports) or
|
||||
map_size(modules_to_recompile) != map_size(checkpoint_modules) do
|
||||
write_checkpoint(manifest, stale_local_mods, modules_to_recompile)
|
||||
write_checkpoint(manifest, stale_modules, stale_exports, modules_to_recompile)
|
||||
end
|
||||
|
||||
sources_stats =
|
||||
@@ -332,8 +337,8 @@ defmodule Mix.Compilers.Elixir do
|
||||
all_modules,
|
||||
all_sources,
|
||||
removed ++ changed,
|
||||
stale_local_mods,
|
||||
stale_local_exports,
|
||||
stale_modules,
|
||||
stale_exports,
|
||||
dest
|
||||
)
|
||||
|
||||
@@ -654,16 +659,16 @@ defmodule Mix.Compilers.Elixir do
|
||||
# files that have changed. Then it recursively figures out
|
||||
# all the files that changed (via the module dependencies) and
|
||||
# return the non-changed entries and the removed sources.
|
||||
defp update_stale_entries(modules, _sources, [], stale_mods, stale_exports, _compile_path)
|
||||
when stale_mods == %{} and stale_exports == %{} do
|
||||
defp update_stale_entries(modules, _sources, [], stale_modules, stale_exports, _compile_path)
|
||||
when stale_modules == %{} and stale_exports == %{} do
|
||||
{modules, %{}, []}
|
||||
end
|
||||
|
||||
defp update_stale_entries(modules, sources, changed, stale_mods, stale_exports, compile_path) do
|
||||
defp update_stale_entries(modules, sources, changed, stale_modules, stale_exports, compile_path) do
|
||||
# TODO: Use :maps.from_keys/2 on Erlang/OTP 24+
|
||||
changed = Enum.into(changed, %{}, &{&1, []})
|
||||
reducer = &remove_stale_entry(&1, &2, sources, stale_exports, compile_path)
|
||||
remove_stale_entries(modules, %{}, changed, stale_mods, reducer)
|
||||
remove_stale_entries(modules, %{}, changed, stale_modules, reducer)
|
||||
end
|
||||
|
||||
defp remove_stale_entries(modules, exports, old_changed, old_stale, reducer) do
|
||||
@@ -720,6 +725,8 @@ defmodule Mix.Compilers.Elixir do
|
||||
defp stale_local_deps(manifest, stale_modules, modified, old_exports) do
|
||||
base = Path.basename(manifest)
|
||||
|
||||
# The stale modules so far will become both stale_modules and stale_exports,
|
||||
# as any export from a dependency needs to be recompiled.
|
||||
# TODO: Use :maps.from_keys/2 on Erlang/OTP 24+
|
||||
stale_modules = for module <- stale_modules, do: {module, []}, into: %{}
|
||||
|
||||
@@ -727,7 +734,7 @@ defmodule Mix.Compilers.Elixir do
|
||||
not scm.fetchable?,
|
||||
manifest = Path.join([opts[:build], ".mix", base]),
|
||||
Mix.Utils.last_modified(manifest) > modified,
|
||||
reduce: {stale_modules, %{}, old_exports} do
|
||||
reduce: {stale_modules, stale_modules, old_exports} do
|
||||
{modules, exports, new_exports} ->
|
||||
{_manifest_modules, dep_sources} = read_manifest(manifest)
|
||||
|
||||
@@ -1017,19 +1024,19 @@ defmodule Mix.Compilers.Elixir do
|
||||
(manifest <> ".checkpoint") |> File.read!() |> :erlang.binary_to_term()
|
||||
rescue
|
||||
_ ->
|
||||
{%{}, %{}}
|
||||
{%{}, %{}, %{}}
|
||||
else
|
||||
{@manifest_vsn, stale, recompile_modules} ->
|
||||
{stale, recompile_modules}
|
||||
{@checkpoint_vsn, stale_modules, stale_exports, recompile_modules} ->
|
||||
{stale_modules, stale_exports, recompile_modules}
|
||||
|
||||
_ ->
|
||||
{%{}, %{}}
|
||||
{%{}, %{}, %{}}
|
||||
end
|
||||
end
|
||||
|
||||
defp write_checkpoint(manifest, stale, recompile_modules) do
|
||||
defp write_checkpoint(manifest, stale_modules, stale_exports, recompile_modules) do
|
||||
File.mkdir_p!(Path.dirname(manifest))
|
||||
term = {@manifest_vsn, stale, recompile_modules}
|
||||
term = {@checkpoint_vsn, stale_modules, stale_exports, recompile_modules}
|
||||
checkpoint_data = :erlang.term_to_binary(term, [:compressed])
|
||||
File.write!(manifest <> ".checkpoint", checkpoint_data)
|
||||
end
|
||||
|
||||
@@ -104,11 +104,11 @@ defmodule Mix.Tasks.Format do
|
||||
|
||||
The `opts` passed to `format/2` contains all the formatting options and either:
|
||||
|
||||
* `:sigil` (atom) - the sigil being formatted, e.g. `:M`.
|
||||
* `:sigil` (atom) - the sigil being formatted, e.g. `:M`.
|
||||
|
||||
* `:modifiers` (charlist) - list of sigil modifiers.
|
||||
* `:modifiers` (charlist) - list of sigil modifiers.
|
||||
|
||||
* `:extension` (string) - the extension of the file being formatted, e.g. `".md"`.
|
||||
* `:extension` (string) - the extension of the file being formatted, e.g. `".md"`.
|
||||
|
||||
Now any application can use your formatter as follows:
|
||||
|
||||
@@ -288,7 +288,15 @@ defmodule Mix.Tasks.Format do
|
||||
end
|
||||
end
|
||||
|
||||
formatter_opts = Keyword.put(formatter_opts, :plugins, plugins)
|
||||
sigils =
|
||||
for plugin <- plugins,
|
||||
sigil <- find_sigils_from_plugins(plugin, formatter_opts),
|
||||
do: {sigil, &plugin.format(&1, &2 ++ formatter_opts)}
|
||||
|
||||
formatter_opts =
|
||||
formatter_opts
|
||||
|> Keyword.put(:plugins, plugins)
|
||||
|> Keyword.put(:sigils, sigils)
|
||||
|
||||
if deps == [] and subs == [] do
|
||||
{{formatter_opts, []}, sources}
|
||||
@@ -496,7 +504,7 @@ defmodule Mix.Tasks.Format do
|
||||
|
||||
cond do
|
||||
plugin = find_plugin_for_extension(formatter_opts, ext) ->
|
||||
&plugin.format(&1, [extension: ext] ++ formatter_opts)
|
||||
&plugin.format(&1, [extension: ext, file: file] ++ formatter_opts)
|
||||
|
||||
ext in ~w(.ex .exs) ->
|
||||
&elixir_format(&1, [file: file] ++ formatter_opts)
|
||||
@@ -537,12 +545,7 @@ defmodule Mix.Tasks.Format do
|
||||
defp stdin_or_wildcard(path), do: path |> Path.expand() |> Path.wildcard(match_dot: true)
|
||||
|
||||
defp elixir_format(content, formatter_opts) do
|
||||
sigils =
|
||||
for plugin <- Keyword.fetch!(formatter_opts, :plugins),
|
||||
sigil <- find_sigils_from_plugins(plugin, formatter_opts),
|
||||
do: {sigil, &plugin.format(&1, &2 ++ formatter_opts)}
|
||||
|
||||
IO.iodata_to_binary([Code.format_string!(content, [sigils: sigils] ++ formatter_opts), ?\n])
|
||||
IO.iodata_to_binary([Code.format_string!(content, formatter_opts), ?\n])
|
||||
end
|
||||
|
||||
defp find_sigils_from_plugins(plugin, formatter_opts) do
|
||||
|
||||
@@ -350,7 +350,7 @@ defmodule Mix.Tasks.Test.Coverage do
|
||||
Mix.shell().info("-----------|--------------------------")
|
||||
results |> Enum.sort() |> Enum.each(&display(&1, threshold))
|
||||
Mix.shell().info("-----------|--------------------------")
|
||||
display({totals, "Total"}, opts)
|
||||
display({totals, "Total"}, threshold)
|
||||
Mix.shell().info("")
|
||||
end
|
||||
|
||||
|
||||
@@ -182,6 +182,11 @@ defmodule Mix.Tasks.Compile.ElixirTest do
|
||||
Process.put({MixTest.Case.Sample, :application}, extra_applications: [:logger])
|
||||
File.mkdir_p!("config")
|
||||
|
||||
File.write!("config/config.exs", """
|
||||
import Config
|
||||
config :logger, :level, :debug
|
||||
""")
|
||||
|
||||
File.write!("lib/a.ex", """
|
||||
defmodule A do
|
||||
_ = Logger.metadata()
|
||||
@@ -234,6 +239,12 @@ defmodule Mix.Tasks.Compile.ElixirTest do
|
||||
refute_received {:mix_shell, :info, ["Compiled lib/b.ex"]}
|
||||
assert File.stat!("_build/dev/lib/sample/.mix/compile.elixir").mtime > @old_time
|
||||
|
||||
# No-op does not recompile
|
||||
File.touch!("_build/dev/lib/sample/.mix/compile.elixir", @old_time)
|
||||
assert recompile.() == {:ok, []}
|
||||
refute_received {:mix_shell, :info, ["Compiled lib/a.ex"]}
|
||||
refute_received {:mix_shell, :info, ["Compiled lib/b.ex"]}
|
||||
|
||||
# Changing self fully recompiles
|
||||
File.write!("config/config.exs", """
|
||||
import Config
|
||||
@@ -263,6 +274,76 @@ defmodule Mix.Tasks.Compile.ElixirTest do
|
||||
Application.delete_env(:sample, :foo, persistent: true)
|
||||
end
|
||||
|
||||
test "recompiles files when config changes export dependencies" do
|
||||
in_fixture("no_mixfile", fn ->
|
||||
Mix.Project.push(MixTest.Case.Sample)
|
||||
Process.put({MixTest.Case.Sample, :application}, extra_applications: [:ex_unit])
|
||||
File.mkdir_p!("config")
|
||||
|
||||
File.write!("lib/a.ex", """
|
||||
defmodule A do
|
||||
def test_struct do
|
||||
%ExUnit.Test{}
|
||||
end
|
||||
end
|
||||
""")
|
||||
|
||||
assert Mix.Tasks.Compile.Elixir.run(["--verbose"]) == {:ok, []}
|
||||
assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]}
|
||||
assert_received {:mix_shell, :info, ["Compiled lib/b.ex"]}
|
||||
|
||||
recompile = fn ->
|
||||
Mix.ProjectStack.pop()
|
||||
Mix.Project.push(MixTest.Case.Sample)
|
||||
Mix.Tasks.Loadconfig.load_compile("config/config.exs")
|
||||
Mix.Tasks.Compile.Elixir.run(["--verbose"])
|
||||
end
|
||||
|
||||
# Adding config recompiles
|
||||
File.write!("config/config.exs", """
|
||||
import Config
|
||||
config :ex_unit, :some, :config
|
||||
""")
|
||||
|
||||
File.touch!("_build/dev/lib/sample/.mix/compile.elixir", @old_time)
|
||||
assert recompile.() == {:ok, []}
|
||||
assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]}
|
||||
refute_received {:mix_shell, :info, ["Compiled lib/b.ex"]}
|
||||
assert File.stat!("_build/dev/lib/sample/.mix/compile.elixir").mtime > @old_time
|
||||
|
||||
# Changing config recompiles
|
||||
File.write!("config/config.exs", """
|
||||
import Config
|
||||
config :ex_unit, :some, :another
|
||||
""")
|
||||
|
||||
File.touch!("_build/dev/lib/sample/.mix/compile.elixir", @old_time)
|
||||
assert recompile.() == {:ok, []}
|
||||
assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]}
|
||||
refute_received {:mix_shell, :info, ["Compiled lib/b.ex"]}
|
||||
assert File.stat!("_build/dev/lib/sample/.mix/compile.elixir").mtime > @old_time
|
||||
|
||||
# Removing config recompiles
|
||||
File.write!("config/config.exs", """
|
||||
import Config
|
||||
""")
|
||||
|
||||
File.touch!("_build/dev/lib/sample/.mix/compile.elixir", @old_time)
|
||||
assert recompile.() == {:ok, []}
|
||||
assert_received {:mix_shell, :info, ["Compiled lib/a.ex"]}
|
||||
refute_received {:mix_shell, :info, ["Compiled lib/b.ex"]}
|
||||
assert File.stat!("_build/dev/lib/sample/.mix/compile.elixir").mtime > @old_time
|
||||
|
||||
# No-op does not recompile
|
||||
File.touch!("_build/dev/lib/sample/.mix/compile.elixir", @old_time)
|
||||
assert recompile.() == {:ok, []}
|
||||
refute_received {:mix_shell, :info, ["Compiled lib/a.ex"]}
|
||||
refute_received {:mix_shell, :info, ["Compiled lib/b.ex"]}
|
||||
end)
|
||||
after
|
||||
Application.delete_env(:ex_unit, :some, persistent: true)
|
||||
end
|
||||
|
||||
test "recompiles files when config changes with crashes" do
|
||||
in_fixture("no_mixfile", fn ->
|
||||
Mix.Project.push(MixTest.Case.Sample)
|
||||
|
||||
@@ -206,6 +206,8 @@ defmodule Mix.Tasks.FormatTest do
|
||||
assert opts[:from_formatter_exs] == :yes
|
||||
assert opts[:sigil] == :W
|
||||
assert opts[:modifiers] == 'abc'
|
||||
assert opts[:line] == 2
|
||||
assert opts[:file] =~ ~r/\/a\.ex$/
|
||||
contents |> String.split(~r/\s/) |> Enum.join("\n")
|
||||
end
|
||||
end
|
||||
@@ -247,12 +249,15 @@ defmodule Mix.Tasks.FormatTest do
|
||||
|
||||
def features(opts) do
|
||||
assert opts[:from_formatter_exs] == :yes
|
||||
[extensions: ~w(.w)]
|
||||
[extensions: ~w(.w), sigils: [:W]]
|
||||
end
|
||||
|
||||
def format(contents, opts) do
|
||||
assert opts[:from_formatter_exs] == :yes
|
||||
assert opts[:extension] == ".w"
|
||||
assert opts[:file] =~ ~r/\/a\.w$/
|
||||
assert [W: sigil_fun] = opts[:sigils]
|
||||
assert is_function(sigil_fun, 2)
|
||||
contents |> String.split(~r/\s/) |> Enum.join("\n")
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user