A custom vet analyzer every repo runs has reported nothing for a quarter. How do you prove it still fires?
answer
- silence and not running look identical
- one experiment per layer, top down
- a fixture you control comes first
- want comments assert what should be reported
- keep a deliberate violation as a canary
basics
~20 sTreat silence as a failure until proven otherwise. Prove the matcher on an analysistest fixture with want comments, then work up the chain: analyzer linked into the shipped binary, packages actually analysed, files not hidden by build constraints, results not served from cache.
solid answer
~50 sA check that reports nothing is indistinguishable from one that is not running, so I work down the stack with one experiment per layer. Does it fire at all: `analysistest.Run(t, analysistest.TestData(), Analyzer, "a")` over a fixture with `// want` comments proves the matching logic on code I control, and a check with no failing fixture has never actually been shown to fire. Does it fire on real code: run the shipped binary over a package where I planted a violation. Does it fire under the build: the same package through `go vet -vettool`. Wherever the chain breaks names the cause — analyzer not linked into the suite binary, packages outside the patterns, files behind a build constraint so they never reach `pass.Files`, a package that fails to type-check and is skipped, or stale cached results. Then I leave a canary fixture behind so the next regression fails a test instead of lasting a quarter.
code
go · 3 linesfunc TestAnalyzer(t *testing.T) {
analysistest.Run(t, analysistest.TestData(), Analyzer, "a")
}go deeper
Remember that a static check must be tested like any other code: a fixture that would be flagged, and a test that fails if it is not.
Explain how analysistest and want comments work, and name the layers between your matching logic and a diagnostic actually appearing in someone's build.
Show the layer-by-layer bisection and the controls that make silence detectable, including a canary violation and reading a sample of hits before a check goes mandatory.
Own the fact that an unenforced check is worse than no check, because teams stop reviewing for the thing it was supposed to catch; decide who is accountable for proving it still runs.
## Silence is not evidence A false negative in a static check is uniquely hard to notice: the build is green, nobody files a ticket, and everyone assumes the rule is being enforced. The only defence is to make the check prove itself continuously, and the only way to debug one is to take the chain apart layer by layer, because at least five different layers can swallow a diagnostic. ## Layer 1 — does the matching logic fire on a fixture? `golang.org/x/tools/go/analysis/analysistest` exists for this. `analysistest.TestData()` returns the calling package's `testdata` directory; `analysistest.Run(t, dir, Analyzer, "a")` analyses the package `a` beneath it and compares the diagnostics against `// want` comments in the source. Each `// want "regexp"` asserts that a diagnostic whose message matches that regular expression is reported on that line; an unmatched expectation and an unexpected diagnostic both fail the test. The discipline that actually prevents the quarter-long outage is: **for every rule the check enforces, keep both a positive fixture and a near-miss negative fixture.** The positive one fails loudly if the matching logic regresses. The negative one — code that looks similar and must *not* be flagged — fails if somebody widens the check to make the positive pass again. `analysistest.RunWithSuggestedFixes` extends this to fixes, comparing the rewritten source against `.golden` files in the same tree. If the fixture does not report, the bug is in the check itself: most often a syntax-shaped match that the real call sites do not have, or a type predicate narrowed to one spelling of an import. ## Layer 2 — is the analyzer in the binary anyone runs? A suite built with `multichecker` only contains the analyzers passed to `Main`. A check added on a branch and never added to that list passes its own tests forever. Run the shipped binary directly over a package with a planted violation. If the fixture reports and the binary does not, the analyzer is either missing from the driver or disabled by its own flag — drivers expose one flag per check, and a check switched off during a rollout tends to stay off. ## Layer 3 — are the packages being analysed at all? `go vet` analyses the packages matching the patterns you gave it. A repository whose command names a subset, a directory excluded by the pattern, or generated code the team routinely skips all produce genuine silence. Related and easier to miss: files excluded by build constraints or by `GOOS`/`GOARCH` never appear in `pass.Files` at all — they land in `pass.IgnoredFiles`. A rule about platform-specific code checked only on one platform's build is silently unenforced on the others. ## Layer 4 — was the analyzer skipped? If a package does not type-check, the driver does not run analyzers on it unless the analyzer sets `RunDespiteErrors`. In a healthy repository this rarely matters, because the build would fail too — but in a repository with a package that is only ever built under a tag, or with vendored code that no longer type-checks, whole subtrees can be quietly skipped. ## Layer 5 — is the result cached? The go command caches vet results, keyed in part on the identity the tool reports for `-V=full`. A rebuilt analyzer that does not change that identity can therefore serve results computed by the previous version. The first time a change to a check appears to do nothing on unchanged packages, this is worth ruling out — touch the input or clear the build cache and compare. ## Turning the answer into a control The diagnosis is the easy half; the reason it lasted a quarter is that nothing was watching. Two cheap controls fix that: 1. **Fixtures in the check's own repository**, run in its tests, covering every rule with a positive and a negative case. This is the golden-file discipline: a regenerated expectation is reviewed as a diff, so widening a check is a visible change rather than a silent one. 2. **A canary in the consuming build** — a tiny package, excluded from the shipped binary, containing one deliberate violation, where the *expected* outcome is a diagnostic. If the check ever stops running end to end, the canary stops reporting and that is observable. And when the check is later found to have missed real code, the fix is not only to widen the matcher: it is to add the missed shape as a fixture, so the same gap cannot reopen. ## The habit worth stating in an interview Every new check should be introduced by first watching it fire. Run it over the whole corpus in a non-blocking mode and read a sample of the hits and, more importantly, a sample of the places you *expected* a hit and did not get one. A check that reports zero findings on its first run over a large codebase is almost never a clean codebase; it is almost always a broken check.
- What exactly does a want comment in an analysistest fixture assert?That the analyzer reports a diagnostic on that line whose message matches the given regular expression. The test fails both when an expectation goes unmatched and when a diagnostic appears that no expectation covers, so the fixture pins the check in both directions — it cannot silently stop firing, and it cannot silently start flagging the near-miss case sitting next to it.
- The check fires locally but never in the build. Where do you look first?At whether the same files are even in play. Build constraints and the target platform decide what lands in `pass.Files`, and the package patterns decide which packages are analysed at all. After that, whether the shipped binary contains the analyzer and whether its flag is enabled, and finally whether cached vet results are being reused for a tool whose reported identity did not change.
- You widen the matcher to catch the code it missed. What else belongs in that change?The missed shape as a new positive fixture, and at least one near-miss negative fixture around it, because widening is exactly when false positives get introduced. It is also worth re-running the check over the whole corpus non-blocking and reading a sample of the new hits before the widened version becomes mandatory.
saying these in an interview costs you the question
- Treating zero findings as proof the codebase is clean
- Shipping a check with no failing fixture behind it
- Testing only code that should be flagged, never near misses
- Forgetting that build constraints hide files from pass.Files
- Assuming a rebuilt tool always invalidates cached vet results