Fix config binder source gen for a sole read-only collection ctor param#131358
Fix config binder source gen for a sole read-only collection ctor param#131358TemRevil wants to merge 2 commits intodotnet:maindotnet/runtime:mainfrom TemRevil:fix/config-binder-solo-readonly-collection-ctor-paramTemRevil/runtime:fix/config-binder-solo-readonly-collection-ctor-paramCopy head branch name to clipboard
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.
| Assert.NotNull(result.GeneratedSource); | ||
| Assert.Empty(result.Diagnostics); | ||
|
|
||
| AssertCanCreateAssemblyImage(result.OutputCompilation); |
There was a problem hiding this comment.
The assertions stop at compile-clean plus a produced assembly image. Since this fixes a source generator vs reflection parity regression, consider also asserting runtime binding behavior: populate an in-memory configuration with a value for Values, call Get<Options>(), and assert the resulting collection contents. A generator can emit compilable but functionally wrong binding, and only a functional assertion guards against that.
There was a problem hiding this comment.
Reopening: this new functional assertion is failing in CI across 7 configurations (osx/linux/alpine/windows) in the current run.
System.IO.FileLoadException : Could not load file or assembly 'test.dll'.
A different copy of assembly 'test.dll' is already loaded.
at ...LoadAndInvokeMain(...) GeneratorTests.Helpers.cs:line 244
at ...SoleReadOnlyCollectionConstructorParameterIsBindable(String collectionType) GeneratorTests.cs:line 586
Root cause: LoadAndInvokeMain loads the compiled image into AssemblyLoadContext.Default, and every theory case compiles an assembly with the same simple name test. The first InlineData case loads fine; each subsequent case collides with the already-loaded identity and throws. The new complex-element test will hit the same problem when it runs in the same process.
Two ways to fix:
- Give each compilation a unique assembly name (for example derive it from
collectionTypeor a GUID) so the identities do not collide. This is the smaller change and matches how these tests otherwise isolate compilations. - Load each image into a fresh collectible
AssemblyLoadContextper invocation instead ofDefault.
Please apply one of these and confirm the theory actually runs green in CI (check the test count in the log).
| } | ||
| } | ||
|
|
||
| public record Options({{collectionType}}<string> Values); |
There was a problem hiding this comment.
Two coverage gaps around this shape:
- Sole member plus complex element type is not exercised. The existing
ComplexReadOnlyListConstructorParameterIsBindablepairs the collection with a bindableName(soHasBindableMembersis true, the old path). Here the element isstring. The intersection, for examplerecord Options(IReadOnlyList<Child> Values)with no other member, is what newly drives the property walk transitive registration andRegisterForGen_AsConfigWithChildrenHelperin the changed code path, and it is untested. - Nested usage is not exercised. Reaching this type as a member of an outer bound type goes through
BindCoreandEmitObjectInitrather than top levelGetCore, which is a distinct emission path. It very likely works, but a nested case would lock it in.
… 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:
|
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).