diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/AttachedBindablePropertySourceGeneratorTests/BaseAttachedBindablePropertyAttributeSourceGeneratorTest.cs b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/AttachedBindablePropertySourceGeneratorTests/BaseAttachedBindablePropertyAttributeSourceGeneratorTest.cs new file mode 100644 index 0000000000..ea789b5b3a --- /dev/null +++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/AttachedBindablePropertySourceGeneratorTests/BaseAttachedBindablePropertyAttributeSourceGeneratorTest.cs @@ -0,0 +1,95 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Testing; +using System.IO; +using System.Collections.Generic; +using Xunit; + +namespace CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests.AttachedBindablePropertySourceGeneratorTests; + +public class BaseAttachedBindablePropertyAttributeSourceGeneratorTest : CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests.BaseTest +{ + protected const string defaultTestClassName = "TestView"; + protected const string defaultTestNamespace = "TestNamespace"; + + protected const string expectedAttribute = + /* language=C#-test */ + //lang=csharp + $$""" + // + // See: CommunityToolkit.Maui.SourceGenerators.Internal.AttachedBindablePropertySourceGenerator + + #pragma warning disable + #nullable enable + namespace CommunityToolkit.Maui; + + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + [global::System.AttributeUsage(global::System.AttributeTargets.Property, AllowMultiple = false, Inherited = false)] + [global::System.Diagnostics.CodeAnalysis.Experimental("{{AttachedBindablePropertySourceGenerator.BindablePropertyAttributeExperimentalDiagnosticId}}")] + sealed partial class AttachedBindablePropertyAttribute : global::System.Attribute + { + public string? PropertyName { get; } + public global::System.Type? DeclaringType { get; set; } + public global::Microsoft.Maui.Controls.BindingMode DefaultBindingMode { get; set; } + public string ValidateValueMethodName { get; set; } = string.Empty; + public string PropertyChangedMethodName { get; set; } = string.Empty; + public string PropertyChangingMethodName { get; set; } = string.Empty; + public string CoerceValueMethodName { get; set; } = string.Empty; + public string DefaultValueCreatorMethodName { get; set; } = string.Empty; + } + """; + + protected static async Task VerifyAttachedSourceGeneratorAsync(string source, string expectedGenerated) + { + const string sourceGeneratorNamespace = "CommunityToolkit.Maui.SourceGenerators.Internal"; + const string attachedBindablePropertyAttributeGeneratedFileName = "AttachedBindablePropertyAttribute.g.cs"; + var attachedSourceGeneratorFullName = typeof(AttachedBindablePropertySourceGenerator).FullName ?? throw new InvalidOperationException("Source Generator Type Path cannot be null"); + + var test = new AttachedBindablePropertyTest + { +#if NET10_0 + ReferenceAssemblies = Microsoft.CodeAnalysis.Testing.ReferenceAssemblies.Net.Net100, +#else +#error ReferenceAssemblies must be updated to current version of .NET +#endif + TestState = + { + Sources = { source }, + + AdditionalReferences = + { + MetadataReference.CreateFromFile(typeof(Microsoft.Maui.Controls.BindableObject).Assembly.Location), + MetadataReference.CreateFromFile(typeof(Microsoft.Maui.Controls.BindableProperty).Assembly.Location), + MetadataReference.CreateFromFile(typeof(Microsoft.Maui.Controls.BindingMode).Assembly.Location) + } + } + }; + + var expectedAttributeText = Microsoft.CodeAnalysis.Text.SourceText.From(expectedAttribute, System.Text.Encoding.UTF8); + // Use the same prefix the test harness uses so the expected generated file path matches actual + var attributeFilePath = Path.Combine(sourceGeneratorNamespace, attachedSourceGeneratorFullName, attachedBindablePropertyAttributeGeneratedFileName); + test.TestState.GeneratedSources.Add((attributeFilePath, expectedAttributeText)); + + if (!string.IsNullOrEmpty(expectedGenerated)) + { + var expectedGeneratedText = Microsoft.CodeAnalysis.Text.SourceText.From(expectedGenerated, System.Text.Encoding.UTF8); + var generatedFilePath = Path.Combine(sourceGeneratorNamespace, attachedSourceGeneratorFullName, $"{defaultTestClassName}.g.cs"); + test.TestState.GeneratedSources.Add((generatedFilePath, expectedGeneratedText)); + } + + await test.RunAsync(TestContext.Current.CancellationToken); + } + + sealed class AttachedBindablePropertyTest : CSharpSourceGeneratorTest + { + protected override CompilationOptions CreateCompilationOptions() + { + var compilationOptions = base.CreateCompilationOptions(); + + return compilationOptions.WithSpecificDiagnosticOptions(new Dictionary + { + { AttachedBindablePropertySourceGenerator.BindablePropertyAttributeExperimentalDiagnosticId, ReportDiagnostic.Warn } + }); + } + } +} diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/AttachedBindablePropertySourceGeneratorTests/CommonUsageTests.cs b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/AttachedBindablePropertySourceGeneratorTests/CommonUsageTests.cs new file mode 100644 index 0000000000..bf8dbdcd91 --- /dev/null +++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/AttachedBindablePropertySourceGeneratorTests/CommonUsageTests.cs @@ -0,0 +1,202 @@ +using System.Threading.Tasks; +using Xunit; + +namespace CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests.AttachedBindablePropertySourceGeneratorTests; + +public class CommonUsageTests : BaseAttachedBindablePropertyAttributeSourceGeneratorTest +{ + [Fact] + public async Task GenerateAttachedBindableProperty_SimpleExample_GeneratesCorrectCode() + { + const string source = + /* language=C#-test */ + //lang=csharp + $$""" + using CommunityToolkit.Maui; + using Microsoft.Maui.Controls; + + namespace {{defaultTestNamespace}}; + + public partial class {{defaultTestClassName}} : View + { + [AttachedBindableProperty] + public partial string Text { get; set; } + } + """; + + const string expectedGenerated = + /* language=C#-test */ + //lang=csharp + $$""" + // + // See: CommunityToolkit.Maui.SourceGenerators.Internal.AttachedBindablePropertySourceGenerator + #pragma warning disable + #nullable enable + using global::Microsoft.Maui.Controls; + + namespace {{defaultTestNamespace}}; + public partial class {{defaultTestClassName}} + { + /// + /// Backing BindableProperty for the property. + /// + public static readonly global::Microsoft.Maui.Controls.BindableProperty TextProperty = global::Microsoft.Maui.Controls.BindableProperty.CreateAttached("Text", typeof(string), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); + public partial string Text { get => (string)GetValue(TextProperty); set => SetValue(TextProperty, value); } + + public static string GetText(BindableObject view) => (string)view.GetValue(TextProperty); + public static void SetText(BindableObject view, string value) => view.SetValue(TextProperty, value); + } + """; + + await VerifyAttachedSourceGeneratorAsync(source, expectedGenerated); + } + + [Fact] + public async Task GenerateAttachedBindableProperty_WithNewKeyword_GeneratesCorrectCode() + { + const string source = + /* language=C#-test */ + //lang=csharp + $$""" + using CommunityToolkit.Maui; + using Microsoft.Maui.Controls; + + namespace {{defaultTestNamespace}}; + + public partial class {{defaultTestClassName}} : View + { + [AttachedBindableProperty] + public new partial string Text { get; set; } + } + """; + + const string expectedGenerated = + /* language=C#-test */ + //lang=csharp + $$""" + // + // See: CommunityToolkit.Maui.SourceGenerators.Internal.AttachedBindablePropertySourceGenerator + #pragma warning disable + #nullable enable + using global::Microsoft.Maui.Controls; + + namespace {{defaultTestNamespace}}; + public partial class {{defaultTestClassName}} + { + /// + /// Backing BindableProperty for the property. + /// + public new static readonly global::Microsoft.Maui.Controls.BindableProperty TextProperty = global::Microsoft.Maui.Controls.BindableProperty.CreateAttached("Text", typeof(string), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); + public new partial string Text { get => (string)GetValue(TextProperty); set => SetValue(TextProperty, value); } + + public static string GetText(BindableObject view) => (string)view.GetValue(TextProperty); + public static void SetText(BindableObject view, string value) => view.SetValue(TextProperty, value); + } + """; + + await VerifyAttachedSourceGeneratorAsync(source, expectedGenerated); + } + + [Fact] + public async Task GenerateAttachedBindableProperty_NullableReferenceType_GeneratesCorrectCode() + { + const string source = + /* language=C#-test */ + //lang=csharp + $$""" + using CommunityToolkit.Maui; + using Microsoft.Maui.Controls; + + namespace {{defaultTestNamespace}}; + + public partial class {{defaultTestClassName}} : View + { + [AttachedBindableProperty] + public partial string? Text { get; set; } + } + """; + + const string expectedGenerated = + /* language=C#-test */ + //lang=csharp + $$""" + // + // See: CommunityToolkit.Maui.SourceGenerators.Internal.AttachedBindablePropertySourceGenerator + #pragma warning disable + #nullable enable + using global::Microsoft.Maui.Controls; + + namespace {{defaultTestNamespace}}; + public partial class {{defaultTestClassName}} + { + /// + /// Backing BindableProperty for the property. + /// + public static readonly global::Microsoft.Maui.Controls.BindableProperty TextProperty = global::Microsoft.Maui.Controls.BindableProperty.CreateAttached("Text", typeof(string), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); + public partial string? Text { get => (string)GetValue(TextProperty); set => SetValue(TextProperty, value); } + + public static string? GetText(BindableObject view) => (string)view.GetValue(TextProperty); + public static void SetText(BindableObject view, string? value) => view.SetValue(TextProperty, value); + } + """; + + await VerifyAttachedSourceGeneratorAsync(source, expectedGenerated); + } + + [Fact] + public async Task GenerateAttachedBindableProperty_MultipleProperties_GeneratesCorrectCode() + { + const string source = + /* language=C#-test */ + //lang=csharp + $$""" + using CommunityToolkit.Maui; + using Microsoft.Maui.Controls; + + namespace {{defaultTestNamespace}}; + + public partial class {{defaultTestClassName}} : View + { + [AttachedBindableProperty] + public partial string Text { get; set; } + + [AttachedBindableProperty] + public partial int Number { get; set; } + } + """; + + const string expectedGenerated = + /* language=C#-test */ + //lang=csharp + $$""" + // + // See: CommunityToolkit.Maui.SourceGenerators.Internal.AttachedBindablePropertySourceGenerator + #pragma warning disable + #nullable enable + using global::Microsoft.Maui.Controls; + + namespace {{defaultTestNamespace}}; + public partial class {{defaultTestClassName}} + { + /// + /// Backing BindableProperty for the property. + /// + public static readonly global::Microsoft.Maui.Controls.BindableProperty TextProperty = global::Microsoft.Maui.Controls.BindableProperty.CreateAttached("Text", typeof(string), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); + public partial string Text { get => (string)GetValue(TextProperty); set => SetValue(TextProperty, value); } + + public static string GetText(BindableObject view) => (string)view.GetValue(TextProperty); + public static void SetText(BindableObject view, string value) => view.SetValue(TextProperty, value); + /// + /// Backing BindableProperty for the property. + /// + public static readonly global::Microsoft.Maui.Controls.BindableProperty NumberProperty = global::Microsoft.Maui.Controls.BindableProperty.CreateAttached("Number", typeof(int), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); + public partial int Number { get => (int)GetValue(NumberProperty); set => SetValue(NumberProperty, value); } + + public static int GetNumber(BindableObject view) => (int)view.GetValue(NumberProperty); + public static void SetNumber(BindableObject view, int value) => view.SetValue(NumberProperty, value); + } + """; + + await VerifyAttachedSourceGeneratorAsync(source, expectedGenerated); + } +} \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/AttachedBindablePropertySourceGeneratorTests/EdgeCaseTests.cs b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/AttachedBindablePropertySourceGeneratorTests/EdgeCaseTests.cs new file mode 100644 index 0000000000..96a6e969ec --- /dev/null +++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/AttachedBindablePropertySourceGeneratorTests/EdgeCaseTests.cs @@ -0,0 +1,148 @@ +using Xunit; + +namespace CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests.AttachedBindablePropertySourceGeneratorTests; + +public class EdgeCaseTests : BaseAttachedBindablePropertyAttributeSourceGeneratorTest +{ + [Fact] + public async Task GenerateAttachedBindableProperty_PropertyIsByteEnum_GeneratesCorrectCode() + { + const string source = + /* language=C#-test */ + //lang=csharp + $$""" + using CommunityToolkit.Maui; + using Microsoft.Maui.Controls; + + namespace {{defaultTestNamespace}}; + + public partial class {{defaultTestClassName}} : View + { + [AttachedBindableProperty] + public partial Status InvoiceStatus { get; set; } = Status.Approved; + } + + public enum Status : byte + { + Pending = 0, + Approved = 1, + Rejected = 2 + } + """; + + const string expectedGenerated = + /* language=C#-test */ + //lang=csharp + $$""" + // + // See: CommunityToolkit.Maui.SourceGenerators.Internal.AttachedBindablePropertySourceGenerator + #pragma warning disable + #nullable enable + using global::Microsoft.Maui.Controls; + + namespace {{defaultTestNamespace}}; + public partial class {{defaultTestClassName}} + { + /// + /// Backing BindableProperty for the property. + /// + public static readonly global::Microsoft.Maui.Controls.BindableProperty InvoiceStatusProperty = global::Microsoft.Maui.Controls.BindableProperty.CreateAttached("InvoiceStatus", typeof(TestNamespace.Status), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, Microsoft.Maui.Controls.BindingMode.OneWay, 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); } + + public static TestNamespace.Status GetInvoiceStatus(BindableObject view) => (TestNamespace.Status)view.GetValue(InvoiceStatusProperty); + public static void SetInvoiceStatus(BindableObject view, TestNamespace.Status value) => view.SetValue(InvoiceStatusProperty, value); + } + + file static class __{{defaultTestClassName}}BindablePropertyInitHelpers + { + public static bool IsInitializingInvoiceStatus = false; + public static object CreateDefaultInvoiceStatus(BindableObject bindable) + { + IsInitializingInvoiceStatus = true; + object defaultValue; + if (bindable is TestView inst) + { + defaultValue = ((TestView)bindable).InvoiceStatus; + } + else + { + defaultValue = default(TestNamespace.Status); + } + + IsInitializingInvoiceStatus = false; + return defaultValue; + } + } + """; + + await VerifyAttachedSourceGeneratorAsync(source, expectedGenerated); + } + + [Fact] + public async Task GenerateAttachedBindableProperty_ArrayTypes_GeneratesCorrectCode() + { + const string source = + /* language=C#-test */ + //lang=csharp + $$""" + using CommunityToolkit.Maui; + using Microsoft.Maui.Controls; + + namespace {{defaultTestNamespace}}; + + public partial class {{defaultTestClassName}} : View + { + [AttachedBindableProperty] + public partial string[] StringArray { get; set; } + + [AttachedBindableProperty] + public partial int[,] MultiDimensionalArray { get; set; } + + [AttachedBindableProperty] + public partial byte[][] JaggedArray { get; set; } + } + """; + + const string expectedGenerated = + /* language=C#-test */ + //lang=csharp + $$""" + // + // See: CommunityToolkit.Maui.SourceGenerators.Internal.AttachedBindablePropertySourceGenerator + #pragma warning disable + #nullable enable + using global::Microsoft.Maui.Controls; + + namespace {{defaultTestNamespace}}; + public partial class {{defaultTestClassName}} + { + /// + /// Backing BindableProperty for the property. + /// + public static readonly global::Microsoft.Maui.Controls.BindableProperty StringArrayProperty = global::Microsoft.Maui.Controls.BindableProperty.CreateAttached("StringArray", typeof(string[]), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); + public partial string[] StringArray { get => (string[])GetValue(StringArrayProperty); set => SetValue(StringArrayProperty, value); } + + public static string[] GetStringArray(BindableObject view) => (string[])view.GetValue(StringArrayProperty); + public static void SetStringArray(BindableObject view, string[] value) => view.SetValue(StringArrayProperty, value); + /// + /// Backing BindableProperty for the property. + /// + public static readonly global::Microsoft.Maui.Controls.BindableProperty MultiDimensionalArrayProperty = global::Microsoft.Maui.Controls.BindableProperty.CreateAttached("MultiDimensionalArray", typeof(int[, ]), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); + public partial int[, ] MultiDimensionalArray { get => (int[, ])GetValue(MultiDimensionalArrayProperty); set => SetValue(MultiDimensionalArrayProperty, value); } + + public static int[, ] GetMultiDimensionalArray(BindableObject view) => (int[, ])view.GetValue(MultiDimensionalArrayProperty); + public static void SetMultiDimensionalArray(BindableObject view, int[, ] value) => view.SetValue(MultiDimensionalArrayProperty, value); + /// + /// Backing BindableProperty for the property. + /// + public static readonly global::Microsoft.Maui.Controls.BindableProperty JaggedArrayProperty = global::Microsoft.Maui.Controls.BindableProperty.CreateAttached("JaggedArray", typeof(byte[][]), typeof({{defaultTestNamespace}}.{{defaultTestClassName}}), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); + public partial byte[][] JaggedArray { get => (byte[][])GetValue(JaggedArrayProperty); set => SetValue(JaggedArrayProperty, value); } + + public static byte[][] GetJaggedArray(BindableObject view) => (byte[][])view.GetValue(JaggedArrayProperty); + public static void SetJaggedArray(BindableObject view, byte[][] value) => view.SetValue(JaggedArrayProperty, value); + } + """; + + await VerifyAttachedSourceGeneratorAsync(source, expectedGenerated); + } +} diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/AttachedBindablePropertySourceGeneratorTests/IntegrationTests.cs b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/AttachedBindablePropertySourceGeneratorTests/IntegrationTests.cs new file mode 100644 index 0000000000..01a4ce611b --- /dev/null +++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/AttachedBindablePropertySourceGeneratorTests/IntegrationTests.cs @@ -0,0 +1,55 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Testing; +using Xunit; + +namespace CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests.AttachedBindablePropertySourceGeneratorTests; + +public class IntegrationTests : BaseAttachedBindablePropertyAttributeSourceGeneratorTest +{ + [Fact] + public async Task GenerateAttachedBindableProperty_Basic_GeneratesCorrectCode() + { + const string source = + /* language=C#-test */ + //lang=csharp + $$""" + using CommunityToolkit.Maui; + using Microsoft.Maui.Controls; + + namespace TestNamespace; + + public partial class TestView : View + { + [AttachedBindableProperty] + public partial string Text { get; set; } + } + """; + + const string expectedGenerated = + /* language=C#-test */ + //lang=csharp + $$""" + // + // See: CommunityToolkit.Maui.SourceGenerators.Internal.AttachedBindablePropertySourceGenerator + #pragma warning disable + #nullable enable + using global::Microsoft.Maui.Controls; + + namespace TestNamespace; + public partial class TestView + { + /// + /// Backing BindableProperty for the property. + /// + public static readonly global::Microsoft.Maui.Controls.BindableProperty TextProperty = global::Microsoft.Maui.Controls.BindableProperty.CreateAttached("Text", typeof(string), typeof(TestNamespace.TestView), null, Microsoft.Maui.Controls.BindingMode.OneWay, null, null, null, null, null); + public partial string Text { get => (string)GetValue(TextProperty); set => SetValue(TextProperty, value); } + + public static string GetText(BindableObject view) => (string)view.GetValue(TextProperty); + public static void SetText(BindableObject view, string value) => view.SetValue(TextProperty, value); + } + """; + + await VerifyAttachedSourceGeneratorAsync(source, expectedGenerated); + } +} diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BaseTest.cs b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BaseTest.cs index a0eb7fc4b1..efc1b01aa0 100644 --- a/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BaseTest.cs +++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal.UnitTests/BaseTest.cs @@ -11,7 +11,9 @@ protected static async Task VerifySourceGeneratorAsync(string source, string exp { const string sourceGeneratorNamespace = "CommunityToolkit.Maui.SourceGenerators.Internal"; const string bindablePropertyAttributeGeneratedFileName = "BindablePropertyAttribute.g.cs"; - var sourceGeneratorFullName = typeof(BindablePropertyAttributeSourceGenerator).FullName ?? throw new InvalidOperationException("Source Generator Type Path cannot be null"); + const string attachedBindablePropertyAttributeGeneratedFileName = "AttachedBindablePropertyAttribute.g.cs"; + var bindableSourceGeneratorFullName = typeof(BindablePropertyAttributeSourceGenerator).FullName ?? throw new InvalidOperationException("Source Generator Type Path cannot be null"); + var attachedSourceGeneratorFullName = typeof(AttachedBindablePropertySourceGenerator).FullName ?? throw new InvalidOperationException("Source Generator Type Path cannot be null"); var test = new ExperimentalBindablePropertyTest { @@ -34,13 +36,16 @@ protected static async Task VerifySourceGeneratorAsync(string source, string exp }; var expectedAttributeText = Microsoft.CodeAnalysis.Text.SourceText.From(expectedAttribute, System.Text.Encoding.UTF8); - var bindablePropertyAttributeFilePath = Path.Combine(sourceGeneratorNamespace, sourceGeneratorFullName, bindablePropertyAttributeGeneratedFileName); + var bindablePropertyAttributeFilePath = Path.Combine(sourceGeneratorNamespace, bindableSourceGeneratorFullName, bindablePropertyAttributeGeneratedFileName); test.TestState.GeneratedSources.Add((bindablePropertyAttributeFilePath, expectedAttributeText)); foreach (var generatedFile in expectedGenerated.Where(static x => !string.IsNullOrEmpty(x.GeneratedFile))) { var expectedGeneratedText = Microsoft.CodeAnalysis.Text.SourceText.From(generatedFile.GeneratedFile, System.Text.Encoding.UTF8); - var generatedFilePath = Path.Combine(sourceGeneratorNamespace, sourceGeneratorFullName, generatedFile.FileName); + var generatorFullName = generatedFile.FileName == attachedBindablePropertyAttributeGeneratedFileName + ? attachedSourceGeneratorFullName + : bindableSourceGeneratorFullName; + var generatedFilePath = Path.Combine(sourceGeneratorNamespace, generatorFullName, generatedFile.FileName); test.TestState.GeneratedSources.Add((generatedFilePath, expectedGeneratedText)); } diff --git a/src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/AttachedBindablePropertySourceGenerator.cs b/src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/AttachedBindablePropertySourceGenerator.cs new file mode 100644 index 0000000000..4e37bd5625 --- /dev/null +++ b/src/CommunityToolkit.Maui.SourceGenerators.Internal/Generators/AttachedBindablePropertySourceGenerator.cs @@ -0,0 +1,723 @@ +using System.Collections.Immutable; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Text; +using CommunityToolkit.Maui.SourceGenerators.Helpers; +using CommunityToolkit.Maui.SourceGenerators.Internal.Helpers; +using CommunityToolkit.Maui.SourceGenerators.Internal.Models; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; + +namespace CommunityToolkit.Maui.SourceGenerators.Internal; + +[Generator] +public class AttachedBindablePropertySourceGenerator : IIncrementalGenerator +{ + public const string BindablePropertyAttributeExperimentalDiagnosticId = "MCTEXP001"; + + static readonly SemanticValues emptySemanticValues = new(default, []); + + + // Use the actual BindableProperty type: CreateAttached/CreateReadOnlyAttached are static members on BindableProperty + const string bpFullName = "global::Microsoft.Maui.Controls.BindableProperty"; + // Keep the Generated attribute text identical to test expectations to avoid formatting diffs + const string attachedPropertyAttribute = + /* language=C#-test */ + //lang=csharp + $$""" + // + // See: CommunityToolkit.Maui.SourceGenerators.Internal.AttachedBindablePropertySourceGenerator + + #pragma warning disable + #nullable enable + namespace CommunityToolkit.Maui; + + [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + [global::System.AttributeUsage(global::System.AttributeTargets.Property, AllowMultiple = false, Inherited = false)] + [global::System.Diagnostics.CodeAnalysis.Experimental("{{BindablePropertyAttributeExperimentalDiagnosticId}}")] + sealed partial class AttachedBindablePropertyAttribute : global::System.Attribute + { + public string? PropertyName { get; } + public global::System.Type? DeclaringType { get; set; } + public global::Microsoft.Maui.Controls.BindingMode DefaultBindingMode { get; set; } + public string ValidateValueMethodName { get; set; } = string.Empty; + public string PropertyChangedMethodName { get; set; } = string.Empty; + public string PropertyChangingMethodName { get; set; } = string.Empty; + public string CoerceValueMethodName { get; set; } = string.Empty; + public string DefaultValueCreatorMethodName { get; set; } = string.Empty; + } + """; + public void Initialize(IncrementalGeneratorInitializationContext context) + { +#if DEBUG + + if (!Debugger.IsAttached) + { + // To debug this SG, uncomment the line below and rebuild the SourceGenerator project. + + //Debugger.Launch(); + } +#endif + + context.RegisterPostInitializationOutput(static ctx => ctx.AddSource("AttachedBindablePropertyAttribute.g.cs", SourceText.From(attachedPropertyAttribute, Encoding.UTF8))); + + var provider = context.SyntaxProvider.ForAttributeWithMetadataName("CommunityToolkit.Maui.AttachedBindablePropertyAttribute", + IsNonEmptyPropertyDeclarationSyntax, SemanticTransform) + .Where(static x => x.ClassInformation != default || !x.BindableProperties.IsEmpty) + .Collect(); + + context.RegisterSourceOutput(provider, ExecuteAllValues); + } + + static void ExecuteAllValues(SourceProductionContext context, ImmutableArray semanticValues) + { + // Pre-allocate dictionary with expected capacity + var groupedValues = new Dictionary<(string, string, string, string), List>(semanticValues.Length); + + // Single-pass grouping without LINQ + foreach (var sv in semanticValues) + { + var key = (sv.ClassInformation.ClassName, sv.ClassInformation.ContainingNamespace, sv.ClassInformation.ContainingTypes, sv.ClassInformation.GenericTypeParameters); + + if (!groupedValues.TryGetValue(key, out var list)) + { + list = []; + groupedValues[key] = list; + } + list.Add(sv); + } + + // Use ArrayPool for temporary storage + var bindablePropertiesBuffer = System.Buffers.ArrayPool.Shared.Rent(32); + + try + { + foreach (var keyValuePair in groupedValues) + { + var (className, containingNamespace, containingTypes, genericTypeParameters) = keyValuePair.Key; + var values = keyValuePair.Value; + + if (values.Count is 0 || string.IsNullOrEmpty(className) || string.IsNullOrEmpty(containingNamespace)) + { + continue; + } + + // Flatten bindable properties without SelectMany allocation + var bindablePropertiesCount = 0; + foreach (var value in values) + { + foreach (var bp in value.BindableProperties) + { + if (bindablePropertiesCount >= bindablePropertiesBuffer.Length) + { + var newBuffer = System.Buffers.ArrayPool.Shared.Rent(bindablePropertiesBuffer.Length * 2); + Array.Copy(bindablePropertiesBuffer, newBuffer, bindablePropertiesBuffer.Length); + System.Buffers.ArrayPool.Shared.Return(bindablePropertiesBuffer); + bindablePropertiesBuffer = newBuffer; + } + bindablePropertiesBuffer[bindablePropertiesCount++] = bp; + } + } + + var bindableProperties = ImmutableArray.Create(bindablePropertiesBuffer, 0, bindablePropertiesCount); + + var classAccessibility = values[0].ClassInformation.DeclaredAccessibility; + + var combinedClassInfo = new ClassInformation(className, classAccessibility, containingNamespace, containingTypes, genericTypeParameters); + var combinedValues = new SemanticValues(combinedClassInfo, bindableProperties); + + var fileNameSuffix = string.IsNullOrEmpty(containingTypes) ? className : string.Concat(containingTypes, ".", className); + var source = GenerateSource(combinedValues); + SourceStringService.FormatText(ref source); + context.AddSource($"{fileNameSuffix}.g.cs", SourceText.From(source, Encoding.UTF8)); + } + } + finally + { + System.Buffers.ArrayPool.Shared.Return(bindablePropertiesBuffer); + } + } + + static string GenerateSource(SemanticValues value) + { + // Pre-calculate StringBuilder capacity to avoid resizing + var estimatedCapacity = 500 + (value.BindableProperties.Count() * 400); + var sb = new StringBuilder(estimatedCapacity); + + // Use string concatenation for simple cases + sb.Append("// \n// See: CommunityToolkit.Maui.SourceGenerators.Internal.AttachedBindablePropertySourceGenerator\n\n#pragma warning disable\n#nullable enable\n\n"); + + // Add using for BindableObject so generated Get/Set accessors can use the unqualified type name. + sb.Append("using global::Microsoft.Maui.Controls;\n\n"); + + if (!IsGlobalNamespace(value.ClassInformation)) + { + sb.Append("namespace ").Append(value.ClassInformation.ContainingNamespace).Append(";\n\n"); + + } + + // Generate nested class hierarchy + if (!string.IsNullOrEmpty(value.ClassInformation.ContainingTypes)) + { + var containingTypeNames = value.ClassInformation.ContainingTypes.Split('.'); + foreach (var typeName in containingTypeNames) + { + sb.Append(value.ClassInformation.DeclaredAccessibility).Append(" partial class ").Append(typeName).Append("\n{\n\n"); + } + } + + // Get the class name with generic parameters + var classNameWithGenerics = value.ClassInformation.ClassName; + if (!string.IsNullOrEmpty(value.ClassInformation.GenericTypeParameters)) + { + classNameWithGenerics = string.Concat(value.ClassInformation.ClassName, "<", value.ClassInformation.GenericTypeParameters, ">"); + } + + sb.Append(value.ClassInformation.DeclaredAccessibility).Append(" partial class ").Append(classNameWithGenerics).Append("\n{\n\n"); + + // Prepare helper builder for file-static class members (static flags + default creators) + var fileStaticClassStringBuilder = new StringBuilder(256); + var helperNames = new HashSet(); + + // Build fully-qualified declaring type name for helper method casts (include containing types) + var fullDeclaringType = string.IsNullOrEmpty(value.ClassInformation.ContainingTypes) + ? classNameWithGenerics + : string.Concat(value.ClassInformation.ContainingTypes, ".", classNameWithGenerics); + + var fileStaticClassName = $"__{classNameWithGenerics}BindablePropertyInitHelpers"; + + foreach (var info in value.BindableProperties) + { + if (info.IsReadOnlyBindableProperty) + { + GenerateReadOnlyAttachedBindableProperty(sb, in info, fileStaticClassName); + } + else + { + GenerateAttachedBindableProperty(sb, in info, fileStaticClassName); + } + if (info.ShouldUsePropertyInitializer) + { + // Generate only references within the class; actual static field and creator method + // will be placed inside the file static helper class below. + if (helperNames.Add(info.InitializingPropertyName)) + { + AppendHelperInitializingField(fileStaticClassStringBuilder, in info); + } + if (helperNames.Add(info.EffectiveDefaultValueCreatorMethodName)) + { + AppendHelperDefaultValueMethod(fileStaticClassStringBuilder, in info, fullDeclaringType); + } + } + + GenerateProperty(sb, in info, fileStaticClassName); + + // After generating the property, generate the attached accessors in the expected compact form + GenerateAttachedAccessors(sb, in info); + } + + sb.Append('}'); + + // Close nested class hierarchy + if (!string.IsNullOrEmpty(value.ClassInformation.ContainingTypes)) + { + var containingTypeNames = value.ClassInformation.ContainingTypes.Split('.'); + for (int i = 0; i < containingTypeNames.Length; i++) + { + sb.Append("\n}"); + } + } + + // If we generated any helper members, emit a file static class with them. + if (fileStaticClassStringBuilder.Length > 0) + { + sb.Append("\n\nfile static class ").Append(fileStaticClassName).Append("\n{\n"); + sb.Append(fileStaticClassStringBuilder.ToString()); + sb.Append("}\n"); + } + + return sb.ToString(); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static void GenerateReadOnlyAttachedBindableProperty(StringBuilder sb, in BindablePropertyModel info, in string fileStaticClassName) + { + // Sanitize the Return Type because Nullable Reference Types cannot be used in the `typeof()` operator + var nonNullableReturnType = ConvertToNonNullableTypeSymbol(info.ReturnType); + var sanitizedPropertyName = IsDotnetKeyword(info.PropertyName) ? string.Concat("@", info.PropertyName) : info.PropertyName; + + sb.Append("/// \r\n/// Backing BindableProperty for the property.\r\n/// \r\n"); + + // Generate BindablePropertyKey for read-only properties + sb.Append(info.PropertyAccessibility) + .Append("static readonly global::Microsoft.Maui.Controls.BindablePropertyKey ") + .Append(info.BindablePropertyKeyName) + .Append(" = \n") + .Append(bpFullName) + .Append(".CreateReadOnly(") + .Append("\"") + .Append(sanitizedPropertyName) + .Append("\", typeof(") + .Append(GetFormattedReturnType(nonNullableReturnType)) + .Append("), typeof(") + .Append(info.DeclaringType) + .Append("), null, ") + .Append(info.DefaultBindingMode) + .Append(", ") + .Append(info.ValidateValueMethodName) + .Append(", ") + .Append(info.PropertyChangedMethodName) + .Append(", ") + .Append(info.PropertyChangingMethodName) + .Append(", ") + .Append(info.CoerceValueMethodName) + .Append(", "); + + if (info.ShouldUsePropertyInitializer) + { + sb.Append(fileStaticClassName) + .Append('.'); + } + + sb.Append(info.EffectiveDefaultValueCreatorMethodName) + .Append(");\n\n"); + + // Generate BindableProperty from the key with the same accessibility as the property + sb.Append(info.PropertyAccessibility) + .Append(info.NewKeywordText) + .Append("static readonly ") + .Append(bpFullName) + .Append(' ') + .Append(info.BindablePropertyName) + .Append(" = ") + .Append(info.BindablePropertyKeyName) + .Append(".BindableProperty;\n"); + + sb.Append('\n'); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static void GenerateAttachedBindableProperty(StringBuilder sb, in BindablePropertyModel info, in string helperClassName) + { + // Sanitize the Return Type because Nullable Reference Types cannot be used in the `typeof()` operator + var nonNullableReturnType = ConvertToNonNullableTypeSymbol(info.ReturnType); + var sanitizedPropertyName = IsDotnetKeyword(info.PropertyName) ? string.Concat("@", info.PropertyName) : info.PropertyName; + + sb.Append("/// \r\n/// Backing BindableProperty for the property.\r\n/// \r\n"); + + // Generate regular BindableProperty (field only) with the requested accessibility + sb.Append(info.PropertyAccessibility) + .Append(info.NewKeywordText) + .Append("static readonly ") + .Append(bpFullName) + .Append(' ') + .Append(info.BindablePropertyName) + .Append(" = \n") + .Append(bpFullName) + .Append(".CreateAttached(\"") + .Append(sanitizedPropertyName) + .Append("\", typeof(") + .Append(GetFormattedReturnType(nonNullableReturnType)) + .Append("), typeof(") + .Append(info.DeclaringType) + .Append("), null, ") + .Append(info.DefaultBindingMode) + .Append(", ") + .Append(info.ValidateValueMethodName) + .Append(", ") + .Append(info.PropertyChangedMethodName) + .Append(", ") + .Append(info.PropertyChangingMethodName) + .Append(", ") + .Append(info.CoerceValueMethodName) + .Append(", "); + + if (info.ShouldUsePropertyInitializer) + { + sb.Append(helperClassName) + .Append('.'); + } + + sb.Append(info.EffectiveDefaultValueCreatorMethodName) + .Append(");\n") + .Append('\n'); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static void GenerateProperty(StringBuilder sb, in BindablePropertyModel info, in string fileStaticClassName) + { + var sanitizedPropertyName = IsDotnetKeyword(info.PropertyName) ? string.Concat("@", info.PropertyName) : info.PropertyName; + var formattedReturnType = GetFormattedReturnType(info.ReturnType); + var nonNullableReturnType = ConvertToNonNullableTypeSymbol(info.ReturnType); + var formattedNonNullableReturnType = GetFormattedReturnType(nonNullableReturnType).Trim(); + + sb.Append(info.PropertyAccessibility) + .Append(info.NewKeywordText) + .Append("partial ") + .Append(formattedReturnType) + .Append(' ') + .Append(sanitizedPropertyName) + .Append("\n{\nget => "); + + if (info.HasInitializer) + { + if (info.ShouldUsePropertyInitializer) + { + // Now reference the static flag on the file static helper class + sb.Append(fileStaticClassName).Append('.').Append(info.InitializingPropertyName); + } + else + { + sb.Append("false"); + } + + sb.Append(" ? field : "); + } + + sb.Append("(") + .Append(formattedNonNullableReturnType) + .Append(")GetValue(") + .Append(info.BindablePropertyName) + .Append(");\n"); + + if (info.SetterAccessibility is not null) + { + if (string.IsNullOrEmpty(info.SetterAccessibility)) + { + sb.Append("set => SetValue(") + .Append(info.IsReadOnlyBindableProperty ? info.BindablePropertyKeyName : info.BindablePropertyName) + .Append(", value);\n"); + } + else + { + sb.Append(info.SetterAccessibility) + .Append("set => SetValue(") + .Append(info.IsReadOnlyBindableProperty ? info.BindablePropertyKeyName : info.BindablePropertyName) + .Append(", value);\n"); + } + } + + sb.Append("}\n"); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static void GenerateAttachedAccessors(StringBuilder sb, in BindablePropertyModel info) + { + var sanitizedPropertyName = IsDotnetKeyword(info.PropertyName) ? string.Concat("@", info.PropertyName) : info.PropertyName; + var formattedReturnType = GetFormattedReturnType(info.ReturnType); + var nonNullableReturnType = ConvertToNonNullableTypeSymbol(info.ReturnType); + var formattedNonNullableReturnType = GetFormattedReturnType(nonNullableReturnType).Trim(); + + // Append a blank line to separate property and accessors + sb.Append('\n'); + + // Expression-bodied Get (cast uses non-nullable type to match baseline expectations) + sb.Append(info.PropertyAccessibility).Append("static ").Append(formattedReturnType).Append(" Get").Append(sanitizedPropertyName) + .Append("(BindableObject view) => (").Append(formattedNonNullableReturnType).Append(")view.GetValue(").Append(info.BindablePropertyName).Append(");\n"); + + // Expression-bodied Set (parameter type uses full formattedReturnType, cast not required) + sb.Append(info.PropertyAccessibility).Append("static void Set").Append(sanitizedPropertyName) + .Append("(BindableObject view, ").Append(formattedReturnType).Append(" value) => view.SetValue(").Append(info.BindablePropertyName).Append(", value);\n"); + } + + static SemanticValues SemanticTransform(GeneratorAttributeSyntaxContext context, CancellationToken cancellationToken) + { + var propertyDeclarationSyntax = Unsafe.As(context.TargetNode); + var semanticModel = context.SemanticModel; + var propertySymbol = (IPropertySymbol?)ModelExtensions.GetDeclaredSymbol(semanticModel, propertyDeclarationSyntax, cancellationToken); + var hasInitializer = propertyDeclarationSyntax.Initializer is not null; + + if (propertySymbol is null) + { + return emptySemanticValues; + } + + var @namespace = propertySymbol.ContainingNamespace.ToDisplayString(); + var className = propertySymbol.ContainingType.Name; + var classAccessibility = propertySymbol.ContainingSymbol.DeclaredAccessibility.ToString().ToLower(); + var returnType = propertySymbol.Type; + + // Build containing types hierarchy + var containingTypes = GetContainingTypes(propertySymbol.ContainingType); + + // Extract generic type parameters + var genericTypeParameters = GetGenericTypeParameters(propertySymbol.ContainingType); + + var propertyInfo = new ClassInformation(className, classAccessibility, @namespace, containingTypes, genericTypeParameters); + + // Use array instead of List to avoid resizing + var bindablePropertyModels = new BindablePropertyModel[context.Attributes.Length]; + + var doesContainNewKeyword = HasNewKeyword(propertyDeclarationSyntax); + var (isReadOnlyBindableProperty, setterAccessibility, propertyAccessibility) = GetPropertyAccessibility(propertySymbol, propertyDeclarationSyntax); + + // Determine how to emit the setter accessibility: + // - null => no setter (get-only) + // - empty string => emit setter without explicit accessibility modifier (matching source that had none) + // - non-empty string => emit that explicit modifier (e.g., "internal ") + string? setterAccessibilityText; + if (propertyDeclarationSyntax.AccessorList is not null) + { + var setAccessor = propertyDeclarationSyntax.AccessorList.Accessors.FirstOrDefault(a => a.Kind() == SyntaxKind.SetAccessorDeclaration); + if (setAccessor is null) + { + setterAccessibilityText = null; + } + else if (setAccessor.Modifiers.Count == 0) + { + setterAccessibilityText = string.Empty; // emit setter but no explicit modifier + } + else + { + setterAccessibilityText = setterAccessibility; + } + } + else + { + setterAccessibilityText = setterAccessibility; + } + + var attributeData = context.Attributes[0]; + bindablePropertyModels[0] = CreateBindablePropertyModel(attributeData, propertySymbol.ContainingType, propertySymbol.Name, returnType, doesContainNewKeyword, isReadOnlyBindableProperty, setterAccessibilityText, hasInitializer, propertyAccessibility); + + return new(propertyInfo, ImmutableArray.Create(bindablePropertyModels)); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static bool HasNewKeyword(PropertyDeclarationSyntax syntax) + { + foreach (var modifier in syntax.Modifiers) + { + if (modifier.IsKind(SyntaxKind.NewKeyword)) + { + return true; + } + } + return false; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static (bool IsReadOnlyBindableProperty, string? SetterAccessibility, string PropertyAccessibility) GetPropertyAccessibility(IPropertySymbol propertySymbol, PropertyDeclarationSyntax syntax) + { + // Determine declared accessibility for the property itself + var propertyAccessibility = propertySymbol.DeclaredAccessibility switch + { + Accessibility.NotApplicable => throw new NotSupportedException($"The property accessibility for {propertySymbol.Name} is not yet supported"), + Accessibility.Private => "private ", + Accessibility.ProtectedAndInternal => "private protected ", + Accessibility.Protected => "protected ", + Accessibility.Internal => "internal ", + Accessibility.ProtectedOrInternal => "protected internal ", + Accessibility.Public => "public ", + _ => throw new NotSupportedException($"The property accessibility for {propertySymbol.Name} is not yet supported"), + }; + + // Check if property is get-only (no setter) + if (propertySymbol.SetMethod is null) + { + return (true, null, propertyAccessibility); + } + + return propertySymbol.SetMethod.DeclaredAccessibility switch + { + Accessibility.NotApplicable => throw new NotSupportedException($"The setter type for {propertySymbol.Name} is not yet supported"), + Accessibility.Private => (true, "private ", propertyAccessibility), + Accessibility.ProtectedAndInternal => (true, "private protected ", propertyAccessibility), + Accessibility.Protected => (true, "protected ", propertyAccessibility), + Accessibility.Internal => (false, "internal ", propertyAccessibility), + Accessibility.ProtectedOrInternal => (false, "protected internal ", propertyAccessibility), + Accessibility.Public => (false, " ", propertyAccessibility), // Keep the SetterAccessibility empty because the Property is public and the setter will inherit that accessibility modified, e.g. `public string Test { get; set; }` + _ => throw new NotSupportedException($"The setter type for {propertySymbol.Name} is not yet supported"), + }; + } + + static string GetContainingTypes(INamedTypeSymbol typeSymbol) + { + var current = typeSymbol.ContainingType; + if (current is null) + { + return string.Empty; + } + + var sb = new StringBuilder(100); + var stack = new Stack(4); + + while (current is not null) + { + stack.Push(current.Name); + current = current.ContainingType; + } + + var first = true; + while (stack.Count > 0) + { + if (!first) + { + sb.Append('.'); + } + sb.Append(stack.Pop()); + first = false; + } + + return sb.ToString(); + } + + static string GetGenericTypeParameters(INamedTypeSymbol typeSymbol) + { + if (!typeSymbol.IsGenericType || typeSymbol.TypeParameters.IsEmpty) + { + return string.Empty; + } + + var typeParams = typeSymbol.TypeParameters; + if (typeParams.Length == 1) + { + return typeParams[0].Name; + } + + var sb = new StringBuilder(typeParams.Length * 10); + for (int i = 0; i < typeParams.Length; i++) + { + if (i > 0) + { + sb.Append(", "); + } + sb.Append(typeParams[i].Name); + } + + 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) + { + if (attributeData.AttributeClass is null) + { + throw new ArgumentException($"{nameof(attributeData)}.{nameof(attributeData.AttributeClass)} Cannot Be Null", nameof(attributeData)); + } + + var coerceValueMethodName = attributeData.GetNamedMethodGroupArgumentsAttributeValueByNameAsString(nameof(BindablePropertyModel.CoerceValueMethodName)); + var defaultBindingMode = attributeData.GetNamedTypeArgumentsAttributeValueForDefaultBindingMode(nameof(BindablePropertyModel.DefaultBindingMode), "Microsoft.Maui.Controls.BindingMode.OneWay"); + var defaultValueCreatorMethodName = attributeData.GetNamedMethodGroupArgumentsAttributeValueByNameAsString(nameof(BindablePropertyModel.DefaultValueCreatorMethodName)); + var propertyChangedMethodName = attributeData.GetNamedMethodGroupArgumentsAttributeValueByNameAsString(nameof(BindablePropertyModel.PropertyChangedMethodName)); + var propertyChangingMethodName = attributeData.GetNamedMethodGroupArgumentsAttributeValueByNameAsString(nameof(BindablePropertyModel.PropertyChangingMethodName)); + 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); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static ITypeSymbol ConvertToNonNullableTypeSymbol(in ITypeSymbol typeSymbol) + { + // Check for Nullable + if (typeSymbol is INamedTypeSymbol { IsGenericType: true, ConstructedFrom.SpecialType: SpecialType.System_Nullable_T }) + { + return typeSymbol; + } + + // Check for Nullable Reference Type + if (typeSymbol.NullableAnnotation is NullableAnnotation.Annotated) + { + // For reference types, NullableAnnotation.None indicates non-nullable. + return typeSymbol.WithNullableAnnotation(NullableAnnotation.None); + } + + return typeSymbol; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static bool IsNonEmptyPropertyDeclarationSyntax(SyntaxNode node, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + return node is PropertyDeclarationSyntax { AttributeLists.Count: > 0 }; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static bool IsDotnetKeyword(in string name) => SyntaxFacts.GetKeywordKind(name) is not SyntaxKind.None; + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + static bool IsGlobalNamespace(in ClassInformation classInformation) + { + return classInformation.ContainingNamespace is ""; + } + + static string GetFormattedReturnType(ITypeSymbol typeSymbol) + { + if (typeSymbol is IArrayTypeSymbol arrayTypeSymbol) + { + // Get the element type name (e.g., "int") + string elementType = GetFormattedReturnType(arrayTypeSymbol.ElementType); + + // Construct the correct rank syntax with commas (e.g., "[,]") + var rank = arrayTypeSymbol.Rank > 1 ? new string(',', arrayTypeSymbol.Rank - 1) : string.Empty; + + return string.Concat(elementType, "[", rank, "]"); + } + else + { + // Use ToDisplayString with the correct format for the base type (e.g., "int") + return typeSymbol.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat); + } + } + + /// + /// Appends the initializing flag into the file-static helper class. + /// + /// Helper StringBuilder used to collect helper members. + /// Property model. + static void AppendHelperInitializingField(StringBuilder fileStaticClassStringBuilder, in BindablePropertyModel info) + { + // Make the flag public static so it can be referenced from the generated partial class in the same file. + fileStaticClassStringBuilder.Append("public static bool ") + .Append(info.InitializingPropertyName) + .Append(" = false;\n"); + } + + /// + /// Appends a default value creator method into the file-static helper class. + /// The method sets the static initializing flag, reads the property's initializer value by casting the bindable + /// to the declaring type, then clears the flag and returns the value. + /// For attached properties the bindable may not be an instance of the declaring type (e.g. docking property on children), + /// so we fall back to returning the default(...) for the property's type when the cast fails. + /// + /// Helper StringBuilder used to collect helper members. + /// Property model. + /// Declaring type including containing types and generic parameters. + static void AppendHelperDefaultValueMethod(StringBuilder fileStaticClassStringBuilder, in BindablePropertyModel info, string fullDeclaringType) + { + var sanitizedPropertyName = IsDotnetKeyword(info.PropertyName) ? string.Concat("@", info.PropertyName) : info.PropertyName; + var formattedReturnType = GetFormattedReturnType(info.ReturnType); + + fileStaticClassStringBuilder.Append("public static object ") + .Append(info.EffectiveDefaultValueCreatorMethodName) + .Append("(BindableObject bindable)\n") + .Append("{\n") + .Append(info.InitializingPropertyName) + .Append(" = true;\n") + .Append("object defaultValue;\n") + .Append("if (bindable is ") + .Append(fullDeclaringType) + .Append(" inst)\n{\n") + .Append("defaultValue = ((") + .Append(fullDeclaringType) + .Append(")bindable).") + .Append(sanitizedPropertyName) + .Append(";\n") + .Append("}\nelse\n{\n") + .Append("defaultValue = default(") + .Append(formattedReturnType) + .Append(");\n") + .Append("}\n") + .Append(info.InitializingPropertyName) + .Append(" = false;\n") + .Append("return defaultValue;\n") + .Append("}\n\n"); + } +}