Skip to content

Add exhaustive switch vetting and enforce it across the tree#844

Merged
evanphx merged 10 commits into
mainfrom
mir-1202-add-exhaustive-switch-vetting
Jun 6, 2026
Merged

Add exhaustive switch vetting and enforce it across the tree#844
evanphx merged 10 commits into
mainfrom
mir-1202-add-exhaustive-switch-vetting

Conversation

@evanphx

@evanphx evanphx commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Enables the exhaustive analyzer (bundled in golangci-lint) and makes every enum switch in the tree satisfy it, so adding a new enum member forces a compile-time decision at each relevant switch instead of silently slipping through a default.

What changed

  • Lint config: enable the exhaustive linter in .golangci.yml with default-signifies-exhaustive: false (a bare default: does not by itself make a switch exhaustive). It honors //exhaustive:ignore directives.
  • Enforcement (38 switches): each switch now enumerates all enum members explicitly. Where the missing members should behave exactly like the existing default, they're grouped into a case that fallthroughs into the default — so runtime behavior is unchanged, but the cases are now documented and checked. Each added case carries a short comment explaining why those members share that path (terminal / transient / unexpected / etc.).

Intentional exceptions

//exhaustive:ignore is kept only on switches over large external/stdlib enums where listing every arm is impractical, with the rationale inline:

Type Members Location
syscall.Errno ~130 servers/httpingress/httpingress.go
tea.KeyType ~90 pkg/ui/prompt.go, cli/commands/deploy_ui.go
reflect.Kind ~27 pkg/entity/reader.go, controllers/deployment/specs_match_test.go

Coverage note

golangci-lint's path exclusions are global, so the already lint-exempt paths (lsvd/, disk/, dataset/, pkg/cel, pkg/containerdx, *.gen.go) are not exhaustive-checked. The switches in those paths (3 in lsvd/) were still made exhaustive in this PR; they're just not re-enforced going forward.

Verification

  • golangci-lint run ./... — no issues (exhaustive active; confirmed it flags a removed case)
  • go build ./... and affected test packages compile

Wire the nishanths/exhaustive analyzer in as a `go vet` vettool (new
`make vet` target + go.mod tool directive) and make every enum switch
satisfy it.

Rather than blanket-suppress with `//exhaustive:ignore`, each switch now
lists all enum members explicitly. Where the missing members should
behave identically to the existing `default`, they're grouped into a
case that `fallthrough`s into the default so runtime behavior is
unchanged while a future enum addition forces a deliberate decision.

The ignore directive is kept only on switches over large external/stdlib
enums where enumeration is impractical, with the rationale inline:
  - syscall.Errno (~130 members) in httpingress
  - tea.KeyType (~90 members) in prompt/deploy_ui
  - reflect.Kind (~27 members) in entity reader and specs_match_test

CI: run `make vet` as part of the lint job in test.yml.
@evanphx evanphx requested a review from a team as a code owner June 4, 2026 22:48
@coderabbitai

coderabbitai Bot commented Jun 4, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This pull request enables the exhaustive linter and makes switch statements explicit across the codebase. It adds explicit case branches, fallthroughs, and selective //exhaustive:ignore comments for many high-cardinality enums; updates disk/mount/volume reconciliation paths, sandbox lifecycle and health checks, watch/event conversion, authorization routing, saga terminal handling, value parsing/formatting, builder defaults, tests, and logging. No public APIs or behavioral changes were introduced; existing control flow and default behaviors are preserved.


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

@phinze phinze 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.

Yay, one step closer to Rust ;) We checked the few no-op cases (SetPortStatus x2, lsvd FlushNo) and they're all safe.

One ask before merging: can we wire it through golangci-lint (already in the lint job, bundles the same exhaustive analyzer) instead of a standalone go vet step? We'd get the shared single load, parallelism, and caching for free, and drop the vet target, CI step, and go.mod/go.sum churn. With golangci-lint already pinned, the bundled version is just as deterministic.

Only difference: golangci-lint still excludes a few dirs (lsvd/, disk/, etc.), so those won't be covered. Fine to punt, whether those should be linted at all is a separate cleanup TODO.

evanphx commented Jun 4, 2026

Copy link
Copy Markdown
Contributor Author

Sure, let me give it a look.

golangci-lint bundles the exhaustive analyzer, so enable it there rather
than maintaining a parallel `go vet` vettool. This drops the dedicated
plumbing:
  - remove the `make vet` target
  - remove the `go vet (exhaustive)` step from the lint CI job
  - remove the nishanths/exhaustive tool directive from go.mod/go.sum

The linter is configured with default-signifies-exhaustive: false to
match the prior vettool behavior, and still honors `//exhaustive:ignore`.

Tradeoff: golangci-lint's path exclusions are global, so the already
lint-exempt paths (lsvd/, disk/, dataset/, pkg/cel, pkg/containerdx,
*.gen.go) are no longer exhaustive-checked. The switches in those paths
remain exhaustive; they're just not re-enforced.

@coderabbitai coderabbitai Bot 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.

🧹 Nitpick comments (1)
go.mod (1)

93-93: Go mod: gjson v1.18.0 looks safe; optionally bump for currency

github.com/tidwall/gjson v1.18.0 (now a direct dependency) has no known security vulnerabilities reported specifically for that version. Latest upstream release is v1.19.1 (May 2026), so updating is recommended for staying current, not for security remediation.

🤖 Prompt for 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.

In `@go.mod` at line 93, The go.mod currently pins github.com/tidwall/gjson at
v1.18.0; update the dependency to a newer release (e.g., v1.19.1) by editing the
module line for github.com/tidwall/gjson in go.mod, run go get
github.com/tidwall/gjson@v1.19.1 (or go get -u github.com/tidwall/gjson) to
update go.sum and tidy the module, and then run go mod tidy and go test to
ensure nothing breaks; target symbols: the dependency entry
"github.com/tidwall/gjson" in go.mod and the overall module's go.mod/go.sum
maintenance commands.
🤖 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.

Nitpick comments:
In `@go.mod`:
- Line 93: The go.mod currently pins github.com/tidwall/gjson at v1.18.0; update
the dependency to a newer release (e.g., v1.19.1) by editing the module line for
github.com/tidwall/gjson in go.mod, run go get github.com/tidwall/gjson@v1.19.1
(or go get -u github.com/tidwall/gjson) to update go.sum and tidy the module,
and then run go mod tidy and go test to ensure nothing breaks; target symbols:
the dependency entry "github.com/tidwall/gjson" in go.mod and the overall
module's go.mod/go.sum maintenance commands.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 479c618a-a96d-4217-ae67-aea51a26863b

📥 Commits

Reviewing files that changed from the base of the PR and between 1312315 and e0e1564.

📒 Files selected for processing (2)
  • .golangci.yml
  • go.mod
✅ Files skipped from review due to trivial changes (1)
  • .golangci.yml

evanphx added 5 commits June 4, 2026 16:37
…ve-switch-vetting

# Conflicts:
#	components/ipalloc/ipalloc.go
Merging the latest main surfaced enum switches that the now-enabled
exhaustive linter flags:

  - controllers/sandbox/log.go: gjson.Type — handle Null/JSON (skipped)
  - pkg/controller/controller.go: EntityOperation — Progress/Compacted
    route to the existing no-op default
  - pkg/entity/indexwatch/watcher.go: EntityOperation in two switches —
    Progress/Compacted handled explicitly; Create/Update/Delete fall
    through to eventFromOp
  - components/ipalloc/ipalloc.go: indexwatch.EventType — handle
    EventDeleted (resolved during the merge)

EntityOperation gained Progress and Compacted members on main, which is
exactly the kind of addition this linter is meant to catch.
Enabling the exhaustive analyzer in golangci-lint perturbs staticcheck's
analyzer scheduling enough that it intermittently stops recognizing
t.Fatal as terminating, raising a false-positive SA5011 (possible nil
deref) on the guarded block.Bytes access.

Add an explicit return after t.Fatal so the non-nil invariant holds
structurally, independent of whether staticcheck models t.Fatal's
control flow. No behavior change.

@coderabbitai coderabbitai Bot 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.

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 `@components/ipalloc/ipalloc.go`:
- Around line 189-190: The EventDeleted branch currently no-ops, which leaks
reservations in the allocations map; update the indexwatch.EventDeleted case to
release any reserved IPs for the deleted service by looking up the entry in
allocations (use the service key/ID used elsewhere in this file), call the
existing release function (e.g., releaseIP, ReleaseReservedIP or whatever the
file provides for freeing an allocation) or if none exists implement the reverse
of the allocation path to return the IP to the pool, and then remove the entry
from allocations so the subnet capacity is reclaimed.
🪄 Autofix (Beta)

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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1ca0879d-2d5f-478e-b5c6-222295e60b33

📥 Commits

Reviewing files that changed from the base of the PR and between 928cd9b and 79c370e.

📒 Files selected for processing (7)
  • components/ipalloc/ipalloc.go
  • controllers/sandbox/log.go
  • controllers/sandbox/sandbox.go
  • pkg/controller/controller.go
  • pkg/entity/indexwatch/watcher.go
  • servers/httpingress/httpingress.go
  • servers/runner/registration_test.go
✅ Files skipped from review due to trivial changes (4)
  • servers/httpingress/httpingress.go
  • controllers/sandbox/log.go
  • pkg/controller/controller.go
  • servers/runner/registration_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • controllers/sandbox/sandbox.go

Comment thread components/ipalloc/ipalloc.go Outdated
evanphx added 3 commits June 4, 2026 17:37
sandbox.go and volume.go gained explicit enum cases for the exhaustive
linter (and sandbox.go also picked up changes from main). The parallel
saga controller (saga_controller.go) carries the matching SandboxStatus
case, so the two code paths stay in sync. Refresh the frozen hashes.
The ipalloc allocation table is append-only: IPs were recorded on
snapshot/assign but never removed. A deleted service therefore held its
addresses forever, slowly exhausting the subnet.

Handle indexwatch.EventDeleted by pruning every allocation owned by the
deleted service id, returning those IPs to the pool. Add a unit test
covering the round-trip (release frees only the target service's IPs and
they can be re-allocated).

Fixes swt-e255.
…ve-switch-vetting

# Conflicts:
#	controllers/sandbox/sandbox_frozen_test.go
@evanphx evanphx merged commit d88c30c into main Jun 6, 2026
19 checks passed
@evanphx evanphx deleted the mir-1202-add-exhaustive-switch-vetting branch June 6, 2026 20:49
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