diff --git a/lib/credo/check.ex b/lib/credo/check.ex index 15b7fc15d..91f2b7299 100644 --- a/lib/credo/check.ex +++ b/lib/credo/check.ex @@ -895,6 +895,38 @@ defmodule Credo.Check do @__default_enabled_checks__ end + @doc false + def all_loaded_checks do + # Source credo's own checks from the curated `standard_checks/0` list + # rather than `:code.all_loaded/0` — until something references an + # unused check module the BEAM hasn't loaded it, so the all-loaded set + # silently misses checks that nothing has yet pulled in. That's the + # root of `MissingCheckInConfig` failing to flag unused checks (#1278). + # + # We supplement with check modules from any *other* loaded application + # (plugins, custom check libraries) by reading their module list from + # `:application.get_key/2`, which doesn't depend on prior code loading. + Enum.uniq(standard_checks() ++ external_check_modules()) + end + + defp external_check_modules do + for {app, _, _} <- Application.loaded_applications(), + app != :credo, + {:ok, modules} <- [:application.get_key(app, :modules)], + module <- modules, + Code.ensure_loaded?(module), + implements_credo_check?(module), + do: module + end + + defp implements_credo_check?(module) do + function_exported?(module, :module_info, 1) and + module.module_info(:attributes) + |> Keyword.get_values(:behaviour) + |> List.flatten() + |> Enum.member?(__MODULE__) + end + @doc false # TODO: decide whether this should live here or in `Credo.Execution`. def mentioned_checks(exec) do diff --git a/test/credo/check_all_loaded_checks_test.exs b/test/credo/check_all_loaded_checks_test.exs new file mode 100644 index 000000000..f47b6fdcd --- /dev/null +++ b/test/credo/check_all_loaded_checks_test.exs @@ -0,0 +1,25 @@ +defmodule Credo.Check.AllLoadedChecksTest do + use ExUnit.Case, async: true + + # `all_loaded_checks/0` used to source its result from `:code.all_loaded/0`, + # so any check module nothing had yet referenced was silently missing — + # which is exactly what made `MissingCheckInConfig` fail to flag unused + # checks (#1278). The fix sources credo's own checks from the curated + # `standard_checks/0` list and supplements with check modules discovered + # via `:application.get_key/2` on every other loaded application, so the + # result is independent of which modules happen to be loaded. + + test "returns every standard credo check regardless of load order" do + all = Credo.Check.all_loaded_checks() + missing = Credo.Check.standard_checks() -- all + + assert missing == [], + "expected all standard checks to be included in `all_loaded_checks/0`, " <> + "but these were missing: #{inspect(missing)}" + end + + test "result contains no duplicates" do + all = Credo.Check.all_loaded_checks() + assert all == Enum.uniq(all) + end +end