AshCredo.Check.Refactor.RaisingCall (ash_credo v0.13.0)

Copy Markdown View Source

Basics

This check is disabled by default.

Learn how to enable it via .credo.exs.

This check has a base priority of low and works with any version of Elixir.

Explanation

Flags Ash bang calls that raise on {:error, _} instead of returning {:ok, _} | {:error, _} tuples - awkward in code paths that need to translate Ash errors into HTTP responses, GraphQL payloads, or job-runner outcomes.

# Flagged
posts = Ash.read!(MyApp.Post)

# Preferred
case Ash.read(MyApp.Post) do
  {:ok, posts} -> ...
  {:error, error} -> ...
end

Requires the host project to be compiled with Ash loaded - same contract as UseCodeInterface, MissingCodeInterface, etc. When Ash isn't loadable the check emits one :ash_missing diagnostic and becomes a no-op.

Two detectors run when the gate passes:

  1. Ash.*! bangs - top-level (Ash.read!, Ash.create!) and nested (Ash.Filter.parse!, Ash.Expr.eval!).

  2. Code-interface bangs - MyApp.Blog.create_post! from a domain's code_interface, or MyApp.Blog.Post.create_post! from a resource's code_interface, including calculation interfaces. Confirmed against Ash.Resource.Info / Ash.Domain.Info - calls to non-Ash modules are silently skipped, as are user modules that aren't yet compiled (they'd be indistinguishable from arbitrary third-party bangs).

Bang-only APIs - bangs that have no non-bang counterpart, like Ash.stream! or Ash.Seed.seed! - are skipped by default. Probing module.__info__(:functions) for the trimmed-! name tells us the suggested replacement wouldn't exist, so a default suggestion would name a nonexistent function. To surface these calls anyway under a generic "ensure failures are handled" message, opt in with flag_bang_only_apis: true. Calls passing stream?: true to a read code-interface are also silently skipped, because the non-bang variant rejects streaming.

The "Prefer Mod.fun" suggestion is worded based on the non-bang counterpart's typespec: tuple-returning APIs (Ash.read, Ash.create, ...) get the "match on {:ok, _} | {:error, _}" message, while helpers whose non-bang twin returns something else (e.g. Ash.Resource.Info.primary_action/2 returns action | nil) get a conservative "handle the returned value explicitly" message.

Use excluded_functions to silence specific bangs by {module, :fun!} tuple:

excluded_functions: [
  {MyApp.Blog, :archive_all_posts!}
]

Test directories are excluded by default since bang versions in tests are idiomatic ("crash loudly on unexpected errors"). Override excluded_paths (e.g. to []) if you want to flag bang calls in tests too. Entries can be path segments ("test" excludes any file under a test/ directory) or full file paths ("priv/seeds.exs" excludes that exact file).

Both detectors resolve aliases lexically. The common alias __MODULE__.Foo pattern is resolved using the enclosing defmodule, so Foo.archive!() and MyApp.Blog.Foo.archive!() are treated identically. Unsupported call shapes that can never be flagged: apply/3, variable modules (mod.fun!()), macro-generated bang names, and bare __MODULE__.fun!() (no alias).

Check-Specific Parameters

Use the following parameters to configure this check:

:excluded_functions

Bang functions to allow without flagging, given as {module, :fun!} tuples. Defaults to [] - bang-only APIs (those with no non-bang counterpart) are detected dynamically via module.__info__(:functions), so no curated allowlist is needed.

This parameter defaults to [].

:excluded_paths

Paths or regexes to skip. Defaults to test directories, where bang versions are idiomatic.

This parameter defaults to [~r/\/test\//, "test"].

:flag_bang_only_apis

When true, also flag bangs that have no non-bang counterpart (e.g. Ash.stream!, Ash.Seed.seed!) with a generic 'ensure failures are handled' message. Defaults to false because the suggested non-bang twin doesn't exist for these calls; opt in only if your team policy is 'no bare bang calls'.

This parameter defaults to false.

General Parameters

Like with all checks, general params can be applied.

Parameters can be configured via the .credo.exs config file.