← all benchmarks

TrueMend vs. Semgrep on Gin

Full-pool blind adjudication on a production Gin API: TrueMend 0.0% wrong-match vs. Semgrep's 50.0%, every one of 51 post-fix findings verified correct. The follow-up we promised — a second, independent Gin corpus — told a harder story: 40.4% wrong-match on first contact, an engine bug found underneath it, fixed back to 5.9%.

Gin's framework-specific detector surface hadn't been through real blind adjudication before — only synthetic fixtures. We ran a full campaign against a real production Gin codebase to find out whether that surface holds up outside the lab.

Judged completely, not sampled

The pool was small enough to judge every finding, not a stratified sample — 91 of 91 (51 TrueMend, 40 Semgrep), each ruled correct or wrong by an independent judge with no knowledge of which tool produced it or what the "right" answer should be.

Result

TrueMend: 0.0% wrong-match. Semgrep: 50.0%. Every single one of TrueMend's 51 post-fix findings checked out against the real source. Semgrep's 50% is dominated by one 18-instance false-positive cluster: an HTML-escaping rule firing on Gin's own render/*.go package (json.go, yaml.go, toml.go, protobuf.go, ...) for writing correctly-serialized, correctly-content-typed bytes — the rule doesn't check content type before applying an HTML-context escaping assumption.

What it took to get there

Six systemic detector bugs, all genuine precision fixes — no detector was disabled or withdrawn to reach this number. The largest: an "unchecked error" check that allowlisted specific stdlib comma-ok patterns (strings.Cut, utf8.Decode*) but not Go's own GetXxx() (T, bool) accessor convention, so it fired on every c.GetQuery, c.GetPostForm, and similar call in Gin's own context.go. Fixed by recognizing the GetXxx/Get naming convention directly — two genuine true positives in the same category still fire correctly post-fix. Full root-cause detail for all six: internal engineering notes, available on request.

The follow-up we promised

This case study originally ended here, with one real corpus exhaustively judged, and an open question: would the surface hold up against a second, independent Gin codebase, or did these six fixes just overfit to one repo's idioms? We ran that follow-up. It didn't go the way the first result would predict.

Round 2 — first contact with a second corpus, and it's ugly

The second corpus is gin-gonic/gin's own source tree — its own test suite and internal packages exercise gin.Context, c.Bind/c.ShouldBind, and error handling the same way a consuming application does, just written by the framework's own maintainers with their own idioms. A fresh, from-scratch full pool sample (156 judged, TrueMend-only — golangci-lint wasn't installed this round) came back at 40.4% wrong-match. Confirmed 2 of the 3 prior fixes already held; the number is bad because eight Go/Gin-specific detectors were sitting at 75–100% wrong on idioms this second corpus surfaces that the first one never did — test-helper error-check wrappers (require.NoError), the http.NewRequest-isn't-I/O distinction, and a batch of whole-construct claims (named-return signatures, operand types) no line-scan can see. Publishing a worse number two rounds later isn't a retraction of the 0.0% result above — different corpus, different failure modes — but it's the honest version of "does this generalize," and the honest answer was: not yet.

Round 3 — an engine bug underneath the detector bugs

Eight detectors, each root-caused against the real gin-gonic/gin source. Three were genuine line-scan fixes. Five were quarantined — whole-construct properties (a named-return signature, an operand's type, a return type) that no bounded regex window can see, so we disabled and documented them rather than chase precision with a window tweak that would just trade false positives for silently dropped true positives.

The load-bearing finding wasn't a detector at all. One fix cluster wouldn't narrow no matter how the regex was adjusted, and root-causing it against source turned up an indexing bug in the detection engine itself: the per-function scan path computed each match's position using the wrong baseline, so any detector using a negative-context check on that path silently never suppressed anything — the check ran, found nothing, and kept every match, including the ones it existed to filter out. Confirmed directly against the corpus: one detector's raw findings dropped from 66 to 1 the moment the engine fix landed, with no change to that detector's own rule. It had been correct the whole time; it just never ran correctly.

RoundJudgedWrong-matchReal-rateNote
Round 1 (2026-07-26, above)510.0%Production Gin API, vs. Semgrep 50.0%
Round 2 (fresh corpus)15640.4%13.5%gin-gonic/gin itself — 8 detectors newly exposed
Round 3 (post-fix)1025.9%7.8%3 fixes, 5 quarantines, 1 engine bug

Under our own 10% bar again, on a corpus that initially broke the surface far worse than round 1's clean result would have predicted. Raw findings on this corpus: 3049 → 2184 (−865 false positives) between rounds 2 and 3. No competitor ran this round — golangci-lint wasn't installed — so this isn't a head-to-head rerun of the 0.0%-vs-50.0% result above, it's the generalization check that result was missing.

Still open

The engine bug's fix applies everywhere the same code path runs, not just to Go/Gin — but this campaign only re-verified it here. Whether other language surfaces were quietly carrying the same silently-disabled negation checks, and how much their own certified numbers move once re-measured, is unverified and the next thing worth checking.