Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 8 additions & 10 deletions src/Controls/src/SourceGen/CompiledBindingMarkup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -407,19 +407,17 @@ bool TryParsePath(
}
}

if (isNullable)
propertyType = previousPartType;

// Apply nullable annotation if any part of the path introduces nullability
// For reference types, mark as nullable so the TypedBinding signature is correct
// For value types, we don't mark as nullable here because GenerateGetterExpression
// will add ?? default fallback for non-nullable value types with conditional access
if (isNullable && !propertyType.IsValueType)
{
if (propertyType.IsValueType)
{
propertyType = _context.Compilation.GetSpecialType(SpecialType.System_Nullable_T).Construct(propertyType);
}
else
{
propertyType = propertyType.WithNullableAnnotation(NullableAnnotation.Annotated);
}
propertyType = propertyType.WithNullableAnnotation(NullableAnnotation.Annotated);
}

propertyType = previousPartType;
bindingPath = new EquatableArray<IPathPart>(bindingPathParts.ToArray());
return true;
}
Expand Down
141 changes: 141 additions & 0 deletions src/Controls/tests/BindingSourceGen.UnitTests/IntegrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2303,6 +2303,147 @@ internal static partial class GeneratedBindingInterceptors
""",
result.GeneratedFiles["Path-To-Program.cs-GeneratedBindingInterceptors-14-11.g.cs"]);
}

[Fact]
public void GenerateBindingWithNullableObjectAndNullableReferenceTypeProperty_SetBinding()
{
var source = """
using Microsoft.Maui.Controls;
var label = new Label();
label.SetBinding(Label.TextProperty, static (Foo f) => f.NullableObject?.StringProperty);

class Foo
{
public Bar? NullableObject { get; set; }
}

class Bar
{
public string StringProperty { get; set; } = "";
}
""";

var result = SourceGenHelpers.Run(source);
Assert.NotNull(result.Binding);

var id = Math.Abs(result.Binding.SimpleLocation!.GetHashCode());

// The key assertion: no CS8603 errors should be present
AssertExtensions.AssertNoDiagnostics(result);
AssertExtensions.CodeIsEqual(
$$"""
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a .NET MAUI source generator.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
#nullable enable

namespace System.Runtime.CompilerServices
{
using System;
using System.Diagnostics;

{{BindingCodeWriter.GeneratedCodeAttribute}}
[Conditional("DEBUG")]
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
file sealed class InterceptsLocationAttribute : Attribute
{
public InterceptsLocationAttribute(int version, string data)
{
_ = version;
_ = data;
}
}
}

namespace Microsoft.Maui.Controls.Generated
{
internal static partial class GeneratedBindingInterceptors
{

{{BindingCodeWriter.GeneratedCodeAttribute}}
[global::System.Runtime.CompilerServices.InterceptsLocationAttribute({{result.Binding.InterceptableLocation!.Version}}, @"{{result.Binding.InterceptableLocation!.Data}}")]
public static void SetBinding{{id}}(
this global::Microsoft.Maui.Controls.BindableObject bindableObject,
global::Microsoft.Maui.Controls.BindableProperty bindableProperty,
global::System.Func<global::Foo, string?> getter,
global::Microsoft.Maui.Controls.BindingMode mode = global::Microsoft.Maui.Controls.BindingMode.Default,
global::Microsoft.Maui.Controls.IValueConverter? converter = null,
object? converterParameter = null,
string? stringFormat = null,
object? source = null,
object? fallbackValue = null,
object? targetNullValue = null)
{
global::System.Action<global::Foo, string?>? setter = null;
if (ShouldUseSetter(mode, bindableProperty))
{
setter = static (source, value) =>
{
if (value is null)
{
return;
}
if (source.NullableObject is {} p0)
{
p0.StringProperty = value;
}
};
}

var binding = new global::Microsoft.Maui.Controls.Internals.TypedBinding<global::Foo, string?>(
getter: source => (getter(source), true),
setter,
handlers: new global::System.Tuple<global::System.Func<global::Foo, object?>, string>[]
{
new(static source => source, "NullableObject"),
new(static source => source.NullableObject, "StringProperty"),
})
{
Mode = mode,
Converter = converter,
ConverterParameter = converterParameter,
StringFormat = stringFormat,
Source = source,
FallbackValue = fallbackValue,
TargetNullValue = targetNullValue,
};
bindableObject.SetBinding(bindableProperty, binding);
}
}
}
""",
result.GeneratedFiles["Path-To-Program.cs-GeneratedBindingInterceptors-3-7.g.cs"]);
}

[Fact]
public void GenerateBindingWithNullableObjectAndNullableReferenceTypeProperty_Create()
{
var source = """
using Microsoft.Maui.Controls;
var binding = Binding.Create(static (Foo f) => f.NullableObject?.StringProperty);

class Foo
{
public Bar? NullableObject { get; set; }
}

class Bar
{
public string StringProperty { get; set; } = "";
}
""";

var result = SourceGenHelpers.Run(source);
Assert.NotNull(result.Binding);

// The key assertion: no CS8603 errors should be present
AssertExtensions.AssertNoDiagnostics(result);
}
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -299,4 +299,193 @@ static IEnumerable<string> SplitCode(string code)
.Select(static line => line.Trim())
.Where(static line => !string.IsNullOrWhiteSpace(line));
}

[Fact]
public void CorrectlyDetectsNullableReferenceTypes()
{
var xaml =
"""
<?xml version="1.0" encoding="UTF-8"?>
<ContentPage
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:test="clr-namespace:Test"
x:Class="Test.TestPage"
x:DataType="test:TestPage">
<Entry Text="{Binding Product.Name}"/>
</ContentPage>
""";

var code =
"""
#nullable enable
using System;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Controls.Xaml;

namespace Test;

[XamlProcessing(XamlInflator.SourceGen)]
public partial class TestPage : ContentPage
{
public Product? Product { get; set; } = null;

public TestPage()
{
InitializeComponent();
}
}

public class Product
{
public string Name { get; set; } = "";
}
""";
var testXamlFilePath = Path.Combine(Environment.CurrentDirectory, "Test.xaml");
var expected = $$"""
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a .NET MAUI source generator.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
#nullable enable

namespace Test;

[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Maui.Controls.SourceGen, Version=10.0.0.0, Culture=neutral, PublicKeyToken=null", "10.0.0.0")]
public partial class TestPage
{
private partial void InitializeComponent()
{
// Fallback to Runtime inflation if the page was updated by HotReload
static string? getPathForType(global::System.Type type)
{
var assembly = type.Assembly;
foreach (var xria in global::System.Reflection.CustomAttributeExtensions.GetCustomAttributes<global::Microsoft.Maui.Controls.Xaml.XamlResourceIdAttribute>(assembly))
{
if (xria.Type == type)
return xria.Path;
}
return null;
}

var rlr = global::Microsoft.Maui.Controls.Internals.ResourceLoader.ResourceProvider2?.Invoke(new global::Microsoft.Maui.Controls.Internals.ResourceLoader.ResourceLoadingQuery
{
AssemblyName = typeof(global::Test.TestPage).Assembly.GetName(),
ResourcePath = getPathForType(typeof(global::Test.TestPage)),
Instance = this,
});

if (rlr?.ResourceContent != null)
{
this.InitializeComponentRuntime();
return;
}

var bindingExtension = new global::Microsoft.Maui.Controls.Xaml.BindingExtension();
global::Microsoft.Maui.VisualDiagnostics.RegisterSourceInfo(bindingExtension!, new global::System.Uri(@"Test.xaml;assembly=SourceGeneratorDriver.Generated", global::System.UriKind.Relative), 8, 9);
var entry = new global::Microsoft.Maui.Controls.Entry();
global::Microsoft.Maui.VisualDiagnostics.RegisterSourceInfo(entry!, new global::System.Uri(@"Test.xaml;assembly=SourceGeneratorDriver.Generated", global::System.UriKind.Relative), 8, 3);
var __root = this;
global::Microsoft.Maui.VisualDiagnostics.RegisterSourceInfo(__root!, new global::System.Uri(@"Test.xaml;assembly=SourceGeneratorDriver.Generated", global::System.UriKind.Relative), 2, 2);
#if !_MAUIXAML_SG_NAMESCOPE_DISABLE
global::Microsoft.Maui.Controls.Internals.INameScope iNameScope = global::Microsoft.Maui.Controls.Internals.NameScope.GetNameScope(__root) ?? new global::Microsoft.Maui.Controls.Internals.NameScope();
#endif
#if !_MAUIXAML_SG_NAMESCOPE_DISABLE
global::Microsoft.Maui.Controls.Internals.NameScope.SetNameScope(__root, iNameScope);
#endif
#if !_MAUIXAML_SG_NAMESCOPE_DISABLE
entry.transientNamescope = iNameScope;
#endif
#line 8 "{{testXamlFilePath}}"
bindingExtension.Path = "Product.Name";
#line default
var bindingBase = CreateTypedBindingFrom_bindingExtension(bindingExtension);
if (global::Microsoft.Maui.VisualDiagnostics.GetSourceInfo(bindingBase!) == null)
global::Microsoft.Maui.VisualDiagnostics.RegisterSourceInfo(bindingBase!, new global::System.Uri(@"Test.xaml;assembly=SourceGeneratorDriver.Generated", global::System.UriKind.Relative), 8, 9);
entry.SetBinding(global::Microsoft.Maui.Controls.Entry.TextProperty, bindingBase);
#line 8 "{{testXamlFilePath}}"
__root.SetValue(global::Microsoft.Maui.Controls.ContentPage.ContentProperty, entry);
#line default
static global::Microsoft.Maui.Controls.BindingBase CreateTypedBindingFrom_bindingExtension(global::Microsoft.Maui.Controls.Xaml.BindingExtension extension)
{
global::System.Action<global::Test.TestPage, string?>? setter = static (source, value) =>
{
if (source.Product is {} p0)
{
p0.Name = value;
}
};

return new global::Microsoft.Maui.Controls.Internals.TypedBinding<global::Test.TestPage, string?>(
getter: source => (source.Product?.Name, true),
setter,
handlers: new global::System.Tuple<global::System.Func<global::Test.TestPage, object?>, string>[]
{
new(static source => source, "Product"),
new(static source => source.Product, "Name"),
});
}
}
}

""";

var (result, generated) = RunGenerator(xaml, code);
Assert.False(result.Diagnostics.Any());
CodeIsEqual(expected, generated ?? string.Empty);
}

[Fact]
public void CorrectlyDetectsNullableReferenceTypesWithNonNullableTarget()
{
// This test reproduces the CS8603 error scenario
var xaml =
"""
<?xml version="1.0" encoding="UTF-8"?>
<ContentPage
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:test="clr-namespace:Test"
x:Class="Test.TestPage"
x:DataType="test:TestPage">
<Entry Text="{Binding Product.Name}"/>
</ContentPage>
""";

var code =
"""
#nullable enable
using System;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Controls.Xaml;

namespace Test;

[XamlProcessing(XamlInflator.SourceGen)]
public partial class TestPage : ContentPage
{
public Product? Product { get; set; } = null;

public TestPage()
{
InitializeComponent();
}
}

public class Product
{
public string Name { get; set; } = ""; // Non-nullable string
}
""";
var (result, generated) = RunGenerator(xaml, code);

// Check that no CS8603 errors are present - even with non-nullable target property,
// the generated getter should handle the nullable path correctly
Assert.False(result.Diagnostics.Any(d => d.Id == "CS8603"));
Assert.False(result.Diagnostics.Any(d => d.Severity == Microsoft.CodeAnalysis.DiagnosticSeverity.Error));
}
}
Loading