Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
416bf41a03 | ||
|
|
c758c5e401 | ||
|
|
7b4576f0fc | ||
|
|
9192907e68 | ||
|
|
5533cc4665 | ||
|
|
873dbc16ff | ||
|
|
84c91ef914 | ||
|
|
a977afcd6c | ||
|
|
8ed30ad926 | ||
|
|
8496d52df5 | ||
|
|
977bb418a1 | ||
|
|
735296a4ab | ||
|
|
e34dde79ea | ||
|
|
1eb11374c3 | ||
|
|
9c2ec6a260 | ||
|
|
92cf02321b | ||
|
|
2b5e69aa05 | ||
|
|
a56606d5d6 | ||
|
|
b2329d11a8 | ||
|
|
88b3e864d5 | ||
|
|
efaca6c3e9 | ||
|
|
67aabe0bac | ||
|
|
8506cc68bf | ||
|
|
a1bbc1bc20 | ||
|
|
18dfca21f0 |
+4
-3
@@ -102,11 +102,11 @@ In any case, the previous behaviour can be brought back with the `--verbose` fla
|
||||
|
||||
### mix xref
|
||||
|
||||
Speaking about warnings, Mix v1.3 includes a new task called `xref` that performs cross reference checks in your code. One of such checks is the ability to find calls to modules and functions that do not exist. For example, if in your library code you call `ThisModuleDoesNotExist.foo(1, 2, 3)`, `mix xref --unreachable` will be able to find such code and let you know about it.
|
||||
Speaking about warnings, Mix v1.3 includes a new task called `xref` that performs cross reference checks in your code. One of such checks is the ability to find calls to modules and functions that do not exist. For example, if in your library code you call `ThisModuleDoesNotExist.foo(1, 2, 3)`, `mix xref unreachable` will be able to find such code and let you know about it.
|
||||
|
||||
Since such checks can discover possible bugs in your codebase, a new compiler called `xref` has been added to `Mix.compilers/0`, so they run by default every time you compile your code.
|
||||
|
||||
We have included other modes in `xref`, such as `mix xref --callers Foo`, to find all places in your code that a function from the module `Foo` is called. We hope other tools and text editors can leverage such features to provide useful functionality for their users.
|
||||
We have included other modes in `xref`, such as `mix xref callers Foo`, to find all places in your code that a function from the module `Foo` is called. We hope other tools and text editors can leverage such features to provide useful functionality for their users.
|
||||
|
||||
### Better dependency tracking
|
||||
|
||||
@@ -268,7 +268,7 @@ end
|
||||
|
||||
By restricting hierarchies in favor of named setups, it is straight-forward for the developer to glance at each describe block and know exactly the setup steps involved.
|
||||
|
||||
## v1.3.0-rc.1 (2016-08-06)
|
||||
## v1.3.0 (2016-06-21)
|
||||
|
||||
### 1. Enhancements
|
||||
|
||||
@@ -283,6 +283,7 @@ By restricting hierarchies in favor of named setups, it is straight-forward for
|
||||
* [CLI] Add `--logger-otp-reports BOOL` and `--logger-sasl-reports BOOL` switches
|
||||
* [Compiler] Emit a summary of compilation errors when modules are missing
|
||||
* [Enum] Add `Enum.group_by/3` that allows developers to map on the value being grouped
|
||||
* [Enum] Make list values in maps returned by `Enum.group_by/2` and `Enum.group_by/3` preserve the order of the input enumerable instead of reversing it.
|
||||
* [Enum] Add `Enum.drop_every/2` that drops every `nth`, including the first one
|
||||
* [Exception] Suggest possible functions on `UndefinedFunctionError` for existing modules
|
||||
* [Exception] Warn if unknown fields are given to `raise/2`
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
REBAR ?= "$(CURDIR)/rebar"
|
||||
PREFIX ?= /usr/local
|
||||
DOCS := master
|
||||
CANONICAL := master
|
||||
DOCS := v1.3
|
||||
CANONICAL := stable
|
||||
ELIXIRC := bin/elixirc --verbose --ignore-module-conflict
|
||||
ERLC := erlc -I lib/elixir/include
|
||||
ERL := erl -I lib/elixir/include -noshell -pa lib/elixir/ebin
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ This document simply outlines the release process:
|
||||
|
||||
11. Create a new branch "vMAJOR.MINOR"
|
||||
|
||||
12. Move docs generation to `docs/vMAJOR.MINOR` in Makefile, set CANONICAL reference and copy them to `docs/stable` (change index.html accordingly)
|
||||
12. Move docs generation to `docs/vMAJOR.MINOR` in Makefile, set CANONICAL to stable and copy them to `docs/stable` (change index.html accordingly)
|
||||
|
||||
13. In master, bump versions, start new CHANGELOG, add `-dev` back and commit "Start vVERSION+1"
|
||||
|
||||
|
||||
@@ -1098,7 +1098,7 @@ defmodule Macro do
|
||||
|
||||
case expand do
|
||||
{:ok, receiver, quoted} ->
|
||||
next = :elixir_counter.next
|
||||
next = :erlang.unique_integer()
|
||||
{:elixir_quote.linify_with_context_counter(0, {receiver, next}, quoted), true}
|
||||
{:ok, _receiver, _name, _args} ->
|
||||
{original, false}
|
||||
@@ -1119,7 +1119,7 @@ defmodule Macro do
|
||||
|
||||
case expand do
|
||||
{:ok, receiver, quoted} ->
|
||||
next = :elixir_counter.next
|
||||
next = :erlang.unique_integer()
|
||||
{:elixir_quote.linify_with_context_counter(0, {receiver, next}, quoted), true}
|
||||
:error ->
|
||||
{original, false}
|
||||
|
||||
@@ -499,7 +499,7 @@ defmodule Module do
|
||||
raise ArgumentError, "expected :file to be given as option"
|
||||
end
|
||||
|
||||
next = :elixir_counter.next
|
||||
next = :erlang.unique_integer()
|
||||
line = Keyword.get(opts, :line, 0)
|
||||
quoted = :elixir_quote.linify_with_context_counter(line, {module, next}, quoted)
|
||||
:elixir_module.compile(module, quoted, [], :elixir.env_for_eval(opts))
|
||||
|
||||
@@ -34,7 +34,6 @@ defmodule System do
|
||||
next = System.monotonic_time()
|
||||
diff = next - prev
|
||||
|
||||
|
||||
Generally speaking, the VM provides three time measurements:
|
||||
|
||||
* `os_time/0` - the time reported by the OS. This time may be
|
||||
@@ -157,8 +156,7 @@ defmodule System do
|
||||
%{build: build,
|
||||
date: get_date,
|
||||
revision: revision,
|
||||
version: version,
|
||||
}
|
||||
version: version}
|
||||
end
|
||||
|
||||
# Returns a string of the build info
|
||||
@@ -730,6 +728,30 @@ defmodule System do
|
||||
:os.system_time(normalize_time_unit(unit))
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the OTP release number.
|
||||
"""
|
||||
@spec otp_release :: String.t
|
||||
def otp_release do
|
||||
:erlang.list_to_binary :erlang.system_info(:otp_release)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the number of schedulers in the VM.
|
||||
"""
|
||||
@spec schedulers :: pos_integer
|
||||
def schedulers do
|
||||
:erlang.system_info(:schedulers)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the number of schedulers online in the VM.
|
||||
"""
|
||||
@spec schedulers_online :: pos_integer
|
||||
def schedulers_online do
|
||||
:erlang.system_info(:schedulers_online)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Generates and returns an integer that is unique in the current runtime
|
||||
instance.
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
-module(elixir_counter).
|
||||
-export([start_link/0, init/1, handle_call/3, handle_cast/2,
|
||||
handle_info/2, terminate/2, code_change/3, next/0]).
|
||||
-behaviour(gen_server).
|
||||
|
||||
-define(timeout, 30000). %% 30 seconds
|
||||
-define(limit, 4294967295). %% 2^32 - 1
|
||||
|
||||
next() ->
|
||||
gen_server:call(?MODULE, next, ?timeout).
|
||||
|
||||
start_link() ->
|
||||
gen_server:start_link({local, ?MODULE}, ?MODULE, 0, []).
|
||||
|
||||
init(Counter) ->
|
||||
{ok, Counter}.
|
||||
|
||||
handle_call(next, _From, Counter) ->
|
||||
{reply, Counter, bump(Counter)};
|
||||
handle_call(Request, _From, Counter) ->
|
||||
{stop, {badcall, Request}, Counter}.
|
||||
|
||||
handle_cast(Request, Counter) ->
|
||||
{stop, {badcast, Request}, Counter}.
|
||||
|
||||
handle_info(_Request, Counter) ->
|
||||
{noreply, Counter}.
|
||||
|
||||
terminate(_Reason, _Counter) ->
|
||||
ok.
|
||||
|
||||
code_change(_Old, Counter, _Extra) ->
|
||||
{ok, Counter}.
|
||||
|
||||
bump(Counter) when Counter < ?limit ->
|
||||
Counter + 1;
|
||||
bump(_Counter) ->
|
||||
0.
|
||||
@@ -218,7 +218,7 @@ is_macro(_) -> false.
|
||||
unwrap_definitions(File, Module) ->
|
||||
Table = elixir_module:defs_table(Module),
|
||||
Entries = ets:match(Table, {{def, '$1'}, '_', '_', '_', '_', '_', '_'}),
|
||||
{All, Private} = unwrap_definition(Entries, File, Module, Table, [], []),
|
||||
{All, Private} = unwrap_definition(lists:sort(Entries), File, Module, Table, [], []),
|
||||
Unreachable = elixir_locals:warn_unused_local(File, Module, Private),
|
||||
split_definition(All, Unreachable, [], [], [], [], [], {[], []}).
|
||||
|
||||
|
||||
@@ -202,7 +202,7 @@ expand_macro_named(Meta, Receiver, Name, Arity, Args, E) ->
|
||||
|
||||
expand_quoted(Meta, Receiver, Name, Arity, Quoted, E) ->
|
||||
Line = ?line(Meta),
|
||||
Next = elixir_counter:next(),
|
||||
Next = erlang:unique_integer(),
|
||||
|
||||
try
|
||||
elixir_exp:expand(
|
||||
|
||||
@@ -8,7 +8,7 @@ translate(Meta, Clauses, S) ->
|
||||
{Args, Guards} = elixir_clauses:extract_splat_guards(ArgsWithGuards),
|
||||
{TClause, TS } = elixir_clauses:clause(CMeta, fun translate_fn_match/2,
|
||||
Args, Expr, Guards, Acc),
|
||||
{TClause, elixir_scope:mergef(S, TS)}
|
||||
{TClause, elixir_scope:mergec(S, TS)}
|
||||
end,
|
||||
|
||||
{TClauses, NS} = lists:mapfoldl(Transformer, S, Clauses),
|
||||
@@ -111,7 +111,7 @@ handle_capture(false, Meta, Expr, E, Sequential) ->
|
||||
do_capture(Meta, Expr, E, Sequential).
|
||||
|
||||
do_capture(Meta, Expr, E, Sequential) ->
|
||||
case do_escape(Expr, elixir_counter:next(), E, []) of
|
||||
case do_escape(Expr, erlang:unique_integer(), E, []) of
|
||||
{_, []} when not Sequential ->
|
||||
invalid_capture(Meta, Expr, E);
|
||||
{EExpr, EDict} ->
|
||||
|
||||
@@ -19,7 +19,7 @@ expand_struct(Meta, Left, Right, #{context := Context} = E) ->
|
||||
true when is_atom(ELeft) ->
|
||||
%% We always record structs when they are expanded
|
||||
%% as they expect the reference at compile time.
|
||||
elixir_lexical:record_remote(ELeft, nil, ?m(E, lexical_tracker));
|
||||
elixir_lexical:record_remote(ELeft, '__struct__', 1, nil, ?line(Meta), ?m(E, lexical_tracker));
|
||||
true ->
|
||||
ok;
|
||||
false when Context == match ->
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
-module(elixir_scope).
|
||||
-export([translate_var/4, build_var/2, context_info/1,
|
||||
load_binding/2, dump_binding/2,
|
||||
mergev/2, mergec/2, mergef/2,
|
||||
merge_vars/2, merge_opt_vars/2,
|
||||
mergev/2, mergec/2, merge_vars/2, merge_opt_vars/2,
|
||||
warn_unsafe_var/4, warn_underscored_var_access/3, format_error/1
|
||||
]).
|
||||
-include("elixir.hrl").
|
||||
@@ -128,14 +127,6 @@ mergec(S1, S2) ->
|
||||
caller=S2#elixir_scope.caller
|
||||
}.
|
||||
|
||||
%% Similar to mergec but does not merge the user vars counter.
|
||||
|
||||
mergef(S1, S2) ->
|
||||
S1#elixir_scope{
|
||||
super=S2#elixir_scope.super,
|
||||
caller=S2#elixir_scope.caller
|
||||
}.
|
||||
|
||||
%% Mergers.
|
||||
|
||||
merge_vars(V, V) -> V;
|
||||
@@ -212,8 +203,8 @@ format_error({unused_match, Name, Kind}) ->
|
||||
|
||||
format_error({unsafe_var, Name}) ->
|
||||
io_lib:format("the variable \"~ts\" is unsafe as it has been set inside "
|
||||
"a case/cond/receive/if/&&/||. Please explicitly return the "
|
||||
"variable value instead. For example:\n\n"
|
||||
"a case/cond/receive/if/&&/||. Please explicitly return "
|
||||
"the variable value instead. For example:\n\n"
|
||||
" case int do\n"
|
||||
" 1 -> atom = :one\n"
|
||||
" 2 -> atom = :two\n"
|
||||
|
||||
@@ -15,7 +15,7 @@ init(ok) ->
|
||||
2000, % Shutdown = brutal_kill | int() >= 0 | infinity
|
||||
worker, % Type = worker | supervisor
|
||||
[elixir_config] % Modules = [Module] | dynamic
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
elixir_code_server,
|
||||
@@ -25,17 +25,7 @@ init(ok) ->
|
||||
2000, % Shutdown = brutal_kill | int() >= 0 | infinity
|
||||
worker, % Type = worker | supervisor
|
||||
[elixir_code_server] % Modules = [Module] | dynamic
|
||||
},
|
||||
|
||||
{
|
||||
elixir_counter,
|
||||
{elixir_counter, start_link, []},
|
||||
|
||||
permanent, % Restart = permanent | transient | temporary
|
||||
2000, % Shutdown = brutal_kill | int() >= 0 | infinity
|
||||
worker, % Type = worker | supervisor
|
||||
[elixir_counter] % Modules = [Module] | dynamic
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
{ok, {{one_for_one, 3, 10}, Workers}}.
|
||||
|
||||
@@ -65,15 +65,23 @@ expand_else(nil, E) ->
|
||||
|
||||
translate(Meta, Args, S) ->
|
||||
{Parts, [{do, Expr} | ExprList]} = elixir_utils:split_last(Args),
|
||||
CaseExpr =
|
||||
case ExprList of
|
||||
[{else, ElseExpr}] ->
|
||||
build_else(Meta, build_case(Parts, {ok, Expr}, fun(X) -> {error, X} end), ElseExpr);
|
||||
[] ->
|
||||
build_case(Parts, Expr, fun(X) -> X end)
|
||||
end,
|
||||
{TC, TS} = elixir_translator:translate(CaseExpr, S#elixir_scope{extra=nil}),
|
||||
{TC, elixir_scope:mergec(S, TS)}.
|
||||
case ExprList of
|
||||
[{else, ElseExpr}] ->
|
||||
{TCases, TS} = translate_case(Parts, {ok, Expr}, fun(X) -> {error, X} end, S),
|
||||
translate_else(Meta, TCases, ElseExpr, TS);
|
||||
[] ->
|
||||
translate_case(Parts, Expr, fun(X) -> X end, S)
|
||||
end.
|
||||
|
||||
translate_case(Parts, DoExpr, Wrapper, S) ->
|
||||
Cases = build_case(Parts, DoExpr, Wrapper),
|
||||
{TCases, TS} = elixir_translator:translate(Cases, S#elixir_scope{extra=nil}),
|
||||
{TCases, elixir_scope:mergec(S, TS)}.
|
||||
|
||||
translate_else(Meta, WithCases, ElseExpr, S) ->
|
||||
ElseClauses = build_else(Meta, ElseExpr),
|
||||
{TClauses, TS} = elixir_clauses:clauses(Meta, ElseClauses, S#elixir_scope{extra=nil}),
|
||||
{{'case', ?ann(Meta), WithCases, TClauses}, elixir_scope:mergec(S, TS)}.
|
||||
|
||||
build_case([{'<-', Meta, [{Name, _, Ctx}, _] = Args} | Rest], DoExpr, Wrapper)
|
||||
when is_atom(Name) andalso is_atom(Ctx) ->
|
||||
@@ -90,18 +98,15 @@ build_case([Expr | Rest], DoExpr, Wrapper) ->
|
||||
build_case([], DoExpr, _Wrapper) ->
|
||||
DoExpr.
|
||||
|
||||
build_else(Meta, WithCases, ElseClauses) ->
|
||||
build_else(Meta, ElseClauses) ->
|
||||
Result = {result, Meta, ?MODULE},
|
||||
Clauses = [
|
||||
{'->', Meta, [[{ok, Result}], Result]}
|
||||
| else_to_error_clause(ElseClauses)
|
||||
] ++ [build_raise(Meta)],
|
||||
{'case', Meta, [WithCases, [{do, Clauses}]]}.
|
||||
[{match, Meta, [{ok, Result}], Result} |
|
||||
each_clause_to_error_match(ElseClauses)] ++ [build_raise(Meta)].
|
||||
|
||||
else_to_error_clause(Clauses) ->
|
||||
[{'->', Meta, [[{error, Match}], Expr]} ||
|
||||
each_clause_to_error_match(Clauses) ->
|
||||
[{match, Meta, [{error, Match}], Expr} ||
|
||||
{'->', Meta, [[Match], Expr]} <- Clauses].
|
||||
|
||||
build_raise(Meta) ->
|
||||
Other = {raise, Meta, ?MODULE},
|
||||
{'->', ?generated, [[{error, Other}], {{'.', Meta, [erlang, error]}, Meta, [{with_clause, Other}]}]}.
|
||||
Other = {other, Meta, ?MODULE},
|
||||
{match, ?generated, [{error, Other}], {{'.', Meta, [erlang, error]}, Meta, [{with_clause, Other}]}}.
|
||||
|
||||
@@ -18,6 +18,18 @@ defmodule Kernel.FnTest do
|
||||
refute (fn ^x -> true; _ -> false end).(1.0)
|
||||
end
|
||||
|
||||
test "case function hoisting does not affect anonymous fns" do
|
||||
assert :undefined =
|
||||
(if is_a?(:atom, 0) do
|
||||
user = :defined
|
||||
else
|
||||
(fn() ->
|
||||
user = :undefined
|
||||
user
|
||||
end).()
|
||||
end)
|
||||
end
|
||||
|
||||
test "capture with access" do
|
||||
assert (&(&1[:hello])).([hello: :world]) == :world
|
||||
end
|
||||
|
||||
@@ -161,6 +161,7 @@ defmodule Kernel.LexicalTrackerTest do
|
||||
&R.func/0
|
||||
&Remote.func/0
|
||||
&Integer.is_even/1
|
||||
%Macro.Env{}
|
||||
end
|
||||
|
||||
&extract/2
|
||||
@@ -171,6 +172,8 @@ defmodule Kernel.LexicalTrackerTest do
|
||||
|
||||
&is_record/1; def b(a), do: is_record(a)
|
||||
|
||||
%Macro.Env{}
|
||||
|
||||
Kernel.LexicalTracker.remote_dispatches(__ENV__.module)
|
||||
end |> elem(3)
|
||||
""")
|
||||
@@ -181,14 +184,16 @@ defmodule Kernel.LexicalTrackerTest do
|
||||
assert {9, Integer, :is_even, 1} in compile_remote_calls
|
||||
assert {15, Record, :is_record, 1} in compile_remote_calls
|
||||
assert {18, Integer, :is_even, 1} in compile_remote_calls
|
||||
assert {21, Record, :extract, 2} in compile_remote_calls
|
||||
assert {22, Record, :is_record, 1} in compile_remote_calls
|
||||
assert {23, Remote, :func, 0} in compile_remote_calls
|
||||
assert {19, Macro.Env, :__struct__, 1} in compile_remote_calls
|
||||
assert {22, Record, :extract, 2} in compile_remote_calls
|
||||
assert {23, Record, :is_record, 1} in compile_remote_calls
|
||||
assert {24, Remote, :func, 0} in compile_remote_calls
|
||||
assert {25, Integer, :is_even, 1} in compile_remote_calls
|
||||
assert {27, Kernel, :def, 2} in compile_remote_calls
|
||||
assert {27, Record, :is_record, 1} in compile_remote_calls
|
||||
assert {29, Kernel.LexicalTracker, :remote_dispatches, 1} in compile_remote_calls
|
||||
assert {25, Remote, :func, 0} in compile_remote_calls
|
||||
assert {26, Integer, :is_even, 1} in compile_remote_calls
|
||||
assert {28, Kernel, :def, 2} in compile_remote_calls
|
||||
assert {28, Record, :is_record, 1} in compile_remote_calls
|
||||
assert {30, Macro.Env, :__struct__, 1} in compile_remote_calls
|
||||
assert {32, Kernel.LexicalTracker, :remote_dispatches, 1} in compile_remote_calls
|
||||
|
||||
runtime_remote_calls = unroll_dispatches(runtime_remote_calls)
|
||||
assert {7, Record, :extract, 2} in runtime_remote_calls
|
||||
@@ -200,7 +205,7 @@ defmodule Kernel.LexicalTrackerTest do
|
||||
assert {16, Remote, :func, 0} in runtime_remote_calls
|
||||
assert {17, Remote, :func, 0} in runtime_remote_calls
|
||||
assert {18, :erlang, :==, 2} in runtime_remote_calls
|
||||
assert {27, :erlang, :is_tuple, 1} in runtime_remote_calls
|
||||
assert {28, :erlang, :is_tuple, 1} in runtime_remote_calls
|
||||
end
|
||||
|
||||
defp unroll_dispatches(dispatches) do
|
||||
|
||||
@@ -48,7 +48,7 @@ defmodule Kernel.MacrosTest do
|
||||
assert_raise UndefinedFunctionError, fn -> x.value end
|
||||
end
|
||||
|
||||
test "bang do block" do
|
||||
test "macros with bang and do block have proper precedence" do
|
||||
import Kernel.MacrosTest.Nested
|
||||
assert (do_identity! do 1 end) == 1
|
||||
assert (Kernel.MacrosTest.Nested.do_identity! do 1 end) == 1
|
||||
|
||||
@@ -47,6 +47,13 @@ defmodule Kernel.WithTest do
|
||||
assert result == :error
|
||||
end
|
||||
|
||||
test "does not leak variables to else" do
|
||||
state = 1
|
||||
result = with 1 <- state, state = 2, :ok <- error(), do: state, else: (_ -> state)
|
||||
assert result == 1
|
||||
assert state == 1
|
||||
end
|
||||
|
||||
test "errors in with" do
|
||||
assert_raise RuntimeError, fn ->
|
||||
with({:ok, res} <- oops(), do: res)
|
||||
|
||||
@@ -245,6 +245,17 @@ defmodule ModuleTest do
|
||||
assert ModuleHygiene.test == [1, 2, 3]
|
||||
end
|
||||
|
||||
test "ensure function clauses are ordered" do
|
||||
{_, _, binary, _} =
|
||||
defmodule Ordered do
|
||||
def foo(:foo), do: :bar
|
||||
def baz(:baz), do: :bat
|
||||
end
|
||||
atoms = :beam_lib.chunks(binary, [:atoms])
|
||||
assert :erlang.phash2(atoms) == 53987778
|
||||
end
|
||||
|
||||
# TODO: Remove this check once we depend only on 19
|
||||
if :erlang.system_info(:otp_release) >= '19' do
|
||||
test "create with generated true does not emit warnings" do
|
||||
contents =
|
||||
|
||||
@@ -187,4 +187,16 @@ defmodule SystemTest do
|
||||
time = System.monotonic_time(:nanoseconds)
|
||||
assert abs(System.convert_time_unit(time, :nanoseconds, :microseconds)) < abs(time)
|
||||
end
|
||||
|
||||
test "schedulers/0" do
|
||||
assert System.schedulers >= 1
|
||||
end
|
||||
|
||||
test "schedulers_online/0" do
|
||||
assert System.schedulers_online >= 1
|
||||
end
|
||||
|
||||
test "otp_release/0" do
|
||||
assert is_binary System.otp_release
|
||||
end
|
||||
end
|
||||
|
||||
@@ -15,7 +15,6 @@ extract_interpolations(String) ->
|
||||
Z
|
||||
end.
|
||||
|
||||
|
||||
% Interpolations
|
||||
|
||||
extract_interpolations_without_interpolation_test() ->
|
||||
|
||||
@@ -379,7 +379,6 @@ defmodule ExUnit.Assertions do
|
||||
<> ExUnit.Assertions.__mailbox__(self()))
|
||||
end
|
||||
|
||||
_ = unquote(vars) # Silence warnings
|
||||
received
|
||||
end
|
||||
end
|
||||
@@ -428,6 +427,14 @@ defmodule ExUnit.Assertions do
|
||||
Enum.uniq_by(pins, &elem(&1, 0))
|
||||
end
|
||||
|
||||
defp collect_vars_from_pattern({:when, _, [left, right]}) do
|
||||
pattern = collect_vars_from_pattern(left)
|
||||
for {name, _, context} = var <- collect_vars_from_pattern(right),
|
||||
Enum.any?(pattern, &match?({^name, _, ^context}, &1)),
|
||||
into: pattern,
|
||||
do: var
|
||||
end
|
||||
|
||||
defp collect_vars_from_pattern(expr) do
|
||||
Macro.prewalk(expr, [], fn
|
||||
{:::, _, [left, _]}, acc ->
|
||||
|
||||
@@ -266,6 +266,13 @@ defmodule ExUnit.AssertionsTest do
|
||||
:world = world
|
||||
end
|
||||
|
||||
test "assert received does not leak external variables used in guards" do
|
||||
send self(), {:hello, :world}
|
||||
guard_world = :world
|
||||
assert_received {:hello, world} when world == guard_world
|
||||
:world = world
|
||||
end
|
||||
|
||||
test "refute received does not wait" do
|
||||
false = refute_received :hello
|
||||
end
|
||||
|
||||
@@ -23,8 +23,8 @@ defmodule IEx.Helpers do
|
||||
There are many other helpers available:
|
||||
|
||||
* `b/1` - prints callbacks info and docs for a given module
|
||||
* `c/1` - compiles a file at the current directory
|
||||
* `c/2` - compiles a file at the given path
|
||||
* `c/1` - compiles a file into the current directory
|
||||
* `c/2` - compiles a file to the given path
|
||||
* `cd/1` - changes the current directory
|
||||
* `clear/0` - clears the screen
|
||||
* `flush/0` - flushes all messages sent to the shell
|
||||
@@ -115,6 +115,7 @@ defmodule IEx.Helpers do
|
||||
|
||||
c "baz.ex"
|
||||
#=> [Baz]
|
||||
|
||||
"""
|
||||
def c(files, path \\ ".") when is_binary(path) do
|
||||
files = List.wrap(files)
|
||||
@@ -123,10 +124,7 @@ defmodule IEx.Helpers do
|
||||
raise ArgumentError, "expected a binary or a list of binaries as argument"
|
||||
end
|
||||
|
||||
{found, not_found} =
|
||||
files
|
||||
|> Enum.map(&Path.expand(&1, path))
|
||||
|> Enum.partition(&File.exists?/1)
|
||||
{found, not_found} = Enum.partition(files, &File.exists?/1)
|
||||
|
||||
unless Enum.empty?(not_found) do
|
||||
raise ArgumentError, "could not find files #{Enum.join(not_found, ", ")}"
|
||||
|
||||
@@ -177,8 +177,6 @@ defmodule Mix.Compilers.Elixir do
|
||||
end
|
||||
|
||||
defp each_module(pid, cwd, source, module, binary) do
|
||||
beam = Atom.to_string(module) <> ".beam"
|
||||
|
||||
{compile_references, runtime_references} = Kernel.LexicalTracker.remote_references(module)
|
||||
|
||||
compile_references =
|
||||
@@ -214,7 +212,7 @@ defmodule Mix.Compilers.Elixir do
|
||||
module: module,
|
||||
kind: kind,
|
||||
source: source,
|
||||
beam: beam,
|
||||
beam: nil, # They are calculated when writing the manifest
|
||||
binary: binary
|
||||
)
|
||||
|
||||
@@ -265,8 +263,10 @@ defmodule Mix.Compilers.Elixir do
|
||||
## Resolution
|
||||
|
||||
defp update_stale_sources(sources, removed, changed) do
|
||||
# Remove delete sources
|
||||
sources =
|
||||
Enum.reject(sources, fn source(source: source) -> source in removed end)
|
||||
Enum.reduce(removed, sources, &List.keydelete(&2, &1, source(:source)))
|
||||
# Store empty sources for the changed ones as the compiler appends data
|
||||
sources =
|
||||
Enum.reduce(changed, sources, &List.keystore(&2, &1, source(:source), source(source: &1)))
|
||||
sources
|
||||
@@ -394,13 +394,14 @@ defmodule Mix.Compilers.Elixir do
|
||||
File.mkdir_p!(Path.dirname(manifest))
|
||||
|
||||
modules =
|
||||
for module(beam: beam, binary: binary) = module <- modules do
|
||||
for module(binary: binary, module: module) = entry <- modules do
|
||||
beam = Atom.to_string(module) <> ".beam"
|
||||
if binary do
|
||||
beam_path = Path.join(compile_path, beam)
|
||||
File.write!(beam_path, binary)
|
||||
File.touch!(beam_path, timestamp)
|
||||
end
|
||||
module(module, binary: nil)
|
||||
module(entry, binary: nil, beam: beam)
|
||||
end
|
||||
|
||||
manifest_data =
|
||||
|
||||
@@ -56,11 +56,18 @@ defmodule Mix.Tasks.App.Tree do
|
||||
|
||||
if opts[:format] == "dot" do
|
||||
Mix.Utils.write_dot_graph!("app_tree.dot", "application tree",
|
||||
{:normal, app}, callback, opts)
|
||||
Mix.shell.info "Generated \"app_tree.dot\" in current directory.\n" <>
|
||||
"You can use http://www.graphviz.org/ to open it."
|
||||
[{:normal, app}], callback, opts)
|
||||
"""
|
||||
Generated "app_tree.dot" in the current directory. To generate a PNG:
|
||||
|
||||
dot -Tpng app_tree.dot -o app_tree.png
|
||||
|
||||
For more options see http://www.graphviz.org/.
|
||||
"""
|
||||
|> String.trim_trailing
|
||||
|> Mix.shell.info
|
||||
else
|
||||
Mix.Utils.print_tree({:normal, app}, callback, opts)
|
||||
Mix.Utils.print_tree([{:normal, app}], callback, opts)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ defmodule Mix.Tasks.Compile do
|
||||
## Configuration
|
||||
|
||||
* `:compilers` - compilers to run, defaults to:
|
||||
`[:yeec, :leex, :erlang, :elixir, :xref, :app]`
|
||||
`[:yecc, :leex, :erlang, :elixir, :xref, :app]`
|
||||
|
||||
* `:consolidate_protocols` - when `true`, runs protocol
|
||||
consolidation via the `compile.protocols` task
|
||||
|
||||
@@ -36,7 +36,7 @@ defmodule Mix.Tasks.Compile.Xref do
|
||||
end
|
||||
|
||||
defp run_xref do
|
||||
Mix.Task.run("xref", ["--warnings"])
|
||||
Mix.Task.run("xref", ["warnings"])
|
||||
end
|
||||
|
||||
defp needs_xref?(opts) do
|
||||
|
||||
@@ -95,11 +95,16 @@ defmodule Mix.Tasks.Deps.Check do
|
||||
|
||||
defp partition([dep | deps], not_ok, compile) do
|
||||
cond do
|
||||
from_umbrella?(dep) -> partition(deps, not_ok, compile)
|
||||
compilable?(dep) -> partition(deps, not_ok, [dep | compile])
|
||||
ok?(dep) and local?(dep) -> partition(deps, not_ok, [dep | compile])
|
||||
ok?(dep) -> partition(deps, not_ok, compile)
|
||||
true -> partition(deps, [dep | not_ok], compile)
|
||||
compilable?(dep) ->
|
||||
if from_umbrella?(dep) do
|
||||
partition(deps, not_ok, compile)
|
||||
else
|
||||
partition(deps, not_ok, [dep | compile])
|
||||
end
|
||||
ok?(dep) ->
|
||||
partition(deps, not_ok, compile)
|
||||
true ->
|
||||
partition(deps, [dep | not_ok], compile)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -123,7 +128,7 @@ defmodule Mix.Tasks.Deps.Check do
|
||||
defp compilable?(%Mix.Dep{status: {:noappfile, _}}), do: true
|
||||
defp compilable?(%Mix.Dep{status: {:scmlock, _}}), do: true
|
||||
defp compilable?(%Mix.Dep{status: :compile}), do: true
|
||||
defp compilable?(%Mix.Dep{}), do: false
|
||||
defp compilable?(%Mix.Dep{} = dep), do: ok?(dep) and local?(dep)
|
||||
|
||||
defp show_not_ok!([]) do
|
||||
:ok
|
||||
|
||||
@@ -33,7 +33,7 @@ defmodule Mix.Tasks.Deps.Compile do
|
||||
import Mix.Dep, only: [loaded: 1, available?: 1, loaded_by_name: 2,
|
||||
make?: 1, mix?: 1]
|
||||
|
||||
@switches [include_children: :boolean]
|
||||
@switches [include_children: :boolean, force: :boolean]
|
||||
|
||||
@spec run(OptionParser.argv) :: :ok
|
||||
def run(args) do
|
||||
@@ -44,18 +44,18 @@ defmodule Mix.Tasks.Deps.Compile do
|
||||
Mix.Project.get!
|
||||
|
||||
case OptionParser.parse(args, switches: @switches) do
|
||||
{_, [], _} ->
|
||||
{opts, [], _} ->
|
||||
# Because this command may be invoked explicitly with
|
||||
# deps.compile, we simply try to compile any available
|
||||
# dependency.
|
||||
compile(Enum.filter(loaded(env: Mix.env), &available?/1))
|
||||
compile(Enum.filter(loaded(env: Mix.env), &available?/1), opts)
|
||||
{opts, tail, _} ->
|
||||
compile(loaded_by_name(tail, [env: Mix.env] ++ opts))
|
||||
compile(loaded_by_name(tail, [env: Mix.env] ++ opts), opts)
|
||||
end
|
||||
end
|
||||
|
||||
@doc false
|
||||
def compile(deps) do
|
||||
def compile(deps, options \\ []) do
|
||||
shell = Mix.shell
|
||||
config = Mix.Project.deps_config
|
||||
|
||||
@@ -65,6 +65,8 @@ defmodule Mix.Tasks.Deps.Compile do
|
||||
Enum.map(deps, fn %Mix.Dep{app: app, status: status, opts: opts, scm: scm} = dep ->
|
||||
check_unavailable!(app, status)
|
||||
|
||||
clean(app, options)
|
||||
|
||||
compiled? = cond do
|
||||
not is_nil(opts[:compile]) ->
|
||||
do_compile dep, config
|
||||
@@ -93,6 +95,12 @@ defmodule Mix.Tasks.Deps.Compile do
|
||||
if true in compiled, do: Mix.Dep.Lock.touch_manifest, else: :ok
|
||||
end
|
||||
|
||||
defp clean(app, opts) do
|
||||
if Keyword.get(opts, :force, false) do
|
||||
File.rm_rf! Path.join [Mix.Project.build_path, "lib", Atom.to_string(app)]
|
||||
end
|
||||
end
|
||||
|
||||
defp touch_fetchable(scm, path) do
|
||||
if scm.fetchable? do
|
||||
File.mkdir_p!(path)
|
||||
|
||||
@@ -51,12 +51,19 @@ defmodule Mix.Tasks.Deps.Tree do
|
||||
|
||||
if opts[:format] == "dot" do
|
||||
callback = callback(&format_dot/1, deps, opts)
|
||||
Mix.Utils.write_dot_graph!("deps_tree.dot", "dependency tree", root, callback, opts)
|
||||
Mix.shell.info "Generated \"deps_tree.dot\" in current directory.\n" <>
|
||||
"You can use http://www.graphviz.org/ to open it."
|
||||
Mix.Utils.write_dot_graph!("deps_tree.dot", "dependency tree", [root], callback, opts)
|
||||
"""
|
||||
Generated "deps_tree.dot" in the current directory. To generate a PNG:
|
||||
|
||||
dot -Tpng deps_tree.dot -o deps_tree.png
|
||||
|
||||
For more options see http://www.graphviz.org/.
|
||||
"""
|
||||
|> String.trim_trailing
|
||||
|> Mix.shell.info
|
||||
else
|
||||
callback = callback(&format_tree/1, deps, opts)
|
||||
Mix.Utils.print_tree(root, callback, opts)
|
||||
Mix.Utils.print_tree([root], callback, opts)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -251,7 +251,7 @@ defmodule Mix.Tasks.New do
|
||||
elixir: "~> <%= @version %>",
|
||||
build_embedded: Mix.env == :prod,
|
||||
start_permanent: Mix.env == :prod,
|
||||
deps: deps]
|
||||
deps: deps()]
|
||||
end
|
||||
|
||||
# Configuration for the OTP application
|
||||
|
||||
@@ -3,7 +3,7 @@ defmodule Mix.Tasks.Test do
|
||||
@moduledoc false
|
||||
|
||||
def start(compile_path, opts) do
|
||||
Mix.shell.info "Cover compiling modules ... "
|
||||
Mix.shell.info "Cover compiling modules ..."
|
||||
_ = :cover.start
|
||||
|
||||
case :cover.compile_beam_directory(compile_path |> to_charlist) do
|
||||
@@ -16,7 +16,7 @@ defmodule Mix.Tasks.Test do
|
||||
output = opts[:output]
|
||||
|
||||
fn() ->
|
||||
Mix.shell.info "\nGenerating cover results ... "
|
||||
Mix.shell.info "\nGenerating cover results ..."
|
||||
File.mkdir_p!(output)
|
||||
Enum.each :cover.modules, fn(mod) ->
|
||||
{:ok, _} = :cover.analyse_to_file(mod, '#{output}/#{mod}.html', [:html])
|
||||
|
||||
+202
-18
@@ -2,7 +2,7 @@ defmodule Mix.Tasks.Xref do
|
||||
use Mix.Task
|
||||
|
||||
alias Mix.Tasks.Compile.Elixir, as: E
|
||||
import Mix.Compilers.Elixir, only: [read_manifest: 2, source: 1, source: 2]
|
||||
import Mix.Compilers.Elixir, only: [read_manifest: 2, source: 1, source: 2, module: 1]
|
||||
|
||||
@shortdoc "Performs cross reference checks"
|
||||
@recursive true
|
||||
@@ -12,23 +12,82 @@ defmodule Mix.Tasks.Xref do
|
||||
|
||||
## Xref modes
|
||||
|
||||
The following options control the information xref can emit.
|
||||
The `xref` task expects a mode as first argument:
|
||||
|
||||
mix xref MODE
|
||||
|
||||
All available modes are discussed below.
|
||||
|
||||
### warnings
|
||||
|
||||
* `--warnings` - prints warnings for violated cross reference checks
|
||||
* `--unreachable` - prints all unreachable "file:line: module.function/arity" entries
|
||||
* `--callers` - prints all references of given `Module`, `Module.function`, or
|
||||
`Module.function/arity`
|
||||
Prints warnings for violated cross reference checks:
|
||||
|
||||
mix xref warnings
|
||||
|
||||
This is the mode used by Mix during compilation.
|
||||
|
||||
## Command line options
|
||||
### unreachable
|
||||
|
||||
Prints all unreachable "file:line: module.function/arity" entries:
|
||||
|
||||
mix xref unreachable
|
||||
|
||||
The "file:line" represents the file and line a call to an unknown
|
||||
"module.function/arity" is made.
|
||||
|
||||
### callers CALLEE
|
||||
|
||||
Prints all callers of the given `CALLEE`, which can be one of: `Module`,
|
||||
`Module.function`, or `Module.function/arity`. Examples:
|
||||
|
||||
mix xref callers MyMod
|
||||
mix xref callers MyMod.fun
|
||||
mix xref callers MyMod.fun/3
|
||||
|
||||
### graph
|
||||
|
||||
Prints a file dependency graph where an edge from `A` to `B` indicates
|
||||
that `A` depends on `B`.
|
||||
|
||||
mix xref graph --format dot
|
||||
|
||||
The following options are accepted:
|
||||
|
||||
* `--exclude` - paths to exclude
|
||||
|
||||
* `--source` - display all files that the given source file references (directly or indirectly)
|
||||
|
||||
* `--sink` - display all files that reference the given file (directly or indirectly)
|
||||
|
||||
* `--format` - can be set to one of:
|
||||
|
||||
* `pretty` - use Unicode codepoints for formatting the graph.
|
||||
This is the default except on Windows
|
||||
|
||||
* `plain` - do not use Unicode codepoints for formatting the graph.
|
||||
This is the default on Windows
|
||||
|
||||
* `dot` - produces a DOT graph description in `xref_graph.dot` in the
|
||||
current directory. Warning: this will override any previously generated file
|
||||
|
||||
The `--source` and `--sink` options are particularly useful when trying to understand how
|
||||
the modules in a particular file interact with the whole system.
|
||||
|
||||
## Shared options
|
||||
|
||||
Those options are shared across all modes:
|
||||
|
||||
* `--no-compile` - do not compile even if files require compilation
|
||||
|
||||
* `--no-deps-check` - do not check dependencies
|
||||
|
||||
* `--no-archives-check` - do not check archives
|
||||
|
||||
* `--no-elixir-version-check` - do not check the Elixir version from mix.exs
|
||||
|
||||
## Configuration
|
||||
|
||||
All configuration for Xref should be placed under the key `:xref`.
|
||||
All configuration for Xref should be placed under the key `:xref`.
|
||||
|
||||
* `:exclude` - a list of modules and `{module, function, arity}` tuples to ignore when checking
|
||||
cross references. For example: `[MissingModule, {MissingModule2, :missing_func, 2}]`
|
||||
@@ -36,31 +95,32 @@ defmodule Mix.Tasks.Xref do
|
||||
"""
|
||||
|
||||
@switches [compile: :boolean, deps_check: :boolean, archives_check: :boolean,
|
||||
warnings: :boolean, unreachable: :boolean, elixir_version_check: :boolean,
|
||||
callers: :string]
|
||||
elixir_version_check: :boolean, exclude: :keep, format: :string,
|
||||
source: :string, sink: :string]
|
||||
|
||||
@doc """
|
||||
Runs this task.
|
||||
"""
|
||||
@spec run(OptionParser.argv) :: :ok | :error
|
||||
def run(args) do
|
||||
{opts, _} =
|
||||
{opts, args} =
|
||||
OptionParser.parse!(args, strict: @switches)
|
||||
|
||||
if Keyword.get(opts, :compile, true) do
|
||||
Mix.Task.run("compile")
|
||||
end
|
||||
|
||||
modes = [:warnings, :unreachable, :callers]
|
||||
case Keyword.take(opts, modes) do
|
||||
[warnings: true] ->
|
||||
case args do
|
||||
["warnings"] ->
|
||||
warnings()
|
||||
[unreachable: true] ->
|
||||
["unreachable"] ->
|
||||
unreachable()
|
||||
[callers: callee] ->
|
||||
["callers", callee] ->
|
||||
callers(callee)
|
||||
["graph"] ->
|
||||
graph(opts)
|
||||
_ ->
|
||||
Mix.raise "xref expects exactly one of the following modes: --warnings, --unreachable, --callers"
|
||||
Mix.raise "xref doesn't support this command, see mix help xref for more information"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -90,6 +150,12 @@ defmodule Mix.Tasks.Xref do
|
||||
:ok
|
||||
end
|
||||
|
||||
defp graph(opts) do
|
||||
write_graph(file_references(), excluded(opts), opts)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
## Unreachable
|
||||
|
||||
defp unreachable(pair_fun) do
|
||||
@@ -279,12 +345,130 @@ defmodule Mix.Tasks.Xref do
|
||||
|
||||
defp raise_invalid_callee(callee) do
|
||||
message =
|
||||
"xref --callers expects Module, Module.function, or Module.function/arity, got: " <>
|
||||
"xref callers CALLEE expects Module, Module.function, or Module.function/arity, got: " <>
|
||||
callee
|
||||
|
||||
Mix.raise message
|
||||
end
|
||||
|
||||
## Graph helpers
|
||||
|
||||
defp excluded(opts) do
|
||||
Keyword.get_values(opts, :exclude)
|
||||
|> Enum.flat_map(&[{&1, nil}, {&1, "(compile)"}, {&1, "(runtime)"}])
|
||||
end
|
||||
|
||||
defp file_references() do
|
||||
module_sources =
|
||||
for manifest <- E.manifests(),
|
||||
manifest_data = read_manifest(manifest, ""),
|
||||
module(module: module, source: source) <- manifest_data,
|
||||
source = Enum.find(manifest_data, &match?(source(source: ^source), &1)),
|
||||
do: {module, source},
|
||||
into: %{}
|
||||
|
||||
all_modules = MapSet.new(module_sources, &elem(&1, 0))
|
||||
|
||||
Map.new module_sources, fn {module, source} ->
|
||||
source(runtime_references: runtime, compile_references: compile, source: file) = source
|
||||
compile_references =
|
||||
compile
|
||||
|> MapSet.new()
|
||||
|> MapSet.delete(module)
|
||||
|> MapSet.intersection(all_modules)
|
||||
|> Enum.filter(&module_sources[&1] != source)
|
||||
|> Enum.map(&{source(module_sources[&1], :source), "(compile)"})
|
||||
|
||||
runtime_references =
|
||||
runtime
|
||||
|> MapSet.new()
|
||||
|> MapSet.delete(module)
|
||||
|> MapSet.intersection(all_modules)
|
||||
|> Enum.filter(&module_sources[&1] != source)
|
||||
|> Enum.map(&{source(module_sources[&1], :source), nil})
|
||||
|
||||
{file, compile_references ++ runtime_references}
|
||||
end
|
||||
end
|
||||
|
||||
defp write_graph(file_references, excluded, opts) do
|
||||
{root, file_references} =
|
||||
case {opts[:source], opts[:sink]} do
|
||||
{nil, nil} ->
|
||||
{Enum.map(file_references, &{elem(&1, 0), nil}) -- excluded, file_references}
|
||||
|
||||
{source, nil} ->
|
||||
if file_references[source] do
|
||||
{[{source, nil}], file_references}
|
||||
else
|
||||
Mix.raise "Source could not be found: #{source}"
|
||||
end
|
||||
|
||||
{nil, sink} ->
|
||||
if file_references[sink] do
|
||||
file_references = filter_for_sink(file_references, sink)
|
||||
roots =
|
||||
file_references
|
||||
|> Map.delete(sink)
|
||||
|> Enum.map(&{elem(&1, 0), nil})
|
||||
{roots -- excluded, file_references}
|
||||
else
|
||||
Mix.raise "Sink could not be found: #{sink}"
|
||||
end
|
||||
|
||||
{_, _} ->
|
||||
Mix.raise "mix xref graph expects only one of --source and --sink"
|
||||
end
|
||||
|
||||
callback =
|
||||
fn {file, type} ->
|
||||
children = Map.get(file_references, file, [])
|
||||
{{file, type}, children -- excluded}
|
||||
end
|
||||
|
||||
if opts[:format] == "dot" do
|
||||
Mix.Utils.write_dot_graph!("xref_graph.dot", "xref graph",
|
||||
root, callback, opts)
|
||||
"""
|
||||
Generated "xref_graph.dot" in the current directory. To generate a PNG:
|
||||
|
||||
dot -Tpng xref_graph.dot -o xref_graph.png
|
||||
|
||||
For more options see http://www.graphviz.org/.
|
||||
"""
|
||||
|> String.trim_trailing()
|
||||
|> Mix.shell.info()
|
||||
else
|
||||
Mix.Utils.print_tree(root, callback, opts)
|
||||
end
|
||||
end
|
||||
|
||||
defp filter_for_sink(file_references, sink) do
|
||||
file_references
|
||||
|> invert_references()
|
||||
|> do_filter_for_sink([{sink, nil}], %{})
|
||||
|> invert_references()
|
||||
end
|
||||
|
||||
defp do_filter_for_sink(file_references, new_nodes, acc) do
|
||||
Enum.reduce new_nodes, acc, fn {new_node_name, _type}, acc ->
|
||||
new_nodes = file_references[new_node_name]
|
||||
if acc[new_node_name] || !new_nodes do
|
||||
acc
|
||||
else
|
||||
do_filter_for_sink(file_references, new_nodes, Map.put(acc, new_node_name, new_nodes))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp invert_references(file_references) do
|
||||
Enum.reduce file_references, %{}, fn {file, references}, acc ->
|
||||
Enum.reduce references, acc, fn {reference, type}, acc ->
|
||||
Map.update(acc, reference, [{file, type}], &[{file, type} | &1])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
## Helpers
|
||||
|
||||
defp each_source_entries(entries_fun, pair_fun) do
|
||||
|
||||
+48
-34
@@ -142,31 +142,40 @@ defmodule Mix.Utils do
|
||||
end) |> Enum.uniq
|
||||
end
|
||||
|
||||
@type tree_node :: {name :: String.Chars.t, edge_info :: String.Chars.t}
|
||||
|
||||
@doc """
|
||||
Prints the given tree according to the callback.
|
||||
|
||||
The callback will be invoked for each node and it
|
||||
must either return `{printed, children}` tuple or
|
||||
`false` if the given node must not be printed.
|
||||
must return a `{printed, children}` tuple.
|
||||
"""
|
||||
@spec print_tree(term, (term -> {String.t, [term]}), Keyword.t) :: :ok
|
||||
def print_tree(root, callback, opts \\ []) do
|
||||
@spec print_tree([tree_node], (tree_node -> {tree_node, [tree_node]}), Keyword.t) :: :ok
|
||||
def print_tree(nodes, callback, opts \\ []) do
|
||||
pretty =
|
||||
case Keyword.get(opts, :format) do
|
||||
"pretty" -> true
|
||||
"plain" -> false
|
||||
_ -> elem(:os.type, 0) != :win32
|
||||
end
|
||||
print_tree([root], [], pretty, callback)
|
||||
print_tree(nodes, [], nil, MapSet.new(), pretty, callback)
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
defp print_tree([], _depth, _pretty, _callback), do: :ok
|
||||
defp print_tree([node | nodes], depth, pretty, callback) do
|
||||
{{name, info}, children} = callback.(node)
|
||||
space = if info, do: " ", else: ""
|
||||
Mix.shell.info("#{depth(pretty, depth)}#{prefix(pretty, depth, nodes)}#{name}#{space}#{info}")
|
||||
print_tree(children, [(nodes != []) | depth], pretty, callback)
|
||||
print_tree(nodes, depth, pretty, callback)
|
||||
defp print_tree([], _depth, _parent, seen, _pretty, _callback), do: seen
|
||||
defp print_tree([node | nodes], depth, parent, seen, pretty, callback) do
|
||||
{{name, info}, children} = callback.(node)
|
||||
key = {parent, name}
|
||||
|
||||
if MapSet.member?(seen, key) do
|
||||
seen
|
||||
else
|
||||
space = if info, do: " ", else: ""
|
||||
Mix.shell.info("#{depth(pretty, depth)}#{prefix(pretty, depth, nodes)}#{name}#{space}#{info}")
|
||||
seen = print_tree(children, [(nodes != []) | depth], name, MapSet.put(seen, key), pretty, callback)
|
||||
print_tree(nodes, depth, parent, seen, pretty, callback)
|
||||
end
|
||||
end
|
||||
|
||||
defp depth(_pretty, []), do: ""
|
||||
@@ -188,37 +197,42 @@ defmodule Mix.Utils do
|
||||
Outputs the given tree according to the callback as a DOT graph.
|
||||
|
||||
The callback will be invoked for each node and it
|
||||
must either return `{printed, children}` tuple or
|
||||
`false` if the given node must not be printed.
|
||||
must return a `{printed, children}` tuple.
|
||||
"""
|
||||
@spec write_dot_graph!(Path.t, String.t, term, (term -> {String.t, [term]}), Keyword.t) :: :ok
|
||||
def write_dot_graph!(path, title, root, callback, _opts \\ []) do
|
||||
{{parent, _}, children} = callback.(root)
|
||||
{dot, _} = build_dot_graph(parent, children, %{}, callback)
|
||||
@spec write_dot_graph!(Path.t, String.t, [tree_node], (tree_node -> {tree_node, [tree_node]}), Keyword.t) :: :ok
|
||||
def write_dot_graph!(path, title, nodes, callback, _opts \\ []) do
|
||||
{dot, _} = build_dot_graph(make_ref(), nodes, MapSet.new(), callback)
|
||||
File.write! path, "digraph \"#{title}\" {\n#{dot}}\n"
|
||||
end
|
||||
|
||||
defp build_dot_graph(_parent, [], seen, _callback), do: {"", seen}
|
||||
defp build_dot_graph(parent, [node | nodes], seen, callback) do
|
||||
{{name, edge_info}, children} = callback.(node)
|
||||
{current, seen} = build_dot_current(parent, name, edge_info, seen)
|
||||
{children, seen} = build_dot_graph(name, children, seen, callback)
|
||||
{siblings, seen} = build_dot_graph(parent, nodes, seen, callback)
|
||||
{current <> children <> siblings, seen}
|
||||
key = {parent, name}
|
||||
|
||||
if MapSet.member?(seen, key) do
|
||||
{"", seen}
|
||||
else
|
||||
seen = MapSet.put(seen, key)
|
||||
current = build_dot_current(parent, name, edge_info)
|
||||
{children, seen} = build_dot_graph(name, children, seen, callback)
|
||||
{siblings, seen} = build_dot_graph(parent, nodes, seen, callback)
|
||||
{current <> children <> siblings, seen}
|
||||
end
|
||||
end
|
||||
|
||||
defp build_dot_current(parent, name, edge_info, seen) do
|
||||
key = {parent, name}
|
||||
case seen do
|
||||
%{^key => _} ->
|
||||
{"", seen}
|
||||
%{} when is_nil(edge_info) ->
|
||||
{~s( "#{parent}" -> "#{name}"\n),
|
||||
Map.put(seen, key, true),}
|
||||
%{} ->
|
||||
{~s( "#{parent}" -> "#{name}" [label=\"#{edge_info}\"]\n),
|
||||
Map.put(seen, key, true)}
|
||||
end
|
||||
defp build_dot_current(parent, name, edge_info) do
|
||||
edge_info =
|
||||
if edge_info do
|
||||
~s( [label="#{edge_info}"])
|
||||
end
|
||||
|
||||
parent =
|
||||
unless is_reference(parent) do
|
||||
~s("#{parent}" -> )
|
||||
end
|
||||
|
||||
~s( #{parent}"#{name}"#{edge_info}\n)
|
||||
end
|
||||
|
||||
@doc false
|
||||
|
||||
@@ -87,6 +87,7 @@ defmodule Mix.Tasks.App.TreeTest do
|
||||
|
||||
assert File.read!("app_tree.dot") == """
|
||||
digraph "application tree" {
|
||||
"test"
|
||||
"test" -> "elixir"
|
||||
"test" -> "logger"
|
||||
"logger" -> "elixir"
|
||||
|
||||
@@ -117,6 +117,7 @@ defmodule Mix.Tasks.Deps.TreeTest do
|
||||
|
||||
assert File.read!("deps_tree.dot") == """
|
||||
digraph "dependency tree" {
|
||||
"sample"
|
||||
"sample" -> "git_repo" [label=">= 0.1.0"]
|
||||
"sample" -> "deps_on_git_repo" [label="0.2.0"]
|
||||
}
|
||||
@@ -127,6 +128,7 @@ defmodule Mix.Tasks.Deps.TreeTest do
|
||||
|
||||
assert File.read!("deps_tree.dot") == """
|
||||
digraph "dependency tree" {
|
||||
"sample"
|
||||
"sample" -> "git_repo" [label=">= 0.1.0"]
|
||||
"sample" -> "deps_on_git_repo" [label="0.2.0"]
|
||||
"deps_on_git_repo" -> "git_repo" [label=""]
|
||||
|
||||
@@ -193,6 +193,20 @@ defmodule Mix.Tasks.DepsTest do
|
||||
|
||||
## deps.unlock
|
||||
|
||||
test "cleans and recompiles artifacts if --force given" do
|
||||
Mix.Project.push SuccessfulDepsApp
|
||||
|
||||
in_fixture "deps_status", fn ->
|
||||
Mix.Tasks.Deps.Compile.run []
|
||||
File.touch! "_build/dev/lib/ok/clean-me"
|
||||
|
||||
Mix.Tasks.Deps.Compile.run ["--force"]
|
||||
refute File.exists? "_build/dev/lib/ok/clean-me"
|
||||
end
|
||||
end
|
||||
|
||||
## deps.unlock
|
||||
|
||||
test "unlocks all deps", context do
|
||||
Mix.Project.push DepsApp
|
||||
in_tmp context.test, fn ->
|
||||
|
||||
@@ -241,7 +241,7 @@ defmodule Mix.Tasks.XrefTest do
|
||||
File.write!("lib/a.ex", contents)
|
||||
|
||||
assert capture_io(:stderr, fn ->
|
||||
assert Mix.Task.run("xref", ["--warnings"]) == :error
|
||||
assert Mix.Task.run("xref", ["warnings"]) == :error
|
||||
end) == expected
|
||||
end
|
||||
end
|
||||
@@ -251,7 +251,7 @@ defmodule Mix.Tasks.XrefTest do
|
||||
File.write!("lib/a.ex", contents)
|
||||
|
||||
assert capture_io(:stderr, fn ->
|
||||
assert Mix.Task.run("xref", ["--warnings"]) == :ok
|
||||
assert Mix.Task.run("xref", ["warnings"]) == :ok
|
||||
end) == ""
|
||||
end
|
||||
end
|
||||
@@ -274,7 +274,7 @@ defmodule Mix.Tasks.XrefTest do
|
||||
File.write!("lib/a.ex", contents)
|
||||
|
||||
assert capture_io(fn ->
|
||||
assert Mix.Task.run("xref", ["--unreachable"]) == :error
|
||||
assert Mix.Task.run("xref", ["unreachable"]) == :error
|
||||
end) == expected
|
||||
end
|
||||
end
|
||||
@@ -420,12 +420,10 @@ defmodule Mix.Tasks.XrefTest do
|
||||
|
||||
test "callers: no argument gives error" do
|
||||
in_fixture "no_mixfile", fn ->
|
||||
message =
|
||||
"Could not invoke task \"xref\": 1 error found!\n" <>
|
||||
"--callers : Missing argument of type string"
|
||||
message = "xref doesn't support this command, see mix help xref for more information"
|
||||
|
||||
assert_raise Mix.Error, message, fn ->
|
||||
assert Mix.Task.run("xref", ["--callers"]) == :error
|
||||
assert Mix.Task.run("xref", ["callers"]) == :error
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -433,10 +431,10 @@ defmodule Mix.Tasks.XrefTest do
|
||||
test "callers: gives nice error for quotable but invalid callers spec" do
|
||||
in_fixture "no_mixfile", fn ->
|
||||
message =
|
||||
"xref --callers expects Module, Module.function, or Module.function/arity, got: Module.func(arg)"
|
||||
"xref callers CALLEE expects Module, Module.function, or Module.function/arity, got: Module.func(arg)"
|
||||
|
||||
assert_raise Mix.Error, message, fn ->
|
||||
Mix.Task.run("xref", ["--callers", "Module.func(arg)"])
|
||||
Mix.Task.run("xref", ["callers", "Module.func(arg)"])
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -444,10 +442,10 @@ defmodule Mix.Tasks.XrefTest do
|
||||
test "callers: gives nice error for unquotable callers spec" do
|
||||
in_fixture "no_mixfile", fn ->
|
||||
message =
|
||||
"xref --callers expects Module, Module.function, or Module.function/arity, got: %"
|
||||
"xref callers CALLEE expects Module, Module.function, or Module.function/arity, got: %"
|
||||
|
||||
assert_raise Mix.Error, message, fn ->
|
||||
Mix.Task.run("xref", ["--callers", "%"])
|
||||
Mix.Task.run("xref", ["callers", "%"])
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -458,8 +456,146 @@ defmodule Mix.Tasks.XrefTest do
|
||||
File.write!("lib/b.ex", contents_b)
|
||||
|
||||
assert capture_io(fn ->
|
||||
assert Mix.Task.run("xref", ["--callers", callee]) == :ok
|
||||
assert Mix.Task.run("xref", ["callers", callee]) == :ok
|
||||
end) == expected
|
||||
end
|
||||
end
|
||||
|
||||
## Graph
|
||||
|
||||
test "graph: basic usage" do
|
||||
assert_graph """
|
||||
lib/a.ex
|
||||
└── lib/b.ex
|
||||
└── lib/a.ex
|
||||
lib/b.ex
|
||||
lib/c.ex
|
||||
lib/d.ex
|
||||
└── lib/a.ex (compile)
|
||||
"""
|
||||
end
|
||||
|
||||
test "graph: exclude" do
|
||||
assert_graph ~w[--exclude lib/c.ex --exclude lib/b.ex], """
|
||||
lib/a.ex
|
||||
lib/d.ex
|
||||
└── lib/a.ex (compile)
|
||||
"""
|
||||
end
|
||||
|
||||
test "graph: exclude 1" do
|
||||
assert_graph ~w[--exclude lib/d.ex], """
|
||||
lib/a.ex
|
||||
└── lib/b.ex
|
||||
└── lib/a.ex
|
||||
lib/b.ex
|
||||
lib/c.ex
|
||||
"""
|
||||
end
|
||||
|
||||
test "graph: dot format" do
|
||||
assert_graph ~w[--format dot], true, """
|
||||
digraph "xref graph" {
|
||||
"lib/a.ex"
|
||||
"lib/a.ex" -> "lib/b.ex"
|
||||
"lib/b.ex" -> "lib/a.ex"
|
||||
"lib/b.ex"
|
||||
"lib/c.ex"
|
||||
"lib/d.ex"
|
||||
"lib/d.ex" -> "lib/a.ex" [label="(compile)"]
|
||||
}
|
||||
"""
|
||||
end
|
||||
|
||||
test "graph: source" do
|
||||
assert_graph ~w[--source lib/a.ex], """
|
||||
lib/a.ex
|
||||
└── lib/b.ex
|
||||
└── lib/a.ex
|
||||
"""
|
||||
end
|
||||
|
||||
test "graph: invalid source" do
|
||||
assert_raise Mix.Error, "Source could not be found: lib/a2.ex", fn ->
|
||||
assert_graph ~w[--source lib/a2.ex], ""
|
||||
end
|
||||
end
|
||||
|
||||
test "graph: sink" do
|
||||
assert_graph ~w[--sink lib/b.ex], """
|
||||
lib/a.ex
|
||||
└── lib/b.ex
|
||||
└── lib/a.ex
|
||||
lib/d.ex
|
||||
└── lib/a.ex (compile)
|
||||
"""
|
||||
end
|
||||
|
||||
test "graph: invalid sink" do
|
||||
assert_raise Mix.Error, "Sink could not be found: lib/b2.ex", fn ->
|
||||
assert_graph ~w[--sink lib/b2.ex], ""
|
||||
end
|
||||
end
|
||||
|
||||
test "graph: sink and source is error" do
|
||||
assert_raise Mix.Error, "mix xref graph expects only one of --source and --sink", fn ->
|
||||
assert_graph ~w[--source lib/a.ex --sink lib/b.ex], ""
|
||||
end
|
||||
end
|
||||
|
||||
defp assert_graph(opts \\ [], dot \\ false, expected) do
|
||||
in_fixture "no_mixfile", fn ->
|
||||
File.write! "lib/a.ex", """
|
||||
defmodule A do
|
||||
def a do
|
||||
B.a
|
||||
end
|
||||
|
||||
def b, do: :ok
|
||||
end
|
||||
"""
|
||||
|
||||
File.write! "lib/b.ex", """
|
||||
defmodule B do
|
||||
def a do
|
||||
A.a
|
||||
B.a
|
||||
end
|
||||
end
|
||||
"""
|
||||
|
||||
File.write! "lib/c.ex", """
|
||||
defmodule C do
|
||||
end
|
||||
"""
|
||||
|
||||
File.write! "lib/d.ex", """
|
||||
defmodule :d do
|
||||
A.b
|
||||
end
|
||||
"""
|
||||
|
||||
assert Mix.Task.run("xref", opts ++ ["graph"]) == :ok
|
||||
|
||||
result =
|
||||
if dot do
|
||||
File.read!("xref_graph.dot")
|
||||
else
|
||||
assert "Compiling 4 files (.ex)\nGenerated sample app\n" <> result =
|
||||
receive_until_no_messages([])
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
assert result == expected
|
||||
end
|
||||
end
|
||||
|
||||
defp receive_until_no_messages(acc) do
|
||||
receive do
|
||||
{:mix_shell, :info, [line]} -> receive_until_no_messages([acc, line | "\n"])
|
||||
after
|
||||
0 -> IO.iodata_to_binary(acc)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -135,7 +135,7 @@ defmodule Mix.UmbrellaTest do
|
||||
Mix.env(:test)
|
||||
end
|
||||
|
||||
test "loads umbrella child dependencies in umbrellas" do
|
||||
test "loads umbrella sibling dependencies with :in_umbrella" do
|
||||
in_fixture "umbrella_dep/deps/umbrella", fn ->
|
||||
Mix.Project.in_project :umbrella, ".", fn _ ->
|
||||
File.write! "apps/bar/mix.exs", """
|
||||
@@ -157,6 +157,31 @@ defmodule Mix.UmbrellaTest do
|
||||
end
|
||||
end
|
||||
|
||||
test "finds umbrella sibling dependencies conflicts with :in_umbrella" do
|
||||
in_fixture "umbrella_dep/deps/umbrella", fn ->
|
||||
Mix.Project.in_project :umbrella, ".", fn _ ->
|
||||
File.write! "apps/bar/mix.exs", """
|
||||
defmodule Bar.Mixfile do
|
||||
use Mix.Project
|
||||
|
||||
def project do
|
||||
[app: :bar,
|
||||
version: "0.1.0",
|
||||
deps: [{:foo, in_umbrella: true, env: :unknown}]]
|
||||
end
|
||||
end
|
||||
"""
|
||||
|
||||
assert_raise Mix.Error, fn ->
|
||||
Mix.Tasks.Deps.Get.run []
|
||||
end
|
||||
|
||||
assert_received {:mix_shell, :error, ["Dependencies have diverged:"]}
|
||||
assert_received {:mix_shell, :error, [" the dependency foo in mix.exs is overriding a child dependency" <> _]}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
## Umbrellas as a dependency
|
||||
|
||||
test "list deps for umbrella as dependency" do
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{application, elixir,
|
||||
[{description, "elixir"},
|
||||
{vsn, "1.3.0-rc.1"},
|
||||
{vsn, "1.3.0"},
|
||||
{modules, [
|
||||
elixir
|
||||
]},
|
||||
|
||||
Reference in New Issue
Block a user