Skip to content

Fix MissingCheckInConfig missing unused checks (#1278)#1285

Open
dl-alexandre wants to merge 3 commits into
rrrene:masterfrom
dl-alexandre:fix/missing-check-in-config-discovery
Open

Fix MissingCheckInConfig missing unused checks (#1278)#1285
dl-alexandre wants to merge 3 commits into
rrrene:masterfrom
dl-alexandre:fix/missing-check-in-config-discovery

Conversation

@dl-alexandre
Copy link
Copy Markdown

Closes #1278.

Credo.Check.all_loaded_checks/0 was sourcing its result via Credo.Code.loaded_modules_implementing/1, which iterates :code.all_loaded/0. Because nothing references an unused check module, the BEAM never loads it — so it never appeared in the result, and MissingCheckInConfig silently failed to flag it. The bug is hidden in credo's own test suite, where every check module gets loaded incidentally; the reporter (@PragTob) diagnosed this correctly in the issue.

This sources credo's own checks from the curated standard_checks/0 list instead. Two side benefits beyond fixing the original bug:

  1. UnusedOperation is no longer flagged as missing. It's not in standard_checks/0 (it's a generic check that does nothing without modules: [...] configuration), so it falls out naturally. That matches @rrrene's note on the issue ("I'd also probably exclude it from being warned about, as the check does nothing without configuration").

  2. Plugin checks are now discovered without depending on load order. A new private external_check_modules/0 walks Application.loaded_applications/0 and reads each app's module list from :application.get_key/2, then filters to modules implementing the Credo.Check behaviour. That doesn't depend on :code.all_loaded/0 either, so plugin checks show up whether or not anything has referenced them yet.

The only caller of Credo.Check.all_loaded_checks/0 in lib/ is MissingCheckInConfig, so the contract change is well-scoped.

Added a small unit test asserting that all_loaded_checks/0 returns a superset of standard_checks/0 regardless of load order, and that the result has no duplicates. CI will verify the full matrix.

`Credo.Check.all_loaded_checks/0` was sourcing its result through
`Credo.Code.loaded_modules_implementing/1`, which iterates
`:code.all_loaded/0`. Because nothing references an unused check
module, the BEAM never loads it — so it never appeared in the result,
and `MissingCheckInConfig` silently failed to flag it. The bug was
hidden in credo's own test suite, where every check module is loaded
incidentally.

Source credo's own checks from the curated `standard_checks/0` list
instead. That has the side benefit of excluding helper checks like
`Credo.Check.Warning.UnusedOperation` from the warning — they do
nothing without explicit configuration and shouldn't be flagged as
"missing from config" (per @rrrene's note on rrrene#1278).

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.

Refs rrrene#1278.
@rrrene
Copy link
Copy Markdown
Owner

rrrene commented May 17, 2026

Hi, thx for trying to solve this.

But isn't this approach (going apps -> modules -> Code.ensure_loaded?(module)) just :code.all_loaded/0 with extra steps?

@dl-alexandre
Copy link
Copy Markdown
Author

Hi René, thanks for the question.

The key distinction is the source of the candidate modules, not the loading step.

:code.all_loaded/0 returns only modules already resident in the VM. Because unused checks are never referenced, the BEAM never loads them — so they are absent from the result, which is exactly why MissingCheckInConfig missed them (#1278).

:application.get_key(app, :modules) reads the .app manifest of each loaded application, which declares every module the app ships with, regardless of whether anything has loaded it yet. Code.ensure_loaded?/1 is then used only as a filter to verify the module exists and implements Credo.Check.

For Credo's own checks we skip dynamic discovery entirely and use the static standard_checks/0 list from .credo.exs. The manifest walk is solely for discovering plugin / external checks.

If you'd prefer to avoid any loading side-effects for plugins, we could switch that path to :beam_lib.chunks/2 to inspect the :behaviour attribute directly from the .beam file on disk.

Does that address the concern?

@rrrene
Copy link
Copy Markdown
Owner

rrrene commented May 25, 2026

We can't load the chunks because we can not assume the project is compiled.

Could you explain in your own words what the new test from your last commit actually tests?

@dl-alexandre
Copy link
Copy Markdown
Author

You're right, the :beam_lib.chunks/2 follow-up was the wrong direction because it assumes compiled BEAM files are available. I reverted that commit.

The test I added there only proved that a compiled module listed in an .app manifest could be discovered without loading it. That does not cover the uncompiled project case Credo has to support, so it was not good evidence for this PR.

I think the core fix here is still the static standard_checks/0 path for Credo's own checks, which addresses #1278 without depending on load order. The external/plugin discovery part is the questionable bit, and I'm happy to scope that down if you'd prefer this PR to only cover Credo's built-in checks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MissingCheckInConfig doesn't find unused checks, as it relies on the checks being loaded

2 participants