Skip to content

Commit d9d3f79

Browse files
author
José Valim
committed
Do not allow loops in dot graph formats
1 parent e236830 commit d9d3f79

File tree

5 files changed

+88
-77
lines changed

5 files changed

+88
-77
lines changed

lib/mix/lib/mix/tasks/app.tree.ex

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -49,19 +49,18 @@ defmodule Mix.Tasks.App.Tree do
4949
excluded = Keyword.get_values(opts, :exclude) |> Enum.map(&String.to_atom/1)
5050
excluded = @default_excluded ++ excluded
5151

52-
app_tree_callback =
53-
fn {type, app} ->
54-
load(app)
55-
{"#{app}#{type(type)}", children_for(app, excluded)}
56-
end
52+
callback = fn {type, app} ->
53+
load(app)
54+
{{app, type(type)}, children_for(app, excluded)}
55+
end
5756

5857
if opts[:format] == "dot" do
59-
app_tree = Mix.Utils.build_dot_graph("application tree", [{:normal, app}], app_tree_callback, opts)
60-
filename = "app_tree.dot"
61-
File.write!(filename, app_tree <> "\n")
62-
Mix.shell.info("Generated `#{filename}` in current directory")
58+
Mix.Utils.write_dot_graph!("app_tree.dot", "application tree",
59+
{:normal, app}, callback, opts)
60+
Mix.shell.info "Generated \"app_tree.dot\" in current directory.\n" <>
61+
"You can use http://www.graphviz.org/ to open it."
6362
else
64-
Mix.Utils.print_tree([{:normal, app}], app_tree_callback, opts)
63+
Mix.Utils.print_tree({:normal, app}, callback, opts)
6564
end
6665
end
6766

@@ -79,6 +78,6 @@ defmodule Mix.Tasks.App.Tree do
7978
Enum.map(apps, &{:normal, &1}) ++ Enum.map(included_apps, &{:included, &1})
8079
end
8180

82-
defp type(:normal), do: ""
83-
defp type(:included), do: " (included)"
81+
defp type(:normal), do: nil
82+
defp type(:included), do: "(included)"
8483
end

lib/mix/lib/mix/tasks/deps.tree.ex

Lines changed: 39 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,6 @@ defmodule Mix.Tasks.Deps.Tree do
3939

4040
deps_opts = if only = opts[:only], do: [env: :"#{only}"], else: []
4141
deps = Mix.Dep.loaded(deps_opts)
42-
excluded = Keyword.get_values(opts, :exclude) |> Enum.map(&String.to_atom/1)
43-
top_level = Enum.filter(deps, & &1.top_level)
4442

4543
root =
4644
case args do
@@ -51,9 +49,23 @@ defmodule Mix.Tasks.Deps.Tree do
5149
find_dep(deps, app) || Mix.raise("could not find dependency #{app}")
5250
end
5351

54-
deps_tree_callback =
55-
fn
56-
%Mix.Dep{app: app} = dep ->
52+
if opts[:format] == "dot" do
53+
callback = callback(&format_dot/1, deps, opts)
54+
Mix.Utils.write_dot_graph!("deps_tree.dot", "dependency tree", root, callback, opts)
55+
Mix.shell.info "Generated \"deps_tree.dot\" in current directory.\n" <>
56+
"You can use http://www.graphviz.org/ to open it."
57+
else
58+
callback = callback(&format_tree/1, deps, opts)
59+
Mix.Utils.print_tree(root, callback, opts)
60+
end
61+
end
62+
63+
defp callback(formatter, deps, opts) do
64+
excluded = Keyword.get_values(opts, :exclude) |> Enum.map(&String.to_atom/1)
65+
top_level = Enum.filter(deps, & &1.top_level)
66+
67+
fn
68+
%Mix.Dep{app: app} = dep ->
5769
deps =
5870
# Do not show dependencies if they were
5971
# already shown at the top level
@@ -62,42 +74,42 @@ defmodule Mix.Tasks.Deps.Tree do
6274
else
6375
find_dep(deps, app).deps
6476
end
65-
{format_dep(dep, opts), exclude(deps, excluded)}
77+
{formatter.(dep), exclude(deps, excluded)}
6678
app ->
67-
{Atom.to_string(app), exclude(top_level, excluded)}
68-
end
69-
70-
if opts[:format] == "dot" do
71-
deps_tree = Mix.Utils.build_dot_graph("dependency tree", [root], deps_tree_callback, opts)
72-
filename = "deps_tree.dot"
73-
File.write!(filename, deps_tree <> "\n")
74-
Mix.shell.info("Generated `#{filename}` in current directory")
75-
else
76-
Mix.Utils.print_tree([root], deps_tree_callback, opts)
79+
{{Atom.to_string(app), nil}, exclude(top_level, excluded)}
7780
end
7881
end
7982

8083
defp exclude(deps, excluded) do
8184
Enum.reject deps, & &1.app in excluded
8285
end
8386

84-
defp format_dep(%{app: app, scm: scm, requirement: requirement, opts: deps_opts}, opts) do
87+
defp format_dot(%{app: app, requirement: requirement, opts: opts}) do
8588
override =
86-
if deps_opts[:override] do
87-
"#{IO.ANSI.bright} *override*#{IO.ANSI.normal}"
89+
if opts[:override] do
90+
" *override*"
8891
else
8992
""
9093
end
91-
if opts[:format] == "dot" do
92-
{app, "#{requirement(requirement)}#{override}"}
93-
else
94-
"#{app}#{requirement(requirement)} (#{scm.format(deps_opts)})#{override}"
95-
end
94+
95+
requirement = requirement && requirement(requirement)
96+
{app, "#{requirement}#{override}"}
97+
end
98+
99+
defp format_tree(%{app: app, scm: scm, requirement: requirement, opts: opts}) do
100+
override =
101+
if opts[:override] do
102+
IO.ANSI.format([:bright, " *override*"])
103+
else
104+
""
105+
end
106+
107+
requirement = requirement && "#{requirement(requirement)} "
108+
{app, "#{requirement}(#{scm.format(opts)})#{override}"}
96109
end
97110

98-
defp requirement(nil), do: ""
99-
defp requirement(%Regex{} = regex), do: " #{inspect regex}"
100-
defp requirement(binary) when is_binary(binary), do: " #{binary}"
111+
defp requirement(%Regex{} = regex), do: "#{inspect regex}"
112+
defp requirement(binary) when is_binary(binary), do: binary
101113

102114
defp find_dep(deps, app) do
103115
Enum.find(deps, & &1.app == app)

lib/mix/lib/mix/utils.ex

Lines changed: 33 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -149,21 +149,22 @@ defmodule Mix.Utils do
149149
must either return `{printed, children}` tuple or
150150
`false` if the given node must not be printed.
151151
"""
152-
@spec print_tree([term], (term -> {String.t, [term]}), Keyword.t) :: :ok
153-
def print_tree(nodes, callback, opts \\ []) do
152+
@spec print_tree(term, (term -> {String.t, [term]}), Keyword.t) :: :ok
153+
def print_tree(root, callback, opts \\ []) do
154154
pretty =
155155
case Keyword.get(opts, :format) do
156156
"pretty" -> true
157157
"plain" -> false
158158
_ -> elem(:os.type, 0) != :win32
159159
end
160-
print_tree(nodes, [], pretty, callback)
160+
print_tree([root], [], pretty, callback)
161161
end
162162

163163
defp print_tree([], _depth, _pretty, _callback), do: :ok
164164
defp print_tree([node | nodes], depth, pretty, callback) do
165-
{print, children} = callback.(node)
166-
Mix.shell.info("#{depth(pretty, depth)}#{prefix(pretty, depth, nodes)}#{print}")
165+
{{name, info}, children} = callback.(node)
166+
space = if info, do: " ", else: ""
167+
Mix.shell.info("#{depth(pretty, depth)}#{prefix(pretty, depth, nodes)}#{name}#{space}#{info}")
167168
print_tree(children, [(nodes != []) | depth], pretty, callback)
168169
print_tree(nodes, depth, pretty, callback)
169170
end
@@ -190,35 +191,34 @@ defmodule Mix.Utils do
190191
must either return `{printed, children}` tuple or
191192
`false` if the given node must not be printed.
192193
"""
193-
@spec build_dot_graph(String.t, [term], (term -> {String.t, [term]}), Keyword.t) :: :ok
194-
def build_dot_graph(title, nodes, callback, _opts \\ []) do
195-
{parent_name, _} = callback.(hd(nodes))
196-
"digraph \"#{title}\" {\n" <>
197-
do_build_dot_graph(parent_name, nodes, callback) <>
198-
"}"
199-
end
200-
201-
defp do_build_dot_graph(_parent, [], _callback), do: ""
202-
defp do_build_dot_graph(parent, [node | nodes], callback) do
203-
{name, children} = callback.(node)
204-
205-
parent_name = case parent do
206-
{parent_name, _} -> parent_name
207-
parent_name -> parent_name
194+
@spec write_dot_graph!(Path.t, String.t, term, (term -> {String.t, [term]}), Keyword.t) :: :ok
195+
def write_dot_graph!(path, title, root, callback, _opts \\ []) do
196+
{{parent, _}, children} = callback.(root)
197+
{dot, _} = build_dot_graph(parent, children, %{}, callback)
198+
File.write! path, "digraph \"#{title}\" {\n#{dot}}\n"
199+
end
200+
201+
defp build_dot_graph(_parent, [], seen, _callback), do: {"", seen}
202+
defp build_dot_graph(parent, [node | nodes], seen, callback) do
203+
{{name, edge_info}, children} = callback.(node)
204+
{current, seen} = build_dot_current(parent, name, edge_info, seen)
205+
{children, seen} = build_dot_graph(name, children, seen, callback)
206+
{siblings, seen} = build_dot_graph(parent, nodes, seen, callback)
207+
{current <> children <> siblings, seen}
208+
end
209+
210+
defp build_dot_current(parent, name, edge_info, seen) do
211+
key = {parent, name}
212+
case seen do
213+
%{^key => _} ->
214+
{"", seen}
215+
%{} when is_nil(edge_info) ->
216+
{~s( "#{parent}" -> "#{name}"\n),
217+
Map.put(seen, key, true),}
218+
%{} ->
219+
{~s( "#{parent}" -> "#{name}" [label=\"#{edge_info}\"]\n),
220+
Map.put(seen, key, true)}
208221
end
209-
210-
if parent != name do
211-
case name do
212-
{node_name, edge_info} ->
213-
~s( "#{parent_name}" -> "#{node_name}" [label=\"#{edge_info}\"]\n)
214-
node_name ->
215-
~s( "#{parent_name}" -> "#{node_name}"\n)
216-
end
217-
else
218-
""
219-
end <>
220-
do_build_dot_graph(name, children, callback) <>
221-
do_build_dot_graph(parent, nodes, callback)
222222
end
223223

224224
@doc false

lib/mix/test/mix/tasks/app.tree_test.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ defmodule Mix.Tasks.App.TreeTest do
9292
"logger" -> "elixir"
9393
"test" -> "app_deps_sample"
9494
"app_deps_sample" -> "app_deps2_sample"
95-
"app_deps2_sample" -> "app_deps4_sample (included)"
95+
"app_deps2_sample" -> "app_deps4_sample" [label="(included)"]
9696
"app_deps_sample" -> "app_deps3_sample"
9797
}
9898
"""

lib/mix/test/mix/tasks/deps.tree_test.exs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,8 @@ defmodule Mix.Tasks.Deps.TreeTest do
117117

118118
assert File.read!("deps_tree.dot") == """
119119
digraph "dependency tree" {
120-
"sample" -> "git_repo" [label=" >= 0.1.0"]
121-
"sample" -> "deps_on_git_repo" [label=" 0.2.0"]
120+
"sample" -> "git_repo" [label=">= 0.1.0"]
121+
"sample" -> "deps_on_git_repo" [label="0.2.0"]
122122
}
123123
"""
124124

@@ -127,8 +127,8 @@ defmodule Mix.Tasks.Deps.TreeTest do
127127

128128
assert File.read!("deps_tree.dot") == """
129129
digraph "dependency tree" {
130-
"sample" -> "git_repo" [label=" >= 0.1.0"]
131-
"sample" -> "deps_on_git_repo" [label=" 0.2.0"]
130+
"sample" -> "git_repo" [label=">= 0.1.0"]
131+
"sample" -> "deps_on_git_repo" [label="0.2.0"]
132132
"deps_on_git_repo" -> "git_repo" [label=""]
133133
}
134134
"""

0 commit comments

Comments
 (0)