ref:552f599224a0d3eb23233de6763625474bf9f6ff

Enforce Dialyzer in CI (#1)

## Summary - Fix all 4 existing Dialyzer warnings (type mismatches in diff.ex, pack/reader.ex, protocol/receive_pack.ex) - Add `mix dialyzer` step to `.anvil.yml` CI pipeline - Any new Dialyzer warning now fails the build Closes #20 ## Changes - **diff.ex**: `diff_blobs` spec was missing the `{:ok, :binary, sizes}` return variant - **pack/reader.ex**: Exposed `read_object/4` with external resolver param, making the REF_DELTA code path reachable (previously dead code) - **protocol/receive_pack.ex**: Added missing `pre_receive_hook` and `cmd_buffer` fields to `state()` type; targeted `@dialyzer {:no_opaque, init: 2}` for known MapSet opaque type limitation - **.anvil.yml**: Added dialyzer CI step depending on compile ## Test plan - [x] `mix dialyzer` passes with 0 errors locally - [x] `mix compile --warnings-as-errors` passes - [x] `mix format --check-formatted` passes - [x] `mix test` — 529 tests, 0 failures
SHA: 552f599224a0d3eb23233de6763625474bf9f6ff
Author: Anvil <noreply@anvil.fangorn.io>
Date: 2026-04-09 04:35
Parents: ee9b4d1
4 files changed +31 -1
Type
.anvil.yml +7 −0
@@ -21,6 +21,13 @@
export MIX_HOME=/workspace/.mix
mix format --check-formatted
depends_on: [deps]
- name: dialyzer
run: |
set -e
export MIX_HOME=/workspace/.mix
mkdir -p priv/plts
mix dialyzer
depends_on: [compile]
- name: test
run: |
set -e
lib/ex_git_objectstore/diff.ex +3 −1
@@ -69,6 +69,8 @@
Returns hunks with context lines.
"""
@spec diff_blobs(Repo.t(), String.t() | nil, String.t() | nil, keyword()) ::
{:ok, [hunk()]} | {:error, term()}
{:ok, [hunk()]}
| {:ok, :binary, %{old_size: non_neg_integer(), new_size: non_neg_integer()}}
| {:error, term()}
def diff_blobs(repo, old_sha, new_sha, opts \\ []) do
context = Keyword.get(opts, :context, 3)
lib/ex_git_objectstore/pack/reader.ex +15 −0
@@ -132,6 +132,21 @@
do_read_object(pack_data, offset, cache, 0, nil)
end
@doc """
Read object with a pre-populated cache and an external resolver for REF_DELTA bases.
The `external_resolver` is a function `(sha :: binary()) -> {:ok, {atom(), binary()}} | {:error, term()}`
used to look up base objects not found within the pack itself (thin packs).
"""
@spec read_object(binary(), non_neg_integer(), map(), (binary() ->
{:ok, {atom(), binary()}}
| {:error, term()})) ::
{:ok, {atom(), binary()}} | {:error, term()}
def read_object(pack_data, offset, cache, external_resolver)
when is_function(external_resolver, 1) do
do_read_object(pack_data, offset, cache, 0, external_resolver)
end
# -- Entry parsing (sequential scan) --
defp parse_entries(_rest, 0, acc, cache, _abs_offset), do: {:ok, Enum.reverse(acc), cache}
lib/ex_git_objectstore/protocol/receive_pack.ex +6 −0
@@ -50,9 +50,11 @@
@type state :: %__MODULE__{
repo: Repo.t(),
pre_receive_hook: (Repo.t(), [command()] -> :ok | {:error, term()}) | nil,
phase: :advertise | :commands | :pack | :done,
commands: [command()],
client_caps: MapSet.t(),
cmd_buffer: binary(),
pack_buffer: binary(),
result: nil | :ok | {:error, term()}
}
@@ -72,6 +74,10 @@
Create a new receive-pack state machine and generate the ref advertisement.
Returns `{advertisement_data, state}`.
"""
# Dialyzer contract_with_opaque: MapSet.t() is opaque and Dialyzer cannot
# reconcile the struct constructor's inferred type with the spec. This is a
# known Dialyzer limitation, not a real type error.
@dialyzer {:no_opaque, init: 2}
@spec init(Repo.t(), keyword()) :: {binary(), state()}
def init(%Repo{} = repo, opts \\ []) do
pre_receive_hook = Keyword.get(opts, :pre_receive_hook)