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
105 changes: 97 additions & 8 deletions lib/mix/lib/mix/tasks/xref.ex
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ defmodule Mix.Tasks.Xref do

The following options are accepted:

* `--exclude` - paths to exclude
* `--exclude` - path to exclude. Can be repeated to exclude multiple paths.

* `--label` - only shows relationships with the given label.
The labels are "compile", "export" and "runtime". By default,
Expand All @@ -90,17 +90,25 @@ defmodule Mix.Tasks.Xref do
with at least one transitive dependency. See "Dependencies types"
section below.

* `--group` - provide comma-separated paths to consider as a group. Dependencies
from and into multiple files of the group are considered a single dependency.
Dependencies between the group elements are ignored. This is useful when you
are computing compile and compile-connected dependencies and you want a
series of files to be treated as one. The group is printed using the first path,
with a `+` suffix. Can be repeated to create multiple groups.

* `--only-direct` - keeps only files with the direct relationship
given by `--label`

* `--only-nodes` - only shows the node names (no edges).
Generally useful with the `--sink` flag

* `--source` - displays all files that the given source file
references (directly or indirectly)
references (directly or indirectly). Can be repeated to display
references from multiple sources.

* `--sink` - displays all files that reference the given file
(directly or indirectly)
(directly or indirectly). Can be repeated.

* `--min-cycle-size` - controls the minimum cycle size on formats
like `stats` and `cycles`
Expand Down Expand Up @@ -260,6 +268,7 @@ defmodule Mix.Tasks.Xref do
exclude: :keep,
fail_above: :integer,
format: :string,
group: :keep,
include_siblings: :boolean,
label: :string,
only_nodes: :boolean,
Expand Down Expand Up @@ -584,6 +593,74 @@ defmodule Mix.Tasks.Xref do

## Graph

defp merge_groups(file_references, comma_separated_groups) do
for group_paths <- comma_separated_groups,
reduce: {file_references, %{}} do
{file_references, aliases} ->
group_paths
|> String.split(",")
|> check_files(file_references, :group)
|> group(file_references, aliases)
end
end

@type_order %{
compile: 0,
export: 1,
nil: 2
}

# Group the given paths.
# In graph theory vocabulary, this is done by vertex identification
# and removal of edges between contracting vertices.
defp group(paths, file_references, aliases) do
group_name = hd(paths) <> "+"
aliases = paths |> Map.new(&{&1, group_name}) |> Map.merge(aliases)

# Merge the references *from* the paths to group
{from_group, file_references} = Map.split(file_references, paths)

file_references =
Map.put(file_references, group_name, merge_references_from_group(from_group))

# Remap the references *to* the merged group
file_references =
Map.new(file_references, fn {file, references} ->
{file, remap_references_to_group(references, aliases, group_name)}
end)

# Remove the resulting reference from the merged group to itself, if there is one
file_references = Map.update!(file_references, group_name, &List.keydelete(&1, group_name, 0))

{file_references, aliases}
end

# Calculate the references from the merged group by concatenating all the references
# from its components; in case of duplicates keep the one with the most important type.
defp merge_references_from_group(file_references_to_merge) do
file_references_to_merge
|> Map.values()
|> Enum.concat()
|> Enum.sort_by(fn {_ref, type} -> @type_order[type] end)
|> Enum.uniq_by(fn {ref, _type} -> ref end)
|> Enum.sort()
end

defp remap_references_to_group(references, aliases, group_name) do
case Enum.split_with(references, fn {ref, _type} -> Map.has_key?(aliases, ref) end) do
{[], _all_references} ->
references

{refs_to_merge, other_refs} ->
type =
refs_to_merge
|> Enum.map(fn {_ref, type} -> type end)
|> Enum.min_by(&@type_order[&1])

Enum.sort([{group_name, type} | other_refs])
end
end

defp exclude(file_references, nil), do: file_references

defp exclude(file_references, excluded) do
Expand Down Expand Up @@ -659,14 +736,22 @@ defmodule Mix.Tasks.Xref do
end

@humanize_option %{
group: "Group files",
source: "Sources",
sink: "Sinks",
exclude: "Excluded files"
}

defp get_files(what, opts, file_references) do
files = Keyword.get_values(opts, what)
defp get_files(what, opts, file_references, aliases) do
files =
for file <- Keyword.get_values(opts, what) do
Map.get(aliases, file, file)
end

check_files(files, file_references, what)
end

defp check_files(files, file_references, what) do
case files -- Map.keys(file_references) do
[_ | _] = missing ->
Mix.raise("#{@humanize_option[what]} could not be found: #{Enum.join(missing, ", ")}")
Expand All @@ -679,9 +764,13 @@ defmodule Mix.Tasks.Xref do
end

defp write_graph(file_references, filter, opts) do
file_references = exclude(file_references, get_files(:exclude, opts, file_references))
sources = get_files(:source, opts, file_references)
sinks = get_files(:sink, opts, file_references)
{file_references, aliases} = merge_groups(file_references, Keyword.get_values(opts, :group))

file_references =
exclude(file_references, get_files(:exclude, opts, file_references, aliases))

sources = get_files(:source, opts, file_references, aliases)
sinks = get_files(:sink, opts, file_references, aliases)

file_references =
cond do
Expand Down
47 changes: 47 additions & 0 deletions lib/mix/test/mix/tasks/xref_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -842,6 +842,53 @@ defmodule Mix.Tasks.XrefTest do
end)
end

test "group with multiple unconnected files" do
assert_graph(~w[--group lib/a.ex,lib/c.ex,lib/e.ex], """
lib/a.ex+
|-- lib/b.ex (compile)
`-- lib/d.ex (compile)
lib/b.ex
`-- lib/a.ex+ (compile)
lib/d.ex
`-- lib/a.ex+
""")
end

test "group with directly dependent files and cycle" do
assert_graph(~w[--group lib/a.ex,lib/b.ex], """
lib/a.ex+
|-- lib/c.ex
`-- lib/e.ex (compile)
lib/c.ex
`-- lib/d.ex (compile)
lib/d.ex
`-- lib/e.ex
lib/e.ex
""")
end

test "multiple groups" do
assert_graph(~w[--group lib/a.ex,lib/b.ex --group lib/c.ex,lib/e.ex], """
lib/a.ex+
`-- lib/c.ex+ (compile)
lib/c.ex+
`-- lib/d.ex (compile)
lib/d.ex
`-- lib/c.ex+
""")
end

test "group with sink" do
assert_graph(~w[--group lib/a.ex,lib/c.ex,lib/e.ex --sink lib/e.ex], """
lib/b.ex
`-- lib/a.ex+ (compile)
|-- lib/b.ex (compile)
`-- lib/d.ex (compile)
lib/d.ex
`-- lib/a.ex+
""")
end

@default_files %{
"lib/a.ex" => """
defmodule A do
Expand Down