Compare commits

...
13 Commits
Author SHA1 Message Date
José Valim b63f8f541e Release v1.13.3 2022-02-09 16:26:17 +01:00
José Valim babbae7e47 Handle more unicode sequences in split_at, closes #11617 2022-02-09 15:53:39 +01:00
José Valim 0f0d4c0f6e Track all stale modules from config/lock as exports too 2022-02-04 16:50:37 +01:00
Steve Hall 5ee492af55 Fix coverage threshold ignored for Total line in Summary (#11571)
An incorrect variable is being passed to the `display/2` function on line 353 for the Total line in the coverage summary report. The result is that that Total line will always display in red no matter what the threshold is set to.
2022-02-03 13:22:32 +01:00
José Valim 25e480f3d0 Do not break signatures over multiple lines 2022-02-01 10:04:30 +01:00
José Valim 2dc6773d6c Fix recursion on guards with map fields, closes #11602 2022-01-31 14:58:39 +01:00
José Valim ff103b19b9 Fix indentation of options in mix format 2022-01-24 16:48:28 +01:00
Matt McCoy d99a16256b Supply file and line to formatter plugins (#11591)
Allow formatter plugins to print errors with accurate file and
line information. The line is only provided when a sigil is being
formatted.
2022-01-24 08:37:17 +01:00
José Valim 1601b309b8 Check for fun arity in formatter 2022-01-24 08:37:17 +01:00
Jonatan Männchen 421a77d508 Embedded Elixir Expressions in Formatter Plugins (#11587)
Support Sigils inside embedded expressions in files formatted
by a formatter plugin.
2022-01-22 09:36:02 +01:00
felipe stival cc9e9b29a7 Fix duplicate bindings causing weird behaviour (#11584)
Before this fix, evaluating `b = a` with assignments of: `a: 1, a: 2, c:
3` would eval AST equivalent to `^c = a`. This happened due to binding
normalization generating version numbers larger than the total number of
bindings. Later in the pipeline, the number of bindings is used to
compute the next version number, which would then conflict with an
existing binding.

This commit fixes that by not increasing the version number when
a repeated binding is normalized.
2022-01-19 19:38:23 +01:00
Eksperimental 67f7c6ba5b Fix bug with edge-case when converting to algebra quoted Elixir alias (#11572)
The following code would crash:

    iex> Code.quoted_to_algebra(Elixir)
    ** (MatchError) no match of right hand side value: "Elixir"
        (elixir 1.13.1) lib/code/normalizer.ex:254: Code.Normalizer.normalize_literal/3
        (elixir 1.13.1) lib/code.ex:1107: Code.quoted_to_algebra/2

Therefore, this as well:

    iex> Macro.to_string(Elixir)
    ** (MatchError) no match of right hand side value: "Elixir"
        (elixir 1.13.1) lib/code/normalizer.ex:254: Code.Normalizer.normalize_literal/3
        (elixir 1.13.1) lib/code.ex:1107: Code.quoted_to_algebra/2
        (elixir 1.13.1) lib/macro.ex:948: Macro.to_string/1
2022-01-16 09:28:25 +01:00
José Valim c99a17b8cd Optimize Map.new/2 2022-01-13 15:28:01 +01:00
25 changed files with 282 additions and 72 deletions
+24
View File
@@ -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
View File
@@ -1 +1 @@
1.13.2
1.13.3
+1 -1
View File
@@ -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
View File
@@ -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
+11 -2
View File
@@ -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]
+9 -5
View File
@@ -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
View File
@@ -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()
+11 -7
View File
@@ -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
+8 -4
View File
@@ -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
+14 -7
View File
@@ -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} =
+3 -1
View File
@@ -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"/
+3
View File
@@ -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
+2
View File
@@ -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
+5
View File
@@ -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"
+32 -25
View File
@@ -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
+14 -11
View File
@@ -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
+1 -1
View File
@@ -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)
+6 -1
View File
@@ -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