From 61a36e6ce679f5c627ef0847206d286bc119000c Mon Sep 17 00:00:00 2001 From: Matt Goldman Date: Sun, 19 Apr 2026 04:49:21 +1000 Subject: [PATCH 01/14] Add compile-time property initializer for default value sin BindableProperty source generaor --- ...tributeSourceGenerator_CommonUsageTests.cs | 29 +- ...yAttributeSourceGenerator_EdgeCaseTests.cs | 170 ++--------- ...tributeSourceGenerator_IntegrationTests.cs | 17 +- ...indablePropertyAttributeSourceGenerator.cs | 21 +- .../Helpers/InitializerExpressionResolver.cs | 287 ++++++++++++++++++ .../Models/BindablePropertyRecords.cs | 5 +- .../Services/PopupServiceTests.cs | 4 +- .../Validators/ValidationBehavior.shared.cs | 15 +- 8 files changed, 350 insertions(+), 198 deletions(-) create mode 100644 src/CommunityToolkit.Maui.SourceGenerators/Helpers/InitializerExpressionResolver.cs diff --git a/src/CommunityToolkit.Maui.SourceGenerators.UnitTests/BindablePropertyAttributeSourceGeneratorTests/BindablePropertyAttributeSourceGenerator_CommonUsageTests.cs b/src/CommunityToolkit.Maui.SourceGenerators.UnitTests/BindablePropertyAttributeSourceGeneratorTests/BindablePropertyAttributeSourceGenerator_CommonUsageTests.cs index 9b73716179..4f9b1d3eef 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators.UnitTests/BindablePropertyAttributeSourceGeneratorTests/BindablePropertyAttributeSourceGenerator_CommonUsageTests.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators.UnitTests/BindablePropertyAttributeSourceGeneratorTests/BindablePropertyAttributeSourceGenerator_CommonUsageTests.cs @@ -598,35 +598,14 @@ public partial class {{defaultTestClassName}} /// /// BindableProperty for the property. /// - public static readonly global::Microsoft.Maui.Controls.BindableProperty TextProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Text", typeof(string), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, (Microsoft.Maui.Controls.BindingMode)0, null, null, null, null, __{{defaultTestClassName}}BindablePropertyInitHelpers.CreateDefaultText); - public partial string Text { get => __{{defaultTestClassName}}BindablePropertyInitHelpers.IsInitializingText ? field : (string)GetValue(TextProperty); set => SetValue(TextProperty, value); } + public static readonly global::Microsoft.Maui.Controls.BindableProperty TextProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Text", typeof(string), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), "Initial Value", (Microsoft.Maui.Controls.BindingMode)0, null, null, null, null, null); + public partial string Text { get => false ? field : (string)GetValue(TextProperty); set => SetValue(TextProperty, value); } /// /// BindableProperty for the property. /// - public static readonly global::Microsoft.Maui.Controls.BindableProperty CustomDurationProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("CustomDuration", typeof(System.TimeSpan), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, (Microsoft.Maui.Controls.BindingMode)0, null, null, null, null, __{{defaultTestClassName}}BindablePropertyInitHelpers.CreateDefaultCustomDuration); - public partial System.TimeSpan CustomDuration { get => __{{defaultTestClassName}}BindablePropertyInitHelpers.IsInitializingCustomDuration ? field : (System.TimeSpan)GetValue(CustomDurationProperty); set => SetValue(CustomDurationProperty, value); } - } - - file static class __{{defaultTestClassName}}BindablePropertyInitHelpers - { - public static volatile bool IsInitializingText = false; - public static object CreateDefaultText(global::Microsoft.Maui.Controls.BindableObject bindable) - { - IsInitializingText = true; - var defaultValue = ((TestView)bindable).Text; - IsInitializingText = false; - return defaultValue; - } - - public static volatile bool IsInitializingCustomDuration = false; - public static object CreateDefaultCustomDuration(global::Microsoft.Maui.Controls.BindableObject bindable) - { - IsInitializingCustomDuration = true; - var defaultValue = ((TestView)bindable).CustomDuration; - IsInitializingCustomDuration = false; - return defaultValue; - } + public static readonly global::Microsoft.Maui.Controls.BindableProperty CustomDurationProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("CustomDuration", typeof(System.TimeSpan), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), global::System.TimeSpan.FromSeconds(30), (Microsoft.Maui.Controls.BindingMode)0, null, null, null, null, null); + public partial System.TimeSpan CustomDuration { get => false ? field : (System.TimeSpan)GetValue(CustomDurationProperty); set => SetValue(CustomDurationProperty, value); } } """; diff --git a/src/CommunityToolkit.Maui.SourceGenerators.UnitTests/BindablePropertyAttributeSourceGeneratorTests/BindablePropertyAttributeSourceGenerator_EdgeCaseTests.cs b/src/CommunityToolkit.Maui.SourceGenerators.UnitTests/BindablePropertyAttributeSourceGeneratorTests/BindablePropertyAttributeSourceGenerator_EdgeCaseTests.cs index 951ffe92a1..e03f8d3fce 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators.UnitTests/BindablePropertyAttributeSourceGeneratorTests/BindablePropertyAttributeSourceGenerator_EdgeCaseTests.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators.UnitTests/BindablePropertyAttributeSourceGeneratorTests/BindablePropertyAttributeSourceGenerator_EdgeCaseTests.cs @@ -94,20 +94,8 @@ public partial class {{defaultTestClassName}} /// /// BindableProperty for the property. /// - public static readonly global::Microsoft.Maui.Controls.BindableProperty InvoiceStatusProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("InvoiceStatus", typeof(TestNamespace.Status), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, (Microsoft.Maui.Controls.BindingMode)0, null, null, null, null, __{{defaultTestClassName}}BindablePropertyInitHelpers.CreateDefaultInvoiceStatus); - public partial TestNamespace.Status InvoiceStatus { get => __{{defaultTestClassName}}BindablePropertyInitHelpers.IsInitializingInvoiceStatus ? field : (TestNamespace.Status)GetValue(InvoiceStatusProperty); set => SetValue(InvoiceStatusProperty, value); } - } - - file static class __{{defaultTestClassName}}BindablePropertyInitHelpers - { - public static volatile bool IsInitializingInvoiceStatus = false; - public static object CreateDefaultInvoiceStatus(global::Microsoft.Maui.Controls.BindableObject bindable) - { - IsInitializingInvoiceStatus = true; - var defaultValue = ((TestView)bindable).InvoiceStatus; - IsInitializingInvoiceStatus = false; - return defaultValue; - } + public static readonly global::Microsoft.Maui.Controls.BindableProperty InvoiceStatusProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("InvoiceStatus", typeof(TestNamespace.Status), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), global::TestNamespace.Status.Approved, (Microsoft.Maui.Controls.BindingMode)0, null, null, null, null, null); + public partial TestNamespace.Status InvoiceStatus { get => false ? field : (TestNamespace.Status)GetValue(InvoiceStatusProperty); set => SetValue(InvoiceStatusProperty, value); } } """; @@ -154,20 +142,8 @@ public partial class {{defaultTestClassName}} /// /// BindableProperty for the property. /// - public static readonly global::Microsoft.Maui.Controls.BindableProperty InvoiceStatusProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("InvoiceStatus", typeof(TestNamespace.Status), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, (Microsoft.Maui.Controls.BindingMode)0, null, null, null, null, __{{defaultTestClassName}}BindablePropertyInitHelpers.CreateDefaultInvoiceStatus); - public partial TestNamespace.Status InvoiceStatus { get => __{{defaultTestClassName}}BindablePropertyInitHelpers.IsInitializingInvoiceStatus ? field : (TestNamespace.Status)GetValue(InvoiceStatusProperty); set => SetValue(InvoiceStatusProperty, value); } - } - - file static class __{{defaultTestClassName}}BindablePropertyInitHelpers - { - public static volatile bool IsInitializingInvoiceStatus = false; - public static object CreateDefaultInvoiceStatus(global::Microsoft.Maui.Controls.BindableObject bindable) - { - IsInitializingInvoiceStatus = true; - var defaultValue = ((TestView)bindable).InvoiceStatus; - IsInitializingInvoiceStatus = false; - return defaultValue; - } + public static readonly global::Microsoft.Maui.Controls.BindableProperty InvoiceStatusProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("InvoiceStatus", typeof(TestNamespace.Status), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), global::TestNamespace.Status.Rejected, (Microsoft.Maui.Controls.BindingMode)0, null, null, null, null, null); + public partial TestNamespace.Status InvoiceStatus { get => false ? field : (TestNamespace.Status)GetValue(InvoiceStatusProperty); set => SetValue(InvoiceStatusProperty, value); } } """; @@ -453,7 +429,7 @@ public partial class {{defaultTestClassName}} : View public partial double DoubleEpsilon { get; set; } = double.Epsilon; [BindableProperty] - public partial double SingleEpsilon { get; set; } = float.Epsilon; + public partial float SingleEpsilon { get; set; } = float.Epsilon; [BindableProperty] public partial DateTimeOffset CurrentTime { get; set; } = DateTimeOffset.UtcNow; @@ -474,110 +450,44 @@ public partial class {{defaultTestClassName}} /// /// BindableProperty for the property. /// - public static readonly global::Microsoft.Maui.Controls.BindableProperty IsEnabledProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("IsEnabled", typeof(bool), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, (Microsoft.Maui.Controls.BindingMode)0, null, null, null, null, __{{defaultTestClassName}}BindablePropertyInitHelpers.CreateDefaultIsEnabled); - public partial bool IsEnabled { get => __{{defaultTestClassName}}BindablePropertyInitHelpers.IsInitializingIsEnabled ? field : (bool)GetValue(IsEnabledProperty); set => SetValue(IsEnabledProperty, value); } + public static readonly global::Microsoft.Maui.Controls.BindableProperty IsEnabledProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("IsEnabled", typeof(bool), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), true, (Microsoft.Maui.Controls.BindingMode)0, null, null, null, null, null); + public partial bool IsEnabled { get => false ? field : (bool)GetValue(IsEnabledProperty); set => SetValue(IsEnabledProperty, value); } /// /// BindableProperty for the property. /// - public static readonly global::Microsoft.Maui.Controls.BindableProperty PiProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Pi", typeof(double), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, (Microsoft.Maui.Controls.BindingMode)0, null, null, null, null, __{{defaultTestClassName}}BindablePropertyInitHelpers.CreateDefaultPi); - public partial double Pi { get => __{{defaultTestClassName}}BindablePropertyInitHelpers.IsInitializingPi ? field : (double)GetValue(PiProperty); set => SetValue(PiProperty, value); } + public static readonly global::Microsoft.Maui.Controls.BindableProperty PiProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Pi", typeof(double), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), 3.14, (Microsoft.Maui.Controls.BindingMode)0, null, null, null, null, null); + public partial double Pi { get => false ? field : (double)GetValue(PiProperty); set => SetValue(PiProperty, value); } /// /// BindableProperty for the property. /// - public static readonly global::Microsoft.Maui.Controls.BindableProperty LetterProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Letter", typeof(char), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, (Microsoft.Maui.Controls.BindingMode)0, null, null, null, null, __{{defaultTestClassName}}BindablePropertyInitHelpers.CreateDefaultLetter); - public partial char Letter { get => __{{defaultTestClassName}}BindablePropertyInitHelpers.IsInitializingLetter ? field : (char)GetValue(LetterProperty); set => SetValue(LetterProperty, value); } + public static readonly global::Microsoft.Maui.Controls.BindableProperty LetterProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Letter", typeof(char), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), 'A', (Microsoft.Maui.Controls.BindingMode)0, null, null, null, null, null); + public partial char Letter { get => false ? field : (char)GetValue(LetterProperty); set => SetValue(LetterProperty, value); } /// /// BindableProperty for the property. /// - public static readonly global::Microsoft.Maui.Controls.BindableProperty TimeSpentProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("TimeSpent", typeof(System.TimeSpan), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, (Microsoft.Maui.Controls.BindingMode)0, null, null, null, null, __{{defaultTestClassName}}BindablePropertyInitHelpers.CreateDefaultTimeSpent); - public partial System.TimeSpan TimeSpent { get => __{{defaultTestClassName}}BindablePropertyInitHelpers.IsInitializingTimeSpent ? field : (System.TimeSpan)GetValue(TimeSpentProperty); set => SetValue(TimeSpentProperty, value); } + public static readonly global::Microsoft.Maui.Controls.BindableProperty TimeSpentProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("TimeSpent", typeof(System.TimeSpan), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), global::System.TimeSpan.Zero, (Microsoft.Maui.Controls.BindingMode)0, null, null, null, null, null); + public partial System.TimeSpan TimeSpent { get => false ? field : (System.TimeSpan)GetValue(TimeSpentProperty); set => SetValue(TimeSpentProperty, value); } /// /// BindableProperty for the property. /// - public static readonly global::Microsoft.Maui.Controls.BindableProperty DoubleEpsilonProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("DoubleEpsilon", typeof(double), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, (Microsoft.Maui.Controls.BindingMode)0, null, null, null, null, __{{defaultTestClassName}}BindablePropertyInitHelpers.CreateDefaultDoubleEpsilon); - public partial double DoubleEpsilon { get => __{{defaultTestClassName}}BindablePropertyInitHelpers.IsInitializingDoubleEpsilon ? field : (double)GetValue(DoubleEpsilonProperty); set => SetValue(DoubleEpsilonProperty, value); } + public static readonly global::Microsoft.Maui.Controls.BindableProperty DoubleEpsilonProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("DoubleEpsilon", typeof(double), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), double.Epsilon, (Microsoft.Maui.Controls.BindingMode)0, null, null, null, null, null); + public partial double DoubleEpsilon { get => false ? field : (double)GetValue(DoubleEpsilonProperty); set => SetValue(DoubleEpsilonProperty, value); } /// /// BindableProperty for the property. /// - public static readonly global::Microsoft.Maui.Controls.BindableProperty SingleEpsilonProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("SingleEpsilon", typeof(double), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, (Microsoft.Maui.Controls.BindingMode)0, null, null, null, null, __{{defaultTestClassName}}BindablePropertyInitHelpers.CreateDefaultSingleEpsilon); - public partial double SingleEpsilon { get => __{{defaultTestClassName}}BindablePropertyInitHelpers.IsInitializingSingleEpsilon ? field : (double)GetValue(SingleEpsilonProperty); set => SetValue(SingleEpsilonProperty, value); } + public static readonly global::Microsoft.Maui.Controls.BindableProperty SingleEpsilonProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("SingleEpsilon", typeof(float), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), float.Epsilon, (Microsoft.Maui.Controls.BindingMode)0, null, null, null, null, null); + public partial float SingleEpsilon { get => false ? field : (float)GetValue(SingleEpsilonProperty); set => SetValue(SingleEpsilonProperty, value); } /// /// BindableProperty for the property. /// - public static readonly global::Microsoft.Maui.Controls.BindableProperty CurrentTimeProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("CurrentTime", typeof(System.DateTimeOffset), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, (Microsoft.Maui.Controls.BindingMode)0, null, null, null, null, __{{defaultTestClassName}}BindablePropertyInitHelpers.CreateDefaultCurrentTime); - public partial System.DateTimeOffset CurrentTime { get => __{{defaultTestClassName}}BindablePropertyInitHelpers.IsInitializingCurrentTime ? field : (System.DateTimeOffset)GetValue(CurrentTimeProperty); set => SetValue(CurrentTimeProperty, value); } - } - - file static class __{{defaultTestClassName}}BindablePropertyInitHelpers - { - public static volatile bool IsInitializingIsEnabled = false; - public static object CreateDefaultIsEnabled(global::Microsoft.Maui.Controls.BindableObject bindable) - { - IsInitializingIsEnabled = true; - var defaultValue = ((TestView)bindable).IsEnabled; - IsInitializingIsEnabled = false; - return defaultValue; - } - - public static volatile bool IsInitializingPi = false; - public static object CreateDefaultPi(global::Microsoft.Maui.Controls.BindableObject bindable) - { - IsInitializingPi = true; - var defaultValue = ((TestView)bindable).Pi; - IsInitializingPi = false; - return defaultValue; - } - - public static volatile bool IsInitializingLetter = false; - public static object CreateDefaultLetter(global::Microsoft.Maui.Controls.BindableObject bindable) - { - IsInitializingLetter = true; - var defaultValue = ((TestView)bindable).Letter; - IsInitializingLetter = false; - return defaultValue; - } - - public static volatile bool IsInitializingTimeSpent = false; - public static object CreateDefaultTimeSpent(global::Microsoft.Maui.Controls.BindableObject bindable) - { - IsInitializingTimeSpent = true; - var defaultValue = ((TestView)bindable).TimeSpent; - IsInitializingTimeSpent = false; - return defaultValue; - } - - public static volatile bool IsInitializingDoubleEpsilon = false; - public static object CreateDefaultDoubleEpsilon(global::Microsoft.Maui.Controls.BindableObject bindable) - { - IsInitializingDoubleEpsilon = true; - var defaultValue = ((TestView)bindable).DoubleEpsilon; - IsInitializingDoubleEpsilon = false; - return defaultValue; - } - - public static volatile bool IsInitializingSingleEpsilon = false; - public static object CreateDefaultSingleEpsilon(global::Microsoft.Maui.Controls.BindableObject bindable) - { - IsInitializingSingleEpsilon = true; - var defaultValue = ((TestView)bindable).SingleEpsilon; - IsInitializingSingleEpsilon = false; - return defaultValue; - } - - public static volatile bool IsInitializingCurrentTime = false; - public static object CreateDefaultCurrentTime(global::Microsoft.Maui.Controls.BindableObject bindable) - { - IsInitializingCurrentTime = true; - var defaultValue = ((TestView)bindable).CurrentTime; - IsInitializingCurrentTime = false; - return defaultValue; - } + public static readonly global::Microsoft.Maui.Controls.BindableProperty CurrentTimeProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("CurrentTime", typeof(System.DateTimeOffset), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), global::System.DateTimeOffset.UtcNow, (Microsoft.Maui.Controls.BindingMode)0, null, null, null, null, null); + public partial System.DateTimeOffset CurrentTime { get => false ? field : (System.DateTimeOffset)GetValue(CurrentTimeProperty); set => SetValue(CurrentTimeProperty, value); } } """; @@ -671,51 +581,21 @@ public partial class {{defaultTestClassName}} /// /// BindableProperty for the property. /// - internal static readonly global::Microsoft.Maui.Controls.BindableProperty TextProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Text", typeof(string), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, (Microsoft.Maui.Controls.BindingMode)0, null, null, null, null, __{{defaultTestClassName}}BindablePropertyInitHelpers.CreateDefaultText); - internal partial string Text { get => __{{defaultTestClassName}}BindablePropertyInitHelpers.IsInitializingText ? field : (string)GetValue(TextProperty); set => SetValue(TextProperty, value); } + internal static readonly global::Microsoft.Maui.Controls.BindableProperty TextProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Text", typeof(string), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), "Initial Value", (Microsoft.Maui.Controls.BindingMode)0, null, null, null, null, null); + internal partial string Text { get => false ? field : (string)GetValue(TextProperty); set => SetValue(TextProperty, value); } /// /// BindableProperty for the property. /// - protected internal static readonly global::Microsoft.Maui.Controls.BindableProperty TimeProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Time", typeof(string), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, (Microsoft.Maui.Controls.BindingMode)0, null, null, null, null, __{{defaultTestClassName}}BindablePropertyInitHelpers.CreateDefaultTime); - protected internal partial string Time { get => __{{defaultTestClassName}}BindablePropertyInitHelpers.IsInitializingTime ? field : (string)GetValue(TimeProperty); set => SetValue(TimeProperty, value); } + protected internal static readonly global::Microsoft.Maui.Controls.BindableProperty TimeProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Time", typeof(string), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), string.Empty, (Microsoft.Maui.Controls.BindingMode)0, null, null, null, null, null); + protected internal partial string Time { get => false ? field : (string)GetValue(TimeProperty); set => SetValue(TimeProperty, value); } - static readonly global::Microsoft.Maui.Controls.BindablePropertyKey customDurationPropertyKey = global::Microsoft.Maui.Controls.BindableProperty.CreateReadOnly("CustomDuration", typeof(System.TimeSpan), typeof(TestNamespace.TestView), null, (Microsoft.Maui.Controls.BindingMode)0, null, null, null, null, __TestViewBindablePropertyInitHelpers.CreateDefaultCustomDuration); + static readonly global::Microsoft.Maui.Controls.BindablePropertyKey customDurationPropertyKey = global::Microsoft.Maui.Controls.BindableProperty.CreateReadOnly("CustomDuration", typeof(System.TimeSpan), typeof(TestNamespace.TestView), global::System.TimeSpan.FromSeconds(30), (Microsoft.Maui.Controls.BindingMode)0, null, null, null, null, null); /// /// BindableProperty for the property. /// internal static readonly global::Microsoft.Maui.Controls.BindableProperty CustomDurationProperty = customDurationPropertyKey.BindableProperty; - internal partial System.TimeSpan CustomDuration { get => __{{defaultTestClassName}}BindablePropertyInitHelpers.IsInitializingCustomDuration ? field : (System.TimeSpan)GetValue(CustomDurationProperty); private set => SetValue(customDurationPropertyKey, value); } - } - - file static class __{{defaultTestClassName}}BindablePropertyInitHelpers - { - public static volatile bool IsInitializingText = false; - public static object CreateDefaultText(global::Microsoft.Maui.Controls.BindableObject bindable) - { - IsInitializingText = true; - var defaultValue = ((TestView)bindable).Text; - IsInitializingText = false; - return defaultValue; - } - - public static volatile bool IsInitializingTime = false; - public static object CreateDefaultTime(global::Microsoft.Maui.Controls.BindableObject bindable) - { - IsInitializingTime = true; - var defaultValue = ((TestView)bindable).Time; - IsInitializingTime = false; - return defaultValue; - } - - public static volatile bool IsInitializingCustomDuration = false; - public static object CreateDefaultCustomDuration(global::Microsoft.Maui.Controls.BindableObject bindable) - { - IsInitializingCustomDuration = true; - var defaultValue = ((TestView)bindable).CustomDuration; - IsInitializingCustomDuration = false; - return defaultValue; - } + internal partial System.TimeSpan CustomDuration { get => false ? field : (System.TimeSpan)GetValue(CustomDurationProperty); private set => SetValue(customDurationPropertyKey, value); } } """; diff --git a/src/CommunityToolkit.Maui.SourceGenerators.UnitTests/BindablePropertyAttributeSourceGeneratorTests/BindablePropertyAttributeSourceGenerator_IntegrationTests.cs b/src/CommunityToolkit.Maui.SourceGenerators.UnitTests/BindablePropertyAttributeSourceGeneratorTests/BindablePropertyAttributeSourceGenerator_IntegrationTests.cs index 5f47781e03..7f3fcbbc1d 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators.UnitTests/BindablePropertyAttributeSourceGeneratorTests/BindablePropertyAttributeSourceGenerator_IntegrationTests.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators.UnitTests/BindablePropertyAttributeSourceGeneratorTests/BindablePropertyAttributeSourceGenerator_IntegrationTests.cs @@ -162,21 +162,8 @@ public partial class {{defaultTestClassName}} /// /// BindableProperty for the property. /// - public static readonly global::Microsoft.Maui.Controls.BindableProperty ValueProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Value", typeof(T), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, (Microsoft.Maui.Controls.BindingMode)0, null, null, null, null, __{{defaultTestClassName}}BindablePropertyInitHelpers.CreateDefaultValue); - public partial T? Value { get => __{{defaultTestClassName}}BindablePropertyInitHelpers.IsInitializingValue ? field : (T? )GetValue(ValueProperty); set => SetValue(ValueProperty, value); } - - [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] - private static class __{{defaultTestClassName}}BindablePropertyInitHelpers - { - public static volatile bool IsInitializingValue = false; - public static object CreateDefaultValue(global::Microsoft.Maui.Controls.BindableObject bindable) - { - IsInitializingValue = true; - var defaultValue = (({{defaultTestClassName}})bindable).Value; - IsInitializingValue = false; - return defaultValue; - } - } + public static readonly global::Microsoft.Maui.Controls.BindableProperty ValueProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Value", typeof(T), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), default, (Microsoft.Maui.Controls.BindingMode)0, null, null, null, null, null); + public partial T? Value { get => false ? field : (T? )GetValue(ValueProperty); set => SetValue(ValueProperty, value); } } """; diff --git a/src/CommunityToolkit.Maui.SourceGenerators/Generators/BindablePropertyAttributeSourceGenerator.cs b/src/CommunityToolkit.Maui.SourceGenerators/Generators/BindablePropertyAttributeSourceGenerator.cs index b7d163b3a2..3f241c4552 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators/Generators/BindablePropertyAttributeSourceGenerator.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators/Generators/BindablePropertyAttributeSourceGenerator.cs @@ -237,7 +237,7 @@ static void GenerateReadOnlyBindableProperty(StringBuilder sb, in BindableProper .Append(GetFormattedReturnType(nonNullableReturnType)) .Append("), typeof(") .Append(info.DeclaringType) - .Append("), null, ") + .Append("), ").Append(info.ResolvedInitializerExpression ?? "null").Append(", ") .Append(info.DefaultBindingMode) .Append(", ") .Append(info.ValidateValueMethodName) @@ -304,7 +304,7 @@ static void GenerateBindableProperty(StringBuilder sb, in BindablePropertyModel .Append(GetFormattedReturnType(nonNullableReturnType)) .Append("), typeof(") .Append(info.DeclaringType) - .Append("), null, ") + .Append("), ").Append(info.ResolvedInitializerExpression ?? "null").Append(", ") .Append(info.DefaultBindingMode) .Append(", ") .Append(info.ValidateValueMethodName) @@ -408,8 +408,16 @@ static BindablePropertySemanticValues SemanticTransform(GeneratorAttributeSyntax var propertyAccessibility = GetPropertyAccessibility(propertySymbol); + // Resolve initializer expression to a fully qualified string for use as defaultValue + string? resolvedInitializer = null; + if (hasInitializer && propertyDeclarationSyntax.Initializer is not null) + { + resolvedInitializer = Helpers.InitializerExpressionResolver.TryResolve( + propertyDeclarationSyntax.Initializer.Value, semanticModel); + } + var attributeData = context.Attributes[0]; - bindablePropertyModels[0] = CreateBindablePropertyModel(attributeData, propertySymbol.ContainingType, propertySymbol.Name, returnType, doesContainNewKeyword, isReadOnlyBindableProperty, setterAccessibility, hasInitializer, propertyAccessibility); + bindablePropertyModels[0] = CreateBindablePropertyModel(attributeData, propertySymbol.ContainingType, propertySymbol.Name, returnType, doesContainNewKeyword, isReadOnlyBindableProperty, setterAccessibility, hasInitializer, propertyAccessibility, resolvedInitializer); return new(propertyInfo, ImmutableArray.Create(bindablePropertyModels)); } @@ -517,7 +525,7 @@ static string GetGenericTypeParameters(INamedTypeSymbol typeSymbol) return sb.ToString(); } - static BindablePropertyModel CreateBindablePropertyModel(in AttributeData attributeData, in INamedTypeSymbol declaringType, in string propertyName, in ITypeSymbol returnType, in bool doesContainNewKeyword, in bool isReadOnly, in string? setterAccessibility, in bool hasInitializer, in string? propertyAccessibility) + static BindablePropertyModel CreateBindablePropertyModel(in AttributeData attributeData, in INamedTypeSymbol declaringType, in string propertyName, in ITypeSymbol returnType, in bool doesContainNewKeyword, in bool isReadOnly, in string? setterAccessibility, in bool hasInitializer, in string? propertyAccessibility, in string? resolvedInitializerExpression) { if (attributeData.AttributeClass is null) { @@ -532,7 +540,10 @@ static BindablePropertyModel CreateBindablePropertyModel(in AttributeData attrib var validateValueMethodName = attributeData.GetNamedMethodGroupArgumentsAttributeValueByNameAsString(nameof(BindablePropertyModel.ValidateValueMethodName)); var newKeywordText = doesContainNewKeyword ? "new " : string.Empty; - return new BindablePropertyModel(propertyName, returnType, declaringType, defaultBindingMode, validateValueMethodName, propertyChangedMethodName, propertyChangingMethodName, coerceValueMethodName, defaultValueCreatorMethodName, newKeywordText, isReadOnly, setterAccessibility, hasInitializer, propertyAccessibility); + // Only use the resolved initializer when there's no explicit DefaultValueCreatorMethodName + var effectiveResolvedInitializer = defaultValueCreatorMethodName is "null" ? resolvedInitializerExpression : null; + + return new BindablePropertyModel(propertyName, returnType, declaringType, defaultBindingMode, validateValueMethodName, propertyChangedMethodName, propertyChangingMethodName, coerceValueMethodName, defaultValueCreatorMethodName, newKeywordText, isReadOnly, setterAccessibility, hasInitializer, propertyAccessibility, effectiveResolvedInitializer); } [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/src/CommunityToolkit.Maui.SourceGenerators/Helpers/InitializerExpressionResolver.cs b/src/CommunityToolkit.Maui.SourceGenerators/Helpers/InitializerExpressionResolver.cs new file mode 100644 index 0000000000..4c69d2bbed --- /dev/null +++ b/src/CommunityToolkit.Maui.SourceGenerators/Helpers/InitializerExpressionResolver.cs @@ -0,0 +1,287 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace CommunityToolkit.Maui.SourceGenerators.Helpers; + +/// +/// Resolves property initializer expressions to fully qualified C# code suitable for use +/// as the defaultValue parameter in BindableProperty.Create. +/// +static class InitializerExpressionResolver +{ + /// + /// Attempts to resolve an initializer expression to a fully qualified C# expression string. + /// Returns null if the expression cannot be resolved (e.g. collection expressions, complex lambdas). + /// + public static string? TryResolve(ExpressionSyntax expression, SemanticModel semanticModel) + { + // Try symbolic resolution first, preserves readable identifiers like float.Epsilon + var resolved = TryResolveExpression(expression, semanticModel); + if (resolved is not null) + { + return resolved; + } + + // Fall back to constant folding, handles nameof(), some compiler-reduced expressions + var constantValue = semanticModel.GetConstantValue(expression); + if (constantValue.HasValue) + { + var typeInfo = semanticModel.GetTypeInfo(expression); + return FormatConstantValue(constantValue.Value, typeInfo.Type); + } + + return null; + } + + static string? TryResolveExpression(ExpressionSyntax expression, SemanticModel semanticModel) + { + switch (expression) + { + case LiteralExpressionSyntax literal: + return literal.Token.Text; + + case PrefixUnaryExpressionSyntax prefixUnary: + var operand = TryResolveExpression(prefixUnary.Operand, semanticModel); + if (operand is null) + { + return null; + } + return $"{prefixUnary.OperatorToken.Text}{operand}"; + + case MemberAccessExpressionSyntax memberAccess: + return TryResolveMemberAccess(memberAccess, semanticModel); + + case IdentifierNameSyntax identifier: + return TryResolveIdentifier(identifier, semanticModel); + + case PredefinedTypeSyntax: + // Keywords like int, double, string, valid as-is in generated code + return expression.ToString(); + + case CastExpressionSyntax castExpression: + var innerExpr = TryResolveExpression(castExpression.Expression, semanticModel); + if (innerExpr is null) + { + return null; + } + var castType = semanticModel.GetTypeInfo(castExpression.Type).Type; + if (castType is null) + { + return null; + } + var qualifiedCastType = castType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + return $"({qualifiedCastType}){innerExpr}"; + + case ParenthesizedExpressionSyntax paren: + var inner = TryResolveExpression(paren.Expression, semanticModel); + return inner is not null ? $"({inner})" : null; + + case BinaryExpressionSyntax binary: + var left = TryResolveExpression(binary.Left, semanticModel); + var right = TryResolveExpression(binary.Right, semanticModel); + if (left is null || right is null) + { + return null; + } + return $"{left} {binary.OperatorToken.Text} {right}"; + + case DefaultExpressionSyntax defaultExpr: + if (defaultExpr.Type is not null) + { + var defType = semanticModel.GetTypeInfo(defaultExpr.Type).Type; + if (defType is not null) + { + return $"default({defType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)})"; + } + } + return "default"; + + case InvocationExpressionSyntax invocation: + return TryResolveInvocation(invocation, semanticModel); + + case ObjectCreationExpressionSyntax objectCreation: + return TryResolveObjectCreation(objectCreation, semanticModel); + + default: + // Collection expressions, lambdas, etc., cannot resolve + return null; + } + } + + static string? TryResolveMemberAccess(MemberAccessExpressionSyntax memberAccess, SemanticModel semanticModel) + { + var receiver = TryResolveExpression(memberAccess.Expression, semanticModel); + if (receiver is null) + { + return null; + } + + return $"{receiver}.{memberAccess.Name.Identifier.Text}"; + } + + static string? TryResolveIdentifier(IdentifierNameSyntax identifier, SemanticModel semanticModel) + { + var symbolInfo = semanticModel.GetSymbolInfo(identifier); + var symbol = symbolInfo.Symbol; + + if (symbol is null) + { + return null; + } + + // For type references (e.g. the "ValidationBehaviorDefaults" in "ValidationBehaviorDefaults.Flags") + if (symbol is INamedTypeSymbol typeSymbol) + { + return typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + } + + // For namespace references (e.g. "System" in "System.TimeSpan.Zero") + if (symbol is INamespaceSymbol namespaceSymbol) + { + return namespaceSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + } + + // For static field/property references used as standalone identifiers + if (symbol is IFieldSymbol { IsStatic: true } or IPropertySymbol { IsStatic: true }) + { + var containingType = symbol.ContainingType; + if (containingType is not null) + { + return $"{containingType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}.{symbol.Name}"; + } + } + + return null; + } + + static string? TryResolveInvocation(InvocationExpressionSyntax invocation, SemanticModel semanticModel) + { + var methodExpr = TryResolveExpression(invocation.Expression, semanticModel); + if (methodExpr is null) + { + return null; + } + + // Resolve type arguments if present + var symbolInfo = semanticModel.GetSymbolInfo(invocation); + if (symbolInfo.Symbol is IMethodSymbol methodSymbol && methodSymbol.IsGenericMethod) + { + var typeArgs = string.Join(", ", methodSymbol.TypeArguments.Select( + t => t.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat))); + // Replace the method name with the generic version + var lastDot = methodExpr.LastIndexOf('.'); + if (lastDot >= 0) + { + methodExpr = $"{methodExpr[..lastDot]}.{methodSymbol.Name}<{typeArgs}>"; + } + else + { + methodExpr = $"{methodSymbol.Name}<{typeArgs}>"; + } + } + + var args = new List(invocation.ArgumentList.Arguments.Count); + foreach (var arg in invocation.ArgumentList.Arguments) + { + var resolved = TryResolveExpression(arg.Expression, semanticModel); + if (resolved is null) + { + return null; + } + args.Add(resolved); + } + + return $"{methodExpr}({string.Join(", ", args)})"; + } + + static string? TryResolveObjectCreation(ObjectCreationExpressionSyntax objectCreation, SemanticModel semanticModel) + { + var typeInfo = semanticModel.GetTypeInfo(objectCreation); + if (typeInfo.Type is null) + { + return null; + } + + var qualifiedTypeName = typeInfo.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + + if (objectCreation.ArgumentList is null || objectCreation.ArgumentList.Arguments.Count is 0) + { + return $"new {qualifiedTypeName}()"; + } + + var args = new List(objectCreation.ArgumentList.Arguments.Count); + foreach (var arg in objectCreation.ArgumentList.Arguments) + { + var resolved = TryResolveExpression(arg.Expression, semanticModel); + if (resolved is null) + { + return null; + } + args.Add(resolved); + } + + return $"new {qualifiedTypeName}({string.Join(", ", args)})"; + } + + static string FormatConstantValue(object? value, ITypeSymbol? typeSymbol) + { + if (value is null) + { + return "null"; + } + + // For enum types, cast the underlying value to the enum type + if (typeSymbol is INamedTypeSymbol { TypeKind: TypeKind.Enum } enumType) + { + var qualifiedType = enumType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + return $"({qualifiedType}){FormatPrimitiveValue(value)}"; + } + + return FormatPrimitiveValue(value); + } + + static string FormatPrimitiveValue(object value) => value switch + { + bool b => b ? "true" : "false", + char c => $"'{EscapeChar(c)}'", + string s => $"\"{EscapeString(s)}\"", + float f => float.IsPositiveInfinity(f) ? "float.PositiveInfinity" + : float.IsNegativeInfinity(f) ? "float.NegativeInfinity" + : float.IsNaN(f) ? "float.NaN" + : $"{f}f", + double d => double.IsPositiveInfinity(d) ? "double.PositiveInfinity" + : double.IsNegativeInfinity(d) ? "double.NegativeInfinity" + : double.IsNaN(d) ? "double.NaN" + : FormattableString.Invariant($"{d}d"), + decimal m => FormattableString.Invariant($"{m}m"), + byte b => b.ToString(), + sbyte sb => sb.ToString(), + short s => s.ToString(), + ushort us => us.ToString(), + int i => i.ToString(), + uint u => $"{u}u", + long l => $"{l}L", + ulong ul => $"{ul}UL", + _ => value.ToString() ?? "null" + }; + + static string EscapeChar(char c) => c switch + { + '\'' => @"\'", + '\\' => @"\\", + '\0' => @"\0", + '\n' => @"\n", + '\r' => @"\r", + '\t' => @"\t", + _ => c.ToString() + }; + + static string EscapeString(string s) => s + .Replace("\\", @"\\") + .Replace("\"", @"\""") + .Replace("\n", @"\n") + .Replace("\r", @"\r") + .Replace("\t", @"\t") + .Replace("\0", @"\0"); +} diff --git a/src/CommunityToolkit.Maui.SourceGenerators/Models/BindablePropertyRecords.cs b/src/CommunityToolkit.Maui.SourceGenerators/Models/BindablePropertyRecords.cs index ff334352b2..7283e84e80 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators/Models/BindablePropertyRecords.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators/Models/BindablePropertyRecords.cs @@ -3,10 +3,11 @@ namespace CommunityToolkit.Maui.SourceGenerators.Models; -public record BindablePropertyModel(string PropertyName, ITypeSymbol ReturnType, ITypeSymbol DeclaringType, string DefaultBindingMode, string ValidateValueMethodName, string PropertyChangedMethodName, string PropertyChangingMethodName, string CoerceValueMethodName, string DefaultValueCreatorMethodName, string NewKeywordText, bool IsReadOnlyBindableProperty, string? SetterAccessibility, bool HasInitializer, string? PropertyAccessibility) +public record BindablePropertyModel(string PropertyName, ITypeSymbol ReturnType, ITypeSymbol DeclaringType, string DefaultBindingMode, string ValidateValueMethodName, string PropertyChangedMethodName, string PropertyChangingMethodName, string CoerceValueMethodName, string DefaultValueCreatorMethodName, string NewKeywordText, bool IsReadOnlyBindableProperty, string? SetterAccessibility, bool HasInitializer, string? PropertyAccessibility, string? ResolvedInitializerExpression = null) { // When both a DefaultValueCreatorMethodName and an initializer are provided, we implement the DefaultValueCreator method and the ignore the partial Property initializer - public bool ShouldUsePropertyInitializer => HasInitializer && DefaultValueCreatorMethodName is "null"; + public bool ShouldUsePropertyInitializer => HasInitializer && DefaultValueCreatorMethodName is "null" && ResolvedInitializerExpression is null; + public bool ShouldUseDirectDefaultValue => ResolvedInitializerExpression is not null; public string BindablePropertyName => $"{PropertyName}Property"; public string BindablePropertyKeyName => $"{char.ToLower(PropertyName[0])}{PropertyName[1..]}PropertyKey"; public string EffectiveDefaultValueCreatorMethodName => ShouldUsePropertyInitializer ? $"CreateDefault{PropertyName}" : DefaultValueCreatorMethodName; diff --git a/src/CommunityToolkit.Maui.UnitTests/Services/PopupServiceTests.cs b/src/CommunityToolkit.Maui.UnitTests/Services/PopupServiceTests.cs index 498e7f7756..a9701cb1c4 100644 --- a/src/CommunityToolkit.Maui.UnitTests/Services/PopupServiceTests.cs +++ b/src/CommunityToolkit.Maui.UnitTests/Services/PopupServiceTests.cs @@ -653,12 +653,12 @@ protected virtual async void HandlePopupOpened(object? sender, EventArgs e) } Console.WriteLine( - $@"{DateTime.Now:O} Closing {BindingContext.GetType().Name} - {Application.Current?.Windows[0].Page?.Navigation.ModalStack.Count}"); + $@"{DateTime.Now:O} Closing {BindingContext.GetType().Name} - {Application.Current?.Windows.FirstOrDefault()?.Page?.Navigation.ModalStack.Count}"); await CloseAsync(Result, cancellationTokenSource?.Token ?? TestContext.Current.CancellationToken); Console.WriteLine( - $@"{DateTime.Now:O} Closed {BindingContext.GetType().Name} - {Application.Current?.Windows[0].Page?.Navigation.ModalStack.Count}"); + $@"{DateTime.Now:O} Closed {BindingContext.GetType().Name} - {Application.Current?.Windows.FirstOrDefault()?.Page?.Navigation.ModalStack.Count}"); popupClosedTCS.SetResult(); } diff --git a/src/CommunityToolkit.Maui/Behaviors/Validators/ValidationBehavior.shared.cs b/src/CommunityToolkit.Maui/Behaviors/Validators/ValidationBehavior.shared.cs index 3cb6d14433..976b466868 100644 --- a/src/CommunityToolkit.Maui/Behaviors/Validators/ValidationBehavior.shared.cs +++ b/src/CommunityToolkit.Maui/Behaviors/Validators/ValidationBehavior.shared.cs @@ -191,7 +191,7 @@ protected override async void OnAttachedTo(VisualElement bindable) currentStatus = ValidationFlags.ValidateOnAttaching; OnValuePropertyNamePropertyChanged(); - await UpdateStateAsync(View, Flags, false).ConfigureAwait(false); + await UpdateStateAsync(View, Flags, false); } finally { @@ -221,7 +221,7 @@ protected override async void OnViewPropertyChanged(VisualElement sender, Proper false => ValidationFlags.ValidateOnUnfocused }; - await UpdateStateAsync(View, Flags, false).ConfigureAwait(false); + await UpdateStateAsync(View, Flags, false); } } @@ -233,8 +233,15 @@ protected override async void OnViewPropertyChanged(VisualElement sender, Proper /// protected static async void OnValidationPropertyChanged(BindableObject bindable, object oldValue, object newValue) { - var validationBehavior = (ValidationBehavior)bindable; - await validationBehavior.UpdateStateAsync(validationBehavior.View, validationBehavior.Flags, false).ConfigureAwait(false); + try + { + var validationBehavior = (ValidationBehavior)bindable; + await validationBehavior.UpdateStateAsync(validationBehavior.View, validationBehavior.Flags, false); + } + catch (Exception ex) when (Options.ShouldSuppressExceptionsInBehaviors) + { + Trace.TraceInformation("{0}", ex); + } } static void OnIsValidPropertyChanged(BindableObject bindable, object oldValue, object newValue) From 91be822c472eb6c81c5e8b075044f3dbde365673 Mon Sep 17 00:00:00 2001 From: Matt Goldman Date: Sun, 19 Apr 2026 07:09:28 +1000 Subject: [PATCH 02/14] Update src/CommunityToolkit.Maui.SourceGenerators/Helpers/InitializerExpressionResolver.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Helpers/InitializerExpressionResolver.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/CommunityToolkit.Maui.SourceGenerators/Helpers/InitializerExpressionResolver.cs b/src/CommunityToolkit.Maui.SourceGenerators/Helpers/InitializerExpressionResolver.cs index 4c69d2bbed..25e4a3cd5d 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators/Helpers/InitializerExpressionResolver.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators/Helpers/InitializerExpressionResolver.cs @@ -101,6 +101,11 @@ static class InitializerExpressionResolver return TryResolveInvocation(invocation, semanticModel); case ObjectCreationExpressionSyntax objectCreation: + var objectCreationType = semanticModel.GetTypeInfo(objectCreation).Type; + if (objectCreationType is null || !objectCreationType.IsValueType) + { + return null; + } return TryResolveObjectCreation(objectCreation, semanticModel); default: From 6bdce2a1a208aacdde2b194b92140a21b749060d Mon Sep 17 00:00:00 2001 From: Matt Goldman Date: Sun, 19 Apr 2026 07:09:44 +1000 Subject: [PATCH 03/14] Update src/CommunityToolkit.Maui.SourceGenerators/Helpers/InitializerExpressionResolver.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Helpers/InitializerExpressionResolver.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CommunityToolkit.Maui.SourceGenerators/Helpers/InitializerExpressionResolver.cs b/src/CommunityToolkit.Maui.SourceGenerators/Helpers/InitializerExpressionResolver.cs index 25e4a3cd5d..310723a700 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators/Helpers/InitializerExpressionResolver.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators/Helpers/InitializerExpressionResolver.cs @@ -254,7 +254,7 @@ static string FormatConstantValue(object? value, ITypeSymbol? typeSymbol) float f => float.IsPositiveInfinity(f) ? "float.PositiveInfinity" : float.IsNegativeInfinity(f) ? "float.NegativeInfinity" : float.IsNaN(f) ? "float.NaN" - : $"{f}f", + : global::System.FormattableString.Invariant($"{f}f"), double d => double.IsPositiveInfinity(d) ? "double.PositiveInfinity" : double.IsNegativeInfinity(d) ? "double.NegativeInfinity" : double.IsNaN(d) ? "double.NaN" From 7ca5aca14af8d7272186113ba363bcb772f5164a Mon Sep 17 00:00:00 2001 From: Matt Goldman Date: Sun, 19 Apr 2026 07:10:35 +1000 Subject: [PATCH 04/14] Update src/CommunityToolkit.Maui.SourceGenerators/Helpers/InitializerExpressionResolver.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Helpers/InitializerExpressionResolver.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/CommunityToolkit.Maui.SourceGenerators/Helpers/InitializerExpressionResolver.cs b/src/CommunityToolkit.Maui.SourceGenerators/Helpers/InitializerExpressionResolver.cs index 310723a700..4c8a7742ca 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators/Helpers/InitializerExpressionResolver.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators/Helpers/InitializerExpressionResolver.cs @@ -39,6 +39,14 @@ static class InitializerExpressionResolver switch (expression) { case LiteralExpressionSyntax literal: + if (literal.IsKind(SyntaxKind.DefaultLiteralExpression)) + { + var defaultLiteralType = semanticModel.GetTypeInfo(literal).Type; + if (defaultLiteralType is not null) + { + return $"default({defaultLiteralType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)})"; + } + } return literal.Token.Text; case PrefixUnaryExpressionSyntax prefixUnary: From 4a92cbe84299a16b81e4336ec5cc5669ffb103de Mon Sep 17 00:00:00 2001 From: Matt Goldman Date: Sun, 19 Apr 2026 07:42:29 +1000 Subject: [PATCH 05/14] Address copilot feedback about properties and invocations --- ...yAttributeSourceGenerator_EdgeCaseTests.cs | 16 +++++++++-- .../Helpers/InitializerExpressionResolver.cs | 28 ++++++++++++++++--- 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/src/CommunityToolkit.Maui.SourceGenerators.UnitTests/BindablePropertyAttributeSourceGeneratorTests/BindablePropertyAttributeSourceGenerator_EdgeCaseTests.cs b/src/CommunityToolkit.Maui.SourceGenerators.UnitTests/BindablePropertyAttributeSourceGeneratorTests/BindablePropertyAttributeSourceGenerator_EdgeCaseTests.cs index e03f8d3fce..347f6f3cdd 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators.UnitTests/BindablePropertyAttributeSourceGeneratorTests/BindablePropertyAttributeSourceGenerator_EdgeCaseTests.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators.UnitTests/BindablePropertyAttributeSourceGeneratorTests/BindablePropertyAttributeSourceGenerator_EdgeCaseTests.cs @@ -486,8 +486,20 @@ public partial class {{defaultTestClassName}} /// /// BindableProperty for the property. /// - public static readonly global::Microsoft.Maui.Controls.BindableProperty CurrentTimeProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("CurrentTime", typeof(System.DateTimeOffset), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), global::System.DateTimeOffset.UtcNow, (Microsoft.Maui.Controls.BindingMode)0, null, null, null, null, null); - public partial System.DateTimeOffset CurrentTime { get => false ? field : (System.DateTimeOffset)GetValue(CurrentTimeProperty); set => SetValue(CurrentTimeProperty, value); } + public static readonly global::Microsoft.Maui.Controls.BindableProperty CurrentTimeProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("CurrentTime", typeof(System.DateTimeOffset), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, (Microsoft.Maui.Controls.BindingMode)0, null, null, null, null, __{{defaultTestClassName}}BindablePropertyInitHelpers.CreateDefaultCurrentTime); + public partial System.DateTimeOffset CurrentTime { get => __{{defaultTestClassName}}BindablePropertyInitHelpers.IsInitializingCurrentTime ? field : (System.DateTimeOffset)GetValue(CurrentTimeProperty); set => SetValue(CurrentTimeProperty, value); } + } + + file static class __{{defaultTestClassName}}BindablePropertyInitHelpers + { + public static volatile bool IsInitializingCurrentTime = false; + public static object CreateDefaultCurrentTime(global::Microsoft.Maui.Controls.BindableObject bindable) + { + IsInitializingCurrentTime = true; + var defaultValue = (({{defaultTestClassName}})bindable).CurrentTime; + IsInitializingCurrentTime = false; + return defaultValue; + } } """; diff --git a/src/CommunityToolkit.Maui.SourceGenerators/Helpers/InitializerExpressionResolver.cs b/src/CommunityToolkit.Maui.SourceGenerators/Helpers/InitializerExpressionResolver.cs index 4c8a7742ca..9156ceae8b 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators/Helpers/InitializerExpressionResolver.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators/Helpers/InitializerExpressionResolver.cs @@ -105,8 +105,10 @@ static class InitializerExpressionResolver } return "default"; - case InvocationExpressionSyntax invocation: - return TryResolveInvocation(invocation, semanticModel); + case InvocationExpressionSyntax: + // Invocations (e.g. Guid.NewGuid(), TimeSpan.FromSeconds(5)) may return different + // values per call; resolve only if GetConstantValue can fold them (e.g. nameof()). + return null; case ObjectCreationExpressionSyntax objectCreation: var objectCreationType = semanticModel.GetTypeInfo(objectCreation).Type; @@ -124,6 +126,24 @@ static class InitializerExpressionResolver static string? TryResolveMemberAccess(MemberAccessExpressionSyntax memberAccess, SemanticModel semanticModel) { + // Only resolve member accesses that are provably safe to evaluate once and share + // across all instances (type/namespace navigation, const fields, static readonly fields). + // Reject static properties (e.g. DateTimeOffset.UtcNow) which may return different + // values per call or have side effects. + var symbol = semanticModel.GetSymbolInfo(memberAccess).Symbol; + switch (symbol) + { + case INamedTypeSymbol: + case INamespaceSymbol: + break; // Type/namespace navigation, always safe + case IFieldSymbol { IsConst: true }: + break; // Compile-time constants (including enum members) + case IFieldSymbol { IsStatic: true, IsReadOnly: true }: + break; // Static readonly fields (e.g. TimeSpan.Zero) + default: + return null; // Reject properties, non-readonly fields, methods, etc. + } + var receiver = TryResolveExpression(memberAccess.Expression, semanticModel); if (receiver is null) { @@ -155,8 +175,8 @@ static class InitializerExpressionResolver return namespaceSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); } - // For static field/property references used as standalone identifiers - if (symbol is IFieldSymbol { IsStatic: true } or IPropertySymbol { IsStatic: true }) + // For const or static readonly field references used as standalone identifiers + if (symbol is IFieldSymbol { IsConst: true } or IFieldSymbol { IsStatic: true, IsReadOnly: true }) { var containingType = symbol.ContainingType; if (containingType is not null) From 7803e2599ae6369566fdfd03d649b19ea89c0981 Mon Sep 17 00:00:00 2001 From: Matt Goldman Date: Sun, 19 Apr 2026 07:44:18 +1000 Subject: [PATCH 06/14] Update test to accomodate literal expression change --- ...BindablePropertyAttributeSourceGenerator_IntegrationTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CommunityToolkit.Maui.SourceGenerators.UnitTests/BindablePropertyAttributeSourceGeneratorTests/BindablePropertyAttributeSourceGenerator_IntegrationTests.cs b/src/CommunityToolkit.Maui.SourceGenerators.UnitTests/BindablePropertyAttributeSourceGeneratorTests/BindablePropertyAttributeSourceGenerator_IntegrationTests.cs index 7f3fcbbc1d..f12f488f2e 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators.UnitTests/BindablePropertyAttributeSourceGeneratorTests/BindablePropertyAttributeSourceGenerator_IntegrationTests.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators.UnitTests/BindablePropertyAttributeSourceGeneratorTests/BindablePropertyAttributeSourceGenerator_IntegrationTests.cs @@ -162,7 +162,7 @@ public partial class {{defaultTestClassName}} /// /// BindableProperty for the property. /// - public static readonly global::Microsoft.Maui.Controls.BindableProperty ValueProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Value", typeof(T), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), default, (Microsoft.Maui.Controls.BindingMode)0, null, null, null, null, null); + public static readonly global::Microsoft.Maui.Controls.BindableProperty ValueProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Value", typeof(T), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), default(T), (Microsoft.Maui.Controls.BindingMode)0, null, null, null, null, null); public partial T? Value { get => false ? field : (T? )GetValue(ValueProperty); set => SetValue(ValueProperty, value); } } """; From b92032f3343ea6a824897836a505dcf970f4bf09 Mon Sep 17 00:00:00 2001 From: Matt Goldman Date: Sun, 19 Apr 2026 08:01:34 +1000 Subject: [PATCH 07/14] Update timespan tests for new resolver pattern --- ...yAttributeSourceGenerator_CommonUsageTests.cs | 16 ++++++++++++++-- ...ertyAttributeSourceGenerator_EdgeCaseTests.cs | 16 ++++++++++++++-- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/CommunityToolkit.Maui.SourceGenerators.UnitTests/BindablePropertyAttributeSourceGeneratorTests/BindablePropertyAttributeSourceGenerator_CommonUsageTests.cs b/src/CommunityToolkit.Maui.SourceGenerators.UnitTests/BindablePropertyAttributeSourceGeneratorTests/BindablePropertyAttributeSourceGenerator_CommonUsageTests.cs index 4f9b1d3eef..df3f84cd34 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators.UnitTests/BindablePropertyAttributeSourceGeneratorTests/BindablePropertyAttributeSourceGenerator_CommonUsageTests.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators.UnitTests/BindablePropertyAttributeSourceGeneratorTests/BindablePropertyAttributeSourceGenerator_CommonUsageTests.cs @@ -604,8 +604,20 @@ public partial class {{defaultTestClassName}} /// /// BindableProperty for the property. /// - public static readonly global::Microsoft.Maui.Controls.BindableProperty CustomDurationProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("CustomDuration", typeof(System.TimeSpan), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), global::System.TimeSpan.FromSeconds(30), (Microsoft.Maui.Controls.BindingMode)0, null, null, null, null, null); - public partial System.TimeSpan CustomDuration { get => false ? field : (System.TimeSpan)GetValue(CustomDurationProperty); set => SetValue(CustomDurationProperty, value); } + public static readonly global::Microsoft.Maui.Controls.BindableProperty CustomDurationProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("CustomDuration", typeof(System.TimeSpan), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, (Microsoft.Maui.Controls.BindingMode)0, null, null, null, null, __{{defaultTestClassName}}BindablePropertyInitHelpers.CreateDefaultCustomDuration); + public partial System.TimeSpan CustomDuration { get => __{{defaultTestClassName}}BindablePropertyInitHelpers.IsInitializingCustomDuration ? field : (System.TimeSpan)GetValue(CustomDurationProperty); set => SetValue(CustomDurationProperty, value); } + } + + file static class __{{defaultTestClassName}}BindablePropertyInitHelpers + { + public static volatile bool IsInitializingCustomDuration = false; + public static object CreateDefaultCustomDuration(global::Microsoft.Maui.Controls.BindableObject bindable) + { + IsInitializingCustomDuration = true; + var defaultValue = (({{defaultTestClassName}})bindable).CustomDuration; + IsInitializingCustomDuration = false; + return defaultValue; + } } """; diff --git a/src/CommunityToolkit.Maui.SourceGenerators.UnitTests/BindablePropertyAttributeSourceGeneratorTests/BindablePropertyAttributeSourceGenerator_EdgeCaseTests.cs b/src/CommunityToolkit.Maui.SourceGenerators.UnitTests/BindablePropertyAttributeSourceGeneratorTests/BindablePropertyAttributeSourceGenerator_EdgeCaseTests.cs index 347f6f3cdd..0a7b0a9500 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators.UnitTests/BindablePropertyAttributeSourceGeneratorTests/BindablePropertyAttributeSourceGenerator_EdgeCaseTests.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators.UnitTests/BindablePropertyAttributeSourceGeneratorTests/BindablePropertyAttributeSourceGenerator_EdgeCaseTests.cs @@ -602,12 +602,24 @@ public partial class {{defaultTestClassName}} protected internal static readonly global::Microsoft.Maui.Controls.BindableProperty TimeProperty = global::Microsoft.Maui.Controls.BindableProperty.Create("Time", typeof(string), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), string.Empty, (Microsoft.Maui.Controls.BindingMode)0, null, null, null, null, null); protected internal partial string Time { get => false ? field : (string)GetValue(TimeProperty); set => SetValue(TimeProperty, value); } - static readonly global::Microsoft.Maui.Controls.BindablePropertyKey customDurationPropertyKey = global::Microsoft.Maui.Controls.BindableProperty.CreateReadOnly("CustomDuration", typeof(System.TimeSpan), typeof(TestNamespace.TestView), global::System.TimeSpan.FromSeconds(30), (Microsoft.Maui.Controls.BindingMode)0, null, null, null, null, null); + static readonly global::Microsoft.Maui.Controls.BindablePropertyKey customDurationPropertyKey = global::Microsoft.Maui.Controls.BindableProperty.CreateReadOnly("CustomDuration", typeof(System.TimeSpan), typeof(TestNamespace.TestView), null, (Microsoft.Maui.Controls.BindingMode)0, null, null, null, null, __{{defaultTestClassName}}BindablePropertyInitHelpers.CreateDefaultCustomDuration); /// /// BindableProperty for the property. /// internal static readonly global::Microsoft.Maui.Controls.BindableProperty CustomDurationProperty = customDurationPropertyKey.BindableProperty; - internal partial System.TimeSpan CustomDuration { get => false ? field : (System.TimeSpan)GetValue(CustomDurationProperty); private set => SetValue(customDurationPropertyKey, value); } + internal partial System.TimeSpan CustomDuration { get => __{{defaultTestClassName}}BindablePropertyInitHelpers.IsInitializingCustomDuration ? field : (System.TimeSpan)GetValue(CustomDurationProperty); private set => SetValue(customDurationPropertyKey, value); } + } + + file static class __{{defaultTestClassName}}BindablePropertyInitHelpers + { + public static volatile bool IsInitializingCustomDuration = false; + public static object CreateDefaultCustomDuration(global::Microsoft.Maui.Controls.BindableObject bindable) + { + IsInitializingCustomDuration = true; + var defaultValue = (({{defaultTestClassName}})bindable).CustomDuration; + IsInitializingCustomDuration = false; + return defaultValue; + } } """; From 9e56230dada7b3ce4248f4845a42b636fb46c641 Mon Sep 17 00:00:00 2001 From: Matt Goldman Date: Sun, 19 Apr 2026 09:59:03 +1000 Subject: [PATCH 08/14] Update src/CommunityToolkit.Maui.SourceGenerators/Helpers/InitializerExpressionResolver.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Helpers/InitializerExpressionResolver.cs | 40 ------------------- 1 file changed, 40 deletions(-) diff --git a/src/CommunityToolkit.Maui.SourceGenerators/Helpers/InitializerExpressionResolver.cs b/src/CommunityToolkit.Maui.SourceGenerators/Helpers/InitializerExpressionResolver.cs index 9156ceae8b..c47ad0400e 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators/Helpers/InitializerExpressionResolver.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators/Helpers/InitializerExpressionResolver.cs @@ -188,46 +188,6 @@ static class InitializerExpressionResolver return null; } - static string? TryResolveInvocation(InvocationExpressionSyntax invocation, SemanticModel semanticModel) - { - var methodExpr = TryResolveExpression(invocation.Expression, semanticModel); - if (methodExpr is null) - { - return null; - } - - // Resolve type arguments if present - var symbolInfo = semanticModel.GetSymbolInfo(invocation); - if (symbolInfo.Symbol is IMethodSymbol methodSymbol && methodSymbol.IsGenericMethod) - { - var typeArgs = string.Join(", ", methodSymbol.TypeArguments.Select( - t => t.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat))); - // Replace the method name with the generic version - var lastDot = methodExpr.LastIndexOf('.'); - if (lastDot >= 0) - { - methodExpr = $"{methodExpr[..lastDot]}.{methodSymbol.Name}<{typeArgs}>"; - } - else - { - methodExpr = $"{methodSymbol.Name}<{typeArgs}>"; - } - } - - var args = new List(invocation.ArgumentList.Arguments.Count); - foreach (var arg in invocation.ArgumentList.Arguments) - { - var resolved = TryResolveExpression(arg.Expression, semanticModel); - if (resolved is null) - { - return null; - } - args.Add(resolved); - } - - return $"{methodExpr}({string.Join(", ", args)})"; - } - static string? TryResolveObjectCreation(ObjectCreationExpressionSyntax objectCreation, SemanticModel semanticModel) { var typeInfo = semanticModel.GetTypeInfo(objectCreation); From f7af305dcbd0438c99fb91ded2a88ad65139b689 Mon Sep 17 00:00:00 2001 From: Matt Goldman Date: Sun, 19 Apr 2026 10:09:12 +1000 Subject: [PATCH 09/14] Address copilot feedback --- .../Helpers/InitializerExpressionResolver.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/CommunityToolkit.Maui.SourceGenerators/Helpers/InitializerExpressionResolver.cs b/src/CommunityToolkit.Maui.SourceGenerators/Helpers/InitializerExpressionResolver.cs index c47ad0400e..53ecc2a6d7 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators/Helpers/InitializerExpressionResolver.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators/Helpers/InitializerExpressionResolver.cs @@ -116,6 +116,13 @@ static class InitializerExpressionResolver { return null; } + // Could incorrectly drop the initializer block if present, so early exit in that case + // to avoid this. If required can add a traversal to resolve the initializer block, + // but probably a rare case so not worth the complexity for now. + if (objectCreation.Initializer is not null) + { + return null; + } return TryResolveObjectCreation(objectCreation, semanticModel); default: From 27b7ab21b1ba752a068bb8c5b646004f32bcdc5b Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+TheCodeTraveler@users.noreply.github.com> Date: Sun, 19 Apr 2026 14:56:14 -0700 Subject: [PATCH 10/14] Use Try Pattern --- .../instructions/codestyle.instructions.md | 13 ++ .../Helpers/InitializerExpressionResolver.cs | 155 +++++++++++------- 2 files changed, 112 insertions(+), 56 deletions(-) diff --git a/.github/instructions/codestyle.instructions.md b/.github/instructions/codestyle.instructions.md index 0e5cac87ea..5851398d85 100644 --- a/.github/instructions/codestyle.instructions.md +++ b/.github/instructions/codestyle.instructions.md @@ -566,3 +566,16 @@ Within a class, struct, or interface, elements should be positioned in the follo 11. Records 12. Structs 13. Classes + +### Try Pattern + +When a method name begins with `Try` and returns a variable, ensure it adheres to the Try Pattern. + +Here Key Components of the Try Pattern: +1. Method Name: Begins with Try (e.g., TryParse, TryGetValue). +2. Return Type: bool (indicating success or failure). +3. Out Parameter: An out parameter to return the result if successful. +4. Null Analysis Attribute: [NotNullWhen(true)] (or [MaybeNullWhen(false)]) informs the compiler that if the method returns true, the out parameter is guaranteed to be non-null. + +This pattern allows for high-performance retrieval or parsing without throwing exceptions for expected failures. It also allows cleaner call sites by eliminating the need for null-checking the output variable within the if block, as seen in Dictionary.TryGetValue. +``` diff --git a/src/CommunityToolkit.Maui.SourceGenerators/Helpers/InitializerExpressionResolver.cs b/src/CommunityToolkit.Maui.SourceGenerators/Helpers/InitializerExpressionResolver.cs index 53ecc2a6d7..c6577140cb 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators/Helpers/InitializerExpressionResolver.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators/Helpers/InitializerExpressionResolver.cs @@ -1,3 +1,4 @@ +using System.Diagnostics.CodeAnalysis; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; @@ -14,13 +15,13 @@ static class InitializerExpressionResolver /// Attempts to resolve an initializer expression to a fully qualified C# expression string. /// Returns null if the expression cannot be resolved (e.g. collection expressions, complex lambdas). /// - public static string? TryResolve(ExpressionSyntax expression, SemanticModel semanticModel) + public static bool TryResolve(ExpressionSyntax expression, SemanticModel semanticModel, [NotNullWhen(true)] out string? resolvedExpressionString) { // Try symbolic resolution first, preserves readable identifiers like float.Epsilon - var resolved = TryResolveExpression(expression, semanticModel); - if (resolved is not null) + if (TryResolveExpression(expression, semanticModel, out var resolved)) { - return resolved; + resolvedExpressionString = resolved; + return true; } // Fall back to constant folding, handles nameof(), some compiler-reduced expressions @@ -28,13 +29,15 @@ static class InitializerExpressionResolver if (constantValue.HasValue) { var typeInfo = semanticModel.GetTypeInfo(expression); - return FormatConstantValue(constantValue.Value, typeInfo.Type); + resolvedExpressionString = FormatConstantValue(constantValue.Value, typeInfo.Type); + return true; } - return null; + resolvedExpressionString = null; + return false; } - static string? TryResolveExpression(ExpressionSyntax expression, SemanticModel semanticModel) + static bool TryResolveExpression(ExpressionSyntax expression, SemanticModel semanticModel, [NotNullWhen(true)] out string? resolvedExpressionString) { switch (expression) { @@ -44,55 +47,73 @@ static class InitializerExpressionResolver var defaultLiteralType = semanticModel.GetTypeInfo(literal).Type; if (defaultLiteralType is not null) { - return $"default({defaultLiteralType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)})"; + resolvedExpressionString = $"default({defaultLiteralType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)})"; + return true; } } - return literal.Token.Text; + + resolvedExpressionString = literal.Token.Text; + return true; case PrefixUnaryExpressionSyntax prefixUnary: - var operand = TryResolveExpression(prefixUnary.Operand, semanticModel); - if (operand is null) + if (!TryResolveExpression(prefixUnary.Operand, semanticModel, out var operand)) { - return null; + resolvedExpressionString = null; + return false; } - return $"{prefixUnary.OperatorToken.Text}{operand}"; + + resolvedExpressionString = $"{prefixUnary.OperatorToken.Text}{operand}"; + return true; case MemberAccessExpressionSyntax memberAccess: - return TryResolveMemberAccess(memberAccess, semanticModel); + return TryResolveMemberAccess(memberAccess, semanticModel, out resolvedExpressionString); case IdentifierNameSyntax identifier: - return TryResolveIdentifier(identifier, semanticModel); + return TryResolveIdentifier(identifier, semanticModel, out resolvedExpressionString); case PredefinedTypeSyntax: // Keywords like int, double, string, valid as-is in generated code - return expression.ToString(); + resolvedExpressionString = expression.ToString(); + return true; case CastExpressionSyntax castExpression: - var innerExpr = TryResolveExpression(castExpression.Expression, semanticModel); - if (innerExpr is null) + if (!TryResolveExpression(castExpression.Expression, semanticModel, out var innerExpression)) { - return null; + resolvedExpressionString = null; + return false; } + var castType = semanticModel.GetTypeInfo(castExpression.Type).Type; if (castType is null) { - return null; + resolvedExpressionString = null; + return false; } + var qualifiedCastType = castType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); - return $"({qualifiedCastType}){innerExpr}"; + resolvedExpressionString = $"({qualifiedCastType}){innerExpression}"; + + return true; case ParenthesizedExpressionSyntax paren: - var inner = TryResolveExpression(paren.Expression, semanticModel); - return inner is not null ? $"({inner})" : null; + if (!TryResolveExpression(paren.Expression, semanticModel, out var inner)) + { + resolvedExpressionString = null; + return false; + } + + resolvedExpressionString = $"({inner})"; + return true; case BinaryExpressionSyntax binary: - var left = TryResolveExpression(binary.Left, semanticModel); - var right = TryResolveExpression(binary.Right, semanticModel); - if (left is null || right is null) + if (!TryResolveExpression(binary.Left, semanticModel, out var left) || !TryResolveExpression(binary.Right, semanticModel, out var right)) { - return null; + resolvedExpressionString = null; + return false; } - return $"{left} {binary.OperatorToken.Text} {right}"; + + resolvedExpressionString = $"{left} {binary.OperatorToken.Text} {right}"; + return true; case DefaultExpressionSyntax defaultExpr: if (defaultExpr.Type is not null) @@ -100,38 +121,47 @@ static class InitializerExpressionResolver var defType = semanticModel.GetTypeInfo(defaultExpr.Type).Type; if (defType is not null) { - return $"default({defType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)})"; + resolvedExpressionString = $"default({defType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)})"; + return true; } } - return "default"; + + resolvedExpressionString = "default"; + return true; case InvocationExpressionSyntax: // Invocations (e.g. Guid.NewGuid(), TimeSpan.FromSeconds(5)) may return different // values per call; resolve only if GetConstantValue can fold them (e.g. nameof()). - return null; + resolvedExpressionString = null; + return false; case ObjectCreationExpressionSyntax objectCreation: var objectCreationType = semanticModel.GetTypeInfo(objectCreation).Type; if (objectCreationType is null || !objectCreationType.IsValueType) { - return null; + resolvedExpressionString = null; + return false; } + // Could incorrectly drop the initializer block if present, so early exit in that case // to avoid this. If required can add a traversal to resolve the initializer block, // but probably a rare case so not worth the complexity for now. if (objectCreation.Initializer is not null) { - return null; + resolvedExpressionString = null; + return false; } - return TryResolveObjectCreation(objectCreation, semanticModel); + + return TryResolveObjectCreation(objectCreation, semanticModel, out resolvedExpressionString); default: // Collection expressions, lambdas, etc., cannot resolve - return null; + resolvedExpressionString = null; + return false; } } - static string? TryResolveMemberAccess(MemberAccessExpressionSyntax memberAccess, SemanticModel semanticModel) + static bool TryResolveMemberAccess(MemberAccessExpressionSyntax memberAccess, SemanticModel semanticModel, [NotNullWhen(true)] out string? memberAccessString) { // Only resolve member accesses that are provably safe to evaluate once and share // across all instances (type/namespace navigation, const fields, static readonly fields). @@ -148,38 +178,43 @@ static class InitializerExpressionResolver case IFieldSymbol { IsStatic: true, IsReadOnly: true }: break; // Static readonly fields (e.g. TimeSpan.Zero) default: - return null; // Reject properties, non-readonly fields, methods, etc. + memberAccessString = null; + return false; // Reject properties, non-readonly fields, methods, etc. } - var receiver = TryResolveExpression(memberAccess.Expression, semanticModel); - if (receiver is null) + if (!TryResolveExpression(memberAccess.Expression, semanticModel, out var receiver)) { - return null; + memberAccessString = null; + return false; } - return $"{receiver}.{memberAccess.Name.Identifier.Text}"; + memberAccessString = $"{receiver}.{memberAccess.Name.Identifier.Text}"; + return true; } - static string? TryResolveIdentifier(IdentifierNameSyntax identifier, SemanticModel semanticModel) + static bool TryResolveIdentifier(IdentifierNameSyntax identifier, SemanticModel semanticModel, [NotNullWhen(true)] out string? identifierString) { var symbolInfo = semanticModel.GetSymbolInfo(identifier); var symbol = symbolInfo.Symbol; if (symbol is null) { - return null; + identifierString = null; + return false; } // For type references (e.g. the "ValidationBehaviorDefaults" in "ValidationBehaviorDefaults.Flags") if (symbol is INamedTypeSymbol typeSymbol) { - return typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + identifierString = typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + return true; } // For namespace references (e.g. "System" in "System.TimeSpan.Zero") if (symbol is INamespaceSymbol namespaceSymbol) { - return namespaceSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + identifierString = namespaceSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + return true; } // For const or static readonly field references used as standalone identifiers @@ -188,40 +223,48 @@ static class InitializerExpressionResolver var containingType = symbol.ContainingType; if (containingType is not null) { - return $"{containingType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}.{symbol.Name}"; + identifierString = $"{containingType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)}.{symbol.Name}"; + return true; } } - return null; + identifierString = null; + return false; } - static string? TryResolveObjectCreation(ObjectCreationExpressionSyntax objectCreation, SemanticModel semanticModel) + static bool TryResolveObjectCreation(ObjectCreationExpressionSyntax objectCreation, SemanticModel semanticModel, [NotNullWhen(true)] out string? objectCreationString) { var typeInfo = semanticModel.GetTypeInfo(objectCreation); if (typeInfo.Type is null) { - return null; + objectCreationString = null; + return false; } var qualifiedTypeName = typeInfo.Type.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); if (objectCreation.ArgumentList is null || objectCreation.ArgumentList.Arguments.Count is 0) { - return $"new {qualifiedTypeName}()"; + objectCreationString = $"new {qualifiedTypeName}()"; + return true; } var args = new List(objectCreation.ArgumentList.Arguments.Count); foreach (var arg in objectCreation.ArgumentList.Arguments) { - var resolved = TryResolveExpression(arg.Expression, semanticModel); - if (resolved is null) + if (TryResolveExpression(arg.Expression, semanticModel, out var resolved)) + { + args.Add(resolved); + } + else { - return null; + objectCreationString = null; + return false; } - args.Add(resolved); } - return $"new {qualifiedTypeName}({string.Join(", ", args)})"; + objectCreationString = $"new {qualifiedTypeName}({string.Join(", ", args)})"; + return true; } static string FormatConstantValue(object? value, ITypeSymbol? typeSymbol) @@ -284,4 +327,4 @@ static string EscapeString(string s) => s .Replace("\r", @"\r") .Replace("\t", @"\t") .Replace("\0", @"\0"); -} +} \ No newline at end of file From 24d143524f9a996f559fb8bd881b365890fdcc45 Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+TheCodeTraveler@users.noreply.github.com> Date: Sun, 19 Apr 2026 14:56:24 -0700 Subject: [PATCH 11/14] Update formatting --- .../BindablePropertyAttributeSourceGenerator.cs | 11 +++++++---- .../Models/BindablePropertyRecords.cs | 2 +- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/CommunityToolkit.Maui.SourceGenerators/Generators/BindablePropertyAttributeSourceGenerator.cs b/src/CommunityToolkit.Maui.SourceGenerators/Generators/BindablePropertyAttributeSourceGenerator.cs index 3f241c4552..365c844ed1 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators/Generators/BindablePropertyAttributeSourceGenerator.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators/Generators/BindablePropertyAttributeSourceGenerator.cs @@ -237,7 +237,9 @@ static void GenerateReadOnlyBindableProperty(StringBuilder sb, in BindableProper .Append(GetFormattedReturnType(nonNullableReturnType)) .Append("), typeof(") .Append(info.DeclaringType) - .Append("), ").Append(info.ResolvedInitializerExpression ?? "null").Append(", ") + .Append("), ") + .Append(info.ResolvedInitializerExpression ?? "null") + .Append(", ") .Append(info.DefaultBindingMode) .Append(", ") .Append(info.ValidateValueMethodName) @@ -304,7 +306,9 @@ static void GenerateBindableProperty(StringBuilder sb, in BindablePropertyModel .Append(GetFormattedReturnType(nonNullableReturnType)) .Append("), typeof(") .Append(info.DeclaringType) - .Append("), ").Append(info.ResolvedInitializerExpression ?? "null").Append(", ") + .Append("), ") + .Append(info.ResolvedInitializerExpression ?? "null") + .Append(", ") .Append(info.DefaultBindingMode) .Append(", ") .Append(info.ValidateValueMethodName) @@ -412,8 +416,7 @@ static BindablePropertySemanticValues SemanticTransform(GeneratorAttributeSyntax string? resolvedInitializer = null; if (hasInitializer && propertyDeclarationSyntax.Initializer is not null) { - resolvedInitializer = Helpers.InitializerExpressionResolver.TryResolve( - propertyDeclarationSyntax.Initializer.Value, semanticModel); + InitializerExpressionResolver.TryResolve(propertyDeclarationSyntax.Initializer.Value, semanticModel, out resolvedInitializer); } var attributeData = context.Attributes[0]; diff --git a/src/CommunityToolkit.Maui.SourceGenerators/Models/BindablePropertyRecords.cs b/src/CommunityToolkit.Maui.SourceGenerators/Models/BindablePropertyRecords.cs index 7283e84e80..9ab4540edc 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators/Models/BindablePropertyRecords.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators/Models/BindablePropertyRecords.cs @@ -3,7 +3,7 @@ namespace CommunityToolkit.Maui.SourceGenerators.Models; -public record BindablePropertyModel(string PropertyName, ITypeSymbol ReturnType, ITypeSymbol DeclaringType, string DefaultBindingMode, string ValidateValueMethodName, string PropertyChangedMethodName, string PropertyChangingMethodName, string CoerceValueMethodName, string DefaultValueCreatorMethodName, string NewKeywordText, bool IsReadOnlyBindableProperty, string? SetterAccessibility, bool HasInitializer, string? PropertyAccessibility, string? ResolvedInitializerExpression = null) +public record BindablePropertyModel(string PropertyName, ITypeSymbol ReturnType, ITypeSymbol DeclaringType, string DefaultBindingMode, string ValidateValueMethodName, string PropertyChangedMethodName, string PropertyChangingMethodName, string CoerceValueMethodName, string DefaultValueCreatorMethodName, string NewKeywordText, bool IsReadOnlyBindableProperty, string? SetterAccessibility, bool HasInitializer, string? PropertyAccessibility, string? ResolvedInitializerExpression) { // When both a DefaultValueCreatorMethodName and an initializer are provided, we implement the DefaultValueCreator method and the ignore the partial Property initializer public bool ShouldUsePropertyInitializer => HasInitializer && DefaultValueCreatorMethodName is "null" && ResolvedInitializerExpression is null; From c352c59fb861ff6d74780e0d671610fe4e7fbc98 Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+TheCodeTraveler@users.noreply.github.com> Date: Sun, 19 Apr 2026 14:59:06 -0700 Subject: [PATCH 12/14] Update BindablePropertyModelTests.cs --- .../BindablePropertyModelTests.cs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/CommunityToolkit.Maui.SourceGenerators.UnitTests/BindablePropertyModelTests.cs b/src/CommunityToolkit.Maui.SourceGenerators.UnitTests/BindablePropertyModelTests.cs index 84f7a6655f..a010b99f6d 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators.UnitTests/BindablePropertyModelTests.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators.UnitTests/BindablePropertyModelTests.cs @@ -30,7 +30,8 @@ public void BindablePropertyName_ReturnsCorrectPropertyName() true, // IsReadOnlyBindableProperty string.Empty, // SetterAccessibility false, - "null" + "null", + null ); // Act @@ -74,7 +75,8 @@ public void BindablePropertyModel_WithAllParameters_StoresCorrectValues() true, // IsReadOnlyBindableProperty string.Empty, // SetterAccessibility hasInitializer, - propertyAccessibility + propertyAccessibility, + null ); // Assert @@ -135,7 +137,8 @@ public void SemanticValues_WithClassInfoAndProperties_StoresCorrectValues() true, // IsReadOnlyBindableProperty string.Empty, // SetterAccessibilityText false, - "public" + "public", + null ); var bindableProperties = new[] { bindableProperty }.ToImmutableArray(); @@ -194,7 +197,8 @@ public class TestClass { public string TestProperty { get; set; } = "Initial Val true, string.Empty, hasInitializer, - "public" + "public", + null ); // Act @@ -234,7 +238,8 @@ public class TestClass { public string TestProperty { get; set; } = "Initial Val true, string.Empty, hasInitializer, - "public" + "public", + null ); // Act From 375870b69884a3bc956a67d32b40f1df2d236ceb Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+TheCodeTraveler@users.noreply.github.com> Date: Sun, 19 Apr 2026 15:17:57 -0700 Subject: [PATCH 13/14] Update src/CommunityToolkit.Maui.SourceGenerators/Helpers/InitializerExpressionResolver.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../Helpers/InitializerExpressionResolver.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/CommunityToolkit.Maui.SourceGenerators/Helpers/InitializerExpressionResolver.cs b/src/CommunityToolkit.Maui.SourceGenerators/Helpers/InitializerExpressionResolver.cs index c6577140cb..dc3b5368c0 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators/Helpers/InitializerExpressionResolver.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators/Helpers/InitializerExpressionResolver.cs @@ -182,13 +182,13 @@ static bool TryResolveMemberAccess(MemberAccessExpressionSyntax memberAccess, Se return false; // Reject properties, non-readonly fields, methods, etc. } - if (!TryResolveExpression(memberAccess.Expression, semanticModel, out var receiver)) + if (!TryResolveExpression(memberAccess.Expression, semanticModel, out _)) { memberAccessString = null; return false; } - memberAccessString = $"{receiver}.{memberAccess.Name.Identifier.Text}"; + memberAccessString = symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); return true; } From 483ffba7354cd782a67af096c673e59372954d0e Mon Sep 17 00:00:00 2001 From: Brandon Minnick <13558917+TheCodeTraveler@users.noreply.github.com> Date: Sun, 19 Apr 2026 15:19:07 -0700 Subject: [PATCH 14/14] Revert "Update src/CommunityToolkit.Maui.SourceGenerators/Helpers/InitializerExpressionResolver.cs" This reverts commit 375870b69884a3bc956a67d32b40f1df2d236ceb. --- .../Helpers/InitializerExpressionResolver.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/CommunityToolkit.Maui.SourceGenerators/Helpers/InitializerExpressionResolver.cs b/src/CommunityToolkit.Maui.SourceGenerators/Helpers/InitializerExpressionResolver.cs index dc3b5368c0..c6577140cb 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators/Helpers/InitializerExpressionResolver.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators/Helpers/InitializerExpressionResolver.cs @@ -182,13 +182,13 @@ static bool TryResolveMemberAccess(MemberAccessExpressionSyntax memberAccess, Se return false; // Reject properties, non-readonly fields, methods, etc. } - if (!TryResolveExpression(memberAccess.Expression, semanticModel, out _)) + if (!TryResolveExpression(memberAccess.Expression, semanticModel, out var receiver)) { memberAccessString = null; return false; } - memberAccessString = symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + memberAccessString = $"{receiver}.{memberAccess.Name.Identifier.Text}"; return true; }