From 37d9cb15f163c6adc808e8230460289cd48ac782 Mon Sep 17 00:00:00 2001 From: Wojtek Mach Date: Tue, 25 Nov 2025 13:59:02 +0100 Subject: [PATCH 01/10] mix help app:APP: Load current Mix project Prior to this patch, it was impossible to get help for the current app: nimble_csv$ mix help app:nimble_csv Application nimble_csv does not exist or is not loaded It also helps with projects with depenendencies when they haven't been compiled yet: req$ mix deps.get && mix help app:finch # mix deps.compile haven't been run yet Application finch does not exist or is not loaded Now it works: nimble_csv$ mix help app:nimble_csv Compiling 1 file (.ex) Generated nimble_csv app # NimbleCSV NimbleCSV is a small and fast parsing and dumping library. # NimbleCSV.RFC4180 A CSV parser that uses comma as separator and double-quotes as escape according to RFC4180. # NimbleCSV.Spreadsheet A parser with spreadsheet friendly settings. The task continues to work as expected outside of a Mix project: /tmp$ mix help app:nimble_csv Application nimble_csv does not exist or is not loaded /tmp$ mix help app:public_key # :public_key API module for public-key infrastructure. --- lib/mix/lib/mix/tasks/help.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/mix/lib/mix/tasks/help.ex b/lib/mix/lib/mix/tasks/help.ex index e3895756e7..f543915a6d 100644 --- a/lib/mix/lib/mix/tasks/help.ex +++ b/lib/mix/lib/mix/tasks/help.ex @@ -112,6 +112,7 @@ defmodule Mix.Tasks.Help do def run(["app:" <> app]) do loadpaths!() + Mix.Task.run("app.config") app = String.to_atom(app) # If the application is not available, attempt to load it from Erlang/Elixir From 403f4fb71356e176bd7f9fb6def424098b96f5e8 Mon Sep 17 00:00:00 2001 From: Wojtek Mach Date: Tue, 25 Nov 2025 14:12:22 +0100 Subject: [PATCH 02/10] up --- lib/mix/lib/mix/tasks/help.ex | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/mix/lib/mix/tasks/help.ex b/lib/mix/lib/mix/tasks/help.ex index f543915a6d..828a992b77 100644 --- a/lib/mix/lib/mix/tasks/help.ex +++ b/lib/mix/lib/mix/tasks/help.ex @@ -112,7 +112,11 @@ defmodule Mix.Tasks.Help do def run(["app:" <> app]) do loadpaths!() - Mix.Task.run("app.config") + + if Mix.Project.get() do + Mix.Task.run("compile") + end + app = String.to_atom(app) # If the application is not available, attempt to load it from Erlang/Elixir From 4536218b9bd41edf3725b84fc49f661385f62895 Mon Sep 17 00:00:00 2001 From: Wojtek Mach Date: Thu, 27 Nov 2025 10:54:34 +0100 Subject: [PATCH 03/10] Try to compile project --- lib/mix/lib/mix/tasks/help.ex | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/mix/lib/mix/tasks/help.ex b/lib/mix/lib/mix/tasks/help.ex index 828a992b77..7aa44fecbf 100644 --- a/lib/mix/lib/mix/tasks/help.ex +++ b/lib/mix/lib/mix/tasks/help.ex @@ -124,7 +124,12 @@ defmodule Mix.Tasks.Help do try do Mix.ensure_application!(app) rescue - _ -> :ok + _ -> + if Mix.Project.get() do + Mix.Task.run("compile") + end + + Mix.ensure_application!(app) end end From 9f0998221b6bb082624d9e7952c1af8fb0ae6b3d Mon Sep 17 00:00:00 2001 From: Wojtek Mach Date: Thu, 27 Nov 2025 10:57:18 +0100 Subject: [PATCH 04/10] up --- lib/mix/lib/mix/tasks/help.ex | 30 ++++++++++++---------------- lib/mix/test/mix/tasks/help_test.exs | 5 +++-- 2 files changed, 16 insertions(+), 19 deletions(-) diff --git a/lib/mix/lib/mix/tasks/help.ex b/lib/mix/lib/mix/tasks/help.ex index 7aa44fecbf..10a78e09b7 100644 --- a/lib/mix/lib/mix/tasks/help.ex +++ b/lib/mix/lib/mix/tasks/help.ex @@ -133,24 +133,20 @@ defmodule Mix.Tasks.Help do end end - if modules = Application.spec(app, :modules) do - for module <- modules, - not (module |> Atom.to_string() |> String.starts_with?("Elixir.Mix.Tasks.")), - {:docs_v1, _, _, "text/markdown", %{"en" => <>}, _, _} <- - [Code.fetch_docs(module)] do - leading = doc |> String.split(["\n\n", "\r\n\r\n"], parts: 2) |> hd() - "# #{inspect(module)}\n#{leading}\n" - end - |> case do - [] -> - Mix.shell().error("No modules with accessible documentation found for #{app}") + for module <- Application.spec(app, :modules), + not (module |> Atom.to_string() |> String.starts_with?("Elixir.Mix.Tasks.")), + {:docs_v1, _, _, "text/markdown", %{"en" => <>}, _, _} <- + [Code.fetch_docs(module)] do + leading = doc |> String.split(["\n\n", "\r\n\r\n"], parts: 2) |> hd() + "# #{inspect(module)}\n#{leading}\n" + end + |> case do + [] -> + Mix.shell().error("No modules with accessible documentation found for #{app}") - listing -> - docs = listing |> Enum.sort() |> Enum.join() - IO.ANSI.Docs.print(docs, "text/markdown", ansi_opts()) - end - else - Mix.shell().error("Application #{app} does not exist or is not loaded") + listing -> + docs = listing |> Enum.sort() |> Enum.join() + IO.ANSI.Docs.print(docs, "text/markdown", ansi_opts()) end end diff --git a/lib/mix/test/mix/tasks/help_test.exs b/lib/mix/test/mix/tasks/help_test.exs index c48f8c17a0..d889d000da 100644 --- a/lib/mix/test/mix/tasks/help_test.exs +++ b/lib/mix/test/mix/tasks/help_test.exs @@ -217,8 +217,9 @@ defmodule Mix.Tasks.HelpTest do test "help app:UNKNOWN", context do in_tmp(context.test, fn -> - Mix.Tasks.Help.run(["app:foobar"]) - assert_received {:mix_shell, :error, ["Application foobar does not exist or is not loaded"]} + assert_raise Mix.Error, ~r/The application "foobar" could not be found/, fn -> + Mix.Tasks.Help.run(["app:foobar"]) + end end) end From a0ddfb9faece4c1fdb065becb708902e3a974e58 Mon Sep 17 00:00:00 2001 From: Wojtek Mach Date: Thu, 27 Nov 2025 11:02:49 +0100 Subject: [PATCH 05/10] up --- lib/mix/lib/mix/tasks/help.ex | 32 +++++++++++++++------------- lib/mix/test/mix/tasks/help_test.exs | 5 ++--- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/lib/mix/lib/mix/tasks/help.ex b/lib/mix/lib/mix/tasks/help.ex index 10a78e09b7..e66b320704 100644 --- a/lib/mix/lib/mix/tasks/help.ex +++ b/lib/mix/lib/mix/tasks/help.ex @@ -128,25 +128,27 @@ defmodule Mix.Tasks.Help do if Mix.Project.get() do Mix.Task.run("compile") end - - Mix.ensure_application!(app) end end - for module <- Application.spec(app, :modules), - not (module |> Atom.to_string() |> String.starts_with?("Elixir.Mix.Tasks.")), - {:docs_v1, _, _, "text/markdown", %{"en" => <>}, _, _} <- - [Code.fetch_docs(module)] do - leading = doc |> String.split(["\n\n", "\r\n\r\n"], parts: 2) |> hd() - "# #{inspect(module)}\n#{leading}\n" - end - |> case do - [] -> - Mix.shell().error("No modules with accessible documentation found for #{app}") + if modules = Application.spec(app, :modules) do + for module <- modules, + not (module |> Atom.to_string() |> String.starts_with?("Elixir.Mix.Tasks.")), + {:docs_v1, _, _, "text/markdown", %{"en" => <>}, _, _} <- + [Code.fetch_docs(module)] do + leading = doc |> String.split(["\n\n", "\r\n\r\n"], parts: 2) |> hd() + "# #{inspect(module)}\n#{leading}\n" + end + |> case do + [] -> + Mix.shell().error("No modules with accessible documentation found for #{app}") - listing -> - docs = listing |> Enum.sort() |> Enum.join() - IO.ANSI.Docs.print(docs, "text/markdown", ansi_opts()) + listing -> + docs = listing |> Enum.sort() |> Enum.join() + IO.ANSI.Docs.print(docs, "text/markdown", ansi_opts()) + end + else + Mix.shell().error("Application #{app} does not exist or is not loaded") end end diff --git a/lib/mix/test/mix/tasks/help_test.exs b/lib/mix/test/mix/tasks/help_test.exs index d889d000da..c48f8c17a0 100644 --- a/lib/mix/test/mix/tasks/help_test.exs +++ b/lib/mix/test/mix/tasks/help_test.exs @@ -217,9 +217,8 @@ defmodule Mix.Tasks.HelpTest do test "help app:UNKNOWN", context do in_tmp(context.test, fn -> - assert_raise Mix.Error, ~r/The application "foobar" could not be found/, fn -> - Mix.Tasks.Help.run(["app:foobar"]) - end + Mix.Tasks.Help.run(["app:foobar"]) + assert_received {:mix_shell, :error, ["Application foobar does not exist or is not loaded"]} end) end From 17bd6fddbb45acda5563227e5559191c013858bd Mon Sep 17 00:00:00 2001 From: Wojtek Mach Date: Thu, 27 Nov 2025 11:05:00 +0100 Subject: [PATCH 06/10] up --- lib/mix/lib/mix/tasks/help.ex | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/lib/mix/lib/mix/tasks/help.ex b/lib/mix/lib/mix/tasks/help.ex index e66b320704..4ae35d1bc4 100644 --- a/lib/mix/lib/mix/tasks/help.ex +++ b/lib/mix/lib/mix/tasks/help.ex @@ -121,13 +121,8 @@ defmodule Mix.Tasks.Help do # If the application is not available, attempt to load it from Erlang/Elixir if is_nil(Application.spec(app, :vsn)) do - try do - Mix.ensure_application!(app) - rescue - _ -> - if Mix.Project.get() do - Mix.Task.run("compile") - end + if Mix.Project.get() do + Mix.Task.run("compile") end end From edc11cf01fb710853d07d576503884a0ca655cb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 27 Nov 2025 11:07:20 +0100 Subject: [PATCH 07/10] Apply suggestions from code review --- lib/mix/lib/mix/tasks/help.ex | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/mix/lib/mix/tasks/help.ex b/lib/mix/lib/mix/tasks/help.ex index 4ae35d1bc4..6cd6c3f218 100644 --- a/lib/mix/lib/mix/tasks/help.ex +++ b/lib/mix/lib/mix/tasks/help.ex @@ -113,16 +113,18 @@ defmodule Mix.Tasks.Help do def run(["app:" <> app]) do loadpaths!() - if Mix.Project.get() do - Mix.Task.run("compile") - end - app = String.to_atom(app) # If the application is not available, attempt to load it from Erlang/Elixir if is_nil(Application.spec(app, :vsn)) do - if Mix.Project.get() do - Mix.Task.run("compile") + try do + Mix.ensure_application!(app) + rescue + _ -> + if Mix.Project.get() do + Mix.Task.run("compile") + end + end end end From 433ba8f0deadc8edab8457d91e6c41abd9e13385 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 27 Nov 2025 11:07:40 +0100 Subject: [PATCH 08/10] Update lib/mix/lib/mix/tasks/help.ex --- lib/mix/lib/mix/tasks/help.ex | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/mix/lib/mix/tasks/help.ex b/lib/mix/lib/mix/tasks/help.ex index 6cd6c3f218..5a27818cc5 100644 --- a/lib/mix/lib/mix/tasks/help.ex +++ b/lib/mix/lib/mix/tasks/help.ex @@ -125,7 +125,6 @@ defmodule Mix.Tasks.Help do Mix.Task.run("compile") end end - end end if modules = Application.spec(app, :modules) do From 73ffff23578e7a78340400ef1691464c189c3e17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Thu, 27 Nov 2025 11:08:32 +0100 Subject: [PATCH 09/10] Update lib/mix/lib/mix/tasks/help.ex --- lib/mix/lib/mix/tasks/help.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/mix/lib/mix/tasks/help.ex b/lib/mix/lib/mix/tasks/help.ex index 5a27818cc5..18e15b2df3 100644 --- a/lib/mix/lib/mix/tasks/help.ex +++ b/lib/mix/lib/mix/tasks/help.ex @@ -121,6 +121,7 @@ defmodule Mix.Tasks.Help do Mix.ensure_application!(app) rescue _ -> + # Otherwise, it may be a dep or the current project not yet compiled if Mix.Project.get() do Mix.Task.run("compile") end From e34dcd83b8864d93ecae4cb51dc71d7a288eda85 Mon Sep 17 00:00:00 2001 From: Wojtek Mach Date: Thu, 27 Nov 2025 15:16:46 +0100 Subject: [PATCH 10/10] Add test --- lib/mix/test/mix/tasks/help_test.exs | 35 ++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/lib/mix/test/mix/tasks/help_test.exs b/lib/mix/test/mix/tasks/help_test.exs index c48f8c17a0..e29e47e656 100644 --- a/lib/mix/test/mix/tasks/help_test.exs +++ b/lib/mix/test/mix/tasks/help_test.exs @@ -222,6 +222,41 @@ defmodule Mix.Tasks.HelpTest do end) end + defmodule ExampleProject do + def project do + [ + app: :sample, + version: "0.1.0" + ] + end + end + + test "help app:APP for current project", context do + in_tmp(context.test, fn -> + Mix.Project.push(ExampleProject) + File.mkdir_p!("lib") + + File.write!("lib/example.ex", ~s''' + defmodule Example do + @moduledoc """ + This is an example module. + """ + end + ''') + + output = + capture_io(fn -> + Mix.Tasks.Help.run(["app:sample"]) + end) + + assert output =~ """ + # Example + + This is an example module. + """ + end) + end + test "help Elixir MODULE", context do in_tmp(context.test, fn -> output =