Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 53 additions & 24 deletions lib/mix/lib/mix/tasks/xref.ex
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,11 @@ defmodule Mix.Tasks.Xref do

Those options are shared across all modes:

* `--fail-above` - generates a failure if the relevant metric is above the
given threshold. This metric is the number of references, except for
`--format cycles` where it is the number of cycles, and `--format stats`
which has none.

* `--include-siblings` - includes dependencies that have `:in_umbrella` set
to true in the current project in the reports. This can be used to find
callers or to analyze graphs between projects
Expand All @@ -204,6 +209,7 @@ defmodule Mix.Tasks.Xref do
deps_check: :boolean,
elixir_version_check: :boolean,
exclude: :keep,
fail_above: :integer,
format: :string,
include_siblings: :boolean,
label: :string,
Expand Down Expand Up @@ -384,13 +390,21 @@ defmodule Mix.Tasks.Xref do
Mix.shell().info([file, " (", type, ")"])
end

check_failure(:references, length(file_callers), opts[:fail_above])
end

defp check_failure(found, count, max_count)
when not is_nil(max_count) and count > max_count do
Mix.raise("Too many #{found} (found: #{count}, permitted: #{max_count})")
end

defp check_failure(_, _, _) do
:ok
end

defp graph(opts) do
{direct_filter, transitive_filter} = label_filter(opts[:label])
write_graph(file_references(direct_filter, opts), transitive_filter, opts)
:ok
end

## Callers
Expand Down Expand Up @@ -515,35 +529,47 @@ defmodule Mix.Tasks.Xref do
{{file, type}, Enum.sort(children -- excluded)}
end

case opts[:format] do
"dot" ->
Mix.Utils.write_dot_graph!(
"xref_graph.dot",
"xref graph",
Enum.sort(roots),
callback,
opts
)
{found, count} =
case opts[:format] do
"dot" ->
Mix.Utils.write_dot_graph!(
"xref_graph.dot",
"xref graph",
Enum.sort(roots),
callback,
opts
)

"""
Generated "xref_graph.dot" in the current directory. To generate a PNG:
"""
Generated "xref_graph.dot" in the current directory. To generate a PNG:

dot -Tpng xref_graph.dot -o xref_graph.png
dot -Tpng xref_graph.dot -o xref_graph.png

For more options see http://www.graphviz.org/.
"""
|> String.trim_trailing()
|> Mix.shell().info()
For more options see http://www.graphviz.org/.
"""
|> String.trim_trailing()
|> Mix.shell().info()

"stats" ->
print_stats(file_references, opts)
{:references, count_references(file_references)}

"cycles" ->
print_cycles(file_references, opts)
"stats" ->
print_stats(file_references, opts)
{:stats, 0}

_ ->
Mix.Utils.print_tree(Enum.sort(roots), callback, opts)
end
"cycles" ->
{:cycles, print_cycles(file_references, opts)}

_ ->
Mix.Utils.print_tree(Enum.sort(roots), callback, opts)

{:references, count_references(file_references)}
end

check_failure(found, count, opts[:fail_above])
end

defp count_references(file_references) do
Enum.reduce(file_references, 0, fn {_, refs}, total -> total + length(refs) end)
end

defp connected?([_ | _]), do: true
Expand Down Expand Up @@ -698,6 +724,7 @@ defmodule Mix.Tasks.Xref do
case graph |> cycles(opts) |> Enum.sort(:desc) do
[] ->
shell.info("No cycles found")
0

cycles ->
shell.info("#{length(cycles)} cycles found. Showing them in decreasing size:\n")
Expand All @@ -711,6 +738,8 @@ defmodule Mix.Tasks.Xref do

shell.info("")
end

length(cycles)
end
end)
end
Expand Down
82 changes: 62 additions & 20 deletions lib/mix/test/mix/tasks/xref_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -98,27 +98,35 @@ defmodule Mix.Tasks.XrefTest do
end

describe "mix xref callers MODULE" do
@callers_files %{
"lib/a.ex" => """
defmodule A do
def a, do: :ok
end
""",
"lib/b.ex" => """
defmodule B do
def b, do: A.a()
end
"""
}

@callers_output """
Compiling 2 files (.ex)
Generated sample app
lib/b.ex (runtime)
"""

test "prints callers of specified Module" do
files = %{
"lib/a.ex" => """
defmodule A do
def a, do: :ok
end
""",
"lib/b.ex" => """
defmodule B do
def b, do: A.a()
end
"""
}
assert_callers("A", @callers_files, @callers_output)
end

output = """
Compiling 2 files (.ex)
Generated sample app
lib/b.ex (runtime)
"""
test "filter by compile-connected label with fail-above" do
message = "Too many references (found: 1, permitted: 0)"

assert_callers("A", files, output)
assert_raise Mix.Error, message, fn ->
assert_callers(~w[--fail-above 0], "A", @callers_files, @callers_output)
end
end

test "handles aliases" do
Expand Down Expand Up @@ -203,14 +211,14 @@ defmodule Mix.Tasks.XrefTest do
end)
end

defp assert_callers(module, files, expected) do
defp assert_callers(opts \\ [], module, files, expected) do
in_fixture("no_mixfile", fn ->
for {file, contents} <- files do
File.write!(file, contents)
end

capture_io(:stderr, fn ->
assert Mix.Task.run("xref", ["callers", module]) == :ok
assert Mix.Task.run("xref", opts ++ ["callers", module]) == :ok
end)

assert ^expected = receive_until_no_messages([])
Expand Down Expand Up @@ -272,6 +280,23 @@ defmodule Mix.Tasks.XrefTest do
""")
end

test "cycles with `--fail-above`" do
message = "Too many cycles (found: 1, permitted: 0)"

assert_raise Mix.Error, message, fn ->
assert_graph(["--format", "cycles", "--fail-above", "0"], """
1 cycles found. Showing them in decreasing size:

Cycle of length 3:

lib/b.ex
lib/a.ex
lib/b.ex

""")
end
end

test "cycles with min cycle size" do
assert_graph(["--format", "cycles", "--min-cycle-size", "3"], """
No cycles found
Expand Down Expand Up @@ -337,6 +362,23 @@ defmodule Mix.Tasks.XrefTest do
""")
end

test "filter by compile-connected label with fail-above" do
message = "Too many references (found: 3, permitted: 2)"

assert_raise Mix.Error, message, fn ->
assert_graph(~w[--label compile-connected --fail-above 2], """
lib/a.ex
`-- lib/b.ex (compile)
lib/b.ex
`-- lib/d.ex (compile)
lib/c.ex
`-- lib/d.ex (compile)
lib/d.ex
lib/e.ex
""")
end
end

test "filter by compile-direct label" do
assert_graph(~w[--label compile-direct], """
lib/a.ex
Expand Down