Skip to content

Fix type warnings emitted by use Storex.Store on Elixir 1.18+ - #10

Open
drozdzynski wants to merge 3 commits into
masterfrom
fix/elixir-1.18-type-warnings
Open

Fix type warnings emitted by use Storex.Store on Elixir 1.18+#10
drozdzynski wants to merge 3 commits into
masterfrom
fix/elixir-1.18-type-warnings

Conversation

@drozdzynski

@drozdzynski drozdzynski commented Sep 6, 2026

Copy link
Copy Markdown
Member

Problem

Storex.Store.__before_compile__/1 generates a Server module whose init_store/2 runs a case over the return value of the user's init/2 callback, with a clause for every shape the behaviour allows. @store is a concrete module at expansion time, so Elixir's set-theoretic type checker (1.18+) infers the callback's exact return type and proves the remaining clauses dead.

The result is a warning per store module in every downstream project, attributed to the author's own file, pointing at code they did not write. Any project building with mix compile --warnings-as-errors cannot compile against storex on Elixir 1.18 or newer.

Reproduced on this repo's own fixtures:

Elixir / OTP warnings before
1.18.1 / 27.2 8
1.19.5 / 28.4 8
1.20.4 / 29.0.6 12

(1.20 additionally reports the defensive _ -> catch-all as unreachable.)

Fix

Move the branching out of the macro-expanded code into three helpers on Storex.Store:

  • __init__/3
  • __mutation__/6
  • __terminate__/4

They dispatch with apply/3 and are compiled once, against the full callback contract, instead of being specialised per store. Every clause stays reachable, so nothing is reported dead.

The public callback contract is untouched and runtime behaviour is identical:

  • an unsupported init/2 return still raises,
  • an unsupported mutation/5 return still resolves to {:error, "Return value of mutation should be …"},
  • an unmatched mutation name is still rescued from FunctionClauseError and reported as an error.

The mutation/5 site was already immune to the warning, but only incidentally — it used Kernel.apply/3, which is opaque to the checker. Swapping it for a direct @store.mutation(...) call raises the warning count from 12 to 56. Routing it through the shared helper makes that property explicit instead of accidental.

Note: moving only init/2 into a helper that is called with the store's own return value is not enough — the checker propagates the helper's return type per call site and the warning simply relocates to the with in start_link/2. Dispatching via apply/3 inside the helper is what actually removes it.

Tests

New test/storex/store_test.exs covers each valid init/2 shape, each valid mutation/5 shape, both invalid-return paths and the unmatched-mutation path, plus the store server start/mutate path through Storex.Supervisor. Two fixtures added: invalid_init.ex, invalid_mutation.ex.

CI

Matrix gains Elixir 1.18, 1.19 and 1.20 (on ubuntu-24.04; existing rows pinned to ubuntu-22.04 so the OTP 24/25 builds keep resolving), and a mix compile --warnings-as-errors step so this cannot regress silently.

Verified locally

mix compile --force --warnings-as-errors0 warnings, and the non-browser suite → 52 passed, on:

  • Elixir 1.20.4 / OTP 29.0.6
  • Elixir 1.19.5 / OTP 28.4
  • Elixir 1.18.1 / OTP 27.2
  • Elixir 1.16.2 / OTP 26.2.3

Browser tests (Wallaby) were not run locally — no chromedriver on this machine. Elixir 1.14/1.15 were not available locally either; if the new --warnings-as-errors step trips on a pre-existing warning there, those rows may need adjusting.

`Storex.Store.__before_compile__/1` generated a `case` over the return
value of each store's `init/2` callback. Because `@store` is a concrete
module at expansion time, Elixir's type checker (1.18+) proves the
unused clauses dead and warns once per store — attributed to the
downstream author's own file, where it cannot be fixed. Projects
building with `mix compile --warnings-as-errors` could not compile
against storex on Elixir 1.18 or newer.

Move the branching into `Storex.Store.__init__/3`, `__mutation__/6` and
`__terminate__/4`. These dispatch with `apply/3` and are compiled once
against the full callback contract rather than specialised per store, so
every clause stays reachable and no warning is emitted. The public
callback contract and runtime behaviour are unchanged: an unsupported
`init/2` return still raises, an unsupported `mutation/5` return still
resolves to an error tuple, and an unmatched mutation name is still
reported as an error.

The `mutation/5` dispatch was already immune because it used
`Kernel.apply/3`; routing it through the same helper keeps that
property from being lost to a future cleanup.

CI gains Elixir 1.18, 1.19 and 1.20 entries and a
`mix compile --warnings-as-errors` step so this cannot regress.
The matrix stopped at OTP 28, so the newest supported Erlang was not
covered. Elixir 1.20 ships otp-29 builds and OTP 29.0.6 is available for
ubuntu-24.04.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Storex.Store.__mutation__/6 currently rescues all FunctionClauseErrors and can misreport real runtime bugs as “No mutation matching …”, which should be narrowed to only the unmatched-mutation case.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR refactors the use Storex.Store macro-generated server code to avoid Elixir 1.18+ set-theoretic type checker “dead clause” warnings by moving callback-return-shape dispatch into shared Storex.Store helper functions compiled once.

Changes:

  • Add Storex.Store.__init__/3, __mutation__/6, and __terminate__/4 helpers and route the macro-generated Server module through them.
  • Add focused ExUnit coverage for valid/invalid callback return shapes and the “unmatched mutation” path, plus new invalid fixtures.
  • Expand CI to test Elixir 1.18–1.20 and enforce mix compile --warnings-as-errors; bump version to 0.6.2 and update changelog.
File summaries
File Description
lib/storex/store.ex Adds shared dispatch helpers and updates generated server code to call them (reduces type-warning noise on Elixir 1.18+).
test/storex/store_test.exs Adds unit/integration tests for init/mutation dispatch and server behavior.
test/fixtures/stores/invalid_init.ex Fixture store with invalid init/2 return to exercise the raise path.
test/fixtures/stores/invalid_mutation.ex Fixture store with invalid/error mutation returns to exercise error normalization.
CHANGELOG.md Adds 0.6.2 entry documenting the warning fix.
mix.exs Bumps library version to 0.6.2.
package.json Bumps JS package version to 0.6.2.
.github/workflows/main.yml Adds Elixir 1.18–1.20 matrix entries and enforces warnings-as-errors at compile time.
Review details
  • Files reviewed: 8/8 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread lib/storex/store.ex
Comment on lines +62 to +66
rescue
FunctionClauseError ->
{:error,
"No mutation matching #{inspect(name)} with data #{inspect(data)} in store #{inspect(store)}"}
end
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.

2 participants