Assay - incremental Dialyzer with watch mode, and human readable formatters

Hello everyone,

I’m finally happy enough with my library Assay to share it here. Assay is an incremental Dialyzer wrapper built for CI and local workflows. It reads your existing Dialyzer settings (with caveats) from mix.exs, runs the incremental engine, filters warnings via dialyzer_ignore.exs, and outputs results in formats suited for humans, CI, editors, and LLM-driven tools.

Why Assay?

Running Dialyzer in Elixir projects has long meant slow full rebuilds, cryptic Erlang-style output, and poor integration with CI and editor tooling. Assay tackles these pain points:

  • Incremental by default — leverages OPT26 Dialyzer’s incremental mode so re-analysis is fast after the initial run
  • Watch mode — mix assay.watch debounces file changes and re-runs analysis automatically, giving you continuous type-checking feedback as you code
  • Multiple output formats — text, github (annotations) lsp, llm, and elixir (pretty-prints Erlang terms as Elixir notation via erlex with some really nicely formatted errors)
  • Igniter-powered installer — mix assay.install auto-detects your project/umbrella apps, configures apps and warning_apps, creates ignore files, and optionally generates CI workflows (GitHub Actions or GitLab CI)
  • Good umbrella app support- Assay was built to run on a large umbrella app
  • It’s fast: We’ve seen a 7m Dialyzer run fall to as little as 9s for small changes.

How does this relate to Elixir’s new type system?

Elixir 1.18+ introduced gradual set-theoretic types in the compiler, with significant inference improvements landing in 1.19 and 1.20. This is exciting work but Dialyzer and the new type system are complementary, not competing. If you like Dialyzer you should like Assay.

Dialyzer, by contrast, performs whole-program analysis across module
boundaries. It catches things the compiler currently cannot:

  • Contract violations between callers and callees across modules
  • Functions that will never return due to type mismatches
  • Unreachable code and impossible pattern matches that span module boundaries
  • Violations of your existing @spec annotations (which the compiler doesn’t check yet)

Even as the compiler’s type system matures, there’s a good chance Dialyzer will continue to catch a different class of bugs. Running increment Dialyzer via Assay is cheap insurance.

Quick Start

Using the Igniter installer (recommended):

mix.exs

  [`
    {:assay, “~> 0.5”, runtime: false, only: [:dev, :test]},
    {:igniter, “~> 0.6”}
  ]
end

then run:

mix assay.install --yes
mix assay

Or configure manually — just add the dep and set your config in mix.exs:

def project do
[
  app: :my_app,
  assay: [
    dialyzer: [
      apps: :project_plus_deps,
      warning_apps: :project]
  ]
]
end

Assay supports symbolic selectors like :project, :project_plus_deps, :current, and :current_plus_deps — particularly handy for umbrella projects.

Output Formats

Run with multiple formatters at once for CI pipelines:

mix assay --format github --format sarif

The github formatter produces rich annotations with formatted bodies instead of raw Dialyzer text. The sarif formatter emits SARIF 2.1.0 JSON for standardized CI tool consumption. The llm format outputs
structured JSON optimized for LLM agents.

Links

9 Likes