0.8.0 - 2026-07-14
Added
ForgeCredoChecks.OneModulePerFile: flags every literaldefmoduleafter the first in a source file, including nested and reopened module declarations, while ignoring quoted module AST generated by macros. Files undertest/and files ending in_test.exsare excluded by default because tests sometimes need nested fixture modules; configure:excluded_pathsor set it to[]to enforce the rule there too.ForgeCredoChecks.MultilineStringConcat: flags a multi-line<>chain of two or more plain string literals ("a" <> "b" <> "c"spread across source lines) and suggests an Elixir heredoc. Only fires when every operand is a static string binary and the chain spans more than one source line, so"prefix " <> some_var, single-line<>of literals, and chains with an interpolated operand ("a" <> "b#{x}") are left alone. Emits exactly one issue per chain by neutralizing the matched subtree.ForgeCredoChecks.NoApplicationGetEnvInLib: flagsApplication.get_env/2,3insidelib/(default scope), whereApplication.compile_env/2,3is the correct reader for compile-time values.get_envsilently returnsnilor a stale value when the app env is not yet loaded, a bug class that is silent inmixand surfaces only in a packaged release.compile_envis excluded by construction (only:get_envis matched), not by allowlist. Supportsincluded_paths(default[~r"^lib/"]) to narrow scope andallowed_paths(default[]) to permit the rarelib/module that legitimately reads runtime config.fetch_env/fetch_env!are deliberately out of scope.ForgeCredoChecks.TelemetryControlFlow: flags:telemetry.attach/attach_manywith an inline anonymous-function handler that performs control flow (send/2,:erlang.send/2,GenServer.call/cast) instead of observation-only recording. Now also detects same-file function captures (&func/arity,&__MODULE__.func/arity) by resolving the captured function's body within the same source file and inspecting it for control-flow calls. A raising telemetry handler is auto-detached by the runtime, so using the bus to deliver load-bearing signals fails silently; the idiomatic mechanism isPhoenix.PubSub. Cross-module captures and MFA-tuple handlers remain out of scope. Supports:excluded_pathsas a shrinking migration bridge;# credo:disable-for-next-lineis honored for intentional exceptions.ForgeCredoChecks.NoAnyOrTermTypes: flags broadany()/term()uses in specs, callbacks, macrocallbacks, public/private/opaque type definitions, and nested type expressions so Dialyzer keeps useful shape information. Each finding carries the banned type's own column, so multiple broad types on a single spec or type line are reported as distinct, individually-actionable locations.ForgeCredoChecks.TaintedSourceInspection: flags=~,String.contains?,Regex.*, andCode.eval_stringwhen they inspect text tainted fromFile.read!/File.stream!of non-test.ex/.exssource paths, while leaving terminal artifact reads quiet. Supports:excluded_pathsfor shrinking migration bridges.ForgeCredoChecks.TimingAndPrivateStateGuard: flags actualProcess.sleep/1and:sys.replace_state/2call nodes while ignoring string, atom, and capture mentions. Supports:excluded_pathsfor shrinking migration bridges.
0.7.0 - 2026-07-03
Added
ForgeCredoChecks.LargeStruct: flags structs with 32 or more fields, which lose the BEAM's flat-map optimization.ForgeCredoChecks.UnsupervisedSpawn: flags rawspawn/Task.startof long-lived processes that belong under a supervisor. Supports:excluded_pathsto skip files (e.g. test support) by path.ForgeCredoChecks.NamespaceTrespassing: flags defining your own modules inside a dependency's namespace.ForgeCredoChecks.NoInlineRegex: flags inline~r/~Rregex sigils inside function bodies. Define regexes as module attributes so placement is machine-enforced instead of reviewer-attention work.ForgeCredoChecks.PortProducerBoundary: flags external-model producers (subprocess/HTTP dispatch) outside the set of declared boundary modules, so a new producer must be added to an explicit allow-list.ForgeCredoChecks.NoSourceInspectionInTest: flags tests that verify behaviour by reading or AST-parsing production source —File.read!/File.stream!/Code.string_to_quotedof alib/*.expath (literal or carried as data) — instead of exercising the real function. Catches both the literal-read and the data-carried-path +Code.string_to_quotedforms, while sparing data-only fixtures and meta-lints that parse only test source.
0.6.0
Added
ForgeCredoChecks.MapGetWithOr: flagsMap.get(_, _) || fallback. UseMap.get/3for local defaults, or normalize the data once at the ingestion boundary.ForgeCredoChecks.ChainedMapGet: flagsMap.get(_, _) || Map.get(_, _)at higher priority because fishing across multiple maps or key spellings signals an unresolved input-shape problem.
0.5.0
Fixed
InconsistentParamNames: clauses are now scoped per module. Previously,def/defpclauses were grouped by{name, arity}across the entire file, so twodefimplblocks for the same protocol (with idiomatically different parameter names likeofficevsvan_stop) produced false positives. Clauses in separatedefmodule,defimpl, ordefprotocolblocks are now never cross-compared. Eliminates ~49% of false positives observed in production evaluation.InconsistentParamNames: multiple inconsistent positions in the same function now produce a single aggregated issue instead of one issue per position.NoUnnecessaryCatchAllRaise: single-clause functions that raise are no longer flagged. The rule's premise ("FunctionClauseError already provides better diagnostics") only applies when there is at least one other clause to fall through from. Stubs, arity redirects, and deliberately-raising test doubles are no longer false positives.NoUnnecessaryCatchAllRaise: sibling-clause counting is now scoped per module (same fix asInconsistentParamNames).NoUnnecessaryCatchAllRaise: message softened from "Remove this clause" to "Consider removing this clause, or narrowing the guard if the raise message documents valid inputs."NoUnnecessaryCatchAllRaise: check explanation now documents how to exclude test files via.credo.exsfiles: %{excluded: [...]}configuration.
0.4.0
Five new checks ported from credence anti-pattern rules, adapted to integrate with the standard Credo runner (so credo:disable-for-* comments work and the rules participate in mix credo --strict):
ForgeCredoChecks.InconsistentParamNames: flags multi-clause functions where the same positional argument has different base names across clauses (e.g.currentin one clause,previn another). Drift makes readers question correctness. Literal and destructuring patterns at a position cause that position to be skipped.ForgeCredoChecks.NoKernelShadowing: flags=/fn/defbinding sites that shadow commonKernelfunctions (max,min,length,elem,hd,tl,abs,round,trunc,div,rem,tuple_size,map_size,byte_size,bit_size). Calls likemax(max, other)become ambiguous to readers — rename the variable.ForgeCredoChecks.NoUnnecessaryCatchAllRaise: flagsdef/defpclauses where every argument is a wildcard AND the body is exactlyraise(...). Elixir's built-inFunctionClauseErroralready names the function and the failing arguments — a hand-written catch-all that raises a hardcoded message throws that signal away.ForgeCredoChecks.NoCaseTrueFalse: flagscase <bool_expr> do true -> ...; false -> ... end(and variants with_as one clause). Theif/elseform makes the truthy branch obvious without a clause-scan.caseon a plain variable is NOT flagged — that's typically a legitimate tristate match.ForgeCredoChecks.NoKernelOpInPipeline: flagspipeline |> Kernel.<op>(arg)for comparison and boolean operators (==,!=,===,!==,<,>,<=,>=,and,or). Use the operator in infix position. Arithmetic operators (+,-,*,/) are NOT flagged — they have legitimate uses in pipelines.
Auto-fix is NOT implemented for any of these checks; credo doesn't run auto-fixers, and source mutation introduces risk that's better handled by the operator at each call site.
0.3.0
Three new checks codifying conventions for the with macro:
ForgeCredoChecks.WithBareBinding: every clause in awithchain must use<-, never=. Smuggled=bindings bypass the fall-through control flow that giveswithits purpose.ForgeCredoChecks.WithElseClauses: flagswithblocks whoseelseexceeds:max_clauses(default1, configurable). Wideelseblocks become dispatch tables on step-specific error shapes; normalize each step's return in a helper instead.ForgeCredoChecks.WithResultTag: flags<-clauses whose atom-tagged LHS is outside:allowed_atoms(default[:ok, :error], configurable). Codebases that use richer control-flow vocabulary (:found,:retry,:locked) extend the allowlist rather than disabling the check.
Check feedback rewritten for agent readers:
- Messages now lead with "Replace X with Y" instead of passive descriptions like "X is more efficient than Y", so an LLM reading a Credo issue gets a concrete edit instruction.
- Every explanation got a
## Why / ## How to fix / ## What NOT to dostructure with concrete BEFORE/AFTER snippets. - The four
Enum-chain checks (MapReject,MapRejectNil,FilterMap,RejectMap) now recommend comprehensions first,Enum.flat_map/2second (where the transform is naturally 0-or-more), andEnum.reduce/3only as a last resort. Thereduce + reversepattern is explicitly called out as an anti-pattern: paying a second pass just to undo the order an accumulator imposed is exactly the tax comprehensions exist to avoid.
0.2.0
First Hex release.
Adds four checks beyond the original two-pass Enum chain set:
ForgeCredoChecks.MapNewFromInto:Enum.map |> Enum.into(%{}, ...)becomesMap.new/2ForgeCredoChecks.MapNewFromReduce:Enum.reduce(_, %{}, &Map.put(acc, k, v))becomesMap.new/2ForgeCredoChecks.ReverseListFirst:xs |> Enum.reverse() |> List.first()becomesList.last(xs)ForgeCredoChecks.SortListFirst:Enum.sort |> List.firstbecomesEnum.min/Enum.max/*_by
Carried over from 0.1.x:
ForgeCredoChecks.FilterMap:Enum.filter |> Enum.mapForgeCredoChecks.RejectMap:Enum.reject |> Enum.mapForgeCredoChecks.MapReject:Enum.map |> Enum.rejectForgeCredoChecks.MapRejectNil:Enum.map |> Enum.reject(&is_nil/1)