Expose extension field numbers as generated Swift constants - #3676
Expose extension field numbers as generated Swift constants#3676loganblevins wants to merge 1 commit into
Conversation
Wire's Swift codegen generates accessor properties for extension fields, but never exposes the underlying field number. Consumers that call parseUnknownField/setUnknownField directly - for example to supply a custom ProtoDecoder with a different enum decoding strategy, or to distinguish an absent extension from an undecodable one via an unknownFields presence check - must hard-code the field number by cross-referencing the .proto source, and nothing keeps those copies in sync with codegen. GPB exposes the same information through GPBExtensionDescriptor; Wire had no Swift equivalent. Generate a `public static let fieldNumber_<name>: UInt32` constant alongside each extension accessor, mirroring the existing `default_<name>` constants. The constant is emitted for every extension field (including repeated fields, which have no default constant), on the extended type only - not on its CopyOnWrite storage type. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
74e1c3f to
28e112b
Compare
|
@oldergod GH won't let me request reviews... tagging instead |
oldergod
left a comment
There was a problem hiding this comment.
Approving with a few non-blocking comments.
wire-swift-generator/src/main/java/com/squareup/wire/swift/SwiftGenerator.kt:1342, The constant covers only half of what parseUnknownField needs. Signed/fixed integer extensions also require the encoding: argument (see the generated parseUnknownField(fieldNumber: 1003, type: Int32.self, encoding: .signed) in wire-tests-swift/no-manifest/src/main/swift/AllTypes.swift:898), and callers still have to copy that from the .proto by hand, a wrong copy silently decodes wrong instead of failing to compile. Emit the encoding alongside the number so the pair travels together.
wire-swift-generator/src/test/java/com/squareup/wire/swift/SwiftGeneratorTest.kt:93, BigMessage sits exactly on the 16-field heap-allocation threshold, and the test never asserts the storage split actually happened. If the threshold moves or a field is dropped, only one extension block is generated and both assertions pass trivially while the test still reads as covering the guard. Add assertThat(code).contains("public struct Storage") as a precondition.
wire-runtime-swift/src/test/swift/ExtensibleTests.swift:132, The sibling default_ constants have a Swift-side test that reads them off real generated types; the new constants have none. Add a case next to testExtensionDefaultValues that reads the constants off LargeExtensible/Extensible and round-trips one through parseUnknownField/setUnknownField.
wire-swift-generator/src/main/java/com/squareup/wire/swift/SwiftGenerator.kt:1428, The !forStorageType guard is the only thing preventing a duplicate declaration when both emission paths run over the same fields, and that invariant is documented nowhere but a test. Add a one-line comment stating the constant belongs on the extended type and the storage pass must skip it.
What
The Swift code generator now emits a
public static let fieldNumber_<name>: UInt32constant alongside every generated extension accessor, mirroring the existingdefault_<name>constants:Why
The generated extension accessors hardcode their decoding behavior:
try? ProtoDecoder().decode(...), i.e. the default.throwErrorenum strategy with the error swallowed. Consumers that need different semantics — a.returnNildecoder so a single unrecognized enum value doesn't nil the entire payload, or anunknownFields[tag]presence check to distinguish an absent extension from an undecodable one — must callparseUnknownField/setUnknownFielddirectly, and those APIs take a rawfieldNumber: UInt32.Today no generated symbol carries that field number: callers copy the literal out of the
.protosource by hand, and nothing keeps the copy in sync with codegen. (GPB exposes the same information viaGPBExtensionDescriptor; Wire has no Swift equivalent.) We hit this while migrating envelopes — a proto2 message whose payloads are all extensions — from GPB to Wire: extension field numbers had to be hardcoded and pinned with hand-written tag-agreement tests. With this change the constants come from codegen, so they are contract-checked at the source.Scope notes:
default_constant).Storagetype.default_<name>precedent; the type isUInt32so the constant can be passed straight toparseUnknownField(fieldNumber:).Testing
SwiftGeneratorTestcases: constants for singular and repeated extension fields, and single emission (extended type only) for heap-allocated messages../gradlew generateTests(AllTypes,FooBar,SwiftModuleOneMessage).swift testpasses locally on macOS.Suggested CHANGELOG entry (left out of the diff since entries appear to be authored at release time):
🤖 Generated with Claude Code