generator: make generated Go getters tolerate a nil receiver - #246
Conversation
The accessors added in crossplane#160 return their field directly, so walking nested getters panics as soon as an intermediate struct is nil: cd.GetStatus().GetProviderConfigRefs().GetAws().GetName() Every field of a generated model is optional, so intermediate nils are the normal case for a partially-populated resource, not an edge case. Callers therefore have to fall back to explicit nil checks at each hop, which is what the accessors were meant to avoid. Guard each getter with a nil-receiver check, as protobuf-generated getters do, so a chain over absent fields yields the zero value instead of panicking. Nilable field types return nil directly; anything else (a value field, a fixed-size array) returns a declared zero value. Setters are deliberately left unguarded: a set on a nil receiver has nowhere to store the value, so panicking is the honest behaviour. Getter signatures are unchanged, and code that worked before still works -- this only widens the set of receivers a getter accepts. Signed-off-by: Erik Miller <erik.miller@gusto.com>
📝 WalkthroughWalkthroughGenerated getters now guard nil receivers and return type-appropriate zero values. Tests verify generated guard structure, preserve unguarded setters, and confirm safe chained access through nil intermediate values. ChangesNil-safe accessor generation
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant TestGeneratedModels
participant ChainOnEmpty
participant GeneratedGetter
TestGeneratedModels->>ChainOnEmpty: Execute chained getter test
ChainOnEmpty->>GeneratedGetter: Call getters on nil intermediate values
GeneratedGetter-->>ChainOnEmpty: Return nil without panic
Suggested reviewers: 🚥 Pre-merge checks | ✅ 6✅ Passed checks (6 passed)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
internal/schemas/generator/accessors_test.go (1)
419-438: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUse table cases that assert the complete nil branch.
Thanks for adding coverage for nil receivers. Convert these checks into cases with
reason,args, andwantfields. Includereturn zeroin the expected scalar and fixed-array branches. The current checks pass if a getter declareszerobut returns a different value.As per path instructions,
**/*_test.gorequires a table-driven test structure withargs/want, case naming, andreasonfields.🤖 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 `@internal/schemas/generator/accessors_test.go` around lines 419 - 438, The nil-receiver assertions in the accessor test should become table-driven cases with case names plus reason, args, and want fields. Update the cases for GetBar, GetCount, and GetFixed to compare the complete generated nil branch, explicitly requiring return nil for the nilable field and var zero followed by return zero for scalar and fixed-array fields; retain the SetBar guard assertion.Source: Path instructions
🤖 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 `@internal/schemas/generator/accessors_test.go`:
- Around line 419-438: The nil-receiver assertions in the accessor test should
become table-driven cases with case names plus reason, args, and want fields.
Update the cases for GetBar, GetCount, and GetFixed to compare the complete
generated nil branch, explicitly requiring return nil for the nilable field and
var zero followed by return zero for scalar and fixed-array fields; retain the
SetBar guard assertion.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 44608a88-35aa-4e42-b63b-f5081f06005b
📒 Files selected for processing (2)
internal/schemas/generator/accessors.gointernal/schemas/generator/accessors_test.go
adamwg
left a comment
There was a problem hiding this comment.
One question, but assuming my assumption is correct, this lgtm. This will definitely help the ergonomics of the Go bindings 🙏
| b.WriteString("\n// Get" + fieldName + " returns the " + fieldName + " field.\n") | ||
| b.WriteString("// It returns the zero value if the receiver is nil.\n") | ||
| b.WriteString("func (" + accessorReceiver + " *" + typeName + ") Get" + fieldName + "() " + fieldType + " {\n") | ||
| b.WriteString("\tif " + accessorReceiver + " == nil {\n") |
There was a problem hiding this comment.
Are the receivers guaranteed to be nil-able? I think they are because we mark every openapi field as optional, so they all become pointers - is that right?
Description of your changes
Follow-up to #160.
Problem
The generated accessors return their field directly:
So walking nested getters panics as soon as an intermediate struct is nil:
goRemoveRequiredmakes every field of a generated model optional, so intermediatenils are the normal case for a partially-populated resource, not an edge case — a
status subresource that the controller hasn't filled in yet hits this immediately.
Callers end up writing explicit nil checks at every hop, which is most of what the
accessors were meant to remove.
What this does
Guards each getter with a nil-receiver check, the way protobuf-generated getters do,
so a chain over absent fields yields the zero value instead of panicking:
Nilable field types return
nildirectly. Anything else — a value field, a fixed-sizearray — returns a declared zero value, so the guard is correct for field shapes the
generator doesn't currently emit but could.
Setters are deliberately left unguarded: a set on a nil receiver has nowhere to store
the value, so panicking is the honest behaviour rather than silently dropping a write.
What this does not do
Scalar getters keep returning pointers (
GetName() *string, notstring). Protobufreturns values for scalars, but for Kubernetes APIs the nil-vs-empty distinction is
load-bearing —
omitempty, patch semantics, and "unset" versus "explicitly empty" areall observable. Collapsing that would lose information, and the pointer return keeps
GetX/SetXsymmetric. Callers still need one nil check at the end of a chain, not oneper hop.
Compatibility
Getter signatures are unchanged, and this only widens the set of receivers a getter
accepts, so no code that works today breaks. #160 is also not in a release yet, so
there are no released consumers either way.
Testing
TestAddAccessorsGuardsNilReceiver— asserts every getter opens with the nil guard,that setters do not, and that nilable fields return
nilwhile a value field and afixed-size array get a declared zero.
getters over an empty resource, and the materialized module is
go tested. Compilingalone would not have caught this, since the old code typechecks fine and only fails at
runtime.
a nil pointer dereference, which is the bug being fixed.
confirmed
cd.GetStatus().GetProviderConfigRefs().GetAws().GetName()returns nil on anempty resource instead of panicking.
go test ./...,go vet ./...,go build ./...andgofmt -l .are all clean.I have:
Run./nix.sh flake checkto ensure this PR is ready for review.Linked a PR or a docs tracking issue to document this change.Addedbackport release-x.ylabels to auto-backport this PR.On the struck items:
nixisn't available in the environment this was developed in, soflake checkhasn't been run —go test/go vet/gofmtwere run instead, and I'dappreciate CI or a reviewer confirming the flake. This changes the body of generated
methods rather than any user-facing command or help text, so there's nothing for
crossplane/docsto pick up. And it fixes an unreleased feature from #160, so it isn't abackport candidate.
Need help with this checklist? See the cheat sheet.