Skip to content

Commit aae0d33

Browse files
StephaneDelcroixCopilotsimonrozsival
authored
[XSG] Fix CS8601 error in XAML SourceGen setter for nullable reference types (#32940)
* Fix CS8601 error in XAML SourceGen setter for nullable reference types When binding path contains nullable access (e.g., Child.Message where Child is nullable), the generated setter was assigning a nullable value to a non-nullable property, causing CS8601 'Possible null reference assignment'. The fix adds an early return for null values in the setter when: - PropertyType.IsNullable is true (the binding path involves nullable access) - SetterOptions.AcceptsNullValue is false (the target property doesn't accept null) This mirrors the existing logic in BindingCodeWriter.cs used for C# binding interceptors. Fixes #32893 * Add test for nullable value types through nullable paths The test verifies that binding to non-nullable value type properties through nullable paths works correctly. In XAML SourceGen, value types accessed through nullable paths remain non-nullable (using ?? default fallback in getter) rather than becoming Nullable<T>, so the setter uses pattern matching instead of HasValue checks. * [XSG] Add comprehensive test coverage for nullable types in conditional access paths (#32951) * Initial plan * Add comprehensive nullable type tests for conditional access paths Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com> * Refactor test assertions to use regex matching and full expressions Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com> --------- 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: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: simonrozsival <374616+simonrozsival@users.noreply.github.com>
1 parent ffd3713 commit aae0d33

File tree

2 files changed

+303
-1
lines changed

2 files changed

+303
-1
lines changed

src/Controls/src/SourceGen/CompiledBindingMarkup.cs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -488,7 +488,27 @@ void AppendSetterLambda(IndentedTextWriter code, BindingInvocationDescription bi
488488
code.WriteLine("{");
489489
code.Indent++;
490490

491-
var setter = Setter.From(binding.Path, "source", "value");
491+
var assignedValueExpression = "value";
492+
493+
// early return for nullable values if the setter doesn't accept them
494+
if (binding.PropertyType.IsNullable && !binding.SetterOptions.AcceptsNullValue)
495+
{
496+
if (binding.PropertyType.IsValueType)
497+
{
498+
code.WriteLine("if (!value.HasValue)");
499+
assignedValueExpression = "value.Value";
500+
}
501+
else
502+
{
503+
code.WriteLine("if (value is null)");
504+
}
505+
using (PrePost.NewBlock(code))
506+
{
507+
code.WriteLine("return;");
508+
}
509+
}
510+
511+
var setter = Setter.From(binding.Path, "source", assignedValueExpression);
492512
if (setter.PatternMatchingExpressions.Length > 0)
493513
{
494514
code.Write("if (");

src/Controls/tests/SourceGen.UnitTests/InitializeComponent/CompiledBindings.cs

Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,10 @@ private partial void InitializeComponent()
421421
{
422422
global::System.Action<global::Test.TestPage, string?>? setter = static (source, value) =>
423423
{
424+
if (value is null)
425+
{
426+
return;
427+
}
424428
if (source.Product is {} p0)
425429
{
426430
p0.Name = value;
@@ -497,6 +501,63 @@ public class Product
497501
Assert.False(result.Diagnostics.Any(d => d.Severity == Microsoft.CodeAnalysis.DiagnosticSeverity.Error));
498502
}
499503

504+
[Fact]
505+
public void CorrectlyHandlesNullableValueTypesWithNonNullableTarget()
506+
{
507+
// This test covers binding to a non-nullable value type property through a nullable path
508+
// Note: In XAML SourceGen, value types accessed through nullable paths remain non-nullable
509+
// (using ?? default fallback in getter) rather than becoming Nullable<T>
510+
var xaml =
511+
"""
512+
<?xml version="1.0" encoding="UTF-8"?>
513+
<ContentPage
514+
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
515+
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
516+
xmlns:test="clr-namespace:Test"
517+
x:Class="Test.TestPage"
518+
x:DataType="test:TestPage">
519+
<Slider Value="{Binding Product.Quantity}"/>
520+
</ContentPage>
521+
""";
522+
523+
var code =
524+
"""
525+
#nullable enable
526+
#pragma warning disable CS0219 // Variable is assigned but its value is never used
527+
using System;
528+
using Microsoft.Maui.Controls;
529+
using Microsoft.Maui.Controls.Xaml;
530+
531+
namespace Test;
532+
533+
[XamlProcessing(XamlInflator.SourceGen)]
534+
public partial class TestPage : ContentPage
535+
{
536+
public Product? Product { get; set; } = null;
537+
538+
public TestPage()
539+
{
540+
InitializeComponent();
541+
}
542+
}
543+
544+
public class Product
545+
{
546+
public double Quantity { get; set; } = 0; // Non-nullable double
547+
}
548+
""";
549+
var (result, generated) = RunGenerator(xaml, code);
550+
551+
// Verify no errors are present - value types through nullable paths don't need null checks
552+
// because they use ?? default fallback in the getter and keep the non-nullable type
553+
Assert.False(result.Diagnostics.Any(d => d.Severity == Microsoft.CodeAnalysis.DiagnosticSeverity.Error));
554+
Assert.NotNull(generated);
555+
556+
// The setter for value types should NOT have HasValue check because PropertyType stays as double, not double?
557+
// The conditional access is handled by pattern matching (source.Product is {} p0)
558+
Assert.Contains("if (source.Product is {} p0)", generated, StringComparison.Ordinal);
559+
}
560+
500561
[Fact]
501562
public void TargetNullValueWithNullablePathGeneratesValidCode()
502563
{
@@ -715,4 +776,225 @@ public class TestViewModel
715776
var emptyHandlersPattern = @"handlers:\s*new\s+global::System\.Tuple<global::System\.Func<global::Test\.TestViewModel,\s*object\?>,\s*string>\[\]\s*\{\s*\}";
716777
Assert.Matches(emptyHandlersPattern, generated);
717778
}
779+
780+
[Fact]
781+
public void ValueTypeAtEndOfConditionalAccessPath()
782+
{
783+
// Test value type property at the end of a binding path with conditional access in the middle
784+
// The setter should use pattern matching and NOT have HasValue check since PropertyType is non-nullable
785+
var xaml =
786+
"""
787+
<?xml version="1.0" encoding="UTF-8"?>
788+
<ContentPage
789+
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
790+
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
791+
xmlns:test="clr-namespace:Test"
792+
x:Class="Test.TestPage"
793+
x:DataType="test:TestPage">
794+
<Label Text="{Binding Container.Count}"/>
795+
</ContentPage>
796+
""";
797+
798+
var code =
799+
"""
800+
#nullable enable
801+
using System;
802+
using Microsoft.Maui.Controls;
803+
using Microsoft.Maui.Controls.Xaml;
804+
805+
namespace Test;
806+
807+
[XamlProcessing(XamlInflator.SourceGen)]
808+
public partial class TestPage : ContentPage
809+
{
810+
public Container? Container { get; set; }
811+
}
812+
813+
public class Container
814+
{
815+
public int Count { get; set; }
816+
}
817+
""";
818+
819+
var (result, generated) = RunGenerator(xaml, code);
820+
821+
Assert.False(result.Diagnostics.Any(d => d.Severity == Microsoft.CodeAnalysis.DiagnosticSeverity.Error));
822+
Assert.NotNull(generated);
823+
824+
// Verify setter uses pattern matching for conditional access
825+
Assert.Contains("if (source.Container is {} p0)", generated, StringComparison.Ordinal);
826+
827+
// Should NOT have HasValue check since Count is int, not int?
828+
Assert.DoesNotContain("HasValue", generated, StringComparison.Ordinal);
829+
830+
// Getter should have ?? default for value type through nullable path
831+
Assert.Contains("getter: source => (source.Container?.Count ?? default, true)", generated, StringComparison.Ordinal);
832+
}
833+
834+
[Fact]
835+
public void NonNullableReferenceTypeAtEndOfConditionalAccessPath()
836+
{
837+
// Test non-nullable reference type property at the end of a binding path with conditional access
838+
// The setter should have an early return for null since the target property doesn't accept null
839+
var xaml =
840+
"""
841+
<?xml version="1.0" encoding="UTF-8"?>
842+
<ContentPage
843+
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
844+
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
845+
xmlns:test="clr-namespace:Test"
846+
x:Class="Test.TestPage"
847+
x:DataType="test:TestPage">
848+
<Entry Text="{Binding Container.Label}"/>
849+
</ContentPage>
850+
""";
851+
852+
var code =
853+
"""
854+
#nullable enable
855+
using System;
856+
using Microsoft.Maui.Controls;
857+
using Microsoft.Maui.Controls.Xaml;
858+
859+
namespace Test;
860+
861+
[XamlProcessing(XamlInflator.SourceGen)]
862+
public partial class TestPage : ContentPage
863+
{
864+
public Container? Container { get; set; }
865+
}
866+
867+
public class Container
868+
{
869+
public string Label { get; set; } = ""; // Non-nullable string
870+
}
871+
""";
872+
873+
var (result, generated) = RunGenerator(xaml, code);
874+
875+
Assert.False(result.Diagnostics.Any(d => d.Severity == Microsoft.CodeAnalysis.DiagnosticSeverity.Error));
876+
Assert.NotNull(generated);
877+
878+
// Verify setter has early return for null since Label doesn't accept null
879+
Assert.Contains("if (value is null)", generated, StringComparison.Ordinal);
880+
Assert.Contains("return;", generated, StringComparison.Ordinal);
881+
882+
// Verify setter uses pattern matching for conditional access
883+
Assert.Contains("if (source.Container is {} p0)", generated, StringComparison.Ordinal);
884+
}
885+
886+
[Fact]
887+
public void NullableReferenceTypeAtEndOfConditionalAccessPath()
888+
{
889+
// Test nullable reference type property at the end of a binding path with conditional access
890+
// The setter should NOT have an early return since the target property accepts null
891+
var xaml =
892+
"""
893+
<?xml version="1.0" encoding="UTF-8"?>
894+
<ContentPage
895+
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
896+
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
897+
xmlns:test="clr-namespace:Test"
898+
x:Class="Test.TestPage"
899+
x:DataType="test:TestPage">
900+
<Entry Text="{Binding Container.Description}"/>
901+
</ContentPage>
902+
""";
903+
904+
var code =
905+
"""
906+
#nullable enable
907+
using System;
908+
using Microsoft.Maui.Controls;
909+
using Microsoft.Maui.Controls.Xaml;
910+
911+
namespace Test;
912+
913+
[XamlProcessing(XamlInflator.SourceGen)]
914+
public partial class TestPage : ContentPage
915+
{
916+
public Container? Container { get; set; }
917+
}
918+
919+
public class Container
920+
{
921+
public string? Description { get; set; } // Nullable string
922+
}
923+
""";
924+
925+
var (result, generated) = RunGenerator(xaml, code);
926+
927+
Assert.False(result.Diagnostics.Any(d => d.Severity == Microsoft.CodeAnalysis.DiagnosticSeverity.Error));
928+
Assert.NotNull(generated);
929+
930+
// Verify setter does NOT have early return since Description accepts null
931+
// The setter should just use pattern matching without null check on value
932+
Assert.Contains("if (source.Container is {} p0)", generated, StringComparison.Ordinal);
933+
934+
// Check the setter signature accepts nullable
935+
Assert.Contains("global::System.Action<global::Test.TestPage, string?>", generated, StringComparison.Ordinal);
936+
937+
// Verify the pattern "if (value is null)" followed by "return;" does NOT appear
938+
// This would indicate an early return that we don't want for nullable target properties
939+
var earlyReturnPattern = @"if\s*\(\s*value\s+is\s+null\s*\)\s*\{\s*return\s*;";
940+
Assert.DoesNotMatch(earlyReturnPattern, generated);
941+
}
942+
943+
[Fact]
944+
public void NullableValueTypeAtEndOfConditionalAccessPath()
945+
{
946+
// Test nullable value type property (int?) at the end of a binding path with conditional access
947+
// The setter should NOT have an early return since the target property accepts null
948+
var xaml =
949+
"""
950+
<?xml version="1.0" encoding="UTF-8"?>
951+
<ContentPage
952+
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
953+
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
954+
xmlns:test="clr-namespace:Test"
955+
x:Class="Test.TestPage"
956+
x:DataType="test:TestPage">
957+
<Label Text="{Binding Container.OptionalCount}"/>
958+
</ContentPage>
959+
""";
960+
961+
var code =
962+
"""
963+
#nullable enable
964+
using System;
965+
using Microsoft.Maui.Controls;
966+
using Microsoft.Maui.Controls.Xaml;
967+
968+
namespace Test;
969+
970+
[XamlProcessing(XamlInflator.SourceGen)]
971+
public partial class TestPage : ContentPage
972+
{
973+
public Container? Container { get; set; }
974+
}
975+
976+
public class Container
977+
{
978+
public int? OptionalCount { get; set; } // Nullable int
979+
}
980+
""";
981+
982+
var (result, generated) = RunGenerator(xaml, code);
983+
984+
Assert.False(result.Diagnostics.Any(d => d.Severity == Microsoft.CodeAnalysis.DiagnosticSeverity.Error));
985+
Assert.NotNull(generated);
986+
987+
// Verify setter uses pattern matching for conditional access
988+
Assert.Contains("if (source.Container is {} p0)", generated, StringComparison.Ordinal);
989+
990+
// Check the setter signature accepts nullable value type
991+
Assert.Contains("global::System.Action<global::Test.TestPage, int?>", generated, StringComparison.Ordinal);
992+
993+
// Verify no early return patterns for nullable value types
994+
// Should NOT have "if (!value.HasValue) { return;" or "if (value is null) { return;"
995+
var hasValueEarlyReturnPattern = @"if\s*\(\s*!\s*value\s*\.\s*HasValue\s*\)\s*\{\s*return\s*;";
996+
var isNullEarlyReturnPattern = @"if\s*\(\s*value\s+is\s+null\s*\)\s*\{\s*return\s*;";
997+
Assert.DoesNotMatch(hasValueEarlyReturnPattern, generated);
998+
Assert.DoesNotMatch(isNullEarlyReturnPattern, generated);
999+
}
7181000
}

0 commit comments

Comments
 (0)