Repository navigation
[XAML SourceGen] Make compiled bindings null-tolerant for intermediate members - #36444
Conversation
Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com>
…paths Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com>
…eference intermediates (#36313) Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com>
…list in Setter Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com>
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 36444Or
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 36444" |
|
Azure Pipelines: Successfully started running 1 pipeline(s). There may be pipelines that require an authorized user to comment /azp run to run. |
There was a problem hiding this comment.
Pull request overview
This PR updates the MAUI XAML Source Generator’s compiled-binding path emission to be null-tolerant when intermediate members are reference types, aligning generated bindings with the long-standing runtime/XamlC behavior (i.e., avoiding NullReferenceException when an intermediate is null at runtime, even if NRT annotations say otherwise).
Changes:
- Generate
?./?[...]for intermediate path parts based on runtime nullability (reference types + nullable value types), and propagate resulting nullability soTypedBinding<,>generic arguments compile correctly. - Add
ITypeSymbolExtensions.CanBeNullAtRuntime()to decouple binding path null-conditional decisions from nullable reference type annotations. - Update/extend SourceGen unit tests to expect null-conditional member/indexer/array access and add a regression test for non-nullable-declared reference intermediates.
Show a summary per file
| File | Description |
|---|---|
| src/Controls/src/SourceGen/CompiledBindingMarkup.cs | Updates binding path parsing to emit conditional access based on runtime-nullability and propagates binding nullability. |
| src/Controls/src/BindingSourceGen/ITypeSymbolExtensions.cs | Adds CanBeNullAtRuntime() helper for runtime-nullability checks. |
| src/Controls/src/BindingSourceGen/Setter.cs | Adjusts setter path-walk to avoid Last() enumeration and adds a capture to avoid CS1612 for certain conditional-access value-type intermediates. |
| src/Controls/tests/SourceGen.UnitTests/InitializeComponent/CompiledBindings.cs | Updates expectations for null-conditional output and adds a regression test for null-tolerant access through non-nullable reference intermediates. |
| src/Controls/tests/SourceGen.UnitTests/InitializeComponent/IndexerBindings.cs | Updates expectations for null-conditional indexer/array access and nullable TypedBinding type arguments. |
| src/Controls/tests/SourceGen.UnitTests/Maui13856Tests.cs | Updates expectation for null-conditional enum indexer access. |
Copilot's findings
- Files reviewed: 6/6 changed files
- Comments generated: 0
|
/azp run maui-pr-devicetests |
|
/azp run maui-ui-devicetests |
|
No pipelines are associated with this pull request. |
|
Azure Pipelines: Successfully started running 1 pipeline(s). |
Making intermediate members null-tolerant widens the generated TypedBinding's TProperty to a nullable type. That fed into the setter's `value is null` early return, so a two-way binding through any multi-hop path silently dropped null writes - e.g. clearing a selection became a no-op. NRT annotations are not enforced at runtime, and neither of the other two inflators does this: BindingExpression assigns null directly, and XamlC's CompiledBindingGetSetter emits no null check at all. Assign through with the null-forgiving operator instead, keeping the generated code CS8601-clean. The early return is retained for non-nullable value types, which genuinely cannot be assigned null. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 10f057bb-7603-49db-aaf3-707c0cf0ddff
A two-way binding through a value-type indexer result, such as
{Binding Items[0].Name} over a List<SomeStruct>, generated
'p0[0].Name = value;'. An indexer returns a copy rather than a variable, so
that fails to compile with CS1612. The conditional-access branch already
captured value-type *members* into a local for this reason; indexers need the
same treatment, in both the conditional and non-conditional positions.
Array element access must be excluded: unlike an indexer it yields a genuine
variable, so capturing would assign to a copy and silently drop the value.
Arrays were previously identified by an empty DefaultMemberName, which only
holds for the XAML generator - PathParser names array access 'Item' as well.
Make it explicit with IndexAccess.IsArrayElement so both generators agree.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 10f057bb-7603-49db-aaf3-707c0cf0ddff
|
I reviewed this and pushed two follow-up commits to the branch. The core change is right — SourceGen was the only inflator dereferencing intermediates unconditionally, and deciding based on NRT annotations is unsound since they aren't enforced at runtime. Worth noting the actual NRE in #36313 comes from Two things I fixed on top: 1. Two-way bindings had stopped writing
|
|
/azp run maui-pr-uitests,maui-pr-devicetests |
|
Azure Pipelines: Successfully started running 2 pipeline(s). |
|
Azure Pipelines: Successfully started running 2 pipeline(s). |
This comment has been minimized.
This comment has been minimized.
|
@kubaflo Could you please take another look? I investigated the current failing CI lanes, and they appear unrelated to this PR:
The CI build covers the current head ( |
This comment has been minimized.
This comment has been minimized.
MauiBot
left a comment
There was a problem hiding this comment.
Expert Review — 2 findings
See inline comments for details.
| var copyBackTarget = accessAccumulator; | ||
| accessAccumulator = AccessExpressionBuilder.ExtendExpression(accessAccumulator, part); | ||
| AddPatternMatchingExpression("{}"); | ||
| copyBackStatements.Add(BuildAssignmentStatement(copyBackTarget, part, accessAccumulator)); |
There was a problem hiding this comment.
🔍 AI-Generated Review (multi-model)
[major] Logic and Correctness — The copy-back statement is emitted whenever an intermediate path part is a value-type property/indexer, but nothing verifies that this intermediate member is writable. NeedsValueTypeCapture only inspects IsValueType/Kind/IsArrayElement, and in CompiledBindingMarkup.TryParsePath the IsWritable flag is recomputed and overwritten on every iteration, so only the last part's writability is ever checked.
Concrete failing scenario (two-way binding, Mode=TwoWay):
public class VM { public Options Opts { get; } = new(); } // get-only
public struct Options { public bool IsEnabled { get; set; } }with {Binding Opts.IsEnabled, Mode=TwoWay} now generates source.Opts is {} p0; p0.IsEnabled = value; source.Opts = p0; → CS0200 "Property or indexer 'VM.Opts' cannot be assigned to — it is read-only", i.e. the user's build breaks in generated code. The same applies to the ConditionalAccess branch (line 64) and to a mid-path read-only indexer, e.g. IReadOnlyList<ItemStruct> Items with {Binding Items[0].Name, Mode=TwoWay} → p0[0] = p1; → CS0200. Before this PR these shapes compiled (they silently no-op'd), so this converts a silent no-op into a hard build failure.
Suggested fix: thread the intermediate member's writability into the path part (or into SetterOptions) and, when an intermediate value-type member is not assignable, suppress setter generation and report the existing read-only diagnostic (the same treatment ReadOnlyIndexer already gets for a final part) instead of emitting uncompilable code.
| } | ||
|
|
||
| [Fact] | ||
| public void TwoWayBindingThroughValueTypeIndexerResultCompiles() |
There was a problem hiding this comment.
🔍 AI-Generated Review (multi-model)
[moderate] Regression Prevention and Test Coverage — The new copy-back branch is only covered for the positive cases (writable indexer, array element, value-type field). There is no test for the negative case that the new branch must not break: an intermediate value-type member that is not assignable — e.g. a get-only struct property (public Options Opts { get; }) or a read-only indexer (IReadOnlyList<ItemStruct> Items) in the middle of a Mode=TwoWay path. Because RunGenerator compiles the generated code, such a test would immediately surface the CS0200 build break described on Setter.cs. Please add a test for a mid-path non-writable value-type member alongside TwoWayBindingThroughValueTypeIndexerResultCompiles.
MauiBot
left a comment
There was a problem hiding this comment.
AI Review Summary
@copilot — new AI review results are available based on commit
121588c.
🗂️ Review Sessions — click to expand
🚦 Gate — Test Before & After Fix
Gate Result: ✅ PASSED
Platform: IOS · Base: main · Merge base: 19a18889
✅ Verified (new API / feature) — this PR adds new API and a test that references it in the same project, so reverting the fix un-compiles the test: there is no valid "fails without the fix" baseline to establish (a compile-coupled baseline). The gate instead verified the fix by a clean build + pass with the fix, so this is a real PASS rather than a non-committal INCONCLUSIVE.
| Test | Without Fix (expect FAIL) | With Fix (expect PASS) |
|---|---|---|
🧪 BindingRepresentationGenTests BindingRepresentationGenTests |
🛠️ BUILD ERROR | ✅ PASS — 24s |
🧪 CompiledBindings CompiledBindings |
✅ FAIL — 16s | ✅ PASS — 7s |
🧪 IndexerBindings IndexerBindings |
✅ FAIL — 8s | ✅ PASS — 5s |
🧪 Maui13856Tests Maui13856Tests |
✅ FAIL — 7s | ✅ PASS — 5s |
🧪 SetterBuilderTests SetterBuilderTests |
🛠️ BUILD ERROR | ✅ PASS — 4s |
🔴 Without fix — 🧪 BindingRepresentationGenTests: 🛠️ BUILD ERROR · 52s
Error-relevant lines (filtered from the build log):
/Users/cloudtest/vss/_work/1/s/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs(376,33): error CS1739: The best overload for 'IndexAccess' does not have a parameter named 'IsArrayElement' [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/BindingSourceGen.UnitTests/Controls.BindingSourceGen.UnitTests.csproj]
/Users/cloudtest/vss/_work/1/s/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs(561,32): error CS1739: The best overload for 'IndexAccess' does not have a parameter named 'IsArrayElement' [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/BindingSourceGen.UnitTests/Controls.BindingSourceGen.UnitTests.csproj]
/Users/cloudtest/vss/_work/1/s/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs(970,51): error CS1739: The best overload for 'IndexAccess' does not have a parameter named 'IsArrayElement' [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/BindingSourceGen.UnitTests/Controls.BindingSourceGen.UnitTests.csproj]
/Users/cloudtest/vss/_work/1/s/src/Controls/tests/BindingSourceGen.UnitTests/SetterBuilderTests.cs(178,50): error CS1061: 'Setter' does not contain a definition for 'CopyBackStatements' and no accessible extension method 'CopyBackStatements' accepting a first argument of type 'Setter' could be found (are you missing a using directive or an assembly reference?) [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/BindingSourceGen.UnitTests/Controls.BindingSourceGen.UnitTests.csproj]
/Users/cloudtest/vss/_work/1/s/src/Controls/tests/BindingSourceGen.UnitTests/SetterBuilderTests.cs(193,40): error CS1061: 'Setter' does not contain a definition for 'CopyBackStatements' and no accessible extension method 'CopyBackStatements' accepting a first argument of type 'Setter' could be found (are you missing a using directive or an assembly reference?) [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/BindingSourceGen.UnitTests/Controls.BindingSourceGen.UnitTests.csproj]
/Users/cloudtest/vss/_work/1/s/src/Controls/tests/BindingSourceGen.UnitTests/SetterBuilderTests.cs(203,47): error CS1739: The best overload for 'IndexAccess' does not have a parameter named 'IsArrayElement' [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/BindingSourceGen.UnitTests/Controls.BindingSourceGen.UnitTests.csproj]
/Users/cloudtest/vss/_work/1/s/src/Controls/tests/BindingSourceGen.UnitTests/SetterBuilderTests.cs(208,23): error CS1061: 'Setter' does not contain a definition for 'CopyBackStatements' and no accessible extension method 'CopyBackStatements' accepting a first argument of type 'Setter' could be found (are you missing a using directive or an assembly reference?) [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/BindingSourceGen.UnitTests/Controls.BindingSourceGen.UnitTests.csproj]
/Users/cloudtest/vss/_work/1/s/src/Controls/tests/BindingSourceGen.UnitTests/SetterBuilderTests.cs(221,23): error CS1061: 'Setter' does not contain a definition for 'CopyBackStatements' and no accessible extension method 'CopyBackStatements' accepting a first argument of type 'Setter' could be found (are you missing a using directive or an assembly reference?) [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/BindingSourceGen.UnitTests/Controls.BindingSourceGen.UnitTests.csproj]
/Users/cloudtest/vss/_work/1/s/src/Controls/tests/BindingSourceGen.UnitTests/SetterBuilderTests.cs(245,11): error CS1061: 'Setter' does not contain a definition for 'CopyBackStatements' and no accessible extension method 'CopyBackStatements' accepting a first argument of type 'Setter' could be found (are you missing a using directive or an assembly reference?) [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/BindingSourceGen.UnitTests/Controls.BindingSourceGen.UnitTests.csproj]
🟢 With fix — 🧪 BindingRepresentationGenTests: PASS ✅ · 24s
(no coded error found; showing last 1200 chars)
IndexerWithIndexerNameAttribute [49 ms]
Passed BindingSourceGen.UnitTests.BindingRepresentationGenTests.GenerateBindingWhenNullableDisabledAndPropertyReferenceType [59 ms]
Passed BindingSourceGen.UnitTests.BindingRepresentationGenTests.SetsIsWritableTrueWhenPropertyComesFromMutableCollection [50 ms]
Passed BindingSourceGen.UnitTests.BindingRepresentationGenTests.GenerateBindingWhenGetterContainsCustomIndexerWithoutAttributes [49 ms]
Passed BindingSourceGen.UnitTests.BindingRepresentationGenTests.GenerateSimpleBinding [49 ms]
Passed BindingSourceGen.UnitTests.BindingRepresentationGenTests.SetsIsWritableFalseWhenCustomIndexerHasNoSetter [91 ms]
Passed BindingSourceGen.UnitTests.BindingRepresentationGenTests.SetsIsWritableFalseWhenPropertyComesFromImmutableCollection [65 ms]
Passed BindingSourceGen.UnitTests.BindingRepresentationGenTests.SetsIsWritableWhenElementAccessIsConditional [70 ms]
[xUnit.net 00:00:03.04] Finished: Controls.BindingSourceGen.UnitTests
Passed BindingSourceGen.UnitTests.BindingRepresentationGenTests.GenerateBindingWhenGetterContainsComplexCombinedIndexers [71 ms]
Test Run Successful.
Total tests: 37
Passed: 37
Total time: 3.3379 Seconds
🔴 Without fix — 🧪 CompiledBindings: FAIL ✅ · 16s
Error-relevant lines (filtered from the build log):
at Microsoft.Maui.Controls.SourceGen.UnitTests.CompiledBindings.CodeIsEqual(String expectedCode, String actualCode) in /_/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/CompiledBindings.cs:line 298
at Microsoft.Maui.Controls.SourceGen.UnitTests.CompiledBindings.CanDetectXDataType() in /_/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/CompiledBindings.cs:line 156
at System.Reflection.MethodBaseInvoker.InterpretedInvoke_Method(Object obj, IntPtr* args)
at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)
at Microsoft.Maui.Controls.SourceGen.UnitTests.CompiledBindings.NullTolerantThroughNonNullableReferenceIntermediates() in /_/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/CompiledBindings.cs:line 1063
at Microsoft.Maui.Controls.SourceGen.UnitTests.CompiledBindings.TwoWayBindingThroughIntermediatesStillWritesNull() in /_/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/CompiledBindings.cs:line 1125
at Microsoft.Maui.Controls.SourceGen.UnitTests.CompiledBindings.NonNullableReferenceTypeAtEndOfConditionalAccessPath() in /_/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/CompiledBindings.cs:line 881
at Microsoft.Maui.Controls.SourceGen.UnitTests.CompiledBindings.CorrectlyDetectsNullableReferenceTypes() in /_/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/CompiledBindings.cs:line 448
🟢 With fix — 🧪 CompiledBindings: PASS ✅ · 7s
(no coded error found; showing last 1200 chars)
ng .}") [24 ms]
Passed Microsoft.Maui.Controls.SourceGen.UnitTests.CompiledBindings.CanCompileSelfBinding(bindingExpression: "{Binding}") [19 ms]
Passed Microsoft.Maui.Controls.SourceGen.UnitTests.CompiledBindings.CanCompileSelfBinding(bindingExpression: "{Binding Path=.}") [11 ms]
[xUnit.net 00:00:01.15] Finished: Microsoft.Maui.Controls.SourceGen.UnitTests
Passed Microsoft.Maui.Controls.SourceGen.UnitTests.CompiledBindings.CorrectlyDetectsNullableTypes [11 ms]
Passed Microsoft.Maui.Controls.SourceGen.UnitTests.CompiledBindings.NonNullableReferenceTypeAtEndOfConditionalAccessPath [12 ms]
Passed Microsoft.Maui.Controls.SourceGen.UnitTests.CompiledBindings.NullableReferenceTypeAtEndOfConditionalAccessPath [11 ms]
Passed Microsoft.Maui.Controls.SourceGen.UnitTests.CompiledBindings.CorrectlyDetectsNullableReferenceTypes [18 ms]
Passed Microsoft.Maui.Controls.SourceGen.UnitTests.CompiledBindings.CorrectlyDetectsNullableReferenceTypesWithNonNullableTarget [12 ms]
Passed Microsoft.Maui.Controls.SourceGen.UnitTests.CompiledBindings.TargetNullValueWithNullablePathGeneratesValidCode [13 ms]
Test Run Successful.
Total tests: 15
Passed: 15
Total time: 1.4176 Seconds
🔴 Without fix — 🧪 IndexerBindings: FAIL ✅ · 8s
Error-relevant lines (filtered from the build log):
at Microsoft.Maui.Controls.SourceGen.UnitTests.IndexerBindings.TwoWayBindingThroughValueTypeArrayElementIsNotCaptured() in /_/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/IndexerBindings.cs:line 725
at System.Reflection.MethodBaseInvoker.InterpretedInvoke_Method(Object obj, IntPtr* args)
at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)
at Microsoft.Maui.Controls.SourceGen.UnitTests.IndexerBindings.BindingToArrayElement() in /_/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/IndexerBindings.cs:line 305
at Microsoft.Maui.Controls.SourceGen.UnitTests.IndexerBindings.BindingToDictionaryIndexer() in /_/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/IndexerBindings.cs:line 254
at Microsoft.Maui.Controls.SourceGen.UnitTests.IndexerBindings.BindingToListIndexer() in /_/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/IndexerBindings.cs:line 196
at Microsoft.Maui.Controls.Xaml.UnitTests.SourceGen.SourceGeneratorDriver.RunGenerator[T](Compilation compilation, AdditionalFile[] additionalFiles, Boolean assertNoCompilationErrors) in /_/src/Controls/tests/SourceGen.UnitTests/SourceGeneratorDriver.cs:line 55
at Microsoft.Maui.Controls.Xaml.UnitTests.SourceGen.SourceGeneratorDriver.RunGenerator[T](Compilation compilation, AdditionalFile additionalFile, Boolean assertNoCompilationErrors) in /_/src/Controls/tests/SourceGen.UnitTests/SourceGeneratorDriver.cs:line 24
at Microsoft.Maui.Controls.SourceGen.UnitTests.SourceGenXamlInitializeComponentTestBase.RunGenerator(String xaml, String code, String noWarn, String targetFramework, String path, String lineinfo, Boolean assertNoCompilationErrors, Boolean enablePreviewFeatures) in /_/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/SourceGenXamlInitializeComponentTests.cs:line 25
at Microsoft.Maui.Controls.SourceGen.UnitTests.IndexerBindings.TwoWayBindingThroughValueTypeIndexerResultCompiles() in /_/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/IndexerBindings.cs:line 660
at Microsoft.Maui.Controls.SourceGen.UnitTests.IndexerBindings.BindingToStringIndexer() in /_/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/IndexerBindings.cs:line 549
at Microsoft.Maui.Controls.SourceGen.UnitTests.IndexerBindings.BindingToDefaultIndexer() in /_/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/IndexerBindings.cs:line 144
at Microsoft.Maui.Controls.SourceGen.UnitTests.IndexerBindings.BindingToNestedIndexer() in /_/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/IndexerBindings.cs:line 374
at Microsoft.Maui.Controls.SourceGen.UnitTests.IndexerBindings.BindingToCustomIndexerWithIndexerNameAttribute() in /_/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/IndexerBindings.cs:line 78
at Microsoft.Maui.Controls.SourceGen.UnitTests.IndexerBindings.BindingToIndexerWithTwoWayMode() in /_/src/Controls/tests/SourceGen.UnitTests/InitializeComponent/IndexerBindings.cs:line 436
🟢 With fix — 🧪 IndexerBindings: PASS ✅ · 5s
(no coded error found; showing last 1200 chars)
trols.SourceGen.UnitTests.IndexerBindings.BindingToArrayElement [16 ms]
Passed Microsoft.Maui.Controls.SourceGen.UnitTests.IndexerBindings.BindingToDictionaryIndexer [20 ms]
Passed Microsoft.Maui.Controls.SourceGen.UnitTests.IndexerBindings.BindingToNullableIndexer [13 ms]
Passed Microsoft.Maui.Controls.SourceGen.UnitTests.IndexerBindings.BindingToListIndexer [12 ms]
Passed Microsoft.Maui.Controls.SourceGen.UnitTests.IndexerBindings.TwoWayBindingThroughValueTypeIndexerResultCompiles [14 ms]
Passed Microsoft.Maui.Controls.SourceGen.UnitTests.IndexerBindings.BindingToStringIndexer [19 ms]
Passed Microsoft.Maui.Controls.SourceGen.UnitTests.IndexerBindings.BindingToDefaultIndexer [17 ms]
[xUnit.net 00:00:01.05] Finished: Microsoft.Maui.Controls.SourceGen.UnitTests
Passed Microsoft.Maui.Controls.SourceGen.UnitTests.IndexerBindings.BindingToNestedIndexer [13 ms]
Passed Microsoft.Maui.Controls.SourceGen.UnitTests.IndexerBindings.BindingToCustomIndexerWithIndexerNameAttribute [13 ms]
Passed Microsoft.Maui.Controls.SourceGen.UnitTests.IndexerBindings.BindingToIndexerWithTwoWayMode [18 ms]
Test Run Successful.
Total tests: 12
Passed: 12
Total time: 1.3214 Seconds
🔴 Without fix — 🧪 Maui13856Tests: FAIL ✅ · 7s
Error-relevant lines (filtered from the build log):
at Microsoft.Maui.Controls.Xaml.UnitTests.SourceGen.Maui13856Tests.DictionaryWithEnumKeyBindingDoesNotCauseErrors() in /_/src/Controls/tests/SourceGen.UnitTests/Maui13856Tests.cs:line 85
at System.Reflection.MethodBaseInvoker.InterpretedInvoke_Method(Object obj, IntPtr* args)
at System.Reflection.MethodBaseInvoker.InvokeWithNoArgs(Object obj, BindingFlags invokeAttr)
🟢 With fix — 🧪 Maui13856Tests: PASS ✅ · 5s
Determining projects to restore...
All projects are up-to-date for restore.
Test run for /Users/cloudtest/vss/_work/1/s/artifacts/bin/SourceGen.UnitTests/Debug/net10.0/Microsoft.Maui.Controls.SourceGen.UnitTests.dll (.NETCoreApp,Version=v10.0)
VSTest version 18.0.1 (arm64)
Starting test execution, please wait...
A total of 1 test files matched the specified pattern.
[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v2.8.2+699d445a1a (64-bit .NET 10.0.0)
[xUnit.net 00:00:00.04] Discovering: Microsoft.Maui.Controls.SourceGen.UnitTests
[xUnit.net 00:00:00.06] Discovered: Microsoft.Maui.Controls.SourceGen.UnitTests
[xUnit.net 00:00:00.07] Starting: Microsoft.Maui.Controls.SourceGen.UnitTests
[xUnit.net 00:00:00.92] Finished: Microsoft.Maui.Controls.SourceGen.UnitTests
Passed Microsoft.Maui.Controls.Xaml.UnitTests.SourceGen.Maui13856Tests.DictionaryWithEnumKeyBindingDoesNotCauseErrors [825 ms]
Test Run Successful.
Total tests: 1
Passed: 1
Total time: 1.2310 Seconds
🔴 Without fix — 🧪 SetterBuilderTests: 🛠️ BUILD ERROR · 5s
Error-relevant lines (filtered from the build log):
/Users/cloudtest/vss/_work/1/s/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs(376,33): error CS1739: The best overload for 'IndexAccess' does not have a parameter named 'IsArrayElement' [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/BindingSourceGen.UnitTests/Controls.BindingSourceGen.UnitTests.csproj]
/Users/cloudtest/vss/_work/1/s/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs(561,32): error CS1739: The best overload for 'IndexAccess' does not have a parameter named 'IsArrayElement' [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/BindingSourceGen.UnitTests/Controls.BindingSourceGen.UnitTests.csproj]
/Users/cloudtest/vss/_work/1/s/src/Controls/tests/BindingSourceGen.UnitTests/BindingRepresentationGenTests.cs(970,51): error CS1739: The best overload for 'IndexAccess' does not have a parameter named 'IsArrayElement' [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/BindingSourceGen.UnitTests/Controls.BindingSourceGen.UnitTests.csproj]
/Users/cloudtest/vss/_work/1/s/src/Controls/tests/BindingSourceGen.UnitTests/SetterBuilderTests.cs(178,50): error CS1061: 'Setter' does not contain a definition for 'CopyBackStatements' and no accessible extension method 'CopyBackStatements' accepting a first argument of type 'Setter' could be found (are you missing a using directive or an assembly reference?) [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/BindingSourceGen.UnitTests/Controls.BindingSourceGen.UnitTests.csproj]
/Users/cloudtest/vss/_work/1/s/src/Controls/tests/BindingSourceGen.UnitTests/SetterBuilderTests.cs(193,40): error CS1061: 'Setter' does not contain a definition for 'CopyBackStatements' and no accessible extension method 'CopyBackStatements' accepting a first argument of type 'Setter' could be found (are you missing a using directive or an assembly reference?) [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/BindingSourceGen.UnitTests/Controls.BindingSourceGen.UnitTests.csproj]
/Users/cloudtest/vss/_work/1/s/src/Controls/tests/BindingSourceGen.UnitTests/SetterBuilderTests.cs(203,47): error CS1739: The best overload for 'IndexAccess' does not have a parameter named 'IsArrayElement' [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/BindingSourceGen.UnitTests/Controls.BindingSourceGen.UnitTests.csproj]
/Users/cloudtest/vss/_work/1/s/src/Controls/tests/BindingSourceGen.UnitTests/SetterBuilderTests.cs(208,23): error CS1061: 'Setter' does not contain a definition for 'CopyBackStatements' and no accessible extension method 'CopyBackStatements' accepting a first argument of type 'Setter' could be found (are you missing a using directive or an assembly reference?) [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/BindingSourceGen.UnitTests/Controls.BindingSourceGen.UnitTests.csproj]
/Users/cloudtest/vss/_work/1/s/src/Controls/tests/BindingSourceGen.UnitTests/SetterBuilderTests.cs(221,23): error CS1061: 'Setter' does not contain a definition for 'CopyBackStatements' and no accessible extension method 'CopyBackStatements' accepting a first argument of type 'Setter' could be found (are you missing a using directive or an assembly reference?) [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/BindingSourceGen.UnitTests/Controls.BindingSourceGen.UnitTests.csproj]
/Users/cloudtest/vss/_work/1/s/src/Controls/tests/BindingSourceGen.UnitTests/SetterBuilderTests.cs(245,11): error CS1061: 'Setter' does not contain a definition for 'CopyBackStatements' and no accessible extension method 'CopyBackStatements' accepting a first argument of type 'Setter' could be found (are you missing a using directive or an assembly reference?) [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/BindingSourceGen.UnitTests/Controls.BindingSourceGen.UnitTests.csproj]
🟢 With fix — 🧪 SetterBuilderTests: PASS ✅ · 4s
(no coded error found; showing last 1200 chars)
ilderTests.GeneratesSetterWithValueTypeInTheMiddle [< 1 ms]
Passed BindingSourceGen.UnitTests.SetterBuilderTests.GeneratesSetterWithoutCapturingValueTypeField [< 1 ms]
[xUnit.net 00:00:00.10] Finished: Controls.BindingSourceGen.UnitTests
Passed BindingSourceGen.UnitTests.SetterBuilderTests.GeneratesSetterWithInaccessibleFieldMemberAccess [< 1 ms]
Passed BindingSourceGen.UnitTests.SetterBuilderTests.GeneratesSetterWithCorrectConditionalAccess [< 1 ms]
Passed BindingSourceGen.UnitTests.SetterBuilderTests.GeneratesSetterWithSourceNotNullPatternMatchingForSinglePathStepWhenSourceTypeIsNullableAndConditionalAccess [< 1 ms]
Passed BindingSourceGen.UnitTests.SetterBuilderTests.GeneratesSetterWithPatternMatchingWithValueTypeCast2 [< 1 ms]
Passed BindingSourceGen.UnitTests.SetterBuilderTests.GeneratesSetterThatSetsInaccessibleProperty [< 1 ms]
Passed BindingSourceGen.UnitTests.SetterBuilderTests.GeneratesSetterCapturingValueTypeIndexerResultBehindConditionalAccess [< 1 ms]
Passed BindingSourceGen.UnitTests.SetterBuilderTests.GeneratesSetterWithInaccessiblePropertyMemberAccessInPath [< 1 ms]
Test Run Successful.
Total tests: 16
Passed: 16
Total time: 0.3698 Seconds
⚠️ Failure Details
- 🛠️ BindingRepresentationGenTests without fix: build failed before tests could run
/Users/cloudtest/vss/_work/1/s/src/Controls/tests/BindingSourceGen.UnitTests/SetterBuilderTests.cs(15,23): error CS1061: 'Setter' does not contain a definition for 'CopyBackStatements' and no accessib...
- 🛠️ SetterBuilderTests without fix: build failed before tests could run
/Users/cloudtest/vss/_work/1/s/src/Controls/tests/BindingSourceGen.UnitTests/SetterBuilderTests.cs(15,23): error CS1061: 'Setter' does not contain a definition for 'CopyBackStatements' and no accessib...
📁 Fix files reverted (6 files)
src/Controls/src/BindingSourceGen/BindingCodeWriter.cssrc/Controls/src/BindingSourceGen/ITypeSymbolExtensions.cssrc/Controls/src/BindingSourceGen/PathParser.cssrc/Controls/src/BindingSourceGen/PathPart.cssrc/Controls/src/BindingSourceGen/Setter.cssrc/Controls/src/SourceGen/CompiledBindingMarkup.cs
📋 Pre-Flight — Context & Validation
PR #36444 Pre-Flight
Context
- PR:
[XAML SourceGen] Make compiled bindings null-tolerant for intermediate members - Issue: #36313,
XAML Source Generation doesn't check for null references - Base:
main - Materialized PR commit:
937582ce254b7522ba8662b02ab6a6b05839e5ad - Platform requested by the review: iOS. The affected implementation is platform-neutral source-generator code, so validation uses its unit-test projects rather than an iOS device suite.
The bug is that XAML source generation emits direct access such as source.A.B.C.Text. A runtime null in any reference-type intermediate throws even when nullable-reference annotations declare that member non-null. Runtime bindings and XamlC tolerate the same condition.
Existing PR Approach
The PR changes six production files:
src/Controls/src/BindingSourceGen/BindingCodeWriter.cssrc/Controls/src/BindingSourceGen/ITypeSymbolExtensions.cssrc/Controls/src/BindingSourceGen/PathParser.cssrc/Controls/src/BindingSourceGen/PathPart.cssrc/Controls/src/BindingSourceGen/Setter.cssrc/Controls/src/SourceGen/CompiledBindingMarkup.cs
Direct diff inspection shows that the PR:
- Adds
CanBeNullAtRuntimeand treats every reference type as potentially null for intermediate-path conditional access. - Propagates widened nullability through members, arrays, and indexers.
- Distinguishes array-element lvalues from indexer-property rvalues.
- Captures value-type property/indexer intermediates and emits reverse-order copy-back statements.
- Allows two-way reference endpoints widened by a conditional path to receive
null, while preserving guards for non-nullable value types.
Five test files update exact generated-code expectations and cover null intermediates, null writes, indexers, arrays, and value-type copy-back.
Gate Evidence
The existing gate is PASSED and must not be rerun. It reports:
- Primary:
BindingRepresentationGenTests(37 passed with the fix). - Mandatory regressions:
SetterBuilderTests(16),CompiledBindings(15),IndexerBindings(12), andMaui13856Tests(1). - The baseline is compile-coupled for newly introduced test-facing APIs such as
Setter.CopyBackStatementsandIndexAccess.IsArrayElement; this is expected.
Only these targeted commands may be used by try-fix candidates:
dotnet test src/Controls/tests/BindingSourceGen.UnitTests/Controls.BindingSourceGen.UnitTests.csproj --filter "FullyQualifiedName~BindingRepresentationGenTests|FullyQualifiedName~SetterBuilderTests"
dotnet test src/Controls/tests/SourceGen.UnitTests/SourceGen.UnitTests.csproj --filter "FullyQualifiedName~CompiledBindings|FullyQualifiedName~IndexerBindings|FullyQualifiedName~Maui13856Tests"Both commands together are the candidate test command. No full suite or gate verification is permitted.
Attempt Constraints
- Each candidate must use a root-cause mechanism different from the PR and all recorded earlier candidates.
- Each invocation gets one implementation/test pass and at most one focused correction/retest.
- The
try-fixbaseline script defines the actual editable allow-list. Existing unrelated worktree changes are harness-owned and must remain untouched. - Every attempt must run the skill's inline expert self-review, capture its diff and test evidence, and restore using only
pwsh .github/scripts/EstablishBrokenBaseline.ps1 -Restore.
🔬 Code Review — Deep Analysis
Expert PR Evaluation
Verdict: NEEDS_CHANGES
The submitted fix correctly makes reference-type intermediates null-tolerant, propagates widened nullability through generated binding types, preserves two-way null writes, and adds reverse-order copy-back for writable value-type property and indexer intermediates. The array-element distinction and index-based final-part check are also sound.
Findings
❌ Error — Non-writable value-type intermediates emit uncompilable copy-back
src/Controls/src/BindingSourceGen/Setter.cs:64 and :74 emit copy-back assignments for every captured value-type property or indexer without retaining whether that intermediate member is writable. TryParsePath only exposes the final path part's IsWritable state. A two-way path through a get-only struct property or read-only struct-valued indexer therefore generates an assignment such as source.Options = p0; or p0[0] = p1;, producing CS0200 in generated code. Before this change these paths compiled, albeit without persisting the nested mutation, so the PR introduces a build regression.
❌ Error — Nullable value-type intermediates lose writes
A prior review identified an unresolved state transition in Setter.From: when a value-type part is followed by ConditionalAccess, the code deliberately skips capture and copy-back. For a writable MyStruct? Wrapped followed by Wrapped.Name, pattern matching unwraps Wrapped into a local and assigns local.Name, but no source.Wrapped = local statement is emitted. The generated setter compiles while silently discarding the update.
⚠️ Warning — Missing negative coverage
The added copy-back tests cover writable non-nullable properties/indexers, arrays, and fields, but not a get-only value-type intermediate or a nullable value-type intermediate in a two-way path.
Consolidated Improvement
Retain assignability for each parsed path part. If copy-back would require assigning through a non-writable intermediate, suppress setter generation using the existing read-only binding behavior rather than emitting invalid C#. Separately, preserve an outer copy-back assignment when conditional access unwraps a writable nullable value-type intermediate. Add focused coverage for both paths.
The trusted gate passed for the submitted scenarios, but it does not cover these newly identified negative cases.
Prior Review and CI Reconciliation
Earlier findings about array detection, unconditional field capture, and missing copy-back for writable non-nullable property/indexer results are fixed in the submitted HEAD. The two error-level cases above remain unresolved. The trusted Gate is green. The public live PR head differs from the materialized review commit and currently has failing and in-progress checks; anonymous APIs cannot identify the required-only subset, so no clean-CI claim is made.
🛠️ Try-Fix — Analysis & Comparison
PR #36444 Try-Fix Candidates
Candidate 1 — Receiver-Driven Guard Normalization
Model: claude-opus-5
Result: PASS
Attempt artifact: CustomAgentLogsTmp/PRState/36444/PRAgent/try-fix/attempt-1
Complete narrative and embedded diff: CustomAgentLogsTmp/PRState/36444/PRAgent/try-fix-1/content.md
Candidate 1 records the receiver type associated with each unguarded path part, then performs one normalization pass over the completed path. A part is wrapped in ConditionalAccess when its receiver can be null at runtime, and the binding's widened nullability is derived from the normalized path rather than maintained as parallel incremental state.
This differs from the PR's mechanism: the PR changes what feeds previousPartIsNullable and manually widens isNullable at each member/index append site. Candidate 1 removes those distributed wrap decisions, centralizes receiver guarding, and derives declared nullability from the resulting conditional-access structure. It retains the PR's required value-type capture/copy-back support.
The saved candidate diff changes the six baseline-allowed production files and no tests:
src/Controls/src/SourceGen/CompiledBindingMarkup.cs(+63/-30)src/Controls/src/BindingSourceGen/Setter.cs(+41/-4)src/Controls/src/BindingSourceGen/ITypeSymbolExtensions.cs(+9)src/Controls/src/BindingSourceGen/PathParser.cs(+13/-3)src/Controls/src/BindingSourceGen/PathPart.cs(+2/-2)src/Controls/src/BindingSourceGen/BindingCodeWriter.cs(+4)
The exact targeted command passed on the first implementation run with no correction:
BindingRepresentationGenTests|SetterBuilderTests: 53 passed, 0 failed.CompiledBindings|IndexerBindings|Maui13856Tests: 28 passed, 0 failed.
Inline expert self-review recorded one moderate finding: the parallel partReceivers and bindingPathParts lists rely on an implicit index-alignment invariant, although all three append sites were verified to update both. There were no critical or major findings. The final diff is stored in attempt-1/fix.diff; test evidence is in attempt-1/test-output.log.
The attempt restored all six baseline files with EstablishBrokenBaseline.ps1 -Restore reporting Restored True; no candidate changes or processes remain.
Candidate 2 — Atomic Typed Path Cursor
Model: gpt-5.6-sol
Result: PASS
Attempt artifact: CustomAgentLogsTmp/PRState/36444/PRAgent/try-fix/attempt-2
Complete narrative and embedded diff: CustomAgentLogsTmp/PRState/36444/PRAgent/try-fix-2/content.md
Candidate 2 introduces a private BindingPathBuilder that owns the current receiver type. Its atomic Append(access, resultType) operation decides whether the receiver requires conditional access, appends the guarded or unguarded part, advances the current type, and records whether any guard widened the binding value. Required value-type property/indexer capture and reverse copy-back behavior remains intact, as do direct lvalue writes for fields and array elements and two-way null writes for widened reference endpoints.
This differs from both earlier mechanisms. Unlike the PR, it has no distributed previousPartIsNullable propagation or branch-local guard/widen operations. Unlike Candidate 1, it has no parallel receiver list and no completed-path normalization pass. Receiver association, guard insertion, type advancement, and nullable widening are one structural transition, removing Candidate 1's index-alignment invariant.
The saved candidate diff changes five baseline-allowed production files and no tests:
src/Controls/src/BindingSourceGen/BindingCodeWriter.cs(+4/-0)src/Controls/src/BindingSourceGen/PathParser.cs(+13/-4)src/Controls/src/BindingSourceGen/PathPart.cs(+3/-2)src/Controls/src/BindingSourceGen/Setter.cs(+35/-5)src/Controls/src/SourceGen/CompiledBindingMarkup.cs(+77/-54)
ITypeSymbolExtensions.cs remains unchanged from the broken baseline. The exact targeted command passed on the first implementation run with no correction:
BindingRepresentationGenTests|SetterBuilderTests: 53 passed, 0 failed, 0 skipped.CompiledBindings|IndexerBindings|Maui13856Tests: 28 passed, 0 failed, 0 skipped.
Inline expert self-review recorded zero findings. The final diff is stored in attempt-2/fix.diff; test evidence is in attempt-2/test-output.log.
The attempt restored all six baseline files with EstablishBrokenBaseline.ps1 -Restore reporting Restored True; no candidate changes or processes remain.
Aggregate Outcome
Two candidates were produced, the maximum permitted. Both passed the primary and all mandatory regression tests on their first bounded implementation run, and neither required a correction. Candidate 2 is the cleaner structural alternative because it preserves Candidate 1's centralized correctness while eliminating the parallel-list invariant identified by Candidate 1's self-review.
📝 PR Finalize — Recommended Title & Description
Assessment: ✏️ Recommend updating — the current description overstates value-type intermediate support because the submitted HEAD can emit invalid copy-back for read-only intermediates and lose writes through nullable value-type intermediates.
Recommended title
[XAML SourceGen] Compiled bindings: Make intermediate member access null-tolerant
Recommended description
### Description of Change
XAML Source Generation emitted direct member access for binding paths, so a `null` intermediate (for example, `{Binding A.B.C.Text}` where `A` or `B` is null at runtime) threw `NullReferenceException`. This diverged from runtime and XamlC bindings, which tolerate null intermediates.
The root cause was that the generator based null-conditional access on nullable reference-type annotations. Those annotations are not enforced at runtime: a reference declared non-nullable can still be `null`.
This change:
- Uses runtime nullability for member, array-index, and indexer path parts, and propagates the resulting nullable type through generated getters, setters, handlers, and `TypedBinding<,>` arguments.
- Keeps two-way bindings able to write `null` through null-tolerant paths. Reference-type endpoints assign with the null-forgiving operator to remain warning-free; non-nullable value types retain the existing null guard.
- Handles writable value-type property and indexer intermediates with read-modify-write generation. `Setter.CopyBackStatements` updates a captured copy and assigns it back through each property or indexer in reverse order, avoiding both CS1612 and silently dropped writes.
- Identifies array elements from the receiver type through `IndexAccess.IsArrayElement`. Array elements and value-type fields remain direct lvalues and are not captured.
Getter output, before and after:
```csharp
// A, B, and C are non-nullable-declared reference types
getter: source => (source.A.B.C.Text, true),
getter: source => (source.A?.B?.C?.Text, true),
The first hop remains non-conditional (source.Foo, not source?.Foo) because TypedBinding.ApplyCore invokes the getter only with a non-null source. Non-nullable value-type endpoints retain their ?? default fallback so their binding property types do not change.
Known limitations in the submitted implementation:
- A two-way path through a get-only value-type property or read-only struct-valued indexer can emit an invalid copy-back assignment and fail generated-code compilation.
- A writable nullable value-type intermediate can be unwrapped into a local without being assigned back, so the generated setter compiles but loses the update.
These limitations must be resolved before merge.
Tests
- Updated exact generated-code expectations for compiled bindings, indexers, and Maui13856.
- Added coverage for non-nullable-declared reference intermediates.
- Added coverage proving two-way paths still write
null. - Added setter coverage for writable value-type properties and indexers, arrays, fields, and nested reverse-order copy-back.
Issues Fixed
Fixes #36313
</details>
---
<details>
<summary><strong>🏁 Report — Final Recommendation</strong></summary>
<br/>
## ⚠️ Final Recommendation: REQUEST CHANGES
**Winner:** `pr-plus-reviewer`
The submitted PR fixes the reported null-intermediate failure and its trusted gate passed, but expert review found a blocking generated-code regression: two-way bindings through a non-writable value-type intermediate can emit copy-back assignments to a get-only property or read-only indexer and fail with CS0200. Prior-review reconciliation also confirmed that writable nullable value-type intermediates can still lose updates. `pr-plus-reviewer` is the strongest relative candidate because it addresses the first blocker while retaining the submitted behavior and passing all prescribed tests, but it still needs the nullable-value-type defect fixed.
## Candidate Comparison
| Rank | Candidate | Validation | Assessment |
|---:|---|---|---|
| 1 | `pr-plus-reviewer` | PASS — 54 BindingSourceGen tests; 29 SourceGen tests | Fixes the non-writable-intermediate compile regression and adds its missing negative test. It still inherits the unresolved lost-write path for writable nullable value-type intermediates. |
| 2 | `try-fix-2` | PASS — 53 BindingSourceGen tests; 28 SourceGen tests | Its atomic typed path cursor is cleaner than distributed nullable-state tracking, but it retains both setter defects and lacks negative coverage for them. |
| 3 | `pr` | Trusted Gate PASS — 53 BindingSourceGen tests; 28 SourceGen tests | Correct for the submitted positive scenarios, but blocked by invalid copy-back through read-only value-type intermediates and lost writes through nullable value-type intermediates. |
| 4 | `try-fix-1` | PASS — 53 BindingSourceGen tests; 28 SourceGen tests | Retains both blockers and adds a parallel-list alignment invariant called out by its own review, making it less maintainable than Candidate 2. |
## Why `pr-plus-reviewer` Wins
The candidate fixes the concrete non-writable regression at the shared path representation boundary rather than special-casing generated text. Writability is captured by both path parsers, checked before either writer emits a setter, and ignored for reference-type intermediates that do not require copy-back. This keeps writable non-nullable value-type copy-back intact while preventing invalid generated C# for read-only value-type intermediates.
The required sandbox identity and immutable baseline were verified, `git diff --check` passed, and both required targeted commands passed on their single permitted run. No candidate resolves the nullable-value-type lost-write path, and the bounded tests do not cover it. Per the one-pass contract, no further patch or test cycle was attempted; `pr-plus-reviewer` wins comparatively, but the submitted PR still requires changes before merge.
## Prior Review and CI Reconciliation
Earlier inline findings about receiver-based array detection, field capture, and absent copy-back for writable non-nullable results are resolved in the submitted HEAD. The prior error-level nullable-value-type finding remains applicable by direct state-transition tracing. The trusted Gate passed. The public live head (`121588c`) differs from the materialized review commit and currently reports six failed and one in-progress check runs; anonymous APIs cannot identify the required-only subset, so live required-check status remains undetermined and no clean-CI claim is made.
</details>
---
<details>
<summary><strong>📱 UI Tests — Button,Label,Layout</strong></summary>
<br/>
**Detected UI test categories:** `Button,Label,Layout`
<!-- DEEP_UITESTS_BEGIN -->
⚠️ **Deep UI tests** — The build failed to **compile** for 3 categories (`Button`, `Label`, `Layout`), so those deep UI tests could not run. The compiler reported: `error CS8795: Partial method 'AppThemePage.InitializeComponent()' must have an implementation part because it has accessibility modifiers. [/Users/cloudtest/vss/_work/1/s/src/Controls/tests/TestCases.HostApp/Controls.TestCases.HostApp.cspro…`. Check the `build-output.log` in the `drop-deep-uitests` artifact to see whether `main` or this PR introduced it, then re-run once the build is fixed.
<!-- DEEP_UITESTS_END -->
</details>
</details>
<!-- SESSION:121588c END -->
---
<details>
<summary><strong>🧭 Next Steps</strong> — reviewer changes required</summary>
<br/>
**The reviewer-enhanced candidate identified changes that are not yet in the submitted PR.**
**Why:** pr-plus-reviewer is the strongest relative candidate because it prevents invalid copy-back through non-writable value-type intermediates, adds focused coverage, and passes both required targeted commands. It still shares the unresolved nullable-value-type lost-write defect with every candidate, so it is not merge-ready.
Address the actionable findings in this review before merging.
</details>
…e members (#36444) <!-- Please let the below note in for people that find this PR --> > [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! ### Description of Change XAML Source Generation emitted direct member access for binding paths, so a `null` intermediate (for example, `{Binding A.B.C.Text}` where `A` or `B` is null at runtime) threw `NullReferenceException`. This diverged from runtime and XamlC bindings, which tolerate null intermediates. The root cause was that the generator based null-conditional access on nullable reference-type annotations. Those annotations are not enforced at runtime: a reference declared non-nullable can still be `null`. This change: - Uses runtime nullability for member, array-index, and indexer path parts, and propagates the resulting nullable type through generated getters, setters, handlers, and `TypedBinding<,>` arguments. - Keeps two-way bindings able to write `null` through null-tolerant paths. Reference-type endpoints assign with the null-forgiving operator to remain warning-free; non-nullable value types retain the existing null guard. - Handles value-type property and indexer intermediates with read-modify-write generation. The setter updates a captured copy and assigns it back through each property/indexer in reverse order, avoiding both CS1612 and silently dropped writes. - Identifies array elements from the receiver type. Array elements and value-type fields remain direct lvalues and are not captured. Getter output, before and after: ```csharp // A, B, and C are non-nullable-declared reference types getter: source => (source.A.B.C.Text, true), getter: source => (source.A?.B?.C?.Text, true), ``` The first hop remains non-conditional (`source.Foo`, not `source?.Foo`) because `TypedBinding.ApplyCore` invokes the getter only with a non-null source. Non-nullable value-type endpoints retain their `?? default` fallback so their binding property types do not change. ### Tests - Updated exact generated-code expectations for compiled bindings, indexers, and Maui13856. - Added coverage for non-nullable-declared reference intermediates. - Added coverage proving two-way paths still write `null`. - Added setter coverage for value-type properties, indexers, arrays, fields, and nested reverse-order copy-back. ### Issues Fixed Fixes #36313 --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com> Co-authored-by: Jonathan Peppers <jonathan.peppers@microsoft.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Co-authored-by: Simon Rozsival <simon@rozsival.com> Copilot-Session: 10f057bb-7603-49db-aaf3-707c0cf0ddff
…e members (#36444) <!-- Please let the below note in for people that find this PR --> > [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! XAML Source Generation emitted direct member access for binding paths, so a `null` intermediate (for example, `{Binding A.B.C.Text}` where `A` or `B` is null at runtime) threw `NullReferenceException`. This diverged from runtime and XamlC bindings, which tolerate null intermediates. The root cause was that the generator based null-conditional access on nullable reference-type annotations. Those annotations are not enforced at runtime: a reference declared non-nullable can still be `null`. This change: - Uses runtime nullability for member, array-index, and indexer path parts, and propagates the resulting nullable type through generated getters, setters, handlers, and `TypedBinding<,>` arguments. - Keeps two-way bindings able to write `null` through null-tolerant paths. Reference-type endpoints assign with the null-forgiving operator to remain warning-free; non-nullable value types retain the existing null guard. - Handles value-type property and indexer intermediates with read-modify-write generation. The setter updates a captured copy and assigns it back through each property/indexer in reverse order, avoiding both CS1612 and silently dropped writes. - Identifies array elements from the receiver type. Array elements and value-type fields remain direct lvalues and are not captured. Getter output, before and after: ```csharp // A, B, and C are non-nullable-declared reference types getter: source => (source.A.B.C.Text, true), getter: source => (source.A?.B?.C?.Text, true), ``` The first hop remains non-conditional (`source.Foo`, not `source?.Foo`) because `TypedBinding.ApplyCore` invokes the getter only with a non-null source. Non-nullable value-type endpoints retain their `?? default` fallback so their binding property types do not change. - Updated exact generated-code expectations for compiled bindings, indexers, and Maui13856. - Added coverage for non-nullable-declared reference intermediates. - Added coverage proving two-way paths still write `null`. - Added setter coverage for value-type properties, indexers, arrays, fields, and nested reverse-order copy-back. Fixes #36313 --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com> Co-authored-by: Jonathan Peppers <jonathan.peppers@microsoft.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Co-authored-by: Simon Rozsival <simon@rozsival.com> Copilot-Session: 10f057bb-7603-49db-aaf3-707c0cf0ddff
…e members (#36444) <!-- Please let the below note in for people that find this PR --> > [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! ### Description of Change XAML Source Generation emitted direct member access for binding paths, so a `null` intermediate (for example, `{Binding A.B.C.Text}` where `A` or `B` is null at runtime) threw `NullReferenceException`. This diverged from runtime and XamlC bindings, which tolerate null intermediates. The root cause was that the generator based null-conditional access on nullable reference-type annotations. Those annotations are not enforced at runtime: a reference declared non-nullable can still be `null`. This change: - Uses runtime nullability for member, array-index, and indexer path parts, and propagates the resulting nullable type through generated getters, setters, handlers, and `TypedBinding<,>` arguments. - Keeps two-way bindings able to write `null` through null-tolerant paths. Reference-type endpoints assign with the null-forgiving operator to remain warning-free; non-nullable value types retain the existing null guard. - Handles value-type property and indexer intermediates with read-modify-write generation. The setter updates a captured copy and assigns it back through each property/indexer in reverse order, avoiding both CS1612 and silently dropped writes. - Identifies array elements from the receiver type. Array elements and value-type fields remain direct lvalues and are not captured. Getter output, before and after: ```csharp // A, B, and C are non-nullable-declared reference types getter: source => (source.A.B.C.Text, true), getter: source => (source.A?.B?.C?.Text, true), ``` The first hop remains non-conditional (`source.Foo`, not `source?.Foo`) because `TypedBinding.ApplyCore` invokes the getter only with a non-null source. Non-nullable value-type endpoints retain their `?? default` fallback so their binding property types do not change. ### Tests - Updated exact generated-code expectations for compiled bindings, indexers, and Maui13856. - Added coverage for non-nullable-declared reference intermediates. - Added coverage proving two-way paths still write `null`. - Added setter coverage for value-type properties, indexers, arrays, fields, and nested reverse-order copy-back. ### Issues Fixed Fixes #36313 --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com> Co-authored-by: Jonathan Peppers <jonathan.peppers@microsoft.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Co-authored-by: Simon Rozsival <simon@rozsival.com> Copilot-Session: 10f057bb-7603-49db-aaf3-707c0cf0ddff
…e members (#36444) <!-- Please let the below note in for people that find this PR --> > [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! ### Description of Change XAML Source Generation emitted direct member access for binding paths, so a `null` intermediate (for example, `{Binding A.B.C.Text}` where `A` or `B` is null at runtime) threw `NullReferenceException`. This diverged from runtime and XamlC bindings, which tolerate null intermediates. The root cause was that the generator based null-conditional access on nullable reference-type annotations. Those annotations are not enforced at runtime: a reference declared non-nullable can still be `null`. This change: - Uses runtime nullability for member, array-index, and indexer path parts, and propagates the resulting nullable type through generated getters, setters, handlers, and `TypedBinding<,>` arguments. - Keeps two-way bindings able to write `null` through null-tolerant paths. Reference-type endpoints assign with the null-forgiving operator to remain warning-free; non-nullable value types retain the existing null guard. - Handles value-type property and indexer intermediates with read-modify-write generation. The setter updates a captured copy and assigns it back through each property/indexer in reverse order, avoiding both CS1612 and silently dropped writes. - Identifies array elements from the receiver type. Array elements and value-type fields remain direct lvalues and are not captured. Getter output, before and after: ```csharp // A, B, and C are non-nullable-declared reference types getter: source => (source.A.B.C.Text, true), getter: source => (source.A?.B?.C?.Text, true), ``` The first hop remains non-conditional (`source.Foo`, not `source?.Foo`) because `TypedBinding.ApplyCore` invokes the getter only with a non-null source. Non-nullable value-type endpoints retain their `?? default` fallback so their binding property types do not change. ### Tests - Updated exact generated-code expectations for compiled bindings, indexers, and Maui13856. - Added coverage for non-nullable-declared reference intermediates. - Added coverage proving two-way paths still write `null`. - Added setter coverage for value-type properties, indexers, arrays, fields, and nested reverse-order copy-back. ### Issues Fixed Fixes #36313 --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com> Co-authored-by: Jonathan Peppers <jonathan.peppers@microsoft.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Co-authored-by: Simon Rozsival <simon@rozsival.com> Copilot-Session: 10f057bb-7603-49db-aaf3-707c0cf0ddff
…e members (#36444) <!-- Please let the below note in for people that find this PR --> > [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! ### Description of Change XAML Source Generation emitted direct member access for binding paths, so a `null` intermediate (for example, `{Binding A.B.C.Text}` where `A` or `B` is null at runtime) threw `NullReferenceException`. This diverged from runtime and XamlC bindings, which tolerate null intermediates. The root cause was that the generator based null-conditional access on nullable reference-type annotations. Those annotations are not enforced at runtime: a reference declared non-nullable can still be `null`. This change: - Uses runtime nullability for member, array-index, and indexer path parts, and propagates the resulting nullable type through generated getters, setters, handlers, and `TypedBinding<,>` arguments. - Keeps two-way bindings able to write `null` through null-tolerant paths. Reference-type endpoints assign with the null-forgiving operator to remain warning-free; non-nullable value types retain the existing null guard. - Handles value-type property and indexer intermediates with read-modify-write generation. The setter updates a captured copy and assigns it back through each property/indexer in reverse order, avoiding both CS1612 and silently dropped writes. - Identifies array elements from the receiver type. Array elements and value-type fields remain direct lvalues and are not captured. Getter output, before and after: ```csharp // A, B, and C are non-nullable-declared reference types getter: source => (source.A.B.C.Text, true), getter: source => (source.A?.B?.C?.Text, true), ``` The first hop remains non-conditional (`source.Foo`, not `source?.Foo`) because `TypedBinding.ApplyCore` invokes the getter only with a non-null source. Non-nullable value-type endpoints retain their `?? default` fallback so their binding property types do not change. ### Tests - Updated exact generated-code expectations for compiled bindings, indexers, and Maui13856. - Added coverage for non-nullable-declared reference intermediates. - Added coverage proving two-way paths still write `null`. - Added setter coverage for value-type properties, indexers, arrays, fields, and nested reverse-order copy-back. ### Issues Fixed Fixes #36313 --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com> Co-authored-by: Jonathan Peppers <jonathan.peppers@microsoft.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Co-authored-by: Simon Rozsival <simon@rozsival.com> Copilot-Session: 10f057bb-7603-49db-aaf3-707c0cf0ddff
…e members (#36444) <!-- Please let the below note in for people that find this PR --> > [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! ### Description of Change XAML Source Generation emitted direct member access for binding paths, so a `null` intermediate (for example, `{Binding A.B.C.Text}` where `A` or `B` is null at runtime) threw `NullReferenceException`. This diverged from runtime and XamlC bindings, which tolerate null intermediates. The root cause was that the generator based null-conditional access on nullable reference-type annotations. Those annotations are not enforced at runtime: a reference declared non-nullable can still be `null`. This change: - Uses runtime nullability for member, array-index, and indexer path parts, and propagates the resulting nullable type through generated getters, setters, handlers, and `TypedBinding<,>` arguments. - Keeps two-way bindings able to write `null` through null-tolerant paths. Reference-type endpoints assign with the null-forgiving operator to remain warning-free; non-nullable value types retain the existing null guard. - Handles value-type property and indexer intermediates with read-modify-write generation. The setter updates a captured copy and assigns it back through each property/indexer in reverse order, avoiding both CS1612 and silently dropped writes. - Identifies array elements from the receiver type. Array elements and value-type fields remain direct lvalues and are not captured. Getter output, before and after: ```csharp // A, B, and C are non-nullable-declared reference types getter: source => (source.A.B.C.Text, true), getter: source => (source.A?.B?.C?.Text, true), ``` The first hop remains non-conditional (`source.Foo`, not `source?.Foo`) because `TypedBinding.ApplyCore` invokes the getter only with a non-null source. Non-nullable value-type endpoints retain their `?? default` fallback so their binding property types do not change. ### Tests - Updated exact generated-code expectations for compiled bindings, indexers, and Maui13856. - Added coverage for non-nullable-declared reference intermediates. - Added coverage proving two-way paths still write `null`. - Added setter coverage for value-type properties, indexers, arrays, fields, and nested reverse-order copy-back. ### Issues Fixed Fixes #36313 --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com> Co-authored-by: Jonathan Peppers <jonathan.peppers@microsoft.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Co-authored-by: Simon Rozsival <simon@rozsival.com> Copilot-Session: 10f057bb-7603-49db-aaf3-707c0cf0ddff
…e members (#36444) <!-- Please let the below note in for people that find this PR --> > [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! ### Description of Change XAML Source Generation emitted direct member access for binding paths, so a `null` intermediate (for example, `{Binding A.B.C.Text}` where `A` or `B` is null at runtime) threw `NullReferenceException`. This diverged from runtime and XamlC bindings, which tolerate null intermediates. The root cause was that the generator based null-conditional access on nullable reference-type annotations. Those annotations are not enforced at runtime: a reference declared non-nullable can still be `null`. This change: - Uses runtime nullability for member, array-index, and indexer path parts, and propagates the resulting nullable type through generated getters, setters, handlers, and `TypedBinding<,>` arguments. - Keeps two-way bindings able to write `null` through null-tolerant paths. Reference-type endpoints assign with the null-forgiving operator to remain warning-free; non-nullable value types retain the existing null guard. - Handles value-type property and indexer intermediates with read-modify-write generation. The setter updates a captured copy and assigns it back through each property/indexer in reverse order, avoiding both CS1612 and silently dropped writes. - Identifies array elements from the receiver type. Array elements and value-type fields remain direct lvalues and are not captured. Getter output, before and after: ```csharp // A, B, and C are non-nullable-declared reference types getter: source => (source.A.B.C.Text, true), getter: source => (source.A?.B?.C?.Text, true), ``` The first hop remains non-conditional (`source.Foo`, not `source?.Foo`) because `TypedBinding.ApplyCore` invokes the getter only with a non-null source. Non-nullable value-type endpoints retain their `?? default` fallback so their binding property types do not change. ### Tests - Updated exact generated-code expectations for compiled bindings, indexers, and Maui13856. - Added coverage for non-nullable-declared reference intermediates. - Added coverage proving two-way paths still write `null`. - Added setter coverage for value-type properties, indexers, arrays, fields, and nested reverse-order copy-back. ### Issues Fixed Fixes #36313 --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com> Co-authored-by: Jonathan Peppers <jonathan.peppers@microsoft.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Co-authored-by: Simon Rozsival <simon@rozsival.com> Copilot-Session: 10f057bb-7603-49db-aaf3-707c0cf0ddff
…e members (#36444) <!-- Please let the below note in for people that find this PR --> > [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! ### Description of Change XAML Source Generation emitted direct member access for binding paths, so a `null` intermediate (for example, `{Binding A.B.C.Text}` where `A` or `B` is null at runtime) threw `NullReferenceException`. This diverged from runtime and XamlC bindings, which tolerate null intermediates. The root cause was that the generator based null-conditional access on nullable reference-type annotations. Those annotations are not enforced at runtime: a reference declared non-nullable can still be `null`. This change: - Uses runtime nullability for member, array-index, and indexer path parts, and propagates the resulting nullable type through generated getters, setters, handlers, and `TypedBinding<,>` arguments. - Keeps two-way bindings able to write `null` through null-tolerant paths. Reference-type endpoints assign with the null-forgiving operator to remain warning-free; non-nullable value types retain the existing null guard. - Handles value-type property and indexer intermediates with read-modify-write generation. The setter updates a captured copy and assigns it back through each property/indexer in reverse order, avoiding both CS1612 and silently dropped writes. - Identifies array elements from the receiver type. Array elements and value-type fields remain direct lvalues and are not captured. Getter output, before and after: ```csharp // A, B, and C are non-nullable-declared reference types getter: source => (source.A.B.C.Text, true), getter: source => (source.A?.B?.C?.Text, true), ``` The first hop remains non-conditional (`source.Foo`, not `source?.Foo`) because `TypedBinding.ApplyCore` invokes the getter only with a non-null source. Non-nullable value-type endpoints retain their `?? default` fallback so their binding property types do not change. ### Tests - Updated exact generated-code expectations for compiled bindings, indexers, and Maui13856. - Added coverage for non-nullable-declared reference intermediates. - Added coverage proving two-way paths still write `null`. - Added setter coverage for value-type properties, indexers, arrays, fields, and nested reverse-order copy-back. ### Issues Fixed Fixes #36313 --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com> Co-authored-by: Jonathan Peppers <jonathan.peppers@microsoft.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Co-authored-by: Simon Rozsival <simon@rozsival.com> Copilot-Session: 10f057bb-7603-49db-aaf3-707c0cf0ddff
…e members (#36444) <!-- Please let the below note in for people that find this PR --> > [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! ### Description of Change XAML Source Generation emitted direct member access for binding paths, so a `null` intermediate (for example, `{Binding A.B.C.Text}` where `A` or `B` is null at runtime) threw `NullReferenceException`. This diverged from runtime and XamlC bindings, which tolerate null intermediates. The root cause was that the generator based null-conditional access on nullable reference-type annotations. Those annotations are not enforced at runtime: a reference declared non-nullable can still be `null`. This change: - Uses runtime nullability for member, array-index, and indexer path parts, and propagates the resulting nullable type through generated getters, setters, handlers, and `TypedBinding<,>` arguments. - Keeps two-way bindings able to write `null` through null-tolerant paths. Reference-type endpoints assign with the null-forgiving operator to remain warning-free; non-nullable value types retain the existing null guard. - Handles value-type property and indexer intermediates with read-modify-write generation. The setter updates a captured copy and assigns it back through each property/indexer in reverse order, avoiding both CS1612 and silently dropped writes. - Identifies array elements from the receiver type. Array elements and value-type fields remain direct lvalues and are not captured. Getter output, before and after: ```csharp // A, B, and C are non-nullable-declared reference types getter: source => (source.A.B.C.Text, true), getter: source => (source.A?.B?.C?.Text, true), ``` The first hop remains non-conditional (`source.Foo`, not `source?.Foo`) because `TypedBinding.ApplyCore` invokes the getter only with a non-null source. Non-nullable value-type endpoints retain their `?? default` fallback so their binding property types do not change. ### Tests - Updated exact generated-code expectations for compiled bindings, indexers, and Maui13856. - Added coverage for non-nullable-declared reference intermediates. - Added coverage proving two-way paths still write `null`. - Added setter coverage for value-type properties, indexers, arrays, fields, and nested reverse-order copy-back. ### Issues Fixed Fixes #36313 --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com> Co-authored-by: Jonathan Peppers <jonathan.peppers@microsoft.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Co-authored-by: Simon Rozsival <simon@rozsival.com> Copilot-Session: 10f057bb-7603-49db-aaf3-707c0cf0ddff
…e members (#36444) <!-- Please let the below note in for people that find this PR --> > [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! ### Description of Change XAML Source Generation emitted direct member access for binding paths, so a `null` intermediate (for example, `{Binding A.B.C.Text}` where `A` or `B` is null at runtime) threw `NullReferenceException`. This diverged from runtime and XamlC bindings, which tolerate null intermediates. The root cause was that the generator based null-conditional access on nullable reference-type annotations. Those annotations are not enforced at runtime: a reference declared non-nullable can still be `null`. This change: - Uses runtime nullability for member, array-index, and indexer path parts, and propagates the resulting nullable type through generated getters, setters, handlers, and `TypedBinding<,>` arguments. - Keeps two-way bindings able to write `null` through null-tolerant paths. Reference-type endpoints assign with the null-forgiving operator to remain warning-free; non-nullable value types retain the existing null guard. - Handles value-type property and indexer intermediates with read-modify-write generation. The setter updates a captured copy and assigns it back through each property/indexer in reverse order, avoiding both CS1612 and silently dropped writes. - Identifies array elements from the receiver type. Array elements and value-type fields remain direct lvalues and are not captured. Getter output, before and after: ```csharp // A, B, and C are non-nullable-declared reference types getter: source => (source.A.B.C.Text, true), getter: source => (source.A?.B?.C?.Text, true), ``` The first hop remains non-conditional (`source.Foo`, not `source?.Foo`) because `TypedBinding.ApplyCore` invokes the getter only with a non-null source. Non-nullable value-type endpoints retain their `?? default` fallback so their binding property types do not change. ### Tests - Updated exact generated-code expectations for compiled bindings, indexers, and Maui13856. - Added coverage for non-nullable-declared reference intermediates. - Added coverage proving two-way paths still write `null`. - Added setter coverage for value-type properties, indexers, arrays, fields, and nested reverse-order copy-back. ### Issues Fixed Fixes #36313 --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com> Co-authored-by: Jonathan Peppers <jonathan.peppers@microsoft.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Co-authored-by: Simon Rozsival <simon@rozsival.com> Copilot-Session: 10f057bb-7603-49db-aaf3-707c0cf0ddff
…e members (#36444) <!-- Please let the below note in for people that find this PR --> > [!NOTE] > Are you waiting for the changes in this PR to be merged? > It would be very helpful if you could [test the resulting artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from this PR and let us know in a comment if this change resolves your issue. Thank you! ### Description of Change XAML Source Generation emitted direct member access for binding paths, so a `null` intermediate (for example, `{Binding A.B.C.Text}` where `A` or `B` is null at runtime) threw `NullReferenceException`. This diverged from runtime and XamlC bindings, which tolerate null intermediates. The root cause was that the generator based null-conditional access on nullable reference-type annotations. Those annotations are not enforced at runtime: a reference declared non-nullable can still be `null`. This change: - Uses runtime nullability for member, array-index, and indexer path parts, and propagates the resulting nullable type through generated getters, setters, handlers, and `TypedBinding<,>` arguments. - Keeps two-way bindings able to write `null` through null-tolerant paths. Reference-type endpoints assign with the null-forgiving operator to remain warning-free; non-nullable value types retain the existing null guard. - Handles value-type property and indexer intermediates with read-modify-write generation. The setter updates a captured copy and assigns it back through each property/indexer in reverse order, avoiding both CS1612 and silently dropped writes. - Identifies array elements from the receiver type. Array elements and value-type fields remain direct lvalues and are not captured. Getter output, before and after: ```csharp // A, B, and C are non-nullable-declared reference types getter: source => (source.A.B.C.Text, true), getter: source => (source.A?.B?.C?.Text, true), ``` The first hop remains non-conditional (`source.Foo`, not `source?.Foo`) because `TypedBinding.ApplyCore` invokes the getter only with a non-null source. Non-nullable value-type endpoints retain their `?? default` fallback so their binding property types do not change. ### Tests - Updated exact generated-code expectations for compiled bindings, indexers, and Maui13856. - Added coverage for non-nullable-declared reference intermediates. - Added coverage proving two-way paths still write `null`. - Added setter coverage for value-type properties, indexers, arrays, fields, and nested reverse-order copy-back. ### Issues Fixed Fixes #36313 --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com> Co-authored-by: Jonathan Peppers <jonathan.peppers@microsoft.com> Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Co-authored-by: Simon Rozsival <simon@rozsival.com> Copilot-Session: 10f057bb-7603-49db-aaf3-707c0cf0ddff
Note
Are you waiting for the changes in this PR to be merged?
It would be very helpful if you could test the resulting artifacts from this PR and let us know in a comment if this change resolves your issue. Thank you!
Description of Change
XAML Source Generation emitted direct member access for binding paths, so a
nullintermediate (for example,{Binding A.B.C.Text}whereAorBis null at runtime) threwNullReferenceException. This diverged from runtime and XamlC bindings, which tolerate null intermediates.The root cause was that the generator based null-conditional access on nullable reference-type annotations. Those annotations are not enforced at runtime: a reference declared non-nullable can still be
null.This change:
TypedBinding<,>arguments.nullthrough null-tolerant paths. Reference-type endpoints assign with the null-forgiving operator to remain warning-free; non-nullable value types retain the existing null guard.Getter output, before and after:
The first hop remains non-conditional (
source.Foo, notsource?.Foo) becauseTypedBinding.ApplyCoreinvokes the getter only with a non-null source. Non-nullable value-type endpoints retain their?? defaultfallback so their binding property types do not change.Tests
null.Issues Fixed
Fixes #36313