From bd265fc9e8d2eb216dede07ef1ab9142b61f895a Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 7 Dec 2025 18:29:23 -0800 Subject: [PATCH 001/100] 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 0000000000..1ac293ad4e --- /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 191327bee740f1259bea6248907a9aa26ebbc1ca Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 7 Dec 2025 21:53:54 -0800 Subject: [PATCH 002/100] 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 0000000000..4af55419b0 --- /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 4294e459e9b19c29d2e86408ef3186ce8e408fbf Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 7 Dec 2025 21:57:31 -0800 Subject: [PATCH 003/100] 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 0000000000..d3ed23f4ec --- /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 e5c4cb9a424349b4bfa051996f6c3f205870fb90 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 8 Dec 2025 10:35:59 -0800 Subject: [PATCH 004/100] 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 0000000000..cc510ce9d3 --- /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 23de3787dff0dd401ab8bbd156580d16e467aad5 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 8 Dec 2025 10:44:39 -0800 Subject: [PATCH 005/100] 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 0000000000..4876819ed7 --- /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 0000000000..9126e1e188 --- /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 5e08a475f4bc4d40280c6d3384d33b1e42414608 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 8 Dec 2025 15:11:37 -0800 Subject: [PATCH 006/100] 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 0000000000..04f60a0894 --- /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 1e419ac9530b084e3292a5a39e6516299ac1c462 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 8 Dec 2025 15:17:30 -0800 Subject: [PATCH 007/100] 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 0000000000..876e504641 --- /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 2180a35775be2648a760820e4f23f1e1d8e88f10 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 8 Dec 2025 15:23:33 -0800 Subject: [PATCH 008/100] 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 876e504641..a8214e3880 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 7c7f274a84829d62c43fea7fadb8576c27152e57 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 8 Dec 2025 15:26:29 -0800 Subject: [PATCH 009/100] 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 9126e1e188..0be1331159 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 169491cd68cb0987cb7637e1c579eb8533a0fa7f Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 8 Dec 2025 15:30:14 -0800 Subject: [PATCH 010/100] 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 223d8a6b54..384a904e1f 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 8a4944e41bc8912eb91b3a380f0db92615da0f26 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 8 Dec 2025 15:32:54 -0800 Subject: [PATCH 011/100] 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 0000000000..740772942a --- /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 a99dbb9736c3af6a4ffe14f2b265cfa0f2d4a1ce Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 8 Dec 2025 15:35:05 -0800 Subject: [PATCH 012/100] 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 21cfeef592..f449d8fe53 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 290d516dd7dc9807de11fc2029c3ddbac1b4e196 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 8 Dec 2025 15:35:15 -0800 Subject: [PATCH 013/100] 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 0000000000..ff56fc6983 --- /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 0000000000..b7575e9990 --- /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 69fbe29da6d443f9aa444a28509d8f0a91ea39c1 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 8 Dec 2025 15:49:04 -0800 Subject: [PATCH 014/100] 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 4876819ed7..c79eab4956 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 869d57fd23aadd278572ce41b055acc8995e7133 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 8 Dec 2025 16:01:54 -0800 Subject: [PATCH 015/100] 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 0000000000..5c95394f53 --- /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 0a40e16c3f4a6052069dc9fdada68d573f3b2d6e Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 8 Dec 2025 16:45:14 -0800 Subject: [PATCH 016/100] 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 740772942a..9e163709da 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 a0567e6aa1cdbea94ece2fb4e92ec8ecd305c317 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 8 Dec 2025 16:45:19 -0800 Subject: [PATCH 017/100] 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 5c95394f53..a04906d25c 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 a744c1b4dfbaab690909b8bec6d37e5c6100d89b Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 8 Dec 2025 16:52:32 -0800 Subject: [PATCH 018/100] 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 117fcd2cdf..69993e1931 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 480db17ccf7fde4cea14f5fb925906f28e2a8e44 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 8 Dec 2025 16:57:56 -0800 Subject: [PATCH 019/100] 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 c79eab4956..d033fa2bc1 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 0be1331159..af68369a1c 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 3d2924049a1dd50223cc1d215561e0179e0c2dfe Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 8 Dec 2025 18:04:54 -0800 Subject: [PATCH 020/100] 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 a8214e3880..71c5e25060 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 f6d69dde6e7793c85c7e35714a9e2685d784c479 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 8 Dec 2025 18:06:33 -0800 Subject: [PATCH 021/100] 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 d033fa2bc1..17445d0820 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 610ac3849b0a6a49cf1776326077e106a2945c51 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 9 Dec 2025 13:40:47 -0800 Subject: [PATCH 022/100] 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 17445d0820..e4fe262285 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 af68369a1c..3cd570c088 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 0000000000..99b1a4aabf --- /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 0000000000..118422c955 --- /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 30dfca7348b29860f691f320a6d09f30c2ce3892 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 9 Dec 2025 13:47:00 -0800 Subject: [PATCH 023/100] 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 0000000000..b98aaf08b0 --- /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 e4fe262285..efb6b64197 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 3cd570c088..e342f16129 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 6e07ad48e5fe225ce7fe1e23a04f68eba5309a07 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 10 Dec 2025 06:37:09 -0800 Subject: [PATCH 024/100] 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 efb6b64197..64ef3783c3 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 9e163709da..78729d4268 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 6f2ac229603b9a65b84fb45acf0c4583cf57d97d Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 10 Dec 2025 06:52:27 -0800 Subject: [PATCH 025/100] 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 99b1a4aabf..c2a567ed9c 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 1b2106b63dee7bb9050ae74ee9f3f0f2b71b57fe Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 10 Dec 2025 07:16:48 -0800 Subject: [PATCH 026/100] 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 64ef3783c3..9b3b2bfeab 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 c8779606120f647c7e796f1802666503fc3d06ee Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 10 Dec 2025 07:16:53 -0800 Subject: [PATCH 027/100] 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 b98aaf08b0..61e054003f 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 c6174aa183f45d53b86adc8cc132d3830697e703 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 10 Dec 2025 07:24:29 -0800 Subject: [PATCH 028/100] 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 f9e366e402..72b523a375 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 2df28d0f6a..3a03c3efd6 100644 --- a/src/Tests/FunctionalTests/ClassActivation/Program.cs +++ b/src/Tests/FunctionalTests/ClassActivation/Program.cs @@ -11,7 +11,9 @@ using TestComponentCSharp; using Windows.Foundation; using Windows.Foundation.Collections; +using Windows.UI.Xaml.Data; using WindowsRuntime.InteropServices; +using WindowsRuntime.Xaml; CustomDisposableTest customDisposableTest = new(); customDisposableTest.Dispose(); @@ -179,6 +181,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 { } @@ -194,6 +218,13 @@ public void Dispose() } } +[Guid("3C832AA5-5F7E-46EE-B1BF-7FE03AE866AF")] +[GeneratedComInterface] +partial interface IClassicComAction +{ + void Invoke(); +} + class GenericBaseType : IEnumerable, IDisposable { public void Dispose() @@ -261,6 +292,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. @@ -290,13 +337,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 9beffa76e373cc1d5dc1de0ca4f46aea9cd9f882 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 11 Dec 2025 09:21:05 -0800 Subject: [PATCH 029/100] 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 0000000000..662958a67b --- /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 a26bc0778f4bd875c21c857b7a9871e34e9759cc Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 11 Dec 2025 09:21:12 -0800 Subject: [PATCH 030/100] 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 0000000000..cd58f03539 --- /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 0000000000..6640189c3f --- /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 556e838190..3a2e979d92 100644 --- a/src/Authoring/WinRT.SourceGenerator2/WinRT.SourceGenerator2.csproj +++ b/src/Authoring/WinRT.SourceGenerator2/WinRT.SourceGenerator2.csproj @@ -69,6 +69,12 @@ ..\..\WinRT.Runtime2\key.snk + + + + + + From b1aedf001dd80f63c475faa3a6e5bff5d4c67585 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 11 Dec 2025 09:31:12 -0800 Subject: [PATCH 031/100] 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 0000000000..38e3078fbc --- /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 0d9939f90698935089244e114a0cff441125643b Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 11 Dec 2025 09:35:23 -0800 Subject: [PATCH 032/100] 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 0000000000..e9b2327bfb --- /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 22d4abe9032ccdc233947b2016b528caad5d6814 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 16 Dec 2025 20:18:53 -0800 Subject: [PATCH 033/100] 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 0000000000..79b84650a9 --- /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 af6d7709c9..896349a74e 100644 --- a/src/cswinrt.slnx +++ b/src/cswinrt.slnx @@ -206,6 +206,7 @@ + From 43606c4bf8a7fd7b28e1614284b3fb2964b6319f Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 16 Dec 2025 20:33:55 -0800 Subject: [PATCH 034/100] 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 9b3b2bfeab..b50dc95cec 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 6311e469c7eefa05ee8fd74790db08912bee3d29 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 16 Dec 2025 20:34:03 -0800 Subject: [PATCH 035/100] 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 79b84650a9..baf75d37fd 100644 --- a/src/Tests/SourceGenerator2Test/SourceGenerator2Test.csproj +++ b/src/Tests/SourceGenerator2Test/SourceGenerator2Test.csproj @@ -7,7 +7,7 @@ - + From af7825b0bb34451ba22c1c35966ebad3fc1679b0 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 16 Dec 2025 20:34:07 -0800 Subject: [PATCH 036/100] 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 fc050eeb09..5e79ba155a 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -46,6 +46,7 @@ + From 6a2b0718b3efd78f8c6d663c788eb2d2425d10af Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 16 Dec 2025 20:34:41 -0800 Subject: [PATCH 037/100] 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 0000000000..1152878def --- /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 0d1029918c734629d1244b676ef70bad80315191 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 16 Dec 2025 20:42:08 -0800 Subject: [PATCH 038/100] 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 0000000000..0fcbd29c87 --- /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 15bfb65ccd80d40f64d412b11a7dcbdffbdee9c8 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 16 Dec 2025 20:46:02 -0800 Subject: [PATCH 039/100] 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 0000000000..3a10b898b2 --- /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 7aa7800925dee657e58d983666ee755f7926d8b9 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 16 Dec 2025 20:51:19 -0800 Subject: [PATCH 040/100] 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 0000000000..c1f202b046 --- /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 d70dfba20178546be0665ae129d13f7dbc6909c3 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 16 Dec 2025 20:51:23 -0800 Subject: [PATCH 041/100] 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 0fcbd29c87..1c70650f22 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 58dc0d3a3843f978de4a44033d5cb6918245abb6 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 16 Dec 2025 20:57:45 -0800 Subject: [PATCH 042/100] 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 0000000000..07550f0b61 --- /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 a54eb71dbcf4f50efb8dd47f52c9e1e0617f94e8 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 17 Dec 2025 14:24:08 -0800 Subject: [PATCH 043/100] 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 0000000000..79836a3adf --- /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 c1f202b046..016020c7ed 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 c08c65033214f29b010746aa9cae5e399a58fade Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 8 Feb 2026 23:09:56 -0800 Subject: [PATCH 044/100] 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 b50dc95cec..2126a7316d 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 3a10b898b2..d3599c2d95 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 f8a6ee2419ad3c258da7a03462b09647f0c53455 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 8 Feb 2026 23:20:35 -0800 Subject: [PATCH 045/100] 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 61e054003f..eec7a2898c 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 82aaa0dba69a60bde87ea52f43640a3a1b00af3f Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 8 Feb 2026 23:22:38 -0800 Subject: [PATCH 046/100] 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 3a03c3efd6..d00a4d2507 100644 --- a/src/Tests/FunctionalTests/ClassActivation/Program.cs +++ b/src/Tests/FunctionalTests/ClassActivation/Program.cs @@ -186,20 +186,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 404b6ce62a727bf2713c2d9db3b48d542826388d Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 8 Feb 2026 23:23:51 -0800 Subject: [PATCH 047/100] 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 eec7a2898c..4519c620d1 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 71c5e25060..2fe6d5cbb8 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 016020c7ed..37d418906f 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 4d2002652619ad62084e8881210fea1db98246ce Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 8 Feb 2026 23:26:17 -0800 Subject: [PATCH 048/100] 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 5e79ba155a..0da774a19a 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -17,8 +17,8 @@ - - + + From e5973d416fc1bd6bdd95ce36f4418ea0007ba70d Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sat, 14 Feb 2026 21:02:00 -0800 Subject: [PATCH 049/100] 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 0eb151d997..7649102f7d 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 4519c620d1..8fff974a01 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()); } /// From 885cbb635db7779849a42d7b5cba436077e4f8d7 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Mon, 23 Feb 2026 16:16:11 -0800 Subject: [PATCH 050/100] Use WinRT.Runtime.csproj in test project Update SourceGenerator2Test.csproj to reference ..\..\WinRT.Runtime\WinRT.Runtime.csproj instead of the previous WinRT.Runtime2 path. This fixes the project reference so the test project points to the correct runtime project (likely due to a rename or consolidation). --- 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 baf75d37fd..dbfec6c788 100644 --- a/src/Tests/SourceGenerator2Test/SourceGenerator2Test.csproj +++ b/src/Tests/SourceGenerator2Test/SourceGenerator2Test.csproj @@ -13,6 +13,6 @@ - + From 225ec3c166e7ffc80c3ae68b239bf5588d8ed94f Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 10 Mar 2026 20:20:17 -0700 Subject: [PATCH 051/100] Add Directory.Build.props to SourceGenerator2Test Add a Directory.Build.props for the SourceGenerator2Test project that enables SimulateCsWinRTNugetReference and imports the shared Directory.Build.props from two levels up. This ensures tests simulate a CsWinRT NuGet reference consistently with the repository's shared MSBuild configuration. --- src/Tests/SourceGenerator2Test/Directory.Build.props | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 src/Tests/SourceGenerator2Test/Directory.Build.props diff --git a/src/Tests/SourceGenerator2Test/Directory.Build.props b/src/Tests/SourceGenerator2Test/Directory.Build.props new file mode 100644 index 0000000000..629129c597 --- /dev/null +++ b/src/Tests/SourceGenerator2Test/Directory.Build.props @@ -0,0 +1,9 @@ + + + + true + + + + + From a08cb86997e199ab1874bd6484b1f3ad21edea26 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 10 Mar 2026 20:20:26 -0700 Subject: [PATCH 052/100] Remove VersionOverride from CSharp.Workspaces ref Remove the VersionOverride attribute from the Microsoft.CodeAnalysis.CSharp.Workspaces PackageReference in SourceGenerator2Test.csproj so the project will use the default/centrally-resolved package version instead of the hardcoded 5.0.0 override. --- 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 dbfec6c788..d4d4e38c9a 100644 --- a/src/Tests/SourceGenerator2Test/SourceGenerator2Test.csproj +++ b/src/Tests/SourceGenerator2Test/SourceGenerator2Test.csproj @@ -7,7 +7,7 @@ - + From 1c757cceb17c738cb4f0d934cbe98488c16e0006 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Tue, 10 Mar 2026 20:22:44 -0700 Subject: [PATCH 053/100] Refactor MSTest references and add projections Remove the monolithic MSTest package entry from Directory.Packages.props and update SourceGenerator2Test.csproj to use granular MSTest packages (TestFramework, Analyzers, Engine, SourceGeneration) plus MSBuild test platform and TRX reporting. Also add ProjectReferences for Windows and WinAppSDK projections so the tests can build against those projections. These changes enable finer-grained control over test components and reporting for the SourceGenerator2 test project. --- src/Directory.Packages.props | 1 - .../SourceGenerator2Test.csproj | 15 ++++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 0da774a19a..995432db31 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -46,7 +46,6 @@ - diff --git a/src/Tests/SourceGenerator2Test/SourceGenerator2Test.csproj b/src/Tests/SourceGenerator2Test/SourceGenerator2Test.csproj index d4d4e38c9a..bbdc9b04da 100644 --- a/src/Tests/SourceGenerator2Test/SourceGenerator2Test.csproj +++ b/src/Tests/SourceGenerator2Test/SourceGenerator2Test.csproj @@ -8,11 +8,24 @@ - + + + + + + + + + + + + + + From 627eb2b4e5269676ca5e4393947b0fcef0af22fd Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 11 Mar 2026 10:35:40 -0700 Subject: [PATCH 054/100] Centralize MSTest using and set OutputType Remove Microsoft.VisualStudio.TestTools.UnitTesting directives from several test source files and AssemblyInfo, and add a project-level Using for MSTest in SourceGenerator2Test.csproj. Also set the test project OutputType to Exe. This centralizes the testing using directive and simplifies individual files while configuring the test project output. --- .../Helpers/CSharpGeneratorTest{TGenerator}.cs | 1 - src/Tests/SourceGenerator2Test/Properties/AssemblyInfo.cs | 2 -- src/Tests/SourceGenerator2Test/SourceGenerator2Test.csproj | 5 +++++ .../Test_CustomPropertyProviderGenerator.cs | 1 - ...Test_GeneratedCustomPropertyProviderTargetTypeAnalyzer.cs | 1 - 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Tests/SourceGenerator2Test/Helpers/CSharpGeneratorTest{TGenerator}.cs b/src/Tests/SourceGenerator2Test/Helpers/CSharpGeneratorTest{TGenerator}.cs index 1c70650f22..9787e682b4 100644 --- a/src/Tests/SourceGenerator2Test/Helpers/CSharpGeneratorTest{TGenerator}.cs +++ b/src/Tests/SourceGenerator2Test/Helpers/CSharpGeneratorTest{TGenerator}.cs @@ -8,7 +8,6 @@ using Basic.Reference.Assemblies; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; -using Microsoft.VisualStudio.TestTools.UnitTesting; namespace WindowsRuntime.SourceGenerator.Tests.Helpers; diff --git a/src/Tests/SourceGenerator2Test/Properties/AssemblyInfo.cs b/src/Tests/SourceGenerator2Test/Properties/AssemblyInfo.cs index 1152878def..d1e038ca49 100644 --- a/src/Tests/SourceGenerator2Test/Properties/AssemblyInfo.cs +++ b/src/Tests/SourceGenerator2Test/Properties/AssemblyInfo.cs @@ -1,6 +1,4 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using Microsoft.VisualStudio.TestTools.UnitTesting; - [assembly: Parallelize] \ No newline at end of file diff --git a/src/Tests/SourceGenerator2Test/SourceGenerator2Test.csproj b/src/Tests/SourceGenerator2Test/SourceGenerator2Test.csproj index bbdc9b04da..834258e8ea 100644 --- a/src/Tests/SourceGenerator2Test/SourceGenerator2Test.csproj +++ b/src/Tests/SourceGenerator2Test/SourceGenerator2Test.csproj @@ -1,5 +1,6 @@  + Exe net10.0 @@ -28,4 +29,8 @@ + + + + diff --git a/src/Tests/SourceGenerator2Test/Test_CustomPropertyProviderGenerator.cs b/src/Tests/SourceGenerator2Test/Test_CustomPropertyProviderGenerator.cs index d3599c2d95..39b3208539 100644 --- a/src/Tests/SourceGenerator2Test/Test_CustomPropertyProviderGenerator.cs +++ b/src/Tests/SourceGenerator2Test/Test_CustomPropertyProviderGenerator.cs @@ -2,7 +2,6 @@ // Licensed under the MIT License. using System.Threading.Tasks; -using Microsoft.VisualStudio.TestTools.UnitTesting; using WindowsRuntime.SourceGenerator.Tests.Helpers; namespace WindowsRuntime.SourceGenerator.Tests; diff --git a/src/Tests/SourceGenerator2Test/Test_GeneratedCustomPropertyProviderTargetTypeAnalyzer.cs b/src/Tests/SourceGenerator2Test/Test_GeneratedCustomPropertyProviderTargetTypeAnalyzer.cs index 07550f0b61..90527deecb 100644 --- a/src/Tests/SourceGenerator2Test/Test_GeneratedCustomPropertyProviderTargetTypeAnalyzer.cs +++ b/src/Tests/SourceGenerator2Test/Test_GeneratedCustomPropertyProviderTargetTypeAnalyzer.cs @@ -2,7 +2,6 @@ // Licensed under the MIT License. using System.Threading.Tasks; -using Microsoft.VisualStudio.TestTools.UnitTesting; using WindowsRuntime.SourceGenerator.Diagnostics; using WindowsRuntime.SourceGenerator.Tests.Helpers; From 530d1c4910dcab66c7960bff7fe615d4504a15a1 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 11 Mar 2026 13:03:54 -0700 Subject: [PATCH 055/100] Add x86/x64 platform targets to SourceGenerator2Test Enable explicit x86 and x64 builds for the SourceGenerator2Test project by adding Platforms, disabling CsWinRT, and adding Platform-specific PropertyGroups (RuntimeIdentifier and PlatformTarget). Update the solution entry to map solution platforms to the project platforms and disable ARM/ARM64 builds for this test project so the solution builds correctly for x86 and x64. --- .../SourceGenerator2Test/SourceGenerator2Test.csproj | 12 ++++++++++++ src/cswinrt.slnx | 9 ++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/src/Tests/SourceGenerator2Test/SourceGenerator2Test.csproj b/src/Tests/SourceGenerator2Test/SourceGenerator2Test.csproj index 834258e8ea..0070a86912 100644 --- a/src/Tests/SourceGenerator2Test/SourceGenerator2Test.csproj +++ b/src/Tests/SourceGenerator2Test/SourceGenerator2Test.csproj @@ -2,6 +2,18 @@ Exe net10.0 + x64;x86 + false + + + + win-x86 + x86 + + + + win-x64 + x64 diff --git a/src/cswinrt.slnx b/src/cswinrt.slnx index 896349a74e..d4087f6519 100644 --- a/src/cswinrt.slnx +++ b/src/cswinrt.slnx @@ -206,7 +206,14 @@ - + + + + + + + + From 7b2ab039b128f4be672ed14c885a2824685a72c8 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 11 Mar 2026 13:54:41 -0700 Subject: [PATCH 056/100] Update GeneratedCustomPropertyProvider analyzer tests Revise unit tests for GeneratedCustomPropertyProviderTargetTypeAnalyzer: - Add a new ValidTargetType_InValidHierarchy_DoesNotWarn test covering nested types and class/struct modifiers. - Adjust DataRow inputs to differentiate between valid and invalid modifier combinations (e.g. use non-partial vs partial modifiers). - Move diagnostic expectation markers from the attribute to the target type identifier (MyType) in test source strings so the analyzer is asserted against the type declaration. These changes clarify which type declarations should trigger CSWINRT2000/2001 diagnostics and add coverage for nested type scenarios. --- ...ustomPropertyProviderTargetTypeAnalyzer.cs | 41 ++++++++++++++----- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/src/Tests/SourceGenerator2Test/Test_GeneratedCustomPropertyProviderTargetTypeAnalyzer.cs b/src/Tests/SourceGenerator2Test/Test_GeneratedCustomPropertyProviderTargetTypeAnalyzer.cs index 90527deecb..d33d59f935 100644 --- a/src/Tests/SourceGenerator2Test/Test_GeneratedCustomPropertyProviderTargetTypeAnalyzer.cs +++ b/src/Tests/SourceGenerator2Test/Test_GeneratedCustomPropertyProviderTargetTypeAnalyzer.cs @@ -26,17 +26,38 @@ public async Task ValidTargetType_DoesNotWarn(string modifier) } [TestMethod] - [DataRow("abstract class")] - [DataRow("static class")] - [DataRow("static struct")] - [DataRow("ref struct")] + [DataRow("class")] + [DataRow("struct")] + public async Task ValidTargetType_InValidHierarchy_DoesNotWarn(string modifier) + { + string source = $$""" + using WindowsRuntime.Xaml; + + public partial struct A + { + public partial class B + { + [GeneratedCustomPropertyProvider] + public partial {{modifier}} MyType; + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source); + } + + [TestMethod] + [DataRow("abstract partial class")] + [DataRow("static partial class")] + [DataRow("static partial struct")] + [DataRow("ref partial struct")] public async Task InvalidTargetType_Warns(string modifiers) { string source = $$""" using WindowsRuntime.Xaml; - [{|CSWINRT2000:GeneratedCustomPropertyProvider|}] - public {{modifiers}} MyType; + [GeneratedCustomPropertyProvider] + public {{modifiers}} {|CSWINRT2000:MyType|}; """; await CSharpAnalyzerTest.VerifyAnalyzerAsync(source); @@ -50,8 +71,8 @@ public async Task TypeNotPartial_Warns(string modifier) string source = $$""" using WindowsRuntime.Xaml; - [{|CSWINRT2001:GeneratedCustomPropertyProvider|}] - public {{modifier}} MyType; + [GeneratedCustomPropertyProvider] + public {{modifier}} {|CSWINRT2001:MyType|}; """; await CSharpAnalyzerTest.VerifyAnalyzerAsync(source); @@ -67,8 +88,8 @@ public async Task TypeNotInPartialTypeHierarchy_Warns(string modifier) public class ParentType { - [{|CSWINRT2001:GeneratedCustomPropertyProvider|}] - public partial {{modifier}} MyType; + [GeneratedCustomPropertyProvider] + public partial {{modifier}} {|CSWINRT2001:MyType|}; } """; From 9d48a32eb32c173c5bbfbd21a080b72ff9b0c803 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 11 Mar 2026 15:08:02 -0700 Subject: [PATCH 057/100] Add CoreApplication and Button refs to tests Add using directives and metadata references for Windows.ApplicationModel.Core (CoreApplication) and Microsoft.UI.Xaml.Controls (Button) in the C# test helpers. Updates CSharpAnalyzerTest{TAnalyzer}.cs and CSharpGeneratorTest{TGenerator}.cs to include the required assemblies so WinRT/WinUI types resolve during test compilation. --- .../Helpers/CSharpAnalyzerTest{TAnalyzer}.cs | 4 ++++ .../Helpers/CSharpGeneratorTest{TGenerator}.cs | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/Tests/SourceGenerator2Test/Helpers/CSharpAnalyzerTest{TAnalyzer}.cs b/src/Tests/SourceGenerator2Test/Helpers/CSharpAnalyzerTest{TAnalyzer}.cs index 37d418906f..3644ff35c7 100644 --- a/src/Tests/SourceGenerator2Test/Helpers/CSharpAnalyzerTest{TAnalyzer}.cs +++ b/src/Tests/SourceGenerator2Test/Helpers/CSharpAnalyzerTest{TAnalyzer}.cs @@ -8,6 +8,8 @@ using Microsoft.CodeAnalysis.CSharp.Testing; using Microsoft.CodeAnalysis.Diagnostics; using Microsoft.CodeAnalysis.Testing; +using Microsoft.UI.Xaml.Controls; +using Windows.ApplicationModel.Core; namespace WindowsRuntime.SourceGenerator.Tests.Helpers; @@ -64,6 +66,8 @@ public static Task VerifyAnalyzerAsync( test.TestState.ReferenceAssemblies = ReferenceAssemblies.Net.Net100; test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(WindowsRuntimeObject).Assembly.Location)); + test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(CoreApplication).Assembly.Location)); + test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(Button).Assembly.Location)); return test.RunAsync(CancellationToken.None); } diff --git a/src/Tests/SourceGenerator2Test/Helpers/CSharpGeneratorTest{TGenerator}.cs b/src/Tests/SourceGenerator2Test/Helpers/CSharpGeneratorTest{TGenerator}.cs index 9787e682b4..93affa50d9 100644 --- a/src/Tests/SourceGenerator2Test/Helpers/CSharpGeneratorTest{TGenerator}.cs +++ b/src/Tests/SourceGenerator2Test/Helpers/CSharpGeneratorTest{TGenerator}.cs @@ -8,6 +8,8 @@ using Basic.Reference.Assemblies; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; +using Microsoft.UI.Xaml.Controls; +using Windows.ApplicationModel.Core; namespace WindowsRuntime.SourceGenerator.Tests.Helpers; @@ -52,6 +54,8 @@ private static CSharpCompilation CreateCompilation(string source, LanguageVersio [ .. Net100.References.All, MetadataReference.CreateFromFile(typeof(WindowsRuntimeObject).Assembly.Location), + MetadataReference.CreateFromFile(typeof(CoreApplication).Assembly.Location), + MetadataReference.CreateFromFile(typeof(Button).Assembly.Location) ]; // Parse the source text From f738e615e8145b43468bfd506230ba0d044f47bb Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 11 Mar 2026 15:15:52 -0700 Subject: [PATCH 058/100] Use IndentedTextWriter and preallocate buffer Replace StringBuilder with IndentedTextWriter and approximate a starting buffer size to reduce allocations/copies in the source generator. Add constants for header/attributes length, emit an auto-generated header and #pragma warning disable, write per-assembly TypeMapAssemblyTarget attributes, and call ToStringAndClear when adding the generated source. Also remove the now-unused System.Text using. --- .../TypeMapAssemblyTargetGenerator.Execute.cs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/Authoring/WinRT.SourceGenerator2/TypeMapAssemblyTargetGenerator.Execute.cs b/src/Authoring/WinRT.SourceGenerator2/TypeMapAssemblyTargetGenerator.Execute.cs index cee19ae3e5..0dd8fb87af 100644 --- a/src/Authoring/WinRT.SourceGenerator2/TypeMapAssemblyTargetGenerator.Execute.cs +++ b/src/Authoring/WinRT.SourceGenerator2/TypeMapAssemblyTargetGenerator.Execute.cs @@ -2,7 +2,6 @@ // Licensed under the MIT License. using System.Collections.Immutable; -using System.Text; using System.Threading; using Microsoft.CodeAnalysis; @@ -91,25 +90,32 @@ public static void EmitPrivateProjectionsTypeMapAssemblyTargetAttributes(SourceP return; } - StringBuilder builder = new(); + const int ApproximateHeaderLength = 64; + const int ApproximateAttributesLength = 512; - _ = builder.AppendLine($""" + // Approximate a close enough starting length to reduce copies + int approximateLiteralLength = ApproximateHeaderLength + (ApproximateAttributesLength * assemblyNames.Length); + + IndentedTextWriter builder = new(literalLength: approximateLiteralLength, formattedCount: 0); + + // Add the auto-generated header and disable warnings for the generated file + builder.Write($""" // #pragma warning disable - """); // Add a '[TypeMapAssemblyTarget]' entry for each assembly foreach (string assemblyName in assemblyNames) { - _ = builder.AppendLine($""" + builder.WriteLine(); + builder.Write($""" [assembly: global::System.Runtime.InteropServices.TypeMapAssemblyTarget("{assemblyName}")] [assembly: global::System.Runtime.InteropServices.TypeMapAssemblyTarget("{assemblyName}")] [assembly: global::System.Runtime.InteropServices.TypeMapAssemblyTarget("{assemblyName}")] """); } - context.AddSource("TypeMapAssemblyTarget.PrivateProjections.g.cs", builder.ToString()); + context.AddSource("TypeMapAssemblyTarget.PrivateProjections.g.cs", builder.ToStringAndClear()); } /// From 9aa2881e182bd5b93c24b774ed1e99e1a5aff40d Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 11 Mar 2026 15:35:16 -0700 Subject: [PATCH 059/100] Make IndentedTextWriter ref/scoped and update usages Convert IndentedTextWriter API and call sites to use ref and scoped semantics. Update Callback to accept ref IndentedTextWriter and adjust extension helpers and HierarchyInfo.WriteSyntax to pass the writer by ref. Change Block to store a pointer to the writer (unsafe ref struct) and return it with UnscopedRef from WriteBlock to preserve lifetime semantics. Update generator files to pass writer by ref. Also add CS8500 NoWarn to the project and tidy a comment block. These changes enable safe/efficient usage of the ref struct across the codebase and fix related call signatures. --- .../CustomPropertyProviderGenerator.Emit.cs | 22 +++++++++---------- .../IndentedTextWriterExtensions.cs | 12 +++++----- .../Helpers/IndentedTextWriter.cs | 13 ++++++----- .../Models/HierarchyInfo.cs | 10 ++++----- .../WinRT.SourceGenerator2.csproj | 13 ++++++----- 5 files changed, 37 insertions(+), 33 deletions(-) diff --git a/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Emit.cs b/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Emit.cs index 8fff974a01..79ea9f838c 100644 --- a/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Emit.cs +++ b/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Emit.cs @@ -27,7 +27,7 @@ public static void WriteCustomPropertyProviderImplementation(SourceProductionCon // Emit the implementation on the annotated type info.TypeHierarchy.WriteSyntax( state: info, - writer: writer, + writer: ref writer, baseTypes: [info.FullyQualifiedCustomPropertyProviderInterfaceName], memberCallbacks: [ WriteCustomPropertyProviderType, @@ -36,7 +36,7 @@ public static void WriteCustomPropertyProviderImplementation(SourceProductionCon WriteCustomPropertyProviderGetStringRepresentation]); // Emit the additional property implementation types, if needed - WriteCustomPropertyImplementationTypes(info, writer); + WriteCustomPropertyImplementationTypes(info, ref writer); // Add the source file for the annotated type context.AddSource($"{info.TypeHierarchy.FullyQualifiedMetadataName}.g.cs", writer.ToStringAndClear()); @@ -47,7 +47,7 @@ public static void WriteCustomPropertyProviderImplementation(SourceProductionCon /// /// /// - private static void WriteCustomPropertyProviderType(CustomPropertyProviderInfo info, IndentedTextWriter writer) + private static void WriteCustomPropertyProviderType(CustomPropertyProviderInfo info, ref IndentedTextWriter writer) { writer.WriteLine($""" /// @@ -60,7 +60,7 @@ private static void WriteCustomPropertyProviderType(CustomPropertyProviderInfo i /// /// /// - private static void WriteCustomPropertyProviderGetCustomProperty(CustomPropertyProviderInfo info, IndentedTextWriter writer) + private static void WriteCustomPropertyProviderGetCustomProperty(CustomPropertyProviderInfo info, ref IndentedTextWriter writer) { writer.WriteLine($""" /// @@ -104,7 +104,7 @@ private static void WriteCustomPropertyProviderGetCustomProperty(CustomPropertyP /// /// /// - private static void WriteCustomPropertyProviderGetIndexedProperty(CustomPropertyProviderInfo info, IndentedTextWriter writer) + private static void WriteCustomPropertyProviderGetIndexedProperty(CustomPropertyProviderInfo info, ref IndentedTextWriter writer) { writer.WriteLine($""" /// @@ -149,7 +149,7 @@ private static void WriteCustomPropertyProviderGetIndexedProperty(CustomProperty /// /// /// - private static void WriteCustomPropertyProviderGetStringRepresentation(CustomPropertyProviderInfo info, IndentedTextWriter writer) + private static void WriteCustomPropertyProviderGetStringRepresentation(CustomPropertyProviderInfo info, ref IndentedTextWriter writer) { writer.WriteLine($$""" /// @@ -165,7 +165,7 @@ private static void WriteCustomPropertyProviderGetStringRepresentation(CustomPro /// /// /// - private static void WriteCustomPropertyImplementationTypes(CustomPropertyProviderInfo info, IndentedTextWriter writer) + private static void WriteCustomPropertyImplementationTypes(CustomPropertyProviderInfo info, ref IndentedTextWriter writer) { // If we have no custom properties, we don't need to emit any additional code if (info.CustomProperties.IsEmpty) @@ -198,11 +198,11 @@ private static void WriteCustomPropertyImplementationTypes(CustomPropertyProvide // Generate the correct implementation types for normal properties or indexer properties if (propertyInfo.IsIndexer) { - WriteIndexedCustomPropertyImplementationType(info, propertyInfo, writer); + WriteIndexedCustomPropertyImplementationType(info, propertyInfo, ref writer); } else { - WriteNonIndexedCustomPropertyImplementationType(info, propertyInfo, writer); + WriteNonIndexedCustomPropertyImplementationType(info, propertyInfo, ref writer); } } } @@ -214,7 +214,7 @@ private static void WriteCustomPropertyImplementationTypes(CustomPropertyProvide /// /// The input instance for the property to generate the implementation type for. /// - private static void WriteNonIndexedCustomPropertyImplementationType(CustomPropertyProviderInfo info, CustomPropertyInfo propertyInfo, IndentedTextWriter writer) + private static void WriteNonIndexedCustomPropertyImplementationType(CustomPropertyProviderInfo info, CustomPropertyInfo propertyInfo, ref IndentedTextWriter writer) { string implementationTypeName = $"{info.TypeHierarchy.Hierarchy[0].QualifiedName}_{propertyInfo.Name}"; @@ -305,7 +305,7 @@ public void SetIndexedValue(object target, object value, object index) /// /// The input instance for the property to generate the implementation type for. /// - private static void WriteIndexedCustomPropertyImplementationType(CustomPropertyProviderInfo info, CustomPropertyInfo propertyInfo, IndentedTextWriter writer) + private static void WriteIndexedCustomPropertyImplementationType(CustomPropertyProviderInfo info, CustomPropertyInfo propertyInfo, ref IndentedTextWriter writer) { string implementationTypeName = $"{info.TypeHierarchy.Hierarchy[0].QualifiedName}_{propertyInfo.Name}"; diff --git a/src/Authoring/WinRT.SourceGenerator2/Extensions/IndentedTextWriterExtensions.cs b/src/Authoring/WinRT.SourceGenerator2/Extensions/IndentedTextWriterExtensions.cs index f449d8fe53..a0deebb335 100644 --- a/src/Authoring/WinRT.SourceGenerator2/Extensions/IndentedTextWriterExtensions.cs +++ b/src/Authoring/WinRT.SourceGenerator2/Extensions/IndentedTextWriterExtensions.cs @@ -91,8 +91,8 @@ public static void WriteSortedUsingDirectives(this ref IndentedTextWriter writer /// The input items to process. /// The instance to invoke for each item. public static void WriteLineSeparatedMembers( - this ref IndentedTextWriter writer, - ReadOnlySpan items, + this scoped ref IndentedTextWriter writer, + scoped ReadOnlySpan items, IndentedTextWriter.Callback callback) { for (int i = 0; i < items.Length; i++) @@ -102,7 +102,7 @@ public static void WriteLineSeparatedMembers( writer.WriteLine(); } - callback(items[i], writer); + callback(items[i], ref writer); } } @@ -114,13 +114,13 @@ public static void WriteLineSeparatedMembers( /// The input items to process. /// The instance to invoke for each item. public static void WriteInitializationExpressions( - this ref IndentedTextWriter writer, - ReadOnlySpan items, + this scoped ref IndentedTextWriter writer, + scoped ReadOnlySpan items, IndentedTextWriter.Callback callback) { for (int i = 0; i < items.Length; i++) { - callback(items[i], writer); + callback(items[i], ref writer); if (i < items.Length - 1) { diff --git a/src/Authoring/WinRT.SourceGenerator2/Helpers/IndentedTextWriter.cs b/src/Authoring/WinRT.SourceGenerator2/Helpers/IndentedTextWriter.cs index 64ef7c1046..fe3c171cc1 100644 --- a/src/Authoring/WinRT.SourceGenerator2/Helpers/IndentedTextWriter.cs +++ b/src/Authoring/WinRT.SourceGenerator2/Helpers/IndentedTextWriter.cs @@ -100,12 +100,13 @@ public void DecreaseIndent() /// Writes a block to the underlying buffer. /// /// A value to close the open block with. + [UnscopedRef] public Block WriteBlock() { WriteLine("{"); IncreaseIndent(); - return new(this); + return new(ref this); } /// @@ -366,25 +367,25 @@ private void WriteRawText(scoped ReadOnlySpan content) /// The type of data to use. /// The input data to use to write into . /// The instance to write into. - public delegate void Callback(T value, IndentedTextWriter writer); + public delegate void Callback(T value, ref IndentedTextWriter writer); /// /// Represents an indented block that needs to be closed. /// /// The input instance to wrap. - public ref struct Block(IndentedTextWriter writer) : IDisposable + public unsafe ref struct Block(ref IndentedTextWriter writer) : IDisposable { /// /// The instance to write to. /// - private IndentedTextWriter _writer = writer; + private IndentedTextWriter* _writer = (IndentedTextWriter*)Unsafe.AsPointer(ref writer); /// public void Dispose() { - IndentedTextWriter writer = _writer; + ref IndentedTextWriter writer = ref *_writer; - _writer = default; + _writer = null; // We check the indentation as a way of knowing if we have reset the field before. // The field itself can't be 'null', but if we have assigned 'default' to it, then diff --git a/src/Authoring/WinRT.SourceGenerator2/Models/HierarchyInfo.cs b/src/Authoring/WinRT.SourceGenerator2/Models/HierarchyInfo.cs index ff56fc6983..e044ca0b97 100644 --- a/src/Authoring/WinRT.SourceGenerator2/Models/HierarchyInfo.cs +++ b/src/Authoring/WinRT.SourceGenerator2/Models/HierarchyInfo.cs @@ -53,9 +53,9 @@ public static HierarchyInfo From(INamedTypeSymbol typeSymbol) /// The callbacks to use to write members into the declared type. public void WriteSyntax( T state, - IndentedTextWriter writer, - ReadOnlySpan baseTypes, - ReadOnlySpan> memberCallbacks) + scoped ref IndentedTextWriter writer, + scoped ReadOnlySpan baseTypes, + scoped ReadOnlySpan> memberCallbacks) { // Write the generated file header writer.WriteLine("// "); @@ -80,7 +80,7 @@ public void WriteSyntax( if (i == 0 && !baseTypes.IsEmpty) { writer.Write(" : "); - writer.WriteInitializationExpressions(baseTypes, static (item, writer) => writer.Write(item)); + writer.WriteInitializationExpressions(baseTypes, static (item, ref writer) => writer.Write(item)); writer.WriteLine(); } else @@ -93,7 +93,7 @@ public void WriteSyntax( } // Generate all nested members - writer.WriteLineSeparatedMembers(memberCallbacks, (callback, writer) => callback(state, writer)); + writer.WriteLineSeparatedMembers(memberCallbacks, (callback, ref writer) => callback(state, ref writer)); // Close all scopes and reduce the indentation for (int i = 0; i < Hierarchy.Length; i++) diff --git a/src/Authoring/WinRT.SourceGenerator2/WinRT.SourceGenerator2.csproj b/src/Authoring/WinRT.SourceGenerator2/WinRT.SourceGenerator2.csproj index 3a2e979d92..01151f646d 100644 --- a/src/Authoring/WinRT.SourceGenerator2/WinRT.SourceGenerator2.csproj +++ b/src/Authoring/WinRT.SourceGenerator2/WinRT.SourceGenerator2.csproj @@ -6,11 +6,11 @@ true + Suppress the following warning: + "RS1041: This compiler extension should not be implemented in an assembly with target framework + '.NET 10.0'. References to other target frameworks will cause the compiler to behave unpredictably." + Using .NET 10 is fine for the scenarios we need to support. + --> $(NoWarn);RS1041 @@ -41,6 +41,9 @@ true + + $(NoWarn);CS8500 + strict From 94e62fc4e1557ba44331b9361272b9a146f07b3d Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 11 Mar 2026 15:59:29 -0700 Subject: [PATCH 060/100] Add .editorconfig to SourceGenerator2 projects Add .editorconfig files to src/Authoring/WinRT.SourceGenerator2 and src/Tests/SourceGenerator2Test to enforce LF line endings (end_of_line = lf) for all files. --- src/Authoring/WinRT.SourceGenerator2/.editorconfig | 5 +++++ src/Tests/SourceGenerator2Test/.editorconfig | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 src/Authoring/WinRT.SourceGenerator2/.editorconfig create mode 100644 src/Tests/SourceGenerator2Test/.editorconfig diff --git a/src/Authoring/WinRT.SourceGenerator2/.editorconfig b/src/Authoring/WinRT.SourceGenerator2/.editorconfig new file mode 100644 index 0000000000..41a979c4b6 --- /dev/null +++ b/src/Authoring/WinRT.SourceGenerator2/.editorconfig @@ -0,0 +1,5 @@ +# All files +[*] + +# New line preferences +end_of_line = lf \ No newline at end of file diff --git a/src/Tests/SourceGenerator2Test/.editorconfig b/src/Tests/SourceGenerator2Test/.editorconfig new file mode 100644 index 0000000000..41a979c4b6 --- /dev/null +++ b/src/Tests/SourceGenerator2Test/.editorconfig @@ -0,0 +1,5 @@ +# All files +[*] + +# New line preferences +end_of_line = lf \ No newline at end of file From 37d1be5ef212e533f7b360560859bc7060d590ca Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 11 Mar 2026 16:20:28 -0700 Subject: [PATCH 061/100] Use pointers in handlers and Dispose Avoid copying the ref struct by switching to unsafe pointer usage: Dispose now uses an IndentedTextWriter* and checks for null before calling DecreaseIndent/WriteLine to ensure the block is closed only once. The WriteInterpolatedStringHandler was changed to an unsafe ref struct that stores a pointer to the writer via Unsafe.AsPointer(ref Unsafe.AsRef(in writer)), and its Append* methods now call through the pointer. WriteIfInterpolatedStringHandler was updated to accept the writer by in and forward it using in to construct the inner handler. These changes prevent accidental ref-struct copies and ensure correct lifetime handling when compiler-generated interpolated string handlers are used. --- .../Helpers/IndentedTextWriter.cs | 34 +++++++++---------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/src/Authoring/WinRT.SourceGenerator2/Helpers/IndentedTextWriter.cs b/src/Authoring/WinRT.SourceGenerator2/Helpers/IndentedTextWriter.cs index fe3c171cc1..dba02768a6 100644 --- a/src/Authoring/WinRT.SourceGenerator2/Helpers/IndentedTextWriter.cs +++ b/src/Authoring/WinRT.SourceGenerator2/Helpers/IndentedTextWriter.cs @@ -383,17 +383,15 @@ public unsafe ref struct Block(ref IndentedTextWriter writer) : IDisposable /// public void Dispose() { - ref IndentedTextWriter writer = ref *_writer; + IndentedTextWriter* writer = _writer; _writer = null; - // We check the indentation as a way of knowing if we have reset the field before. - // The field itself can't be 'null', but if we have assigned 'default' to it, then - // that field will be 'null' even though it's always set from the constructor. - if (writer._currentIndentation is not null) + // Make sure to only close the block the first time this value is disposed + if (writer is not null) { - writer.DecreaseIndent(); - writer.WriteLine("}"); + writer->DecreaseIndent(); + writer->WriteLine("}"); } } } @@ -403,26 +401,26 @@ public void Dispose() /// [EditorBrowsable(EditorBrowsableState.Never)] [InterpolatedStringHandler] - public readonly ref struct WriteInterpolatedStringHandler + public readonly unsafe ref struct WriteInterpolatedStringHandler { /// The associated to which to append. - private readonly IndentedTextWriter _writer; + private readonly IndentedTextWriter* _writer; /// Creates a handler used to append an interpolated string into a . /// The number of constant characters outside of interpolation expressions in the interpolated string. /// The number of interpolation expressions in the interpolated string. /// The associated to which to append. /// This is intended to be called only by compiler-generated code. Arguments are not validated as they'd otherwise be for members intended to be used directly. - public WriteInterpolatedStringHandler(int literalLength, int formattedCount, IndentedTextWriter writer) + public WriteInterpolatedStringHandler(int literalLength, int formattedCount, in IndentedTextWriter writer) { - _writer = writer; + _writer = (IndentedTextWriter*)Unsafe.AsPointer(ref Unsafe.AsRef(in writer)); } /// Writes the specified string to the handler. /// The string to write. public void AppendLiteral(string value) { - _writer.Write(value); + _writer->Write(value); } /// Writes the specified value to the handler. @@ -436,7 +434,7 @@ public void AppendFormatted(string? value) /// The span to write. public void AppendFormatted(scoped ReadOnlySpan value) { - _writer.Write(value); + _writer->Write(value); } /// Writes the specified value to the handler. @@ -446,7 +444,7 @@ public void AppendFormatted(T? value) { if (value is not null) { - _writer.Write(value.ToString()!); + _writer->Write(value.ToString()!); } } @@ -459,11 +457,11 @@ public void AppendFormatted(T? value, string? format) { if (value is IFormattable) { - _writer.Write(((IFormattable)value).ToString(format, CultureInfo.InvariantCulture)); + _writer->Write(((IFormattable)value).ToString(format, CultureInfo.InvariantCulture)); } else if (value is not null) { - _writer.Write(value.ToString()!); + _writer->Write(value.ToString()!); } } } @@ -485,11 +483,11 @@ public readonly ref struct WriteIfInterpolatedStringHandler /// The condition to use to decide whether or not to write content. /// A value indicating whether formatting should proceed. /// This is intended to be called only by compiler-generated code. Arguments are not validated as they'd otherwise be for members intended to be used directly. - public WriteIfInterpolatedStringHandler(int literalLength, int formattedCount, IndentedTextWriter writer, bool condition, out bool shouldAppend) + public WriteIfInterpolatedStringHandler(int literalLength, int formattedCount, in IndentedTextWriter writer, bool condition, out bool shouldAppend) { if (condition) { - _writer = new WriteInterpolatedStringHandler(literalLength, formattedCount, writer); + _writer = new WriteInterpolatedStringHandler(literalLength, formattedCount, in writer); shouldAppend = true; } From 75be5e78e93308ba6dd3582bde10a95a33c85c2b Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 11 Mar 2026 18:48:33 -0700 Subject: [PATCH 062/100] Improve WriteLine skip logic for trailing chars Trim trailing spaces before evaluating skip condition in IndentedTextWriter.WriteLine(bool). Treat both a double newline ("\n\n") and an open-brace followed by newline ("{\n") as existing line endings to avoid inserting redundant lines after braces or when only spaces follow. --- .../WinRT.SourceGenerator2/Helpers/IndentedTextWriter.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Authoring/WinRT.SourceGenerator2/Helpers/IndentedTextWriter.cs b/src/Authoring/WinRT.SourceGenerator2/Helpers/IndentedTextWriter.cs index dba02768a6..66a5610369 100644 --- a/src/Authoring/WinRT.SourceGenerator2/Helpers/IndentedTextWriter.cs +++ b/src/Authoring/WinRT.SourceGenerator2/Helpers/IndentedTextWriter.cs @@ -226,9 +226,14 @@ public readonly void WriteIf(bool condition, [InterpolatedStringHandlerArgument( /// Indicates whether to skip adding the line if there already is one. public void WriteLine(bool skipIfPresent = false) { - if (skipIfPresent && _handler.Text is [.., '\n', '\n']) + if (skipIfPresent) { - return; + ReadOnlySpan trimmedText = _handler.Text.TrimEnd(' '); + + if (trimmedText is [.., '\n', '\n'] or [.., '{', '\n']) + { + return; + } } _handler.AppendFormatted(DefaultNewLine); From 2c6b1924bab3d0557435d333284e3d238cfbc411 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 11 Mar 2026 18:49:13 -0700 Subject: [PATCH 063/100] Refactor CustomPropertyProvider code generation Clean up and improve the generated custom property implementations: introduce a reusable implementationTypeName for indexers, normalize and escape fully-qualified type/indexer names, and switch to simpler using directives (including common namespaces). Add XML doc comments and Generated attributes for the emitted types, emit ICustomProperty with simple type references, and use "this" as the indexer Name. Also minor writer cleanups (consistent skipIfPresent usage) to make the produced code clearer and more tooling-friendly. --- .../CustomPropertyProviderGenerator.Emit.cs | 37 +++++++++++++++---- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Emit.cs b/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Emit.cs index 79ea9f838c..35c77f6230 100644 --- a/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Emit.cs +++ b/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Emit.cs @@ -129,17 +129,20 @@ private static void WriteCustomPropertyProviderGetIndexedProperty(CustomProperty continue; } + string implementationTypeName = $"{info.TypeHierarchy.Hierarchy[0].QualifiedName}_this_{propertyInfo.FullyQualifiedIndexerTypeName.Replace("global::", "").EscapeIdentifierName()}"; + // 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; + return global::WindowsRuntime.Xaml.Generated.{{implementationTypeName}}.Instance; } """, isMultiline: true); } // If there's no matching property, just return 'null' + writer.WriteLine(skipIfPresent: true); writer.WriteLine("return null;"); } } @@ -179,9 +182,12 @@ private static void WriteCustomPropertyImplementationTypes(CustomPropertyProvide using (writer.WriteBlock()) { - // Using declarations for well-known types we can refer to directly + // Using declarations for well-known namespaces we can use with simple names writer.WriteLine("using global::System;"); - writer.WriteLine($"using global:{info.FullyQualifiedCustomPropertyProviderInterfaceName};"); + writer.WriteLine("using global::System.CodeDom.Compiler"); + writer.WriteLine("using global::System.Diagnostics"); + writer.WriteLine("using global::System.Diagnostics.CodeAnalysis"); + writer.WriteLine($"using global::{info.FullyQualifiedCustomPropertyProviderInterfaceName.Replace(".ICustomPropertyProvider", "")};"); writer.WriteLine(); // Write all custom property implementation types @@ -216,12 +222,19 @@ private static void WriteCustomPropertyImplementationTypes(CustomPropertyProvide /// private static void WriteNonIndexedCustomPropertyImplementationType(CustomPropertyProviderInfo info, CustomPropertyInfo propertyInfo, ref IndentedTextWriter writer) { + string userTypeName = info.TypeHierarchy.GetFullyQualifiedTypeName().Replace("global::", ""); 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}"); + // file sealed class : ICustomProperty + writer.WriteLine($""" + /// + /// The implementation for . + /// + """, isMultiline: true); + writer.WriteGeneratedAttributes(nameof(CustomPropertyProviderGenerator), useFullyQualifiedTypeNames: false); + writer.WriteLine($"file sealed class {implementationTypeName} : ICustomProperty"); using (writer.WriteBlock()) { @@ -307,10 +320,18 @@ public void SetIndexedValue(object target, object value, object index) /// private static void WriteIndexedCustomPropertyImplementationType(CustomPropertyProviderInfo info, CustomPropertyInfo propertyInfo, ref IndentedTextWriter writer) { - string implementationTypeName = $"{info.TypeHierarchy.Hierarchy[0].QualifiedName}_{propertyInfo.Name}"; + string userTypeName = info.TypeHierarchy.GetFullyQualifiedTypeName().Replace("global::", ""); + string indexerTypeName = propertyInfo.FullyQualifiedIndexerTypeName!.Replace("global::", ""); + string implementationTypeName = $"{info.TypeHierarchy.Hierarchy[0].QualifiedName}_this_{indexerTypeName.EscapeIdentifierName()}"; // Emit the implementation type, same as above - writer.WriteLine($"file sealed class {implementationTypeName} : {info.FullyQualifiedCustomPropertyInterfaceName}"); + writer.WriteLine($""" + /// + /// The implementation for 's indexer. + /// + """, isMultiline: true); + writer.WriteGeneratedAttributes(nameof(CustomPropertyProviderGenerator), useFullyQualifiedTypeNames: false); + writer.WriteLine($"file sealed class {implementationTypeName} : ICustomProperty"); using (writer.WriteBlock()) { @@ -328,7 +349,7 @@ private static void WriteIndexedCustomPropertyImplementationType(CustomPropertyP public bool CanWrite => {{propertyInfo.CanWrite.ToString().ToLowerInvariant()}}; /// - public string Name => "{{propertyInfo.Name}}"; + public string Name => "this"; /// public Type Type => typeof({{propertyInfo.FullyQualifiedTypeName}}); From 0d878bf2c6a9320d5a5df182dbdd504c1cdb40e2 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 11 Mar 2026 18:49:49 -0700 Subject: [PATCH 064/100] Update test expected output for property provider Update the expected generated source in Test_CustomPropertyProviderGenerator.cs. The new expected string includes an auto-generated header and pragma, a partial MyClass implementing Microsoft.UI.Xaml.Data.ICustomPropertyProvider (Type, GetCustomProperty, GetIndexedProperty, GetStringRepresentation), and generated ICustomProperty implementations in the WindowsRuntime.Xaml.Generated namespace (MyClass_Name, MyClass_Age, MyClass_this__int) with singleton instances, GeneratedCode attributes, and appropriate getters/setters. Keeps the VerifySources call targeting the updated "MyNamespace.MyClass.g.cs" result and preserves the assembly version placeholder. --- .../Test_CustomPropertyProviderGenerator.cs | 200 +++++++++++++++++- 1 file changed, 199 insertions(+), 1 deletion(-) diff --git a/src/Tests/SourceGenerator2Test/Test_CustomPropertyProviderGenerator.cs b/src/Tests/SourceGenerator2Test/Test_CustomPropertyProviderGenerator.cs index 39b3208539..144411c145 100644 --- a/src/Tests/SourceGenerator2Test/Test_CustomPropertyProviderGenerator.cs +++ b/src/Tests/SourceGenerator2Test/Test_CustomPropertyProviderGenerator.cs @@ -33,7 +33,205 @@ public int this[int index] """; const string result = """" - + // + #pragma warning disable + + namespace MyNamespace + { + /// + partial class MyClass : Microsoft.UI.Xaml.Data.ICustomPropertyProvider + { + /// + global::System.Type Microsoft.UI.Xaml.Data.ICustomPropertyProvider.Type => typeof(MyClass); + + /// + Microsoft.UI.Xaml.Data.ICustomProperty Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetCustomProperty(string name) + { + return name switch + { + nameof(Name) => global::WindowsRuntime.Xaml.Generated.MyClass_Name.Instance, + nameof(Age) => global::WindowsRuntime.Xaml.Generated.MyClass_Age.Instance, + _ => null + } + } + + /// + Microsoft.UI.Xaml.Data.ICustomProperty Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetIndexedProperty(string name, global::System.Type type) + { + if (type == typeof(int)) + { + return global::WindowsRuntime.Xaml.Generated.MyClass_this__int.Instance; + } + + return null; + } + + /// + string Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetStringRepresentation() + { + return ToString(); + } + } + } + + namespace WindowsRuntime.Xaml.Generated + { + using global::System; + using global::System.CodeDom.Compiler + using global::System.Diagnostics + using global::System.Diagnostics.CodeAnalysis + using global::Microsoft.UI.Xaml.Data; + + /// + /// The implementation for . + /// + [GeneratedCode("CustomPropertyProviderGenerator", )] + [DebuggerNonUserCode] + [ExcludeFromCodeCoverage] + file sealed class MyClass_Name : ICustomProperty + { + /// + /// Gets the singleton instance for this custom property. + /// + public static readonly MyClass_Name Instance = new(); + + /// + public bool CanRead => true; + + /// + public bool CanWrite => false; + + /// + public string Name => "Name"; + + /// + public Type Type => typeof(string); + + /// + public object GetValue(object target) + { + return ((global::MyNamespace.MyClass)target).Name; + } + + /// + public void SetValue(object target, object value) + { + ((global::MyNamespace.MyClass)target).Name = (string)value; + } + + /// + public object GetIndexedValue(object target, object index) + { + throw new NotSupportedException(); + } + + /// + public void SetIndexedValue(object target, object value, object index) + { + throw new NotSupportedException(); + } + } + + /// + /// The implementation for . + /// + [GeneratedCode("CustomPropertyProviderGenerator", )] + [DebuggerNonUserCode] + [ExcludeFromCodeCoverage] + file sealed class MyClass_Age : ICustomProperty + { + /// + /// Gets the singleton instance for this custom property. + /// + public static readonly MyClass_Age Instance = new(); + + /// + public bool CanRead => true; + + /// + public bool CanWrite => true; + + /// + public string Name => "Age"; + + /// + public Type Type => typeof(int); + + /// + public object GetValue(object target) + { + return ((global::MyNamespace.MyClass)target).Age; + } + + /// + public void SetValue(object target, object value) + { + ((global::MyNamespace.MyClass)target).Age = (int)value; + } + + /// + public object GetIndexedValue(object target, object index) + { + throw new NotSupportedException(); + } + + /// + public void SetIndexedValue(object target, object value, object index) + { + throw new NotSupportedException(); + } + } + + /// + /// The implementation for 's indexer. + /// + [GeneratedCode("CustomPropertyProviderGenerator", )] + [DebuggerNonUserCode] + [ExcludeFromCodeCoverage] + file sealed class MyClass_this__int : ICustomProperty + { + /// + /// Gets the singleton instance for this custom property. + /// + public static readonly MyClass_this__int Instance = new(); + + /// + public bool CanRead => true; + + /// + public bool CanWrite => true; + + /// + public string Name => "this"; + + /// + public Type Type => typeof(int); + + /// + public object GetValue(object target) + { + throw new NotSupportedException(); + } + + /// + public void SetValue(object target, object value) + { + throw new NotSupportedException(); + } + + /// + public object GetIndexedValue(object target, object index) + { + return ((global::MyNamespace.MyClass)target)[(int)index]; + } + + /// + public void SetIndexedValue(object target, object value, object index) + { + ((global::MyNamespace.MyClass)target)[(int)index] = (int)value; + } + } + } """"; CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyClass.g.cs", result)); From c4e1a30d301b84255d3ec4b040f5b5de44c6c998 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 11 Mar 2026 18:52:59 -0700 Subject: [PATCH 065/100] Nits --- .../Test_CustomPropertyProviderGenerator.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Tests/SourceGenerator2Test/Test_CustomPropertyProviderGenerator.cs b/src/Tests/SourceGenerator2Test/Test_CustomPropertyProviderGenerator.cs index 144411c145..4dfd91e79b 100644 --- a/src/Tests/SourceGenerator2Test/Test_CustomPropertyProviderGenerator.cs +++ b/src/Tests/SourceGenerator2Test/Test_CustomPropertyProviderGenerator.cs @@ -32,7 +32,7 @@ public int this[int index] } """; - const string result = """" + const string result = """ // #pragma warning disable @@ -232,7 +232,7 @@ public void SetIndexedValue(object target, object value, object index) } } } - """"; + """; CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyClass.g.cs", result)); } From 1ba4218a35dfd2108b55e7e367252815f7264058 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 11 Mar 2026 18:54:36 -0700 Subject: [PATCH 066/100] Initialize IndentedTextWriter with estimated size Use a precomputed approximate literalLength (base 2048 bytes plus 2048 per custom property) when constructing IndentedTextWriter instead of zero. This reduces buffer copies/reallocations during source generation by scaling the initial capacity with info.CustomProperties.Length. --- .../CustomPropertyProviderGenerator.Emit.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Emit.cs b/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Emit.cs index 35c77f6230..e987777161 100644 --- a/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Emit.cs +++ b/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Emit.cs @@ -22,7 +22,12 @@ private static class Emit /// The input state to use. public static void WriteCustomPropertyProviderImplementation(SourceProductionContext context, CustomPropertyProviderInfo info) { - IndentedTextWriter writer = new(literalLength: 0, formattedCount: 0); // TODO: adjust the literal length + const int ApproximateTypeDeclarationLength = 2048; + + // Approximate a close enough starting length to reduce copies + int approximateLiteralLength = ApproximateTypeDeclarationLength + (ApproximateTypeDeclarationLength * info.CustomProperties.Length); + + IndentedTextWriter writer = new(literalLength: approximateLiteralLength, formattedCount: 0); // Emit the implementation on the annotated type info.TypeHierarchy.WriteSyntax( From d5b7ec5825a1dedbf177061526d2549fcb75f663 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 11 Mar 2026 19:06:08 -0700 Subject: [PATCH 067/100] Add tests for no properties, normal-only, and indexer-only scenarios Add test cases to Test_CustomPropertyProviderGenerator covering the three core structural code paths in the generator: - ValidClass_NoProperties: type with no public properties at all, verifying both GetCustomProperty and GetIndexedProperty return null directly and no WindowsRuntime.Xaml.Generated namespace is emitted - ValidClass_NormalPropertiesOnly: type with normal properties but no indexers, verifying the switch expression in GetCustomProperty and the null fast-path in GetIndexedProperty - ValidClass_IndexerPropertiesOnly: type with only an indexer and no normal properties, verifying the null fast-path in GetCustomProperty and the type-check logic in GetIndexedProperty Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Test_CustomPropertyProviderGenerator.cs | 338 +++++++++++++++++- 1 file changed, 337 insertions(+), 1 deletion(-) diff --git a/src/Tests/SourceGenerator2Test/Test_CustomPropertyProviderGenerator.cs b/src/Tests/SourceGenerator2Test/Test_CustomPropertyProviderGenerator.cs index 4dfd91e79b..380e7b773d 100644 --- a/src/Tests/SourceGenerator2Test/Test_CustomPropertyProviderGenerator.cs +++ b/src/Tests/SourceGenerator2Test/Test_CustomPropertyProviderGenerator.cs @@ -236,4 +236,340 @@ public void SetIndexedValue(object target, object value, object index) CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyClass.g.cs", result)); } -} \ No newline at end of file + + [TestMethod] + public async Task ValidClass_NoProperties() + { + const string source = """ + using WindowsRuntime.Xaml; + + namespace MyNamespace; + + [GeneratedCustomPropertyProvider] + public partial class MyClass + { + } + """; + + const string result = """ + // + #pragma warning disable + + namespace MyNamespace + { + /// + partial class MyClass : Microsoft.UI.Xaml.Data.ICustomPropertyProvider + { + /// + global::System.Type Microsoft.UI.Xaml.Data.ICustomPropertyProvider.Type => typeof(MyClass); + + /// + Microsoft.UI.Xaml.Data.ICustomProperty Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetCustomProperty(string name) + { + return null; + } + + /// + Microsoft.UI.Xaml.Data.ICustomProperty Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetIndexedProperty(string name, global::System.Type type) + { + return null; + } + + /// + string Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetStringRepresentation() + { + return ToString(); + } + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyClass.g.cs", result)); + } + + [TestMethod] + public async Task ValidClass_NormalPropertiesOnly() + { + const string source = """ + using WindowsRuntime.Xaml; + + namespace MyNamespace; + + [GeneratedCustomPropertyProvider] + public partial class MyClass + { + public string Name => ""; + + public int Age { get; set; } + } + """; + + const string result = """ + // + #pragma warning disable + + namespace MyNamespace + { + /// + partial class MyClass : Microsoft.UI.Xaml.Data.ICustomPropertyProvider + { + /// + global::System.Type Microsoft.UI.Xaml.Data.ICustomPropertyProvider.Type => typeof(MyClass); + + /// + Microsoft.UI.Xaml.Data.ICustomProperty Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetCustomProperty(string name) + { + return name switch + { + nameof(Name) => global::WindowsRuntime.Xaml.Generated.MyClass_Name.Instance, + nameof(Age) => global::WindowsRuntime.Xaml.Generated.MyClass_Age.Instance, + _ => null + } + } + + /// + Microsoft.UI.Xaml.Data.ICustomProperty Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetIndexedProperty(string name, global::System.Type type) + { + return null; + } + + /// + string Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetStringRepresentation() + { + return ToString(); + } + } + } + + namespace WindowsRuntime.Xaml.Generated + { + using global::System; + using global::System.CodeDom.Compiler + using global::System.Diagnostics + using global::System.Diagnostics.CodeAnalysis + using global::Microsoft.UI.Xaml.Data; + + /// + /// The implementation for . + /// + [GeneratedCode("CustomPropertyProviderGenerator", )] + [DebuggerNonUserCode] + [ExcludeFromCodeCoverage] + file sealed class MyClass_Name : ICustomProperty + { + /// + /// Gets the singleton instance for this custom property. + /// + public static readonly MyClass_Name Instance = new(); + + /// + public bool CanRead => true; + + /// + public bool CanWrite => false; + + /// + public string Name => "Name"; + + /// + public Type Type => typeof(string); + + /// + public object GetValue(object target) + { + return ((global::MyNamespace.MyClass)target).Name; + } + + /// + public void SetValue(object target, object value) + { + ((global::MyNamespace.MyClass)target).Name = (string)value; + } + + /// + public object GetIndexedValue(object target, object index) + { + throw new NotSupportedException(); + } + + /// + public void SetIndexedValue(object target, object value, object index) + { + throw new NotSupportedException(); + } + } + + /// + /// The implementation for . + /// + [GeneratedCode("CustomPropertyProviderGenerator", )] + [DebuggerNonUserCode] + [ExcludeFromCodeCoverage] + file sealed class MyClass_Age : ICustomProperty + { + /// + /// Gets the singleton instance for this custom property. + /// + public static readonly MyClass_Age Instance = new(); + + /// + public bool CanRead => true; + + /// + public bool CanWrite => true; + + /// + public string Name => "Age"; + + /// + public Type Type => typeof(int); + + /// + public object GetValue(object target) + { + return ((global::MyNamespace.MyClass)target).Age; + } + + /// + public void SetValue(object target, object value) + { + ((global::MyNamespace.MyClass)target).Age = (int)value; + } + + /// + public object GetIndexedValue(object target, object index) + { + throw new NotSupportedException(); + } + + /// + public void SetIndexedValue(object target, object value, object index) + { + throw new NotSupportedException(); + } + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyClass.g.cs", result)); + } + + [TestMethod] + public async Task ValidClass_IndexerPropertiesOnly() + { + const string source = """ + using WindowsRuntime.Xaml; + + namespace MyNamespace; + + [GeneratedCustomPropertyProvider] + public partial class MyClass + { + public int this[int index] + { + get => 0; + set { } + } + } + """; + + const string result = """ + // + #pragma warning disable + + namespace MyNamespace + { + /// + partial class MyClass : Microsoft.UI.Xaml.Data.ICustomPropertyProvider + { + /// + global::System.Type Microsoft.UI.Xaml.Data.ICustomPropertyProvider.Type => typeof(MyClass); + + /// + Microsoft.UI.Xaml.Data.ICustomProperty Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetCustomProperty(string name) + { + return null; + } + + /// + Microsoft.UI.Xaml.Data.ICustomProperty Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetIndexedProperty(string name, global::System.Type type) + { + if (type == typeof(int)) + { + return global::WindowsRuntime.Xaml.Generated.MyClass_this__int.Instance; + } + + return null; + } + + /// + string Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetStringRepresentation() + { + return ToString(); + } + } + } + + namespace WindowsRuntime.Xaml.Generated + { + using global::System; + using global::System.CodeDom.Compiler + using global::System.Diagnostics + using global::System.Diagnostics.CodeAnalysis + using global::Microsoft.UI.Xaml.Data; + + /// + /// The implementation for 's indexer. + /// + [GeneratedCode("CustomPropertyProviderGenerator", )] + [DebuggerNonUserCode] + [ExcludeFromCodeCoverage] + file sealed class MyClass_this__int : ICustomProperty + { + /// + /// Gets the singleton instance for this custom property. + /// + public static readonly MyClass_this__int Instance = new(); + + /// + public bool CanRead => true; + + /// + public bool CanWrite => true; + + /// + public string Name => "this"; + + /// + public Type Type => typeof(int); + + /// + public object GetValue(object target) + { + throw new NotSupportedException(); + } + + /// + public void SetValue(object target, object value) + { + throw new NotSupportedException(); + } + + /// + public object GetIndexedValue(object target, object index) + { + return ((global::MyNamespace.MyClass)target)[(int)index]; + } + + /// + public void SetIndexedValue(object target, object value, object index) + { + ((global::MyNamespace.MyClass)target)[(int)index] = (int)value; + } + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyClass.g.cs", result)); + } +} From a1f26f2317626edbe07c4e31b0ffa945b487e5b0 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 11 Mar 2026 19:06:20 -0700 Subject: [PATCH 068/100] Add tests for read-only, write-only, static, and read-only indexer properties Add test cases to Test_CustomPropertyProviderGenerator covering property accessor variant code paths in the generator: - ValidClass_ReadOnlyProperty: get-only property, verifying CanRead is true and CanWrite is false - ValidClass_WriteOnlyProperty: set-only property, verifying CanRead is false and CanWrite is true - ValidClass_StaticProperty: static property, verifying the generated get/set dispatching uses the class name directly without target cast - ValidClass_ReadOnlyIndexer: get-only indexer, verifying CanRead is true and CanWrite is false for indexed properties Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Test_CustomPropertyProviderGenerator.cs | 459 +++++++++++++++++- 1 file changed, 458 insertions(+), 1 deletion(-) diff --git a/src/Tests/SourceGenerator2Test/Test_CustomPropertyProviderGenerator.cs b/src/Tests/SourceGenerator2Test/Test_CustomPropertyProviderGenerator.cs index 380e7b773d..10f48f16f7 100644 --- a/src/Tests/SourceGenerator2Test/Test_CustomPropertyProviderGenerator.cs +++ b/src/Tests/SourceGenerator2Test/Test_CustomPropertyProviderGenerator.cs @@ -572,4 +572,461 @@ public void SetIndexedValue(object target, object value, object index) CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyClass.g.cs", result)); } -} + + [TestMethod] + public async Task ValidClass_ReadOnlyProperty() + { + const string source = """ + using WindowsRuntime.Xaml; + + namespace MyNamespace; + + [GeneratedCustomPropertyProvider] + public partial class MyClass + { + public string Name => ""; + } + """; + + const string result = """ + // + #pragma warning disable + + namespace MyNamespace + { + /// + partial class MyClass : Microsoft.UI.Xaml.Data.ICustomPropertyProvider + { + /// + global::System.Type Microsoft.UI.Xaml.Data.ICustomPropertyProvider.Type => typeof(MyClass); + + /// + Microsoft.UI.Xaml.Data.ICustomProperty Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetCustomProperty(string name) + { + return name switch + { + nameof(Name) => global::WindowsRuntime.Xaml.Generated.MyClass_Name.Instance, + _ => null + } + } + + /// + Microsoft.UI.Xaml.Data.ICustomProperty Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetIndexedProperty(string name, global::System.Type type) + { + return null; + } + + /// + string Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetStringRepresentation() + { + return ToString(); + } + } + } + + namespace WindowsRuntime.Xaml.Generated + { + using global::System; + using global::System.CodeDom.Compiler + using global::System.Diagnostics + using global::System.Diagnostics.CodeAnalysis + using global::Microsoft.UI.Xaml.Data; + + /// + /// The implementation for . + /// + [GeneratedCode("CustomPropertyProviderGenerator", )] + [DebuggerNonUserCode] + [ExcludeFromCodeCoverage] + file sealed class MyClass_Name : ICustomProperty + { + /// + /// Gets the singleton instance for this custom property. + /// + public static readonly MyClass_Name Instance = new(); + + /// + public bool CanRead => true; + + /// + public bool CanWrite => false; + + /// + public string Name => "Name"; + + /// + public Type Type => typeof(string); + + /// + public object GetValue(object target) + { + return ((global::MyNamespace.MyClass)target).Name; + } + + /// + public void SetValue(object target, object value) + { + ((global::MyNamespace.MyClass)target).Name = (string)value; + } + + /// + public object GetIndexedValue(object target, object index) + { + throw new NotSupportedException(); + } + + /// + public void SetIndexedValue(object target, object value, object index) + { + throw new NotSupportedException(); + } + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyClass.g.cs", result)); + } + + [TestMethod] + public async Task ValidClass_WriteOnlyProperty() + { + const string source = """ + using WindowsRuntime.Xaml; + + namespace MyNamespace; + + [GeneratedCustomPropertyProvider] + public partial class MyClass + { + public string Name { set { } } + } + """; + + const string result = """ + // + #pragma warning disable + + namespace MyNamespace + { + /// + partial class MyClass : Microsoft.UI.Xaml.Data.ICustomPropertyProvider + { + /// + global::System.Type Microsoft.UI.Xaml.Data.ICustomPropertyProvider.Type => typeof(MyClass); + + /// + Microsoft.UI.Xaml.Data.ICustomProperty Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetCustomProperty(string name) + { + return name switch + { + nameof(Name) => global::WindowsRuntime.Xaml.Generated.MyClass_Name.Instance, + _ => null + } + } + + /// + Microsoft.UI.Xaml.Data.ICustomProperty Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetIndexedProperty(string name, global::System.Type type) + { + return null; + } + + /// + string Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetStringRepresentation() + { + return ToString(); + } + } + } + + namespace WindowsRuntime.Xaml.Generated + { + using global::System; + using global::System.CodeDom.Compiler + using global::System.Diagnostics + using global::System.Diagnostics.CodeAnalysis + using global::Microsoft.UI.Xaml.Data; + + /// + /// The implementation for . + /// + [GeneratedCode("CustomPropertyProviderGenerator", )] + [DebuggerNonUserCode] + [ExcludeFromCodeCoverage] + file sealed class MyClass_Name : ICustomProperty + { + /// + /// Gets the singleton instance for this custom property. + /// + public static readonly MyClass_Name Instance = new(); + + /// + public bool CanRead => false; + + /// + public bool CanWrite => true; + + /// + public string Name => "Name"; + + /// + public Type Type => typeof(string); + + /// + public object GetValue(object target) + { + return ((global::MyNamespace.MyClass)target).Name; + } + + /// + public void SetValue(object target, object value) + { + ((global::MyNamespace.MyClass)target).Name = (string)value; + } + + /// + public object GetIndexedValue(object target, object index) + { + throw new NotSupportedException(); + } + + /// + public void SetIndexedValue(object target, object value, object index) + { + throw new NotSupportedException(); + } + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyClass.g.cs", result)); + } + + [TestMethod] + public async Task ValidClass_StaticProperty() + { + const string source = """ + using WindowsRuntime.Xaml; + + namespace MyNamespace; + + [GeneratedCustomPropertyProvider] + public partial class MyClass + { + public static int Count { get; set; } + } + """; + + const string result = """ + // + #pragma warning disable + + namespace MyNamespace + { + /// + partial class MyClass : Microsoft.UI.Xaml.Data.ICustomPropertyProvider + { + /// + global::System.Type Microsoft.UI.Xaml.Data.ICustomPropertyProvider.Type => typeof(MyClass); + + /// + Microsoft.UI.Xaml.Data.ICustomProperty Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetCustomProperty(string name) + { + return name switch + { + nameof(Count) => global::WindowsRuntime.Xaml.Generated.MyClass_Count.Instance, + _ => null + } + } + + /// + Microsoft.UI.Xaml.Data.ICustomProperty Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetIndexedProperty(string name, global::System.Type type) + { + return null; + } + + /// + string Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetStringRepresentation() + { + return ToString(); + } + } + } + + namespace WindowsRuntime.Xaml.Generated + { + using global::System; + using global::System.CodeDom.Compiler + using global::System.Diagnostics + using global::System.Diagnostics.CodeAnalysis + using global::Microsoft.UI.Xaml.Data; + + /// + /// The implementation for . + /// + [GeneratedCode("CustomPropertyProviderGenerator", )] + [DebuggerNonUserCode] + [ExcludeFromCodeCoverage] + file sealed class MyClass_Count : ICustomProperty + { + /// + /// Gets the singleton instance for this custom property. + /// + public static readonly MyClass_Count Instance = new(); + + /// + public bool CanRead => true; + + /// + public bool CanWrite => true; + + /// + public string Name => "Count"; + + /// + public Type Type => typeof(int); + + /// + public object GetValue(object target) + { + return global::MyNamespace.MyClass.Count; + } + + /// + public void SetValue(object target, object value) + { + global::MyNamespace.MyClass.Count = (int)value; + } + + /// + public object GetIndexedValue(object target, object index) + { + throw new NotSupportedException(); + } + + /// + public void SetIndexedValue(object target, object value, object index) + { + throw new NotSupportedException(); + } + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyClass.g.cs", result)); + } + + [TestMethod] + public async Task ValidClass_ReadOnlyIndexer() + { + const string source = """ + using WindowsRuntime.Xaml; + + namespace MyNamespace; + + [GeneratedCustomPropertyProvider] + public partial class MyClass + { + public string this[int index] => ""; + } + """; + + const string result = """ + // + #pragma warning disable + + namespace MyNamespace + { + /// + partial class MyClass : Microsoft.UI.Xaml.Data.ICustomPropertyProvider + { + /// + global::System.Type Microsoft.UI.Xaml.Data.ICustomPropertyProvider.Type => typeof(MyClass); + + /// + Microsoft.UI.Xaml.Data.ICustomProperty Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetCustomProperty(string name) + { + return null; + } + + /// + Microsoft.UI.Xaml.Data.ICustomProperty Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetIndexedProperty(string name, global::System.Type type) + { + if (type == typeof(int)) + { + return global::WindowsRuntime.Xaml.Generated.MyClass_this__int.Instance; + } + + return null; + } + + /// + string Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetStringRepresentation() + { + return ToString(); + } + } + } + + namespace WindowsRuntime.Xaml.Generated + { + using global::System; + using global::System.CodeDom.Compiler + using global::System.Diagnostics + using global::System.Diagnostics.CodeAnalysis + using global::Microsoft.UI.Xaml.Data; + + /// + /// The implementation for 's indexer. + /// + [GeneratedCode("CustomPropertyProviderGenerator", )] + [DebuggerNonUserCode] + [ExcludeFromCodeCoverage] + file sealed class MyClass_this__int : ICustomProperty + { + /// + /// Gets the singleton instance for this custom property. + /// + public static readonly MyClass_this__int Instance = new(); + + /// + public bool CanRead => true; + + /// + public bool CanWrite => false; + + /// + public string Name => "this"; + + /// + public Type Type => typeof(string); + + /// + public object GetValue(object target) + { + throw new NotSupportedException(); + } + + /// + public void SetValue(object target, object value) + { + throw new NotSupportedException(); + } + + /// + public object GetIndexedValue(object target, object index) + { + return ((global::MyNamespace.MyClass)target)[(int)index]; + } + + /// + public void SetIndexedValue(object target, object value, object index) + { + ((global::MyNamespace.MyClass)target)[(int)index] = (string)value; + } + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyClass.g.cs", result)); + } +} \ No newline at end of file From 032b9482c72f1ce6bce0007078421fbd215f3f90 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 11 Mar 2026 19:17:14 -0700 Subject: [PATCH 069/100] Remove DataRow for static partial struct Delete the DataRow entry for 'static partial struct' in Test_GeneratedCustomPropertyProviderTargetTypeAnalyzer.cs, removing it from the InvalidTargetType_Warns data-driven test. This updates the test cases to no longer treat static partial structs as an invalid target type. --- .../Test_GeneratedCustomPropertyProviderTargetTypeAnalyzer.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Tests/SourceGenerator2Test/Test_GeneratedCustomPropertyProviderTargetTypeAnalyzer.cs b/src/Tests/SourceGenerator2Test/Test_GeneratedCustomPropertyProviderTargetTypeAnalyzer.cs index d33d59f935..421242ce8d 100644 --- a/src/Tests/SourceGenerator2Test/Test_GeneratedCustomPropertyProviderTargetTypeAnalyzer.cs +++ b/src/Tests/SourceGenerator2Test/Test_GeneratedCustomPropertyProviderTargetTypeAnalyzer.cs @@ -49,7 +49,6 @@ public partial class B [TestMethod] [DataRow("abstract partial class")] [DataRow("static partial class")] - [DataRow("static partial struct")] [DataRow("ref partial struct")] public async Task InvalidTargetType_Warns(string modifiers) { From ed7f7648fe04b1fa08390ecfefe760f362f00629 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 12 Mar 2026 14:14:00 -0700 Subject: [PATCH 070/100] Expose CsWinRT XAML projection option to tests Add CompilerVisibleProperty and RuntimeHostConfigurationOption for CSWINRT_USE_WINDOWS_UI_XAML_PROJECTIONS to FunctionalTests/Directory.Build.props and update the comment in AuthoringWuxTest.csproj. This mirrors the workaround in dotnet/sdk#41936 so CsWinRT Windows UI XAML projections are available for projects that do not target a Windows TFM. --- src/Tests/AuthoringWuxTest/AuthoringWuxTest.csproj | 2 +- src/Tests/FunctionalTests/Directory.Build.props | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Tests/AuthoringWuxTest/AuthoringWuxTest.csproj b/src/Tests/AuthoringWuxTest/AuthoringWuxTest.csproj index 1f131e8256..22aab185ea 100644 --- a/src/Tests/AuthoringWuxTest/AuthoringWuxTest.csproj +++ b/src/Tests/AuthoringWuxTest/AuthoringWuxTest.csproj @@ -13,7 +13,7 @@ - + diff --git a/src/Tests/FunctionalTests/Directory.Build.props b/src/Tests/FunctionalTests/Directory.Build.props index 7c0c7c3ac2..f215dc9e0c 100644 --- a/src/Tests/FunctionalTests/Directory.Build.props +++ b/src/Tests/FunctionalTests/Directory.Build.props @@ -42,5 +42,11 @@ true + + + + + + From 805c1478b510f9fb2a9673621e707a08a2826294 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 12 Mar 2026 14:52:41 -0700 Subject: [PATCH 071/100] Remove trailing whitespace in generator file Remove stray trailing whitespace between the throw statement and the XML doc comment in CustomPropertyProviderGenerator.Emit.cs. Pure formatting cleanup; no functional changes. --- .../CustomPropertyProviderGenerator.Emit.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Emit.cs b/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Emit.cs index e987777161..63339942d3 100644 --- a/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Emit.cs +++ b/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Emit.cs @@ -368,7 +368,7 @@ public object GetValue(object target) { throw new NotSupportedException(); } - + /// public void SetValue(object target, object value) { From 0165800de299c11a0b4205d2489af552b37964b4 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 12 Mar 2026 15:38:32 -0700 Subject: [PATCH 072/100] Add HasAnyImplementedMembersForInterface helper Introduce HasAnyImplementedMembersForInterface in ITypeSymbolExtensions. The new helper returns true when the symbol implements the specified interface and has (or inherits) an implementation for at least one of the interface's members (uses AllInterfaces and FindImplementationForInterfaceMember). Co-Authored-By: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Extensions/ITypeSymbolExtensions.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/Authoring/WinRT.SourceGenerator2/Extensions/ITypeSymbolExtensions.cs b/src/Authoring/WinRT.SourceGenerator2/Extensions/ITypeSymbolExtensions.cs index 78729d4268..a96ea4e659 100644 --- a/src/Authoring/WinRT.SourceGenerator2/Extensions/ITypeSymbolExtensions.cs +++ b/src/Authoring/WinRT.SourceGenerator2/Extensions/ITypeSymbolExtensions.cs @@ -56,6 +56,30 @@ public IEnumerable EnumerateAllMembers() } } + /// + /// Checks whether the given implements a specified interface + /// and has or inherits implementations for any of its members. + /// + /// The interface type to check for member implementations. + /// Whether has any implemented members for . + public bool HasAnyImplementedMembersForInterface(INamedTypeSymbol interfaceType) + { + if (!symbol.AllInterfaces.Contains(interfaceType, SymbolEqualityComparer.Default)) + { + return false; + } + + foreach (ISymbol member in interfaceType.GetMembers()) + { + if (symbol.FindImplementationForInterfaceMember(member) is not null) + { + return true; + } + } + + return false; + } + /// /// Gets the fully qualified metadata name for a given instance. /// From c059fffd2ca2ffd00b720f7e4ebf8b96342ba5bf Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 12 Mar 2026 15:39:04 -0700 Subject: [PATCH 073/100] Resolve ICustomPropertyProvider by projection Select the ICustomPropertyProvider metadata name based on useWindowsUIXamlProjections and perform a single type lookup against the compilation. Replace the previous dual-check with a single retrieval (customPropertyProviderType) and return early if missing. Add a guard to return when the annotated type already implements or inherits any members for ICustomPropertyProvider (HasAnyImplementedMembersForInterface) to avoid generating conflicting implementations; diagnostics are handled elsewhere. Also add an extra cancellation-token check. Co-Authored-By: Copilot <223556219+Copilot@users.noreply.github.com> --- .../CustomPropertyProviderGenerator.Execute.cs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Execute.cs b/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Execute.cs index 2126a7316d..b42e339628 100644 --- a/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Execute.cs +++ b/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Execute.cs @@ -65,11 +65,14 @@ public static bool IsTargetNodeValid(SyntaxNode node, CancellationToken token) token.ThrowIfCancellationRequested(); + string customPropertyProviderMetadataName = useWindowsUIXamlProjections + ? "Windows.UI.Xaml.Data.ICustomPropertyProvider" + : "Microsoft.UI.Xaml.Data.ICustomPropertyProvider"; + // 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)) + if (context.SemanticModel.Compilation.GetTypeByMetadataName(customPropertyProviderMetadataName) is not { } customPropertyProviderType) { return null; } @@ -82,6 +85,16 @@ public static bool IsTargetNodeValid(SyntaxNode node, CancellationToken token) return null; } + // If the annotated type already implements 'ICustomPropertyProvider' and has or inherits any + // member implementations, we can't generate the implementation. A separate diagnostic analyzer + // will emit the right diagnostic for this case. + if (typeSymbol.HasAnyImplementedMembersForInterface(customPropertyProviderType)) + { + return null; + } + + token.ThrowIfCancellationRequested(); + // Get the type hierarchy (needed to correctly generate sources for nested types too) HierarchyInfo typeHierarchy = HierarchyInfo.From(typeSymbol); From dc3a00120391a1fdb6eff01e17248d55806a7f8e Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 12 Mar 2026 15:41:54 -0700 Subject: [PATCH 074/100] Add analyzer for GeneratedCustomPropertyProvider Introduce GeneratedCustomPropertyProviderExistingMemberImplementationAnalyzer to detect when [GeneratedCustomPropertyProvider] is applied to a type that already has or inherits implementations for ICustomPropertyProvider members (checks both Windows.UI.Xaml and Microsoft.UI.Xaml). Add a new diagnostic descriptor (CSWINRT2003) describing the error and update AnalyzerReleases.Shipped.md to include the new rule. This prevents the generator from producing duplicate member implementations on annotated types. Co-Authored-By: Copilot <223556219+Copilot@users.noreply.github.com> --- .../AnalyzerReleases.Shipped.md | 3 +- ...derExistingMemberImplementationAnalyzer.cs | 71 +++++++++++++++++++ .../Diagnostics/DiagnosticDescriptors.cs | 13 ++++ 3 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/GeneratedCustomPropertyProviderExistingMemberImplementationAnalyzer.cs diff --git a/src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Shipped.md b/src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Shipped.md index cd58f03539..ccafc41872 100644 --- a/src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Shipped.md +++ b/src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Shipped.md @@ -8,4 +8,5 @@ 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 +CSWINRT2002 | WindowsRuntime.SourceGenerator | Error | 'ICustomPropertyProvider' interface type not available +CSWINRT2003 | WindowsRuntime.SourceGenerator | Error | Existing 'ICustomPropertyProvider' member implementation \ No newline at end of file diff --git a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/GeneratedCustomPropertyProviderExistingMemberImplementationAnalyzer.cs b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/GeneratedCustomPropertyProviderExistingMemberImplementationAnalyzer.cs new file mode 100644 index 0000000000..9a81c7233b --- /dev/null +++ b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/GeneratedCustomPropertyProviderExistingMemberImplementationAnalyzer.cs @@ -0,0 +1,71 @@ +// 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 on a type +/// that already has or inherits implementations for one or more ICustomPropertyProvider members. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class GeneratedCustomPropertyProviderExistingMemberImplementationAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = [DiagnosticDescriptors.GeneratedCustomPropertyProviderExistingMemberImplementation]; + + /// + 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 neither, a different analyzer will handle that + if (windowsUIXamlCustomPropertyProviderType is null && microsoftUIXamlCustomPropertyProviderType is 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; + } + + // Immediately bail if the type doesn't have the attribute + if (!typeSymbol.HasAttributeWithType(attributeType)) + { + return; + } + + // Check whether the type has or inherits any 'ICustomPropertyProvider' member implementations + if ((windowsUIXamlCustomPropertyProviderType is not null && typeSymbol.HasAnyImplementedMembersForInterface(windowsUIXamlCustomPropertyProviderType)) || + (microsoftUIXamlCustomPropertyProviderType is not null && typeSymbol.HasAnyImplementedMembersForInterface(microsoftUIXamlCustomPropertyProviderType))) + { + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.GeneratedCustomPropertyProviderExistingMemberImplementation, + typeSymbol.Locations.FirstOrDefault(), + typeSymbol)); + } + }, SymbolKind.NamedType); + }); + } +} diff --git a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/DiagnosticDescriptors.cs b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/DiagnosticDescriptors.cs index 662958a67b..5fbc82f97e 100644 --- a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/DiagnosticDescriptors.cs +++ b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/DiagnosticDescriptors.cs @@ -48,4 +48,17 @@ internal static partial class DiagnosticDescriptors 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"); + + /// + /// Gets a for when [GeneratedCustomPropertyProvider] is used on a type that already implements ICustomPropertyProvider members. + /// + public static readonly DiagnosticDescriptor GeneratedCustomPropertyProviderExistingMemberImplementation = new( + id: "CSWINRT2003", + title: "Existing 'ICustomPropertyProvider' member implementation", + messageFormat: """The type '{0}' cannot use '[GeneratedCustomPropertyProvider]' because it already has or inherits implementations for one or more 'ICustomPropertyProvider' members""", + category: "WindowsRuntime.SourceGenerator", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "Types annotated with '[GeneratedCustomPropertyProvider]' must not already have or inherit implementations for any 'ICustomPropertyProvider' members, as the generator will provide them.", + helpLinkUri: "https://github.com/microsoft/CsWinRT"); } \ No newline at end of file From c4bf08e58f170fca9461a7b4c19e92787c070fb1 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 12 Mar 2026 15:44:35 -0700 Subject: [PATCH 075/100] Add tests for GeneratedCustomPropertyProvider Add Test_GeneratedCustomPropertyProviderExistingMemberImplementationAnalyzer.cs containing unit tests for GeneratedCustomPropertyProviderExistingMemberImplementationAnalyzer. Tests cover class/struct declarations, cases with no interface or no members (no warnings), explicit and implicit ICustomPropertyProvider implementations (complete and incomplete), and inherited implementations; they verify CSWINRT2003 warnings are produced where appropriate. Co-Authored-By: Copilot <223556219+Copilot@users.noreply.github.com> --- ...derExistingMemberImplementationAnalyzer.cs | 150 ++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 src/Tests/SourceGenerator2Test/Test_GeneratedCustomPropertyProviderExistingMemberImplementationAnalyzer.cs diff --git a/src/Tests/SourceGenerator2Test/Test_GeneratedCustomPropertyProviderExistingMemberImplementationAnalyzer.cs b/src/Tests/SourceGenerator2Test/Test_GeneratedCustomPropertyProviderExistingMemberImplementationAnalyzer.cs new file mode 100644 index 0000000000..5394ddcee5 --- /dev/null +++ b/src/Tests/SourceGenerator2Test/Test_GeneratedCustomPropertyProviderExistingMemberImplementationAnalyzer.cs @@ -0,0 +1,150 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using WindowsRuntime.SourceGenerator.Diagnostics; +using WindowsRuntime.SourceGenerator.Tests.Helpers; + +namespace WindowsRuntime.SourceGenerator.Tests; + +[TestClass] +public class Test_GeneratedCustomPropertyProviderExistingMemberImplementationAnalyzer +{ + [TestMethod] + [DataRow("class")] + [DataRow("struct")] + public async Task ValidType_NoInterface_DoesNotWarn(string modifier) + { + string source = $$""" + using WindowsRuntime.Xaml; + + [GeneratedCustomPropertyProvider] + public partial {{modifier}} MyType + { + public string Name { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source); + } + + [TestMethod] + [DataRow("class")] + [DataRow("struct")] + public async Task ValidType_NoMembers_DoesNotWarn(string modifier) + { + string source = $$""" + using Microsoft.UI.Xaml.Data; + using WindowsRuntime.Xaml; + + [GeneratedCustomPropertyProvider] + public partial {{modifier}} MyType : ICustomPropertyProvider + { + public string Name { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source); + } + + [TestMethod] + public async Task TypeWithExplicitInterfaceImplementation_Warns() + { + string source = """ + using System; + using Microsoft.UI.Xaml.Data; + using WindowsRuntime.Xaml; + + [GeneratedCustomPropertyProvider] + public partial class {|CSWINRT2003:MyType|} : ICustomPropertyProvider + { + Type ICustomPropertyProvider.Type => typeof(MyType); + ICustomProperty ICustomPropertyProvider.GetCustomProperty(string name) => null; + ICustomProperty ICustomPropertyProvider.GetIndexedProperty(string name, Type type) => null; + string ICustomPropertyProvider.GetStringRepresentation() => ""; + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source); + } + + [TestMethod] + public async Task TypeWithExplicitInterfaceImplementation_Incomplete_Warns() + { + string source = """ + using System; + using Microsoft.UI.Xaml.Data; + using WindowsRuntime.Xaml; + + [GeneratedCustomPropertyProvider] + public partial class {|CSWINRT2003:MyType|} : ICustomPropertyProvider + { + Type ICustomPropertyProvider.Type => typeof(MyType); + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source); + } + + [TestMethod] + public async Task TypeWithImplicitInterfaceImplementation_Warns() + { + string source = """ + using System; + using Microsoft.UI.Xaml.Data; + using WindowsRuntime.Xaml; + + [GeneratedCustomPropertyProvider] + public partial class {|CSWINRT2003:MyType|} : ICustomPropertyProvider + { + public Type Type => typeof(MyType); + public ICustomProperty GetCustomProperty(string name) => null; + public ICustomProperty GetIndexedProperty(string name, Type type) => null; + public string GetStringRepresentation() => ""; + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source); + } + + [TestMethod] + public async Task TypeWithImplicitInterfaceImplementation_Incomplete_Warns() + { + string source = """ + using System; + using Microsoft.UI.Xaml.Data; + using WindowsRuntime.Xaml; + + [GeneratedCustomPropertyProvider] + public partial class {|CSWINRT2003:MyType|} : ICustomPropertyProvider + { + public Type Type => typeof(MyType); + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source); + } + + [TestMethod] + public async Task TypeInheritingFromImplementingBase_Warns() + { + string source = """ + using System; + using Microsoft.UI.Xaml.Data; + using WindowsRuntime.Xaml; + + public class BaseType : ICustomPropertyProvider + { + Type ICustomPropertyProvider.Type => typeof(BaseType); + ICustomProperty ICustomPropertyProvider.GetCustomProperty(string name) => null; + ICustomProperty ICustomPropertyProvider.GetIndexedProperty(string name, Type type) => null; + string ICustomPropertyProvider.GetStringRepresentation() => ""; + } + + [GeneratedCustomPropertyProvider] + public partial class {|CSWINRT2003:MyType|} : BaseType; + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source); + } +} From 8991bad7ba1270b529b90f8caf298d1260afce5d Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 12 Mar 2026 16:17:29 -0700 Subject: [PATCH 076/100] Support expectedDiagnostics in analyzer test helper Add an optional ReadOnlySpan expectedDiagnostics parameter to CSharpAnalyzerTest.VerifyAnalyzerAsync and forward its contents into TestState.ExpectedDiagnostics so tests can supply expected diagnostics programmatically. Update tests to import Microsoft.CodeAnalysis.Testing, add a VerifyCS alias, and pass explicit expected diagnostic arrays for the analyzer cases. Also simplify one parameterized test to a single-class case and adjust using/imports accordingly. --- .../Helpers/CSharpAnalyzerTest{TAnalyzer}.cs | 4 ++ ...derExistingMemberImplementationAnalyzer.cs | 45 +++++++++++++++---- 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/src/Tests/SourceGenerator2Test/Helpers/CSharpAnalyzerTest{TAnalyzer}.cs b/src/Tests/SourceGenerator2Test/Helpers/CSharpAnalyzerTest{TAnalyzer}.cs index 3644ff35c7..d1976d465e 100644 --- a/src/Tests/SourceGenerator2Test/Helpers/CSharpAnalyzerTest{TAnalyzer}.cs +++ b/src/Tests/SourceGenerator2Test/Helpers/CSharpAnalyzerTest{TAnalyzer}.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System; using System.Threading; using System.Threading.Tasks; using Microsoft.CodeAnalysis; @@ -55,10 +56,12 @@ protected override ParseOptions CreateParseOptions() /// /// The source code to analyze. + /// The list of expected diagnostic for the test (used as alternative to the markdown syntax). /// Whether to enable unsafe blocks. /// The language version to use to run the test. public static Task VerifyAnalyzerAsync( string source, + ReadOnlySpan expectedDiagnostics = default, bool allowUnsafeBlocks = true, LanguageVersion languageVersion = LanguageVersion.CSharp14) { @@ -68,6 +71,7 @@ public static Task VerifyAnalyzerAsync( test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(WindowsRuntimeObject).Assembly.Location)); test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(CoreApplication).Assembly.Location)); test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(Button).Assembly.Location)); + test.TestState.ExpectedDiagnostics.AddRange([.. expectedDiagnostics]); return test.RunAsync(CancellationToken.None); } diff --git a/src/Tests/SourceGenerator2Test/Test_GeneratedCustomPropertyProviderExistingMemberImplementationAnalyzer.cs b/src/Tests/SourceGenerator2Test/Test_GeneratedCustomPropertyProviderExistingMemberImplementationAnalyzer.cs index 5394ddcee5..1dc506a942 100644 --- a/src/Tests/SourceGenerator2Test/Test_GeneratedCustomPropertyProviderExistingMemberImplementationAnalyzer.cs +++ b/src/Tests/SourceGenerator2Test/Test_GeneratedCustomPropertyProviderExistingMemberImplementationAnalyzer.cs @@ -2,8 +2,13 @@ // Licensed under the MIT License. using System.Threading.Tasks; +using Microsoft; +using Microsoft.CodeAnalysis.Testing; using WindowsRuntime.SourceGenerator.Diagnostics; using WindowsRuntime.SourceGenerator.Tests.Helpers; +using VerifyCS = Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier< + WindowsRuntime.SourceGenerator.Diagnostics.GeneratedCustomPropertyProviderExistingMemberImplementationAnalyzer, + Microsoft.CodeAnalysis.Testing.DefaultVerifier>; namespace WindowsRuntime.SourceGenerator.Tests; @@ -29,22 +34,28 @@ public partial {{modifier}} MyType } [TestMethod] - [DataRow("class")] - [DataRow("struct")] - public async Task ValidType_NoMembers_DoesNotWarn(string modifier) + public async Task ValidType_NoMembers_DoesNotWarn() { string source = $$""" using Microsoft.UI.Xaml.Data; using WindowsRuntime.Xaml; [GeneratedCustomPropertyProvider] - public partial {{modifier}} MyType : ICustomPropertyProvider + public partial class MyType : ICustomPropertyProvider { public string Name { get; set; } } """; - await CSharpAnalyzerTest.VerifyAnalyzerAsync(source); + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, expectedDiagnostics: [ + // /0/Test0.cs(5,31): error CS0535: 'MyType' does not implement interface member 'ICustomPropertyProvider.GetCustomProperty(string)' + DiagnosticResult.CompilerError("CS0535").WithSpan(5, 31, 5, 54).WithArguments("MyType", "Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetCustomProperty(string)"), + // /0/Test0.cs(5,31): error CS0535: 'MyType' does not implement interface member 'ICustomPropertyProvider.GetIndexedProperty(string, Type)' + DiagnosticResult.CompilerError("CS0535").WithSpan(5, 31, 5, 54).WithArguments("MyType", "Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetIndexedProperty(string, System.Type)"), + // /0/Test0.cs(5,31): error CS0535: 'MyType' does not implement interface member 'ICustomPropertyProvider.GetStringRepresentation()' + DiagnosticResult.CompilerError("CS0535").WithSpan(5, 31, 5, 54).WithArguments("MyType", "Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetStringRepresentation()"), + // /0/Test0.cs(5,31): error CS0535: 'MyType' does not implement interface member 'ICustomPropertyProvider.Type' + DiagnosticResult.CompilerError("CS0535").WithSpan(5, 31, 5, 54).WithArguments("MyType", "Microsoft.UI.Xaml.Data.ICustomPropertyProvider.Type")]); } [TestMethod] @@ -77,13 +88,21 @@ public async Task TypeWithExplicitInterfaceImplementation_Incomplete_Warns() using WindowsRuntime.Xaml; [GeneratedCustomPropertyProvider] - public partial class {|CSWINRT2003:MyType|} : ICustomPropertyProvider + public partial class MyType : ICustomPropertyProvider { Type ICustomPropertyProvider.Type => typeof(MyType); } """; - await CSharpAnalyzerTest.VerifyAnalyzerAsync(source); + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, expectedDiagnostics: [ + // /0/Test0.cs(6,22): error CSWINRT2003: The type 'MyType' cannot use '[GeneratedCustomPropertyProvider]' because it already has or inherits implementations for one or more 'ICustomPropertyProvider' members + VerifyCS.Diagnostic().WithSpan(6, 22, 6, 28).WithArguments("MyType"), + // /0/Test0.cs(6,31): error CS0535: 'MyType' does not implement interface member 'ICustomPropertyProvider.GetCustomProperty(string)' + DiagnosticResult.CompilerError("CS0535").WithSpan(6, 31, 6, 54).WithArguments("MyType", "Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetCustomProperty(string)"), + // /0/Test0.cs(6,31): error CS0535: 'MyType' does not implement interface member 'ICustomPropertyProvider.GetIndexedProperty(string, Type)' + DiagnosticResult.CompilerError("CS0535").WithSpan(6, 31, 6, 54).WithArguments("MyType", "Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetIndexedProperty(string, System.Type)"), + // /0/Test0.cs(6,31): error CS0535: 'MyType' does not implement interface member 'ICustomPropertyProvider.GetStringRepresentation()' + DiagnosticResult.CompilerError("CS0535").WithSpan(6, 31, 6, 54).WithArguments("MyType", "Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetStringRepresentation()")]); } [TestMethod] @@ -116,13 +135,21 @@ public async Task TypeWithImplicitInterfaceImplementation_Incomplete_Warns() using WindowsRuntime.Xaml; [GeneratedCustomPropertyProvider] - public partial class {|CSWINRT2003:MyType|} : ICustomPropertyProvider + public partial class MyType : ICustomPropertyProvider { public Type Type => typeof(MyType); } """; - await CSharpAnalyzerTest.VerifyAnalyzerAsync(source); + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, expectedDiagnostics: [ + // /0/Test0.cs(6,22): error CSWINRT2003: The type 'MyType' cannot use '[GeneratedCustomPropertyProvider]' because it already has or inherits implementations for one or more 'ICustomPropertyProvider' members + VerifyCS.Diagnostic().WithSpan(6, 22, 6, 28).WithArguments("MyType"), + // /0/Test0.cs(6,31): error CS0535: 'MyType' does not implement interface member 'ICustomPropertyProvider.GetCustomProperty(string)' + DiagnosticResult.CompilerError("CS0535").WithSpan(6, 31, 6, 54).WithArguments("MyType", "Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetCustomProperty(string)"), + // /0/Test0.cs(6,31): error CS0535: 'MyType' does not implement interface member 'ICustomPropertyProvider.GetIndexedProperty(string, Type)' + DiagnosticResult.CompilerError("CS0535").WithSpan(6, 31, 6, 54).WithArguments("MyType", "Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetIndexedProperty(string, System.Type)"), + // /0/Test0.cs(6,31): error CS0535: 'MyType' does not implement interface member 'ICustomPropertyProvider.GetStringRepresentation()' + DiagnosticResult.CompilerError("CS0535").WithSpan(6, 31, 6, 54).WithArguments("MyType", "Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetStringRepresentation()")]); } [TestMethod] From 3788322533f4c4d2ed032234fa9385792df53456 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 12 Mar 2026 16:20:35 -0700 Subject: [PATCH 077/100] Add .editorconfig and normalize line endings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add comprehensive .editorconfig files for Authoring/WinRT.SourceGenerator2 and Tests/SourceGenerator2Test to enforce consistent C# formatting and .NET coding conventions (indentation, naming, spacing, wrapping, and other rules). Also normalize file endings/EOF presence in several source files (GeneratedCustomPropertyProviderExistingMemberImplementationAnalyzer.cs, IncrementalGeneratorInitializationContextExtensions.cs, IncrementalValuesProviderExtensions.cs, CustomPropertyInfo.cs) — these are non-functional formatting changes to ensure consistent repository style. --- .../WinRT.SourceGenerator2/.editorconfig | 247 +++++++++++++++++- ...derExistingMemberImplementationAnalyzer.cs | 2 +- ...eneratorInitializationContextExtensions.cs | 2 +- .../IncrementalValuesProviderExtensions.cs | 2 +- .../Models/CustomPropertyInfo.cs | 2 +- src/Tests/SourceGenerator2Test/.editorconfig | 247 +++++++++++++++++- 6 files changed, 496 insertions(+), 6 deletions(-) diff --git a/src/Authoring/WinRT.SourceGenerator2/.editorconfig b/src/Authoring/WinRT.SourceGenerator2/.editorconfig index 41a979c4b6..6881bc9577 100644 --- a/src/Authoring/WinRT.SourceGenerator2/.editorconfig +++ b/src/Authoring/WinRT.SourceGenerator2/.editorconfig @@ -1,5 +1,250 @@ +# Remove the line below if you want to inherit .editorconfig settings from higher directories +root = true + # All files [*] +#### Core EditorConfig Options #### + +# Encoding +charset = utf-8 + +# Indentation and spacing +tab_width = 4 +indent_size = 4 +indent_style = space + # New line preferences -end_of_line = lf \ No newline at end of file +end_of_line = lf +insert_final_newline = false +trim_trailing_whitespace = true + +# Misc settings +max_line_length=160 +file_header_template = Copyright (c) Microsoft Corporation.\r\nLicensed under the MIT License. + +# C# files +[*.cs] +csharp_keep_user_linebreaks = true +wrap_before_comma = false +wrap_parameters_style = chop_if_long +max_formal_parameters_on_line = 4 +wrap_after_dot_in_method_calls = false +csharp_preserve_single_line_blocks = false +csharp_new_line_between_query_expression_clauses = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_before_open_brace = all + +#### Core EditorConfig Options #### + +# Indentation and spacing +indent_size = 4 +indent_style = space +tab_width = 4 + +# New line preferences +end_of_line = crlf +insert_final_newline = false + +#### .NET Coding Conventions #### + +# Organize usings +dotnet_separate_import_directive_groups = false +dotnet_sort_system_directives_first = true + +# this. and Me. preferences +dotnet_style_qualification_for_event = false +dotnet_style_qualification_for_field = false +dotnet_style_qualification_for_method = false +dotnet_style_qualification_for_property = false + +# These two diagnostics are just 'Info' by default +dotnet_diagnostic.IDE0003.severity = warning +dotnet_diagnostic.IDE0009.severity = warning + +# Language keywords vs BCL types preferences +dotnet_style_predefined_type_for_locals_parameters_members = true +dotnet_style_predefined_type_for_member_access = true + +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_operators = never_if_unnecessary +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity + +# Modifier preferences +dotnet_style_require_accessibility_modifiers = for_non_interface_members + +# Expression-level preferences +dotnet_style_coalesce_expression = true +dotnet_style_collection_initializer = true +dotnet_style_explicit_tuple_names = true +dotnet_style_null_propagation = true +dotnet_style_object_initializer = true +dotnet_style_operator_placement_when_wrapping = beginning_of_line +dotnet_style_prefer_auto_properties = true +dotnet_style_prefer_compound_assignment = true +dotnet_style_prefer_conditional_expression_over_assignment = true +dotnet_style_prefer_conditional_expression_over_return = true +dotnet_style_prefer_inferred_anonymous_type_member_names = true +dotnet_style_prefer_inferred_tuple_names = true +dotnet_style_prefer_is_null_check_over_reference_equality_method = true +dotnet_style_prefer_simplified_boolean_expressions = true +dotnet_style_prefer_simplified_interpolation = true + +# Field preferences +dotnet_style_readonly_field = true + +# Namespace preferences +dotnet_style_namespace_match_folder = false + +# Primary constructor preference +csharp_style_prefer_primary_constructors = false; + +# Parameter preferences +dotnet_code_quality_unused_parameters = all + +# Suppression preferences +dotnet_remove_unnecessary_suppression_exclusions = none + +#### C# Coding Conventions #### + +# var preferences +csharp_style_var_elsewhere = false +csharp_style_var_for_built_in_types = false +csharp_style_var_when_type_is_apparent = false + +# Expression-bodied members +csharp_style_expression_bodied_accessors = when_on_single_line +csharp_style_expression_bodied_constructors = false +csharp_style_expression_bodied_indexers = when_on_single_line +csharp_style_expression_bodied_lambdas = when_on_single_line +csharp_style_expression_bodied_local_functions = when_on_single_line +csharp_style_expression_bodied_methods = false +csharp_style_expression_bodied_operators = when_on_single_line +csharp_style_expression_bodied_properties = when_on_single_line + +# Pattern matching preferences +csharp_style_pattern_matching_over_as_with_null_check = true +csharp_style_pattern_matching_over_is_with_cast_check = true +csharp_style_prefer_not_pattern = true +csharp_style_prefer_pattern_matching = true +csharp_style_prefer_switch_expression = true + +# Null-checking preferences +csharp_style_conditional_delegate_call = true + +# Modifier preferences +csharp_prefer_static_local_function = true +csharp_preferred_modifier_order = public, private, protected, internal, file, static, extern, new, virtual, abstract, sealed, override, readonly, unsafe, required, volatile, async + +# Code-block preferences +csharp_prefer_braces = true +csharp_prefer_simple_using_statement = true + +# Expression-level preferences +csharp_prefer_simple_default_expression = true +csharp_style_deconstructed_variable_declaration = true +csharp_style_implicit_object_creation_when_type_is_apparent = true +csharp_style_inlined_variable_declaration = true +csharp_style_pattern_local_over_anonymous_function = true +csharp_style_prefer_index_operator = true +csharp_style_prefer_range_operator = true +csharp_style_throw_expression = true +csharp_style_unused_value_assignment_preference = discard_variable +csharp_style_unused_value_expression_statement_preference = discard_variable + +# 'using' directive preferences +csharp_using_directive_placement = outside_namespace + +#### C# Formatting Rules #### + +# New line preferences +csharp_new_line_before_catch = true +csharp_new_line_before_else = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_open_brace = all +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = true +csharp_indent_labels = one_less_than_current +csharp_indent_switch_labels = true + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# Wrapping preferences +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true + +# Namespace declaration preferences +csharp_style_namespace_declarations = file_scoped:warning + +#### Naming styles #### + +# Naming rules + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# Symbol specifications + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# Naming styles + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case \ No newline at end of file diff --git a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/GeneratedCustomPropertyProviderExistingMemberImplementationAnalyzer.cs b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/GeneratedCustomPropertyProviderExistingMemberImplementationAnalyzer.cs index 9a81c7233b..79ecdadbc2 100644 --- a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/GeneratedCustomPropertyProviderExistingMemberImplementationAnalyzer.cs +++ b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/GeneratedCustomPropertyProviderExistingMemberImplementationAnalyzer.cs @@ -68,4 +68,4 @@ public override void Initialize(AnalysisContext context) }, SymbolKind.NamedType); }); } -} +} \ No newline at end of file diff --git a/src/Authoring/WinRT.SourceGenerator2/Extensions/IncrementalGeneratorInitializationContextExtensions.cs b/src/Authoring/WinRT.SourceGenerator2/Extensions/IncrementalGeneratorInitializationContextExtensions.cs index d3ed23f4ec..3f94b3183c 100644 --- a/src/Authoring/WinRT.SourceGenerator2/Extensions/IncrementalGeneratorInitializationContextExtensions.cs +++ b/src/Authoring/WinRT.SourceGenerator2/Extensions/IncrementalGeneratorInitializationContextExtensions.cs @@ -63,4 +63,4 @@ internal readonly struct GeneratorAttributeSyntaxContextWithOptions( /// public AnalyzerConfigOptions GlobalOptions { get; } = globalOptions; -} +} \ No newline at end of file diff --git a/src/Authoring/WinRT.SourceGenerator2/Extensions/IncrementalValuesProviderExtensions.cs b/src/Authoring/WinRT.SourceGenerator2/Extensions/IncrementalValuesProviderExtensions.cs index a04906d25c..3c49e1f14a 100644 --- a/src/Authoring/WinRT.SourceGenerator2/Extensions/IncrementalValuesProviderExtensions.cs +++ b/src/Authoring/WinRT.SourceGenerator2/Extensions/IncrementalValuesProviderExtensions.cs @@ -21,4 +21,4 @@ public static IncrementalValuesProvider SkipNullValues(this IncrementalVal { return provider.Where(static value => value is not null)!; } -} +} \ No newline at end of file diff --git a/src/Authoring/WinRT.SourceGenerator2/Models/CustomPropertyInfo.cs b/src/Authoring/WinRT.SourceGenerator2/Models/CustomPropertyInfo.cs index c2a567ed9c..fd58c9cdc5 100644 --- a/src/Authoring/WinRT.SourceGenerator2/Models/CustomPropertyInfo.cs +++ b/src/Authoring/WinRT.SourceGenerator2/Models/CustomPropertyInfo.cs @@ -27,4 +27,4 @@ internal sealed record CustomPropertyInfo( /// [MemberNotNullWhen(true, nameof(FullyQualifiedIndexerTypeName))] public bool IsIndexer => FullyQualifiedIndexerTypeName is not null; -} +} \ No newline at end of file diff --git a/src/Tests/SourceGenerator2Test/.editorconfig b/src/Tests/SourceGenerator2Test/.editorconfig index 41a979c4b6..6881bc9577 100644 --- a/src/Tests/SourceGenerator2Test/.editorconfig +++ b/src/Tests/SourceGenerator2Test/.editorconfig @@ -1,5 +1,250 @@ +# Remove the line below if you want to inherit .editorconfig settings from higher directories +root = true + # All files [*] +#### Core EditorConfig Options #### + +# Encoding +charset = utf-8 + +# Indentation and spacing +tab_width = 4 +indent_size = 4 +indent_style = space + # New line preferences -end_of_line = lf \ No newline at end of file +end_of_line = lf +insert_final_newline = false +trim_trailing_whitespace = true + +# Misc settings +max_line_length=160 +file_header_template = Copyright (c) Microsoft Corporation.\r\nLicensed under the MIT License. + +# C# files +[*.cs] +csharp_keep_user_linebreaks = true +wrap_before_comma = false +wrap_parameters_style = chop_if_long +max_formal_parameters_on_line = 4 +wrap_after_dot_in_method_calls = false +csharp_preserve_single_line_blocks = false +csharp_new_line_between_query_expression_clauses = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_before_open_brace = all + +#### Core EditorConfig Options #### + +# Indentation and spacing +indent_size = 4 +indent_style = space +tab_width = 4 + +# New line preferences +end_of_line = crlf +insert_final_newline = false + +#### .NET Coding Conventions #### + +# Organize usings +dotnet_separate_import_directive_groups = false +dotnet_sort_system_directives_first = true + +# this. and Me. preferences +dotnet_style_qualification_for_event = false +dotnet_style_qualification_for_field = false +dotnet_style_qualification_for_method = false +dotnet_style_qualification_for_property = false + +# These two diagnostics are just 'Info' by default +dotnet_diagnostic.IDE0003.severity = warning +dotnet_diagnostic.IDE0009.severity = warning + +# Language keywords vs BCL types preferences +dotnet_style_predefined_type_for_locals_parameters_members = true +dotnet_style_predefined_type_for_member_access = true + +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity +dotnet_style_parentheses_in_other_operators = never_if_unnecessary +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity + +# Modifier preferences +dotnet_style_require_accessibility_modifiers = for_non_interface_members + +# Expression-level preferences +dotnet_style_coalesce_expression = true +dotnet_style_collection_initializer = true +dotnet_style_explicit_tuple_names = true +dotnet_style_null_propagation = true +dotnet_style_object_initializer = true +dotnet_style_operator_placement_when_wrapping = beginning_of_line +dotnet_style_prefer_auto_properties = true +dotnet_style_prefer_compound_assignment = true +dotnet_style_prefer_conditional_expression_over_assignment = true +dotnet_style_prefer_conditional_expression_over_return = true +dotnet_style_prefer_inferred_anonymous_type_member_names = true +dotnet_style_prefer_inferred_tuple_names = true +dotnet_style_prefer_is_null_check_over_reference_equality_method = true +dotnet_style_prefer_simplified_boolean_expressions = true +dotnet_style_prefer_simplified_interpolation = true + +# Field preferences +dotnet_style_readonly_field = true + +# Namespace preferences +dotnet_style_namespace_match_folder = false + +# Primary constructor preference +csharp_style_prefer_primary_constructors = false; + +# Parameter preferences +dotnet_code_quality_unused_parameters = all + +# Suppression preferences +dotnet_remove_unnecessary_suppression_exclusions = none + +#### C# Coding Conventions #### + +# var preferences +csharp_style_var_elsewhere = false +csharp_style_var_for_built_in_types = false +csharp_style_var_when_type_is_apparent = false + +# Expression-bodied members +csharp_style_expression_bodied_accessors = when_on_single_line +csharp_style_expression_bodied_constructors = false +csharp_style_expression_bodied_indexers = when_on_single_line +csharp_style_expression_bodied_lambdas = when_on_single_line +csharp_style_expression_bodied_local_functions = when_on_single_line +csharp_style_expression_bodied_methods = false +csharp_style_expression_bodied_operators = when_on_single_line +csharp_style_expression_bodied_properties = when_on_single_line + +# Pattern matching preferences +csharp_style_pattern_matching_over_as_with_null_check = true +csharp_style_pattern_matching_over_is_with_cast_check = true +csharp_style_prefer_not_pattern = true +csharp_style_prefer_pattern_matching = true +csharp_style_prefer_switch_expression = true + +# Null-checking preferences +csharp_style_conditional_delegate_call = true + +# Modifier preferences +csharp_prefer_static_local_function = true +csharp_preferred_modifier_order = public, private, protected, internal, file, static, extern, new, virtual, abstract, sealed, override, readonly, unsafe, required, volatile, async + +# Code-block preferences +csharp_prefer_braces = true +csharp_prefer_simple_using_statement = true + +# Expression-level preferences +csharp_prefer_simple_default_expression = true +csharp_style_deconstructed_variable_declaration = true +csharp_style_implicit_object_creation_when_type_is_apparent = true +csharp_style_inlined_variable_declaration = true +csharp_style_pattern_local_over_anonymous_function = true +csharp_style_prefer_index_operator = true +csharp_style_prefer_range_operator = true +csharp_style_throw_expression = true +csharp_style_unused_value_assignment_preference = discard_variable +csharp_style_unused_value_expression_statement_preference = discard_variable + +# 'using' directive preferences +csharp_using_directive_placement = outside_namespace + +#### C# Formatting Rules #### + +# New line preferences +csharp_new_line_before_catch = true +csharp_new_line_before_else = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_open_brace = all +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = true +csharp_indent_labels = one_less_than_current +csharp_indent_switch_labels = true + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# Wrapping preferences +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = true + +# Namespace declaration preferences +csharp_style_namespace_declarations = file_scoped:warning + +#### Naming styles #### + +# Naming rules + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# Symbol specifications + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# Naming styles + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case \ No newline at end of file From 13465b04d209cae47796b9eeb81d61ebd93aa168 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 12 Mar 2026 16:23:00 -0700 Subject: [PATCH 078/100] Normalize line endings for generator projects --- .../WinRT.SourceGenerator2/AnalyzerReleases.Unshipped.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Unshipped.md b/src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Unshipped.md index 6640189c3f..7634792eb7 100644 --- a/src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Unshipped.md +++ b/src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Unshipped.md @@ -1,4 +1,4 @@ -; Unshipped analyzer release +; Unshipped analyzer release ; https://github.com/dotnet/roslyn-analyzers/blob/master/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md ### New Rules From 666ae831ee25d28cc6ffdf1bca10ddd2ba905dcf Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 12 Mar 2026 21:00:47 -0700 Subject: [PATCH 079/100] Fix missing semicolons in CustomPropertyProviderGenerator output Fix three missing semicolons in generated 'using' directives (CodeDom.Compiler, Diagnostics, Diagnostics.CodeAnalysis) and fix the switch expression closing brace to emit '};' instead of '}' by replacing WriteBlock with manual indent management. Also update all corresponding expected outputs in the unit tests. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../CustomPropertyProviderGenerator.Emit.cs | 31 +++++------ .../Test_CustomPropertyProviderGenerator.cs | 52 +++++++++---------- 2 files changed, 42 insertions(+), 41 deletions(-) diff --git a/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Emit.cs b/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Emit.cs index 63339942d3..01f2b69136 100644 --- a/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Emit.cs +++ b/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Emit.cs @@ -83,24 +83,25 @@ private static void WriteCustomPropertyProviderGetCustomProperty(CustomPropertyP } writer.WriteLine("return name switch"); + writer.WriteLine("{"); + writer.IncreaseIndent(); - using (writer.WriteBlock()) + // Emit a switch case for each available property + foreach (CustomPropertyInfo propertyInfo in info.CustomProperties) { - // Emit a switch case for each available property - foreach (CustomPropertyInfo propertyInfo in info.CustomProperties) + if (propertyInfo.IsIndexer) { - if (propertyInfo.IsIndexer) - { - 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,"); + continue; } - // If there's no matching property, just return 'null' - writer.WriteLine("_ => null"); + // 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"); + writer.DecreaseIndent(); + writer.WriteLine("};"); } } @@ -189,9 +190,9 @@ private static void WriteCustomPropertyImplementationTypes(CustomPropertyProvide { // Using declarations for well-known namespaces we can use with simple names writer.WriteLine("using global::System;"); - writer.WriteLine("using global::System.CodeDom.Compiler"); - writer.WriteLine("using global::System.Diagnostics"); - writer.WriteLine("using global::System.Diagnostics.CodeAnalysis"); + writer.WriteLine("using global::System.CodeDom.Compiler;"); + writer.WriteLine("using global::System.Diagnostics;"); + writer.WriteLine("using global::System.Diagnostics.CodeAnalysis;"); writer.WriteLine($"using global::{info.FullyQualifiedCustomPropertyProviderInterfaceName.Replace(".ICustomPropertyProvider", "")};"); writer.WriteLine(); diff --git a/src/Tests/SourceGenerator2Test/Test_CustomPropertyProviderGenerator.cs b/src/Tests/SourceGenerator2Test/Test_CustomPropertyProviderGenerator.cs index 10f48f16f7..31016abeec 100644 --- a/src/Tests/SourceGenerator2Test/Test_CustomPropertyProviderGenerator.cs +++ b/src/Tests/SourceGenerator2Test/Test_CustomPropertyProviderGenerator.cs @@ -52,7 +52,7 @@ Microsoft.UI.Xaml.Data.ICustomProperty Microsoft.UI.Xaml.Data.ICustomPropertyPro nameof(Name) => global::WindowsRuntime.Xaml.Generated.MyClass_Name.Instance, nameof(Age) => global::WindowsRuntime.Xaml.Generated.MyClass_Age.Instance, _ => null - } + }; } /// @@ -77,9 +77,9 @@ string Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetStringRepresentation() namespace WindowsRuntime.Xaml.Generated { using global::System; - using global::System.CodeDom.Compiler - using global::System.Diagnostics - using global::System.Diagnostics.CodeAnalysis + using global::System.CodeDom.Compiler; + using global::System.Diagnostics; + using global::System.Diagnostics.CodeAnalysis; using global::Microsoft.UI.Xaml.Data; /// @@ -324,7 +324,7 @@ Microsoft.UI.Xaml.Data.ICustomProperty Microsoft.UI.Xaml.Data.ICustomPropertyPro nameof(Name) => global::WindowsRuntime.Xaml.Generated.MyClass_Name.Instance, nameof(Age) => global::WindowsRuntime.Xaml.Generated.MyClass_Age.Instance, _ => null - } + }; } /// @@ -344,9 +344,9 @@ string Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetStringRepresentation() namespace WindowsRuntime.Xaml.Generated { using global::System; - using global::System.CodeDom.Compiler - using global::System.Diagnostics - using global::System.Diagnostics.CodeAnalysis + using global::System.CodeDom.Compiler; + using global::System.Diagnostics; + using global::System.Diagnostics.CodeAnalysis; using global::Microsoft.UI.Xaml.Data; /// @@ -513,9 +513,9 @@ string Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetStringRepresentation() namespace WindowsRuntime.Xaml.Generated { using global::System; - using global::System.CodeDom.Compiler - using global::System.Diagnostics - using global::System.Diagnostics.CodeAnalysis + using global::System.CodeDom.Compiler; + using global::System.Diagnostics; + using global::System.Diagnostics.CodeAnalysis; using global::Microsoft.UI.Xaml.Data; /// @@ -607,7 +607,7 @@ Microsoft.UI.Xaml.Data.ICustomProperty Microsoft.UI.Xaml.Data.ICustomPropertyPro { nameof(Name) => global::WindowsRuntime.Xaml.Generated.MyClass_Name.Instance, _ => null - } + }; } /// @@ -627,9 +627,9 @@ string Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetStringRepresentation() namespace WindowsRuntime.Xaml.Generated { using global::System; - using global::System.CodeDom.Compiler - using global::System.Diagnostics - using global::System.Diagnostics.CodeAnalysis + using global::System.CodeDom.Compiler; + using global::System.Diagnostics; + using global::System.Diagnostics.CodeAnalysis; using global::Microsoft.UI.Xaml.Data; /// @@ -721,7 +721,7 @@ Microsoft.UI.Xaml.Data.ICustomProperty Microsoft.UI.Xaml.Data.ICustomPropertyPro { nameof(Name) => global::WindowsRuntime.Xaml.Generated.MyClass_Name.Instance, _ => null - } + }; } /// @@ -741,9 +741,9 @@ string Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetStringRepresentation() namespace WindowsRuntime.Xaml.Generated { using global::System; - using global::System.CodeDom.Compiler - using global::System.Diagnostics - using global::System.Diagnostics.CodeAnalysis + using global::System.CodeDom.Compiler; + using global::System.Diagnostics; + using global::System.Diagnostics.CodeAnalysis; using global::Microsoft.UI.Xaml.Data; /// @@ -835,7 +835,7 @@ Microsoft.UI.Xaml.Data.ICustomProperty Microsoft.UI.Xaml.Data.ICustomPropertyPro { nameof(Count) => global::WindowsRuntime.Xaml.Generated.MyClass_Count.Instance, _ => null - } + }; } /// @@ -855,9 +855,9 @@ string Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetStringRepresentation() namespace WindowsRuntime.Xaml.Generated { using global::System; - using global::System.CodeDom.Compiler - using global::System.Diagnostics - using global::System.Diagnostics.CodeAnalysis + using global::System.CodeDom.Compiler; + using global::System.Diagnostics; + using global::System.Diagnostics.CodeAnalysis; using global::Microsoft.UI.Xaml.Data; /// @@ -970,9 +970,9 @@ string Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetStringRepresentation() namespace WindowsRuntime.Xaml.Generated { using global::System; - using global::System.CodeDom.Compiler - using global::System.Diagnostics - using global::System.Diagnostics.CodeAnalysis + using global::System.CodeDom.Compiler; + using global::System.Diagnostics; + using global::System.Diagnostics.CodeAnalysis; using global::Microsoft.UI.Xaml.Data; /// From ad53ee0a6b7e4befd78bea5e8f2f18a91e0c67d7 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 12 Mar 2026 23:10:35 -0700 Subject: [PATCH 080/100] Fix GetValue/SetValue emit for read-only and write-only properties The generator was unconditionally emitting property access code in GetValue/SetValue/GetIndexedValue/SetIndexedValue regardless of whether the property supported that operation. This caused CS0200 errors when a read-only property's generated SetValue tried to assign to it. Now, when CanRead is false, GetValue/GetIndexedValue emit 'throw new NotSupportedException()' instead of the property/indexer access. Likewise for CanWrite being false with SetValue/SetIndexedValue. Updated expected outputs in ReadOnlyProperty, WriteOnlyProperty and ReadOnlyIndexer unit tests. Co-Authored-By: Copilot <223556219+Copilot@users.noreply.github.com> --- .../CustomPropertyProviderGenerator.Emit.cs | 111 ++++++++++++++---- .../Test_CustomPropertyProviderGenerator.cs | 6 +- 2 files changed, 90 insertions(+), 27 deletions(-) diff --git a/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Emit.cs b/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Emit.cs index 01f2b69136..980a007956 100644 --- a/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Emit.cs +++ b/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Emit.cs @@ -264,34 +264,56 @@ private static void WriteNonIndexedCustomPropertyImplementationType(CustomProper 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(); + + // Emit 'GetValue' depending on whether the property is readable and whether it's static + if (propertyInfo.CanRead && propertyInfo.IsStatic) { - writer.WriteLine(); writer.WriteLine($$""" /// public object GetValue(object target) { return {{info.TypeHierarchy.GetFullyQualifiedTypeName()}}.{{propertyInfo.Name}}; } - + """, isMultiline: true); + } + else if (propertyInfo.CanRead) + { + writer.WriteLine($$""" /// - public void SetValue(object target, object value) + public object GetValue(object target) { - {{info.TypeHierarchy.GetFullyQualifiedTypeName()}}.{{propertyInfo.Name}} = ({{propertyInfo.FullyQualifiedTypeName}})value; + return (({{info.TypeHierarchy.GetFullyQualifiedTypeName()}})target).{{propertyInfo.Name}}; } """, isMultiline: true); } else { - writer.WriteLine(); - writer.WriteLine($$""" + writer.WriteLine(""" /// public object GetValue(object target) { - return (({{info.TypeHierarchy.GetFullyQualifiedTypeName()}})target).{{propertyInfo.Name}}; + throw new NotSupportedException(); } - + """, isMultiline: true); + } + + writer.WriteLine(); + + // Emit 'SetValue' depending on whether the property is writable and whether it's static + if (propertyInfo.CanWrite && propertyInfo.IsStatic) + { + writer.WriteLine($$""" + /// + public void SetValue(object target, object value) + { + {{info.TypeHierarchy.GetFullyQualifiedTypeName()}}.{{propertyInfo.Name}} = ({{propertyInfo.FullyQualifiedTypeName}})value; + } + """, isMultiline: true); + } + else if (propertyInfo.CanWrite) + { + writer.WriteLine($$""" /// public void SetValue(object target, object value) { @@ -299,6 +321,16 @@ public void SetValue(object target, object value) } """, isMultiline: true); } + else + { + writer.WriteLine(""" + /// + 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(); @@ -377,21 +409,52 @@ public void SetValue(object target, object value) } """, isMultiline: true); - // Emit the indexer property accessors (not supported) + // Emit the indexer property accessors, conditionally based on CanRead/CanWrite 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); + + if (propertyInfo.CanRead) + { + writer.WriteLine($$""" + /// + public object GetIndexedValue(object target, object index) + { + return (({{info.TypeHierarchy.GetFullyQualifiedTypeName()}})target)[({{propertyInfo.FullyQualifiedIndexerTypeName}})index]; + } + """, isMultiline: true); + } + else + { + writer.WriteLine(""" + /// + public object GetIndexedValue(object target, object index) + { + throw new NotSupportedException(); + } + """, isMultiline: true); + } + + writer.WriteLine(); + + if (propertyInfo.CanWrite) + { + writer.WriteLine($$""" + /// + public void SetIndexedValue(object target, object value, object index) + { + (({{info.TypeHierarchy.GetFullyQualifiedTypeName()}})target)[({{propertyInfo.FullyQualifiedIndexerTypeName}})index] = ({{propertyInfo.FullyQualifiedTypeName}})value; + } + """, isMultiline: true); + } + else + { + writer.WriteLine(""" + /// + public void SetIndexedValue(object target, object value, object index) + { + throw new NotSupportedException(); + } + """, isMultiline: true); + } } } } diff --git a/src/Tests/SourceGenerator2Test/Test_CustomPropertyProviderGenerator.cs b/src/Tests/SourceGenerator2Test/Test_CustomPropertyProviderGenerator.cs index 31016abeec..97bbe25b5f 100644 --- a/src/Tests/SourceGenerator2Test/Test_CustomPropertyProviderGenerator.cs +++ b/src/Tests/SourceGenerator2Test/Test_CustomPropertyProviderGenerator.cs @@ -666,7 +666,7 @@ public object GetValue(object target) /// public void SetValue(object target, object value) { - ((global::MyNamespace.MyClass)target).Name = (string)value; + throw new NotSupportedException(); } /// @@ -774,7 +774,7 @@ namespace WindowsRuntime.Xaml.Generated /// public object GetValue(object target) { - return ((global::MyNamespace.MyClass)target).Name; + throw new NotSupportedException(); } /// @@ -1021,7 +1021,7 @@ public object GetIndexedValue(object target, object index) /// public void SetIndexedValue(object target, object value, object index) { - ((global::MyNamespace.MyClass)target)[(int)index] = (string)value; + throw new NotSupportedException(); } } } From 697e7515848eba087afd7a6f3ed2087f55634152 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 12 Mar 2026 23:17:01 -0700 Subject: [PATCH 081/100] Add [GeneratedCode] attribute to generated ICustomPropertyProvider members Call WriteGeneratedAttributes with includeNonUserCodeAttributes: false on each of the four generated explicit interface implementations (Type, GetCustomProperty, GetIndexedProperty, GetStringRepresentation). This annotates them with [GeneratedCode] so the analyzer can distinguish generated implementations from user-authored ones. Updated all expected outputs in unit tests accordingly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../CustomPropertyProviderGenerator.Emit.cs | 16 ++++++---- .../Test_CustomPropertyProviderGenerator.cs | 32 +++++++++++++++++++ 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Emit.cs b/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Emit.cs index 980a007956..c63a1979e9 100644 --- a/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Emit.cs +++ b/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Emit.cs @@ -54,10 +54,9 @@ public static void WriteCustomPropertyProviderImplementation(SourceProductionCon /// private static void WriteCustomPropertyProviderType(CustomPropertyProviderInfo info, ref IndentedTextWriter writer) { - writer.WriteLine($""" - /// - global::System.Type {info.FullyQualifiedCustomPropertyProviderInterfaceName}.Type => typeof({info.TypeHierarchy.Hierarchy[0].QualifiedName}); - """, isMultiline: true); + writer.WriteLine("/// "); + writer.WriteGeneratedAttributes(nameof(CustomPropertyProviderGenerator), includeNonUserCodeAttributes: false); + writer.WriteLine($"global::System.Type {info.FullyQualifiedCustomPropertyProviderInterfaceName}.Type => typeof({info.TypeHierarchy.Hierarchy[0].QualifiedName});"); } /// @@ -67,8 +66,9 @@ private static void WriteCustomPropertyProviderType(CustomPropertyProviderInfo i /// private static void WriteCustomPropertyProviderGetCustomProperty(CustomPropertyProviderInfo info, ref IndentedTextWriter writer) { + writer.WriteLine("/// "); + writer.WriteGeneratedAttributes(nameof(CustomPropertyProviderGenerator), includeNonUserCodeAttributes: false); writer.WriteLine($""" - /// {info.FullyQualifiedCustomPropertyInterfaceName} {info.FullyQualifiedCustomPropertyProviderInterfaceName}.GetCustomProperty(string name) """, isMultiline: true); @@ -112,8 +112,9 @@ private static void WriteCustomPropertyProviderGetCustomProperty(CustomPropertyP /// private static void WriteCustomPropertyProviderGetIndexedProperty(CustomPropertyProviderInfo info, ref IndentedTextWriter writer) { + writer.WriteLine("/// "); + writer.WriteGeneratedAttributes(nameof(CustomPropertyProviderGenerator), includeNonUserCodeAttributes: false); writer.WriteLine($""" - /// {info.FullyQualifiedCustomPropertyInterfaceName} {info.FullyQualifiedCustomPropertyProviderInterfaceName}.GetIndexedProperty(string name, global::System.Type type) """, isMultiline: true); @@ -160,8 +161,9 @@ private static void WriteCustomPropertyProviderGetIndexedProperty(CustomProperty /// private static void WriteCustomPropertyProviderGetStringRepresentation(CustomPropertyProviderInfo info, ref IndentedTextWriter writer) { + writer.WriteLine("/// "); + writer.WriteGeneratedAttributes(nameof(CustomPropertyProviderGenerator), includeNonUserCodeAttributes: false); writer.WriteLine($$""" - /// string {{info.FullyQualifiedCustomPropertyProviderInterfaceName}}.GetStringRepresentation() { return ToString(); diff --git a/src/Tests/SourceGenerator2Test/Test_CustomPropertyProviderGenerator.cs b/src/Tests/SourceGenerator2Test/Test_CustomPropertyProviderGenerator.cs index 97bbe25b5f..a0416ac837 100644 --- a/src/Tests/SourceGenerator2Test/Test_CustomPropertyProviderGenerator.cs +++ b/src/Tests/SourceGenerator2Test/Test_CustomPropertyProviderGenerator.cs @@ -42,9 +42,11 @@ namespace MyNamespace partial class MyClass : Microsoft.UI.Xaml.Data.ICustomPropertyProvider { /// + [global::System.CodeDom.Compiler.GeneratedCode("CustomPropertyProviderGenerator", )] global::System.Type Microsoft.UI.Xaml.Data.ICustomPropertyProvider.Type => typeof(MyClass); /// + [global::System.CodeDom.Compiler.GeneratedCode("CustomPropertyProviderGenerator", )] Microsoft.UI.Xaml.Data.ICustomProperty Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetCustomProperty(string name) { return name switch @@ -56,6 +58,7 @@ Microsoft.UI.Xaml.Data.ICustomProperty Microsoft.UI.Xaml.Data.ICustomPropertyPro } /// + [global::System.CodeDom.Compiler.GeneratedCode("CustomPropertyProviderGenerator", )] Microsoft.UI.Xaml.Data.ICustomProperty Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetIndexedProperty(string name, global::System.Type type) { if (type == typeof(int)) @@ -67,6 +70,7 @@ Microsoft.UI.Xaml.Data.ICustomProperty Microsoft.UI.Xaml.Data.ICustomPropertyPro } /// + [global::System.CodeDom.Compiler.GeneratedCode("CustomPropertyProviderGenerator", )] string Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetStringRepresentation() { return ToString(); @@ -261,21 +265,25 @@ namespace MyNamespace partial class MyClass : Microsoft.UI.Xaml.Data.ICustomPropertyProvider { /// + [global::System.CodeDom.Compiler.GeneratedCode("CustomPropertyProviderGenerator", )] global::System.Type Microsoft.UI.Xaml.Data.ICustomPropertyProvider.Type => typeof(MyClass); /// + [global::System.CodeDom.Compiler.GeneratedCode("CustomPropertyProviderGenerator", )] Microsoft.UI.Xaml.Data.ICustomProperty Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetCustomProperty(string name) { return null; } /// + [global::System.CodeDom.Compiler.GeneratedCode("CustomPropertyProviderGenerator", )] Microsoft.UI.Xaml.Data.ICustomProperty Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetIndexedProperty(string name, global::System.Type type) { return null; } /// + [global::System.CodeDom.Compiler.GeneratedCode("CustomPropertyProviderGenerator", )] string Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetStringRepresentation() { return ToString(); @@ -314,9 +322,11 @@ namespace MyNamespace partial class MyClass : Microsoft.UI.Xaml.Data.ICustomPropertyProvider { /// + [global::System.CodeDom.Compiler.GeneratedCode("CustomPropertyProviderGenerator", )] global::System.Type Microsoft.UI.Xaml.Data.ICustomPropertyProvider.Type => typeof(MyClass); /// + [global::System.CodeDom.Compiler.GeneratedCode("CustomPropertyProviderGenerator", )] Microsoft.UI.Xaml.Data.ICustomProperty Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetCustomProperty(string name) { return name switch @@ -328,12 +338,14 @@ Microsoft.UI.Xaml.Data.ICustomProperty Microsoft.UI.Xaml.Data.ICustomPropertyPro } /// + [global::System.CodeDom.Compiler.GeneratedCode("CustomPropertyProviderGenerator", )] Microsoft.UI.Xaml.Data.ICustomProperty Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetIndexedProperty(string name, global::System.Type type) { return null; } /// + [global::System.CodeDom.Compiler.GeneratedCode("CustomPropertyProviderGenerator", )] string Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetStringRepresentation() { return ToString(); @@ -483,15 +495,18 @@ namespace MyNamespace partial class MyClass : Microsoft.UI.Xaml.Data.ICustomPropertyProvider { /// + [global::System.CodeDom.Compiler.GeneratedCode("CustomPropertyProviderGenerator", )] global::System.Type Microsoft.UI.Xaml.Data.ICustomPropertyProvider.Type => typeof(MyClass); /// + [global::System.CodeDom.Compiler.GeneratedCode("CustomPropertyProviderGenerator", )] Microsoft.UI.Xaml.Data.ICustomProperty Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetCustomProperty(string name) { return null; } /// + [global::System.CodeDom.Compiler.GeneratedCode("CustomPropertyProviderGenerator", )] Microsoft.UI.Xaml.Data.ICustomProperty Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetIndexedProperty(string name, global::System.Type type) { if (type == typeof(int)) @@ -503,6 +518,7 @@ Microsoft.UI.Xaml.Data.ICustomProperty Microsoft.UI.Xaml.Data.ICustomPropertyPro } /// + [global::System.CodeDom.Compiler.GeneratedCode("CustomPropertyProviderGenerator", )] string Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetStringRepresentation() { return ToString(); @@ -598,9 +614,11 @@ namespace MyNamespace partial class MyClass : Microsoft.UI.Xaml.Data.ICustomPropertyProvider { /// + [global::System.CodeDom.Compiler.GeneratedCode("CustomPropertyProviderGenerator", )] global::System.Type Microsoft.UI.Xaml.Data.ICustomPropertyProvider.Type => typeof(MyClass); /// + [global::System.CodeDom.Compiler.GeneratedCode("CustomPropertyProviderGenerator", )] Microsoft.UI.Xaml.Data.ICustomProperty Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetCustomProperty(string name) { return name switch @@ -611,12 +629,14 @@ Microsoft.UI.Xaml.Data.ICustomProperty Microsoft.UI.Xaml.Data.ICustomPropertyPro } /// + [global::System.CodeDom.Compiler.GeneratedCode("CustomPropertyProviderGenerator", )] Microsoft.UI.Xaml.Data.ICustomProperty Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetIndexedProperty(string name, global::System.Type type) { return null; } /// + [global::System.CodeDom.Compiler.GeneratedCode("CustomPropertyProviderGenerator", )] string Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetStringRepresentation() { return ToString(); @@ -712,9 +732,11 @@ namespace MyNamespace partial class MyClass : Microsoft.UI.Xaml.Data.ICustomPropertyProvider { /// + [global::System.CodeDom.Compiler.GeneratedCode("CustomPropertyProviderGenerator", )] global::System.Type Microsoft.UI.Xaml.Data.ICustomPropertyProvider.Type => typeof(MyClass); /// + [global::System.CodeDom.Compiler.GeneratedCode("CustomPropertyProviderGenerator", )] Microsoft.UI.Xaml.Data.ICustomProperty Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetCustomProperty(string name) { return name switch @@ -725,12 +747,14 @@ Microsoft.UI.Xaml.Data.ICustomProperty Microsoft.UI.Xaml.Data.ICustomPropertyPro } /// + [global::System.CodeDom.Compiler.GeneratedCode("CustomPropertyProviderGenerator", )] Microsoft.UI.Xaml.Data.ICustomProperty Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetIndexedProperty(string name, global::System.Type type) { return null; } /// + [global::System.CodeDom.Compiler.GeneratedCode("CustomPropertyProviderGenerator", )] string Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetStringRepresentation() { return ToString(); @@ -826,9 +850,11 @@ namespace MyNamespace partial class MyClass : Microsoft.UI.Xaml.Data.ICustomPropertyProvider { /// + [global::System.CodeDom.Compiler.GeneratedCode("CustomPropertyProviderGenerator", )] global::System.Type Microsoft.UI.Xaml.Data.ICustomPropertyProvider.Type => typeof(MyClass); /// + [global::System.CodeDom.Compiler.GeneratedCode("CustomPropertyProviderGenerator", )] Microsoft.UI.Xaml.Data.ICustomProperty Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetCustomProperty(string name) { return name switch @@ -839,12 +865,14 @@ Microsoft.UI.Xaml.Data.ICustomProperty Microsoft.UI.Xaml.Data.ICustomPropertyPro } /// + [global::System.CodeDom.Compiler.GeneratedCode("CustomPropertyProviderGenerator", )] Microsoft.UI.Xaml.Data.ICustomProperty Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetIndexedProperty(string name, global::System.Type type) { return null; } /// + [global::System.CodeDom.Compiler.GeneratedCode("CustomPropertyProviderGenerator", )] string Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetStringRepresentation() { return ToString(); @@ -940,15 +968,18 @@ namespace MyNamespace partial class MyClass : Microsoft.UI.Xaml.Data.ICustomPropertyProvider { /// + [global::System.CodeDom.Compiler.GeneratedCode("CustomPropertyProviderGenerator", )] global::System.Type Microsoft.UI.Xaml.Data.ICustomPropertyProvider.Type => typeof(MyClass); /// + [global::System.CodeDom.Compiler.GeneratedCode("CustomPropertyProviderGenerator", )] Microsoft.UI.Xaml.Data.ICustomProperty Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetCustomProperty(string name) { return null; } /// + [global::System.CodeDom.Compiler.GeneratedCode("CustomPropertyProviderGenerator", )] Microsoft.UI.Xaml.Data.ICustomProperty Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetIndexedProperty(string name, global::System.Type type) { if (type == typeof(int)) @@ -960,6 +991,7 @@ Microsoft.UI.Xaml.Data.ICustomProperty Microsoft.UI.Xaml.Data.ICustomPropertyPro } /// + [global::System.CodeDom.Compiler.GeneratedCode("CustomPropertyProviderGenerator", )] string Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetStringRepresentation() { return ToString(); From c71c913df091e4316467bf33cfd7d36595696bff Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 12 Mar 2026 23:20:19 -0700 Subject: [PATCH 082/100] Fix CSWINRT2003 analyzer to exclude generated implementations The analyzer was incorrectly firing for types using [GeneratedCustomPropertyProvider] because it detected the interface member implementations produced by the generator itself. Now HasAnyImplementedMembersForInterface accepts an optional GeneratedCodeAttribute type and generator name: implementations annotated with [GeneratedCode("CustomPropertyProviderGenerator", ...)] are excluded from the check. Also removed the unnecessary explicit ': ICustomPropertyProvider' from TestCustomPropertyProvider in the functional tests (the generator adds it via the partial class). Added two new analyzer tests: - TypeWithGeneratedExplicitInterfaceImplementation_DoesNotWarn - TypeWithMixedGeneratedAndUserImplementation_Warns Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...derExistingMemberImplementationAnalyzer.cs | 11 +++- .../Extensions/ITypeSymbolExtensions.cs | 42 +++++++++++++- .../ClassActivation/Program.cs | 2 +- ...derExistingMemberImplementationAnalyzer.cs | 55 +++++++++++++++++++ 4 files changed, 103 insertions(+), 7 deletions(-) diff --git a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/GeneratedCustomPropertyProviderExistingMemberImplementationAnalyzer.cs b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/GeneratedCustomPropertyProviderExistingMemberImplementationAnalyzer.cs index 79ecdadbc2..92f9b78d58 100644 --- a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/GeneratedCustomPropertyProviderExistingMemberImplementationAnalyzer.cs +++ b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/GeneratedCustomPropertyProviderExistingMemberImplementationAnalyzer.cs @@ -42,6 +42,9 @@ public override void Initialize(AnalysisContext context) return; } + // Resolve the '[GeneratedCode]' attribute type, used to skip generated implementations + INamedTypeSymbol? generatedCodeAttributeType = context.Compilation.GetTypeByMetadataName("System.CodeDom.Compiler.GeneratedCodeAttribute"); + context.RegisterSymbolAction(context => { // Only classes and structs can be targets of the attribute @@ -56,9 +59,11 @@ public override void Initialize(AnalysisContext context) return; } - // Check whether the type has or inherits any 'ICustomPropertyProvider' member implementations - if ((windowsUIXamlCustomPropertyProviderType is not null && typeSymbol.HasAnyImplementedMembersForInterface(windowsUIXamlCustomPropertyProviderType)) || - (microsoftUIXamlCustomPropertyProviderType is not null && typeSymbol.HasAnyImplementedMembersForInterface(microsoftUIXamlCustomPropertyProviderType))) + // Check whether the type has or inherits any 'ICustomPropertyProvider' member implementations. + // We exclude implementations generated by CustomPropertyProviderGenerator itself, so that + // only user-authored implementations trigger this diagnostic. + if ((windowsUIXamlCustomPropertyProviderType is not null && typeSymbol.HasAnyImplementedMembersForInterface(windowsUIXamlCustomPropertyProviderType, generatedCodeAttributeType, nameof(CustomPropertyProviderGenerator))) || + (microsoftUIXamlCustomPropertyProviderType is not null && typeSymbol.HasAnyImplementedMembersForInterface(microsoftUIXamlCustomPropertyProviderType, generatedCodeAttributeType, nameof(CustomPropertyProviderGenerator)))) { context.ReportDiagnostic(Diagnostic.Create( DiagnosticDescriptors.GeneratedCustomPropertyProviderExistingMemberImplementation, diff --git a/src/Authoring/WinRT.SourceGenerator2/Extensions/ITypeSymbolExtensions.cs b/src/Authoring/WinRT.SourceGenerator2/Extensions/ITypeSymbolExtensions.cs index a96ea4e659..d306ab1acf 100644 --- a/src/Authoring/WinRT.SourceGenerator2/Extensions/ITypeSymbolExtensions.cs +++ b/src/Authoring/WinRT.SourceGenerator2/Extensions/ITypeSymbolExtensions.cs @@ -61,8 +61,19 @@ public IEnumerable EnumerateAllMembers() /// and has or inherits implementations for any of its members. /// /// The interface type to check for member implementations. + /// + /// If not , implementations annotated with [GeneratedCode] of this + /// type and whose first constructor argument matches are excluded. + /// + /// + /// The generator name to match against the first constructor argument of [GeneratedCode]. + /// Only used when is not . + /// /// Whether has any implemented members for . - public bool HasAnyImplementedMembersForInterface(INamedTypeSymbol interfaceType) + public bool HasAnyImplementedMembersForInterface( + INamedTypeSymbol interfaceType, + INamedTypeSymbol? generatedCodeAttributeType = null, + string? generatorName = null) { if (!symbol.AllInterfaces.Contains(interfaceType, SymbolEqualityComparer.Default)) { @@ -71,13 +82,38 @@ public bool HasAnyImplementedMembersForInterface(INamedTypeSymbol interfaceType) foreach (ISymbol member in interfaceType.GetMembers()) { - if (symbol.FindImplementationForInterfaceMember(member) is not null) + if (symbol.FindImplementationForInterfaceMember(member) is not { } implementation) { - return true; + continue; } + + // If requested, skip implementations generated by the specified generator + if (generatedCodeAttributeType is not null && + HasGeneratedCodeAttributeWithName(implementation, generatedCodeAttributeType, generatorName)) + { + continue; + } + + return true; } return false; + + // Checks whether a symbol has a [GeneratedCode] attribute with the specified generator name. + static bool HasGeneratedCodeAttributeWithName(ISymbol symbol, INamedTypeSymbol generatedCodeAttributeType, string? generatorName) + { + foreach (AttributeData attribute in symbol.GetAttributes()) + { + if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, generatedCodeAttributeType) && + attribute.ConstructorArguments is [{ Value: string name }, ..] && + name == generatorName) + { + return true; + } + } + + return false; + } } /// diff --git a/src/Tests/FunctionalTests/ClassActivation/Program.cs b/src/Tests/FunctionalTests/ClassActivation/Program.cs index d00a4d2507..f9d2a3d7ae 100644 --- a/src/Tests/FunctionalTests/ClassActivation/Program.cs +++ b/src/Tests/FunctionalTests/ClassActivation/Program.cs @@ -290,7 +290,7 @@ IEnumerator> IEnumerable>.GetEnumerato } [GeneratedCustomPropertyProvider] -sealed partial class TestCustomPropertyProvider : ICustomPropertyProvider +sealed partial class TestCustomPropertyProvider { public string Text => "Hello"; diff --git a/src/Tests/SourceGenerator2Test/Test_GeneratedCustomPropertyProviderExistingMemberImplementationAnalyzer.cs b/src/Tests/SourceGenerator2Test/Test_GeneratedCustomPropertyProviderExistingMemberImplementationAnalyzer.cs index 1dc506a942..9810434058 100644 --- a/src/Tests/SourceGenerator2Test/Test_GeneratedCustomPropertyProviderExistingMemberImplementationAnalyzer.cs +++ b/src/Tests/SourceGenerator2Test/Test_GeneratedCustomPropertyProviderExistingMemberImplementationAnalyzer.cs @@ -174,4 +174,59 @@ public partial class {|CSWINRT2003:MyType|} : BaseType; await CSharpAnalyzerTest.VerifyAnalyzerAsync(source); } + + [TestMethod] + public async Task TypeWithGeneratedExplicitInterfaceImplementation_DoesNotWarn() + { + string source = """ + using System; + using System.CodeDom.Compiler; + using Microsoft.UI.Xaml.Data; + using WindowsRuntime.Xaml; + + [GeneratedCustomPropertyProvider] + public partial class MyType : ICustomPropertyProvider + { + public string Name { get; set; } + + [GeneratedCode("CustomPropertyProviderGenerator", "1.0.0")] + Type ICustomPropertyProvider.Type => typeof(MyType); + [GeneratedCode("CustomPropertyProviderGenerator", "1.0.0")] + ICustomProperty ICustomPropertyProvider.GetCustomProperty(string name) => null; + [GeneratedCode("CustomPropertyProviderGenerator", "1.0.0")] + ICustomProperty ICustomPropertyProvider.GetIndexedProperty(string name, Type type) => null; + [GeneratedCode("CustomPropertyProviderGenerator", "1.0.0")] + string ICustomPropertyProvider.GetStringRepresentation() => ""; + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source); + } + + [TestMethod] + public async Task TypeWithMixedGeneratedAndUserImplementation_Warns() + { + string source = """ + using System; + using System.CodeDom.Compiler; + using Microsoft.UI.Xaml.Data; + using WindowsRuntime.Xaml; + + [GeneratedCustomPropertyProvider] + public partial class {|CSWINRT2003:MyType|} : ICustomPropertyProvider + { + public string Name { get; set; } + + Type ICustomPropertyProvider.Type => typeof(MyType); + [GeneratedCode("CustomPropertyProviderGenerator", "1.0.0")] + ICustomProperty ICustomPropertyProvider.GetCustomProperty(string name) => null; + [GeneratedCode("CustomPropertyProviderGenerator", "1.0.0")] + ICustomProperty ICustomPropertyProvider.GetIndexedProperty(string name, Type type) => null; + [GeneratedCode("CustomPropertyProviderGenerator", "1.0.0")] + string ICustomPropertyProvider.GetStringRepresentation() => ""; + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source); + } } From 80e75bbde6ca91cda010c7f5896e63ecd25239a2 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 12 Mar 2026 23:29:35 -0700 Subject: [PATCH 083/100] Refactor HasAnyImplementedMembersForInterface into three methods Split the monolithic HasAnyImplementedMembersForInterface into: - EnumerateImplementedMembersForInterface: enumerates all implemented members - HasAnyImplementedMembersForInterface: simplified to delegate to the enumerator - AreAllImplementedByGenerator: checks if all symbols in a sequence are generated Update the analyzer to use AreAllImplementedByGenerator, so it no longer warns when all interface member implementations are produced by our generator. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...derExistingMemberImplementationAnalyzer.cs | 18 ++++-- .../Extensions/ISymbolExtensions.cs | 40 ++++++++++++ .../Extensions/ITypeSymbolExtensions.cs | 61 ++++++------------- 3 files changed, 73 insertions(+), 46 deletions(-) diff --git a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/GeneratedCustomPropertyProviderExistingMemberImplementationAnalyzer.cs b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/GeneratedCustomPropertyProviderExistingMemberImplementationAnalyzer.cs index 92f9b78d58..c9390b4c86 100644 --- a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/GeneratedCustomPropertyProviderExistingMemberImplementationAnalyzer.cs +++ b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/GeneratedCustomPropertyProviderExistingMemberImplementationAnalyzer.cs @@ -60,16 +60,26 @@ public override void Initialize(AnalysisContext context) } // Check whether the type has or inherits any 'ICustomPropertyProvider' member implementations. - // We exclude implementations generated by CustomPropertyProviderGenerator itself, so that - // only user-authored implementations trigger this diagnostic. - if ((windowsUIXamlCustomPropertyProviderType is not null && typeSymbol.HasAnyImplementedMembersForInterface(windowsUIXamlCustomPropertyProviderType, generatedCodeAttributeType, nameof(CustomPropertyProviderGenerator))) || - (microsoftUIXamlCustomPropertyProviderType is not null && typeSymbol.HasAnyImplementedMembersForInterface(microsoftUIXamlCustomPropertyProviderType, generatedCodeAttributeType, nameof(CustomPropertyProviderGenerator)))) + // We don't warn if all implementations are generated by CustomPropertyProviderGenerator itself, + // so that only user-authored implementations trigger this diagnostic. + if (HasNonGeneratedImplementedMembers(typeSymbol, windowsUIXamlCustomPropertyProviderType, generatedCodeAttributeType) || + HasNonGeneratedImplementedMembers(typeSymbol, microsoftUIXamlCustomPropertyProviderType, generatedCodeAttributeType)) { context.ReportDiagnostic(Diagnostic.Create( DiagnosticDescriptors.GeneratedCustomPropertyProviderExistingMemberImplementation, typeSymbol.Locations.FirstOrDefault(), typeSymbol)); } + + static bool HasNonGeneratedImplementedMembers( + INamedTypeSymbol typeSymbol, + INamedTypeSymbol? interfaceType, + INamedTypeSymbol? generatedCodeAttributeType) + { + return interfaceType is not null && (generatedCodeAttributeType is not null + ? !typeSymbol.EnumerateImplementedMembersForInterface(interfaceType).AreAllImplementedByGenerator(generatedCodeAttributeType, nameof(CustomPropertyProviderGenerator)) + : typeSymbol.HasAnyImplementedMembersForInterface(interfaceType)); + } }, SymbolKind.NamedType); }); } diff --git a/src/Authoring/WinRT.SourceGenerator2/Extensions/ISymbolExtensions.cs b/src/Authoring/WinRT.SourceGenerator2/Extensions/ISymbolExtensions.cs index 69993e1931..c8adaf66ea 100644 --- a/src/Authoring/WinRT.SourceGenerator2/Extensions/ISymbolExtensions.cs +++ b/src/Authoring/WinRT.SourceGenerator2/Extensions/ISymbolExtensions.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using Microsoft.CodeAnalysis; @@ -77,4 +78,43 @@ public bool IsAccessibleFromCompilationAssembly(Compilation compilation) return compilation.IsSymbolAccessibleWithin(symbol, compilation.Assembly); } } + + /// The input sequence of instances. + extension(IEnumerable symbols) + { + /// + /// Checks whether all symbols in the sequence are generated by a specific generator. + /// + /// The for [GeneratedCode]. + /// The generator name to match against the first constructor argument of [GeneratedCode]. + /// Whether all symbols in have a [GeneratedCode] attribute matching . + public bool AreAllImplementedByGenerator(INamedTypeSymbol generatedCodeAttributeType, string generatorName) + { + foreach (ISymbol symbol in symbols) + { + if (!HasGeneratedCodeAttributeWithName(symbol, generatedCodeAttributeType, generatorName)) + { + return false; + } + } + + return true; + + // Checks whether a symbol has a [GeneratedCode] attribute with the specified generator name. + static bool HasGeneratedCodeAttributeWithName(ISymbol symbol, INamedTypeSymbol generatedCodeAttributeType, string generatorName) + { + foreach (AttributeData attribute in symbol.GetAttributes()) + { + if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, generatedCodeAttributeType) && + attribute.ConstructorArguments is [{ Value: string name }, ..] && + name == generatorName) + { + return true; + } + } + + return false; + } + } + } } \ No newline at end of file diff --git a/src/Authoring/WinRT.SourceGenerator2/Extensions/ITypeSymbolExtensions.cs b/src/Authoring/WinRT.SourceGenerator2/Extensions/ITypeSymbolExtensions.cs index d306ab1acf..a003cb5df1 100644 --- a/src/Authoring/WinRT.SourceGenerator2/Extensions/ITypeSymbolExtensions.cs +++ b/src/Authoring/WinRT.SourceGenerator2/Extensions/ITypeSymbolExtensions.cs @@ -57,63 +57,40 @@ public IEnumerable EnumerateAllMembers() } /// - /// Checks whether the given implements a specified interface - /// and has or inherits implementations for any of its members. + /// Enumerates all members of a specified interface that have implementations on the given . /// - /// The interface type to check for member implementations. - /// - /// If not , implementations annotated with [GeneratedCode] of this - /// type and whose first constructor argument matches are excluded. - /// - /// - /// The generator name to match against the first constructor argument of [GeneratedCode]. - /// Only used when is not . - /// - /// Whether has any implemented members for . - public bool HasAnyImplementedMembersForInterface( - INamedTypeSymbol interfaceType, - INamedTypeSymbol? generatedCodeAttributeType = null, - string? generatorName = null) + /// The interface type to enumerate member implementations for. + /// The sequence of all implemented members for on . + public IEnumerable EnumerateImplementedMembersForInterface(INamedTypeSymbol interfaceType) { if (!symbol.AllInterfaces.Contains(interfaceType, SymbolEqualityComparer.Default)) { - return false; + yield break; } foreach (ISymbol member in interfaceType.GetMembers()) { - if (symbol.FindImplementationForInterfaceMember(member) is not { } implementation) - { - continue; - } - - // If requested, skip implementations generated by the specified generator - if (generatedCodeAttributeType is not null && - HasGeneratedCodeAttributeWithName(implementation, generatedCodeAttributeType, generatorName)) + if (symbol.FindImplementationForInterfaceMember(member) is { } implementation) { - continue; + yield return implementation; } + } + } + /// + /// Checks whether the given implements a specified interface + /// and has or inherits implementations for any of its members. + /// + /// The interface type to check for member implementations. + /// Whether has any implemented members for . + public bool HasAnyImplementedMembersForInterface(INamedTypeSymbol interfaceType) + { + foreach (ISymbol _ in symbol.EnumerateImplementedMembersForInterface(interfaceType)) + { return true; } return false; - - // Checks whether a symbol has a [GeneratedCode] attribute with the specified generator name. - static bool HasGeneratedCodeAttributeWithName(ISymbol symbol, INamedTypeSymbol generatedCodeAttributeType, string? generatorName) - { - foreach (AttributeData attribute in symbol.GetAttributes()) - { - if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, generatedCodeAttributeType) && - attribute.ConstructorArguments is [{ Value: string name }, ..] && - name == generatorName) - { - return true; - } - } - - return false; - } } /// From 130602a77cf273e6c1175dae2c635d6718660cb2 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 13 Mar 2026 10:56:23 -0700 Subject: [PATCH 084/100] Refactor generated-code checks and helper Improve detection of generated ICustomPropertyProvider implementations and simplify attribute handling. Make GeneratedCodeAttribute resolution non-nullable and add HasNonGeneratedImplementedMembers helper to centralize logic for checking implemented interface members that were not emitted by this generator. Simplify attribute checks in ISymbolExtensions by using TryGetAttributeWithType and validating the constructor argument, and replace manual loop with LINQ.Any in ITypeSymbolExtensions (added System.Linq). Removes duplicate/nullable checks and streamlines the analyzer to avoid false diagnostics for generator-emitted members. --- ...derExistingMemberImplementationAnalyzer.cs | 26 ++++++++++--------- .../Extensions/ISymbolExtensions.cs | 23 +++++----------- .../Extensions/ITypeSymbolExtensions.cs | 8 ++---- 3 files changed, 23 insertions(+), 34 deletions(-) diff --git a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/GeneratedCustomPropertyProviderExistingMemberImplementationAnalyzer.cs b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/GeneratedCustomPropertyProviderExistingMemberImplementationAnalyzer.cs index c9390b4c86..1ce46ad4e4 100644 --- a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/GeneratedCustomPropertyProviderExistingMemberImplementationAnalyzer.cs +++ b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/GeneratedCustomPropertyProviderExistingMemberImplementationAnalyzer.cs @@ -43,7 +43,7 @@ public override void Initialize(AnalysisContext context) } // Resolve the '[GeneratedCode]' attribute type, used to skip generated implementations - INamedTypeSymbol? generatedCodeAttributeType = context.Compilation.GetTypeByMetadataName("System.CodeDom.Compiler.GeneratedCodeAttribute"); + INamedTypeSymbol generatedCodeAttributeType = context.Compilation.GetTypeByMetadataName("System.CodeDom.Compiler.GeneratedCodeAttribute")!; context.RegisterSymbolAction(context => { @@ -59,8 +59,20 @@ public override void Initialize(AnalysisContext context) return; } + // Helper to check whether a symbol has any generated interface members that are not emitted by this generator. his covers + // both manually implemented members and those produced by other generators, and explicitly interface implementations too. + static bool HasNonGeneratedImplementedMembers( + INamedTypeSymbol typeSymbol, + INamedTypeSymbol? interfaceType, + INamedTypeSymbol generatedCodeAttributeType) + { + return + interfaceType is not null && + !typeSymbol.EnumerateImplementedMembersForInterface(interfaceType).AreAllImplementedByGenerator(generatedCodeAttributeType, nameof(CustomPropertyProviderGenerator)); + } + // Check whether the type has or inherits any 'ICustomPropertyProvider' member implementations. - // We don't warn if all implementations are generated by CustomPropertyProviderGenerator itself, + // We don't warn if all implementations are generated by 'CustomPropertyProviderGenerator' itself, // so that only user-authored implementations trigger this diagnostic. if (HasNonGeneratedImplementedMembers(typeSymbol, windowsUIXamlCustomPropertyProviderType, generatedCodeAttributeType) || HasNonGeneratedImplementedMembers(typeSymbol, microsoftUIXamlCustomPropertyProviderType, generatedCodeAttributeType)) @@ -70,16 +82,6 @@ public override void Initialize(AnalysisContext context) typeSymbol.Locations.FirstOrDefault(), typeSymbol)); } - - static bool HasNonGeneratedImplementedMembers( - INamedTypeSymbol typeSymbol, - INamedTypeSymbol? interfaceType, - INamedTypeSymbol? generatedCodeAttributeType) - { - return interfaceType is not null && (generatedCodeAttributeType is not null - ? !typeSymbol.EnumerateImplementedMembersForInterface(interfaceType).AreAllImplementedByGenerator(generatedCodeAttributeType, nameof(CustomPropertyProviderGenerator)) - : typeSymbol.HasAnyImplementedMembersForInterface(interfaceType)); - } }, SymbolKind.NamedType); }); } diff --git a/src/Authoring/WinRT.SourceGenerator2/Extensions/ISymbolExtensions.cs b/src/Authoring/WinRT.SourceGenerator2/Extensions/ISymbolExtensions.cs index c8adaf66ea..94df0494be 100644 --- a/src/Authoring/WinRT.SourceGenerator2/Extensions/ISymbolExtensions.cs +++ b/src/Authoring/WinRT.SourceGenerator2/Extensions/ISymbolExtensions.cs @@ -92,29 +92,20 @@ public bool AreAllImplementedByGenerator(INamedTypeSymbol generatedCodeAttribute { foreach (ISymbol symbol in symbols) { - if (!HasGeneratedCodeAttributeWithName(symbol, generatedCodeAttributeType, generatorName)) + // Stop if the symbol doesn't have '[GeneratedCode]' on it + if (!symbol.TryGetAttributeWithType(generatedCodeAttributeType, out AttributeData? attributeData)) { return false; } - } - - return true; - // Checks whether a symbol has a [GeneratedCode] attribute with the specified generator name. - static bool HasGeneratedCodeAttributeWithName(ISymbol symbol, INamedTypeSymbol generatedCodeAttributeType, string generatorName) - { - foreach (AttributeData attribute in symbol.GetAttributes()) + // Check that the symbol was specifically generated by the target generator + if (attributeData.ConstructorArguments is not [{ Value: string name }, ..] || name != generatorName) { - if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, generatedCodeAttributeType) && - attribute.ConstructorArguments is [{ Value: string name }, ..] && - name == generatorName) - { - return true; - } + return false; } - - return false; } + + return true; } } } \ No newline at end of file diff --git a/src/Authoring/WinRT.SourceGenerator2/Extensions/ITypeSymbolExtensions.cs b/src/Authoring/WinRT.SourceGenerator2/Extensions/ITypeSymbolExtensions.cs index a003cb5df1..30cd258ea2 100644 --- a/src/Authoring/WinRT.SourceGenerator2/Extensions/ITypeSymbolExtensions.cs +++ b/src/Authoring/WinRT.SourceGenerator2/Extensions/ITypeSymbolExtensions.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using Microsoft.CodeAnalysis; #pragma warning disable CS1734, IDE0046 @@ -85,12 +86,7 @@ public IEnumerable EnumerateImplementedMembersForInterface(INamedTypeSy /// Whether has any implemented members for . public bool HasAnyImplementedMembersForInterface(INamedTypeSymbol interfaceType) { - foreach (ISymbol _ in symbol.EnumerateImplementedMembersForInterface(interfaceType)) - { - return true; - } - - return false; + return symbol.EnumerateImplementedMembersForInterface(interfaceType).Any(); } /// From 5659ecf35697b1557a8a29a73f90749b684e01ca Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 13 Mar 2026 12:38:26 -0700 Subject: [PATCH 085/100] Add diagnostic descriptors for attribute argument validation Add 5 new diagnostic descriptors (CSWINRT2004-CSWINRT2008) for validating [GeneratedCustomPropertyProvider] attribute arguments: - CSWINRT2004: Null property name - CSWINRT2005: Null indexer type - CSWINRT2006: Property name not found - CSWINRT2007: Indexer type not found - CSWINRT2008: Static indexer not supported Also update AnalyzerReleases.Unshipped.md with the new rules. Co-Authored-By: Copilot <223556219+Copilot@users.noreply.github.com> --- .../AnalyzerReleases.Shipped.md | 7 +- .../Diagnostics/DiagnosticDescriptors.cs | 65 +++++++++++++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) diff --git a/src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Shipped.md b/src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Shipped.md index ccafc41872..d6343c4c03 100644 --- a/src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Shipped.md +++ b/src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Shipped.md @@ -9,4 +9,9 @@ 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 -CSWINRT2003 | WindowsRuntime.SourceGenerator | Error | Existing 'ICustomPropertyProvider' member implementation \ No newline at end of file +CSWINRT2003 | WindowsRuntime.SourceGenerator | Error | Existing 'ICustomPropertyProvider' member implementation +CSWINRT2004 | WindowsRuntime.SourceGenerator | Error | Null property name in '[GeneratedCustomPropertyProvider]' +CSWINRT2005 | WindowsRuntime.SourceGenerator | Error | Null indexer type in '[GeneratedCustomPropertyProvider]' +CSWINRT2006 | WindowsRuntime.SourceGenerator | Error | Property name not found for '[GeneratedCustomPropertyProvider]' +CSWINRT2007 | WindowsRuntime.SourceGenerator | Error | Indexer type not found for '[GeneratedCustomPropertyProvider]' +CSWINRT2008 | WindowsRuntime.SourceGenerator | Error | Static indexer for '[GeneratedCustomPropertyProvider]' \ No newline at end of file diff --git a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/DiagnosticDescriptors.cs b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/DiagnosticDescriptors.cs index 5fbc82f97e..99c315ef29 100644 --- a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/DiagnosticDescriptors.cs +++ b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/DiagnosticDescriptors.cs @@ -61,4 +61,69 @@ internal static partial class DiagnosticDescriptors isEnabledByDefault: true, description: "Types annotated with '[GeneratedCustomPropertyProvider]' must not already have or inherit implementations for any 'ICustomPropertyProvider' members, as the generator will provide them.", helpLinkUri: "https://github.com/microsoft/CsWinRT"); + + /// + /// Gets a for when a property name is specified in [GeneratedCustomPropertyProvider]. + /// + public static readonly DiagnosticDescriptor GeneratedCustomPropertyProviderNullPropertyName = new( + id: "CSWINRT2004", + title: "Null property name in '[GeneratedCustomPropertyProvider]'", + messageFormat: """A null property name was specified in '[GeneratedCustomPropertyProvider]' on type '{0}'""", + category: "WindowsRuntime.SourceGenerator", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "Property names specified in '[GeneratedCustomPropertyProvider]' must not be null.", + helpLinkUri: "https://github.com/microsoft/CsWinRT"); + + /// + /// Gets a for when a indexer type is specified in [GeneratedCustomPropertyProvider]. + /// + public static readonly DiagnosticDescriptor GeneratedCustomPropertyProviderNullIndexerType = new( + id: "CSWINRT2005", + title: "Null indexer type in '[GeneratedCustomPropertyProvider]'", + messageFormat: """A null indexer type was specified in '[GeneratedCustomPropertyProvider]' on type '{0}'""", + category: "WindowsRuntime.SourceGenerator", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "Indexer types specified in '[GeneratedCustomPropertyProvider]' must not be null.", + helpLinkUri: "https://github.com/microsoft/CsWinRT"); + + /// + /// Gets a for when a property name in [GeneratedCustomPropertyProvider] doesn't match any accessible property. + /// + public static readonly DiagnosticDescriptor GeneratedCustomPropertyProviderPropertyNameNotFound = new( + id: "CSWINRT2006", + title: "Property name not found for '[GeneratedCustomPropertyProvider]'", + messageFormat: """The property name '{0}' specified in '[GeneratedCustomPropertyProvider]' on type '{1}' does not match any accessible property""", + category: "WindowsRuntime.SourceGenerator", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "Property names specified in '[GeneratedCustomPropertyProvider]' must match the name of a public, non-override, non-indexer property on the annotated type.", + helpLinkUri: "https://github.com/microsoft/CsWinRT"); + + /// + /// Gets a for when an indexer type in [GeneratedCustomPropertyProvider] doesn't match any accessible indexer. + /// + public static readonly DiagnosticDescriptor GeneratedCustomPropertyProviderIndexerTypeNotFound = new( + id: "CSWINRT2007", + title: "Indexer type not found for '[GeneratedCustomPropertyProvider]'", + messageFormat: """The indexer type '{0}' specified in '[GeneratedCustomPropertyProvider]' on type '{1}' does not match any accessible indexer""", + category: "WindowsRuntime.SourceGenerator", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "Indexer types specified in '[GeneratedCustomPropertyProvider]' must match the parameter type of a public, non-override, non-static, single-parameter indexer on the annotated type.", + helpLinkUri: "https://github.com/microsoft/CsWinRT"); + + /// + /// Gets a for when an indexer type in [GeneratedCustomPropertyProvider] matches a static indexer. + /// + public static readonly DiagnosticDescriptor GeneratedCustomPropertyProviderStaticIndexer = new( + id: "CSWINRT2008", + title: "Static indexer for '[GeneratedCustomPropertyProvider]'", + messageFormat: """The indexer type '{0}' specified in '[GeneratedCustomPropertyProvider]' on type '{1}' matches a static indexer, which is not supported""", + category: "WindowsRuntime.SourceGenerator", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "Indexers used with '[GeneratedCustomPropertyProvider]' must be instance indexers, not static indexers.", + helpLinkUri: "https://github.com/microsoft/CsWinRT"); } \ No newline at end of file From 659dd5bace9dea0c40c4a8e792a9029661d0e785 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 13 Mar 2026 12:39:26 -0700 Subject: [PATCH 086/100] Add analyzer for [GeneratedCustomPropertyProvider] attribute arguments Add GeneratedCustomPropertyProviderAttributeArgumentAnalyzer that validates the explicit property names and indexer types passed to the attribute constructor, replicating the filtering logic from the generator: - Reports CSWINRT2004 for null property names in the array - Reports CSWINRT2005 for null indexer types in the array - Reports CSWINRT2006 when a property name doesn't match any accessible public, non-override, non-indexer property on the type - Reports CSWINRT2007 when an indexer type doesn't match any accessible public, non-override, non-static, single-parameter indexer - Reports CSWINRT2008 when the matching indexer is static Co-Authored-By: Copilot <223556219+Copilot@users.noreply.github.com> --- ...opertyProviderAttributeArgumentAnalyzer.cs | 203 ++++++++++++++++++ 1 file changed, 203 insertions(+) create mode 100644 src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/GeneratedCustomPropertyProviderAttributeArgumentAnalyzer.cs diff --git a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/GeneratedCustomPropertyProviderAttributeArgumentAnalyzer.cs b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/GeneratedCustomPropertyProviderAttributeArgumentAnalyzer.cs new file mode 100644 index 0000000000..c9e8c04693 --- /dev/null +++ b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/GeneratedCustomPropertyProviderAttributeArgumentAnalyzer.cs @@ -0,0 +1,203 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace WindowsRuntime.SourceGenerator.Diagnostics; + +/// +/// A diagnostic analyzer that validates the arguments of [GeneratedCustomPropertyProvider] when +/// using the constructor taking explicit property names and indexer types. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class GeneratedCustomPropertyProviderAttributeArgumentAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = [ + DiagnosticDescriptors.GeneratedCustomPropertyProviderNullPropertyName, + DiagnosticDescriptors.GeneratedCustomPropertyProviderNullIndexerType, + DiagnosticDescriptors.GeneratedCustomPropertyProviderPropertyNameNotFound, + DiagnosticDescriptors.GeneratedCustomPropertyProviderIndexerTypeNotFound, + DiagnosticDescriptors.GeneratedCustomPropertyProviderStaticIndexer]; + + /// + 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; + } + + // Get the attribute instance, if present + if (!typeSymbol.TryGetAttributeWithType(attributeType, out AttributeData? attribute)) + { + return; + } + + // Only validate when using the constructor with explicit property names and indexer types + if (attribute.ConstructorArguments is not [ + { Kind: TypedConstantKind.Array, Values: var typedPropertyNames }, + { Kind: TypedConstantKind.Array, Values: var typedIndexerTypes }]) + { + return; + } + + // Validate all property name arguments + foreach (TypedConstant typedName in typedPropertyNames) + { + // Check that we have a valid 'string' value + if (typedName.Value is not string propertyName) + { + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.GeneratedCustomPropertyProviderNullPropertyName, + typeSymbol.Locations.FirstOrDefault(), + typeSymbol)); + + continue; + } + + // Check whether any public, non-override, non-partial-impl, non-indexer property has this name + if (!HasAccessiblePropertyWithName(typeSymbol, propertyName)) + { + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.GeneratedCustomPropertyProviderPropertyNameNotFound, + typeSymbol.Locations.FirstOrDefault(), + propertyName, + typeSymbol)); + } + } + + // Validate all indexer type arguments + foreach (TypedConstant typedType in typedIndexerTypes) + { + // Check that we have a valid 'Type' value (the parameter type of the target indexer) + if (typedType.Value is not ITypeSymbol indexerType) + { + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.GeneratedCustomPropertyProviderNullIndexerType, + typeSymbol.Locations.FirstOrDefault(), + typeSymbol)); + + continue; + } + + // Check whether there's a static indexer with a matching parameter type (which wouldn't be usable) + if (HasStaticIndexerWithParameterType(typeSymbol, indexerType)) + { + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.GeneratedCustomPropertyProviderStaticIndexer, + typeSymbol.Locations.FirstOrDefault(), + indexerType, + typeSymbol)); + } + else if (!HasAccessibleIndexerWithParameterType(typeSymbol, indexerType)) + { + // No matching instance or static indexer at all + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.GeneratedCustomPropertyProviderIndexerTypeNotFound, + typeSymbol.Locations.FirstOrDefault(), + indexerType, + typeSymbol)); + } + } + }, SymbolKind.NamedType); + }); + } + + /// + /// Checks whether a type has an accessible (public, non-override, non-indexer) property with a given name. + /// + /// The type to inspect. + /// The property name to look for. + /// Whether a matching property exists. + private static bool HasAccessiblePropertyWithName(INamedTypeSymbol typeSymbol, string propertyName) + { + foreach (ISymbol symbol in typeSymbol.EnumerateAllMembers()) + { + // Filter to public properties that we might care about (we ignore indexers here) + if (symbol is not IPropertySymbol { DeclaredAccessibility: Accessibility.Public, IsOverride: false, PartialDefinitionPart: null, IsIndexer: false } property) + { + continue; + } + + if (property.Name.Equals(propertyName, StringComparison.Ordinal)) + { + return true; + } + } + + return false; + } + + /// + /// Checks whether a type has an accessible (public, non-override, non-static, single-parameter) indexer + /// with a parameter matching a given type. + /// + /// The type to inspect. + /// The indexer parameter type to look for. + /// Whether a matching indexer exists. + private static bool HasAccessibleIndexerWithParameterType(INamedTypeSymbol typeSymbol, ITypeSymbol indexerType) + { + foreach (ISymbol symbol in typeSymbol.EnumerateAllMembers()) + { + // Same filtering as above, but we also exclude static members, and we only look for indexers + if (symbol is not IPropertySymbol { DeclaredAccessibility: Accessibility.Public, IsOverride: false, PartialDefinitionPart: null, IsIndexer: true, IsStatic: false } property) + { + continue; + } + + // Check that we have a single parameter, that matches theone we're looking for + if (property.Parameters is [{ } parameter] && SymbolEqualityComparer.Default.Equals(parameter.Type, indexerType)) + { + return true; + } + } + + return false; + } + + /// + /// Checks whether a type has a static indexer with a parameter matching a given type. + /// This is used to provide a more specific diagnostic when the indexer exists but is static. + /// + /// The type to inspect. + /// The indexer parameter type to look for. + /// Whether a matching static indexer exists. + private static bool HasStaticIndexerWithParameterType(INamedTypeSymbol typeSymbol, ITypeSymbol indexerType) + { + foreach (ISymbol symbol in typeSymbol.EnumerateAllMembers()) + { + // Same filtering as above, but this time including static properties + if (symbol is not IPropertySymbol { DeclaredAccessibility: Accessibility.Public, IsOverride: false, PartialDefinitionPart: null, IsIndexer: true, IsStatic: true } property) + { + continue; + } + + // Validate the parameter type (same as above) + if (property.Parameters is [{ } parameter] && SymbolEqualityComparer.Default.Equals(parameter.Type, indexerType)) + { + return true; + } + } + + return false; + } +} From 4753601481dae66c76d62b0eb1a9f8fb8ccc8cf8 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 13 Mar 2026 13:12:02 -0700 Subject: [PATCH 087/100] Add tests for attribute argument analyzer Add comprehensive test coverage for GeneratedCustomPropertyProviderAttributeArgumentAnalyzer with 20 test cases covering all 5 diagnostics: No-warning cases (10): - Default constructor, empty arrays, valid names/types - Inherited members, multiple valid names, overridden properties CSWINRT2004 - Null property name (2): - Single null, null among valid names CSWINRT2005 - Null indexer type (2): - Single null, null among valid types CSWINRT2006 - Property name not found (3): - Missing name, private property, multiple missing names CSWINRT2007 - Indexer type not found (4): - Wrong type, no indexer, multi-parameter indexer, multiple missing Combined (1): - Mixed invalid arguments reports all applicable diagnostics Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...opertyProviderAttributeArgumentAnalyzer.cs | 394 ++++++++++++++++++ 1 file changed, 394 insertions(+) create mode 100644 src/Tests/SourceGenerator2Test/Test_GeneratedCustomPropertyProviderAttributeArgumentAnalyzer.cs diff --git a/src/Tests/SourceGenerator2Test/Test_GeneratedCustomPropertyProviderAttributeArgumentAnalyzer.cs b/src/Tests/SourceGenerator2Test/Test_GeneratedCustomPropertyProviderAttributeArgumentAnalyzer.cs new file mode 100644 index 0000000000..715feb3539 --- /dev/null +++ b/src/Tests/SourceGenerator2Test/Test_GeneratedCustomPropertyProviderAttributeArgumentAnalyzer.cs @@ -0,0 +1,394 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Testing; +using WindowsRuntime.SourceGenerator.Diagnostics; +using WindowsRuntime.SourceGenerator.Tests.Helpers; +using VerifyCS = Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier< + WindowsRuntime.SourceGenerator.Diagnostics.GeneratedCustomPropertyProviderAttributeArgumentAnalyzer, + Microsoft.CodeAnalysis.Testing.DefaultVerifier>; + +namespace WindowsRuntime.SourceGenerator.Tests; + +[TestClass] +public class Test_GeneratedCustomPropertyProviderAttributeArgumentAnalyzer +{ + [TestMethod] + [DataRow("class")] + [DataRow("struct")] + public async Task DefaultConstructor_DoesNotWarn(string modifier) + { + string source = $$""" + using WindowsRuntime.Xaml; + + [GeneratedCustomPropertyProvider] + public partial {{modifier}} MyType + { + public string Name { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source); + } + + [TestMethod] + public async Task EmptyArrays_DoesNotWarn() + { + string source = """ + using System; + using WindowsRuntime.Xaml; + + [GeneratedCustomPropertyProvider(new string[] { }, new Type[] { })] + public partial class MyType + { + public string Name { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source); + } + + [TestMethod] + public async Task ValidPropertyName_DoesNotWarn() + { + string source = """ + using System; + using WindowsRuntime.Xaml; + + [GeneratedCustomPropertyProvider(new string[] { "Name" }, new Type[] { })] + public partial class MyType + { + public string Name { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source); + } + + [TestMethod] + public async Task ValidIndexerType_DoesNotWarn() + { + string source = """ + using System; + using WindowsRuntime.Xaml; + + [GeneratedCustomPropertyProvider(new string[] { }, new Type[] { typeof(int) })] + public partial class MyType + { + public string this[int index] => ""; + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source); + } + + [TestMethod] + public async Task ValidPropertyNameAndIndexerType_DoesNotWarn() + { + string source = """ + using System; + using WindowsRuntime.Xaml; + + [GeneratedCustomPropertyProvider(new string[] { "Name" }, new Type[] { typeof(int) })] + public partial class MyType + { + public string Name { get; set; } + public string this[int index] => ""; + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source); + } + + [TestMethod] + public async Task InheritedPropertyName_DoesNotWarn() + { + string source = """ + using System; + using WindowsRuntime.Xaml; + + public class BaseType + { + public string Name { get; set; } + } + + [GeneratedCustomPropertyProvider(new string[] { "Name" }, new Type[] { })] + public partial class MyType : BaseType; + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source); + } + + [TestMethod] + public async Task InheritedIndexerType_DoesNotWarn() + { + string source = """ + using System; + using WindowsRuntime.Xaml; + + public class BaseType + { + public string this[int index] => ""; + } + + [GeneratedCustomPropertyProvider(new string[] { }, new Type[] { typeof(int) })] + public partial class MyType : BaseType; + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source); + } + + [TestMethod] + public async Task MultipleValidPropertyNames_DoesNotWarn() + { + string source = """ + using System; + using WindowsRuntime.Xaml; + + [GeneratedCustomPropertyProvider(new string[] { "Name", "Age" }, new Type[] { })] + public partial class MyType + { + public string Name { get; set; } + public int Age { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source); + } + + [TestMethod] + public async Task OverriddenPropertyName_DoesNotWarn() + { + string source = """ + using System; + using WindowsRuntime.Xaml; + + public class BaseType + { + public virtual string Name { get; set; } + } + + [GeneratedCustomPropertyProvider(new string[] { "Name" }, new Type[] { })] + public partial class MyType : BaseType + { + public override string Name { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source); + } + + [TestMethod] + public async Task NullPropertyName_Warns() + { + string source = """ + using System; + using WindowsRuntime.Xaml; + + [GeneratedCustomPropertyProvider(new string[] { null }, new Type[] { })] + public partial class {|CSWINRT2004:MyType|} + { + public string Name { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source); + } + + [TestMethod] + public async Task NullAmongValidPropertyNames_Warns() + { + string source = """ + using System; + using WindowsRuntime.Xaml; + + [GeneratedCustomPropertyProvider(new string[] { "Name", null }, new Type[] { })] + public partial class {|CSWINRT2004:MyType|} + { + public string Name { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source); + } + + [TestMethod] + public async Task NullIndexerType_Warns() + { + string source = """ + using System; + using WindowsRuntime.Xaml; + + [GeneratedCustomPropertyProvider(new string[] { }, new Type[] { null })] + public partial class {|CSWINRT2005:MyType|} + { + public string this[int index] => ""; + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source); + } + + [TestMethod] + public async Task NullAmongValidIndexerTypes_Warns() + { + string source = """ + using System; + using WindowsRuntime.Xaml; + + [GeneratedCustomPropertyProvider(new string[] { }, new Type[] { typeof(int), null })] + public partial class {|CSWINRT2005:MyType|} + { + public string this[int index] => ""; + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source); + } + + [TestMethod] + public async Task PropertyNameNotFound_Warns() + { + string source = """ + using System; + using WindowsRuntime.Xaml; + + [GeneratedCustomPropertyProvider(new string[] { "Missing" }, new Type[] { })] + public partial class {|CSWINRT2006:MyType|} + { + public string Name { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source); + } + + [TestMethod] + public async Task PropertyNameMatchesPrivateProperty_Warns() + { + string source = """ + using System; + using WindowsRuntime.Xaml; + + [GeneratedCustomPropertyProvider(new string[] { "Secret" }, new Type[] { })] + public partial class {|CSWINRT2006:MyType|} + { + private string Secret { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source); + } + + [TestMethod] + public async Task MultiplePropertyNamesNotFound_WarnsForEach() + { + string source = """ + using System; + using WindowsRuntime.Xaml; + + [GeneratedCustomPropertyProvider(new string[] { "Missing1", "Missing2" }, new Type[] { })] + public partial class MyType + { + public string Name { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, expectedDiagnostics: [ + VerifyCS.Diagnostic("CSWINRT2006").WithSpan(5, 22, 5, 28).WithArguments("Missing1", "MyType"), + VerifyCS.Diagnostic("CSWINRT2006").WithSpan(5, 22, 5, 28).WithArguments("Missing2", "MyType")]); + } + + [TestMethod] + public async Task IndexerTypeNotFound_WrongType_Warns() + { + string source = """ + using System; + using WindowsRuntime.Xaml; + + [GeneratedCustomPropertyProvider(new string[] { }, new Type[] { typeof(string) })] + public partial class {|CSWINRT2007:MyType|} + { + public string this[int index] => ""; + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source); + } + + [TestMethod] + public async Task IndexerTypeNotFound_NoIndexer_Warns() + { + string source = """ + using System; + using WindowsRuntime.Xaml; + + [GeneratedCustomPropertyProvider(new string[] { }, new Type[] { typeof(int) })] + public partial class {|CSWINRT2007:MyType|} + { + public string Name { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source); + } + + [TestMethod] + public async Task IndexerTypeMatchesMultiParameterIndexer_Warns() + { + string source = """ + using System; + using WindowsRuntime.Xaml; + + [GeneratedCustomPropertyProvider(new string[] { }, new Type[] { typeof(int) })] + public partial class {|CSWINRT2007:MyType|} + { + public string this[int row, int col] => ""; + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source); + } + + [TestMethod] + public async Task MultipleIndexerTypesNotFound_WarnsForEach() + { + string source = """ + using System; + using WindowsRuntime.Xaml; + + [GeneratedCustomPropertyProvider(new string[] { }, new Type[] { typeof(string), typeof(double) })] + public partial class MyType + { + public string this[int index] => ""; + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, expectedDiagnostics: [ + VerifyCS.Diagnostic("CSWINRT2007").WithSpan(5, 22, 5, 28).WithArguments("double", "MyType"), + VerifyCS.Diagnostic("CSWINRT2007").WithSpan(5, 22, 5, 28).WithArguments("string", "MyType")]); + } + + [TestMethod] + public async Task MixedInvalidArguments_WarnsForEach() + { + string source = """ + using System; + using WindowsRuntime.Xaml; + + [GeneratedCustomPropertyProvider(new string[] { "Missing", null }, new Type[] { typeof(string), null })] + public partial class MyType + { + public string Name { get; set; } + public string this[int index] => ""; + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, expectedDiagnostics: [ + VerifyCS.Diagnostic("CSWINRT2004").WithSpan(5, 22, 5, 28).WithArguments("MyType"), + VerifyCS.Diagnostic("CSWINRT2005").WithSpan(5, 22, 5, 28).WithArguments("MyType"), + VerifyCS.Diagnostic("CSWINRT2006").WithSpan(5, 22, 5, 28).WithArguments("Missing", "MyType"), + VerifyCS.Diagnostic("CSWINRT2007").WithSpan(5, 22, 5, 28).WithArguments("string", "MyType")]); + } +} From 69bc2de0723a49518d5343d9721ecb9271bfe49c Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 13 Mar 2026 15:25:13 -0700 Subject: [PATCH 088/100] Detect generated code on implemented members Make detection of generator-produced members more robust. Return false early when the interface type is null and collect implemented members to ensure at least one exists before treating them as "all generated". Enhance AreAllImplementedByGenerator to handle accessor methods by moving to the associated property and to walk parent symbols to find a [GeneratedCode] attribute on the closest containing symbol; also tighten the attribute pattern check and return false for empty symbol sets. These changes prevent false positives when members are inherited/implemented but not actually produced by the generator. --- ...derExistingMemberImplementationAnalyzer.cs | 12 +++++-- .../Extensions/ISymbolExtensions.cs | 32 ++++++++++++++++--- 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/GeneratedCustomPropertyProviderExistingMemberImplementationAnalyzer.cs b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/GeneratedCustomPropertyProviderExistingMemberImplementationAnalyzer.cs index 1ce46ad4e4..7b2cfaefc8 100644 --- a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/GeneratedCustomPropertyProviderExistingMemberImplementationAnalyzer.cs +++ b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/GeneratedCustomPropertyProviderExistingMemberImplementationAnalyzer.cs @@ -66,9 +66,15 @@ static bool HasNonGeneratedImplementedMembers( INamedTypeSymbol? interfaceType, INamedTypeSymbol generatedCodeAttributeType) { - return - interfaceType is not null && - !typeSymbol.EnumerateImplementedMembersForInterface(interfaceType).AreAllImplementedByGenerator(generatedCodeAttributeType, nameof(CustomPropertyProviderGenerator)); + if (interfaceType is null) + { + return false; + } + + ISymbol[] implementedMembers = [.. typeSymbol.EnumerateImplementedMembersForInterface(interfaceType)]; + + // Check that we have at least one implemented member, and that not all of them are produced by our generator + return implementedMembers.Length > 0 && !implementedMembers.AreAllImplementedByGenerator(generatedCodeAttributeType, nameof(CustomPropertyProviderGenerator)); } // Check whether the type has or inherits any 'ICustomPropertyProvider' member implementations. diff --git a/src/Authoring/WinRT.SourceGenerator2/Extensions/ISymbolExtensions.cs b/src/Authoring/WinRT.SourceGenerator2/Extensions/ISymbolExtensions.cs index 94df0494be..59441b65cb 100644 --- a/src/Authoring/WinRT.SourceGenerator2/Extensions/ISymbolExtensions.cs +++ b/src/Authoring/WinRT.SourceGenerator2/Extensions/ISymbolExtensions.cs @@ -90,22 +90,44 @@ public bool IsAccessibleFromCompilationAssembly(Compilation compilation) /// Whether all symbols in have a [GeneratedCode] attribute matching . public bool AreAllImplementedByGenerator(INamedTypeSymbol generatedCodeAttributeType, string generatorName) { + bool hasAtLeastOneImplementedMember = false; + foreach (ISymbol symbol in symbols) { - // Stop if the symbol doesn't have '[GeneratedCode]' on it - if (!symbol.TryGetAttributeWithType(generatedCodeAttributeType, out AttributeData? attributeData)) + hasAtLeastOneImplementedMember = true; + + ISymbol currentSymbol = symbol; + + // First, try to get the attribute directly on the symbol (this should work in most cases) + if (!currentSymbol.TryGetAttributeWithType(generatedCodeAttributeType, out AttributeData? attributeData)) { - return false; + // If the current symbol is some property accessor, move to the associated property instead. + // This is also needed because usually '[GeneratedCode]' would only be on the property. + if (currentSymbol is IMethodSymbol { AssociatedSymbol: { } associatedSymbol }) + { + currentSymbol = associatedSymbol; + } + + // Stop if the symbol doesn't have '[GeneratedCode]' on it. Note, we want to traverse the symbol + // hierarchy and find the attribute on the closest parent (so we can also handle e.g. accessors). + for (ISymbol? parentSymbol = currentSymbol; parentSymbol is not null; parentSymbol = parentSymbol.ContainingSymbol) + { + if (parentSymbol.TryGetAttributeWithType(generatedCodeAttributeType, out attributeData)) + { + break; + } + } } // Check that the symbol was specifically generated by the target generator - if (attributeData.ConstructorArguments is not [{ Value: string name }, ..] || name != generatorName) + if (attributeData is not { ConstructorArguments: [{ Value: string name }, ..] } || name != generatorName) { return false; } } - return true; + // If the input set is empty, we want to return 'false' to indicate that there are no implemented members + return hasAtLeastOneImplementedMember; } } } \ No newline at end of file From 86a3376c78906b3233827c92b05d90de0a651c62 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 13 Mar 2026 16:57:32 -0700 Subject: [PATCH 089/100] Default CsWinRTUseWindowsUIXamlProjections to false Add a conditional PropertyGroup in src/Tests/FunctionalTests/Directory.Build.props that sets CsWinRTUseWindowsUIXamlProjections to false when it is not already defined, preventing the feature switch below from being considered invalid. Also normalize whitespace in a commented CsWinRTKeepGeneratedSources line in src/Tests/AuthoringWuxTest/AuthoringWuxTest.csproj (formatting-only change). --- src/Tests/AuthoringWuxTest/AuthoringWuxTest.csproj | 2 +- src/Tests/FunctionalTests/Directory.Build.props | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Tests/AuthoringWuxTest/AuthoringWuxTest.csproj b/src/Tests/AuthoringWuxTest/AuthoringWuxTest.csproj index 22aab185ea..24e964724b 100644 --- a/src/Tests/AuthoringWuxTest/AuthoringWuxTest.csproj +++ b/src/Tests/AuthoringWuxTest/AuthoringWuxTest.csproj @@ -10,7 +10,7 @@ true - + diff --git a/src/Tests/FunctionalTests/Directory.Build.props b/src/Tests/FunctionalTests/Directory.Build.props index f215dc9e0c..bf8b4dc243 100644 --- a/src/Tests/FunctionalTests/Directory.Build.props +++ b/src/Tests/FunctionalTests/Directory.Build.props @@ -43,6 +43,11 @@ true + + + false + + From d87d7bae8fe181debf53072b71ae047de9598777 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Fri, 13 Mar 2026 19:46:34 -0700 Subject: [PATCH 090/100] Add SourceGenerator2Test to CI build script Add a :sourcegenerator2test block to build.cmd that runs the SourceGenerator2Test project via dotnet test, following the same pattern as the existing :sourcegeneratortest block. Includes platform parameter since the project requires x64/x86. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- build/AzurePipelineTemplates/CsWinRT-Test-Steps.yml | 12 ++++++++++++ src/build.cmd | 10 ++++++++++ 2 files changed, 22 insertions(+) diff --git a/build/AzurePipelineTemplates/CsWinRT-Test-Steps.yml b/build/AzurePipelineTemplates/CsWinRT-Test-Steps.yml index c89ba88e43..a9b2308f8a 100644 --- a/build/AzurePipelineTemplates/CsWinRT-Test-Steps.yml +++ b/build/AzurePipelineTemplates/CsWinRT-Test-Steps.yml @@ -22,6 +22,18 @@ steps: --no-build testRunTitle: Unit Tests +# Run Source Generator 2 Tests + - task: DotNetCoreCLI@2 + displayName: Run Source Generator 2 Tests + condition: and(succeeded(), or(eq(variables['BuildPlatform'], 'x86'), eq(variables['BuildPlatform'], 'x64'))) + inputs: + command: test + projects: 'src/Tests/SourceGenerator2Test/SourceGenerator2Test.csproj' + arguments: > + /p:platform=$(BuildPlatform);configuration=$(BuildConfiguration) + --no-build + testRunTitle: Source Generator 2 Tests + # Run Host Tests - task: CmdLine@2 displayName: Run Host Tests diff --git a/src/build.cmd b/src/build.cmd index c151eeb0a7..404bb60f87 100644 --- a/src/build.cmd +++ b/src/build.cmd @@ -189,6 +189,16 @@ if ErrorLevel 1 ( exit /b !ErrorLevel! ) +:sourcegenerator2test +rem Running Source Generator 2 Unit Tests +echo Running source generator 2 tests for %cswinrt_platform% %cswinrt_configuration% +call :exec %dotnet_exe% test --verbosity normal --no-build --logger trx;LogFilePath=%~dp0sourcegenerator2test_%cswinrt_version_string%.trx %this_dir%Tests\SourceGenerator2Test\SourceGenerator2Test.csproj /nologo /m /p:platform=%cswinrt_platform%;configuration=%cswinrt_configuration% -- RunConfiguration.TreatNoTestsAsError=true +if ErrorLevel 1 ( + echo. + echo ERROR: Source generator 2 unit test failed, skipping NuGet pack + exit /b !ErrorLevel! +) + :hosttest rem Run WinRT.Host tests echo Running cswinrt host tests for %cswinrt_platform% %cswinrt_configuration% From 571f8649276066f739fecd9915d1bde870dfba04 Mon Sep 17 00:00:00 2001 From: Manodasan Wignarajah Date: Fri, 13 Mar 2026 21:10:45 -0700 Subject: [PATCH 091/100] Fix nuget feed issue in test --- .../Extensions/ReferenceAssembliesExtensions.cs | 15 +++++++++++---- .../SourceGenerator2Test.csproj | 7 +++++++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/src/Tests/SourceGenerator2Test/Extensions/ReferenceAssembliesExtensions.cs b/src/Tests/SourceGenerator2Test/Extensions/ReferenceAssembliesExtensions.cs index 79836a3adf..43ef97bfbb 100644 --- a/src/Tests/SourceGenerator2Test/Extensions/ReferenceAssembliesExtensions.cs +++ b/src/Tests/SourceGenerator2Test/Extensions/ReferenceAssembliesExtensions.cs @@ -15,10 +15,17 @@ 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"))); + private static readonly Lazy Net100 = new(static () => + { + // Given we use a different nuget feed, we pass nuget.config. + string nugetConfigFilePath = Path.Combine(Path.GetDirectoryName(typeof(ReferenceAssembliesExtensions).Assembly.Location), "nuget.config"); + + ReferenceAssemblies referenceAssembly = new( + targetFramework: "net10.0", + referenceAssemblyPackage: new PackageIdentity("Microsoft.NETCore.App.Ref", "10.0.1"), + referenceAssemblyPath: Path.Combine("ref", "net10.0")); + return referenceAssembly.WithNuGetConfigFilePath(nugetConfigFilePath); + }); extension(ReferenceAssemblies.Net) { diff --git a/src/Tests/SourceGenerator2Test/SourceGenerator2Test.csproj b/src/Tests/SourceGenerator2Test/SourceGenerator2Test.csproj index 0070a86912..48c194ae68 100644 --- a/src/Tests/SourceGenerator2Test/SourceGenerator2Test.csproj +++ b/src/Tests/SourceGenerator2Test/SourceGenerator2Test.csproj @@ -45,4 +45,11 @@ + + + + Always + + + From e29f1a5b67b6188bb9d28855989859cf8a0c1339 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sat, 14 Mar 2026 14:52:02 -0700 Subject: [PATCH 092/100] Set LF in SourceGenerator2 .editorconfig Update WinRT.SourceGenerator2/.editorconfig to use LF line endings (copy of root file with CRLF replaced by LF) so generated sources use consistent LF endings (helps with multiline string literals and formatting). Remove redundant .editorconfig from Tests/SourceGenerator2Test. --- .../WinRT.SourceGenerator2/.editorconfig | 5 +- src/Tests/SourceGenerator2Test/.editorconfig | 250 ------------------ 2 files changed, 4 insertions(+), 251 deletions(-) delete mode 100644 src/Tests/SourceGenerator2Test/.editorconfig diff --git a/src/Authoring/WinRT.SourceGenerator2/.editorconfig b/src/Authoring/WinRT.SourceGenerator2/.editorconfig index 6881bc9577..c14a34e2cf 100644 --- a/src/Authoring/WinRT.SourceGenerator2/.editorconfig +++ b/src/Authoring/WinRT.SourceGenerator2/.editorconfig @@ -1,4 +1,7 @@ -# Remove the line below if you want to inherit .editorconfig settings from higher directories +# This '.editorconfig' file is a copy of the root one, except that we replace the 'CRLF' line endings +# with 'LF'. This is because we want all generated sources to use 'LF', as that simplifies handling +# indentations and formatting when writing text. We need this change because we often use multiline +# string literal, which are affected by the actual line endings being used in the source files. root = true # All files diff --git a/src/Tests/SourceGenerator2Test/.editorconfig b/src/Tests/SourceGenerator2Test/.editorconfig deleted file mode 100644 index 6881bc9577..0000000000 --- a/src/Tests/SourceGenerator2Test/.editorconfig +++ /dev/null @@ -1,250 +0,0 @@ -# Remove the line below if you want to inherit .editorconfig settings from higher directories -root = true - -# All files -[*] - -#### Core EditorConfig Options #### - -# Encoding -charset = utf-8 - -# Indentation and spacing -tab_width = 4 -indent_size = 4 -indent_style = space - -# New line preferences -end_of_line = lf -insert_final_newline = false -trim_trailing_whitespace = true - -# Misc settings -max_line_length=160 -file_header_template = Copyright (c) Microsoft Corporation.\r\nLicensed under the MIT License. - -# C# files -[*.cs] -csharp_keep_user_linebreaks = true -wrap_before_comma = false -wrap_parameters_style = chop_if_long -max_formal_parameters_on_line = 4 -wrap_after_dot_in_method_calls = false -csharp_preserve_single_line_blocks = false -csharp_new_line_between_query_expression_clauses = true -csharp_new_line_before_members_in_anonymous_types = true -csharp_new_line_before_open_brace = all - -#### Core EditorConfig Options #### - -# Indentation and spacing -indent_size = 4 -indent_style = space -tab_width = 4 - -# New line preferences -end_of_line = crlf -insert_final_newline = false - -#### .NET Coding Conventions #### - -# Organize usings -dotnet_separate_import_directive_groups = false -dotnet_sort_system_directives_first = true - -# this. and Me. preferences -dotnet_style_qualification_for_event = false -dotnet_style_qualification_for_field = false -dotnet_style_qualification_for_method = false -dotnet_style_qualification_for_property = false - -# These two diagnostics are just 'Info' by default -dotnet_diagnostic.IDE0003.severity = warning -dotnet_diagnostic.IDE0009.severity = warning - -# Language keywords vs BCL types preferences -dotnet_style_predefined_type_for_locals_parameters_members = true -dotnet_style_predefined_type_for_member_access = true - -# Parentheses preferences -dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity -dotnet_style_parentheses_in_other_binary_operators = always_for_clarity -dotnet_style_parentheses_in_other_operators = never_if_unnecessary -dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity - -# Modifier preferences -dotnet_style_require_accessibility_modifiers = for_non_interface_members - -# Expression-level preferences -dotnet_style_coalesce_expression = true -dotnet_style_collection_initializer = true -dotnet_style_explicit_tuple_names = true -dotnet_style_null_propagation = true -dotnet_style_object_initializer = true -dotnet_style_operator_placement_when_wrapping = beginning_of_line -dotnet_style_prefer_auto_properties = true -dotnet_style_prefer_compound_assignment = true -dotnet_style_prefer_conditional_expression_over_assignment = true -dotnet_style_prefer_conditional_expression_over_return = true -dotnet_style_prefer_inferred_anonymous_type_member_names = true -dotnet_style_prefer_inferred_tuple_names = true -dotnet_style_prefer_is_null_check_over_reference_equality_method = true -dotnet_style_prefer_simplified_boolean_expressions = true -dotnet_style_prefer_simplified_interpolation = true - -# Field preferences -dotnet_style_readonly_field = true - -# Namespace preferences -dotnet_style_namespace_match_folder = false - -# Primary constructor preference -csharp_style_prefer_primary_constructors = false; - -# Parameter preferences -dotnet_code_quality_unused_parameters = all - -# Suppression preferences -dotnet_remove_unnecessary_suppression_exclusions = none - -#### C# Coding Conventions #### - -# var preferences -csharp_style_var_elsewhere = false -csharp_style_var_for_built_in_types = false -csharp_style_var_when_type_is_apparent = false - -# Expression-bodied members -csharp_style_expression_bodied_accessors = when_on_single_line -csharp_style_expression_bodied_constructors = false -csharp_style_expression_bodied_indexers = when_on_single_line -csharp_style_expression_bodied_lambdas = when_on_single_line -csharp_style_expression_bodied_local_functions = when_on_single_line -csharp_style_expression_bodied_methods = false -csharp_style_expression_bodied_operators = when_on_single_line -csharp_style_expression_bodied_properties = when_on_single_line - -# Pattern matching preferences -csharp_style_pattern_matching_over_as_with_null_check = true -csharp_style_pattern_matching_over_is_with_cast_check = true -csharp_style_prefer_not_pattern = true -csharp_style_prefer_pattern_matching = true -csharp_style_prefer_switch_expression = true - -# Null-checking preferences -csharp_style_conditional_delegate_call = true - -# Modifier preferences -csharp_prefer_static_local_function = true -csharp_preferred_modifier_order = public, private, protected, internal, file, static, extern, new, virtual, abstract, sealed, override, readonly, unsafe, required, volatile, async - -# Code-block preferences -csharp_prefer_braces = true -csharp_prefer_simple_using_statement = true - -# Expression-level preferences -csharp_prefer_simple_default_expression = true -csharp_style_deconstructed_variable_declaration = true -csharp_style_implicit_object_creation_when_type_is_apparent = true -csharp_style_inlined_variable_declaration = true -csharp_style_pattern_local_over_anonymous_function = true -csharp_style_prefer_index_operator = true -csharp_style_prefer_range_operator = true -csharp_style_throw_expression = true -csharp_style_unused_value_assignment_preference = discard_variable -csharp_style_unused_value_expression_statement_preference = discard_variable - -# 'using' directive preferences -csharp_using_directive_placement = outside_namespace - -#### C# Formatting Rules #### - -# New line preferences -csharp_new_line_before_catch = true -csharp_new_line_before_else = true -csharp_new_line_before_finally = true -csharp_new_line_before_members_in_anonymous_types = true -csharp_new_line_before_members_in_object_initializers = true -csharp_new_line_before_open_brace = all -csharp_new_line_between_query_expression_clauses = true - -# Indentation preferences -csharp_indent_block_contents = true -csharp_indent_braces = false -csharp_indent_case_contents = true -csharp_indent_case_contents_when_block = true -csharp_indent_labels = one_less_than_current -csharp_indent_switch_labels = true - -# Space preferences -csharp_space_after_cast = false -csharp_space_after_colon_in_inheritance_clause = true -csharp_space_after_comma = true -csharp_space_after_dot = false -csharp_space_after_keywords_in_control_flow_statements = true -csharp_space_after_semicolon_in_for_statement = true -csharp_space_around_binary_operators = before_and_after -csharp_space_around_declaration_statements = false -csharp_space_before_colon_in_inheritance_clause = true -csharp_space_before_comma = false -csharp_space_before_dot = false -csharp_space_before_open_square_brackets = false -csharp_space_before_semicolon_in_for_statement = false -csharp_space_between_empty_square_brackets = false -csharp_space_between_method_call_empty_parameter_list_parentheses = false -csharp_space_between_method_call_name_and_opening_parenthesis = false -csharp_space_between_method_call_parameter_list_parentheses = false -csharp_space_between_method_declaration_empty_parameter_list_parentheses = false -csharp_space_between_method_declaration_name_and_open_parenthesis = false -csharp_space_between_method_declaration_parameter_list_parentheses = false -csharp_space_between_parentheses = false -csharp_space_between_square_brackets = false - -# Wrapping preferences -csharp_preserve_single_line_blocks = true -csharp_preserve_single_line_statements = true - -# Namespace declaration preferences -csharp_style_namespace_declarations = file_scoped:warning - -#### Naming styles #### - -# Naming rules - -dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion -dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface -dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i - -dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion -dotnet_naming_rule.types_should_be_pascal_case.symbols = types -dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case - -dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion -dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members -dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case - -# Symbol specifications - -dotnet_naming_symbols.interface.applicable_kinds = interface -dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.interface.required_modifiers = - -dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum -dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.types.required_modifiers = - -dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method -dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.non_field_members.required_modifiers = - -# Naming styles - -dotnet_naming_style.pascal_case.required_prefix = -dotnet_naming_style.pascal_case.required_suffix = -dotnet_naming_style.pascal_case.word_separator = -dotnet_naming_style.pascal_case.capitalization = pascal_case - -dotnet_naming_style.begins_with_i.required_prefix = I -dotnet_naming_style.begins_with_i.required_suffix = -dotnet_naming_style.begins_with_i.word_separator = -dotnet_naming_style.begins_with_i.capitalization = pascal_case \ No newline at end of file From 2d8ae33731a2cbc08a6be91094f2db9d777ccda6 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sat, 14 Mar 2026 14:53:45 -0700 Subject: [PATCH 093/100] Add analyzer option for Windows UI Xaml projections Add GetCsWinRTUseWindowsUIXamlProjections extension to AnalyzerConfigOptionsExtensions and update CustomPropertyProviderGenerator to use it instead of calling GetBooleanProperty directly. This centralizes retrieval of the "CsWinRTUseWindowsUIXamlProjections" analyzer/MSBuild option and keeps option access consistent across the codebase. --- .../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 b42e339628..1063c06976 100644 --- a/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Execute.cs +++ b/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Execute.cs @@ -61,7 +61,7 @@ public static bool IsTargetNodeValid(SyntaxNode node, CancellationToken token) /// The resulting instance, if processed successfully. public static CustomPropertyProviderInfo? GetCustomPropertyProviderInfo(GeneratorAttributeSyntaxContextWithOptions context, CancellationToken token) { - bool useWindowsUIXamlProjections = context.GlobalOptions.GetBooleanProperty("CsWinRTUseWindowsUIXamlProjections"); + bool useWindowsUIXamlProjections = context.GlobalOptions.GetCsWinRTUseWindowsUIXamlProjections(); token.ThrowIfCancellationRequested(); From b9a7a6467c3f88242e7066e4d83dd64a17391c79 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sat, 14 Mar 2026 14:54:19 -0700 Subject: [PATCH 094/100] Use Microsoft.WindowsAppSDK.WinUI in diagnostic Update DiagnosticDescriptors.cs message to suggest referencing 'Microsoft.WindowsAppSDK.WinUI' instead of 'WindowsAppSDK.WinUI', so the diagnostic points users to the correct package/namespace to resolve the missing ICustomPropertyProvider interface. --- .../WinRT.SourceGenerator2/Diagnostics/DiagnosticDescriptors.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/DiagnosticDescriptors.cs b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/DiagnosticDescriptors.cs index 99c315ef29..a5f73dd6ad 100644 --- a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/DiagnosticDescriptors.cs +++ b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/DiagnosticDescriptors.cs @@ -42,7 +42,7 @@ internal static partial class DiagnosticDescriptors 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)""", + messageFormat: """The 'ICustomPropertyProvider' interface is not available in the compilation, but it is required to use '[GeneratedCustomPropertyProvider]' (make sure to either reference 'Microsoft.WindowsAppSDK.WinUI' or set the 'UseUwp' property in your .csproj file)""", category: "WindowsRuntime.SourceGenerator", defaultSeverity: DiagnosticSeverity.Error, isEnabledByDefault: true, From 8e83b93bb2c56a7cbebb536a1d4e0acc4046a56a Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sat, 14 Mar 2026 14:55:57 -0700 Subject: [PATCH 095/100] Normalize expected text line endings in tests Update CSharpGeneratorTest to normalize expected text line endings by replacing CRLF with LF when constructing expectedText. Added a comment explaining the change so test files won't fail due to platform line ending differences; assembly version replacement remains unchanged. --- .../Helpers/CSharpGeneratorTest{TGenerator}.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Tests/SourceGenerator2Test/Helpers/CSharpGeneratorTest{TGenerator}.cs b/src/Tests/SourceGenerator2Test/Helpers/CSharpGeneratorTest{TGenerator}.cs index 93affa50d9..15a75a1d0d 100644 --- a/src/Tests/SourceGenerator2Test/Helpers/CSharpGeneratorTest{TGenerator}.cs +++ b/src/Tests/SourceGenerator2Test/Helpers/CSharpGeneratorTest{TGenerator}.cs @@ -35,7 +35,8 @@ public static void VerifySources(string source, (string Filename, string Source) // 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}\""); + // Also normalize line endings to 'LF', so the test files don't have to worry about that. + string expectedText = result.Source.Replace("", $"\"{typeof(TGenerator).Assembly.GetName().Version}\"").Replace("\r\n", "\n"); string actualText = compilation.SyntaxTrees.Single(tree => Path.GetFileName(tree.FilePath) == result.Filename).ToString(); Assert.AreEqual(expectedText, actualText); From 45cea4e34bee2f9a1b3d1168baf20a32800a54ef Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sat, 14 Mar 2026 17:39:58 -0700 Subject: [PATCH 096/100] Normalize generated LF newlines Remove the local .editorconfig and normalize generated source line endings to LF. Trim trailing '\r' when splitting lines in IndentedTextWriter so emitted text uses LF regardless of repo CRLF. Mark multi-line string writes in source generators (AuthoringExportTypesGenerator, TypeMapAssemblyTargetGenerator) with isMultiline: true to preserve raw multiline literal formatting and avoid extra indentation/whitespace in generated outputs. --- .../WinRT.SourceGenerator2/.editorconfig | 253 ------------------ .../AuthoringExportTypesGenerator.Execute.cs | 22 +- .../Helpers/IndentedTextWriter.cs | 8 + .../TypeMapAssemblyTargetGenerator.Execute.cs | 4 +- 4 files changed, 21 insertions(+), 266 deletions(-) delete mode 100644 src/Authoring/WinRT.SourceGenerator2/.editorconfig diff --git a/src/Authoring/WinRT.SourceGenerator2/.editorconfig b/src/Authoring/WinRT.SourceGenerator2/.editorconfig deleted file mode 100644 index c14a34e2cf..0000000000 --- a/src/Authoring/WinRT.SourceGenerator2/.editorconfig +++ /dev/null @@ -1,253 +0,0 @@ -# This '.editorconfig' file is a copy of the root one, except that we replace the 'CRLF' line endings -# with 'LF'. This is because we want all generated sources to use 'LF', as that simplifies handling -# indentations and formatting when writing text. We need this change because we often use multiline -# string literal, which are affected by the actual line endings being used in the source files. -root = true - -# All files -[*] - -#### Core EditorConfig Options #### - -# Encoding -charset = utf-8 - -# Indentation and spacing -tab_width = 4 -indent_size = 4 -indent_style = space - -# New line preferences -end_of_line = lf -insert_final_newline = false -trim_trailing_whitespace = true - -# Misc settings -max_line_length=160 -file_header_template = Copyright (c) Microsoft Corporation.\r\nLicensed under the MIT License. - -# C# files -[*.cs] -csharp_keep_user_linebreaks = true -wrap_before_comma = false -wrap_parameters_style = chop_if_long -max_formal_parameters_on_line = 4 -wrap_after_dot_in_method_calls = false -csharp_preserve_single_line_blocks = false -csharp_new_line_between_query_expression_clauses = true -csharp_new_line_before_members_in_anonymous_types = true -csharp_new_line_before_open_brace = all - -#### Core EditorConfig Options #### - -# Indentation and spacing -indent_size = 4 -indent_style = space -tab_width = 4 - -# New line preferences -end_of_line = crlf -insert_final_newline = false - -#### .NET Coding Conventions #### - -# Organize usings -dotnet_separate_import_directive_groups = false -dotnet_sort_system_directives_first = true - -# this. and Me. preferences -dotnet_style_qualification_for_event = false -dotnet_style_qualification_for_field = false -dotnet_style_qualification_for_method = false -dotnet_style_qualification_for_property = false - -# These two diagnostics are just 'Info' by default -dotnet_diagnostic.IDE0003.severity = warning -dotnet_diagnostic.IDE0009.severity = warning - -# Language keywords vs BCL types preferences -dotnet_style_predefined_type_for_locals_parameters_members = true -dotnet_style_predefined_type_for_member_access = true - -# Parentheses preferences -dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity -dotnet_style_parentheses_in_other_binary_operators = always_for_clarity -dotnet_style_parentheses_in_other_operators = never_if_unnecessary -dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity - -# Modifier preferences -dotnet_style_require_accessibility_modifiers = for_non_interface_members - -# Expression-level preferences -dotnet_style_coalesce_expression = true -dotnet_style_collection_initializer = true -dotnet_style_explicit_tuple_names = true -dotnet_style_null_propagation = true -dotnet_style_object_initializer = true -dotnet_style_operator_placement_when_wrapping = beginning_of_line -dotnet_style_prefer_auto_properties = true -dotnet_style_prefer_compound_assignment = true -dotnet_style_prefer_conditional_expression_over_assignment = true -dotnet_style_prefer_conditional_expression_over_return = true -dotnet_style_prefer_inferred_anonymous_type_member_names = true -dotnet_style_prefer_inferred_tuple_names = true -dotnet_style_prefer_is_null_check_over_reference_equality_method = true -dotnet_style_prefer_simplified_boolean_expressions = true -dotnet_style_prefer_simplified_interpolation = true - -# Field preferences -dotnet_style_readonly_field = true - -# Namespace preferences -dotnet_style_namespace_match_folder = false - -# Primary constructor preference -csharp_style_prefer_primary_constructors = false; - -# Parameter preferences -dotnet_code_quality_unused_parameters = all - -# Suppression preferences -dotnet_remove_unnecessary_suppression_exclusions = none - -#### C# Coding Conventions #### - -# var preferences -csharp_style_var_elsewhere = false -csharp_style_var_for_built_in_types = false -csharp_style_var_when_type_is_apparent = false - -# Expression-bodied members -csharp_style_expression_bodied_accessors = when_on_single_line -csharp_style_expression_bodied_constructors = false -csharp_style_expression_bodied_indexers = when_on_single_line -csharp_style_expression_bodied_lambdas = when_on_single_line -csharp_style_expression_bodied_local_functions = when_on_single_line -csharp_style_expression_bodied_methods = false -csharp_style_expression_bodied_operators = when_on_single_line -csharp_style_expression_bodied_properties = when_on_single_line - -# Pattern matching preferences -csharp_style_pattern_matching_over_as_with_null_check = true -csharp_style_pattern_matching_over_is_with_cast_check = true -csharp_style_prefer_not_pattern = true -csharp_style_prefer_pattern_matching = true -csharp_style_prefer_switch_expression = true - -# Null-checking preferences -csharp_style_conditional_delegate_call = true - -# Modifier preferences -csharp_prefer_static_local_function = true -csharp_preferred_modifier_order = public, private, protected, internal, file, static, extern, new, virtual, abstract, sealed, override, readonly, unsafe, required, volatile, async - -# Code-block preferences -csharp_prefer_braces = true -csharp_prefer_simple_using_statement = true - -# Expression-level preferences -csharp_prefer_simple_default_expression = true -csharp_style_deconstructed_variable_declaration = true -csharp_style_implicit_object_creation_when_type_is_apparent = true -csharp_style_inlined_variable_declaration = true -csharp_style_pattern_local_over_anonymous_function = true -csharp_style_prefer_index_operator = true -csharp_style_prefer_range_operator = true -csharp_style_throw_expression = true -csharp_style_unused_value_assignment_preference = discard_variable -csharp_style_unused_value_expression_statement_preference = discard_variable - -# 'using' directive preferences -csharp_using_directive_placement = outside_namespace - -#### C# Formatting Rules #### - -# New line preferences -csharp_new_line_before_catch = true -csharp_new_line_before_else = true -csharp_new_line_before_finally = true -csharp_new_line_before_members_in_anonymous_types = true -csharp_new_line_before_members_in_object_initializers = true -csharp_new_line_before_open_brace = all -csharp_new_line_between_query_expression_clauses = true - -# Indentation preferences -csharp_indent_block_contents = true -csharp_indent_braces = false -csharp_indent_case_contents = true -csharp_indent_case_contents_when_block = true -csharp_indent_labels = one_less_than_current -csharp_indent_switch_labels = true - -# Space preferences -csharp_space_after_cast = false -csharp_space_after_colon_in_inheritance_clause = true -csharp_space_after_comma = true -csharp_space_after_dot = false -csharp_space_after_keywords_in_control_flow_statements = true -csharp_space_after_semicolon_in_for_statement = true -csharp_space_around_binary_operators = before_and_after -csharp_space_around_declaration_statements = false -csharp_space_before_colon_in_inheritance_clause = true -csharp_space_before_comma = false -csharp_space_before_dot = false -csharp_space_before_open_square_brackets = false -csharp_space_before_semicolon_in_for_statement = false -csharp_space_between_empty_square_brackets = false -csharp_space_between_method_call_empty_parameter_list_parentheses = false -csharp_space_between_method_call_name_and_opening_parenthesis = false -csharp_space_between_method_call_parameter_list_parentheses = false -csharp_space_between_method_declaration_empty_parameter_list_parentheses = false -csharp_space_between_method_declaration_name_and_open_parenthesis = false -csharp_space_between_method_declaration_parameter_list_parentheses = false -csharp_space_between_parentheses = false -csharp_space_between_square_brackets = false - -# Wrapping preferences -csharp_preserve_single_line_blocks = true -csharp_preserve_single_line_statements = true - -# Namespace declaration preferences -csharp_style_namespace_declarations = file_scoped:warning - -#### Naming styles #### - -# Naming rules - -dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion -dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface -dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i - -dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion -dotnet_naming_rule.types_should_be_pascal_case.symbols = types -dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case - -dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion -dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members -dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case - -# Symbol specifications - -dotnet_naming_symbols.interface.applicable_kinds = interface -dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.interface.required_modifiers = - -dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum -dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.types.required_modifiers = - -dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method -dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected -dotnet_naming_symbols.non_field_members.required_modifiers = - -# Naming styles - -dotnet_naming_style.pascal_case.required_prefix = -dotnet_naming_style.pascal_case.required_suffix = -dotnet_naming_style.pascal_case.word_separator = -dotnet_naming_style.pascal_case.capitalization = pascal_case - -dotnet_naming_style.begins_with_i.required_prefix = I -dotnet_naming_style.begins_with_i.required_suffix = -dotnet_naming_style.begins_with_i.word_separator = -dotnet_naming_style.begins_with_i.capitalization = pascal_case \ No newline at end of file diff --git a/src/Authoring/WinRT.SourceGenerator2/AuthoringExportTypesGenerator.Execute.cs b/src/Authoring/WinRT.SourceGenerator2/AuthoringExportTypesGenerator.Execute.cs index 7649102f7d..a2b215bf52 100644 --- a/src/Authoring/WinRT.SourceGenerator2/AuthoringExportTypesGenerator.Execute.cs +++ b/src/Authoring/WinRT.SourceGenerator2/AuthoringExportTypesGenerator.Execute.cs @@ -134,14 +134,14 @@ namespace ABI.{{info.AssemblyName.EscapeIdentifierName()}}; /// /// Contains the managed exports for activating types from the current project in an authoring scenario. /// - """); + """, isMultiline: true); // Emit the standard generated attributes, and also mark the type as hidden, since it's generated as a public type writer.WriteGeneratedAttributes(nameof(AuthoringExportTypesGenerator), useFullyQualifiedTypeNames: false); writer.WriteLine($$""" [EditorBrowsable(EditorBrowsableState.Never)] public static unsafe class ManagedExports - """); + """, isMultiline: true); // Indent via a block, as we'll also need to emit custom logic for the activation factory using (writer.WriteBlock()) @@ -155,7 +155,7 @@ public static unsafe class ManagedExports /// The class identifier that is associated with an activatable runtime class. /// The resulting pointer to the activation factory that corresponds with the class specified by . public static void* GetActivationFactory(ReadOnlySpan activatableClassId) - """); + """, isMultiline: true); using (writer.WriteBlock()) { @@ -167,7 +167,7 @@ public static unsafe class ManagedExports static extern void* AuthoringGetActivationFactory( [UnsafeAccessorType("ABI.{info.AssemblyName.EscapeIdentifierName()}.ManagedExports, WinRT.Authoring")] object? _, ReadOnlySpan activatableClassId); - """); + """, isMultiline: true); } // Emit the specialized code to redirect the activation @@ -181,7 +181,7 @@ public static unsafe class ManagedExports void* activationFactory = AuthoringGetActivationFactory(null, activatableClassId); return activationFactory ?? ReferencedManagedExports.GetActivationFactory(activatableClassId); - """); + """, isMultiline: true); } else if (info.Options.MergeReferencedActivationFactories) { @@ -198,7 +198,7 @@ public static nint GetActivationFactory(string activatableClassId) { return (nint)GetActivationFactory(activatableClassId.AsSpan()); } - """); + """, isMultiline: true); } // Emit a helper type with the logic for merging activaton factories, if needed @@ -209,7 +209,7 @@ public static nint GetActivationFactory(string activatableClassId) /// /// Contains the logic for activating types from transitively referenced Windows Runtime components. /// - """); + """, isMultiline: true); writer.WriteGeneratedAttributes(nameof(AuthoringExportTypesGenerator), useFullyQualifiedTypeNames: false); writer.WriteLine("file static unsafe class ReferencedManagedExports"); @@ -222,7 +222,7 @@ public static nint GetActivationFactory(string activatableClassId) /// The class identifier that is associated with an activatable runtime class. /// The resulting pointer to the activation factory that corresponds with the class specified by . public static void* GetActivationFactory(ReadOnlySpan activatableClassId) - """); + """, isMultiline: true); using (writer.WriteBlock()) { @@ -239,7 +239,7 @@ public static nint GetActivationFactory(string activatableClassId) { return activationFactory; } - """); + """, isMultiline: true); } // No match across the referenced factories, we can't do anything else @@ -290,7 +290,7 @@ namespace ABI.{info.AssemblyName.EscapeIdentifierName()}; /// /// Contains the native exports for activating types from the current project in an authoring scenario. /// - """); + """, isMultiline: true); // Emit the attributes to mark the code as generated, and to exclude it from code coverage as well. We also use a // file-scoped P/Invoke, so we don't take a dependency on private implementation detail types from 'WinRT.Runtime.dll'. @@ -383,7 +383,7 @@ file static class WindowsRuntimeImports [SupportedOSPlatform("windows6.2")] public static extern char* WindowsGetStringRawBuffer(HSTRING @string, uint* length); } - """); + """, isMultiline: true); context.AddSource("NativeExports.g.cs", writer.ToStringAndClear()); } diff --git a/src/Authoring/WinRT.SourceGenerator2/Helpers/IndentedTextWriter.cs b/src/Authoring/WinRT.SourceGenerator2/Helpers/IndentedTextWriter.cs index 66a5610369..840dd22cde 100644 --- a/src/Authoring/WinRT.SourceGenerator2/Helpers/IndentedTextWriter.cs +++ b/src/Authoring/WinRT.SourceGenerator2/Helpers/IndentedTextWriter.cs @@ -143,6 +143,14 @@ public void Write(scoped ReadOnlySpan content, bool isMultiline = false) { ReadOnlySpan line = content[..newLineIndex]; + // Remove the trailing 'CR' character, if present. This ensures that the resulting + // text will be correctly normalized with 'LF' newlines, regardless of the line + // endings used in source files. In fact, this whole repo uses 'CRLF' line endings. + if (line is [.. var trim, '\r']) + { + line = trim; + } + // Write the current line (if it's empty, we can skip writing the text entirely). // This ensures that raw multiline string literals with blank lines don't have // extra whitespace at the start of those lines, which would otherwise happen. diff --git a/src/Authoring/WinRT.SourceGenerator2/TypeMapAssemblyTargetGenerator.Execute.cs b/src/Authoring/WinRT.SourceGenerator2/TypeMapAssemblyTargetGenerator.Execute.cs index 0dd8fb87af..c93be3bf56 100644 --- a/src/Authoring/WinRT.SourceGenerator2/TypeMapAssemblyTargetGenerator.Execute.cs +++ b/src/Authoring/WinRT.SourceGenerator2/TypeMapAssemblyTargetGenerator.Execute.cs @@ -102,7 +102,7 @@ public static void EmitPrivateProjectionsTypeMapAssemblyTargetAttributes(SourceP builder.Write($""" // #pragma warning disable - """); + """, isMultiline: true); // Add a '[TypeMapAssemblyTarget]' entry for each assembly foreach (string assemblyName in assemblyNames) @@ -112,7 +112,7 @@ public static void EmitPrivateProjectionsTypeMapAssemblyTargetAttributes(SourceP [assembly: global::System.Runtime.InteropServices.TypeMapAssemblyTarget("{assemblyName}")] [assembly: global::System.Runtime.InteropServices.TypeMapAssemblyTarget("{assemblyName}")] [assembly: global::System.Runtime.InteropServices.TypeMapAssemblyTarget("{assemblyName}")] - """); + """, isMultiline: true); } context.AddSource("TypeMapAssemblyTarget.PrivateProjections.g.cs", builder.ToStringAndClear()); From 56fc294f33c884941bec4c6701290ba8440d63ae Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sat, 14 Mar 2026 22:15:17 -0700 Subject: [PATCH 097/100] SetValue now throws NotSupportedException Replace direct assignments to global::MyNamespace.MyClass.Name with throwing NotSupportedException in the generated SetValue implementations (two occurrences). This prevents the generated property provider from attempting to set the property value. --- .../Test_CustomPropertyProviderGenerator.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Tests/SourceGenerator2Test/Test_CustomPropertyProviderGenerator.cs b/src/Tests/SourceGenerator2Test/Test_CustomPropertyProviderGenerator.cs index a0416ac837..8a5aeadca8 100644 --- a/src/Tests/SourceGenerator2Test/Test_CustomPropertyProviderGenerator.cs +++ b/src/Tests/SourceGenerator2Test/Test_CustomPropertyProviderGenerator.cs @@ -120,7 +120,7 @@ public object GetValue(object target) /// public void SetValue(object target, object value) { - ((global::MyNamespace.MyClass)target).Name = (string)value; + throw new NotSupportedException(); } /// @@ -395,7 +395,7 @@ public object GetValue(object target) /// public void SetValue(object target, object value) { - ((global::MyNamespace.MyClass)target).Name = (string)value; + throw new NotSupportedException(); } /// From 11e0dd51c5d0df481e9082a4ce61e091af9e84b5 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Sun, 15 Mar 2026 23:20:08 -0700 Subject: [PATCH 098/100] Remove ContentPropertyAttribute include Delete the temporary -include for Windows.UI.Xaml.Markup.ContentPropertyAttribute from src/Projections/WinAppSDK/WinAppSDK.csproj. This removes an unnecessary/manual projection include now that the corresponding types are provided elsewhere, cleaning up the project file. --- src/Projections/WinAppSDK/WinAppSDK.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Projections/WinAppSDK/WinAppSDK.csproj b/src/Projections/WinAppSDK/WinAppSDK.csproj index 44d39464f9..1cce009a97 100644 --- a/src/Projections/WinAppSDK/WinAppSDK.csproj +++ b/src/Projections/WinAppSDK/WinAppSDK.csproj @@ -23,7 +23,6 @@ # Microsoft.* types instead. Temporarily include these to enable the build -include Windows.UI.Xaml.Interop.Type -include Windows.UI.Xaml.Interop.NotifyCollectionChangedAction - -include Windows.UI.Xaml.Markup.ContentPropertyAttribute -include Windows.UI.Xaml.StyleTypedPropertyAttribute -include Windows.UI.Xaml.TemplatePartAttribute -include Windows.UI.Xaml.TemplateVisualStateAttribute From b96186673f70c39a4a9595befcdf17ce3af1f1df Mon Sep 17 00:00:00 2001 From: Manodasan Wignarajah Date: Sun, 15 Mar 2026 23:58:28 -0700 Subject: [PATCH 099/100] Remove other overlapping includes --- src/Projections/WinAppSDK/WinAppSDK.csproj | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Projections/WinAppSDK/WinAppSDK.csproj b/src/Projections/WinAppSDK/WinAppSDK.csproj index 1cce009a97..188de263f2 100644 --- a/src/Projections/WinAppSDK/WinAppSDK.csproj +++ b/src/Projections/WinAppSDK/WinAppSDK.csproj @@ -21,13 +21,9 @@ -include Microsoft # The current WinUI nuget incorrectly references several Windows.* types that should be # Microsoft.* types instead. Temporarily include these to enable the build - -include Windows.UI.Xaml.Interop.Type - -include Windows.UI.Xaml.Interop.NotifyCollectionChangedAction -include Windows.UI.Xaml.StyleTypedPropertyAttribute -include Windows.UI.Xaml.TemplatePartAttribute -include Windows.UI.Xaml.TemplateVisualStateAttribute - -include Windows.UI.Xaml.Data.BindableAttribute - -include Windows.UI.Xaml.Markup.ContentPropertyAttribute -include Windows.UI.Xaml.Markup.FullXamlMetadataProviderAttribute -include Windows.UI.Xaml.Markup.MarkupExtensionReturnTypeAttribute -include Windows.UI.Xaml.Media.Animation.ConditionallyIndependentlyAnimatableAttribute @@ -52,4 +48,4 @@ - \ No newline at end of file + From dc24e660a59406ad82a9d0d4c1b69c70e0d9fc96 Mon Sep 17 00:00:00 2001 From: Manodasan Wignarajah Date: Mon, 16 Mar 2026 00:57:27 -0700 Subject: [PATCH 100/100] Make reference projection match impl projection --- .../Windows.UI.Xaml/Windows.UI.Xaml.csproj | 11 +++++++++++ src/Projections/Windows/Windows.csproj | 18 +++++++----------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/Projections/Windows.UI.Xaml/Windows.UI.Xaml.csproj b/src/Projections/Windows.UI.Xaml/Windows.UI.Xaml.csproj index 64204b7ff3..1ade586088 100644 --- a/src/Projections/Windows.UI.Xaml/Windows.UI.Xaml.csproj +++ b/src/Projections/Windows.UI.Xaml/Windows.UI.Xaml.csproj @@ -19,6 +19,17 @@ -exclude Windows -include Windows.UI.Xaml +-include Windows.UI.Colors +-include Windows.UI.ColorHelper +-include Windows.UI.IColorHelper +-include Windows.UI.IColors +-include Windows.UI.Text.FontWeights +-include Windows.UI.Text.IFontWeights +-include Windows.ApplicationModel.Store.Preview.WebAuthenticationCoreManagerHelper +-include Windows.ApplicationModel.Store.Preview.IWebAuthenticationCoreManagerHelper +-exclude Windows.UI.Xaml.Interop +-exclude Windows.UI.Xaml.Data.BindableAttribute +-exclude Windows.UI.Xaml.Markup.ContentPropertyAttribute diff --git a/src/Projections/Windows/Windows.csproj b/src/Projections/Windows/Windows.csproj index 2dc1c974cb..ee46556aa7 100644 --- a/src/Projections/Windows/Windows.csproj +++ b/src/Projections/Windows/Windows.csproj @@ -14,26 +14,22 @@ - -include Windows -# Exclude Windows.UI, Windows.UI.Text, Windows.UI.Xaml per Microsoft.Windows.SDK.WinUI.Contracts NuGet --include Windows.UI.Popups -exclude Windows.UI.Colors -exclude Windows.UI.IColors -exclude Windows.UI.ColorHelper -exclude Windows.UI.IColorHelper -exclude Windows.UI.IColorHelperStatics -exclude Windows.UI.IColorHelperStatics2 -#-exclude Windows.UI.Text (must include Windows.UI.Text to work around WinUI nuget issues) +-exclude Windows.UI.Text.FontWeights +-exclude Windows.UI.Text.IFontWeights -exclude Windows.UI.Xaml --exclude Windows.ApplicationModel.Store.Preview -# Allow Windows.UI.Text, Windows.UI.Xaml types used in other namespaces --include Windows.UI.Text.FontStretch --include Windows.UI.Text.FontStyle --include Windows.UI.Text.FontWeight --include Windows.UI.Text.IFontWeights --include Windows.UI.Text.UnderlineType +-exclude Windows.ApplicationModel.Store.Preview.WebAuthenticationCoreManagerHelper +-exclude Windows.ApplicationModel.Store.Preview.IWebAuthenticationCoreManagerHelper +-include Windows.UI.Xaml.Interop +-include Windows.UI.Xaml.Data.BindableAttribute +-include Windows.UI.Xaml.Markup.ContentPropertyAttribute