Flatten anonymous nested struct/union fields (#408)#1721
Merged
jevansaks merged 6 commits intoJun 12, 2026
Conversation
Add an opt-in FlattenNestedAnonymousTypes generator option that surfaces fields nested within anonymous structs and unions as [UnscopedRef] ref-returning properties on the declaring struct, so callers can write value.field instead of value.Anonymous.Anonymous.field. The accessors support read, write, and pointer use, and inherit documentation from the underlying field via <inheritdoc/>. Gated on C# 11+ (UnscopedRefAttribute). Named nested types are left alone; only fields reached exclusively through Anonymous holders are flattened. Leaf fields in nested anonymous types now receive documentation propagated from the declaring type's API docs so the inherited docs resolve.
Cover numbered/multiple anonymous holders (DECIMAL), unsafe pointer leaves (VARDESC), public visibility, and regression guards for the explicit-layout field-type context (ELEMDESC/CS8151) and the deeply-nested managed-union inheritdoc cref suffix (PROPVARIANT/CS1574).
…osoft#408) Verify the option is a no-op for a struct without anonymous members (LUID), and that a fixed-length array leaf inside an anonymous union (BLUETOOTH_ADDRESS.rgBytes) is not surfaced as a ref accessor while its scalar sibling (ullLong) is.
Flattening must hoist only anonymous union/struct grouping fields, never the members of a nested struct *value* reached through them. - INPUT: union of struct values (mi/ki/hi) are surfaced as whole refs; their inner fields (dx/dy/...) are not flattened onto INPUT. - PROPVARIANT: the DECIMAL value (decVal) is surfaced as a single ref; DECIMAL's own union members (Lo64/scale/...) are flattened only onto DECIMAL, not transitively onto PROPVARIANT.
…it exposed Flip GeneratorOptions.FlattenNestedAnonymousTypes (and settings.schema.json) default to true and update the option/schema docs accordingly. Turning the option on across the full metadata surface revealed two latent bugs in the flattened-accessor generator, both now fixed: - CS8151: a nested *managed* struct value reached through an anonymous union (e.g. MSP_EVENT_INFO) was given a fully-qualified ref-return type that applied the _unmanaged suffix to every ancestor, naming the unmanaged twin instead of the type actually reachable through this.Anonymous. The leaf type is now referenced relative to the declaring struct's container chain. - CS0612: a flattened accessor whose body references an [Obsolete] leaf field (e.g. PEER_GROUP_EVENT_DATA) is now itself marked [Obsolete], matching how the existing field/property generation handles obsolete members. Adds explicit regression tests for both cases (option set explicitly so they survive a future default change).
The CLI/source-generator generation path (CsWin32Generator.Tests, exercised by the Heavy FullGen CI jobs) defaults UseComSourceGenerators=true. In that mode an anonymous holder field on a managed struct is typed as the *unmanaged* twin of its nested union (e.g. MSP_EVENT_INFO_unmanaged._Anonymous_e__Union_unmanaged), and each twin declares its own nested leaf types. The previous fix reconstructed the leaf ref-return type from the managed-twin container chain, which did not match the type actually reached through this.Anonymous in source-generator mode, reintroducing CS8151 (MSP_EVENT_INFO, VDS_ASYNC_OUTPUT, SSVARIANT, etc.). Type each flattened accessor relative to the holder field's *actual* declared type (captured from the already-generated field declaration) so the ref-return type always selects the correct managed/unmanaged twin regardless of generation mode. Adds a source-generator regression test covering these structs.
jevansaks
approved these changes
Jun 12, 2026
jevansaks
pushed a commit
that referenced
this pull request
Jun 13, 2026
* Flatten anonymous bitfield sub-properties (#408 phase 2) Phase 1 (#1721) surfaces fields nested in anonymous structs/unions as [UnscopedRef] ref accessors, but only the raw bitfield backing field was reachable that way. This forwards the computed bitfield sub-properties (e.g. PSAPI_WORKING_SET_EX_BLOCK's Valid/ShareCount/Win32Protection) as value get/set properties on the declaring struct, so they can be read and written directly as value.Field instead of value.Anonymous.Anonymous.Field. Each forwarded property delegates to the nested property (readonly get => this.Anonymous...Prop; set => ... = value) and inherits its docs via <inheritdoc>. The projected type is computed from the bitfield width (1 bit => bool, else smallest fitting integer of the backing field's signedness), matching the nested property exactly, so no narrowing occurs even when the backing field is wider (e.g. nuint backing, byte/ushort sub-properties). The raw backing field continues to be surfaced as a ref (phase 1 behavior is unchanged; this is additive). Only fields reached through anonymous holders are forwarded; bitfields under a named holder are not. Gated on C# 11 like the rest of the feature. Adds syntax-shape unit tests (PSAPI_WORKING_SET_EX_BLOCK, including the named-holder negative case and the C# 10 gate) and functional tests proving the forwarded properties alias the same storage, pack without bit overlap (exact backing-field bit pattern asserted), and isolate neighbors. Validated across net8.0, net472, and net10.0. * Fix SA1025 in bitfield test comment alignment The aligned trailing comments in FlattenedBitfieldPropertiesPackWithoutOverlap used multiple consecutive spaces, which fails the CI -warnAsError build (SA1025). Collapse to a single space before each comment.
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.
Addresses #408.
What
Adds an opt-out generator option,
FlattenNestedAnonymousTypes, that surfaces fields nested inside anonymous structs/unions as[UnscopedRef]ref-returning properties on the declaring struct. This lets callers writevalue.fieldinstead ofvalue.Anonymous.Anonymous.field, with full read / write / pointer support — the shape I suggested in the issue and thatdotnet/winforms'VARIANTuses.Generated shape:
Key points to review
true.UnscopedRefAttribute). On older language versions no accessors are emitted and theAnonymousholder remains the only path. Works on .NET Framework via the existing polyfill.Anonymous/Anonymous1/… and that its type is a nested type of the struct. Named nested unions (e.g.KEY_EVENT_RECORD.uChar) are deliberately left alone.this.Anonymous.Anonymous.field), so every nesting level stays usable. Collisions and already-reserved names are skipped.<inheritdoc>. Accessors inherit from the underlying field. Because nested anonymous leaf fields were previously undocumented, this also propagates the declaring type's API-doc field summaries down to those leaf fields so the inherited docs resolve. This required a small refactor ofGenerator.ApiDocs.cs— note: that file's large diff is relocation, not behavior change (EmitDoc/EmitLinepromoted to private static methods;ApplyFieldDocsextracted). Existing doc behavior is unchanged (full suite green).Two bugs caught & fixed during development
A broad stress-test (enabling the option across the whole
GenerationSandbox.Testssurface) surfaced two real issues, each now covered by a dedicated regression test in both marshaling modes:refreturn type mismatches. (Guard:ELEMDESC.)_unmanagedsuffix, so theinheritdoccref must use the mangled name. (Guard:PROPVARIANT, 3 levels deep.)Testing
Microsoft.Windows.CsWin32.Testssuite: 781 passed, 0 failed (net8.0).[UnscopedRef], numbered/multiple holders (DECIMAL), unsafe pointer leaves (VARDESC), public visibility, named-union exclusion, C#10 no-op, no-op for non-anonymous structs (LUID), skipped reinterpreted array leaves (BLUETOOTH_ADDRESS), doc inheritance, plus the two regression guards.GenerationSandbox.Tests(SYSTEM_INFOread/write aliasing + pointer use); whole sandbox (139 tests) compiles & passes across net472/net8/net9/net10.Notes for reviewers
get/setaccessors for fuller coverage.