From d118b2346e5afe70b9cd6a56a1d57d8503929ff8 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 7 Dec 2025 18:29:23 -0800 Subject: [PATCH 01/51] 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 68861bd3e2668dd7d6b11fee57af49861838301f Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 7 Dec 2025 18:29:40 -0800 Subject: [PATCH 02/51] 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 4c4161ea417de5142bd5d0d0ff4ac31e15b155af Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 7 Dec 2025 18:30:40 -0800 Subject: [PATCH 03/51] 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 8c4b8ff7c..e96396084 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 c4f9f4717b5e6a80ce6ee3d2f296b4c7ba326a4b Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 7 Dec 2025 21:53:54 -0800 Subject: [PATCH 04/51] 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 9667ab92acfdbaeaeac6ace906d3e6131ce929f6 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 7 Dec 2025 21:57:31 -0800 Subject: [PATCH 05/51] 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 7dd884fcdb147dc264248736c15903aad17776c1 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 8 Dec 2025 10:35:59 -0800 Subject: [PATCH 06/51] 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 d27681471265c084966ab137cba3a00e3e9aee71 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 8 Dec 2025 10:44:39 -0800 Subject: [PATCH 07/51] 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 fff5a53c19ebeaba36d4dccaa24a328130a4004f Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 8 Dec 2025 15:11:37 -0800 Subject: [PATCH 08/51] 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 8ca59b8cbfe5d843096b0734a7d04866fff1e135 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 8 Dec 2025 15:17:30 -0800 Subject: [PATCH 09/51] 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 1a0d9d8be0016f8c66b0b78f6652603633b1c916 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 8 Dec 2025 15:23:33 -0800 Subject: [PATCH 10/51] 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. --- .../WinRT.SourceGenerator2/Helpers/PooledArrayBuilder{T}.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 a4abc33a8df0fc1626697ec8ae59f09fefcda3f7 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 8 Dec 2025 15:26:29 -0800 Subject: [PATCH 11/51] 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 07e43be8ccebf4be91475bf54eed22b4867f9426 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 8 Dec 2025 15:30:14 -0800 Subject: [PATCH 12/51] 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 3b3726b77eac8686e8f1e440f8661be28c3cae7a Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 8 Dec 2025 15:32:54 -0800 Subject: [PATCH 13/51] 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 8b791bb3a4d054757ca79580b2360ea04159b14a Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 8 Dec 2025 15:35:05 -0800 Subject: [PATCH 14/51] 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 | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/src/Authoring/WinRT.SourceGenerator2/Extensions/IndentedTextWriterExtensions.cs b/src/Authoring/WinRT.SourceGenerator2/Extensions/IndentedTextWriterExtensions.cs index 21cfeef59..f449d8fe5 100644 --- a/src/Authoring/WinRT.SourceGenerator2/Extensions/IndentedTextWriterExtensions.cs +++ b/src/Authoring/WinRT.SourceGenerator2/Extensions/IndentedTextWriterExtensions.cs @@ -5,6 +5,8 @@ // See: https://github.com/Sergio0694/ComputeSharp/blob/main/src/ComputeSharp.SourceGeneration/Helpers/IndentedTextWriter.cs. using System; +using System.Collections.Generic; +using System.Linq; namespace WindowsRuntime.SourceGenerator; @@ -56,4 +58,74 @@ public static void WriteGeneratedAttributes( } } } + + /// + /// 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 ref 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 ref 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 ref 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 6ac1a6064d20555e946ad239249b8d2b0edb57a9 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 8 Dec 2025 15:35:15 -0800 Subject: [PATCH 15/51] 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 bbf8aa3630956ba5a08386ddd83f7ff72beaf3fa Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 8 Dec 2025 15:49:04 -0800 Subject: [PATCH 16/51] 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 f554a367d4242523f1b6c0d6e14aa4ebd311487b Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 8 Dec 2025 16:01:54 -0800 Subject: [PATCH 17/51] 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 39fae2b1d947c2c0ecafc7ee44a561a2c7b90718 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 8 Dec 2025 16:45:14 -0800 Subject: [PATCH 18/51] 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 b711238bed77fdc241442319481e61b23c9af21a Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 8 Dec 2025 16:45:19 -0800 Subject: [PATCH 19/51] 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 b865bcb6e6b694d84a60fe480c32695f50fec5ba Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 8 Dec 2025 16:52:32 -0800 Subject: [PATCH 20/51] 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 117fcd2cd..69993e193 100644 --- a/src/Authoring/WinRT.SourceGenerator2/Extensions/ISymbolExtensions.cs +++ b/src/Authoring/WinRT.SourceGenerator2/Extensions/ISymbolExtensions.cs @@ -16,6 +16,24 @@ internal static class ISymbolExtensions /// The input instance. 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 c19d854bed0cc0e9b7a0a151503b56ffef05db63 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 8 Dec 2025 16:57:56 -0800 Subject: [PATCH 21/51] 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 f3f476f435bdcaff5feead14b5c3ca39f42502bb Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 8 Dec 2025 18:04:54 -0800 Subject: [PATCH 22/51] 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 1706462ac4890eb274ab82a75172bd892d58ba36 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 8 Dec 2025 18:06:33 -0800 Subject: [PATCH 23/51] 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 2e1b5c95703c3ed71aa016f1912f01d62ae42606 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 9 Dec 2025 13:40:47 -0800 Subject: [PATCH 24/51] 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 b91cf21e73c96a95436d0afb16f5687a28f0f5cb Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 9 Dec 2025 13:47:00 -0800 Subject: [PATCH 25/51] 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 7d6dc9fae0033003054688cf0a79adc042b6129d Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 10 Dec 2025 06:37:09 -0800 Subject: [PATCH 26/51] 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 e27b480b618a7a1c6aff28b384d3f6831a518cf7 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 10 Dec 2025 06:52:27 -0800 Subject: [PATCH 27/51] 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 1ad21233a5889400a562de7464c05ae650e9aecb Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 10 Dec 2025 07:16:48 -0800 Subject: [PATCH 28/51] 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 a9a501200a0f08b013bad3f232fcaea65a6c0d37 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 10 Dec 2025 07:16:53 -0800 Subject: [PATCH 29/51] 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 214cb070f7a4f25587d6d5329bb50134779ad689 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 10 Dec 2025 07:24:29 -0800 Subject: [PATCH 30/51] 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 | 54 ++++++++++++++++--- 2 files changed, 49 insertions(+), 7 deletions(-) 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 ee0c8ff29..14797580e 100644 --- a/src/Tests/FunctionalTests/ClassActivation/Program.cs +++ b/src/Tests/FunctionalTests/ClassActivation/Program.cs @@ -12,7 +12,9 @@ using Windows.Foundation; using Windows.Foundation.Collections; using Windows.Foundation.Tasks; +using Windows.UI.Xaml.Data; using WindowsRuntime.InteropServices; +using WindowsRuntime.Xaml; CustomDisposableTest customDisposableTest = new(); customDisposableTest.Dispose(); @@ -180,6 +182,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 { } @@ -195,6 +219,13 @@ public void Dispose() } } +[Guid("3C832AA5-5F7E-46EE-B1BF-7FE03AE866AF")] +[GeneratedComInterface] +partial interface IClassicComAction +{ + void Invoke(); +} + class GenericBaseType : IEnumerable, IDisposable { public void Dispose() @@ -262,6 +293,22 @@ IEnumerator> IEnumerable>.GetEnumerato } } +[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; } +} + class GenericFactory { // This method is caling a generic one, which then constructs a generic type. @@ -291,13 +338,6 @@ public static IAsyncOperation MakeAsyncOperation() } } -[Guid("3C832AA5-5F7E-46EE-B1BF-7FE03AE866AF")] -[GeneratedComInterface] -partial interface IClassicComAction -{ - void Invoke(); -} - file static class ComHelpers { [SupportedOSPlatform("windows6.3")] From 02a48d90a7e35a76c283e2af8575092e87d4366f Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 11 Dec 2025 09:21:05 -0800 Subject: [PATCH 31/51] 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 3fcccee6f93282d04fd75368b0ae9c37c7c61d43 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 11 Dec 2025 09:21:12 -0800 Subject: [PATCH 32/51] 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 1b8783974..bc85fca31 100644 --- a/src/Authoring/WinRT.SourceGenerator2/WinRT.SourceGenerator2.csproj +++ b/src/Authoring/WinRT.SourceGenerator2/WinRT.SourceGenerator2.csproj @@ -69,6 +69,12 @@ ..\..\WinRT.Runtime\key.snk + + + + + + From a5540163928be47abc416a845cb637cfd3ab943c Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 11 Dec 2025 09:31:12 -0800 Subject: [PATCH 33/51] 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 a19a81593fa38a7244683696b3ea2e3b5436a588 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 11 Dec 2025 09:35:23 -0800 Subject: [PATCH 34/51] 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 633abc15e9304c7261b07976ed0cbc16b5b0232c Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 16 Dec 2025 20:18:53 -0800 Subject: [PATCH 35/51] 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 e8e73397c..1be24b716 100644 --- a/src/cswinrt.slnx +++ b/src/cswinrt.slnx @@ -217,6 +217,7 @@ + From 8609fa1ed4008bb1d11980d7e47a1b8496195427 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 16 Dec 2025 20:33:55 -0800 Subject: [PATCH 36/51] 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 f99ef1cc0bbd487503a8f5154c1886376b45577c Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 16 Dec 2025 20:34:03 -0800 Subject: [PATCH 37/51] 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 f7b0f7fc1c31197ed4367836bbedcbbb56e63938 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 16 Dec 2025 20:34:07 -0800 Subject: [PATCH 38/51] 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 c70d66a99..fdc33b0e3 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -32,6 +32,7 @@ + From fea004c584e9ba04458cb26c921dff80aed3d78d Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 16 Dec 2025 20:34:41 -0800 Subject: [PATCH 39/51] 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 71d2024c8d5b0a9287b08ddd63d299d45e34c5d6 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 16 Dec 2025 20:42:08 -0800 Subject: [PATCH 40/51] 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 c3cc9f811fba47b52a0f283e7ec760c1b42bc590 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 16 Dec 2025 20:46:02 -0800 Subject: [PATCH 41/51] 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 29e772d6e3a0ae76677b1f7150bac7cc12b0914a Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 16 Dec 2025 20:51:19 -0800 Subject: [PATCH 42/51] 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 40e25ceda82dcdb22c4a6d381132c574d95d9a30 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 16 Dec 2025 20:51:23 -0800 Subject: [PATCH 43/51] 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 b42400330fa2f7e661707dbc51095b51f656bd3a Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 16 Dec 2025 20:57:45 -0800 Subject: [PATCH 44/51] 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 8d8f96780df92456f4d03e74e957686b3bd6a78a Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 17 Dec 2025 14:24:08 -0800 Subject: [PATCH 45/51] 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); From 2336d1dc67158244da8617961578c8c2c0cda861 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 8 Feb 2026 23:09:56 -0800 Subject: [PATCH 46/51] Fix comment typo and rename test method Correct a typo in a comment in CustomPropertyProviderGenerator.Execute.cs (change 'generated' to 'generate') and rename the test method in Test_CustomPropertyProviderGenerator.cs from SimpleShader_ComputeShader to ValidClass_MixedProperties to better reflect the test intent. --- .../CustomPropertyProviderGenerator.Execute.cs | 2 +- .../Test_CustomPropertyProviderGenerator.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Execute.cs b/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Execute.cs index b50dc95ce..2126a7316 100644 --- a/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Execute.cs +++ b/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Execute.cs @@ -43,7 +43,7 @@ public static bool IsTargetNodeValid(SyntaxNode node, CancellationToken token) return false; } - // We can only generated the 'ICustomPropertyProvider' implementation if the type is 'partial'. + // We can only generate 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) { diff --git a/src/Tests/SourceGenerator2Test/Test_CustomPropertyProviderGenerator.cs b/src/Tests/SourceGenerator2Test/Test_CustomPropertyProviderGenerator.cs index 3a10b898b..d3599c2d9 100644 --- a/src/Tests/SourceGenerator2Test/Test_CustomPropertyProviderGenerator.cs +++ b/src/Tests/SourceGenerator2Test/Test_CustomPropertyProviderGenerator.cs @@ -11,7 +11,7 @@ namespace WindowsRuntime.SourceGenerator.Tests; public class Test_CustomPropertyProviderGenerator { [TestMethod] - public async Task SimpleShader_ComputeShader() + public async Task ValidClass_MixedProperties() { const string source = """ using WindowsRuntime.Xaml; From e1654483eb16a090de149e6cb9f2d97f13ca7ac6 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 8 Feb 2026 23:20:35 -0800 Subject: [PATCH 47/51] Fix ICustomProperty emit for indexed/non-indexed Rename WriteCustomPropertyImplementationType to WriteNonIndexedCustomPropertyImplementationType and adjust emitted code so non-indexed properties generate GetValue/SetValue (handling static and instance dispatch) and throw for indexed access, while the indexed emitter does the inverse. This corrects previously reversed behavior so indexer and non-indexer accessors are emitted appropriately. --- .../CustomPropertyProviderGenerator.Emit.cs | 116 +++++++++--------- 1 file changed, 58 insertions(+), 58 deletions(-) diff --git a/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Emit.cs b/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Emit.cs index 61e054003..eec7a2898 100644 --- a/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Emit.cs +++ b/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Emit.cs @@ -202,19 +202,19 @@ private static void WriteCustomPropertyImplementationTypes(CustomPropertyProvide } else { - WriteCustomPropertyImplementationType(info, propertyInfo, writer); + WriteNonIndexedCustomPropertyImplementationType(info, propertyInfo, writer); } } } } /// - /// Writes a single ICustomProperty implementation type. + /// Writes a single non indexed 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) + private static void WriteNonIndexedCustomPropertyImplementationType(CustomPropertyProviderInfo info, CustomPropertyInfo propertyInfo, IndentedTextWriter writer) { string implementationTypeName = $"{info.TypeHierarchy.Hierarchy[0].QualifiedName}_{propertyInfo.Name}"; @@ -245,35 +245,55 @@ private static void WriteCustomPropertyImplementationType(CustomPropertyProvider 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(); - } + // 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) - { - throw new NotSupportedException(); - } - """, isMultiline: true); + /// + 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 property accessors (indexer properties can only be instance properties) writer.WriteLine(); - writer.WriteLine($$""" + writer.WriteLine(""" /// public object GetIndexedValue(object target, object index) { - return (({{info.TypeHierarchy.GetFullyQualifiedTypeName()}})target)[({{propertyInfo.FullyQualifiedIndexerTypeName}})index]; + throw new NotSupportedException(); } - + /// public void SetIndexedValue(object target, object value, object index) { - (({{info.TypeHierarchy.GetFullyQualifiedTypeName()}})target)[({{propertyInfo.FullyQualifiedIndexerTypeName}})index] = ({{propertyInfo.FullyQualifiedTypeName}})value; + throw new NotSupportedException(); } """, isMultiline: true); } @@ -314,55 +334,35 @@ private static void WriteIndexedCustomPropertyImplementationType(CustomPropertyP 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}}; - } + // This is an indexed property, so non indexed ones will always throw + writer.WriteLine(); + writer.WriteLine($$""" + /// + public object GetValue(object target) + { + throw new NotSupportedException(); + } - /// - public void SetValue(object target, object value) - { - (({{info.TypeHierarchy.GetFullyQualifiedTypeName()}})target).{{propertyInfo.Name}} = ({{propertyInfo.FullyQualifiedTypeName}})value; - } - """, isMultiline: true); - } + /// + public void SetValue(object target, object value) + { + throw new NotSupportedException(); + } + """, isMultiline: true); // Emit the indexer property accessors (not supported) writer.WriteLine(); - writer.WriteLine(""" + writer.WriteLine($$""" /// public object GetIndexedValue(object target, object index) { - throw new NotSupportedException(); + return (({{info.TypeHierarchy.GetFullyQualifiedTypeName()}})target)[({{propertyInfo.FullyQualifiedIndexerTypeName}})index]; } /// public void SetIndexedValue(object target, object value, object index) { - throw new NotSupportedException(); + (({{info.TypeHierarchy.GetFullyQualifiedTypeName()}})target)[({{propertyInfo.FullyQualifiedIndexerTypeName}})index] = ({{propertyInfo.FullyQualifiedTypeName}})value; } """, isMultiline: true); } From ce419efebef68bec4d95facf0012d2cf8b50ce1d Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 8 Feb 2026 23:22:38 -0800 Subject: [PATCH 48/51] Use ComHelpers.EnsureQueryInterface in test Replace manual Marshal.QueryInterface + ThrowExceptionForHR and an explicit customPropertyProviderPtr with ComHelpers.EnsureQueryInterface in the ClassActivation functional test. This removes the unused local pointer and corresponding Free call, simplifying the code path that obtains the ICustomPropertyProvider interface from the unmanaged object. --- src/Tests/FunctionalTests/ClassActivation/Program.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/Tests/FunctionalTests/ClassActivation/Program.cs b/src/Tests/FunctionalTests/ClassActivation/Program.cs index 14797580e..f1e285125 100644 --- a/src/Tests/FunctionalTests/ClassActivation/Program.cs +++ b/src/Tests/FunctionalTests/ClassActivation/Program.cs @@ -187,20 +187,17 @@ 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)); + ComHelpers.EnsureQueryInterface( + unknownPtr: testCustomPropertyProviderUnknownPtr, + iids: [new Guid("7C925755-3E48-42B4-8677-76372267033F")]); } finally { WindowsRuntimeMarshal.Free(testCustomPropertyProviderUnknownPtr); - WindowsRuntimeMarshal.Free(customPropertyProviderPtr); } } From b56f84bb29ef91701bac64c14a6bdf25ed70089a Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 8 Feb 2026 23:23:51 -0800 Subject: [PATCH 49/51] Fix typos in XML/doc comments Correct minor documentation typos to improve clarity: 'proprty' -> 'property' in CustomPropertyProviderGenerator.Emit.cs, 'rented' -> 'pooled' in PooledArrayBuilder{T}.cs, and 'paramaters' -> 'parameters' in CSharpAnalyzerTest{TAnalyzer}.cs. --- .../CustomPropertyProviderGenerator.Emit.cs | 2 +- .../WinRT.SourceGenerator2/Helpers/PooledArrayBuilder{T}.cs | 2 +- .../Helpers/CSharpAnalyzerTest{TAnalyzer}.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Emit.cs b/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Emit.cs index eec7a2898..4519c620d 100644 --- a/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Emit.cs +++ b/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Emit.cs @@ -314,7 +314,7 @@ private static void WriteIndexedCustomPropertyImplementationType(CustomPropertyP using (writer.WriteBlock()) { - // Emit all 'ICustomProperty' members for a normal proprty, and the singleton field + // Emit all 'ICustomProperty' members for a normal property, and the singleton field writer.WriteLine($$""" /// /// Gets the singleton instance for this custom property. diff --git a/src/Authoring/WinRT.SourceGenerator2/Helpers/PooledArrayBuilder{T}.cs b/src/Authoring/WinRT.SourceGenerator2/Helpers/PooledArrayBuilder{T}.cs index 71c5e2506..2fe6d5cbb 100644 --- a/src/Authoring/WinRT.SourceGenerator2/Helpers/PooledArrayBuilder{T}.cs +++ b/src/Authoring/WinRT.SourceGenerator2/Helpers/PooledArrayBuilder{T}.cs @@ -26,7 +26,7 @@ internal struct PooledArrayBuilder : IDisposable private static readonly ObjectPool SharedObjectPool = new(static () => new Writer()); /// - /// The rented instance to use. + /// The pooled instance to use. /// private Writer? _writer; diff --git a/src/Tests/SourceGenerator2Test/Helpers/CSharpAnalyzerTest{TAnalyzer}.cs b/src/Tests/SourceGenerator2Test/Helpers/CSharpAnalyzerTest{TAnalyzer}.cs index 016020c7e..37d418906 100644 --- a/src/Tests/SourceGenerator2Test/Helpers/CSharpAnalyzerTest{TAnalyzer}.cs +++ b/src/Tests/SourceGenerator2Test/Helpers/CSharpAnalyzerTest{TAnalyzer}.cs @@ -29,7 +29,7 @@ internal sealed class CSharpAnalyzerTest : CSharpAnalyzerTest - /// Creates a new instance with the specified paramaters. + /// Creates a new instance with the specified parameters. /// /// Whether to enable unsafe blocks. /// The C# language version to use to parse code. From 7e275aad01747876d9fd8ae2ca6325a7fc54ab8e Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 8 Feb 2026 23:26:17 -0800 Subject: [PATCH 50/51] Bump Roslyn packages to 5.0.0 Update Microsoft.CodeAnalysis.CSharp.CodeStyle and Microsoft.CodeAnalysis.CSharp.Workspaces to 5.0.0 in Directory.Packages.props, replacing CodeStyle 4.11.0 and Workspaces 4.12.0-3.final. This aligns package versions with the Roslyn 5.0 release; no other package changes were made. --- src/Directory.Packages.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index fdc33b0e3..36a27a914 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -17,8 +17,8 @@ - - + + From 4aea9d13044c8e26d9e99c12ac6dbbbca6eda735 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sat, 14 Feb 2026 21:02:00 -0800 Subject: [PATCH 51/51] Use IndentedTextWriter overload and clear buffer Replace parameterless IndentedTextWriter usage with explicit constructor calls (literalLength: 0, formattedCount: 0) and add TODO notes to adjust literal length. Also change context.AddSource to use writer.ToStringAndClear() to avoid retaining the writer's internal buffer after emitting source. Affects AuthoringExportTypesGenerator.Execute.cs and CustomPropertyProviderGenerator.Emit.cs. --- .../AuthoringExportTypesGenerator.Execute.cs | 2 +- .../CustomPropertyProviderGenerator.Emit.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Authoring/WinRT.SourceGenerator2/AuthoringExportTypesGenerator.Execute.cs b/src/Authoring/WinRT.SourceGenerator2/AuthoringExportTypesGenerator.Execute.cs index 0eb151d99..7649102f7 100644 --- a/src/Authoring/WinRT.SourceGenerator2/AuthoringExportTypesGenerator.Execute.cs +++ b/src/Authoring/WinRT.SourceGenerator2/AuthoringExportTypesGenerator.Execute.cs @@ -112,7 +112,7 @@ public static void EmitManagedExports(SourceProductionContext context, Authoring return; } - IndentedTextWriter writer = new(literalLength: 0, formattedCount: 0); + IndentedTextWriter writer = new(literalLength: 0, formattedCount: 0); // TODO: adjust the literal length // Emit the '[WindowsRuntimeComponentAssemblyExportsType]' attribute so other tooling (including this same generator) // can reliably find the generated export types from other assemblies, which is needed when merging activation factories. diff --git a/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Emit.cs b/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Emit.cs index 4519c620d..8fff974a0 100644 --- a/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Emit.cs +++ b/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Emit.cs @@ -22,7 +22,7 @@ private static class Emit /// The input state to use. public static void WriteCustomPropertyProviderImplementation(SourceProductionContext context, CustomPropertyProviderInfo info) { - using IndentedTextWriter writer = new(); + IndentedTextWriter writer = new(literalLength: 0, formattedCount: 0); // TODO: adjust the literal length // Emit the implementation on the annotated type info.TypeHierarchy.WriteSyntax( @@ -39,7 +39,7 @@ public static void WriteCustomPropertyProviderImplementation(SourceProductionCon WriteCustomPropertyImplementationTypes(info, writer); // Add the source file for the annotated type - context.AddSource($"{info.TypeHierarchy.FullyQualifiedMetadataName}.g.cs", writer.ToString()); + context.AddSource($"{info.TypeHierarchy.FullyQualifiedMetadataName}.g.cs", writer.ToStringAndClear()); } ///