From ed4f2418d32124e99bbdbce9054f996661638964 Mon Sep 17 00:00:00 2001 From: Iulian Costan Date: Sun, 15 Mar 2026 11:37:40 +0200 Subject: [PATCH 1/2] fix: documentation retrieval for callbacks (#60) The usage_rules.docs task failed to retrieve documentation for Elixir callbacks. This fix adds a mechanism to detect callback hints from IEx and use the b/1 helper for retrieval. - Improve callback detection using a specific regex that includes the input name - Add comprehensive tests for the docs task covering modules, functions, and callbacks --- lib/mix/tasks/usage_rules.docs.ex | 21 ++++++++- test/mix/tasks/usage_rules.docs_test.exs | 60 ++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 test/mix/tasks/usage_rules.docs_test.exs diff --git a/lib/mix/tasks/usage_rules.docs.ex b/lib/mix/tasks/usage_rules.docs.ex index da44036..1b3c0dc 100644 --- a/lib/mix/tasks/usage_rules.docs.ex +++ b/lib/mix/tasks/usage_rules.docs.ex @@ -63,7 +63,26 @@ defmodule Mix.Tasks.UsageRules.Docs do quote do require IEx.Helpers - IEx.Helpers.h(unquote(quoted)) + original_gl = Process.group_leader() + {:ok, cap} = StringIO.open("") + Process.group_leader(self(), cap) + + try do + IEx.Helpers.h(unquote(quoted)) + {_, output} = StringIO.contents(cap) + + # Use regex with case insensitivity to detect the hint about callbacks + if String.match?(output, ~r/No documentation for function #{Regex.escape(unquote(module))} was found,.*callback.*same name/i) do + Process.group_leader(self(), original_gl) + IEx.Helpers.b(unquote(quoted)) + else + Process.group_leader(self(), original_gl) + IO.write(output) + end + after + Process.group_leader(self(), original_gl) + StringIO.close(cap) + end end ) end diff --git a/test/mix/tasks/usage_rules.docs_test.exs b/test/mix/tasks/usage_rules.docs_test.exs new file mode 100644 index 0000000..5763379 --- /dev/null +++ b/test/mix/tasks/usage_rules.docs_test.exs @@ -0,0 +1,60 @@ +# SPDX-FileCopyrightText: 2025 usage_rules contributors +# +# SPDX-License-Identifier: MIT + +defmodule Mix.Tasks.UsageRules.DocsTest do + use ExUnit.Case + + import ExUnit.CaptureIO + + alias Mix.Tasks.UsageRules.Docs + + defp strip_ansi(string) do + String.replace(string, ~r/\e\[[0-9;]*m/, "") + end + + test "shows documentation for a module" do + output = capture_io(fn -> + Docs.run(["Enum"]) + end) |> strip_ansi() + + assert output =~ "Searching local docs for" + assert output =~ "Enum" + assert output =~ "Functions for working with collections" + end + + test "shows documentation for a function" do + output = capture_io(fn -> + Docs.run(["Enum.map/2"]) + end) |> strip_ansi() + + assert output =~ "Searching local docs for" + assert output =~ "Enum.map/2" + assert output =~ "Returns a list where each element is the result of invoking" + end + + test "shows documentation for a callback" do + output = capture_io(fn -> + Docs.run(["GenServer.handle_call"]) + end) |> strip_ansi() + + assert output =~ "Searching local docs for" + assert output =~ "GenServer.handle_call" + assert output =~ "Invoked to handle synchronous call/3 messages" + refute output =~ "No documentation for function GenServer.handle_call was found" + end + + test "handles invalid expressions" do + assert_raise Mix.Error, ~r/Invalid module or function/, fn -> + Docs.run(["invalid expression"]) + end + end + + test "handles non-existent modules" do + output = capture_io(fn -> + Docs.run(["NonExistentModule"]) + end) + + assert output =~ "Could not load module NonExistentModule" + end +end From 31b5a445d233e74c203457d6935fabbcd61b8365 Mon Sep 17 00:00:00 2001 From: Iulian Costan Date: Tue, 17 Mar 2026 13:25:45 +0200 Subject: [PATCH 2/2] fix: test and formatting --- lib/mix/tasks/usage_rules.docs.ex | 5 +++- test/mix/tasks/usage_rules.docs_test.exs | 33 ++++++++++++++---------- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/lib/mix/tasks/usage_rules.docs.ex b/lib/mix/tasks/usage_rules.docs.ex index 1b3c0dc..b169f3d 100644 --- a/lib/mix/tasks/usage_rules.docs.ex +++ b/lib/mix/tasks/usage_rules.docs.ex @@ -72,7 +72,10 @@ defmodule Mix.Tasks.UsageRules.Docs do {_, output} = StringIO.contents(cap) # Use regex with case insensitivity to detect the hint about callbacks - if String.match?(output, ~r/No documentation for function #{Regex.escape(unquote(module))} was found,.*callback.*same name/i) do + if String.match?( + output, + ~r/No documentation for function #{Regex.escape(unquote(module))} was found,.*callback.*same name/i + ) do Process.group_leader(self(), original_gl) IEx.Helpers.b(unquote(quoted)) else diff --git a/test/mix/tasks/usage_rules.docs_test.exs b/test/mix/tasks/usage_rules.docs_test.exs index 5763379..9084d61 100644 --- a/test/mix/tasks/usage_rules.docs_test.exs +++ b/test/mix/tasks/usage_rules.docs_test.exs @@ -14,9 +14,11 @@ defmodule Mix.Tasks.UsageRules.DocsTest do end test "shows documentation for a module" do - output = capture_io(fn -> - Docs.run(["Enum"]) - end) |> strip_ansi() + output = + capture_io(fn -> + Docs.run(["Enum"]) + end) + |> strip_ansi() assert output =~ "Searching local docs for" assert output =~ "Enum" @@ -24,9 +26,11 @@ defmodule Mix.Tasks.UsageRules.DocsTest do end test "shows documentation for a function" do - output = capture_io(fn -> - Docs.run(["Enum.map/2"]) - end) |> strip_ansi() + output = + capture_io(fn -> + Docs.run(["Enum.map/2"]) + end) + |> strip_ansi() assert output =~ "Searching local docs for" assert output =~ "Enum.map/2" @@ -34,13 +38,15 @@ defmodule Mix.Tasks.UsageRules.DocsTest do end test "shows documentation for a callback" do - output = capture_io(fn -> - Docs.run(["GenServer.handle_call"]) - end) |> strip_ansi() + output = + capture_io(fn -> + Docs.run(["GenServer.handle_call"]) + end) + |> strip_ansi() assert output =~ "Searching local docs for" assert output =~ "GenServer.handle_call" - assert output =~ "Invoked to handle synchronous call/3 messages" + assert output =~ "Invoked to handle synchronous" refute output =~ "No documentation for function GenServer.handle_call was found" end @@ -51,9 +57,10 @@ defmodule Mix.Tasks.UsageRules.DocsTest do end test "handles non-existent modules" do - output = capture_io(fn -> - Docs.run(["NonExistentModule"]) - end) + output = + capture_io(fn -> + Docs.run(["NonExistentModule"]) + end) assert output =~ "Could not load module NonExistentModule" end