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
15 changes: 13 additions & 2 deletions src/Controls/src/BindingSourceGen/ITypeSymbolExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ namespace Microsoft.Maui.Controls.BindingSourceGen;

public static class ITypeSymbolExtensions
{
static readonly SymbolDisplayFormat FullyQualifiedNullableFormat =
SymbolDisplayFormat.FullyQualifiedFormat.WithMiscellaneousOptions(
SymbolDisplayFormat.FullyQualifiedFormat.MiscellaneousOptions
| SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier);

public static bool IsTypeNullable(this ITypeSymbol typeInfo, bool enabledNullable)
{
if (!enabledNullable && typeInfo.IsReferenceType)
Expand Down Expand Up @@ -39,10 +44,16 @@ private static string GetGlobalName(this ITypeSymbol typeSymbol, bool isNullable
if (isNullable && isValueType)
{
// Strips the "?" from the type name
return ((INamedTypeSymbol)typeSymbol).TypeArguments[0].ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
return ((INamedTypeSymbol)typeSymbol).TypeArguments[0].ToDisplayString(FullyQualifiedNullableFormat);
}

return typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
var globalName = typeSymbol.ToDisplayString(FullyQualifiedNullableFormat);

// Keep nullable annotations in generic arguments but avoid nullable top-level type syntax (e.g. typeof(Foo?)).
if (globalName.EndsWith("?", StringComparison.Ordinal))
globalName = globalName.Substring(0, globalName.Length - 1);

Comment on lines +52 to +55
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

globalName.EndsWith("?") uses the culture-sensitive overload. For consistency with other string comparisons in this repo (and to avoid any culture edge cases), use the overload with StringComparison.Ordinal when checking for the trailing ?.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense to add: StringComparison.Ordinal. Just to be safe

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done — added StringComparison.Ordinal.

return globalName;
}

/// <summary>
Expand Down
4 changes: 2 additions & 2 deletions src/Controls/src/SourceGen/NodeSGExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -483,7 +483,7 @@ public static string ConvertWithConverter(this ValueNode valueNode, ITypeSymbol
if (targetType.IsReferenceType || targetType.NullableAnnotation == NullableAnnotation.Annotated)
return $"((global::Microsoft.Maui.Controls.IExtendedTypeConverter)new {typeConverter.ToFQDisplayString()}()).ConvertFromInvariantString(\"{valueString}\", {serviceProvider.ValueAccessor}) as {targetType.ToFQDisplayString()}";
else
return $"({targetType.ToFQDisplayString()})((global::Microsoft.Maui.Controls.IExtendedTypeConverter)new {typeConverter.ToFQDisplayString()}()).ConvertFromInvariantString(\"{valueString}\", {serviceProvider.ValueAccessor})";
return $"({targetType.ToFQDisplayString()})((global::Microsoft.Maui.Controls.IExtendedTypeConverter)new {typeConverter.ToFQDisplayString()}()).ConvertFromInvariantString(\"{valueString}\", {serviceProvider.ValueAccessor})!";
}
else //should never happen. there's no point to implement IExtendedTypeConverter AND accept empty service provider
return $"((global::Microsoft.Maui.Controls.IExtendedTypeConverter)new {typeConverter.ToFQDisplayString()}()).ConvertFromInvariantString(\"{valueString}\", null) as {targetType.ToFQDisplayString()}";
Expand Down Expand Up @@ -699,4 +699,4 @@ public static (IFieldSymbol?, IPropertySymbol?) GetFieldOrBP(ITypeSymbol owner,

public static bool RepresentsType(this INode node, string namespaceUri, string name)
=> node is ElementNode elementNode && elementNode.XmlType.RepresentsType(namespaceUri, name);
}
}
11 changes: 11 additions & 0 deletions src/Controls/tests/Xaml.UnitTests/Issues/Maui34130.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?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:local="clr-namespace:Microsoft.Maui.Controls.Xaml.UnitTests"
x:Class="Microsoft.Maui.Controls.Xaml.UnitTests.Maui34130">
<VerticalStackLayout>
<Button x:DataType="local:Maui34130ViewModel"
CommandParameter="{Binding SelectedItem}" />
<local:Maui34130SizeBox Size="42" />
</VerticalStackLayout>
</ContentPage>
183 changes: 183 additions & 0 deletions src/Controls/tests/Xaml.UnitTests/Issues/Maui34130.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
using System;
using System.ComponentModel;
using System.Globalization;
using Xunit;

using static Microsoft.Maui.Controls.Xaml.UnitTests.MockSourceGenerator;

#nullable enable

namespace Microsoft.Maui.Controls.Xaml.UnitTests;

public partial class Maui34130 : ContentPage
{
public Maui34130()
{
InitializeComponent();
BindingContext = new Maui34130ViewModel();
}

[Collection("Issue")]
public class Tests
{
[Theory]
[XamlInflatorData]
internal void ReproducesSourceGenNullabilityDiagnostics(XamlInflator inflator)
{
if (inflator == XamlInflator.SourceGen)
{
var xaml =
"""
#nullable enable
using System;
using System.ComponentModel;
using System.Globalization;
using Microsoft.Maui.Controls;
using Microsoft.Maui.Controls.Xaml;

namespace Microsoft.Maui.Controls.Xaml.UnitTests;

public partial class Maui34130SourceGenRepro : ContentPage
{
public Maui34130SourceGenRepro()
{
InitializeComponent();
BindingContext = new Maui34130SourceGenReproViewModel();
}
}

[TypeConverter(typeof(Maui34130SourceGenReproSizeConverter))]
public readonly struct Maui34130SourceGenReproSize(double value)
{
public double Value { get; } = value;
}

public class Maui34130SourceGenReproSizeConverter : TypeConverter, IExtendedTypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
=> sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);

public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
=> value is string s && double.TryParse(s, NumberStyles.Any, culture, out double d)
? new Maui34130SourceGenReproSize(d)
: base.ConvertFrom(context, culture, value)!;

public object ConvertFromInvariantString(string value, IServiceProvider serviceProvider)
=> double.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out double d)
? new Maui34130SourceGenReproSize(d)
: new Maui34130SourceGenReproSize(0);
}

public class Maui34130SourceGenReproSizeBox : View
{
public static readonly BindableProperty SizeProperty =
BindableProperty.Create(nameof(Size), typeof(Maui34130SourceGenReproSize), typeof(Maui34130SourceGenReproSizeBox), default(Maui34130SourceGenReproSize));

public Maui34130SourceGenReproSize Size
{
get => (Maui34130SourceGenReproSize)GetValue(SizeProperty);
set => SetValue(SizeProperty, value);
}
}

public class Maui34130SourceGenReproGenericWrapper<T>
{
public T? Value { get; set; }
}

public class Maui34130SourceGenReproItem
{
public string Name { get; set; } = string.Empty;
}

public class Maui34130SourceGenReproViewModel
{
public Maui34130SourceGenReproGenericWrapper<Maui34130SourceGenReproItem?> SelectedItem { get; } = new();
}
""";

var xamlMarkup =
"""
<?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:local="clr-namespace:Microsoft.Maui.Controls.Xaml.UnitTests"
x:Class="Microsoft.Maui.Controls.Xaml.UnitTests.Maui34130SourceGenRepro">
<VerticalStackLayout>
<Button x:DataType="local:Maui34130SourceGenReproViewModel"
CommandParameter="{Binding SelectedItem}" />
<local:Maui34130SourceGenReproSizeBox Size="42" />
</VerticalStackLayout>
</ContentPage>
""";

var compilation = CreateMauiCompilation();
var csharpCompilation = (Microsoft.CodeAnalysis.CSharp.CSharpCompilation)compilation;
compilation = csharpCompilation.WithOptions(
csharpCompilation.Options.WithNullableContextOptions(Microsoft.CodeAnalysis.NullableContextOptions.Enable));

var result = compilation
.WithAdditionalSource(xaml, hintName: "Maui34130SourceGenRepro.xaml.cs")
.RunMauiSourceGenerator(new MockSourceGenerator.AdditionalXamlFile("Issues/Maui34130SourceGenRepro.xaml", xamlMarkup, TargetFramework: "net10.0"));
var generated = result.GeneratedInitializeComponent();

// Verify CS8605 fix: generated conversion for value-type target uses null-forgiving before unboxing
Assert.Contains("ConvertFromInvariantString(\"42\"", generated, StringComparison.Ordinal);
Assert.Contains(")!);", generated, StringComparison.Ordinal);

// Verify CS8619 fix: generated TypedBinding preserves nullable generic type argument (MyItem?)
Assert.Contains("TypedBinding<global::Microsoft.Maui.Controls.Xaml.UnitTests.Maui34130SourceGenReproViewModel, global::Microsoft.Maui.Controls.Xaml.UnitTests.Maui34130SourceGenReproGenericWrapper<global::Microsoft.Maui.Controls.Xaml.UnitTests.Maui34130SourceGenReproItem?>>", generated, StringComparison.Ordinal);
Assert.DoesNotContain("TypedBinding<global::Microsoft.Maui.Controls.Xaml.UnitTests.Maui34130SourceGenReproViewModel, global::Microsoft.Maui.Controls.Xaml.UnitTests.Maui34130SourceGenReproGenericWrapper<global::Microsoft.Maui.Controls.Xaml.UnitTests.Maui34130SourceGenReproItem>>", generated, StringComparison.Ordinal);
Assert.Contains("source.SelectedItem, true", generated, StringComparison.Ordinal);
}
else
Assert.NotNull(new Maui34130(inflator));
}
}
}

[TypeConverter(typeof(Maui34130SizeConverter))]
public readonly record struct Maui34130Size(double Value);

public class Maui34130SizeConverter : TypeConverter, IExtendedTypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType)
=> sourceType == typeof(string) || base.CanConvertFrom(context, sourceType);

public override object ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value)
=> value is string s && double.TryParse(s, NumberStyles.Any, culture, out double d)
? new Maui34130Size(d)
: base.ConvertFrom(context, culture, value)!;

public object ConvertFromInvariantString(string value, IServiceProvider serviceProvider)
=> double.TryParse(value, NumberStyles.Any, CultureInfo.InvariantCulture, out double d)
? new Maui34130Size(d)
: new Maui34130Size(0);
}

public class Maui34130SizeBox : View
{
public static readonly BindableProperty SizeProperty =
BindableProperty.Create(nameof(Size), typeof(Maui34130Size), typeof(Maui34130SizeBox), default(Maui34130Size));

public Maui34130Size Size
{
get => (Maui34130Size)GetValue(SizeProperty);
set => SetValue(SizeProperty, value);
}
}

public class Maui34130GenericWrapper<T>
{
public T? Value { get; set; }
}

public class Maui34130Item
{
public string Name { get; set; } = string.Empty;
}

public class Maui34130ViewModel
{
public Maui34130GenericWrapper<Maui34130Item?> SelectedItem { get; } = new();
}
Loading