Skip to content

chore(lint): enable forcetypeassert and check every assertion - #926

Merged
gaborage merged 6 commits into
mainfrom
chore/forcetypeassert-handle-assertions
Aug 8, 2026
Merged

chore(lint): enable forcetypeassert and check every assertion#926
gaborage merged 6 commits into
mainfrom
chore/forcetypeassert-handle-assertions

Conversation

@gaborage

@gaborage gaborage commented Aug 8, 2026

Copy link
Copy Markdown
Owner

What

Enables forcetypeassert (uber-go "Handle Type Assertion Failures") and fixes all 62
non-test sites; the 156 _test.go findings are excluded alongside errcheck/gosec, on
the same reasoning that a failed assertion in a test panics loudly, which is what the test
wants. Where a container allowed it the assertion was deleted rather than guarded —
sync.Map became a typed map under the mutex that already serialized every mutator, and
newRouteGroup/executeJob narrowed to their concrete unexported types.

Impact

No exported signature changes. cache/testing's MockCache is the largest behavioral
surface: Stats/Has/AllKeys/Dump now take the mutex they previously bypassed, and
AllKeys returns an empty non-nil slice rather than nil — a downstream assert.Nil
would need assert.Empty.

Verification

Guarding instead of deleting would have left unreachable !ok branches, whose mutants the
diff-scoped gate cannot kill; the surviving sites use shapes gremlins generates no mutant
for. make mutate: all mutants on changed lines killed.

Summary by CodeRabbit

  • Bug Fixes

    • Improved reliability during concurrent cache, tenant, and metadata operations.
    • Replaced potential runtime panics with clearer errors for invalid internal values.
    • Added support for defined pointer types in request handling.
    • Enhanced test setup failures with detailed query, row, and type information.
  • Refactor

    • Simplified internal request, routing, scheduling, and mock-value handling.
    • Improved cache initialization, cleanup, and diagnostic output consistency.

forcetypeassert enforces the uber-go guide's "Handle Type Assertion
Failures". It measured 218 findings: 156 in _test.go and 62 outside.

The 156 test findings join the existing `path: _test\.go` exclusion block
alongside errcheck/gosec, for the same reason — a failed assertion in a
test panics loudly, which is the outcome the test wants.

The 62 non-test sites are fixed rather than excluded, in four shapes:

- testing/mocks (44): a package-local generic `arg[T](arguments, method,
  index)` replaces `arguments.Get(i).(T)`. These files are on the public
  import surface, so the `v, _ := ...` rewrite was rejected: it swaps a
  precise panic for a nil return that explodes later somewhere unrelated.
  The helper keeps the panic and adds the mocked method, the return
  index, and the actual type to the message. Note the pre-existing
  `arguments.Get(0) == nil` guards are nil-only, never type checks, so
  they did not make the assertions they precede safe.

- Container invariants (11): deleted rather than guarded. MockCache's
  `sync.Map` becomes `map[string]*cacheEntry` under the mutex that
  already serialized every mutator (Stats/Has/AllKeys/Dump now take it
  too, closing a lock-discipline gap); tenantstore's per-tenant lock map
  becomes `map[string]*sync.Mutex` guarded by the existing mu; the column
  registry's two assertions move behind typed load/loadOrStore helpers.
  resourcepool's singleflight and container/list values cannot be typed
  away, so those three use comma-ok in shapes that generate no mutant.

- Values the function just built (4): newRouteGroup returns *routeGroup
  (unexported, zero API impact), the JOSE error body is hoisted into a
  local instead of re-asserted out of the map it was just put in, and
  the generic request allocator discards ok with a comment naming why
  the assertion cannot fail — no branch and no allocation, so the
  ADR-026 alloc guards stay green on the typed-handler hot path.

- Caller-supplied data (2): NewDatabaseWithData now derives columns from
  the converted rows, leaving one assertion that panics naming the query
  key and offending row index, matching the NOSONAR panic beside it.

scheduler.executeJob took the exported JobContext and asserted it to
*jobContextImpl, so a foreign implementation would panic inside the very
function whose job is to contain panics. Both it and newJobContext now
use the concrete unexported type; the exported surface is unchanged.

Verified by planting the same violation twice: in a non-test file it is
reported, in a _test.go file the new exclusion suppresses it. Non-test
findings are now 0.
@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The PR enables forcetypeassert, replaces unchecked assertions with typed validation, and updates cache and tenant synchronization. It also changes mock cache storage and internal constructors to use concrete types.

Changes

Assertion safety and test infrastructure

Layer / File(s) Summary
Typed test return handling
.golangci.yml, testing/fixtures/database.go, testing/mocks/*
The linter configuration enables forcetypeassert. Mock return extraction uses the generic arg helper. Database fixtures validate row types.
Mutex-protected mock cache
cache/testing/*
MockCache uses a mutex-protected map. Cache operations, cleanup, statistics, key enumeration, dumping, and expired-entry setup use the new storage model.
Typed metadata cache and tenant locks
database/internal/columns/registry.go, internal/tenantstore/tenantstore.go
Column metadata cache access uses typed helpers and atomic storage. Tenant initialization uses per-tenant mutexes stored in a protected map.
Concrete runtime paths and checked assertions
internal/resourcepool/resourcepool.go, scheduler/*, server/*
Internal constructors and job execution use concrete types. Resource-pool assertions check result types. Server request and error-envelope paths remove unchecked assertions. Tests cover defined pointer request types.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

Poem

I check each type with careful paws,
And guard the cache with locking laws.
Mock returns now state their case,
Concrete paths keep types in place.
— A rabbit 🐇

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main change: enabling forcetypeassert and addressing type assertions across the codebase.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chore/forcetypeassert-handle-assertions

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@server/handler.go`:
- Around line 353-355: The request allocation path around newRequestAllocator
must preserve defined pointer types such as RequestPtr. Convert elem to
ra.elemType before the assertion assigning request, or explicitly reject defined
pointer types in newRequestAllocator, ensuring validateNotNil receives the
allocated request instead of nil.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: c78dbaff-d1e8-457f-bdae-65a86623b64f

📥 Commits

Reviewing files that changed from the base of the PR and between 6e7717e and cebcfc3.

📒 Files selected for processing (19)
  • .golangci.yml
  • cache/testing/mock_cache.go
  • cache/testing/mock_cache_test.go
  • database/internal/columns/registry.go
  • internal/resourcepool/resourcepool.go
  • internal/tenantstore/tenantstore.go
  • scheduler/job.go
  • scheduler/module.go
  • server/handler.go
  • server/jose.go
  • server/route_registrar.go
  • server/route_registrar_test.go
  • testing/fixtures/database.go
  • testing/mocks/arg.go
  • testing/mocks/database.go
  • testing/mocks/query_builder.go
  • testing/mocks/registry.go
  • testing/mocks/statement.go
  • testing/mocks/transaction.go

Comment thread server/handler.go Outdated
For a defined pointer type (type P *Request), reflect.New(elemType.Elem())
yields the UNNAMED *Request, which is not assertable to the named P. The
comma-ok introduced by the forcetypeassert sweep discarded that failure, so
`request` stayed nil, validateNotNil rejected it, and the caller got
"request cannot be nil" for a perfectly good payload. Previously the
single-value assertion panicked there instead — both wrong, but a silent
rejection is worse than a loud panic.

Converting to elemType first — T's own reflect.Type — makes the assertion
exact for both the plain *Request and defined-P forms, so discarding ok is
now genuinely safe. The comment claiming it "cannot fail" was false as
written and is corrected.

Convert on a pointer adds no allocation: the ADR-026 AllocsStable guards
pass unchanged, and the hot path stays branch-free.

requestAllocator had no direct test — it was only ever exercised through
the binder, which is why this went unnoticed. Added one that pins the
defined-pointer case and asserts the typed request aliases the binder
target; verified it fails on the pre-fix code at the NotNil(request)
assertion and nowhere else.

Reported by CodeRabbit on #926.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
coderabbitai[bot]
coderabbitai Bot previously approved these changes Aug 8, 2026
The forcetypeassert conversion left three residues that the linter is
happy with but that cost more than they buy.

MockCache.GetOrSet was a literal transcription of sync.Map.LoadOrStore:
store-then-check-expiration, with the "expired" arm building and storing a
second identical cacheEntry and the return computing wasSet as !loaded.
With a plain map the missing and expired cases are the same case, so both
collapse into one guarded store. Same three outcomes, one entry
construction instead of two, and wasSet is a literal instead of a derived
value that can only ever be false where it is read.

Pool.GetOrCreate and Pool.evictIfNeeded each gained an "Unreachable:"
branch guarding an assertion whose producer is the only writer in the
file. Those are branches no test can enter, which is exactly what the
diff-scoped mutation gate has no way to kill. GetOrCreate now uses the
`ok &&` short-circuit that releaseAbandoned already uses forty lines
below — an impossible false falls into the existing bounded retry loop
rather than into a fabricated error string — and evictIfNeeded discards
ok the way server/handler.go's allocator does, since createEntry is the
LRU list's only writer.

mocks.arg called arguments.Get(index) three times to build one panic
message; it now reads the value once.

No behavior change: every branch removed is either provably equivalent to
the one it merged into or provably unenterable.
coderabbitai[bot]
coderabbitai Bot previously approved these changes Aug 8, 2026
gaborage and others added 2 commits August 7, 2026 20:09
SonarCloud S3776 (cognitive complexity, CRITICAL) on GetOrCreate. The function
already carried a retry loop around a singleflight closure and a select; the
comma-ok this branch added at the bottom tipped it past the threshold, with the
nesting penalties compounding down the body.

Dropping the earlier Unreachable: guard was not enough on its own — Sonar still
reported the finding after that commit — so the slow path moves wholesale into
acquireShared: the DoChan closure, the select on ctx.Done, and the result error
handling. GetOrCreate now reads as guard, fast path, slow path, claim. Every
comment moved with the code it explains rather than being dropped.

Suppressing with NOSONAR was not an option: the rule is correct here, the
function really was doing too much, and this repo's convention is to refactor
rather than annotate.

acquireShared returns a nil entry for the assertion that its sole producer makes
impossible, so the caller's `e != nil &&` short-circuits into the existing
bounded retry loop. That keeps the shape the mutation gate needs — no fabricated
error branch for a case no test can enter — matching how releaseAbandoned and
evictIfNeeded already handle the same assertion.

Behavior is unchanged: same singleflight collapsing, same per-caller context
cancellation via DoChan, same single incErrors accounting in the leader, same
abandoned-lease settlement. internal/resourcepool passes under -race.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/resourcepool/resourcepool.go`:
- Around line 191-216: Update acquireShared so createEntry receives
context.WithoutCancel(ctx), preventing the leader’s cancellation or deadline
from terminating the shared resource creation while preserving the caller’s
context for acquisition. Revise the deadline-inheritance test to assert shared
creation outlives an expired leader and succeeds for valid waiters, and add
coverage where the leader deadline expires while a waiter remains active.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f8dbe75b-acd7-4548-9090-5714176872e0

📥 Commits

Reviewing files that changed from the base of the PR and between a46ab57 and a196e17.

📒 Files selected for processing (6)
  • .golangci.yml
  • internal/resourcepool/resourcepool.go
  • scheduler/module.go
  • server/handler.go
  • server/jose.go
  • testing/mocks/query_builder.go

Comment thread internal/resourcepool/resourcepool.go
@sonarqubecloud

sonarqubecloud Bot commented Aug 8, 2026

Copy link
Copy Markdown

@gaborage
gaborage merged commit d97b7c1 into main Aug 8, 2026
46 of 48 checks passed
@gaborage
gaborage deleted the chore/forcetypeassert-handle-assertions branch August 8, 2026 02:47
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.

1 participant