Fix config binder source gen for a sole read-only collection ctor param - #131358
Conversation
The configuration binder source generator emitted a call to an Initialize method that was never generated for a parameterized- constructor type whose only member is a non-bindable copy-constructor collection parameter (IReadOnlyList<T>, IReadOnlyCollection<T>, IReadOnlySet<T>, or IEnumerable<T>, with no other bindable property). The reflection binder already handled this shape correctly, so this was a parity gap that broke the build with CS0103 instead. The registration pass only registered a type for Initialize-method generation inside the HasBindableMembers check, but constructor parameters are bound in Initialize independently of whether the type has any other bindable property. Register the Initialize method (and walk the constructor-parameter properties needed to bind it) whenever the type has a parameterized constructor, regardless of HasBindableMembers. Verified by running the incremental generator directly against the repro from the issue (record with a single IReadOnlyList<string> constructor parameter): before this change the generated source calls InitializeOptions but never defines it, reproducing CS0103 exactly; after this change the method is generated and binds the parameter correctly. Confirmed for all four affected collection interfaces. Fixes dotnet#131320
|
Azure Pipelines: Successfully started running 3 pipeline(s). 13 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
|
Tagging subscribers to this area: @dotnet/area-extensions-configuration |
tarekgh
left a comment
There was a problem hiding this comment.
A few test-hardening notes on the new regression test. The product change itself looks correct and well targeted; these are non-blocking suggestions. One additional stylistic thought: the sibling ReadOnlyCollectionConstructorParameterIsBindable, ComplexReadOnlyListConstructorParameterIsBindable, and this new Sole... test all cover variations of the same shape, so they could eventually be consolidated into a single data-driven theory.
… fix Compiling proved the Initialize method gets emitted, but never asserted the generated code binds the right values. Extend the existing theory to populate real config and check the bound collection contents for all four interface shapes (including IEnumerable, which still routes through the same CopyConstructor/HasBindableMembers=false path as the others). Add a sibling test for a complex (non-string) element type, the other gap called out in review.
|
Added functional coverage for both gaps, pushed as On whether The nested case turned up something real, though not what either of us expected. Binding Since it's a different root cause in a different file, I didn't fold a fix into this PR without checking first. Happy to open a follow-up issue with the repro, take a shot at fixing it separately, or fold it into this PR if you'd rather keep it together, whichever you prefer. |
|
Thanks for the thorough follow-up. The functional assertions and the complex-element sole-member test look good, and confirming that On the nested-usage gap: since the null-binding you found for Two small things before I sign off:
|
Every generator test project shares the same assembly name ('test',
from RoslynTestUtils.CreateTestProject), and LoadAndInvokeMain loads
into AssemblyLoadContext.Default, which never unloads. When more than
one theory case in this test run reaches LoadAndInvokeMain, the second
load collides with the first under the identical assembly identity
(FileLoadException: a different copy of assembly 'test.dll' is already
loaded). Rename the compilation to a fresh unique name right before
emitting so concurrent/sequential loads in the same process don't
collide.
Also note in a test comment that nested binding of this same shape is
tracked separately in dotnet#131399, filed per review
discussion on this PR.
|
Pushed ab00bf5: each compilation now gets renamed to a unique name right before Couldn't run the real suite locally (Arcade still isn't tractable in this environment), so I reproduced the exact failure standalone instead: two Roslyn compilations sharing the Filed #131399 for the nested-binding gap with the repro and root cause, and added a note in Will watch the CI run and confirm the theory cases actually execute this time, not just compile. |
The test project multitargets NetCoreAppCurrent and NetFrameworkCurrent. System.Runtime.Loader.AssemblyLoadContext does not exist on .NET Framework, so the net481 leg failed to compile with CS0234, even though the calling theories are already gated to NetCore at runtime via PlatformDetection.IsNetCore. Wrapped the using directive and the method body in #if NET, matching the same split already used a few lines up in this file for baseline paths.
|
One more thing turned up once CI ran on the last push: Wrapped the using directive and the method body in Couldn't run this through Arcade either, so I checked it with a throwaway project instead: same |
|
Thanks for shepherding this one through, @tarekgh. Your test-hardening notes made the regression test noticeably better than what I first pushed, and the pointer about the net481 target saved me a round trip. I enjoyed digging around the configuration binder source generator, so if there's anything else in that area you'd like a hand with, I'm happy to pick one up. |
Fixes #131320
The configuration binder source generator emitted an uncompilable call to an Initialize method that was never generated, for a parameterized-constructor type whose only member is a non-bindable copy-constructor collection parameter (a positional record with a single IReadOnlyList, IReadOnlyCollection, IReadOnlySet, or IEnumerable parameter and no other bindable property). The reflection binder handles this shape correctly, so this was a parity regression that broke the build instead.
Root cause:
BindingHelperInfo.Builder.TryRegisterTransitiveTypesForMethodGenonly registered a type for Initialize-method generation inside theHasBindableMembers(objectSpec)check. But constructor parameters are bound inInitializeindependently of whether the type has any other bindable property, per the comment already on the ctor-param property loop a few lines above. For a type where the only "property" is a ctor param backed by a non-bindable read-only collection type,HasBindableMembersis false, soInitializenever got registered/emitted, while the emitter'sEmitBindingLogic/EmitObjectInitstill unconditionally callsInitializeXxx(...)for anyParameterizedConstructortype.Fix: register the Initialize method (and walk the constructor-parameter properties needed to bind it) whenever the type has a parameterized constructor, regardless of
HasBindableMembers.BindCoreregistration stays gated onHasBindableMembersas before, since that part is unrelated.Verified by running the incremental generator directly against the repro from the issue:
Before this change the generated source calls
InitializeOptionsbut never defines it, reproducingCS0103: The name 'InitializeOptions' does not exist in the current contextexactly. After this change the method is generated and binds the parameter correctly. Confirmed for all four affected collection interfaces (IReadOnlyList, IReadOnlyCollection, IReadOnlySet, IEnumerable).Added
SoleReadOnlyCollectionConstructorParameterIsBindablenext to the existingReadOnlyCollectionConstructorParameterIsBindabletest, covering the case where the collection parameter is the type's only member (the existing test always paired it with a second, ordinarily-bindable property, so it didn't exercise this gap).