Tiny Robots Credo Checks
Highly opinionatedCredo checks used by Tiny Robots. They encode the Elixir / Phoenix / LiveView conventions we rely on across our apps.
These checks are opinionated by design — they enforce the Tiny Robots house style and may not be suitable (or desired) for contribution upstream to mainline Credo. They're published here so our projects can share them, but anyone sharing our conventions is welcome to use them.
Available checks
See the individual modules for full descriptions and examples.
Design
Rbtz.CredoChecks.Design.BareScriptInHeex: Forbids raw<script>tags in HEEx templates — use aphx-hook, the root layout, or import through the asset bundler.Rbtz.CredoChecks.Design.CnInClassList: Enforces correct use of thecn(...)class-merging helper in HEExclass={...}attributes — flagscn([...])with no@assign(wasted), bare lists mixing literal classes with any caller-provided assign (unwrapped, so TwMerge can't dedupe), andcn(...)calls where assigns aren't listed last (TwMerge keeps the last value, so assigns before literals lose their override). Configurable via:helper_name(default"cn").Rbtz.CredoChecks.Design.CustomAliasInRouterScope: Forbids manualaliasstatements inside Phoenixscopeblocks.Rbtz.CredoChecks.Design.PreferLogsterInLib: Forbids the standardLoggermodule in application code underlib/— use Logster instead.Rbtz.CredoChecks.Design.RawHtmlElementsInHeex: Forbids raw<button>,<input>,<select>,<textarea>, and<a>in HEEx — use the app's components instead.Rbtz.CredoChecks.Design.RawSvgInHeex: Forbids raw<svg>tags in HEEx templates.
Readability
Rbtz.CredoChecks.Readability.AtomHttpStatusCodes: Forbids passing integer HTTP status codes toPlug.Conn/ Phoenix — use atoms like:not_found.Rbtz.CredoChecks.Readability.AwkwardPipe: Flags pipe usages that hurt readability without giving chaining benefit — pipe on either side of&&/||/++/<>/in/and/or, pipes into anyKernel.operator form (e.g.Kernel.&&/Kernel.||/Kernel.+/Kernel.-/Kernel.==/Kernel.</Kernel.in), single-step pipes in tuple literals / string interpolation / non-first arg positions / single-line lambdas, any pipe joined in anif/unless/condcondition, single-step pipes on the RHS of<-inside HEEx:for=/forcomprehensions, and single-step pipes wrapped in parens just to dot-access their result.Rbtz.CredoChecks.Readability.ClassAttrFormatting: Enforces HEExclass={...}attributes use list syntax for multiple values, and flags any class-attribute line — single-line or inside a multi-line list — that exceeds:max_line_length(default 98).Rbtz.CredoChecks.Readability.LiveViewCallbackOrder: Enforces the canonical callback order inPhoenix.LiveViewmodules:mount→handle_params→handle_event→handle_info→handle_async→ helpers →render.Rbtz.CredoChecks.Readability.PreferBooleanDataAttrShorthand: Forbidsdata-[name]:bracket-variant syntax for boolean data attributes — usedata-name:instead, reserving brackets for value matching (data-[state=open]:).Rbtz.CredoChecks.Readability.PreferCapture: Encourages the capture syntax (&foo/1,&Mod.foo/2,&(&1 * 2)) overfn x -> ... endwhen the anonymous function just forwards its arguments to another call in the same order, applies a single operator, or partially applies a call.Rbtz.CredoChecks.Readability.PreferNilEquality: Prefersx == nil/x != niloveris_nil(x)/not is_nil(x)inif/unless/cond/caseconditions and inassert/refutearguments.is_nil/1inwhenguards and Ecto query DSL is unaffected.Rbtz.CredoChecks.Readability.PreferSigilSForEscapedQuotes: Encourages the~ssigil for strings that would otherwise need\"escapes.Rbtz.CredoChecks.Readability.PreferToTimeout: Encouragesto_timeout(minute: 15)(Elixir 1.17+) over Erlang's:timer.seconds/1,:timer.minutes/1,:timer.hours/1, and:timer.hms/3.Rbtz.CredoChecks.Readability.RedundantClassAttrWrapping: Flags HEExclass={...}attributes whose wrapping is unnecessary —class={"foo"}/class={["foo"]}should beclass="foo";class={[expr]}should beclass={expr}.Rbtz.CredoChecks.Readability.ShorthandDefMustBeCompact: Forbids the shorthanddef name(args), do: bodyform whose body spans more than one line — switch to ado...endblock when the body has to wrap. Multi-line heads (e.g. nested pattern matches) are fine as long as the body stays on a single line.Rbtz.CredoChecks.Readability.SnakeCaseVariableNumbering: Encourages numbered variables to use a separating underscore:user_1,user_2(notuser1,user2). Configurable via:exclude.Rbtz.CredoChecks.Readability.TopLevelAliasImportRequire: Ensuresalias,import, andrequirestatements appear only at the top level of a module.
Refactor
Rbtz.CredoChecks.Refactor.PreferEctoMigrationHelper: Discourages raw SQLexecute("...")in Ecto migrations when an equivalent migration helper exists (e.g.CREATE TABLE,ALTER TABLE,CREATE INDEX). Raw SQL forCREATE EXTENSION, data backfills, etc. is allowed.Rbtz.CredoChecks.Refactor.PreferForAttrOverForBlock: Prefers:for={item <- @collection}directly on an element over a<%= for ... do %>EEx block when the block wraps a single element.Rbtz.CredoChecks.Refactor.PreferTextColumns: Ensures Ecto migrations use:textrather than:stringfor column types.Rbtz.CredoChecks.Refactor.PreferToFormInTemplates: Forbids passing a raw@changesetto a<form>/<.form>in HEEx — wrap withto_form/2and pass@forminstead.Rbtz.CredoChecks.Refactor.RawHtmlMatchInLiveViewTests: Forbids=~matches against string literals in LiveView test files.Rbtz.CredoChecks.Refactor.RedundantThen: Flags unnecessary uses ofKernel.then/2— when the function passed tothen/2is a simple pass-through or partial application where the piped value already lands at the first-arg position,then/2is pure indirection and can be removed.
Warning
Rbtz.CredoChecks.Warning.AssertNonEmptyBeforeIterate: Requires tests that iterate a collection withassert/refuteinside to first assert the collection is non-empty.Rbtz.CredoChecks.Warning.BooleanDataAttrCoalescesNil: Requires booleandata-*attributes in HEEx to coalesce withnil(e.g.data-disabled={@disabled || nil}) so the attribute is omitted when falsy.Rbtz.CredoChecks.Warning.DisableMigrationLock: Forbids@disable_migration_lock truein Ecto migration files.Rbtz.CredoChecks.Warning.EnumEachInHeex: Forbids<% Enum.each %>and other side-effecting EEx constructs in HEEx templates.Rbtz.CredoChecks.Warning.LiveViewFormCanBeRehydrated: Ensures LiveView forms (withphx-submit) also carryidandphx-changeso state survives deploys and reconnects.Rbtz.CredoChecks.Warning.PhxClickAwayWithoutId: Requires every element withphx-click-awayto also carry anidattribute.Rbtz.CredoChecks.Warning.PhxHookComponentWithoutStableId: Requires function components whose template usesphx-hookto bind the hook target to a stable DOMid— either a literal (id="foo") orid={@name}bound to an attr declared withrequired: trueor a binarydefault:.Rbtz.CredoChecks.Warning.PhxHookWithoutId: Requires every element withphx-hookto also carry anidattribute.Rbtz.CredoChecks.Warning.PhxUpdateStreamWithoutId: Requires every element withphx-update="stream"to also carry anidattribute.Rbtz.CredoChecks.Warning.PreferGetFieldOnChangeset: RequiresEcto.Changeset.get_field/2overchangeset.fieldorchangeset[:field]— direct access returns the original field, not the changeset's current (potentially changed) value, which is a frequent source of subtle bugs.Rbtz.CredoChecks.Warning.PushEventSocketBinding: Requires the result ofpush_event/3to be reassigned tosocket.Rbtz.CredoChecks.Warning.ReqTestWithoutVerifyOnExit: Requires test modules that mock HTTP withReq.Test.stub/expectto callReq.Test.verify_on_exit!/0in asetup/setup_allblock.Rbtz.CredoChecks.Warning.SetMimicGlobal: Forbids enabling Mimic in global mode (set_mimic_global) inside test files.Rbtz.CredoChecks.Warning.SortKeywordValidateResult: RequiresEnum.sort/1betweenKeyword.validate!/2and any binding that pattern-matches the result — unsorted results can silently break pattern matches on keyword list order.Rbtz.CredoChecks.Warning.StringInterpolationInClassAttr: Forbids string interpolation inside HEExclass=attributes — Tailwind's static class extractor can't see interpolated classes, so they silently don't ship in the compiled CSS.Rbtz.CredoChecks.Warning.UnnamedOtpProcess: RequiresDynamicSupervisorandRegistrychild specs to declare a:name.
Installation and configuration
Add
rbtz_credo_checksto yourmix.exsdependencies:def deps do [ {:rbtz_credo_checks, "~> 0.1", only: [:dev, :test], runtime: false} ] endRun
mix deps.get.Add the desired checks to your
.credo.exs:%{ configs: [ %{ checks: %{ enabled: [ {Rbtz.CredoChecks.Design.BareScriptInHeex, []}, {Rbtz.CredoChecks.Design.CnInClassList, []}, {Rbtz.CredoChecks.Design.CustomAliasInRouterScope, []}, {Rbtz.CredoChecks.Design.PreferLogsterInLib, []}, {Rbtz.CredoChecks.Design.RawHtmlElementsInHeex, []}, {Rbtz.CredoChecks.Design.RawSvgInHeex, []}, {Rbtz.CredoChecks.Readability.AtomHttpStatusCodes, []}, {Rbtz.CredoChecks.Readability.AwkwardPipe, []}, {Rbtz.CredoChecks.Readability.ClassAttrFormatting, []}, {Rbtz.CredoChecks.Readability.LiveViewCallbackOrder, []}, {Rbtz.CredoChecks.Readability.PreferBooleanDataAttrShorthand, []}, {Rbtz.CredoChecks.Readability.PreferCapture, []}, {Rbtz.CredoChecks.Readability.PreferSigilSForEscapedQuotes, []}, {Rbtz.CredoChecks.Readability.PreferToTimeout, []}, {Rbtz.CredoChecks.Readability.RedundantClassAttrWrapping, []}, {Rbtz.CredoChecks.Readability.ShorthandDefMustBeCompact, []}, {Rbtz.CredoChecks.Readability.SnakeCaseVariableNumbering, []}, {Rbtz.CredoChecks.Readability.TopLevelAliasImportRequire, []}, {Rbtz.CredoChecks.Refactor.PreferEctoMigrationHelper, []}, {Rbtz.CredoChecks.Refactor.PreferForAttrOverForBlock, []}, {Rbtz.CredoChecks.Refactor.PreferTextColumns, []}, {Rbtz.CredoChecks.Refactor.PreferToFormInTemplates, []}, {Rbtz.CredoChecks.Refactor.RawHtmlMatchInLiveViewTests, []}, {Rbtz.CredoChecks.Refactor.RedundantThen, []}, {Rbtz.CredoChecks.Warning.AssertNonEmptyBeforeIterate, []}, {Rbtz.CredoChecks.Warning.BooleanDataAttrCoalescesNil, []}, {Rbtz.CredoChecks.Warning.DisableMigrationLock, []}, {Rbtz.CredoChecks.Warning.EnumEachInHeex, []}, {Rbtz.CredoChecks.Warning.LiveViewFormCanBeRehydrated, []}, {Rbtz.CredoChecks.Warning.PhxClickAwayWithoutId, []}, {Rbtz.CredoChecks.Warning.PhxHookComponentWithoutStableId, []}, {Rbtz.CredoChecks.Warning.PhxHookWithoutId, []}, {Rbtz.CredoChecks.Warning.PhxUpdateStreamWithoutId, []}, {Rbtz.CredoChecks.Warning.PreferGetFieldOnChangeset, []}, {Rbtz.CredoChecks.Warning.PushEventSocketBinding, []}, {Rbtz.CredoChecks.Warning.ReqTestWithoutVerifyOnExit, []}, {Rbtz.CredoChecks.Warning.SetMimicGlobal, []}, {Rbtz.CredoChecks.Warning.SortKeywordValidateResult, []}, {Rbtz.CredoChecks.Warning.StringInterpolationInClassAttr, []}, {Rbtz.CredoChecks.Warning.UnnamedOtpProcess, []} ] } } ] }
License
MIT. See LICENSE.