From 2e994715e80e525159a7a8904a629ff37d93e703 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 7 Dec 2025 18:29:23 -0800 Subject: [PATCH 01/46] Add GeneratedCustomPropertyProviderAttribute class Introduces the GeneratedCustomPropertyProviderAttribute for marking bindable properties in XAML scenarios. Supports specifying property names and indexer types for binding code generation via ICustomPropertyProvider interfaces. --- .../GeneratedCustomPropertyProvider.cs | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 src/WinRT.Runtime2/Xaml.Attributes/GeneratedCustomPropertyProvider.cs diff --git a/src/WinRT.Runtime2/Xaml.Attributes/GeneratedCustomPropertyProvider.cs b/src/WinRT.Runtime2/Xaml.Attributes/GeneratedCustomPropertyProvider.cs new file mode 100644 index 000000000..1ac293ad4 --- /dev/null +++ b/src/WinRT.Runtime2/Xaml.Attributes/GeneratedCustomPropertyProvider.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace WindowsRuntime.Xaml; + +/// +/// An attribute used to indicate the properties which are bindable, for XAML (WinUI) scenarios. +/// +/// +/// This attribute will cause binding code to be generated to provide support via the Windows.UI.Xaml.Data.ICustomPropertyProvider +/// and Microsoft.UI.Xaml.Data.ICustomPropertyProvider infrastructure, for the specified properties on the annotated type. +/// +/// +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, Inherited = false, AllowMultiple = false)] +public sealed class GeneratedCustomPropertyProviderAttribute : Attribute +{ + /// + /// Creates a new instance. + /// + /// + /// Using this constructor will mark all public properties as bindable. + /// + public GeneratedCustomPropertyProviderAttribute() + { + } + + /// + /// Creates a new instance with the specified parameters. + /// + /// The name of the non-indexer public properties to mark as bindable. + /// The parameter type of the indexer public properties to mark as bindable. + public GeneratedCustomPropertyProviderAttribute(string[] propertyNames, Type[] indexerPropertyTypes) + { + PropertyNames = propertyNames; + IndexerPropertyTypes = indexerPropertyTypes; + } + + /// + /// Gets the name of the non-indexer public properties to mark as bindable. + /// + /// + /// If , all public properties are considered bindable. + /// + public string[]? PropertyNames { get; } + + /// + /// Gets the parameter type of the indexer public properties to mark as bindable. + /// + /// + /// If , all indexer public properties are considered bindable. + /// + public Type[]? IndexerPropertyTypes { get; } +} \ No newline at end of file From b1b375915b0459c75e3aa3d7288f86e4673d0706 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 7 Dec 2025 18:29:40 -0800 Subject: [PATCH 02/46] Rename IBindableIReadOnlyListAdapter to BindableIReadOnlyListAdapter Renamed the file for consistency with naming conventions. No code changes were made. --- ...bleIReadOnlyListAdapter.cs => BindableIReadOnlyListAdapter.cs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/WinRT.Runtime2/ABI/WindowsRuntime.InteropServices/Bindables/{IBindableIReadOnlyListAdapter.cs => BindableIReadOnlyListAdapter.cs} (100%) diff --git a/src/WinRT.Runtime2/ABI/WindowsRuntime.InteropServices/Bindables/IBindableIReadOnlyListAdapter.cs b/src/WinRT.Runtime2/ABI/WindowsRuntime.InteropServices/Bindables/BindableIReadOnlyListAdapter.cs similarity index 100% rename from src/WinRT.Runtime2/ABI/WindowsRuntime.InteropServices/Bindables/IBindableIReadOnlyListAdapter.cs rename to src/WinRT.Runtime2/ABI/WindowsRuntime.InteropServices/Bindables/BindableIReadOnlyListAdapter.cs From 976f8fcecfde12e25e099ee75e45f8896911e36c Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 7 Dec 2025 18:30:40 -0800 Subject: [PATCH 03/46] Update marshaller attribute for BindableIReadOnlyListAdapter Changed BindableIReadOnlyListAdapterComWrappersMarshallerAttribute from public to internal and removed obsolete/editor-browsable attributes. Applied the marshaller attribute to BindableIReadOnlyListAdapter to improve type registration and usage. --- .../Bindables/BindableIReadOnlyListAdapter.cs | 6 +----- .../Bindables/BindableIReadOnlyListAdapter.cs | 1 + 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/WinRT.Runtime2/ABI/WindowsRuntime.InteropServices/Bindables/BindableIReadOnlyListAdapter.cs b/src/WinRT.Runtime2/ABI/WindowsRuntime.InteropServices/Bindables/BindableIReadOnlyListAdapter.cs index f7cfe892a..b05abe7fb 100644 --- a/src/WinRT.Runtime2/ABI/WindowsRuntime.InteropServices/Bindables/BindableIReadOnlyListAdapter.cs +++ b/src/WinRT.Runtime2/ABI/WindowsRuntime.InteropServices/Bindables/BindableIReadOnlyListAdapter.cs @@ -87,11 +87,7 @@ static BindableIReadOnlyListAdapterInterfaceEntriesImpl() /// /// A custom implementation for . /// -[Obsolete(WindowsRuntimeConstants.PrivateImplementationDetailObsoleteMessage, - DiagnosticId = WindowsRuntimeConstants.PrivateImplementationDetailObsoleteDiagnosticId, - UrlFormat = WindowsRuntimeConstants.CsWinRTDiagnosticsUrlFormat)] -[EditorBrowsable(EditorBrowsableState.Never)] -public sealed unsafe class BindableIReadOnlyListAdapterComWrappersMarshallerAttribute : WindowsRuntimeComWrappersMarshallerAttribute +internal sealed unsafe class BindableIReadOnlyListAdapterComWrappersMarshallerAttribute : WindowsRuntimeComWrappersMarshallerAttribute { /// public override void* GetOrCreateComInterfaceForObject(object value) diff --git a/src/WinRT.Runtime2/InteropServices/Bindables/BindableIReadOnlyListAdapter.cs b/src/WinRT.Runtime2/InteropServices/Bindables/BindableIReadOnlyListAdapter.cs index 23b2862b0..eba5a8f97 100644 --- a/src/WinRT.Runtime2/InteropServices/Bindables/BindableIReadOnlyListAdapter.cs +++ b/src/WinRT.Runtime2/InteropServices/Bindables/BindableIReadOnlyListAdapter.cs @@ -15,6 +15,7 @@ namespace WindowsRuntime.InteropServices; /// still uses "IReadOnlyList" in its name to match the naming convention of adapter types matching .NET type names. /// [WindowsRuntimeManagedOnlyType] +[ABI.WindowsRuntime.InteropServices.BindableIReadOnlyListAdapterComWrappersMarshaller] [Obsolete(WindowsRuntimeConstants.PrivateImplementationDetailObsoleteMessage, DiagnosticId = WindowsRuntimeConstants.PrivateImplementationDetailObsoleteDiagnosticId, UrlFormat = WindowsRuntimeConstants.CsWinRTDiagnosticsUrlFormat)] From 47314a20fae92728d178ae1be0f0ea68405e1a29 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 7 Dec 2025 21:51:27 -0800 Subject: [PATCH 04/46] Remove IIncrementalGenerator implementation The TypeMapAssemblyTargetGenerator class no longer implements the IIncrementalGenerator interface. This may be in preparation for refactoring or changing the generator's integration with Roslyn. --- .../TypeMapAssemblyTargetGenerator.Execute.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Authoring/WinRT.SourceGenerator2/TypeMapAssemblyTargetGenerator.Execute.cs b/src/Authoring/WinRT.SourceGenerator2/TypeMapAssemblyTargetGenerator.Execute.cs index aba7fa179..eee46f15b 100644 --- a/src/Authoring/WinRT.SourceGenerator2/TypeMapAssemblyTargetGenerator.Execute.cs +++ b/src/Authoring/WinRT.SourceGenerator2/TypeMapAssemblyTargetGenerator.Execute.cs @@ -11,7 +11,7 @@ namespace WindowsRuntime.SourceGenerator; /// -public partial class TypeMapAssemblyTargetGenerator : IIncrementalGenerator +public partial class TypeMapAssemblyTargetGenerator { /// /// Generation methods for . From 659a4761044eac1594f56dd0998a7611a7496a93 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 7 Dec 2025 21:53:54 -0800 Subject: [PATCH 05/46] Add SyntaxExtensions with helper methods for Roslyn Introduces a new SyntaxExtensions class providing extension methods for SyntaxNode and SyntaxTokenList. These methods allow checking if a node or token list matches any of the specified SyntaxKind values, improving code readability and reuse in Roslyn-based source generators. --- .../Extensions/SyntaxExtensions.cs | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 src/Authoring/WinRT.SourceGenerator2/Extensions/SyntaxExtensions.cs diff --git a/src/Authoring/WinRT.SourceGenerator2/Extensions/SyntaxExtensions.cs b/src/Authoring/WinRT.SourceGenerator2/Extensions/SyntaxExtensions.cs new file mode 100644 index 000000000..4af55419b --- /dev/null +++ b/src/Authoring/WinRT.SourceGenerator2/Extensions/SyntaxExtensions.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace WindowsRuntime.SourceGenerator; + +/// +/// Extensions for syntax types. +/// +internal static class SyntaxExtensions +{ + extension(SyntaxNode node) + { + /// + /// Determines if is of any of the specified kinds. + /// + /// The syntax kinds to test for. + /// Whether the input node is of any of the specified kinds. + public bool IsAnyKind(params ReadOnlySpan kinds) + { + foreach (SyntaxKind kind in kinds) + { + if (node.IsKind(kind)) + { + return true; + } + } + + return false; + } + } + + extension(SyntaxTokenList list) + { + /// + /// Tests whether a list contains any token of particular kinds. + /// + /// The syntax kinds to test for. + /// Whether the input list contains any of the specified kinds. + public bool ContainsAny(params ReadOnlySpan kinds) + { + foreach (SyntaxKind kind in kinds) + { + if (list.IndexOf(kind) >= 0) + { + return true; + } + } + + return false; + } + } +} \ No newline at end of file From a7622b45e94970e54ce1d4ef73d26c4c2e3c90b3 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 7 Dec 2025 21:57:31 -0800 Subject: [PATCH 06/46] Add extension for attribute analysis with config options Introduces IncrementalGeneratorInitializationContextExtensions with a method to combine attribute-based syntax analysis and analyzer config options. Also adds a struct to encapsulate both GeneratorAttributeSyntaxContext and AnalyzerConfigOptions for use in source generators. --- ...eneratorInitializationContextExtensions.cs | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 src/Authoring/WinRT.SourceGenerator2/Extensions/IncrementalGeneratorInitializationContextExtensions.cs diff --git a/src/Authoring/WinRT.SourceGenerator2/Extensions/IncrementalGeneratorInitializationContextExtensions.cs b/src/Authoring/WinRT.SourceGenerator2/Extensions/IncrementalGeneratorInitializationContextExtensions.cs new file mode 100644 index 000000000..d3ed23f4e --- /dev/null +++ b/src/Authoring/WinRT.SourceGenerator2/Extensions/IncrementalGeneratorInitializationContextExtensions.cs @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Immutable; +using System.Threading; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace WindowsRuntime.SourceGenerator; + +/// +/// Extension methods for . +/// +internal static class IncrementalGeneratorInitializationContextExtensions +{ + /// + public static IncrementalValuesProvider ForAttributeWithMetadataNameAndOptions( + this IncrementalGeneratorInitializationContext context, + string fullyQualifiedMetadataName, + Func predicate, + Func transform) + { + // Invoke 'ForAttributeWithMetadataName' normally, but just return the context directly + IncrementalValuesProvider syntaxContext = context.SyntaxProvider.ForAttributeWithMetadataName( + fullyQualifiedMetadataName, + predicate, + static (context, token) => context); + + // Do the same for the analyzer config options + IncrementalValueProvider configOptions = context.AnalyzerConfigOptionsProvider.Select(static (provider, token) => provider.GlobalOptions); + + // Merge the two and invoke the provided transform on these two values. Neither value + // is equatable, meaning the pipeline will always re-run until this point. This is + // intentional: we don't want any symbols or other expensive objects to be kept alive + // across incremental steps, especially if they could cause entire compilations to be + // rooted, which would significantly increase memory use and introduce more GC pauses. + // In this specific case, flowing non equatable values in a pipeline is therefore fine. + return syntaxContext.Combine(configOptions).Select((input, token) => transform(new GeneratorAttributeSyntaxContextWithOptions(input.Left, input.Right), token)); + } +} + +/// +/// +/// +/// The original value. +/// The original value. +internal readonly struct GeneratorAttributeSyntaxContextWithOptions( + GeneratorAttributeSyntaxContext syntaxContext, + AnalyzerConfigOptions globalOptions) +{ + /// + public SyntaxNode TargetNode { get; } = syntaxContext.TargetNode; + + /// + public ISymbol TargetSymbol { get; } = syntaxContext.TargetSymbol; + + /// + public SemanticModel SemanticModel { get; } = syntaxContext.SemanticModel; + + /// + public ImmutableArray Attributes { get; } = syntaxContext.Attributes; + + /// + public AnalyzerConfigOptions GlobalOptions { get; } = globalOptions; +} From ac97abd0c0ed8354f70614b4e1cfed3e6cdef88d Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 8 Dec 2025 10:35:59 -0800 Subject: [PATCH 07/46] Add MemberDeclarationSyntaxExtensions for partial checks Introduces extension methods to determine if a member declaration is partial and if it resides within a hierarchy of partial type declarations. This aids in analyzing and generating code for partial types in the source generator. --- .../MemberDeclarationSyntaxExtensions.cs | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 src/Authoring/WinRT.SourceGenerator2/Extensions/MemberDeclarationSyntaxExtensions.cs diff --git a/src/Authoring/WinRT.SourceGenerator2/Extensions/MemberDeclarationSyntaxExtensions.cs b/src/Authoring/WinRT.SourceGenerator2/Extensions/MemberDeclarationSyntaxExtensions.cs new file mode 100644 index 000000000..cc510ce9d --- /dev/null +++ b/src/Authoring/WinRT.SourceGenerator2/Extensions/MemberDeclarationSyntaxExtensions.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace WindowsRuntime.SourceGenerator; + +/// +/// Extensions for member declaration syntax types. +/// +internal static class MemberDeclarationSyntaxExtensions +{ + extension(MemberDeclarationSyntax node) + { + /// + /// Gets whether the input member declaration is partial. + /// + public bool IsPartial => node.Modifiers.Any(SyntaxKind.PartialKeyword); + + /// + /// Gets whether the input member declaration is partial and + /// all of its parent type declarations are also partial. + /// + public bool IsPartialAndWithinPartialTypeHierarchy + { + get + { + // If the target node is not partial, stop immediately + if (!node.IsPartial) + { + return false; + } + + // Walk all parent type declarations, stop if any of them is not partial + foreach (SyntaxNode ancestor in node.Ancestors()) + { + if (ancestor is BaseTypeDeclarationSyntax { IsPartial: false }) + { + return false; + } + } + + return true; + } + } + } +} \ No newline at end of file From f4fd8ab817b930bd5397f7b1aee2e54527af00d0 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 8 Dec 2025 10:44:39 -0800 Subject: [PATCH 08/46] WIP --- ...CustomPropertyProviderGenerator.Execute.cs | 53 +++++++++++++++++++ .../CustomPropertyProviderGenerator.cs | 33 ++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Execute.cs create mode 100644 src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.cs diff --git a/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Execute.cs b/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Execute.cs new file mode 100644 index 000000000..4876819ed --- /dev/null +++ b/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Execute.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +#pragma warning disable IDE0046 + +namespace WindowsRuntime.SourceGenerator; + +/// +public partial class CustomPropertyProviderGenerator +{ + /// + /// Generation methods for . + /// + private static class Execute + { + /// + /// Checks whether a target node needs the ICustomPropertyProvider implementation. + /// + /// The target instance to check. + /// The cancellation token for the operation. + /// Whether is a valid target for the ICustomPropertyProvider implementation. + [SuppressMessage("Style", "IDE0060", Justification = "The cancellation token is supplied by Roslyn.")] + public static bool IsTargetNodeValid(SyntaxNode node, CancellationToken token) + { + // We only care about class and struct types, all other types are not valid targets + if (!node.IsAnyKind(SyntaxKind.ClassDeclaration, SyntaxKind.RecordDeclaration, SyntaxKind.StructDeclaration, SyntaxKind.RecordStructDeclaration)) + { + return false; + } + + // If the type is static or abstract, we cannot implement 'ICustomPropertyProvider' on it + if (((MemberDeclarationSyntax)node).Modifiers.ContainsAny(SyntaxKind.StaticKeyword, SyntaxKind.AbstractKeyword)) + { + return false; + } + + // We can only generated the 'ICustomPropertyProvider' implementation if the type is 'partial'. + // Additionally, all parent type declarations must also be 'partial', for generation to work. + if (!((MemberDeclarationSyntax)node).IsPartialAndWithinPartialTypeHierarchy) + { + return false; + } + + return true; + } + } +} \ No newline at end of file diff --git a/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.cs b/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.cs new file mode 100644 index 000000000..9126e1e18 --- /dev/null +++ b/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.cs @@ -0,0 +1,33 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Immutable; +using System.Linq; +using System.Threading; +using Microsoft.CodeAnalysis; + +namespace WindowsRuntime.SourceGenerator; + +/// +/// A generator to emit ICustomPropertyProvider implementations for annotated types. +/// +[Generator] +public sealed partial class CustomPropertyProviderGenerator : IIncrementalGenerator +{ + /// + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var bindableCustomPropertyAttributes = context.ForAttributeWithMetadataNameAndOptions( + fullyQualifiedMetadataName: "WinRT.GeneratedBindableCustomPropertyAttribute", + predicate: Execute.IsTargetNodeValid, + transform: static (n, _) => n) + .Combine(properties) + .Select(static ((GeneratorAttributeSyntaxContext generatorSyntaxContext, CsWinRTAotOptimizerProperties properties) value, CancellationToken _) => + value.properties.IsCsWinRTAotOptimizerEnabled ? GetBindableCustomProperties(value.generatorSyntaxContext) : default) + .Where(static bindableCustomProperties => bindableCustomProperties != default) + .Collect() + .Combine(properties); + context.RegisterImplementationSourceOutput(bindableCustomPropertyAttributes, GenerateBindableCustomProperties); + } +} \ No newline at end of file From cecbaa6ee745d3de740d602bc2c6d330bcb6b0ae Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 8 Dec 2025 15:11:37 -0800 Subject: [PATCH 09/46] Add generic object pool implementation Introduces ObjectPool to efficiently manage reusable objects with a fixed pool size. This class is ported from Roslyn and provides thread-safe allocation and freeing of objects to reduce allocations and improve performance. --- .../Helpers/ObjectPool{T}.cs | 154 ++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 src/Authoring/WinRT.SourceGenerator2/Helpers/ObjectPool{T}.cs diff --git a/src/Authoring/WinRT.SourceGenerator2/Helpers/ObjectPool{T}.cs b/src/Authoring/WinRT.SourceGenerator2/Helpers/ObjectPool{T}.cs new file mode 100644 index 000000000..04f60a089 --- /dev/null +++ b/src/Authoring/WinRT.SourceGenerator2/Helpers/ObjectPool{T}.cs @@ -0,0 +1,154 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Ported from Roslyn. +// See: https://github.com/dotnet/roslyn/blob/main/src/Dependencies/PooledObjects/ObjectPool%601.cs. + +using System; +using System.Runtime.CompilerServices; +using System.Threading; + +#pragma warning disable RS1035 + +namespace WindowsRuntime.SourceGenerator; + +/// +/// +/// Generic implementation of object pooling pattern with predefined pool size limit. The main purpose +/// is that limited number of frequently used objects can be kept in the pool for further recycling. +/// +/// +/// Notes: +/// +/// +/// It is not the goal to keep all returned objects. Pool is not meant for storage. If there +/// is no space in the pool, extra returned objects will be dropped. +/// +/// +/// It is implied that if object was obtained from a pool, the caller will return it back in +/// a relatively short time. Keeping checked out objects for long durations is ok, but +/// reduces usefulness of pooling. Just new up your own. +/// +/// +/// +/// +/// Not returning objects to the pool in not detrimental to the pool's work, but is a bad practice. +/// Rationale: if there is no intent for reusing the object, do not use pool - just use "new". +/// +/// +/// The type of objects to pool. +/// The input factory to produce items. +/// +/// The factory is stored for the lifetime of the pool. We will call this only when pool needs to +/// expand. compared to "new T()", Func gives more flexibility to implementers and faster than "new T()". +/// +/// The pool size to use. +internal sealed class ObjectPool(Func factory, int size) + where T : class +{ + /// + /// The array of cached items. + /// + private readonly Element[] _items = new Element[size - 1]; + + /// + /// Storage for the pool objects. The first item is stored in a dedicated field + /// because we expect to be able to satisfy most requests from it. + /// + private T? _firstItem; + + /// + /// Creates a new instance with the specified parameters. + /// + /// The input factory to produce items. + public ObjectPool(Func factory) + : this(factory, Environment.ProcessorCount * 2) + { + } + + /// + /// Produces a instance. + /// + /// The returned item to use. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public T Allocate() + { + T? item = _firstItem; + + if (item is null || item != Interlocked.CompareExchange(ref _firstItem, null, item)) + { + item = AllocateSlow(); + } + + return item; + } + + /// + /// Returns a given instance to the pool. + /// + /// The instance to return. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Free(T obj) + { + if (_firstItem is null) + { + _firstItem = obj; + } + else + { + FreeSlow(obj); + } + } + + /// + /// Allocates a new item. + /// + /// The returned item to use. + [MethodImpl(MethodImplOptions.NoInlining)] + private T AllocateSlow() + { + foreach (ref Element element in _items.AsSpan()) + { + T? instance = element.Value; + + if (instance is not null) + { + if (instance == Interlocked.CompareExchange(ref element.Value, null, instance)) + { + return instance; + } + } + } + + return factory(); + } + + /// + /// Frees a given item. + /// + /// The item to return to the pool. + [MethodImpl(MethodImplOptions.NoInlining)] + private void FreeSlow(T obj) + { + foreach (ref Element element in _items.AsSpan()) + { + if (element.Value is null) + { + element.Value = obj; + + break; + } + } + } + + /// + /// A container for a produced item (using a wrapper to avoid covariance checks). + /// + private struct Element + { + /// + /// The value held at the current element. + /// + internal T? Value; + } +} \ No newline at end of file From 9204686e806d16287e17f767d8884ae72e6d0dbe Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 8 Dec 2025 15:17:30 -0800 Subject: [PATCH 10/46] Add PooledArrayBuilder helper for pooled arrays Introduces PooledArrayBuilder, a ref struct for efficiently building sequences of values using pooled buffers. This utility provides methods for adding, inserting, and enumerating items, and is ported from ComputeSharp to support high-performance source generation scenarios. --- .../Helpers/PooledArrayBuilder{T}.cs | 357 ++++++++++++++++++ 1 file changed, 357 insertions(+) create mode 100644 src/Authoring/WinRT.SourceGenerator2/Helpers/PooledArrayBuilder{T}.cs diff --git a/src/Authoring/WinRT.SourceGenerator2/Helpers/PooledArrayBuilder{T}.cs b/src/Authoring/WinRT.SourceGenerator2/Helpers/PooledArrayBuilder{T}.cs new file mode 100644 index 000000000..876e50464 --- /dev/null +++ b/src/Authoring/WinRT.SourceGenerator2/Helpers/PooledArrayBuilder{T}.cs @@ -0,0 +1,357 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Ported from ComputeSharp. +// See: https://github.com/Sergio0694/ComputeSharp/blob/main/src/ComputeSharp.SourceGeneration/Helpers/ImmutableArrayBuilder%7BT%7D.cs. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Runtime.CompilerServices; + +#pragma warning disable IDE0032 + +namespace WindowsRuntime.SourceGenerator; + +/// +/// A helper type to build sequences of values with pooled buffers. +/// +/// The type of items to create sequences for. +internal ref struct PooledArrayBuilder : IDisposable +{ + /// + /// The shared instance to share objects. + /// + private static readonly ObjectPool SharedObjectPool = new(static () => new Writer()); + + /// + /// The rented instance to use. + /// + private Writer? _writer; + + /// + /// Creates a new object. + /// + public PooledArrayBuilder() + { + _writer = SharedObjectPool.Allocate(); + } + + /// + /// Gets the number of elements currently written in the current instance. + /// + public readonly int Count + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _writer!.Count; + } + + /// + /// Gets the data written to the underlying buffer so far, as a . + /// + public readonly ReadOnlySpan WrittenSpan + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _writer!.WrittenSpan; + } + + /// + /// Advances the current writer and gets a to the requested memory area. + /// + /// The requested size to advance by. + /// A to the requested memory area. + /// + /// No other data should be written to the builder while the returned + /// is in use, as it could invalidate the memory area wrapped by it, if resizing occurs. + /// + public readonly Span Advance(int requestedSize) + { + return _writer!.Advance(requestedSize); + } + + /// + public readonly void Add(T item) + { + _writer!.Add(item); + } + + /// + /// Adds the specified items to the end of the array. + /// + /// The items to add at the end of the array. + public readonly void AddRange(ReadOnlySpan items) + { + _writer!.AddRange(items); + } + + /// + public readonly void Clear() + { + _writer!.Clear(); + } + + /// + /// Inserts an item to the builder at the specified index. + /// + /// The zero-based index at which should be inserted. + /// The object to insert into the current instance. + public readonly void Insert(int index, T item) + { + _writer!.Insert(index, item); + } + + /// + /// Gets an instance for the current builder. + /// + /// An instance for the current builder. + /// + /// The builder should not be mutated while an enumerator is in use. + /// + public readonly IEnumerable AsEnumerable() + { + return _writer!; + } + + /// + public readonly ImmutableArray ToImmutable() + { + T[] array = _writer!.WrittenSpan.ToArray(); + + return Unsafe.As>(ref array); + } + + /// + public readonly T[] ToArray() + { + return _writer!.WrittenSpan.ToArray(); + } + + /// + public override readonly string ToString() + { + return _writer!.WrittenSpan.ToString(); + } + + /// + public void Dispose() + { + Writer? writer = _writer; + + _writer = null; + + if (writer is not null) + { + writer.Clear(); + + SharedObjectPool.Free(writer); + } + } + + /// + /// A class handling the actual buffer writing. + /// + private sealed class Writer : IList, IReadOnlyList + { + /// + /// The underlying array. + /// + private T[] _array; + + /// + /// The starting offset within . + /// + private int _index; + + /// + /// Creates a new instance with the specified parameters. + /// + public Writer() + { + _array = typeof(T) == typeof(char) + ? new T[1024] + : new T[8]; + + _index = 0; + } + + /// + public int Count + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => _index; + } + + /// + public ReadOnlySpan WrittenSpan + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => new(_array, 0, _index); + } + + /// + bool ICollection.IsReadOnly => true; + + /// + T IReadOnlyList.this[int index] => WrittenSpan[index]; + + /// + T IList.this[int index] + { + get => WrittenSpan[index]; + set => throw new NotSupportedException(); + } + + /// + public Span Advance(int requestedSize) + { + EnsureCapacity(requestedSize); + + Span span = _array.AsSpan(_index, requestedSize); + + _index += requestedSize; + + return span; + } + + /// + public void Add(T value) + { + EnsureCapacity(1); + + _array[_index++] = value; + } + + /// + public void AddRange(ReadOnlySpan items) + { + EnsureCapacity(items.Length); + + items.CopyTo(_array.AsSpan(_index)); + + _index += items.Length; + } + + /// + public void Insert(int index, T item) + { + if (index < 0 || index > _index) + { + PooledArrayBuilder.ThrowArgumentOutOfRangeExceptionForIndex(); + } + + EnsureCapacity(1); + + if (index < _index) + { + Array.Copy(_array, index, _array, index + 1, _index - index); + } + + _array[index] = item; + _index++; + } + + /// + public void Clear() + { + if (RuntimeHelpers.IsReferenceOrContainsReferences()) + { + _array.AsSpan(0, _index).Clear(); + } + + _index = 0; + } + + /// + /// Ensures that has enough free space to contain a given number of new items. + /// + /// The minimum number of items to ensure space for in . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void EnsureCapacity(int requestedSize) + { + if (requestedSize > _array.Length - _index) + { + ResizeBuffer(requestedSize); + } + } + + /// + /// Resizes to ensure it can fit the specified number of new items. + /// + /// The minimum number of items to ensure space for in . + [MethodImpl(MethodImplOptions.NoInlining)] + private void ResizeBuffer(int sizeHint) + { + int minimumSize = _index + sizeHint; + int requestedSize = Math.Max(_array.Length * 2, minimumSize); + + T[] newArray = new T[requestedSize]; + + Array.Copy(_array, newArray, _index); + + _array = newArray; + } + + /// + int IList.IndexOf(T item) + { + return Array.IndexOf(_array, item, 0, _index); + } + + /// + void IList.RemoveAt(int index) + { + throw new NotSupportedException(); + } + + /// + bool ICollection.Contains(T item) + { + return Array.IndexOf(_array, item, 0, _index) >= 0; + } + + /// + void ICollection.CopyTo(T[] array, int arrayIndex) + { + Array.Copy(_array, 0, array, arrayIndex, _index); + } + + /// + bool ICollection.Remove(T item) + { + throw new NotSupportedException(); + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + T?[] array = _array!; + int length = _index; + + for (int i = 0; i < length; i++) + { + yield return array[i]!; + } + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)this).GetEnumerator(); + } + } +} + +/// +/// Private helpers for the type. +/// +file static class PooledArrayBuilder +{ + /// + /// Throws an for "index". + /// + public static void ThrowArgumentOutOfRangeExceptionForIndex() + { + throw new ArgumentOutOfRangeException("index"); + } +} \ No newline at end of file From 07a9d9bbcae8a84764c7a3575e0b61b3ea8a4e78 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 8 Dec 2025 15:23:33 -0800 Subject: [PATCH 11/46] Add IndentedTextWriter helper and update PooledArrayBuilder Introduces IndentedTextWriter, a helper for writing indented text with pooled buffers, ported from ComputeSharp. Changes PooledArrayBuilder from a ref struct to a struct to support usage in IndentedTextWriter and improve compatibility. --- .../Helpers/IndentedTextWriter.cs | 520 ++++++++++++++++++ .../Helpers/PooledArrayBuilder{T}.cs | 2 +- 2 files changed, 521 insertions(+), 1 deletion(-) create mode 100644 src/Authoring/WinRT.SourceGenerator2/Helpers/IndentedTextWriter.cs diff --git a/src/Authoring/WinRT.SourceGenerator2/Helpers/IndentedTextWriter.cs b/src/Authoring/WinRT.SourceGenerator2/Helpers/IndentedTextWriter.cs new file mode 100644 index 000000000..f2d84e94f --- /dev/null +++ b/src/Authoring/WinRT.SourceGenerator2/Helpers/IndentedTextWriter.cs @@ -0,0 +1,520 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Ported from ComputeSharp. +// See: https://github.com/Sergio0694/ComputeSharp/blob/main/src/ComputeSharp.SourceGeneration/Helpers/IndentedTextWriter.cs. + +using System; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Runtime.CompilerServices; +using System.Text; + +namespace WindowsRuntime.SourceGenerator; + +/// +/// A helper type to build sequences of values with pooled buffers. +/// +internal sealed class IndentedTextWriter : IDisposable +{ + /// + /// The default indentation (4 spaces). + /// + private const string DefaultIndentation = " "; + + /// + /// The default new line ('\n'). + /// + private const char DefaultNewLine = '\n'; + + /// + /// The instance that text will be written to. + /// + private PooledArrayBuilder _builder; + + /// + /// The current indentation level. + /// + private int _currentIndentationLevel; + + /// + /// The current indentation, as text. + /// + private string _currentIndentation = ""; + + /// + /// The cached array of available indentations, as text. + /// + private string[] _availableIndentations; + + /// + /// Creates a new object. + /// + public IndentedTextWriter() + { + _builder = new PooledArrayBuilder(); + _currentIndentationLevel = 0; + _currentIndentation = ""; + _availableIndentations = new string[4]; + _availableIndentations[0] = ""; + + for (int i = 1, n = _availableIndentations.Length; i < n; i++) + { + _availableIndentations[i] = _availableIndentations[i - 1] + DefaultIndentation; + } + } + + /// + /// Advances the current writer and gets a to the requested memory area. + /// + /// The requested size to advance by. + /// A to the requested memory area. + /// + /// No other data should be written to the writer while the returned + /// is in use, as it could invalidate the memory area wrapped by it, if resizing occurs. + /// + public Span Advance(int requestedSize) + { + // Add the leading whitespace if needed (same as WriteRawText below) + if (_builder.Count == 0 || _builder.WrittenSpan[^1] == DefaultNewLine) + { + _builder.AddRange(_currentIndentation.AsSpan()); + } + + return _builder.Advance(requestedSize); + } + + /// + /// Increases the current indentation level. + /// + public void IncreaseIndent() + { + _currentIndentationLevel++; + + if (_currentIndentationLevel == _availableIndentations.Length) + { + Array.Resize(ref _availableIndentations, _availableIndentations.Length * 2); + } + + // Set both the current indentation and the current position in the indentations + // array to the expected indentation for the incremented level (ie. one level more). + _currentIndentation = _availableIndentations[_currentIndentationLevel] + ??= _availableIndentations[_currentIndentationLevel - 1] + DefaultIndentation; + } + + /// + /// Decreases the current indentation level. + /// + public void DecreaseIndent() + { + _currentIndentationLevel--; + _currentIndentation = _availableIndentations[_currentIndentationLevel]; + } + + /// + /// Writes a block to the underlying buffer. + /// + /// A value to close the open block with. + public Block WriteBlock() + { + WriteLine("{"); + IncreaseIndent(); + + return new(this); + } + + /// + /// Writes content to the underlying buffer. + /// + /// The content to write. + /// Whether the input content is multiline. + public void Write(string content, bool isMultiline = false) + { + Write(content.AsSpan(), isMultiline); + } + + /// + /// Writes content to the underlying buffer. + /// + /// The content to write. + /// Whether the input content is multiline. + public void Write(ReadOnlySpan content, bool isMultiline = false) + { + if (isMultiline) + { + while (content.Length > 0) + { + int newLineIndex = content.IndexOf(DefaultNewLine); + + if (newLineIndex < 0) + { + // There are no new lines left, so the content can be written as a single line + WriteRawText(content); + + break; + } + else + { + ReadOnlySpan line = content[..newLineIndex]; + + // Write the current line (if it's empty, we can skip writing the text entirely). + // This ensures that raw multiline string literals with blank lines don't have + // extra whitespace at the start of those lines, which would otherwise happen. + WriteIf(!line.IsEmpty, line); + WriteLine(); + + // Move past the new line character (the result could be an empty span) + content = content[(newLineIndex + 1)..]; + } + } + } + else + { + WriteRawText(content); + } + } + + /// + /// Writes content to the underlying buffer. + /// + /// The interpolated string handler with content to write. + [SuppressMessage("Style", "IDE0060", Justification = "The 'handler' parameter is used by the caller via compiler lowering.")] + public void Write([InterpolatedStringHandlerArgument("")] ref WriteInterpolatedStringHandler handler) + { + _ = this; + } + + /// + /// Writes content to the underlying buffer depending on an input condition. + /// + /// The condition to use to decide whether or not to write content. + /// The content to write. + /// Whether the input content is multiline. + public void WriteIf(bool condition, string content, bool isMultiline = false) + { + if (condition) + { + Write(content.AsSpan(), isMultiline); + } + } + + /// + /// Writes content to the underlying buffer depending on an input condition. + /// + /// The condition to use to decide whether or not to write content. + /// The content to write. + /// Whether the input content is multiline. + public void WriteIf(bool condition, ReadOnlySpan content, bool isMultiline = false) + { + if (condition) + { + Write(content, isMultiline); + } + } + + /// + /// Writes content to the underlying buffer depending on an input condition. + /// + /// The condition to use to decide whether or not to write content. + /// The interpolated string handler with content to write. + [SuppressMessage("Style", "IDE0060", Justification = "The 'handler' parameter is used by the caller via compiler lowering.")] + public void WriteIf(bool condition, [InterpolatedStringHandlerArgument("", nameof(condition))] ref WriteIfInterpolatedStringHandler handler) + { + _ = this; + } + + /// + /// Writes a line to the underlying buffer. + /// + /// Indicates whether to skip adding the line if there already is one. + public void WriteLine(bool skipIfPresent = false) + { + if (skipIfPresent && _builder.WrittenSpan is [.., '\n', '\n']) + { + return; + } + + _builder.Add(DefaultNewLine); + } + + /// + /// Writes content to the underlying buffer and appends a trailing new line. + /// + /// The content to write. + /// Whether the input content is multiline. + public void WriteLine(string content, bool isMultiline = false) + { + WriteLine(content.AsSpan(), isMultiline); + } + + /// + /// Writes content to the underlying buffer and appends a trailing new line. + /// + /// The content to write. + /// Whether the input content is multiline. + public void WriteLine(ReadOnlySpan content, bool isMultiline = false) + { + Write(content, isMultiline); + WriteLine(); + } + + /// + /// Writes content to the underlying buffer and appends a trailing new line. + /// + /// The interpolated string handler with content to write. + [SuppressMessage("Style", "IDE0060", Justification = "The 'handler' parameter is used by the caller via compiler lowering.")] + public void WriteLine([InterpolatedStringHandlerArgument("")] ref WriteInterpolatedStringHandler handler) + { + WriteLine(); + } + + /// + /// Writes a line to the underlying buffer depending on an input condition. + /// + /// The condition to use to decide whether or not to write content. + /// Indicates whether to skip adding the line if there already is one. + public void WriteLineIf(bool condition, bool skipIfPresent = false) + { + if (condition) + { + WriteLine(skipIfPresent); + } + } + + /// + /// Writes content to the underlying buffer and appends a trailing new line depending on an input condition. + /// + /// The condition to use to decide whether or not to write content. + /// The content to write. + /// Whether the input content is multiline. + public void WriteLineIf(bool condition, string content, bool isMultiline = false) + { + if (condition) + { + WriteLine(content.AsSpan(), isMultiline); + } + } + + /// + /// Writes content to the underlying buffer and appends a trailing new line depending on an input condition. + /// + /// The condition to use to decide whether or not to write content. + /// The content to write. + /// Whether the input content is multiline. + public void WriteLineIf(bool condition, ReadOnlySpan content, bool isMultiline = false) + { + if (condition) + { + Write(content, isMultiline); + WriteLine(); + } + } + + /// + /// Writes content to the underlying buffer and appends a trailing new line depending on an input condition. + /// + /// The condition to use to decide whether or not to write content. + /// The interpolated string handler with content to write. + [SuppressMessage("Style", "IDE0060", Justification = "The 'handler' parameter is used by the caller via compiler lowering.")] + public void WriteLineIf(bool condition, [InterpolatedStringHandlerArgument("", nameof(condition))] ref WriteIfInterpolatedStringHandler handler) + { + if (condition) + { + WriteLine(); + } + } + + /// + public override string ToString() + { + return _builder.WrittenSpan.Trim().ToString(); + } + + /// + public void Dispose() + { + _builder.Dispose(); + } + + /// + /// Writes raw text to the underlying buffer, adding leading indentation if needed. + /// + /// The raw text to write. + private void WriteRawText(ReadOnlySpan content) + { + if (_builder.Count == 0 || _builder.WrittenSpan[^1] == DefaultNewLine) + { + _builder.AddRange(_currentIndentation.AsSpan()); + } + + _builder.AddRange(content); + } + + /// + /// A delegate representing a callback to write data into an instance. + /// + /// The type of data to use. + /// The input data to use to write into . + /// The instance to write into. + public delegate void Callback(T value, IndentedTextWriter writer); + + /// + /// Represents an indented block that needs to be closed. + /// + /// The input instance to wrap. + public struct Block(IndentedTextWriter writer) : IDisposable + { + /// + /// The instance to write to. + /// + private IndentedTextWriter? _writer = writer; + + /// + public void Dispose() + { + IndentedTextWriter? writer = _writer; + + _writer = null; + + if (writer is not null) + { + writer.DecreaseIndent(); + writer.WriteLine("}"); + } + } + } + + /// + /// Provides a handler used by the language compiler to append interpolated strings into instances. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + [InterpolatedStringHandler] + public readonly ref struct WriteInterpolatedStringHandler + { + /// The associated to which to append. + private readonly IndentedTextWriter _writer; + + /// Creates a handler used to append an interpolated string into a . + /// The number of constant characters outside of interpolation expressions in the interpolated string. + /// The number of interpolation expressions in the interpolated string. + /// The associated to which to append. + /// This is intended to be called only by compiler-generated code. Arguments are not validated as they'd otherwise be for members intended to be used directly. + public WriteInterpolatedStringHandler(int literalLength, int formattedCount, IndentedTextWriter writer) + { + _writer = writer; + } + + /// Writes the specified string to the handler. + /// The string to write. + public void AppendLiteral(string value) + { + _writer.Write(value); + } + + /// Writes the specified value to the handler. + /// The value to write. + public void AppendFormatted(string? value) + { + AppendFormatted(value); + } + + /// Writes the specified character span to the handler. + /// The span to write. + public void AppendFormatted(ReadOnlySpan value) + { + _writer.Write(value); + } + + /// Writes the specified value to the handler. + /// The value to write. + /// The type of the value to write. + public void AppendFormatted(T value) + { + if (value is not null) + { + _writer.Write(value.ToString()!); + } + } + + /// Writes the specified value to the handler. + /// The value to write. + /// The format string. + /// The type of the value to write. + public void AppendFormatted(T value, string? format) + { + if (value is IFormattable) + { + _writer.Write(((IFormattable)value).ToString(format, CultureInfo.InvariantCulture)); + } + else if (value is not null) + { + _writer.Write(value.ToString()!); + } + } + } + + /// + /// Provides a handler used by the language compiler to conditionally append interpolated strings into instances. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + [InterpolatedStringHandler] + public readonly ref struct WriteIfInterpolatedStringHandler + { + /// The associated to use. + private readonly WriteInterpolatedStringHandler handler; + + /// Creates a handler used to append an interpolated string into a . + /// The number of constant characters outside of interpolation expressions in the interpolated string. + /// The number of interpolation expressions in the interpolated string. + /// The associated to which to append. + /// The condition to use to decide whether or not to write content. + /// A value indicating whether formatting should proceed. + /// This is intended to be called only by compiler-generated code. Arguments are not validated as they'd otherwise be for members intended to be used directly. + public WriteIfInterpolatedStringHandler(int literalLength, int formattedCount, IndentedTextWriter writer, bool condition, out bool shouldAppend) + { + if (condition) + { + handler = new WriteInterpolatedStringHandler(literalLength, formattedCount, writer); + + shouldAppend = true; + } + else + { + handler = default; + + shouldAppend = false; + } + } + + /// + public void AppendLiteral(string value) + { + handler.AppendLiteral(value); + } + + /// + public void AppendFormatted(string? value) + { + handler.AppendFormatted(value); + } + + /// + public void AppendFormatted(ReadOnlySpan value) + { + handler.AppendFormatted(value); + } + + /// + public void AppendFormatted(T value) + { + handler.AppendFormatted(value); + } + + /// + public void AppendFormatted(T value, string? format) + { + handler.AppendFormatted(value, format); + } + } +} \ No newline at end of file diff --git a/src/Authoring/WinRT.SourceGenerator2/Helpers/PooledArrayBuilder{T}.cs b/src/Authoring/WinRT.SourceGenerator2/Helpers/PooledArrayBuilder{T}.cs index 876e50464..a8214e388 100644 --- a/src/Authoring/WinRT.SourceGenerator2/Helpers/PooledArrayBuilder{T}.cs +++ b/src/Authoring/WinRT.SourceGenerator2/Helpers/PooledArrayBuilder{T}.cs @@ -18,7 +18,7 @@ namespace WindowsRuntime.SourceGenerator; /// A helper type to build sequences of values with pooled buffers. /// /// The type of items to create sequences for. -internal ref struct PooledArrayBuilder : IDisposable +internal struct PooledArrayBuilder : IDisposable { /// /// The shared instance to share objects. From 821ee2681b68ff00512a7b45e60d03f294a78f18 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 8 Dec 2025 15:26:29 -0800 Subject: [PATCH 12/46] Comment out bindable custom property generation logic The code responsible for generating bindable custom properties in CustomPropertyProviderGenerator has been commented out, likely to temporarily disable this feature or for refactoring purposes. --- .../CustomPropertyProviderGenerator.cs | 26 ++++++++----------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.cs b/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.cs index 9126e1e18..0be133115 100644 --- a/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.cs +++ b/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.cs @@ -1,10 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; -using System.Collections.Immutable; -using System.Linq; -using System.Threading; using Microsoft.CodeAnalysis; namespace WindowsRuntime.SourceGenerator; @@ -18,16 +14,16 @@ public sealed partial class CustomPropertyProviderGenerator : IIncrementalGenera /// public void Initialize(IncrementalGeneratorInitializationContext context) { - var bindableCustomPropertyAttributes = context.ForAttributeWithMetadataNameAndOptions( - fullyQualifiedMetadataName: "WinRT.GeneratedBindableCustomPropertyAttribute", - predicate: Execute.IsTargetNodeValid, - transform: static (n, _) => n) - .Combine(properties) - .Select(static ((GeneratorAttributeSyntaxContext generatorSyntaxContext, CsWinRTAotOptimizerProperties properties) value, CancellationToken _) => - value.properties.IsCsWinRTAotOptimizerEnabled ? GetBindableCustomProperties(value.generatorSyntaxContext) : default) - .Where(static bindableCustomProperties => bindableCustomProperties != default) - .Collect() - .Combine(properties); - context.RegisterImplementationSourceOutput(bindableCustomPropertyAttributes, GenerateBindableCustomProperties); + //var bindableCustomPropertyAttributes = context.ForAttributeWithMetadataNameAndOptions( + // fullyQualifiedMetadataName: "WinRT.GeneratedBindableCustomPropertyAttribute", + // predicate: Execute.IsTargetNodeValid, + // transform: static (n, _) => n) + //.Combine(properties) + //.Select(static ((GeneratorAttributeSyntaxContext generatorSyntaxContext, CsWinRTAotOptimizerProperties properties) value, CancellationToken _) => + // value.properties.IsCsWinRTAotOptimizerEnabled ? GetBindableCustomProperties(value.generatorSyntaxContext) : default) + //.Where(static bindableCustomProperties => bindableCustomProperties != default) + //.Collect() + //.Combine(properties); + //context.RegisterImplementationSourceOutput(bindableCustomPropertyAttributes, GenerateBindableCustomProperties); } } \ No newline at end of file From c969b252eaff5d19d0e039dc2c0b1ab24bea1ffb Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 8 Dec 2025 15:30:14 -0800 Subject: [PATCH 13/46] Add IsDefaultOrEmpty and Length properties to EquatableArray Introduces IsDefaultOrEmpty and Length properties to the EquatableArray struct for easier array state inspection. Also replaces with in documentation comments for improved clarity and consistency. --- .../Helpers/EquatableArray{T}.cs | 28 +++++++++++++++---- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/src/Authoring/WinRT.SourceGenerator2/Helpers/EquatableArray{T}.cs b/src/Authoring/WinRT.SourceGenerator2/Helpers/EquatableArray{T}.cs index 223d8a6b5..384a904e1 100644 --- a/src/Authoring/WinRT.SourceGenerator2/Helpers/EquatableArray{T}.cs +++ b/src/Authoring/WinRT.SourceGenerator2/Helpers/EquatableArray{T}.cs @@ -75,19 +75,37 @@ public bool IsEmpty get => AsImmutableArray().IsEmpty; } - /// + /// + /// Gets a value indicating whether the current array is default or empty. + /// + public bool IsDefaultOrEmpty + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => AsImmutableArray().IsDefaultOrEmpty; + } + + /// + /// Gets the length of the current array. + /// + public int Length + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => AsImmutableArray().Length; + } + + /// public bool Equals(EquatableArray array) { return AsSpan().SequenceEqual(array.AsSpan()); } - /// + /// public override bool Equals([NotNullWhen(true)] object? obj) { return obj is EquatableArray array && Equals(array); } - /// + /// public override int GetHashCode() { if (_array is not T[] array) @@ -152,13 +170,13 @@ public ImmutableArray.Enumerator GetEnumerator() return AsImmutableArray().GetEnumerator(); } - /// + /// IEnumerator IEnumerable.GetEnumerator() { return ((IEnumerable)AsImmutableArray()).GetEnumerator(); } - /// + /// IEnumerator IEnumerable.GetEnumerator() { return ((IEnumerable)AsImmutableArray()).GetEnumerator(); From cfab2d18b72fdb9e87117103cd661f86f4708f1e Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 8 Dec 2025 15:32:54 -0800 Subject: [PATCH 14/46] Add ITypeSymbol extension methods for metadata names Introduces ITypeSymbolExtensions with methods to retrieve and append fully qualified metadata names for ITypeSymbol instances. These utilities facilitate working with Roslyn symbols in source generation scenarios. --- .../Extensions/ITypeSymbolExtensions.cs | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 src/Authoring/WinRT.SourceGenerator2/Extensions/ITypeSymbolExtensions.cs diff --git a/src/Authoring/WinRT.SourceGenerator2/Extensions/ITypeSymbolExtensions.cs b/src/Authoring/WinRT.SourceGenerator2/Extensions/ITypeSymbolExtensions.cs new file mode 100644 index 000000000..740772942 --- /dev/null +++ b/src/Authoring/WinRT.SourceGenerator2/Extensions/ITypeSymbolExtensions.cs @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using Microsoft.CodeAnalysis; + +#pragma warning disable CS1734 + +namespace WindowsRuntime.SourceGenerator; + +/// +/// Extensions for . +/// +internal static class ITypeSymbolExtensions +{ + extension(ITypeSymbol symbol) + { + /// + /// Gets the fully qualified metadata name for a given instance. + /// + /// The fully qualified metadata name for . + public string GetFullyQualifiedMetadataName() + { + using PooledArrayBuilder builder = new(); + + symbol.AppendFullyQualifiedMetadataName(in builder); + + return builder.ToString(); + } + + /// + /// Appends the fully qualified metadata name for a given symbol to a target builder. + /// + /// The target instance. + public void AppendFullyQualifiedMetadataName(ref readonly PooledArrayBuilder builder) + { + static void BuildFrom(ISymbol? symbol, ref readonly PooledArrayBuilder builder) + { + switch (symbol) + { + // Namespaces that are nested also append a leading '.' + case INamespaceSymbol { ContainingNamespace.IsGlobalNamespace: false }: + BuildFrom(symbol.ContainingNamespace, in builder); + builder.Add('.'); + builder.AddRange(symbol.MetadataName.AsSpan()); + break; + + // Other namespaces (ie. the one right before global) skip the leading '.' + case INamespaceSymbol { IsGlobalNamespace: false }: + builder.AddRange(symbol.MetadataName.AsSpan()); + break; + + // Types with no namespace just have their metadata name directly written + case ITypeSymbol { ContainingSymbol: INamespaceSymbol { IsGlobalNamespace: true } }: + builder.AddRange(symbol.MetadataName.AsSpan()); + break; + + // Types with a containing non-global namespace also append a leading '.' + case ITypeSymbol { ContainingSymbol: INamespaceSymbol namespaceSymbol }: + BuildFrom(namespaceSymbol, in builder); + builder.Add('.'); + builder.AddRange(symbol.MetadataName.AsSpan()); + break; + + // Nested types append a leading '+' + case ITypeSymbol { ContainingSymbol: ITypeSymbol typeSymbol }: + BuildFrom(typeSymbol, in builder); + builder.Add('+'); + builder.AddRange(symbol.MetadataName.AsSpan()); + break; + default: + break; + } + } + + BuildFrom(symbol, in builder); + } + } +} \ No newline at end of file From 214ee6c05d3bb281552b2e2ca4c18f9982c5d9e5 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 8 Dec 2025 15:35:05 -0800 Subject: [PATCH 15/46] Add IndentedTextWriter extension methods Introduces IndentedTextWriterExtensions with helper methods for writing generated attributes, sorted using directives, line-separated members, and initialization expressions to streamline code generation tasks. --- .../IndentedTextWriterExtensions.cs | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 src/Authoring/WinRT.SourceGenerator2/Extensions/IndentedTextWriterExtensions.cs diff --git a/src/Authoring/WinRT.SourceGenerator2/Extensions/IndentedTextWriterExtensions.cs b/src/Authoring/WinRT.SourceGenerator2/Extensions/IndentedTextWriterExtensions.cs new file mode 100644 index 000000000..76c2b553e --- /dev/null +++ b/src/Authoring/WinRT.SourceGenerator2/Extensions/IndentedTextWriterExtensions.cs @@ -0,0 +1,128 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; + +namespace WindowsRuntime.SourceGenerator; + +/// +/// Extension methods for the type. +/// +internal static class IndentedTextWriterExtensions +{ + /// + /// Writes the following attributes into a target writer: + /// + /// [global::System.CodeDom.Compiler.GeneratedCode("...", "...")] + /// [global::System.Diagnostics.DebuggerNonUserCode] + /// [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + /// + /// + /// The instance to write into. + /// The name of the generator. + /// Whether to use fully qualified type names or not. + /// Whether to also include the attribute for non-user code. + public static void WriteGeneratedAttributes( + this IndentedTextWriter writer, + string generatorName, + bool useFullyQualifiedTypeNames = true, + bool includeNonUserCodeAttributes = true) + { + // We can use this class to get the assembly, as all files for generators are just included + // via shared projects. As such, the assembly will be the same as the generator type itself. + Version assemblyVersion = typeof(IndentedTextWriterExtensions).Assembly.GetName().Version!; + + if (useFullyQualifiedTypeNames) + { + writer.WriteLine($$"""[global::System.CodeDom.Compiler.GeneratedCode("{{generatorName}}", "{{assemblyVersion}}")]"""); + + if (includeNonUserCodeAttributes) + { + writer.WriteLine($$"""[global::System.Diagnostics.DebuggerNonUserCode]"""); + writer.WriteLine($$"""[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]"""); + } + } + else + { + writer.WriteLine($$"""[GeneratedCode("{{generatorName}}", "{{assemblyVersion}}")]"""); + + if (includeNonUserCodeAttributes) + { + writer.WriteLine($$"""[DebuggerNonUserCode]"""); + writer.WriteLine($$"""[ExcludeFromCodeCoverage]"""); + } + } + } + + /// + /// Writes a sequence of using directives, sorted correctly. + /// + /// The instance to write into. + /// The sequence of using directives to write. + public static void WriteSortedUsingDirectives(this IndentedTextWriter writer, IEnumerable usingDirectives) + { + // Add the System directives first, in the correct order + foreach (string usingDirective in usingDirectives.Where(static name => name.StartsWith("global::System", StringComparison.InvariantCulture)).OrderBy(static name => name)) + { + writer.WriteLine($"using {usingDirective};"); + } + + // Add the other directives, also sorted in the correct order + foreach (string usingDirective in usingDirectives.Where(static name => !name.StartsWith("global::System", StringComparison.InvariantCulture)).OrderBy(static name => name)) + { + writer.WriteLine($"using {usingDirective};"); + } + + // Leave a trailing blank line if at least one using directive has been written. + // This is so that any members will correctly have a leading blank line before. + writer.WriteLineIf(usingDirectives.Any()); + } + + /// + /// Writes a series of members separated by one line between each of them. + /// + /// The type of input items to process. + /// The instance to write into. + /// The input items to process. + /// The instance to invoke for each item. + public static void WriteLineSeparatedMembers( + this IndentedTextWriter writer, + ReadOnlySpan items, + IndentedTextWriter.Callback callback) + { + for (int i = 0; i < items.Length; i++) + { + if (i > 0) + { + writer.WriteLine(); + } + + callback(items[i], writer); + } + } + + /// + /// Writes a series of initialization expressions separated by a comma between each of them. + /// + /// The type of input items to process. + /// The instance to write into. + /// The input items to process. + /// The instance to invoke for each item. + public static void WriteInitializationExpressions( + this IndentedTextWriter writer, + ReadOnlySpan items, + IndentedTextWriter.Callback callback) + { + for (int i = 0; i < items.Length; i++) + { + callback(items[i], writer); + + if (i < items.Length - 1) + { + writer.WriteLine(","); + } + } + } +} \ No newline at end of file From 2200f3b11434fefe7048ef7277fc0015acb67c87 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 8 Dec 2025 15:35:15 -0800 Subject: [PATCH 16/46] Add HierarchyInfo and TypeInfo models Introduces HierarchyInfo and TypeInfo classes to model type hierarchies for source generation. These are ported from ComputeSharp and provide utilities for describing and generating type syntax based on Roslyn symbols. --- .../Models/HierarchyInfo.cs | 139 ++++++++++++++++++ .../WinRT.SourceGenerator2/Models/TypeInfo.cs | 36 +++++ 2 files changed, 175 insertions(+) create mode 100644 src/Authoring/WinRT.SourceGenerator2/Models/HierarchyInfo.cs create mode 100644 src/Authoring/WinRT.SourceGenerator2/Models/TypeInfo.cs diff --git a/src/Authoring/WinRT.SourceGenerator2/Models/HierarchyInfo.cs b/src/Authoring/WinRT.SourceGenerator2/Models/HierarchyInfo.cs new file mode 100644 index 000000000..ff56fc698 --- /dev/null +++ b/src/Authoring/WinRT.SourceGenerator2/Models/HierarchyInfo.cs @@ -0,0 +1,139 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Ported from ComputeSharp. +// See: https://github.com/Sergio0694/ComputeSharp/blob/main/src/ComputeSharp.SourceGeneration/Models/HierarchyInfo.cs. + +using System; +using Microsoft.CodeAnalysis; +using static Microsoft.CodeAnalysis.SymbolDisplayTypeQualificationStyle; + +namespace WindowsRuntime.SourceGenerator.Models; + +/// +/// A model describing the hierarchy info for a specific type. +/// +/// The fully qualified metadata name for the current type. +/// Gets the namespace for the current type. +/// Gets the sequence of type definitions containing the current type. +internal sealed partial record HierarchyInfo(string FullyQualifiedMetadataName, string Namespace, EquatableArray Hierarchy) +{ + /// + /// Creates a new instance from a given . + /// + /// The input instance to gather info for. + /// A instance describing . + public static HierarchyInfo From(INamedTypeSymbol typeSymbol) + { + using PooledArrayBuilder hierarchy = new(); + + for (INamedTypeSymbol? parent = typeSymbol; + parent is not null; + parent = parent.ContainingType) + { + hierarchy.Add(new TypeInfo( + parent.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat), + parent.TypeKind, + parent.IsRecord)); + } + + return new( + typeSymbol.GetFullyQualifiedMetadataName(), + typeSymbol.ContainingNamespace.ToDisplayString(new(typeQualificationStyle: NameAndContainingTypesAndNamespaces)), + hierarchy.ToImmutable()); + } + + /// + /// Writes syntax for the current hierarchy into a target writer. + /// + /// The type of state to pass to callbacks. + /// The input state to pass to callbacks. + /// The target instance to write text to. + /// A list of base types to add to the generated type, if any. + /// The callbacks to use to write members into the declared type. + public void WriteSyntax( + T state, + IndentedTextWriter writer, + ReadOnlySpan baseTypes, + ReadOnlySpan> memberCallbacks) + { + // Write the generated file header + writer.WriteLine("// "); + writer.WriteLine("#pragma warning disable"); + writer.WriteLine(); + + // Declare the namespace, if needed + if (Namespace.Length > 0) + { + writer.WriteLine($"namespace {Namespace}"); + writer.WriteLine("{"); + writer.IncreaseIndent(); + } + + // Declare all the opening types until the inner-most one + for (int i = Hierarchy.Length - 1; i >= 0; i--) + { + writer.WriteLine($$"""/// """); + writer.Write($$"""partial {{Hierarchy[i].GetTypeKeyword()}} {{Hierarchy[i].QualifiedName}}"""); + + // Add any base types, if needed + if (i == 0 && !baseTypes.IsEmpty) + { + writer.Write(" : "); + writer.WriteInitializationExpressions(baseTypes, static (item, writer) => writer.Write(item)); + writer.WriteLine(); + } + else + { + writer.WriteLine(); + } + + writer.WriteLine($$"""{"""); + writer.IncreaseIndent(); + } + + // Generate all nested members + writer.WriteLineSeparatedMembers(memberCallbacks, (callback, writer) => callback(state, writer)); + + // Close all scopes and reduce the indentation + for (int i = 0; i < Hierarchy.Length; i++) + { + writer.DecreaseIndent(); + writer.WriteLine("}"); + } + + // Close the namespace scope as well, if needed + if (Namespace.Length > 0) + { + writer.DecreaseIndent(); + writer.WriteLine("}"); + } + } + + /// + /// Gets the fully qualified type name for the current instance. + /// + /// The fully qualified type name for the current instance. + public string GetFullyQualifiedTypeName() + { + using PooledArrayBuilder fullyQualifiedTypeName = new(); + + fullyQualifiedTypeName.AddRange("global::".AsSpan()); + + if (Namespace.Length > 0) + { + fullyQualifiedTypeName.AddRange(Namespace.AsSpan()); + fullyQualifiedTypeName.Add('.'); + } + + fullyQualifiedTypeName.AddRange(Hierarchy[^1].QualifiedName.AsSpan()); + + for (int i = Hierarchy.Length - 2; i >= 0; i--) + { + fullyQualifiedTypeName.Add('.'); + fullyQualifiedTypeName.AddRange(Hierarchy[i].QualifiedName.AsSpan()); + } + + return fullyQualifiedTypeName.ToString(); + } +} \ No newline at end of file diff --git a/src/Authoring/WinRT.SourceGenerator2/Models/TypeInfo.cs b/src/Authoring/WinRT.SourceGenerator2/Models/TypeInfo.cs new file mode 100644 index 000000000..b7575e999 --- /dev/null +++ b/src/Authoring/WinRT.SourceGenerator2/Models/TypeInfo.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Ported from ComputeSharp. +// See: https://github.com/Sergio0694/ComputeSharp/blob/main/src/ComputeSharp.SourceGeneration/Models/TypeInfo.cs. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; + +namespace WindowsRuntime.SourceGenerator.Models; + +/// +/// A model describing a type info in a type hierarchy. +/// +/// The qualified name for the type. +/// The type of the type in the hierarchy. +/// Whether the type is a record type. +internal sealed record TypeInfo(string QualifiedName, TypeKind Kind, bool IsRecord) +{ + /// + /// Gets the keyword for the current type kind. + /// + /// The keyword for the current type kind. + [SuppressMessage("Style", "IDE0072", Justification = "These are the only relevant cases for type hierarchies.")] + public string GetTypeKeyword() + { + return Kind switch + { + TypeKind.Struct when IsRecord => "record struct", + TypeKind.Struct => "struct", + TypeKind.Interface => "interface", + TypeKind.Class when IsRecord => "record", + _ => "class" + }; + } +} \ No newline at end of file From e3b36ded1240ff0f5a2480a5b94184b812ef4a15 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 8 Dec 2025 15:49:04 -0800 Subject: [PATCH 17/46] Disallow ICustomPropertyProvider on ref types Updated the generator to prevent implementing 'ICustomPropertyProvider' on types marked as 'ref', in addition to static and abstract types. --- .../CustomPropertyProviderGenerator.Execute.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Execute.cs b/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Execute.cs index 4876819ed..c79eab495 100644 --- a/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Execute.cs +++ b/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Execute.cs @@ -34,8 +34,8 @@ public static bool IsTargetNodeValid(SyntaxNode node, CancellationToken token) return false; } - // If the type is static or abstract, we cannot implement 'ICustomPropertyProvider' on it - if (((MemberDeclarationSyntax)node).Modifiers.ContainsAny(SyntaxKind.StaticKeyword, SyntaxKind.AbstractKeyword)) + // If the type is static, abstract, or 'ref', we cannot implement 'ICustomPropertyProvider' on it + if (((MemberDeclarationSyntax)node).Modifiers.ContainsAny(SyntaxKind.StaticKeyword, SyntaxKind.AbstractKeyword, SyntaxKind.RefKeyword)) { return false; } From 2aa2fa4ca892f071e5d4baf89014cb008a078ab0 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 8 Dec 2025 16:01:54 -0800 Subject: [PATCH 18/46] Add SkipNullValues extension for IncrementalValuesProvider Introduces an extension method to filter out null values from IncrementalValuesProvider instances, improving safety and convenience when working with nullable types in source generators. --- .../IncrementalValuesProviderExtensions.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 src/Authoring/WinRT.SourceGenerator2/Extensions/IncrementalValuesProviderExtensions.cs diff --git a/src/Authoring/WinRT.SourceGenerator2/Extensions/IncrementalValuesProviderExtensions.cs b/src/Authoring/WinRT.SourceGenerator2/Extensions/IncrementalValuesProviderExtensions.cs new file mode 100644 index 000000000..5c95394f5 --- /dev/null +++ b/src/Authoring/WinRT.SourceGenerator2/Extensions/IncrementalValuesProviderExtensions.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.CodeAnalysis; + +namespace WindowsRuntime.SourceGenerator; + +/// +/// Extensions for . +/// +internal static class IncrementalValuesProviderExtensions +{ + /// + /// Skips all values from a given provider. + /// + /// The type of values being produced. + /// The input instance. + /// The resulting instance. + public static IncrementalValuesProvider SkipNullValues(IncrementalValuesProvider provider) + where T : class + { + return provider.Where(static value => value is not null)!; + } +} From 00923588b9d869026465ba70222460425f047289 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 8 Dec 2025 16:45:14 -0800 Subject: [PATCH 19/46] Add EnumerateAllMembers extension for ITypeSymbol Introduces EnumerateAllMembers method to enumerate all members of an ITypeSymbol, including inherited members. This enhances symbol analysis capabilities in the source generator. --- .../Extensions/ITypeSymbolExtensions.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/Authoring/WinRT.SourceGenerator2/Extensions/ITypeSymbolExtensions.cs b/src/Authoring/WinRT.SourceGenerator2/Extensions/ITypeSymbolExtensions.cs index 740772942..9e163709d 100644 --- a/src/Authoring/WinRT.SourceGenerator2/Extensions/ITypeSymbolExtensions.cs +++ b/src/Authoring/WinRT.SourceGenerator2/Extensions/ITypeSymbolExtensions.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.Collections.Generic; using Microsoft.CodeAnalysis; #pragma warning disable CS1734 @@ -15,6 +16,23 @@ internal static class ITypeSymbolExtensions { extension(ITypeSymbol symbol) { + /// + /// Enumerates all members of a given instance, including inherited ones. + /// + /// The sequence of all member symbols for . + public IEnumerable EnumerateAllMembers() + { + for (ITypeSymbol? currentSymbol = symbol; + currentSymbol is not (null or { SpecialType: SpecialType.System_ValueType or SpecialType.System_Object }); + currentSymbol = currentSymbol.BaseType) + { + foreach (ISymbol currentMember in currentSymbol.GetMembers()) + { + yield return currentMember; + } + } + } + /// /// Gets the fully qualified metadata name for a given instance. /// From c6e43529dae1f75a7bf1dd31f10a56019a03fcf8 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 8 Dec 2025 16:45:19 -0800 Subject: [PATCH 20/46] Add 'this' modifier to SkipNullValues extension method The 'SkipNullValues' method in IncrementalValuesProviderExtensions is now correctly marked as an extension method by adding the 'this' modifier to the first parameter. --- .../Extensions/IncrementalValuesProviderExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Authoring/WinRT.SourceGenerator2/Extensions/IncrementalValuesProviderExtensions.cs b/src/Authoring/WinRT.SourceGenerator2/Extensions/IncrementalValuesProviderExtensions.cs index 5c95394f5..a04906d25 100644 --- a/src/Authoring/WinRT.SourceGenerator2/Extensions/IncrementalValuesProviderExtensions.cs +++ b/src/Authoring/WinRT.SourceGenerator2/Extensions/IncrementalValuesProviderExtensions.cs @@ -16,7 +16,7 @@ internal static class IncrementalValuesProviderExtensions /// The type of values being produced. /// The input instance. /// The resulting instance. - public static IncrementalValuesProvider SkipNullValues(IncrementalValuesProvider provider) + public static IncrementalValuesProvider SkipNullValues(this IncrementalValuesProvider provider) where T : class { return provider.Where(static value => value is not null)!; From 3cdf30b3bcc2ab74e50d6a13f3def2bdd55804d0 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 8 Dec 2025 16:52:32 -0800 Subject: [PATCH 21/46] Add methods for fully qualified symbol names Introduces GetFullyQualifiedName and GetFullyQualifiedNameWithNullabilityAnnotations extension methods to ISymbolExtensions, allowing retrieval of a symbol's fully qualified name with or without nullability annotations. --- .../Extensions/ISymbolExtensions.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/Authoring/WinRT.SourceGenerator2/Extensions/ISymbolExtensions.cs b/src/Authoring/WinRT.SourceGenerator2/Extensions/ISymbolExtensions.cs index 924c66a9e..34fc89f98 100644 --- a/src/Authoring/WinRT.SourceGenerator2/Extensions/ISymbolExtensions.cs +++ b/src/Authoring/WinRT.SourceGenerator2/Extensions/ISymbolExtensions.cs @@ -15,6 +15,24 @@ internal static class ISymbolExtensions { extension(ISymbol symbol) { + /// + /// Gets the fully qualified name for a given symbol. + /// + /// The fully qualified name for . + public string GetFullyQualifiedName() + { + return symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + } + + /// + /// Gets the fully qualified name for a given symbol, including nullability annotations + /// + /// The fully qualified name for . + public string GetFullyQualifiedNameWithNullabilityAnnotations() + { + return symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat.AddMiscellaneousOptions(SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier)); + } + /// /// Checks whether a type has an attribute with a specified type. /// From ec9da7c37d0e6171635fe2c3aaf6192423a2c76f Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 8 Dec 2025 16:57:56 -0800 Subject: [PATCH 22/46] WIP --- ...CustomPropertyProviderGenerator.Execute.cs | 131 +++++++++++++++++- .../CustomPropertyProviderGenerator.cs | 21 ++- 2 files changed, 140 insertions(+), 12 deletions(-) diff --git a/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Execute.cs b/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Execute.cs index c79eab495..d033fa2bc 100644 --- a/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Execute.cs +++ b/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Execute.cs @@ -2,10 +2,12 @@ // Licensed under the MIT License. using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Threading; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; +using WindowsRuntime.SourceGenerator.Models; #pragma warning disable IDE0046 @@ -49,5 +51,132 @@ public static bool IsTargetNodeValid(SyntaxNode node, CancellationToken token) return true; } + + /// + /// Tries to get the instance for a given annotated symbol. + /// + /// The value to use. + /// The cancellation token for the operation. + /// The resulting instance, if processed successfully. + public static CustomPropertyProviderInfo? GetCustomPropertyProviderInfo(GeneratorAttributeSyntaxContextWithOptions context, CancellationToken token) + { + bool useWindowsUIXamlProjections = context.GlobalOptions.GetBooleanProperty("CsWinRTUseWindowsUIXamlProjections"); + + token.ThrowIfCancellationRequested(); + + // Make sure that the target interface types are available. This is mostly because when UWP XAML projections + // are not used, the target project must be referencing the WinUI package to get the right interface type. + // If we can't find it, we just stop here. A separate diagnostic analyzer will emit the right diagnostic. + if ((useWindowsUIXamlProjections && context.SemanticModel.Compilation.GetTypeByMetadataName("Windows.UI.Xaml.Data.ICustomPropertyProvider") is null) || + (!useWindowsUIXamlProjections && context.SemanticModel.Compilation.GetTypeByMetadataName("Microsoft.UI.Xaml.Data.ICustomPropertyProvider") is null)) + { + return null; + } + + token.ThrowIfCancellationRequested(); + + // Ensure we have a valid named type symbol for the annotated type + if (context.TargetSymbol is not INamedTypeSymbol typeSymbol) + { + return null; + } + + // Get the type hierarchy (needed to correctly generate sources for nested types too) + HierarchyInfo typeHierarchy = HierarchyInfo.From(typeSymbol); + + token.ThrowIfCancellationRequested(); + + // Gather all custom properties, depending on how the attribute was used + EquatableArray customProperties = GetCustomPropertyInfo(typeSymbol, context.Attributes[0], token); + + token.ThrowIfCancellationRequested(); + + return new( + TypeHierarchy: typeHierarchy, + CustomProperties: customProperties, + UseWindowsUIXamlProjections: useWindowsUIXamlProjections); + } + + public static void WriteCustomPropertyProviderImplementations(SourceProductionContext context, CustomPropertyProviderInfo info) + { + } + + private static EquatableArray GetCustomPropertyInfo(INamedTypeSymbol typeSymbol, AttributeData attribute, CancellationToken token) + { + using PooledArrayBuilder customPropertyInfo = new(); + + // Make all public properties in the class bindable including ones in base type. + if (attribute.ConstructorArguments.IsDefaultOrEmpty) + { + foreach (ISymbol symbol in typeSymbol.EnumerateAllMembers()) + { + // Only gather public properties, and ignore overrides (we'll find the base definition instead). + // We also ignore partial property implementations, as we only care about the partial definitions. + if (symbol is not IPropertySymbol { DeclaredAccessibility: Accessibility.Public, IsOverride: false, PartialDefinitionPart: null } propertySymbol) + { + continue; + } + + // We can only support indexers with a single parameter. + // If there's more, an analyzer will emit a warning. + if (propertySymbol.Parameters.Length > 1) + { + continue; + } + + // Gather all the info for the current property + customPropertyInfo.Add(new CustomPropertyInfo( + Name: propertySymbol.Name, + FullyQualifiedTypeName: propertySymbol.Type.GetFullyQualifiedNameWithNullabilityAnnotations(), + FullyQualifiedIndexerTypeName: propertySymbol.Parameters.FirstOrDefault()?.GetFullyQualifiedNameWithNullabilityAnnotations(), + CanRead: propertySymbol.GetMethod is { DeclaredAccessibility: Accessibility.Public }, + CanWrite: propertySymbol.SetMethod is { DeclaredAccessibility: Accessibility.Public }, + IsStatic: propertySymbol.IsStatic)); + } + } + // Make specified public properties in the class bindable including ones in base type. + else if (attributeData.ConstructorArguments is + [ + { Kind: TypedConstantKind.Array, Values: [..] propertyNames }, + { Kind: TypedConstantKind.Array, Values: [..] propertyIndexerTypes } + ]) + { + for (var curSymbol = symbol; curSymbol != null; curSymbol = curSymbol.BaseType) + { + foreach (var member in curSymbol.GetMembers()) + { + if (member is IPropertySymbol propertySymbol && + member.DeclaredAccessibility == Accessibility.Public) + { + if (!propertySymbol.IsIndexer && + propertyNames.Any(p => p.Value is string value && value == propertySymbol.Name)) + { + AddProperty(propertySymbol); + } + else if (propertySymbol.IsIndexer && + // ICustomProperty only supports single indexer parameter. + propertySymbol.Parameters.Length == 1 && + propertyIndexerTypes.Any(p => p.Value is ISymbol typeSymbol && typeSymbol.Equals(propertySymbol.Parameters[0].Type, SymbolEqualityComparer.Default))) + { + AddProperty(propertySymbol); + } + } + } + } + } + } } -} \ No newline at end of file +} + +internal sealed record CustomPropertyInfo( + string Name, + string FullyQualifiedTypeName, + string? FullyQualifiedIndexerTypeName, + bool CanRead, + bool CanWrite, + bool IsStatic); + +internal sealed record CustomPropertyProviderInfo( + HierarchyInfo TypeHierarchy, + EquatableArray CustomProperties, + bool UseWindowsUIXamlProjections); \ No newline at end of file diff --git a/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.cs b/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.cs index 0be133115..af68369a1 100644 --- a/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.cs +++ b/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.cs @@ -14,16 +14,15 @@ public sealed partial class CustomPropertyProviderGenerator : IIncrementalGenera /// public void Initialize(IncrementalGeneratorInitializationContext context) { - //var bindableCustomPropertyAttributes = context.ForAttributeWithMetadataNameAndOptions( - // fullyQualifiedMetadataName: "WinRT.GeneratedBindableCustomPropertyAttribute", - // predicate: Execute.IsTargetNodeValid, - // transform: static (n, _) => n) - //.Combine(properties) - //.Select(static ((GeneratorAttributeSyntaxContext generatorSyntaxContext, CsWinRTAotOptimizerProperties properties) value, CancellationToken _) => - // value.properties.IsCsWinRTAotOptimizerEnabled ? GetBindableCustomProperties(value.generatorSyntaxContext) : default) - //.Where(static bindableCustomProperties => bindableCustomProperties != default) - //.Collect() - //.Combine(properties); - //context.RegisterImplementationSourceOutput(bindableCustomPropertyAttributes, GenerateBindableCustomProperties); + // Gather the info on all types annotated with '[GeneratedCustomPropertyProvider]'. + IncrementalValuesProvider providerInfo = context.ForAttributeWithMetadataNameAndOptions( + fullyQualifiedMetadataName: "WindowsRuntime.Xaml.GeneratedCustomPropertyProviderAttribute", + predicate: Execute.IsTargetNodeValid, + transform: Execute.GetCustomPropertyProviderInfo) + .WithTrackingName("CustomPropertyProviderInfo") + .SkipNullValues(); + + // Write the implementation for all annotated types + context.RegisterSourceOutput(providerInfo, Execute.WriteCustomPropertyProviderImplementations); } } \ No newline at end of file From 51191e0f47aca67963ba1917aacb7b092c465c40 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 8 Dec 2025 18:04:54 -0800 Subject: [PATCH 23/46] Refactor ToImmutable to use ToImmutableArray Replaces manual array conversion and unsafe cast in PooledArrayBuilder.ToImmutable with a direct call to WrittenSpan.ToImmutableArray for improved clarity and safety. --- .../WinRT.SourceGenerator2/Helpers/PooledArrayBuilder{T}.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Authoring/WinRT.SourceGenerator2/Helpers/PooledArrayBuilder{T}.cs b/src/Authoring/WinRT.SourceGenerator2/Helpers/PooledArrayBuilder{T}.cs index a8214e388..71c5e2506 100644 --- a/src/Authoring/WinRT.SourceGenerator2/Helpers/PooledArrayBuilder{T}.cs +++ b/src/Authoring/WinRT.SourceGenerator2/Helpers/PooledArrayBuilder{T}.cs @@ -116,9 +116,7 @@ public readonly IEnumerable AsEnumerable() /// public readonly ImmutableArray ToImmutable() { - T[] array = _writer!.WrittenSpan.ToArray(); - - return Unsafe.As>(ref array); + return _writer!.WrittenSpan.ToImmutableArray(); } /// From 017cc144a9a3b747cd077e9c878ed2327b41bbb4 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 8 Dec 2025 18:06:33 -0800 Subject: [PATCH 24/46] Refactor GetCustomPropertyInfo for clarity and filtering Simplifies and unifies property discovery logic in GetCustomPropertyInfo by consolidating handling of attribute constructor arguments and property filtering. The method now uses explicit property and indexer type filters when provided, and iterates all members in a single loop, improving maintainability and readability. --- ...CustomPropertyProviderGenerator.Execute.cs | 111 +++++++++--------- 1 file changed, 58 insertions(+), 53 deletions(-) diff --git a/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Execute.cs b/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Execute.cs index d033fa2bc..17445d082 100644 --- a/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Execute.cs +++ b/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Execute.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading; @@ -101,69 +102,73 @@ public static void WriteCustomPropertyProviderImplementations(SourceProductionCo { } + /// + /// Gets the values for all applicable properties of a target type. + /// + /// The annotated type. + /// The attribute to trigger generation. + /// The cancellation token for the operation. + /// The resulting values for . private static EquatableArray GetCustomPropertyInfo(INamedTypeSymbol typeSymbol, AttributeData attribute, CancellationToken token) { + string?[]? propertyNames = null; + ITypeSymbol?[]? indexerTypes = null; + + token.ThrowIfCancellationRequested(); + + // If using the attribute constructor taking explicit property names and indexer + // types, get those names to filter the properties. We'll validate them later. + if (attribute.ConstructorArguments is [ + { Kind: TypedConstantKind.Array, Values: var typedPropertyNames }, + { Kind: TypedConstantKind.Array, Values: var typedIndexerTypes }]) + { + propertyNames = [.. typedPropertyNames.Select(tc => tc.Value as string)]; + indexerTypes = [.. typedIndexerTypes.Select(tc => tc.Value as ITypeSymbol)]; + } + + token.ThrowIfCancellationRequested(); + using PooledArrayBuilder customPropertyInfo = new(); - // Make all public properties in the class bindable including ones in base type. - if (attribute.ConstructorArguments.IsDefaultOrEmpty) + // Enumerate all members of the annotated type to discover all properties + foreach (ISymbol symbol in typeSymbol.EnumerateAllMembers()) { - foreach (ISymbol symbol in typeSymbol.EnumerateAllMembers()) + token.ThrowIfCancellationRequested(); + + // Only gather public properties, and ignore overrides (we'll find the base definition instead). + // We also ignore partial property implementations, as we only care about the partial definitions. + if (symbol is not IPropertySymbol { DeclaredAccessibility: Accessibility.Public, IsOverride: false, PartialDefinitionPart: null } propertySymbol) { - // Only gather public properties, and ignore overrides (we'll find the base definition instead). - // We also ignore partial property implementations, as we only care about the partial definitions. - if (symbol is not IPropertySymbol { DeclaredAccessibility: Accessibility.Public, IsOverride: false, PartialDefinitionPart: null } propertySymbol) - { - continue; - } - - // We can only support indexers with a single parameter. - // If there's more, an analyzer will emit a warning. - if (propertySymbol.Parameters.Length > 1) - { - continue; - } - - // Gather all the info for the current property - customPropertyInfo.Add(new CustomPropertyInfo( - Name: propertySymbol.Name, - FullyQualifiedTypeName: propertySymbol.Type.GetFullyQualifiedNameWithNullabilityAnnotations(), - FullyQualifiedIndexerTypeName: propertySymbol.Parameters.FirstOrDefault()?.GetFullyQualifiedNameWithNullabilityAnnotations(), - CanRead: propertySymbol.GetMethod is { DeclaredAccessibility: Accessibility.Public }, - CanWrite: propertySymbol.SetMethod is { DeclaredAccessibility: Accessibility.Public }, - IsStatic: propertySymbol.IsStatic)); + continue; } - } - // Make specified public properties in the class bindable including ones in base type. - else if (attributeData.ConstructorArguments is - [ - { Kind: TypedConstantKind.Array, Values: [..] propertyNames }, - { Kind: TypedConstantKind.Array, Values: [..] propertyIndexerTypes } - ]) - { - for (var curSymbol = symbol; curSymbol != null; curSymbol = curSymbol.BaseType) + + // We can only support indexers with a single parameter. + // If there's more, an analyzer will emit a warning. + if (propertySymbol.Parameters.Length > 1) { - foreach (var member in curSymbol.GetMembers()) - { - if (member is IPropertySymbol propertySymbol && - member.DeclaredAccessibility == Accessibility.Public) - { - if (!propertySymbol.IsIndexer && - propertyNames.Any(p => p.Value is string value && value == propertySymbol.Name)) - { - AddProperty(propertySymbol); - } - else if (propertySymbol.IsIndexer && - // ICustomProperty only supports single indexer parameter. - propertySymbol.Parameters.Length == 1 && - propertyIndexerTypes.Any(p => p.Value is ISymbol typeSymbol && typeSymbol.Equals(propertySymbol.Parameters[0].Type, SymbolEqualityComparer.Default))) - { - AddProperty(propertySymbol); - } - } - } + continue; } + + // Ignore the current property if we have explicit filters and the property doesn't match + if ((propertySymbol.IsIndexer && indexerTypes?.Contains(propertySymbol.Parameters[0].Type, SymbolEqualityComparer.Default) is false) || + (!propertySymbol.IsIndexer && propertyNames?.Contains(propertySymbol.Name, StringComparer.Ordinal) is false)) + { + continue; + } + + // Gather all the info for the current property + customPropertyInfo.Add(new CustomPropertyInfo( + Name: propertySymbol.Name, + FullyQualifiedTypeName: propertySymbol.Type.GetFullyQualifiedNameWithNullabilityAnnotations(), + FullyQualifiedIndexerTypeName: propertySymbol.Parameters.FirstOrDefault()?.GetFullyQualifiedNameWithNullabilityAnnotations(), + CanRead: propertySymbol.GetMethod is { DeclaredAccessibility: Accessibility.Public }, + CanWrite: propertySymbol.SetMethod is { DeclaredAccessibility: Accessibility.Public }, + IsStatic: propertySymbol.IsStatic)); } + + token.ThrowIfCancellationRequested(); + + return customPropertyInfo.ToImmutable(); } } } From a4d7edae47d7d199fe75b38aa63d041c9f66f13f Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 9 Dec 2025 13:40:47 -0800 Subject: [PATCH 25/46] Refactor CustomPropertyProvider models and implementation Moved CustomPropertyInfo and CustomPropertyProviderInfo to a new Models namespace and files. Refactored WriteCustomPropertyProviderImplementations to WriteCustomPropertyProviderImplementation, improving code organization and clarity. Added detailed implementations for ICustomPropertyProvider interface methods. --- ...CustomPropertyProviderGenerator.Execute.cs | 157 ++++++++++++++++-- .../CustomPropertyProviderGenerator.cs | 3 +- .../Models/CustomPropertyInfo.cs | 21 +++ .../Models/CustomPropertyProviderInfo.cs | 30 ++++ 4 files changed, 195 insertions(+), 16 deletions(-) create mode 100644 src/Authoring/WinRT.SourceGenerator2/Models/CustomPropertyInfo.cs create mode 100644 src/Authoring/WinRT.SourceGenerator2/Models/CustomPropertyProviderInfo.cs diff --git a/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Execute.cs b/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Execute.cs index 17445d082..e4fe26228 100644 --- a/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Execute.cs +++ b/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Execute.cs @@ -98,8 +98,28 @@ public static bool IsTargetNodeValid(SyntaxNode node, CancellationToken token) UseWindowsUIXamlProjections: useWindowsUIXamlProjections); } - public static void WriteCustomPropertyProviderImplementations(SourceProductionContext context, CustomPropertyProviderInfo info) + /// + /// Emits the ICustomPropertyProvider implementation for a given annotated type. + /// + /// The value to use. + /// The input state to use. + public static void WriteCustomPropertyProviderImplementation(SourceProductionContext context, CustomPropertyProviderInfo info) { + using IndentedTextWriter writer = new(); + + // Emit the implementation on the annotated type + info.TypeHierarchy.WriteSyntax( + state: info, + writer: writer, + baseTypes: [info.FullyQualifiedCustomPropertyProviderInterfaceName], + memberCallbacks: [ + WriteCustomPropertyProviderType, + WriteCustomPropertyProviderGetCustomProperty, + WriteCustomPropertyProviderGetIndexedProperty, + WriteCustomPropertyProviderGetStringRepresentation]); + + // Add the source file for the annotated type + context.AddSource($"{info.TypeHierarchy.FullyQualifiedMetadataName}.g.cs", writer.ToString()); } /// @@ -170,18 +190,125 @@ private static EquatableArray GetCustomPropertyInfo(INamedTy return customPropertyInfo.ToImmutable(); } + + /// + /// Writes the ICustomPropertyProvider.Type implementation. + /// + /// + /// + private static void WriteCustomPropertyProviderType(CustomPropertyProviderInfo info, IndentedTextWriter writer) + { + writer.WriteLine($""" + /// + global::System.Type {info.FullyQualifiedCustomPropertyProviderInterfaceName}.Type => typeof({info.TypeHierarchy.Hierarchy[0].QualifiedName}); + """, isMultiline: true); + } + + /// + /// Writes the ICustomPropertyProvider.GetCustomProperty implementation. + /// + /// + /// + private static void WriteCustomPropertyProviderGetCustomProperty(CustomPropertyProviderInfo info, IndentedTextWriter writer) + { + writer.WriteLine($""" + /// + {info.FullyQualifiedCustomPropertyInterfaceName} {info.FullyQualifiedCustomPropertyProviderInterfaceName}.GetCustomProperty(string name) + """, isMultiline: true); + + using (writer.WriteBlock()) + { + // Fast-path if there are no non-indexer custom properties + if (!info.CustomProperties.Any(static info => info.FullyQualifiedIndexerTypeName is null)) + { + writer.WriteLine("return null;"); + + return; + } + + writer.WriteLine("return name switch"); + + using (writer.WriteBlock()) + { + // Emit a switch case for each available property + foreach (CustomPropertyInfo propertyInfo in info.CustomProperties) + { + // Skip all indexer properties + if (propertyInfo.FullyQualifiedIndexerTypeName is not null) + { + continue; + } + + // Return the cached property implementation for the current custom property + writer.WriteLine($"nameof({propertyInfo.Name}) => global::WindowsRuntime.Xaml.Generated.{info.TypeHierarchy.Hierarchy[0].QualifiedName}_{propertyInfo.Name}.Instance,"); + } + + // If there's no matching property, just return 'null' + writer.WriteLine("_ => null"); + } + } + } + + /// + /// Writes the ICustomPropertyProvider.GetIndexedProperty implementation. + /// + /// + /// + private static void WriteCustomPropertyProviderGetIndexedProperty(CustomPropertyProviderInfo info, IndentedTextWriter writer) + { + writer.WriteLine($""" + /// + {info.FullyQualifiedCustomPropertyInterfaceName} {info.FullyQualifiedCustomPropertyProviderInterfaceName}.GetIndexedProperty(string name, global::System.Type type) + """, isMultiline: true); + + using (writer.WriteBlock()) + { + // Fast-path if there are no indexer custom properties + if (!info.CustomProperties.Any(static info => info.FullyQualifiedIndexerTypeName is not null)) + { + writer.WriteLine("return null;"); + + return; + } + + // Switch over the type of all available indexer properties + foreach (CustomPropertyInfo propertyInfo in info.CustomProperties) + { + // Skip all not indexer properties + if (propertyInfo.FullyQualifiedIndexerTypeName is null) + { + continue; + } + + // If we have a match, return the cached property implementation for the current indexer + writer.WriteLine(skipIfPresent: true); + writer.WriteLine($$""" + if (type == typeof({{propertyInfo.FullyQualifiedIndexerTypeName}})) + { + return global::WindowsRuntime.Xaml.Generated.{{info.TypeHierarchy.Hierarchy[0].QualifiedName}}_{{propertyInfo.FullyQualifiedIndexerTypeName}}.Instance; + } + """, isMultiline: true); + } + + // If there's no matching property, just return 'null' + writer.WriteLine("return null;"); + } + } + + /// + /// Writes the ICustomPropertyProvider.GetStringRepresentation implementation. + /// + /// + /// + private static void WriteCustomPropertyProviderGetStringRepresentation(CustomPropertyProviderInfo info, IndentedTextWriter writer) + { + writer.WriteLine($$""" + /// + string {{info.FullyQualifiedCustomPropertyProviderInterfaceName}}.GetStringRepresentation() + { + return ToString(); + } + """, isMultiline: true); + } } -} - -internal sealed record CustomPropertyInfo( - string Name, - string FullyQualifiedTypeName, - string? FullyQualifiedIndexerTypeName, - bool CanRead, - bool CanWrite, - bool IsStatic); - -internal sealed record CustomPropertyProviderInfo( - HierarchyInfo TypeHierarchy, - EquatableArray CustomProperties, - bool UseWindowsUIXamlProjections); \ No newline at end of file +} \ No newline at end of file diff --git a/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.cs b/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.cs index af68369a1..3cd570c08 100644 --- a/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.cs +++ b/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using Microsoft.CodeAnalysis; +using WindowsRuntime.SourceGenerator.Models; namespace WindowsRuntime.SourceGenerator; @@ -23,6 +24,6 @@ public void Initialize(IncrementalGeneratorInitializationContext context) .SkipNullValues(); // Write the implementation for all annotated types - context.RegisterSourceOutput(providerInfo, Execute.WriteCustomPropertyProviderImplementations); + context.RegisterSourceOutput(providerInfo, Execute.WriteCustomPropertyProviderImplementation); } } \ No newline at end of file diff --git a/src/Authoring/WinRT.SourceGenerator2/Models/CustomPropertyInfo.cs b/src/Authoring/WinRT.SourceGenerator2/Models/CustomPropertyInfo.cs new file mode 100644 index 000000000..99b1a4aab --- /dev/null +++ b/src/Authoring/WinRT.SourceGenerator2/Models/CustomPropertyInfo.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace WindowsRuntime.SourceGenerator.Models; + +/// +/// A model representing a specific ICustomProperty to generate code for. +/// +/// The property name. +/// The fully qualified type name of the property. +/// The fully qualified type name of the indexer parameter, if applicable. +/// Whether the property can be read. +/// Whether the property can be written to. +/// Whether the property is static. +internal sealed record CustomPropertyInfo( + string Name, + string FullyQualifiedTypeName, + string? FullyQualifiedIndexerTypeName, + bool CanRead, + bool CanWrite, + bool IsStatic); diff --git a/src/Authoring/WinRT.SourceGenerator2/Models/CustomPropertyProviderInfo.cs b/src/Authoring/WinRT.SourceGenerator2/Models/CustomPropertyProviderInfo.cs new file mode 100644 index 000000000..118422c95 --- /dev/null +++ b/src/Authoring/WinRT.SourceGenerator2/Models/CustomPropertyProviderInfo.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace WindowsRuntime.SourceGenerator.Models; + +/// +/// A model describing a type that implements ICustomPropertyProvider. +/// +/// The type hierarchy info for the annotated type. +/// The custom properties to generate code for on the annotated type. +/// Whether to use Windows.UI.Xaml projections. +internal sealed record CustomPropertyProviderInfo( + HierarchyInfo TypeHierarchy, + EquatableArray CustomProperties, + bool UseWindowsUIXamlProjections) +{ + /// + /// Gets the fully qualified name of the ICustomPropertyProvider interface to use. + /// + public string FullyQualifiedCustomPropertyProviderInterfaceName => UseWindowsUIXamlProjections + ? "Windows.UI.Xaml.Data.ICustomPropertyProvider" + : "Microsoft.UI.Xaml.Data.ICustomPropertyProvider"; + + /// + /// Gets the fully qualified name of the ICustomProperty interface to use. + /// + public string FullyQualifiedCustomPropertyInterfaceName => UseWindowsUIXamlProjections + ? "Windows.UI.Xaml.Data.ICustomProperty" + : "Microsoft.UI.Xaml.Data.ICustomProperty"; +} \ No newline at end of file From fb40484d1a1c248e004d52734ce79f726be91529 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 9 Dec 2025 13:47:00 -0800 Subject: [PATCH 26/46] Refactor CustomPropertyProviderGenerator emit logic Moved the ICustomPropertyProvider implementation emission methods from Execute.cs to a new CustomPropertyProviderGenerator.Emit.cs file. Updated references to use the new Emit class for improved code organization and separation of concerns. --- .../CustomPropertyProviderGenerator.Emit.cs | 162 ++++++++++++++++++ ...CustomPropertyProviderGenerator.Execute.cs | 144 ---------------- .../CustomPropertyProviderGenerator.cs | 2 +- 3 files changed, 163 insertions(+), 145 deletions(-) create mode 100644 src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Emit.cs diff --git a/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Emit.cs b/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Emit.cs new file mode 100644 index 000000000..b98aaf08b --- /dev/null +++ b/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Emit.cs @@ -0,0 +1,162 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Linq; +using Microsoft.CodeAnalysis; +using WindowsRuntime.SourceGenerator.Models; + +namespace WindowsRuntime.SourceGenerator; + +/// +public partial class CustomPropertyProviderGenerator +{ + /// + /// Generation methods for . + /// + private static class Emit + { + /// + /// Emits the ICustomPropertyProvider implementation for a given annotated type. + /// + /// The value to use. + /// The input state to use. + public static void WriteCustomPropertyProviderImplementation(SourceProductionContext context, CustomPropertyProviderInfo info) + { + using IndentedTextWriter writer = new(); + + // Emit the implementation on the annotated type + info.TypeHierarchy.WriteSyntax( + state: info, + writer: writer, + baseTypes: [info.FullyQualifiedCustomPropertyProviderInterfaceName], + memberCallbacks: [ + WriteCustomPropertyProviderType, + WriteCustomPropertyProviderGetCustomProperty, + WriteCustomPropertyProviderGetIndexedProperty, + WriteCustomPropertyProviderGetStringRepresentation]); + + // Add the source file for the annotated type + context.AddSource($"{info.TypeHierarchy.FullyQualifiedMetadataName}.g.cs", writer.ToString()); + } + + /// + /// Writes the ICustomPropertyProvider.Type implementation. + /// + /// + /// + private static void WriteCustomPropertyProviderType(CustomPropertyProviderInfo info, IndentedTextWriter writer) + { + writer.WriteLine($""" + /// + global::System.Type {info.FullyQualifiedCustomPropertyProviderInterfaceName}.Type => typeof({info.TypeHierarchy.Hierarchy[0].QualifiedName}); + """, isMultiline: true); + } + + /// + /// Writes the ICustomPropertyProvider.GetCustomProperty implementation. + /// + /// + /// + private static void WriteCustomPropertyProviderGetCustomProperty(CustomPropertyProviderInfo info, IndentedTextWriter writer) + { + writer.WriteLine($""" + /// + {info.FullyQualifiedCustomPropertyInterfaceName} {info.FullyQualifiedCustomPropertyProviderInterfaceName}.GetCustomProperty(string name) + """, isMultiline: true); + + using (writer.WriteBlock()) + { + // Fast-path if there are no non-indexer custom properties + if (!info.CustomProperties.Any(static info => info.FullyQualifiedIndexerTypeName is null)) + { + writer.WriteLine("return null;"); + + return; + } + + writer.WriteLine("return name switch"); + + using (writer.WriteBlock()) + { + // Emit a switch case for each available property + foreach (CustomPropertyInfo propertyInfo in info.CustomProperties) + { + // Skip all indexer properties + if (propertyInfo.FullyQualifiedIndexerTypeName is not null) + { + continue; + } + + // Return the cached property implementation for the current custom property + writer.WriteLine($"nameof({propertyInfo.Name}) => global::WindowsRuntime.Xaml.Generated.{info.TypeHierarchy.Hierarchy[0].QualifiedName}_{propertyInfo.Name}.Instance,"); + } + + // If there's no matching property, just return 'null' + writer.WriteLine("_ => null"); + } + } + } + + /// + /// Writes the ICustomPropertyProvider.GetIndexedProperty implementation. + /// + /// + /// + private static void WriteCustomPropertyProviderGetIndexedProperty(CustomPropertyProviderInfo info, IndentedTextWriter writer) + { + writer.WriteLine($""" + /// + {info.FullyQualifiedCustomPropertyInterfaceName} {info.FullyQualifiedCustomPropertyProviderInterfaceName}.GetIndexedProperty(string name, global::System.Type type) + """, isMultiline: true); + + using (writer.WriteBlock()) + { + // Fast-path if there are no indexer custom properties + if (!info.CustomProperties.Any(static info => info.FullyQualifiedIndexerTypeName is not null)) + { + writer.WriteLine("return null;"); + + return; + } + + // Switch over the type of all available indexer properties + foreach (CustomPropertyInfo propertyInfo in info.CustomProperties) + { + // Skip all not indexer properties + if (propertyInfo.FullyQualifiedIndexerTypeName is null) + { + continue; + } + + // If we have a match, return the cached property implementation for the current indexer + writer.WriteLine(skipIfPresent: true); + writer.WriteLine($$""" + if (type == typeof({{propertyInfo.FullyQualifiedIndexerTypeName}})) + { + return global::WindowsRuntime.Xaml.Generated.{{info.TypeHierarchy.Hierarchy[0].QualifiedName}}_{{propertyInfo.FullyQualifiedIndexerTypeName}}.Instance; + } + """, isMultiline: true); + } + + // If there's no matching property, just return 'null' + writer.WriteLine("return null;"); + } + } + + /// + /// Writes the ICustomPropertyProvider.GetStringRepresentation implementation. + /// + /// + /// + private static void WriteCustomPropertyProviderGetStringRepresentation(CustomPropertyProviderInfo info, IndentedTextWriter writer) + { + writer.WriteLine($$""" + /// + string {{info.FullyQualifiedCustomPropertyProviderInterfaceName}}.GetStringRepresentation() + { + return ToString(); + } + """, isMultiline: true); + } + } +} \ No newline at end of file diff --git a/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Execute.cs b/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Execute.cs index e4fe26228..efb6b6419 100644 --- a/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Execute.cs +++ b/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Execute.cs @@ -98,30 +98,6 @@ public static bool IsTargetNodeValid(SyntaxNode node, CancellationToken token) UseWindowsUIXamlProjections: useWindowsUIXamlProjections); } - /// - /// Emits the ICustomPropertyProvider implementation for a given annotated type. - /// - /// The value to use. - /// The input state to use. - public static void WriteCustomPropertyProviderImplementation(SourceProductionContext context, CustomPropertyProviderInfo info) - { - using IndentedTextWriter writer = new(); - - // Emit the implementation on the annotated type - info.TypeHierarchy.WriteSyntax( - state: info, - writer: writer, - baseTypes: [info.FullyQualifiedCustomPropertyProviderInterfaceName], - memberCallbacks: [ - WriteCustomPropertyProviderType, - WriteCustomPropertyProviderGetCustomProperty, - WriteCustomPropertyProviderGetIndexedProperty, - WriteCustomPropertyProviderGetStringRepresentation]); - - // Add the source file for the annotated type - context.AddSource($"{info.TypeHierarchy.FullyQualifiedMetadataName}.g.cs", writer.ToString()); - } - /// /// Gets the values for all applicable properties of a target type. /// @@ -190,125 +166,5 @@ private static EquatableArray GetCustomPropertyInfo(INamedTy return customPropertyInfo.ToImmutable(); } - - /// - /// Writes the ICustomPropertyProvider.Type implementation. - /// - /// - /// - private static void WriteCustomPropertyProviderType(CustomPropertyProviderInfo info, IndentedTextWriter writer) - { - writer.WriteLine($""" - /// - global::System.Type {info.FullyQualifiedCustomPropertyProviderInterfaceName}.Type => typeof({info.TypeHierarchy.Hierarchy[0].QualifiedName}); - """, isMultiline: true); - } - - /// - /// Writes the ICustomPropertyProvider.GetCustomProperty implementation. - /// - /// - /// - private static void WriteCustomPropertyProviderGetCustomProperty(CustomPropertyProviderInfo info, IndentedTextWriter writer) - { - writer.WriteLine($""" - /// - {info.FullyQualifiedCustomPropertyInterfaceName} {info.FullyQualifiedCustomPropertyProviderInterfaceName}.GetCustomProperty(string name) - """, isMultiline: true); - - using (writer.WriteBlock()) - { - // Fast-path if there are no non-indexer custom properties - if (!info.CustomProperties.Any(static info => info.FullyQualifiedIndexerTypeName is null)) - { - writer.WriteLine("return null;"); - - return; - } - - writer.WriteLine("return name switch"); - - using (writer.WriteBlock()) - { - // Emit a switch case for each available property - foreach (CustomPropertyInfo propertyInfo in info.CustomProperties) - { - // Skip all indexer properties - if (propertyInfo.FullyQualifiedIndexerTypeName is not null) - { - continue; - } - - // Return the cached property implementation for the current custom property - writer.WriteLine($"nameof({propertyInfo.Name}) => global::WindowsRuntime.Xaml.Generated.{info.TypeHierarchy.Hierarchy[0].QualifiedName}_{propertyInfo.Name}.Instance,"); - } - - // If there's no matching property, just return 'null' - writer.WriteLine("_ => null"); - } - } - } - - /// - /// Writes the ICustomPropertyProvider.GetIndexedProperty implementation. - /// - /// - /// - private static void WriteCustomPropertyProviderGetIndexedProperty(CustomPropertyProviderInfo info, IndentedTextWriter writer) - { - writer.WriteLine($""" - /// - {info.FullyQualifiedCustomPropertyInterfaceName} {info.FullyQualifiedCustomPropertyProviderInterfaceName}.GetIndexedProperty(string name, global::System.Type type) - """, isMultiline: true); - - using (writer.WriteBlock()) - { - // Fast-path if there are no indexer custom properties - if (!info.CustomProperties.Any(static info => info.FullyQualifiedIndexerTypeName is not null)) - { - writer.WriteLine("return null;"); - - return; - } - - // Switch over the type of all available indexer properties - foreach (CustomPropertyInfo propertyInfo in info.CustomProperties) - { - // Skip all not indexer properties - if (propertyInfo.FullyQualifiedIndexerTypeName is null) - { - continue; - } - - // If we have a match, return the cached property implementation for the current indexer - writer.WriteLine(skipIfPresent: true); - writer.WriteLine($$""" - if (type == typeof({{propertyInfo.FullyQualifiedIndexerTypeName}})) - { - return global::WindowsRuntime.Xaml.Generated.{{info.TypeHierarchy.Hierarchy[0].QualifiedName}}_{{propertyInfo.FullyQualifiedIndexerTypeName}}.Instance; - } - """, isMultiline: true); - } - - // If there's no matching property, just return 'null' - writer.WriteLine("return null;"); - } - } - - /// - /// Writes the ICustomPropertyProvider.GetStringRepresentation implementation. - /// - /// - /// - private static void WriteCustomPropertyProviderGetStringRepresentation(CustomPropertyProviderInfo info, IndentedTextWriter writer) - { - writer.WriteLine($$""" - /// - string {{info.FullyQualifiedCustomPropertyProviderInterfaceName}}.GetStringRepresentation() - { - return ToString(); - } - """, isMultiline: true); - } } } \ No newline at end of file diff --git a/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.cs b/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.cs index 3cd570c08..e342f1612 100644 --- a/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.cs +++ b/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.cs @@ -24,6 +24,6 @@ public void Initialize(IncrementalGeneratorInitializationContext context) .SkipNullValues(); // Write the implementation for all annotated types - context.RegisterSourceOutput(providerInfo, Execute.WriteCustomPropertyProviderImplementation); + context.RegisterSourceOutput(providerInfo, Emit.WriteCustomPropertyProviderImplementation); } } \ No newline at end of file From 24000bd17f9bc9d024249a7784cb57edb0b81a0e Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 10 Dec 2025 06:37:09 -0800 Subject: [PATCH 27/46] Add CanBeBoxed property to ITypeSymbolExtensions Introduces a CanBeBoxed property to determine if a type can be boxed, and updates CustomPropertyProviderGenerator to skip properties with unboxable types. This improves robustness when generating custom property info for types with indexers or ref-like types. --- ...CustomPropertyProviderGenerator.Execute.cs | 12 +++++++-- .../Extensions/ITypeSymbolExtensions.cs | 25 ++++++++++++++++++- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Execute.cs b/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Execute.cs index efb6b6419..64ef3783c 100644 --- a/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Execute.cs +++ b/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Execute.cs @@ -145,18 +145,26 @@ private static EquatableArray GetCustomPropertyInfo(INamedTy continue; } + ITypeSymbol? indexerType = propertySymbol.Parameters.FirstOrDefault()?.Type; + // Ignore the current property if we have explicit filters and the property doesn't match - if ((propertySymbol.IsIndexer && indexerTypes?.Contains(propertySymbol.Parameters[0].Type, SymbolEqualityComparer.Default) is false) || + if ((propertySymbol.IsIndexer && indexerTypes?.Contains(indexerType, SymbolEqualityComparer.Default) is false) || (!propertySymbol.IsIndexer && propertyNames?.Contains(propertySymbol.Name, StringComparer.Ordinal) is false)) { continue; } + // If any types in the property signature cannot be boxed, we have to skip the property + if (!propertySymbol.Type.CanBeBoxed || indexerType?.CanBeBoxed is false) + { + continue; + } + // Gather all the info for the current property customPropertyInfo.Add(new CustomPropertyInfo( Name: propertySymbol.Name, FullyQualifiedTypeName: propertySymbol.Type.GetFullyQualifiedNameWithNullabilityAnnotations(), - FullyQualifiedIndexerTypeName: propertySymbol.Parameters.FirstOrDefault()?.GetFullyQualifiedNameWithNullabilityAnnotations(), + FullyQualifiedIndexerTypeName: indexerType?.GetFullyQualifiedNameWithNullabilityAnnotations(), CanRead: propertySymbol.GetMethod is { DeclaredAccessibility: Accessibility.Public }, CanWrite: propertySymbol.SetMethod is { DeclaredAccessibility: Accessibility.Public }, IsStatic: propertySymbol.IsStatic)); diff --git a/src/Authoring/WinRT.SourceGenerator2/Extensions/ITypeSymbolExtensions.cs b/src/Authoring/WinRT.SourceGenerator2/Extensions/ITypeSymbolExtensions.cs index 9e163709d..78729d426 100644 --- a/src/Authoring/WinRT.SourceGenerator2/Extensions/ITypeSymbolExtensions.cs +++ b/src/Authoring/WinRT.SourceGenerator2/Extensions/ITypeSymbolExtensions.cs @@ -5,7 +5,7 @@ using System.Collections.Generic; using Microsoft.CodeAnalysis; -#pragma warning disable CS1734 +#pragma warning disable CS1734, IDE0046 namespace WindowsRuntime.SourceGenerator; @@ -16,6 +16,29 @@ internal static class ITypeSymbolExtensions { extension(ITypeSymbol symbol) { + /// + /// Gets a value indicating whether the given can be boxed. + /// + public bool CanBeBoxed + { + get + { + // Byref-like types can't be boxed, and same for all kinds of pointers + if (symbol.IsRefLikeType || symbol.TypeKind is TypeKind.Pointer or TypeKind.FunctionPointer) + { + return false; + } + + // Type parameters with 'allows ref struct' also can't be boxed + if (symbol is ITypeParameterSymbol { AllowsRefLikeType: true }) + { + return false; + } + + return true; + } + } + /// /// Enumerates all members of a given instance, including inherited ones. /// From 2c74f20d9cd30469081ed97e1cac6853d45c49f6 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 10 Dec 2025 06:52:27 -0800 Subject: [PATCH 28/46] Add IsIndexer property to CustomPropertyInfo record Introduces the IsIndexer property to determine if a property is an indexer based on FullyQualifiedIndexerTypeName. Utilizes MemberNotNullWhen attribute for improved nullability annotations. --- .../Models/CustomPropertyInfo.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Authoring/WinRT.SourceGenerator2/Models/CustomPropertyInfo.cs b/src/Authoring/WinRT.SourceGenerator2/Models/CustomPropertyInfo.cs index 99b1a4aab..c2a567ed9 100644 --- a/src/Authoring/WinRT.SourceGenerator2/Models/CustomPropertyInfo.cs +++ b/src/Authoring/WinRT.SourceGenerator2/Models/CustomPropertyInfo.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Diagnostics.CodeAnalysis; + namespace WindowsRuntime.SourceGenerator.Models; /// @@ -18,4 +20,11 @@ internal sealed record CustomPropertyInfo( string? FullyQualifiedIndexerTypeName, bool CanRead, bool CanWrite, - bool IsStatic); + bool IsStatic) +{ + /// + /// Gets whether the current property is an indexer property. + /// + [MemberNotNullWhen(true, nameof(FullyQualifiedIndexerTypeName))] + public bool IsIndexer => FullyQualifiedIndexerTypeName is not null; +} From 710c1a628fce68fc6501a970eb348ceb665c08f8 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 10 Dec 2025 07:16:48 -0800 Subject: [PATCH 29/46] Skip static indexer properties in generator Added a check to ensure that static indexer properties are ignored during code generation, as indexers must be instance properties. This prevents invalid code from being generated for unsupported property types. --- .../CustomPropertyProviderGenerator.Execute.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Execute.cs b/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Execute.cs index 64ef3783c..9b3b2bfea 100644 --- a/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Execute.cs +++ b/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Execute.cs @@ -138,6 +138,12 @@ private static EquatableArray GetCustomPropertyInfo(INamedTy continue; } + // Indexer properties must be instance properties + if (propertySymbol.IsIndexer && propertySymbol.IsStatic) + { + continue; + } + // We can only support indexers with a single parameter. // If there's more, an analyzer will emit a warning. if (propertySymbol.Parameters.Length > 1) From ee9cddcac7a73ccd69efc86ed44900e9366fb0cd Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 10 Dec 2025 07:16:53 -0800 Subject: [PATCH 30/46] Add code generation for ICustomProperty implementation types Introduces methods to emit implementation types for ICustomProperty, generating appropriate classes for both normal and indexer custom properties. Refactors property checks to use the new IsIndexer property and updates code generation logic to support both property types. This enables more complete and correct code generation for custom property providers. --- .../CustomPropertyProviderGenerator.Emit.cs | 221 +++++++++++++++++- 1 file changed, 215 insertions(+), 6 deletions(-) diff --git a/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Emit.cs b/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Emit.cs index b98aaf08b..61e054003 100644 --- a/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Emit.cs +++ b/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Emit.cs @@ -35,6 +35,9 @@ public static void WriteCustomPropertyProviderImplementation(SourceProductionCon WriteCustomPropertyProviderGetIndexedProperty, WriteCustomPropertyProviderGetStringRepresentation]); + // Emit the additional property implementation types, if needed + WriteCustomPropertyImplementationTypes(info, writer); + // Add the source file for the annotated type context.AddSource($"{info.TypeHierarchy.FullyQualifiedMetadataName}.g.cs", writer.ToString()); } @@ -67,7 +70,7 @@ private static void WriteCustomPropertyProviderGetCustomProperty(CustomPropertyP using (writer.WriteBlock()) { // Fast-path if there are no non-indexer custom properties - if (!info.CustomProperties.Any(static info => info.FullyQualifiedIndexerTypeName is null)) + if (!info.CustomProperties.Any(static info => !info.IsIndexer)) { writer.WriteLine("return null;"); @@ -81,8 +84,7 @@ private static void WriteCustomPropertyProviderGetCustomProperty(CustomPropertyP // Emit a switch case for each available property foreach (CustomPropertyInfo propertyInfo in info.CustomProperties) { - // Skip all indexer properties - if (propertyInfo.FullyQualifiedIndexerTypeName is not null) + if (propertyInfo.IsIndexer) { continue; } @@ -112,7 +114,7 @@ private static void WriteCustomPropertyProviderGetIndexedProperty(CustomProperty using (writer.WriteBlock()) { // Fast-path if there are no indexer custom properties - if (!info.CustomProperties.Any(static info => info.FullyQualifiedIndexerTypeName is not null)) + if (!info.CustomProperties.Any(static info => info.IsIndexer)) { writer.WriteLine("return null;"); @@ -122,8 +124,7 @@ private static void WriteCustomPropertyProviderGetIndexedProperty(CustomProperty // Switch over the type of all available indexer properties foreach (CustomPropertyInfo propertyInfo in info.CustomProperties) { - // Skip all not indexer properties - if (propertyInfo.FullyQualifiedIndexerTypeName is null) + if (!propertyInfo.IsIndexer) { continue; } @@ -158,5 +159,213 @@ private static void WriteCustomPropertyProviderGetStringRepresentation(CustomPro } """, isMultiline: true); } + + /// + /// Writes the ICustomProperty implementation types. + /// + /// + /// + private static void WriteCustomPropertyImplementationTypes(CustomPropertyProviderInfo info, IndentedTextWriter writer) + { + // If we have no custom properties, we don't need to emit any additional code + if (info.CustomProperties.IsEmpty) + { + return; + } + + // All generated types go in this well-known namespace + writer.WriteLine(); + writer.WriteLine("namespace WindowsRuntime.Xaml.Generated"); + + using (writer.WriteBlock()) + { + // Using declarations for well-known types we can refer to directly + writer.WriteLine("using global::System;"); + writer.WriteLine($"using global:{info.FullyQualifiedCustomPropertyProviderInterfaceName};"); + writer.WriteLine(); + + // Write all custom property implementation types + for (int i = 0; i < info.CustomProperties.Length; i++) + { + // Ensure members are correctly separated by one line + if (i > 0) + { + writer.WriteLine(); + } + + CustomPropertyInfo propertyInfo = info.CustomProperties[i]; + + // Generate the correct implementation types for normal properties or indexer properties + if (propertyInfo.IsIndexer) + { + WriteIndexedCustomPropertyImplementationType(info, propertyInfo, writer); + } + else + { + WriteCustomPropertyImplementationType(info, propertyInfo, writer); + } + } + } + } + + /// + /// Writes a single ICustomProperty implementation type. + /// + /// + /// The input instance for the property to generate the implementation type for. + /// + private static void WriteCustomPropertyImplementationType(CustomPropertyProviderInfo info, CustomPropertyInfo propertyInfo, IndentedTextWriter writer) + { + string implementationTypeName = $"{info.TypeHierarchy.Hierarchy[0].QualifiedName}_{propertyInfo.Name}"; + + // Emit a type as follows: + // + // file sealed class : + writer.WriteLine($"file sealed class {implementationTypeName} : {info.FullyQualifiedCustomPropertyInterfaceName}"); + + using (writer.WriteBlock()) + { + // Emit all 'ICustomProperty' members for an indexer proprty, and the singleton field + writer.WriteLine($$""" + /// + /// Gets the singleton instance for this custom property. + /// + public static readonly {{implementationTypeName}} Instance = new(); + + /// + public bool CanRead => {{propertyInfo.CanRead.ToString().ToLowerInvariant()}}; + + /// + public bool CanWrite => {{propertyInfo.CanWrite.ToString().ToLowerInvariant()}}; + + /// + public string Name => "{{propertyInfo.Name}}"; + + /// + public Type Type => typeof({{propertyInfo.FullyQualifiedTypeName}}); + """, isMultiline: true); + + // Emit the normal property accessors (not supported) + writer.WriteLine(); + writer.WriteLine(""" + /// + public object GetValue(object target) + { + throw new NotSupportedException(); + } + + /// + public void SetValue(object target, object value) + { + throw new NotSupportedException(); + } + """, isMultiline: true); + + // Emit the property accessors (indexer properties can only be instance properties) + writer.WriteLine(); + writer.WriteLine($$""" + /// + public object GetIndexedValue(object target, object index) + { + return (({{info.TypeHierarchy.GetFullyQualifiedTypeName()}})target)[({{propertyInfo.FullyQualifiedIndexerTypeName}})index]; + } + + /// + public void SetIndexedValue(object target, object value, object index) + { + (({{info.TypeHierarchy.GetFullyQualifiedTypeName()}})target)[({{propertyInfo.FullyQualifiedIndexerTypeName}})index] = ({{propertyInfo.FullyQualifiedTypeName}})value; + } + """, isMultiline: true); + } + } + + /// + /// Writes a single indexed ICustomProperty implementation type. + /// + /// + /// The input instance for the property to generate the implementation type for. + /// + private static void WriteIndexedCustomPropertyImplementationType(CustomPropertyProviderInfo info, CustomPropertyInfo propertyInfo, IndentedTextWriter writer) + { + string implementationTypeName = $"{info.TypeHierarchy.Hierarchy[0].QualifiedName}_{propertyInfo.Name}"; + + // Emit the implementation type, same as above + writer.WriteLine($"file sealed class {implementationTypeName} : {info.FullyQualifiedCustomPropertyInterfaceName}"); + + using (writer.WriteBlock()) + { + // Emit all 'ICustomProperty' members for a normal proprty, and the singleton field + writer.WriteLine($$""" + /// + /// Gets the singleton instance for this custom property. + /// + public static readonly {{implementationTypeName}} Instance = new(); + + /// + public bool CanRead => {{propertyInfo.CanRead.ToString().ToLowerInvariant()}}; + + /// + public bool CanWrite => {{propertyInfo.CanWrite.ToString().ToLowerInvariant()}}; + + /// + public string Name => "{{propertyInfo.Name}}"; + + /// + public Type Type => typeof({{propertyInfo.FullyQualifiedTypeName}}); + """, isMultiline: true); + + // Emit the right dispatching code depending on whether the property is static + if (propertyInfo.IsStatic) + { + writer.WriteLine(); + writer.WriteLine($$""" + /// + public object GetValue(object target) + { + return {{info.TypeHierarchy.GetFullyQualifiedTypeName()}}.{{propertyInfo.Name}}; + } + + /// + public void SetValue(object target, object value) + { + {{info.TypeHierarchy.GetFullyQualifiedTypeName()}}.{{propertyInfo.Name}} = ({{propertyInfo.FullyQualifiedTypeName}})value; + } + """, isMultiline: true); + } + else + { + writer.WriteLine(); + writer.WriteLine($$""" + /// + public object GetValue(object target) + { + return (({{info.TypeHierarchy.GetFullyQualifiedTypeName()}})target).{{propertyInfo.Name}}; + } + + /// + public void SetValue(object target, object value) + { + (({{info.TypeHierarchy.GetFullyQualifiedTypeName()}})target).{{propertyInfo.Name}} = ({{propertyInfo.FullyQualifiedTypeName}})value; + } + """, isMultiline: true); + } + + // Emit the indexer property accessors (not supported) + writer.WriteLine(); + writer.WriteLine(""" + /// + public object GetIndexedValue(object target, object index) + { + throw new NotSupportedException(); + } + + /// + public void SetIndexedValue(object target, object value, object index) + { + throw new NotSupportedException(); + } + """, isMultiline: true); + } + } } } \ No newline at end of file From 65ee305d9e587cfd9475eddbaaf631f9b21ff6e8 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 10 Dec 2025 07:24:29 -0800 Subject: [PATCH 31/46] Add ICustomPropertyProvider test and XAML references Introduces a test for ICustomPropertyProvider interface activation and updates the project to reference Windows.UI.Xaml. Also adds a GeneratedCustomPropertyProvider implementation for testing custom property provider scenarios. --- .../ClassActivation/ClassActivation.csproj | 2 + .../ClassActivation/Program.cs | 40 +++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/src/Tests/FunctionalTests/ClassActivation/ClassActivation.csproj b/src/Tests/FunctionalTests/ClassActivation/ClassActivation.csproj index f9e366e40..72b523a37 100644 --- a/src/Tests/FunctionalTests/ClassActivation/ClassActivation.csproj +++ b/src/Tests/FunctionalTests/ClassActivation/ClassActivation.csproj @@ -6,12 +6,14 @@ x86;x64 win-x86;win-x64 $(MSBuildProjectDirectory)\..\PublishProfiles\win-$(Platform).pubxml + true + diff --git a/src/Tests/FunctionalTests/ClassActivation/Program.cs b/src/Tests/FunctionalTests/ClassActivation/Program.cs index 69b6db59a..1779b7782 100644 --- a/src/Tests/FunctionalTests/ClassActivation/Program.cs +++ b/src/Tests/FunctionalTests/ClassActivation/Program.cs @@ -3,7 +3,9 @@ using System.Runtime.InteropServices.Marshalling; using TestComponent; using TestComponentCSharp; +using Windows.UI.Xaml.Data; using WindowsRuntime.InteropServices; +using WindowsRuntime.Xaml; CustomDisposableTest customDisposableTest = new(); customDisposableTest.Dispose(); @@ -71,6 +73,28 @@ } } +TestCustomPropertyProvider testCustomPropertyProvider = new(); + +unsafe +{ + void* testCustomPropertyProviderUnknownPtr = WindowsRuntimeMarshal.ConvertToUnmanaged(testCustomPropertyProvider); + void* customPropertyProviderPtr = null; + + try + { + // We should be able to get an 'ICustomPropertyProvider' interface pointer + Marshal.ThrowExceptionForHR(Marshal.QueryInterface( + pUnk: (nint)customPropertyProviderPtr, + iid: new Guid("7C925755-3E48-42B4-8677-76372267033F"), + ppv: out *(nint*)&customPropertyProviderPtr)); + } + finally + { + WindowsRuntimeMarshal.Free(testCustomPropertyProviderUnknownPtr); + WindowsRuntimeMarshal.Free(customPropertyProviderPtr); + } +} + sealed class TestComposable : Composable { } @@ -93,6 +117,22 @@ partial interface IClassicComAction void Invoke(); } +[GeneratedCustomPropertyProvider] +sealed partial class TestCustomPropertyProvider : ICustomPropertyProvider +{ + public string Text => "Hello"; + + public int Number { get; set; } + + public int this[string key] + { + get => 0; + set { } + } + + public static string Info { get; set; } +} + /* // new RCW / Factory activation var instance = new Class(); From b66ae97b081ef406996956290e9637516a360dd3 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 11 Dec 2025 09:21:05 -0800 Subject: [PATCH 32/46] Add diagnostic descriptors for custom property provider Introduces DiagnosticDescriptors.cs containing DiagnosticDescriptor instances for errors related to the [GeneratedCustomPropertyProvider] attribute, including invalid target types, missing partial modifiers, and unavailable interface types. --- .../Diagnostics/DiagnosticDescriptors.cs | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 src/Authoring/WinRT.SourceGenerator2/Diagnostics/DiagnosticDescriptors.cs diff --git a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/DiagnosticDescriptors.cs b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/DiagnosticDescriptors.cs new file mode 100644 index 000000000..662958a67 --- /dev/null +++ b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/DiagnosticDescriptors.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.CodeAnalysis; + +namespace WindowsRuntime.SourceGenerator.Diagnostics; + +/// +/// A container for all instances for errors reported by analyzers in this project. +/// +internal static partial class DiagnosticDescriptors +{ + /// + /// Gets a for an invalid target type for [GeneratedCustomPropertyProvider]. + /// + public static readonly DiagnosticDescriptor GeneratedCustomPropertyProviderInvalidTargetType = new( + id: "CSWINRT2000", + title: "Invalid '[GeneratedCustomPropertyProvider]' target type", + messageFormat: """The type '{0}' is not a valid target for '[GeneratedCustomPropertyProvider]': it must be a 'class' or 'struct' type, and it can't be 'static', 'abstract', or 'ref'""", + category: "WindowsRuntime.SourceGenerator", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "Types annotated with '[GeneratedCustomPropertyProvider]' must be 'class' or 'struct' types, and they can't be 'static', 'abstract', or 'ref'.", + helpLinkUri: "https://github.com/microsoft/CsWinRT"); + + /// + /// Gets a for a target type for [GeneratedCustomPropertyProvider] missing . + /// + public static readonly DiagnosticDescriptor GeneratedCustomPropertyProviderMissingPartialModifier = new( + id: "CSWINRT2001", + title: "Missing 'partial' for '[GeneratedCustomPropertyProvider]' target type", + messageFormat: """The type '{0}' (or one of its containing types) is missing the 'partial' modifier, which is required to be used as a target for '[GeneratedCustomPropertyProvider]'""", + category: "WindowsRuntime.SourceGenerator", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "Types annotated with '[GeneratedCustomPropertyProvider]' must be marked as 'partial' across their whole type hierarchy.", + helpLinkUri: "https://github.com/microsoft/CsWinRT"); + + /// + /// Gets a for when [GeneratedCustomPropertyProvider] can't resolve the interface type. + /// + public static readonly DiagnosticDescriptor GeneratedCustomPropertyProviderNoAvailableInterfaceType = new( + id: "CSWINRT2002", + title: "'ICustomPropertyProvider' interface type not available", + messageFormat: """The 'ICustomPropertyProvider' interface is not available in the compilation, but it is required to use '[GeneratedCustomPropertyProvider]' (make sure to either reference 'WindowsAppSDK.WinUI' or set the 'UseUwp' property in your .csproj file)""", + category: "WindowsRuntime.SourceGenerator", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "Using '[GeneratedCustomPropertyProvider]' requires the 'ICustomPropertyProvider' interface type to be available in the compilation, which can be done by either referencing 'WindowsAppSDK.WinUI' or by setting the 'UseUwp' property in the .csproj file.", + helpLinkUri: "https://github.com/microsoft/CsWinRT"); +} \ No newline at end of file From 6a9d8c6f872d068274d082f258ebf09affd01849 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 11 Dec 2025 09:21:12 -0800 Subject: [PATCH 33/46] Add analyzer release tracking and test implementation Introduced AnalyzerReleases.Shipped.md and AnalyzerReleases.Unshipped.md for Roslyn analyzer release tracking. Updated WinRT.SourceGenerator2.csproj to include these files as additional resources. Added Test.cs to WinRT.Runtime2/Xaml.Attributes with ICustomPropertyProvider and ICustomProperty interfaces, a sample Test class, and generated property implementation for testing source generator functionality. --- .../AnalyzerReleases.Shipped.md | 11 +++++++++++ .../AnalyzerReleases.Unshipped.md | 6 ++++++ .../WinRT.SourceGenerator2.csproj | 6 ++++++ 3 files changed, 23 insertions(+) create mode 100644 src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Shipped.md create mode 100644 src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Unshipped.md diff --git a/src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Shipped.md b/src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Shipped.md new file mode 100644 index 000000000..cd58f0353 --- /dev/null +++ b/src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Shipped.md @@ -0,0 +1,11 @@ +; Shipped analyzer releases +; https://github.com/dotnet/roslyn-analyzers/blob/master/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md + +## Release 3.0.0 + +### New Rules +Rule ID | Category | Severity | Notes +--------|----------|----------|------- +CSWINRT2000 | WindowsRuntime.SourceGenerator | Error | Invalid '[GeneratedCustomPropertyProvider]' target type +CSWINRT2001 | WindowsRuntime.SourceGenerator | Error | Missing 'partial' for '[GeneratedCustomPropertyProvider]' target type +CSWINRT2002 | WindowsRuntime.SourceGenerator | Error | 'ICustomPropertyProvider' interface type not available \ No newline at end of file diff --git a/src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Unshipped.md b/src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Unshipped.md new file mode 100644 index 000000000..6640189c3 --- /dev/null +++ b/src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Unshipped.md @@ -0,0 +1,6 @@ +; Unshipped analyzer release +; https://github.com/dotnet/roslyn-analyzers/blob/master/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md + +### New Rules +Rule ID | Category | Severity | Notes +--------|----------|----------|------- diff --git a/src/Authoring/WinRT.SourceGenerator2/WinRT.SourceGenerator2.csproj b/src/Authoring/WinRT.SourceGenerator2/WinRT.SourceGenerator2.csproj index 7485b93f6..72da65c21 100644 --- a/src/Authoring/WinRT.SourceGenerator2/WinRT.SourceGenerator2.csproj +++ b/src/Authoring/WinRT.SourceGenerator2/WinRT.SourceGenerator2.csproj @@ -69,6 +69,12 @@ $(SolutionDir)WinRT.Runtime\key.snk + + + + + + From 3783382a8c3de79f0bad5722afb697d0a7d48e65 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 11 Dec 2025 09:31:12 -0800 Subject: [PATCH 34/46] Add analyzer for GeneratedCustomPropertyProvider targets Introduces GeneratedCustomPropertyProviderTargetTypeAnalyzer to validate that types with [GeneratedCustomPropertyProvider] are valid targets. The analyzer checks for correct type kind, required modifiers, and reports diagnostics for invalid usage. --- ...ustomPropertyProviderTargetTypeAnalyzer.cs | 77 +++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/GeneratedCustomPropertyProviderTargetTypeAnalyzer.cs diff --git a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/GeneratedCustomPropertyProviderTargetTypeAnalyzer.cs b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/GeneratedCustomPropertyProviderTargetTypeAnalyzer.cs new file mode 100644 index 000000000..38e3078fb --- /dev/null +++ b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/GeneratedCustomPropertyProviderTargetTypeAnalyzer.cs @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace WindowsRuntime.SourceGenerator.Diagnostics; + +/// +/// A diagnostic analyzer that validates target types for [GeneratedCustomPropertyProvider]. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class GeneratedCustomPropertyProviderTargetTypeAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = [ + DiagnosticDescriptors.GeneratedCustomPropertyProviderInvalidTargetType, + DiagnosticDescriptors.GeneratedCustomPropertyProviderMissingPartialModifier]; + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static context => + { + // Get the '[GeneratedCustomPropertyProvider]' symbol + if (context.Compilation.GetTypeByMetadataName("WindowsRuntime.Xaml.GeneratedCustomPropertyProviderAttribute") is not { } attributeType) + { + return; + } + + context.RegisterSymbolAction(context => + { + // Only classes and structs can be targets of the attribute + if (context.Symbol is not INamedTypeSymbol { TypeKind: TypeKind.Class or TypeKind.Struct } typeSymbol) + { + return; + } + + // Immediately bail if the type doesn't have the attribute + if (!typeSymbol.HasAttributeWithType(attributeType)) + { + return; + } + + // If the type is static, abstract, or 'ref', it isn't valid + if (typeSymbol.IsAbstract || typeSymbol.IsStatic || typeSymbol.IsRefLikeType) + { + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.GeneratedCustomPropertyProviderInvalidTargetType, + typeSymbol.Locations.FirstOrDefault(), + typeSymbol)); + } + + // Try to get a syntax reference for the symbol, to resolve the syntax node for it + if (typeSymbol.DeclaringSyntaxReferences.FirstOrDefault() is SyntaxReference syntaxReference) + { + SyntaxNode typeNode = syntaxReference.GetSyntax(context.CancellationToken); + + // If there's no 'partial' modifier in the type hierarchy, the target type isn't valid + if (!((MemberDeclarationSyntax)typeNode).IsPartialAndWithinPartialTypeHierarchy) + { + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.GeneratedCustomPropertyProviderMissingPartialModifier, + typeSymbol.Locations.FirstOrDefault(), + typeSymbol)); + } + } + }, SymbolKind.NamedType); + }); + } +} \ No newline at end of file From 9117d59a2c25b193903b917f01554850734d14cb Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 11 Dec 2025 09:35:23 -0800 Subject: [PATCH 35/46] Add analyzer for missing ICustomPropertyProvider interface Introduces GeneratedCustomPropertyProviderNoAvailableInterfaceTypeAnalyzer to report diagnostics when [GeneratedCustomPropertyProvider] is used but no ICustomPropertyProvider interface is available in the compilation. --- ...roviderNoAvailableInterfaceTypeAnalyzer.cs | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/GeneratedCustomPropertyProviderNoAvailableInterfaceTypeAnalyzer.cs diff --git a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/GeneratedCustomPropertyProviderNoAvailableInterfaceTypeAnalyzer.cs b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/GeneratedCustomPropertyProviderNoAvailableInterfaceTypeAnalyzer.cs new file mode 100644 index 000000000..e9b2327bf --- /dev/null +++ b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/GeneratedCustomPropertyProviderNoAvailableInterfaceTypeAnalyzer.cs @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace WindowsRuntime.SourceGenerator.Diagnostics; + +/// +/// A diagnostic analyzer that validates when [GeneratedCustomPropertyProvider] is used but no interface is available. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class GeneratedCustomPropertyProviderNoAvailableInterfaceTypeAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = [DiagnosticDescriptors.GeneratedCustomPropertyProviderNoAvailableInterfaceType]; + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static context => + { + // Get the '[GeneratedCustomPropertyProvider]' symbol + if (context.Compilation.GetTypeByMetadataName("WindowsRuntime.Xaml.GeneratedCustomPropertyProviderAttribute") is not { } attributeType) + { + return; + } + + // Try to get any 'ICustomPropertyProvider' symbol + INamedTypeSymbol? windowsUIXamlCustomPropertyProviderType = context.Compilation.GetTypeByMetadataName("Windows.UI.Xaml.Data.ICustomPropertyProvider"); + INamedTypeSymbol? microsoftUIXamlCustomPropertyProviderType = context.Compilation.GetTypeByMetadataName("Microsoft.UI.Xaml.Data.ICustomPropertyProvider"); + + // If we have either of them, we'll never need to report any diagnostics + if (windowsUIXamlCustomPropertyProviderType is not null || microsoftUIXamlCustomPropertyProviderType is not null) + { + return; + } + + context.RegisterSymbolAction(context => + { + // Only classes and structs can be targets of the attribute + if (context.Symbol is not INamedTypeSymbol { TypeKind: TypeKind.Class or TypeKind.Struct } typeSymbol) + { + return; + } + + // Emit a diagnostic if the type has the attribute, as it can't be used now + if (typeSymbol.HasAttributeWithType(attributeType)) + { + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.GeneratedCustomPropertyProviderNoAvailableInterfaceType, + typeSymbol.Locations.FirstOrDefault(), + typeSymbol)); + } + }, SymbolKind.NamedType); + }); + } +} \ No newline at end of file From 7f134b1e10b532667bea46c631b26665a3e60d58 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 16 Dec 2025 20:18:53 -0800 Subject: [PATCH 36/46] Add SourceGenerator2Test project to solution Introduced a new test project, SourceGenerator2Test, targeting .NET 10.0 with relevant package and project references. Updated cswinrt.slnx to include the new test project. --- .../SourceGenerator2Test.csproj | 18 ++++++++++++++++++ src/cswinrt.slnx | 1 + 2 files changed, 19 insertions(+) create mode 100644 src/Tests/SourceGenerator2Test/SourceGenerator2Test.csproj diff --git a/src/Tests/SourceGenerator2Test/SourceGenerator2Test.csproj b/src/Tests/SourceGenerator2Test/SourceGenerator2Test.csproj new file mode 100644 index 000000000..79b84650a --- /dev/null +++ b/src/Tests/SourceGenerator2Test/SourceGenerator2Test.csproj @@ -0,0 +1,18 @@ + + + net10.0 + + + + + + + + + + + + + + + diff --git a/src/cswinrt.slnx b/src/cswinrt.slnx index 7db15b0a3..fcdc36297 100644 --- a/src/cswinrt.slnx +++ b/src/cswinrt.slnx @@ -185,6 +185,7 @@ + From dc0cd5c71de29405ec4e00cb43420bf099a12e97 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 16 Dec 2025 20:33:55 -0800 Subject: [PATCH 37/46] Suppress CS8620 warning in generator file Added suppression for CS8620 compiler warning in CustomPropertyProviderGenerator.Execute.cs. This is a temporary measure until the underlying compiler warning is resolved. --- .../CustomPropertyProviderGenerator.Execute.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Execute.cs b/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Execute.cs index 9b3b2bfea..b50dc95ce 100644 --- a/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Execute.cs +++ b/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Execute.cs @@ -10,7 +10,7 @@ using Microsoft.CodeAnalysis.CSharp.Syntax; using WindowsRuntime.SourceGenerator.Models; -#pragma warning disable IDE0046 +#pragma warning disable CS8620, IDE0046 // TODO: remove 'CS8620' suppression when compiler warning is fixed namespace WindowsRuntime.SourceGenerator; From b7b78d342964c0b6a0023fe002ddaacb290e57ac Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 16 Dec 2025 20:34:03 -0800 Subject: [PATCH 38/46] Set VersionOverride for CSharp.Workspaces package Added VersionOverride="5.0.0" to the Microsoft.CodeAnalysis.CSharp.Workspaces package reference in the test project to ensure a specific version is used. --- src/Tests/SourceGenerator2Test/SourceGenerator2Test.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tests/SourceGenerator2Test/SourceGenerator2Test.csproj b/src/Tests/SourceGenerator2Test/SourceGenerator2Test.csproj index 79b84650a..baf75d37f 100644 --- a/src/Tests/SourceGenerator2Test/SourceGenerator2Test.csproj +++ b/src/Tests/SourceGenerator2Test/SourceGenerator2Test.csproj @@ -7,7 +7,7 @@ - + From 0ca9b0ebeff149681fff059d1829ebaa3deced6b Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 16 Dec 2025 20:34:07 -0800 Subject: [PATCH 39/46] Add MSTest package to dependencies Included MSTest version 4.0.2 in Directory.Packages.props to ensure it is available as a dependency for the project. --- src/Directory.Packages.props | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 5b1aa5b5c..3d0a6d9dc 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -32,6 +32,7 @@ + From 8f6601df409be79ad13cc91458ab54d5e7cabd18 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 16 Dec 2025 20:34:41 -0800 Subject: [PATCH 40/46] Add AssemblyInfo with Parallelize attribute to tests Introduces AssemblyInfo.cs to the SourceGenerator2Test project and enables parallel test execution using the Parallelize attribute. --- src/Tests/SourceGenerator2Test/Properties/AssemblyInfo.cs | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 src/Tests/SourceGenerator2Test/Properties/AssemblyInfo.cs diff --git a/src/Tests/SourceGenerator2Test/Properties/AssemblyInfo.cs b/src/Tests/SourceGenerator2Test/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..1152878de --- /dev/null +++ b/src/Tests/SourceGenerator2Test/Properties/AssemblyInfo.cs @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +[assembly: Parallelize] \ No newline at end of file From 001f450ee91d2870353714c7002185dd496b7963 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 16 Dec 2025 20:42:08 -0800 Subject: [PATCH 41/46] Add CSharpGeneratorTest helper for source generator tests Introduces a generic static helper class to facilitate testing of source generators. Provides methods to verify generated sources, create compilations, and run generators with specified language versions, streamlining the process of writing and maintaining source generator tests. --- .../CSharpGeneratorTest{TGenerator}.cs | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 src/Tests/SourceGenerator2Test/Helpers/CSharpGeneratorTest{TGenerator}.cs diff --git a/src/Tests/SourceGenerator2Test/Helpers/CSharpGeneratorTest{TGenerator}.cs b/src/Tests/SourceGenerator2Test/Helpers/CSharpGeneratorTest{TGenerator}.cs new file mode 100644 index 000000000..0fcbd29c8 --- /dev/null +++ b/src/Tests/SourceGenerator2Test/Helpers/CSharpGeneratorTest{TGenerator}.cs @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.IO; +using System.Linq; +using Basic.Reference.Assemblies; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace WindowsRuntime.SourceGenerator.Tests.Helpers; + +/// +/// A helper type to run source generator tests. +/// +/// The type of generator to test. +internal static class CSharpGeneratorTest + where TGenerator : IIncrementalGenerator, new() +{ + /// + /// Verifies the resulting sources produced by a source generator. + /// + /// The input source to process. + /// The expected source to be generated. + /// The language version to use to run the test. + public static void VerifySources(string source, (string Filename, string Source) result, LanguageVersion languageVersion = LanguageVersion.CSharp14) + { + RunGenerator(source, out Compilation compilation, out ImmutableArray diagnostics, languageVersion); + + // Ensure that no diagnostics were generated + CollectionAssert.AreEquivalent((Diagnostic[])[], diagnostics); + + // Update the assembly version using the version from the assembly of the input generators. + // This allows the tests to not need updates whenever the version of the generators changes. + string expectedText = result.Source.Replace("", $"\"{typeof(TGenerator).Assembly.GetName().Version}\""); + string actualText = compilation.SyntaxTrees.Single(tree => Path.GetFileName(tree.FilePath) == result.Filename).ToString(); + + Assert.AreEqual(expectedText, actualText); + } + + /// + /// Creates a compilation from a given source. + /// + /// The input source to process. + /// The language version to use to run the test. + /// The resulting object. + private static CSharpCompilation CreateCompilation(string source, LanguageVersion languageVersion = LanguageVersion.CSharp12) + { + // Get all assembly references for the .NET TFM and 'WinRT.Runtime' + IEnumerable metadataReferences = + [ + .. Net100.References.All, + MetadataReference.CreateFromFile(typeof(WindowsRuntimeObject).Assembly.Location), + ]; + + // Parse the source text + SyntaxTree sourceTree = CSharpSyntaxTree.ParseText( + source, + CSharpParseOptions.Default.WithLanguageVersion(languageVersion)); + + // Create the original compilation + return CSharpCompilation.Create( + "original", + [sourceTree], + metadataReferences, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, allowUnsafe: true)); + } + + /// + /// Runs a generator and gathers the output results. + /// + /// The input source to process. + /// + /// + /// The language version to use to run the test. + private static void RunGenerator( + string source, + out Compilation compilation, + out ImmutableArray diagnostics, + LanguageVersion languageVersion = LanguageVersion.CSharp12) + { + Compilation originalCompilation = CreateCompilation(source, languageVersion); + + // Create the generator driver with the D2D shader generator + GeneratorDriver driver = CSharpGeneratorDriver.Create(new TGenerator()).WithUpdatedParseOptions(originalCompilation.SyntaxTrees.First().Options); + + // Run all source generators on the input source code + _ = driver.RunGeneratorsAndUpdateCompilation(originalCompilation, out compilation, out diagnostics); + } +} \ No newline at end of file From 29292037e12845b4a681ab72fe94b89758bfe4f6 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 16 Dec 2025 20:46:02 -0800 Subject: [PATCH 42/46] Add test for CustomPropertyProviderGenerator Introduces a unit test for the CustomPropertyProviderGenerator using a simple class with properties and an indexer. This test verifies the source generator's output for a class annotated with [GeneratedCustomPropertyProvider]. --- .../Test_CustomPropertyProviderGenerator.cs | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 src/Tests/SourceGenerator2Test/Test_CustomPropertyProviderGenerator.cs diff --git a/src/Tests/SourceGenerator2Test/Test_CustomPropertyProviderGenerator.cs b/src/Tests/SourceGenerator2Test/Test_CustomPropertyProviderGenerator.cs new file mode 100644 index 000000000..3a10b898b --- /dev/null +++ b/src/Tests/SourceGenerator2Test/Test_CustomPropertyProviderGenerator.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using WindowsRuntime.SourceGenerator.Tests.Helpers; + +namespace WindowsRuntime.SourceGenerator.Tests; + +[TestClass] +public class Test_CustomPropertyProviderGenerator +{ + [TestMethod] + public async Task SimpleShader_ComputeShader() + { + const string source = """ + using WindowsRuntime.Xaml; + + namespace MyNamespace; + + [GeneratedCustomPropertyProvider] + public partial class MyClass + { + public string Name => ""; + + public int Age { get; set; } + + public int this[int index] + { + get => 0; + set { } + } + } + """; + + const string result = """" + + """"; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyClass.g.cs", result)); + } +} \ No newline at end of file From f26704a41a220c7b02c63f95e27e5c3b71160ffe Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 16 Dec 2025 20:51:19 -0800 Subject: [PATCH 43/46] Add custom CSharpAnalyzerTest helper for analyzer tests Introduces a generic CSharpAnalyzerTest class to facilitate analyzer testing with configurable C# language version and unsafe block support. This helper streamlines test setup and allows specifying reference assemblies and additional references. --- .../Helpers/CSharpAnalyzerTest{TAnalyzer}.cs | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 src/Tests/SourceGenerator2Test/Helpers/CSharpAnalyzerTest{TAnalyzer}.cs diff --git a/src/Tests/SourceGenerator2Test/Helpers/CSharpAnalyzerTest{TAnalyzer}.cs b/src/Tests/SourceGenerator2Test/Helpers/CSharpAnalyzerTest{TAnalyzer}.cs new file mode 100644 index 000000000..c1f202b04 --- /dev/null +++ b/src/Tests/SourceGenerator2Test/Helpers/CSharpAnalyzerTest{TAnalyzer}.cs @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Testing; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Testing; + +namespace WindowsRuntime.SourceGenerator.Tests.Helpers; + +/// +/// A custom that uses a specific C# language version to parse code. +/// +/// The type of the analyzer to test. +internal sealed class CSharpAnalyzerTest : CSharpAnalyzerTest + where TAnalyzer : DiagnosticAnalyzer, new() +{ + /// + /// Whether to enable unsafe blocks. + /// + private readonly bool _allowUnsafeBlocks; + + /// + /// The C# language version to use to parse code. + /// + private readonly LanguageVersion _languageVersion; + + /// + /// Creates a new instance with the specified paramaters. + /// + /// Whether to enable unsafe blocks. + /// The C# language version to use to parse code. + private CSharpAnalyzerTest(bool allowUnsafeBlocks, LanguageVersion languageVersion) + { + _allowUnsafeBlocks = allowUnsafeBlocks; + _languageVersion = languageVersion; + } + + /// + protected override CompilationOptions CreateCompilationOptions() + { + return new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, allowUnsafe: _allowUnsafeBlocks); + } + + /// + protected override ParseOptions CreateParseOptions() + { + return new CSharpParseOptions(_languageVersion, DocumentationMode.Diagnose); + } + + /// + /// The source code to analyze. + /// Whether to enable unsafe blocks. + /// The language version to use to run the test. + public static Task VerifyAnalyzerAsync( + string source, + bool allowUnsafeBlocks = true, + LanguageVersion languageVersion = LanguageVersion.CSharp14) + { + CSharpAnalyzerTest test = new(allowUnsafeBlocks, languageVersion) { TestCode = source }; + + test.TestState.ReferenceAssemblies = ReferenceAssemblies.Net.Net80; // TODO: use the .NET 10 ref assemblies + test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(WindowsRuntimeObject).Assembly.Location)); + + return test.RunAsync(CancellationToken.None); + } +} \ No newline at end of file From f1ce10319dc4f0860bc86d6aa6f7375c06f3434d Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 16 Dec 2025 20:51:23 -0800 Subject: [PATCH 44/46] Refactor RunGenerator parameter order and default Reordered parameters in RunGenerator to place languageVersion before out parameters and removed its default value. Updated VerifySources to match the new signature. --- .../Helpers/CSharpGeneratorTest{TGenerator}.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Tests/SourceGenerator2Test/Helpers/CSharpGeneratorTest{TGenerator}.cs b/src/Tests/SourceGenerator2Test/Helpers/CSharpGeneratorTest{TGenerator}.cs index 0fcbd29c8..1c70650f2 100644 --- a/src/Tests/SourceGenerator2Test/Helpers/CSharpGeneratorTest{TGenerator}.cs +++ b/src/Tests/SourceGenerator2Test/Helpers/CSharpGeneratorTest{TGenerator}.cs @@ -27,7 +27,7 @@ internal static class CSharpGeneratorTest /// The language version to use to run the test. public static void VerifySources(string source, (string Filename, string Source) result, LanguageVersion languageVersion = LanguageVersion.CSharp14) { - RunGenerator(source, out Compilation compilation, out ImmutableArray diagnostics, languageVersion); + RunGenerator(source, languageVersion, out Compilation compilation, out ImmutableArray diagnostics); // Ensure that no diagnostics were generated CollectionAssert.AreEquivalent((Diagnostic[])[], diagnostics); @@ -72,14 +72,14 @@ private static CSharpCompilation CreateCompilation(string source, LanguageVersio /// Runs a generator and gathers the output results. /// /// The input source to process. + /// The language version to use to run the test. /// /// - /// The language version to use to run the test. private static void RunGenerator( string source, + LanguageVersion languageVersion, out Compilation compilation, - out ImmutableArray diagnostics, - LanguageVersion languageVersion = LanguageVersion.CSharp12) + out ImmutableArray diagnostics) { Compilation originalCompilation = CreateCompilation(source, languageVersion); From dd62b22f352c79d9a0fc49541c7b2a198353e888 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 16 Dec 2025 20:57:45 -0800 Subject: [PATCH 45/46] Add tests for GeneratedCustomPropertyProvider analyzer Introduces unit tests for the GeneratedCustomPropertyProviderTargetTypeAnalyzer to verify correct diagnostics for valid and invalid target types, partial type requirements, and type hierarchy scenarios. --- ...ustomPropertyProviderTargetTypeAnalyzer.cs | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 src/Tests/SourceGenerator2Test/Test_GeneratedCustomPropertyProviderTargetTypeAnalyzer.cs diff --git a/src/Tests/SourceGenerator2Test/Test_GeneratedCustomPropertyProviderTargetTypeAnalyzer.cs b/src/Tests/SourceGenerator2Test/Test_GeneratedCustomPropertyProviderTargetTypeAnalyzer.cs new file mode 100644 index 000000000..07550f0b6 --- /dev/null +++ b/src/Tests/SourceGenerator2Test/Test_GeneratedCustomPropertyProviderTargetTypeAnalyzer.cs @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using WindowsRuntime.SourceGenerator.Diagnostics; +using WindowsRuntime.SourceGenerator.Tests.Helpers; + +namespace WindowsRuntime.SourceGenerator.Tests; + +[TestClass] +public class Test_GeneratedCustomPropertyProviderTargetTypeAnalyzer +{ + [TestMethod] + [DataRow("class")] + [DataRow("struct")] + public async Task ValidTargetType_DoesNotWarn(string modifier) + { + string source = $$""" + using WindowsRuntime.Xaml; + + [GeneratedCustomPropertyProvider] + public partial {{modifier}} MyType; + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source); + } + + [TestMethod] + [DataRow("abstract class")] + [DataRow("static class")] + [DataRow("static struct")] + [DataRow("ref struct")] + public async Task InvalidTargetType_Warns(string modifiers) + { + string source = $$""" + using WindowsRuntime.Xaml; + + [{|CSWINRT2000:GeneratedCustomPropertyProvider|}] + public {{modifiers}} MyType; + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source); + } + + [TestMethod] + [DataRow("class")] + [DataRow("struct")] + public async Task TypeNotPartial_Warns(string modifier) + { + string source = $$""" + using WindowsRuntime.Xaml; + + [{|CSWINRT2001:GeneratedCustomPropertyProvider|}] + public {{modifier}} MyType; + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source); + } + + [TestMethod] + [DataRow("class")] + [DataRow("struct")] + public async Task TypeNotInPartialTypeHierarchy_Warns(string modifier) + { + string source = $$""" + using WindowsRuntime.Xaml; + + public class ParentType + { + [{|CSWINRT2001:GeneratedCustomPropertyProvider|}] + public partial {{modifier}} MyType; + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source); + } +} \ No newline at end of file From 2ce2c780ef0c1450c78f1652f23896093f2e70be Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 17 Dec 2025 14:24:08 -0800 Subject: [PATCH 46/46] Add .NET 10 reference assemblies support for tests Introduces ReferenceAssembliesExtensions to provide .NET 10 reference assemblies and updates CSharpAnalyzerTest to use Net100. This enables testing against .NET 10 until official support is available in Roslyn SDK. --- .../ReferenceAssembliesExtensions.cs | 30 +++++++++++++++++++ .../Helpers/CSharpAnalyzerTest{TAnalyzer}.cs | 2 +- 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 src/Tests/SourceGenerator2Test/Extensions/ReferenceAssembliesExtensions.cs diff --git a/src/Tests/SourceGenerator2Test/Extensions/ReferenceAssembliesExtensions.cs b/src/Tests/SourceGenerator2Test/Extensions/ReferenceAssembliesExtensions.cs new file mode 100644 index 000000000..79836a3ad --- /dev/null +++ b/src/Tests/SourceGenerator2Test/Extensions/ReferenceAssembliesExtensions.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.IO; +using Microsoft.CodeAnalysis.Testing; + +namespace WindowsRuntime.SourceGenerator; + +/// +/// Extensions for the type. +/// +internal static class ReferenceAssembliesExtensions +{ + /// + /// The lazy-loaded instance for .NET 10 assemblies. + /// + private static readonly Lazy Net100 = new(static () => new( + targetFramework: "net10.0", + referenceAssemblyPackage: new PackageIdentity("Microsoft.NETCore.App.Ref", "10.0.1"), + referenceAssemblyPath: Path.Combine("ref", "net10.0"))); + + extension(ReferenceAssemblies.Net) + { + /// + /// Gets the value for .NET 10 reference assemblies. + /// + public static ReferenceAssemblies Net100 => Net100.Value; // TODO: remove when https://github.com/dotnet/roslyn-sdk/issues/1233 is resolved + } +} \ No newline at end of file diff --git a/src/Tests/SourceGenerator2Test/Helpers/CSharpAnalyzerTest{TAnalyzer}.cs b/src/Tests/SourceGenerator2Test/Helpers/CSharpAnalyzerTest{TAnalyzer}.cs index c1f202b04..016020c7e 100644 --- a/src/Tests/SourceGenerator2Test/Helpers/CSharpAnalyzerTest{TAnalyzer}.cs +++ b/src/Tests/SourceGenerator2Test/Helpers/CSharpAnalyzerTest{TAnalyzer}.cs @@ -62,7 +62,7 @@ public static Task VerifyAnalyzerAsync( { CSharpAnalyzerTest test = new(allowUnsafeBlocks, languageVersion) { TestCode = source }; - test.TestState.ReferenceAssemblies = ReferenceAssemblies.Net.Net80; // TODO: use the .NET 10 ref assemblies + test.TestState.ReferenceAssemblies = ReferenceAssemblies.Net.Net100; test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(WindowsRuntimeObject).Assembly.Location)); return test.RunAsync(CancellationToken.None);