@@ -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