Add checkergen module for reflection-free code generation - #282
Merged
Conversation
Adds checkergen/, a new isolated module (own go.mod, taskfile.yml, replace ../, needs golang.org/x/tools for go/packages type info, same pattern as checkerlint) that generates a Check<Type>(v *Type) (checker.CheckErrors, bool) function per eligible struct from its checkers/validate tags -- calling the same plain checker/normalizer functions (IsEmail, MinLen[string](8), ...) directly via checker.Check, instead of walking the struct with reflect at runtime through CheckStruct. Design, per the scoping discussion on #264: - A self-contained per-checker call-spec mapping table lives in checkergen itself (callspec.go), not the core module -- emitting Go source text is a different kind of concern than reflecting into a Schema, so this keeps the core module's job unchanged. - v1 scope: a field is only eligible if its Go type is exactly one of the predeclared scalar types (string, bool, int/uint/float kinds) -- not a named/defined type, a pointer, or a nested struct/slice/map. A struct with an ineligible field or unmapped checker is skipped with a clear diagnostic, not generated incorrectly, and doesn't block generation for the rest of the package. - A malformed tag parameter (e.g. after-field missing its ":field" half) panics the same way the equivalent runtime maker does; Generate recovers this per-struct into a skip, mirroring the cli module's CheckWithConfig panic recovery (see CLAUDE.md). - eq-field's generated call uses == (comparable), matching IsEq/IsGte's style, instead of the reflect-based eq-field tag path's reflect.DeepEqual -- documented as a deliberate, practically inconsequential divergence (only differs for a slice/map/struct field, which isn't eligible for generation anyway). - eq/ne/oneof generated calls support any comparable field type via the fully generic IsEq/IsNe/IsOneOf, even though CheckStruct's own reflect-based eq/ne/oneof implementation only supports string fields today (checkEq/checkNe/checkOneof hardcode reflectString) -- a capability superset, not a behavior change for anything CheckStruct already validates. Verified with a differential test suite (testdata/fixture): for a battery of inputs, the generated code and checker.CheckStruct are asserted to report errors under identical field keys and normalize values identically. A separate drift test regenerates the committed fixture into a scratch copy and byte-compares it against the checked -in version, to catch the fixture falling out of sync with the generator itself. Benchmarked (checkergen/benchmark_test.go) against CheckStruct on the same struct/input: ~3x faster with 4-8x fewer allocations. Numbers included in both this module's README and the root README's new "Code Generation" section. Also fixes an unrelated gosec finding this surfaced (G306, WriteFile permissions) by tightening checkergen's own generated-file write to 0600, and adds a proactive -exclude-dir=checkergen entry to the root taskfile.yml's gosec run -- learned from the nethttp/fiber miss that broke main's CI after #276. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARLam6G4Bu9afnWHpmJWeo
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #282 +/- ##
=========================================
Coverage 100.00% 100.00%
=========================================
Files 86 86
Lines 1807 1807
=========================================
Hits 1807 1807 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
checkergen/README.md: new "What Building This Found" section calling out the three real issues the differential-test harness surfaced while building the generator -- a wrong field name in eq-field's generated error data, an unrecovered panic that could take down generation for a whole package on one malformed tag, and the pre-existing eq/ne/oneof string-only limitation in the core module's reflect-based checkers (not a checkergen bug, but found via it). articles/reflection-free-go-validation-with-checkergen.md: a dev.to-formatted draft (published: false, matching the existing draft convention in this directory) walking through the same three findings in narrative form, plus the benchmark numbers and usage. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARLam6G4Bu9afnWHpmJWeo
Drops the "I built this, it found bugs in my own code" framing. Leads with the reflection cost CheckStruct pays, the benchmark numbers, and a worked example swapping checker.CheckStruct for generated code inside a Gin handler, with the same swap spelled out for Echo, Fiber, and net/http -- tying checkergen directly to the existing adapter modules instead of presenting it in isolation. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARLam6G4Bu9afnWHpmJWeo
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARLam6G4Bu9afnWHpmJWeo
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements #264 per the revised scoping comment: a code generator that turns
checkers/validatestruct tags into a reflection-freeCheck<Type>(v *Type) (checker.CheckErrors, bool)function, calling the same plain checker/normalizer functions (IsEmail,MinLen[string](8),IsEqField, ...) directly instead of walking the struct withreflectthroughCheckStruct.checkergen/module (owngo.mod,taskfile.yml,replace ../), needsgolang.org/x/tools(go/packages) for type info — same pattern ascheckerlintcheckergenitself, not the core module — emitting Go source text is a different concern than reflecting into aSchema, so the core module's job stays unchangedGeneraterecovers this per-struct into a skip, mirroring theclimodule'sCheckWithConfigpanic recovery (documented pattern inCLAUDE.md)Two deliberate, documented divergences from
CheckStructeq-field's generated call uses==(comparable), matchingIsEq/IsGte's style, instead of the reflect path'sreflect.DeepEqual— only differs for a slice/map/struct field, which isn't eligible for generation anywayeq/ne/oneofgenerated calls support any comparable field type via the fully genericIsEq/IsNe/IsOneOf, even thoughCheckStruct's own reflect-based implementation only supports string fields today (checkEq/checkNe/checkOneOfhardcodereflectString) — a capability superset, not a behavior change for anythingCheckStructalready validates correctlyVerification
testdata/fixture): for a battery of inputs, generated code andchecker.CheckStructare asserted to report errors under identical field keys and normalize values identicallyPerformance
Benchmarked against
CheckStructon the same struct/input: ~3x faster, 4-8x fewer allocations. Numbers in bothcheckergen/README.mdand the root README's new "Code Generation" section.Also fixes along the way
0600-exclude-dir=checkergento the roottaskfile.yml's gosec run proactively this time, plus a loud warning comment inCLAUDE.mdabout updating both places — learned from thenethttp/fibermiss that brokemain's CI after Add a Fiber adapter module #276Closes #264
Test plan
cd checkergen && go test -cover .— 96.7% coveragecd checkergen && go vet ./..., gosec, staticcheck, revive — all cleango test -cover ./...— 100%, unaffected🤖 Generated with Claude Code
https://claude.ai/code/session_01ARLam6G4Bu9afnWHpmJWeo