diff --git a/src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Shipped.md b/src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Shipped.md
new file mode 100644
index 000000000..cd58f0353
--- /dev/null
+++ b/src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Shipped.md
@@ -0,0 +1,11 @@
+; Shipped analyzer releases
+; https://github.com/dotnet/roslyn-analyzers/blob/master/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
+
+## Release 3.0.0
+
+### New Rules
+Rule ID | Category | Severity | Notes
+--------|----------|----------|-------
+CSWINRT2000 | WindowsRuntime.SourceGenerator | Error | Invalid '[GeneratedCustomPropertyProvider]' target type
+CSWINRT2001 | WindowsRuntime.SourceGenerator | Error | Missing 'partial' for '[GeneratedCustomPropertyProvider]' target type
+CSWINRT2002 | WindowsRuntime.SourceGenerator | Error | 'ICustomPropertyProvider' interface type not available
\ No newline at end of file
diff --git a/src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Unshipped.md b/src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Unshipped.md
new file mode 100644
index 000000000..6640189c3
--- /dev/null
+++ b/src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Unshipped.md
@@ -0,0 +1,6 @@
+; Unshipped analyzer release
+; https://github.com/dotnet/roslyn-analyzers/blob/master/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md
+
+### New Rules
+Rule ID | Category | Severity | Notes
+--------|----------|----------|-------
diff --git a/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Emit.cs b/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Emit.cs
new file mode 100644
index 000000000..61e054003
--- /dev/null
+++ b/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Emit.cs
@@ -0,0 +1,371 @@
+// 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]);
+
+ // 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());
+ }
+
+ ///
+ /// 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.IsIndexer))
+ {
+ 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)
+ {
+ 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,");
+ }
+
+ // 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.IsIndexer))
+ {
+ writer.WriteLine("return null;");
+
+ return;
+ }
+
+ // Switch over the type of all available indexer properties
+ foreach (CustomPropertyInfo propertyInfo in info.CustomProperties)
+ {
+ if (!propertyInfo.IsIndexer)
+ {
+ 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);
+ }
+
+ ///
+ /// 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
diff --git a/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Execute.cs b/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Execute.cs
new file mode 100644
index 000000000..b50dc95ce
--- /dev/null
+++ b/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Execute.cs
@@ -0,0 +1,184 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System;
+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 CS8620, IDE0046 // TODO: remove 'CS8620' suppression when compiler warning is fixed
+
+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, abstract, or 'ref', we cannot implement 'ICustomPropertyProvider' on it
+ if (((MemberDeclarationSyntax)node).Modifiers.ContainsAny(SyntaxKind.StaticKeyword, SyntaxKind.AbstractKeyword, SyntaxKind.RefKeyword))
+ {
+ 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;
+ }
+
+ ///
+ /// 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);
+ }
+
+ ///
+ /// 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();
+
+ // Enumerate all members of the annotated type to discover all properties
+ 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)
+ {
+ 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)
+ {
+ 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(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: indexerType?.GetFullyQualifiedNameWithNullabilityAnnotations(),
+ CanRead: propertySymbol.GetMethod is { DeclaredAccessibility: Accessibility.Public },
+ CanWrite: propertySymbol.SetMethod is { DeclaredAccessibility: Accessibility.Public },
+ IsStatic: propertySymbol.IsStatic));
+ }
+
+ token.ThrowIfCancellationRequested();
+
+ return customPropertyInfo.ToImmutable();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.cs b/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.cs
new file mode 100644
index 000000000..e342f1612
--- /dev/null
+++ b/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.cs
@@ -0,0 +1,29 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using Microsoft.CodeAnalysis;
+using WindowsRuntime.SourceGenerator.Models;
+
+namespace WindowsRuntime.SourceGenerator;
+
+///
+/// A generator to emit ICustomPropertyProvider implementations for annotated types.
+///
+[Generator]
+public sealed partial class CustomPropertyProviderGenerator : IIncrementalGenerator
+{
+ ///
+ public void Initialize(IncrementalGeneratorInitializationContext context)
+ {
+ // 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, Emit.WriteCustomPropertyProviderImplementation);
+ }
+}
\ No newline at end of file
diff --git a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/GeneratedCustomPropertyProviderNoAvailableInterfaceTypeAnalyzer.cs b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/GeneratedCustomPropertyProviderNoAvailableInterfaceTypeAnalyzer.cs
new file mode 100644
index 000000000..e9b2327bf
--- /dev/null
+++ b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/GeneratedCustomPropertyProviderNoAvailableInterfaceTypeAnalyzer.cs
@@ -0,0 +1,63 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Collections.Immutable;
+using System.Linq;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Diagnostics;
+
+namespace WindowsRuntime.SourceGenerator.Diagnostics;
+
+///
+/// A diagnostic analyzer that validates when [GeneratedCustomPropertyProvider] is used but no interface is available.
+///
+[DiagnosticAnalyzer(LanguageNames.CSharp)]
+public sealed class GeneratedCustomPropertyProviderNoAvailableInterfaceTypeAnalyzer : DiagnosticAnalyzer
+{
+ ///
+ public override ImmutableArray SupportedDiagnostics { get; } = [DiagnosticDescriptors.GeneratedCustomPropertyProviderNoAvailableInterfaceType];
+
+ ///
+ public override void Initialize(AnalysisContext context)
+ {
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
+ context.EnableConcurrentExecution();
+
+ context.RegisterCompilationStartAction(static context =>
+ {
+ // Get the '[GeneratedCustomPropertyProvider]' symbol
+ if (context.Compilation.GetTypeByMetadataName("WindowsRuntime.Xaml.GeneratedCustomPropertyProviderAttribute") is not { } attributeType)
+ {
+ return;
+ }
+
+ // Try to get any 'ICustomPropertyProvider' symbol
+ INamedTypeSymbol? windowsUIXamlCustomPropertyProviderType = context.Compilation.GetTypeByMetadataName("Windows.UI.Xaml.Data.ICustomPropertyProvider");
+ INamedTypeSymbol? microsoftUIXamlCustomPropertyProviderType = context.Compilation.GetTypeByMetadataName("Microsoft.UI.Xaml.Data.ICustomPropertyProvider");
+
+ // If we have either of them, we'll never need to report any diagnostics
+ if (windowsUIXamlCustomPropertyProviderType is not null || microsoftUIXamlCustomPropertyProviderType is not null)
+ {
+ return;
+ }
+
+ context.RegisterSymbolAction(context =>
+ {
+ // Only classes and structs can be targets of the attribute
+ if (context.Symbol is not INamedTypeSymbol { TypeKind: TypeKind.Class or TypeKind.Struct } typeSymbol)
+ {
+ return;
+ }
+
+ // Emit a diagnostic if the type has the attribute, as it can't be used now
+ if (typeSymbol.HasAttributeWithType(attributeType))
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ DiagnosticDescriptors.GeneratedCustomPropertyProviderNoAvailableInterfaceType,
+ typeSymbol.Locations.FirstOrDefault(),
+ typeSymbol));
+ }
+ }, SymbolKind.NamedType);
+ });
+ }
+}
\ No newline at end of file
diff --git a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/GeneratedCustomPropertyProviderTargetTypeAnalyzer.cs b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/GeneratedCustomPropertyProviderTargetTypeAnalyzer.cs
new file mode 100644
index 000000000..38e3078fb
--- /dev/null
+++ b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/GeneratedCustomPropertyProviderTargetTypeAnalyzer.cs
@@ -0,0 +1,77 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Collections.Immutable;
+using System.Linq;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Diagnostics;
+
+namespace WindowsRuntime.SourceGenerator.Diagnostics;
+
+///
+/// A diagnostic analyzer that validates target types for [GeneratedCustomPropertyProvider].
+///
+[DiagnosticAnalyzer(LanguageNames.CSharp)]
+public sealed class GeneratedCustomPropertyProviderTargetTypeAnalyzer : DiagnosticAnalyzer
+{
+ ///
+ public override ImmutableArray SupportedDiagnostics { get; } = [
+ DiagnosticDescriptors.GeneratedCustomPropertyProviderInvalidTargetType,
+ DiagnosticDescriptors.GeneratedCustomPropertyProviderMissingPartialModifier];
+
+ ///
+ public override void Initialize(AnalysisContext context)
+ {
+ context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);
+ context.EnableConcurrentExecution();
+
+ context.RegisterCompilationStartAction(static context =>
+ {
+ // Get the '[GeneratedCustomPropertyProvider]' symbol
+ if (context.Compilation.GetTypeByMetadataName("WindowsRuntime.Xaml.GeneratedCustomPropertyProviderAttribute") is not { } attributeType)
+ {
+ return;
+ }
+
+ context.RegisterSymbolAction(context =>
+ {
+ // Only classes and structs can be targets of the attribute
+ if (context.Symbol is not INamedTypeSymbol { TypeKind: TypeKind.Class or TypeKind.Struct } typeSymbol)
+ {
+ return;
+ }
+
+ // Immediately bail if the type doesn't have the attribute
+ if (!typeSymbol.HasAttributeWithType(attributeType))
+ {
+ return;
+ }
+
+ // If the type is static, abstract, or 'ref', it isn't valid
+ if (typeSymbol.IsAbstract || typeSymbol.IsStatic || typeSymbol.IsRefLikeType)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ DiagnosticDescriptors.GeneratedCustomPropertyProviderInvalidTargetType,
+ typeSymbol.Locations.FirstOrDefault(),
+ typeSymbol));
+ }
+
+ // Try to get a syntax reference for the symbol, to resolve the syntax node for it
+ if (typeSymbol.DeclaringSyntaxReferences.FirstOrDefault() is SyntaxReference syntaxReference)
+ {
+ SyntaxNode typeNode = syntaxReference.GetSyntax(context.CancellationToken);
+
+ // If there's no 'partial' modifier in the type hierarchy, the target type isn't valid
+ if (!((MemberDeclarationSyntax)typeNode).IsPartialAndWithinPartialTypeHierarchy)
+ {
+ context.ReportDiagnostic(Diagnostic.Create(
+ DiagnosticDescriptors.GeneratedCustomPropertyProviderMissingPartialModifier,
+ typeSymbol.Locations.FirstOrDefault(),
+ typeSymbol));
+ }
+ }
+ }, SymbolKind.NamedType);
+ });
+ }
+}
\ No newline at end of file
diff --git a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/DiagnosticDescriptors.cs b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/DiagnosticDescriptors.cs
new file mode 100644
index 000000000..662958a67
--- /dev/null
+++ b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/DiagnosticDescriptors.cs
@@ -0,0 +1,51 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using Microsoft.CodeAnalysis;
+
+namespace WindowsRuntime.SourceGenerator.Diagnostics;
+
+///
+/// A container for all instances for errors reported by analyzers in this project.
+///
+internal static partial class DiagnosticDescriptors
+{
+ ///
+ /// Gets a for an invalid target type for [GeneratedCustomPropertyProvider].
+ ///
+ public static readonly DiagnosticDescriptor GeneratedCustomPropertyProviderInvalidTargetType = new(
+ id: "CSWINRT2000",
+ title: "Invalid '[GeneratedCustomPropertyProvider]' target type",
+ messageFormat: """The type '{0}' is not a valid target for '[GeneratedCustomPropertyProvider]': it must be a 'class' or 'struct' type, and it can't be 'static', 'abstract', or 'ref'""",
+ category: "WindowsRuntime.SourceGenerator",
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true,
+ description: "Types annotated with '[GeneratedCustomPropertyProvider]' must be 'class' or 'struct' types, and they can't be 'static', 'abstract', or 'ref'.",
+ helpLinkUri: "https://github.com/microsoft/CsWinRT");
+
+ ///
+ /// Gets a for a target type for [GeneratedCustomPropertyProvider] missing .
+ ///
+ public static readonly DiagnosticDescriptor GeneratedCustomPropertyProviderMissingPartialModifier = new(
+ id: "CSWINRT2001",
+ title: "Missing 'partial' for '[GeneratedCustomPropertyProvider]' target type",
+ messageFormat: """The type '{0}' (or one of its containing types) is missing the 'partial' modifier, which is required to be used as a target for '[GeneratedCustomPropertyProvider]'""",
+ category: "WindowsRuntime.SourceGenerator",
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true,
+ description: "Types annotated with '[GeneratedCustomPropertyProvider]' must be marked as 'partial' across their whole type hierarchy.",
+ helpLinkUri: "https://github.com/microsoft/CsWinRT");
+
+ ///
+ /// Gets a for when [GeneratedCustomPropertyProvider] can't resolve the interface type.
+ ///
+ public static readonly DiagnosticDescriptor GeneratedCustomPropertyProviderNoAvailableInterfaceType = new(
+ id: "CSWINRT2002",
+ title: "'ICustomPropertyProvider' interface type not available",
+ messageFormat: """The 'ICustomPropertyProvider' interface is not available in the compilation, but it is required to use '[GeneratedCustomPropertyProvider]' (make sure to either reference 'WindowsAppSDK.WinUI' or set the 'UseUwp' property in your .csproj file)""",
+ category: "WindowsRuntime.SourceGenerator",
+ defaultSeverity: DiagnosticSeverity.Error,
+ isEnabledByDefault: true,
+ description: "Using '[GeneratedCustomPropertyProvider]' requires the 'ICustomPropertyProvider' interface type to be available in the compilation, which can be done by either referencing 'WindowsAppSDK.WinUI' or by setting the 'UseUwp' property in the .csproj file.",
+ helpLinkUri: "https://github.com/microsoft/CsWinRT");
+}
\ No newline at end of file
diff --git a/src/Authoring/WinRT.SourceGenerator2/Extensions/ISymbolExtensions.cs b/src/Authoring/WinRT.SourceGenerator2/Extensions/ISymbolExtensions.cs
index 924c66a9e..34fc89f98 100644
--- a/src/Authoring/WinRT.SourceGenerator2/Extensions/ISymbolExtensions.cs
+++ b/src/Authoring/WinRT.SourceGenerator2/Extensions/ISymbolExtensions.cs
@@ -15,6 +15,24 @@ internal static class ISymbolExtensions
{
extension(ISymbol symbol)
{
+ ///
+ /// Gets the fully qualified name for a given symbol.
+ ///
+ /// The fully qualified name for .
+ public string GetFullyQualifiedName()
+ {
+ return symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat);
+ }
+
+ ///
+ /// Gets the fully qualified name for a given symbol, including nullability annotations
+ ///
+ /// The fully qualified name for .
+ public string GetFullyQualifiedNameWithNullabilityAnnotations()
+ {
+ return symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat.AddMiscellaneousOptions(SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier));
+ }
+
///
/// Checks whether a type has an attribute with a specified type.
///
diff --git a/src/Authoring/WinRT.SourceGenerator2/Extensions/ITypeSymbolExtensions.cs b/src/Authoring/WinRT.SourceGenerator2/Extensions/ITypeSymbolExtensions.cs
new file mode 100644
index 000000000..78729d426
--- /dev/null
+++ b/src/Authoring/WinRT.SourceGenerator2/Extensions/ITypeSymbolExtensions.cs
@@ -0,0 +1,120 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections.Generic;
+using Microsoft.CodeAnalysis;
+
+#pragma warning disable CS1734, IDE0046
+
+namespace WindowsRuntime.SourceGenerator;
+
+///
+/// Extensions for .
+///
+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.
+ ///
+ /// 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.
+ ///
+ /// 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
diff --git a/src/Authoring/WinRT.SourceGenerator2/Extensions/IncrementalGeneratorInitializationContextExtensions.cs b/src/Authoring/WinRT.SourceGenerator2/Extensions/IncrementalGeneratorInitializationContextExtensions.cs
new file mode 100644
index 000000000..d3ed23f4e
--- /dev/null
+++ b/src/Authoring/WinRT.SourceGenerator2/Extensions/IncrementalGeneratorInitializationContextExtensions.cs
@@ -0,0 +1,66 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections.Immutable;
+using System.Threading;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.Diagnostics;
+
+namespace WindowsRuntime.SourceGenerator;
+
+///
+/// Extension methods for .
+///
+internal static class IncrementalGeneratorInitializationContextExtensions
+{
+ ///
+ public static IncrementalValuesProvider ForAttributeWithMetadataNameAndOptions(
+ this IncrementalGeneratorInitializationContext context,
+ string fullyQualifiedMetadataName,
+ Func predicate,
+ Func transform)
+ {
+ // Invoke 'ForAttributeWithMetadataName' normally, but just return the context directly
+ IncrementalValuesProvider syntaxContext = context.SyntaxProvider.ForAttributeWithMetadataName(
+ fullyQualifiedMetadataName,
+ predicate,
+ static (context, token) => context);
+
+ // Do the same for the analyzer config options
+ IncrementalValueProvider configOptions = context.AnalyzerConfigOptionsProvider.Select(static (provider, token) => provider.GlobalOptions);
+
+ // Merge the two and invoke the provided transform on these two values. Neither value
+ // is equatable, meaning the pipeline will always re-run until this point. This is
+ // intentional: we don't want any symbols or other expensive objects to be kept alive
+ // across incremental steps, especially if they could cause entire compilations to be
+ // rooted, which would significantly increase memory use and introduce more GC pauses.
+ // In this specific case, flowing non equatable values in a pipeline is therefore fine.
+ return syntaxContext.Combine(configOptions).Select((input, token) => transform(new GeneratorAttributeSyntaxContextWithOptions(input.Left, input.Right), token));
+ }
+}
+
+///
+///
+///
+/// The original value.
+/// The original value.
+internal readonly struct GeneratorAttributeSyntaxContextWithOptions(
+ GeneratorAttributeSyntaxContext syntaxContext,
+ AnalyzerConfigOptions globalOptions)
+{
+ ///
+ public SyntaxNode TargetNode { get; } = syntaxContext.TargetNode;
+
+ ///
+ public ISymbol TargetSymbol { get; } = syntaxContext.TargetSymbol;
+
+ ///
+ public SemanticModel SemanticModel { get; } = syntaxContext.SemanticModel;
+
+ ///
+ public ImmutableArray Attributes { get; } = syntaxContext.Attributes;
+
+ ///
+ public AnalyzerConfigOptions GlobalOptions { get; } = globalOptions;
+}
diff --git a/src/Authoring/WinRT.SourceGenerator2/Extensions/IncrementalValuesProviderExtensions.cs b/src/Authoring/WinRT.SourceGenerator2/Extensions/IncrementalValuesProviderExtensions.cs
new file mode 100644
index 000000000..a04906d25
--- /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(this IncrementalValuesProvider provider)
+ where T : class
+ {
+ return provider.Where(static value => value is not null)!;
+ }
+}
diff --git a/src/Authoring/WinRT.SourceGenerator2/Extensions/IndentedTextWriterExtensions.cs b/src/Authoring/WinRT.SourceGenerator2/Extensions/IndentedTextWriterExtensions.cs
new file mode 100644
index 000000000..76c2b553e
--- /dev/null
+++ b/src/Authoring/WinRT.SourceGenerator2/Extensions/IndentedTextWriterExtensions.cs
@@ -0,0 +1,128 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace WindowsRuntime.SourceGenerator;
+
+///
+/// Extension methods for the type.
+///
+internal static class IndentedTextWriterExtensions
+{
+ ///
+ /// Writes the following attributes into a target writer:
+ ///
+ /// [global::System.CodeDom.Compiler.GeneratedCode("...", "...")]
+ /// [global::System.Diagnostics.DebuggerNonUserCode]
+ /// [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
+ ///
+ ///
+ /// The instance to write into.
+ /// The name of the generator.
+ /// Whether to use fully qualified type names or not.
+ /// Whether to also include the attribute for non-user code.
+ public static void WriteGeneratedAttributes(
+ this IndentedTextWriter writer,
+ string generatorName,
+ bool useFullyQualifiedTypeNames = true,
+ bool includeNonUserCodeAttributes = true)
+ {
+ // We can use this class to get the assembly, as all files for generators are just included
+ // via shared projects. As such, the assembly will be the same as the generator type itself.
+ Version assemblyVersion = typeof(IndentedTextWriterExtensions).Assembly.GetName().Version!;
+
+ if (useFullyQualifiedTypeNames)
+ {
+ writer.WriteLine($$"""[global::System.CodeDom.Compiler.GeneratedCode("{{generatorName}}", "{{assemblyVersion}}")]""");
+
+ if (includeNonUserCodeAttributes)
+ {
+ writer.WriteLine($$"""[global::System.Diagnostics.DebuggerNonUserCode]""");
+ writer.WriteLine($$"""[global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]""");
+ }
+ }
+ else
+ {
+ writer.WriteLine($$"""[GeneratedCode("{{generatorName}}", "{{assemblyVersion}}")]""");
+
+ if (includeNonUserCodeAttributes)
+ {
+ writer.WriteLine($$"""[DebuggerNonUserCode]""");
+ writer.WriteLine($$"""[ExcludeFromCodeCoverage]""");
+ }
+ }
+ }
+
+ ///
+ /// Writes a sequence of using directives, sorted correctly.
+ ///
+ /// The instance to write into.
+ /// The sequence of using directives to write.
+ public static void WriteSortedUsingDirectives(this IndentedTextWriter writer, IEnumerable usingDirectives)
+ {
+ // Add the System directives first, in the correct order
+ foreach (string usingDirective in usingDirectives.Where(static name => name.StartsWith("global::System", StringComparison.InvariantCulture)).OrderBy(static name => name))
+ {
+ writer.WriteLine($"using {usingDirective};");
+ }
+
+ // Add the other directives, also sorted in the correct order
+ foreach (string usingDirective in usingDirectives.Where(static name => !name.StartsWith("global::System", StringComparison.InvariantCulture)).OrderBy(static name => name))
+ {
+ writer.WriteLine($"using {usingDirective};");
+ }
+
+ // Leave a trailing blank line if at least one using directive has been written.
+ // This is so that any members will correctly have a leading blank line before.
+ writer.WriteLineIf(usingDirectives.Any());
+ }
+
+ ///
+ /// Writes a series of members separated by one line between each of them.
+ ///
+ /// The type of input items to process.
+ /// The instance to write into.
+ /// The input items to process.
+ /// The instance to invoke for each item.
+ public static void WriteLineSeparatedMembers(
+ this IndentedTextWriter writer,
+ ReadOnlySpan items,
+ IndentedTextWriter.Callback callback)
+ {
+ for (int i = 0; i < items.Length; i++)
+ {
+ if (i > 0)
+ {
+ writer.WriteLine();
+ }
+
+ callback(items[i], writer);
+ }
+ }
+
+ ///
+ /// Writes a series of initialization expressions separated by a comma between each of them.
+ ///
+ /// The type of input items to process.
+ /// The instance to write into.
+ /// The input items to process.
+ /// The instance to invoke for each item.
+ public static void WriteInitializationExpressions(
+ this IndentedTextWriter writer,
+ ReadOnlySpan items,
+ IndentedTextWriter.Callback callback)
+ {
+ for (int i = 0; i < items.Length; i++)
+ {
+ callback(items[i], writer);
+
+ if (i < items.Length - 1)
+ {
+ writer.WriteLine(",");
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Authoring/WinRT.SourceGenerator2/Extensions/MemberDeclarationSyntaxExtensions.cs b/src/Authoring/WinRT.SourceGenerator2/Extensions/MemberDeclarationSyntaxExtensions.cs
new file mode 100644
index 000000000..cc510ce9d
--- /dev/null
+++ b/src/Authoring/WinRT.SourceGenerator2/Extensions/MemberDeclarationSyntaxExtensions.cs
@@ -0,0 +1,49 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace WindowsRuntime.SourceGenerator;
+
+///
+/// Extensions for member declaration syntax types.
+///
+internal static class MemberDeclarationSyntaxExtensions
+{
+ extension(MemberDeclarationSyntax node)
+ {
+ ///
+ /// Gets whether the input member declaration is partial.
+ ///
+ public bool IsPartial => node.Modifiers.Any(SyntaxKind.PartialKeyword);
+
+ ///
+ /// Gets whether the input member declaration is partial and
+ /// all of its parent type declarations are also partial.
+ ///
+ public bool IsPartialAndWithinPartialTypeHierarchy
+ {
+ get
+ {
+ // If the target node is not partial, stop immediately
+ if (!node.IsPartial)
+ {
+ return false;
+ }
+
+ // Walk all parent type declarations, stop if any of them is not partial
+ foreach (SyntaxNode ancestor in node.Ancestors())
+ {
+ if (ancestor is BaseTypeDeclarationSyntax { IsPartial: false })
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Authoring/WinRT.SourceGenerator2/Extensions/SyntaxExtensions.cs b/src/Authoring/WinRT.SourceGenerator2/Extensions/SyntaxExtensions.cs
new file mode 100644
index 000000000..4af55419b
--- /dev/null
+++ b/src/Authoring/WinRT.SourceGenerator2/Extensions/SyntaxExtensions.cs
@@ -0,0 +1,56 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+
+namespace WindowsRuntime.SourceGenerator;
+
+///
+/// Extensions for syntax types.
+///
+internal static class SyntaxExtensions
+{
+ extension(SyntaxNode node)
+ {
+ ///
+ /// Determines if is of any of the specified kinds.
+ ///
+ /// The syntax kinds to test for.
+ /// Whether the input node is of any of the specified kinds.
+ public bool IsAnyKind(params ReadOnlySpan kinds)
+ {
+ foreach (SyntaxKind kind in kinds)
+ {
+ if (node.IsKind(kind))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+ }
+
+ extension(SyntaxTokenList list)
+ {
+ ///
+ /// Tests whether a list contains any token of particular kinds.
+ ///
+ /// The syntax kinds to test for.
+ /// Whether the input list contains any of the specified kinds.
+ public bool ContainsAny(params ReadOnlySpan kinds)
+ {
+ foreach (SyntaxKind kind in kinds)
+ {
+ if (list.IndexOf(kind) >= 0)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Authoring/WinRT.SourceGenerator2/Helpers/EquatableArray{T}.cs b/src/Authoring/WinRT.SourceGenerator2/Helpers/EquatableArray{T}.cs
index 223d8a6b5..384a904e1 100644
--- a/src/Authoring/WinRT.SourceGenerator2/Helpers/EquatableArray{T}.cs
+++ b/src/Authoring/WinRT.SourceGenerator2/Helpers/EquatableArray{T}.cs
@@ -75,19 +75,37 @@ public bool IsEmpty
get => AsImmutableArray().IsEmpty;
}
- ///
+ ///
+ /// Gets a value indicating whether the current array is default or empty.
+ ///
+ public bool IsDefaultOrEmpty
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => AsImmutableArray().IsDefaultOrEmpty;
+ }
+
+ ///
+ /// Gets the length of the current array.
+ ///
+ public int Length
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => AsImmutableArray().Length;
+ }
+
+ ///
public bool Equals(EquatableArray array)
{
return AsSpan().SequenceEqual(array.AsSpan());
}
- ///
+ ///
public override bool Equals([NotNullWhen(true)] object? obj)
{
return obj is EquatableArray array && Equals(array);
}
- ///
+ ///
public override int GetHashCode()
{
if (_array is not T[] array)
@@ -152,13 +170,13 @@ public ImmutableArray.Enumerator GetEnumerator()
return AsImmutableArray().GetEnumerator();
}
- ///
+ ///
IEnumerator IEnumerable.GetEnumerator()
{
return ((IEnumerable)AsImmutableArray()).GetEnumerator();
}
- ///
+ ///
IEnumerator IEnumerable.GetEnumerator()
{
return ((IEnumerable)AsImmutableArray()).GetEnumerator();
diff --git a/src/Authoring/WinRT.SourceGenerator2/Helpers/IndentedTextWriter.cs b/src/Authoring/WinRT.SourceGenerator2/Helpers/IndentedTextWriter.cs
new file mode 100644
index 000000000..f2d84e94f
--- /dev/null
+++ b/src/Authoring/WinRT.SourceGenerator2/Helpers/IndentedTextWriter.cs
@@ -0,0 +1,520 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+// Ported from ComputeSharp.
+// See: https://github.com/Sergio0694/ComputeSharp/blob/main/src/ComputeSharp.SourceGeneration/Helpers/IndentedTextWriter.cs.
+
+using System;
+using System.ComponentModel;
+using System.Diagnostics.CodeAnalysis;
+using System.Globalization;
+using System.Runtime.CompilerServices;
+using System.Text;
+
+namespace WindowsRuntime.SourceGenerator;
+
+///
+/// A helper type to build sequences of values with pooled buffers.
+///
+internal sealed class IndentedTextWriter : IDisposable
+{
+ ///
+ /// The default indentation (4 spaces).
+ ///
+ private const string DefaultIndentation = " ";
+
+ ///
+ /// The default new line ('\n').
+ ///
+ private const char DefaultNewLine = '\n';
+
+ ///
+ /// The instance that text will be written to.
+ ///
+ private PooledArrayBuilder _builder;
+
+ ///
+ /// The current indentation level.
+ ///
+ private int _currentIndentationLevel;
+
+ ///
+ /// The current indentation, as text.
+ ///
+ private string _currentIndentation = "";
+
+ ///
+ /// The cached array of available indentations, as text.
+ ///
+ private string[] _availableIndentations;
+
+ ///
+ /// Creates a new object.
+ ///
+ public IndentedTextWriter()
+ {
+ _builder = new PooledArrayBuilder();
+ _currentIndentationLevel = 0;
+ _currentIndentation = "";
+ _availableIndentations = new string[4];
+ _availableIndentations[0] = "";
+
+ for (int i = 1, n = _availableIndentations.Length; i < n; i++)
+ {
+ _availableIndentations[i] = _availableIndentations[i - 1] + DefaultIndentation;
+ }
+ }
+
+ ///
+ /// Advances the current writer and gets a to the requested memory area.
+ ///
+ /// The requested size to advance by.
+ /// A to the requested memory area.
+ ///
+ /// No other data should be written to the writer while the returned
+ /// is in use, as it could invalidate the memory area wrapped by it, if resizing occurs.
+ ///
+ public Span Advance(int requestedSize)
+ {
+ // Add the leading whitespace if needed (same as WriteRawText below)
+ if (_builder.Count == 0 || _builder.WrittenSpan[^1] == DefaultNewLine)
+ {
+ _builder.AddRange(_currentIndentation.AsSpan());
+ }
+
+ return _builder.Advance(requestedSize);
+ }
+
+ ///
+ /// Increases the current indentation level.
+ ///
+ public void IncreaseIndent()
+ {
+ _currentIndentationLevel++;
+
+ if (_currentIndentationLevel == _availableIndentations.Length)
+ {
+ Array.Resize(ref _availableIndentations, _availableIndentations.Length * 2);
+ }
+
+ // Set both the current indentation and the current position in the indentations
+ // array to the expected indentation for the incremented level (ie. one level more).
+ _currentIndentation = _availableIndentations[_currentIndentationLevel]
+ ??= _availableIndentations[_currentIndentationLevel - 1] + DefaultIndentation;
+ }
+
+ ///
+ /// Decreases the current indentation level.
+ ///
+ public void DecreaseIndent()
+ {
+ _currentIndentationLevel--;
+ _currentIndentation = _availableIndentations[_currentIndentationLevel];
+ }
+
+ ///
+ /// Writes a block to the underlying buffer.
+ ///
+ /// A value to close the open block with.
+ public Block WriteBlock()
+ {
+ WriteLine("{");
+ IncreaseIndent();
+
+ return new(this);
+ }
+
+ ///
+ /// Writes content to the underlying buffer.
+ ///
+ /// The content to write.
+ /// Whether the input content is multiline.
+ public void Write(string content, bool isMultiline = false)
+ {
+ Write(content.AsSpan(), isMultiline);
+ }
+
+ ///
+ /// Writes content to the underlying buffer.
+ ///
+ /// The content to write.
+ /// Whether the input content is multiline.
+ public void Write(ReadOnlySpan content, bool isMultiline = false)
+ {
+ if (isMultiline)
+ {
+ while (content.Length > 0)
+ {
+ int newLineIndex = content.IndexOf(DefaultNewLine);
+
+ if (newLineIndex < 0)
+ {
+ // There are no new lines left, so the content can be written as a single line
+ WriteRawText(content);
+
+ break;
+ }
+ else
+ {
+ ReadOnlySpan line = content[..newLineIndex];
+
+ // Write the current line (if it's empty, we can skip writing the text entirely).
+ // This ensures that raw multiline string literals with blank lines don't have
+ // extra whitespace at the start of those lines, which would otherwise happen.
+ WriteIf(!line.IsEmpty, line);
+ WriteLine();
+
+ // Move past the new line character (the result could be an empty span)
+ content = content[(newLineIndex + 1)..];
+ }
+ }
+ }
+ else
+ {
+ WriteRawText(content);
+ }
+ }
+
+ ///
+ /// Writes content to the underlying buffer.
+ ///
+ /// The interpolated string handler with content to write.
+ [SuppressMessage("Style", "IDE0060", Justification = "The 'handler' parameter is used by the caller via compiler lowering.")]
+ public void Write([InterpolatedStringHandlerArgument("")] ref WriteInterpolatedStringHandler handler)
+ {
+ _ = this;
+ }
+
+ ///
+ /// Writes content to the underlying buffer depending on an input condition.
+ ///
+ /// The condition to use to decide whether or not to write content.
+ /// The content to write.
+ /// Whether the input content is multiline.
+ public void WriteIf(bool condition, string content, bool isMultiline = false)
+ {
+ if (condition)
+ {
+ Write(content.AsSpan(), isMultiline);
+ }
+ }
+
+ ///
+ /// Writes content to the underlying buffer depending on an input condition.
+ ///
+ /// The condition to use to decide whether or not to write content.
+ /// The content to write.
+ /// Whether the input content is multiline.
+ public void WriteIf(bool condition, ReadOnlySpan content, bool isMultiline = false)
+ {
+ if (condition)
+ {
+ Write(content, isMultiline);
+ }
+ }
+
+ ///
+ /// Writes content to the underlying buffer depending on an input condition.
+ ///
+ /// The condition to use to decide whether or not to write content.
+ /// The interpolated string handler with content to write.
+ [SuppressMessage("Style", "IDE0060", Justification = "The 'handler' parameter is used by the caller via compiler lowering.")]
+ public void WriteIf(bool condition, [InterpolatedStringHandlerArgument("", nameof(condition))] ref WriteIfInterpolatedStringHandler handler)
+ {
+ _ = this;
+ }
+
+ ///
+ /// Writes a line to the underlying buffer.
+ ///
+ /// Indicates whether to skip adding the line if there already is one.
+ public void WriteLine(bool skipIfPresent = false)
+ {
+ if (skipIfPresent && _builder.WrittenSpan is [.., '\n', '\n'])
+ {
+ return;
+ }
+
+ _builder.Add(DefaultNewLine);
+ }
+
+ ///
+ /// Writes content to the underlying buffer and appends a trailing new line.
+ ///
+ /// The content to write.
+ /// Whether the input content is multiline.
+ public void WriteLine(string content, bool isMultiline = false)
+ {
+ WriteLine(content.AsSpan(), isMultiline);
+ }
+
+ ///
+ /// Writes content to the underlying buffer and appends a trailing new line.
+ ///
+ /// The content to write.
+ /// Whether the input content is multiline.
+ public void WriteLine(ReadOnlySpan content, bool isMultiline = false)
+ {
+ Write(content, isMultiline);
+ WriteLine();
+ }
+
+ ///
+ /// Writes content to the underlying buffer and appends a trailing new line.
+ ///
+ /// The interpolated string handler with content to write.
+ [SuppressMessage("Style", "IDE0060", Justification = "The 'handler' parameter is used by the caller via compiler lowering.")]
+ public void WriteLine([InterpolatedStringHandlerArgument("")] ref WriteInterpolatedStringHandler handler)
+ {
+ WriteLine();
+ }
+
+ ///
+ /// Writes a line to the underlying buffer depending on an input condition.
+ ///
+ /// The condition to use to decide whether or not to write content.
+ /// Indicates whether to skip adding the line if there already is one.
+ public void WriteLineIf(bool condition, bool skipIfPresent = false)
+ {
+ if (condition)
+ {
+ WriteLine(skipIfPresent);
+ }
+ }
+
+ ///
+ /// Writes content to the underlying buffer and appends a trailing new line depending on an input condition.
+ ///
+ /// The condition to use to decide whether or not to write content.
+ /// The content to write.
+ /// Whether the input content is multiline.
+ public void WriteLineIf(bool condition, string content, bool isMultiline = false)
+ {
+ if (condition)
+ {
+ WriteLine(content.AsSpan(), isMultiline);
+ }
+ }
+
+ ///
+ /// Writes content to the underlying buffer and appends a trailing new line depending on an input condition.
+ ///
+ /// The condition to use to decide whether or not to write content.
+ /// The content to write.
+ /// Whether the input content is multiline.
+ public void WriteLineIf(bool condition, ReadOnlySpan content, bool isMultiline = false)
+ {
+ if (condition)
+ {
+ Write(content, isMultiline);
+ WriteLine();
+ }
+ }
+
+ ///
+ /// Writes content to the underlying buffer and appends a trailing new line depending on an input condition.
+ ///
+ /// The condition to use to decide whether or not to write content.
+ /// The interpolated string handler with content to write.
+ [SuppressMessage("Style", "IDE0060", Justification = "The 'handler' parameter is used by the caller via compiler lowering.")]
+ public void WriteLineIf(bool condition, [InterpolatedStringHandlerArgument("", nameof(condition))] ref WriteIfInterpolatedStringHandler handler)
+ {
+ if (condition)
+ {
+ WriteLine();
+ }
+ }
+
+ ///
+ public override string ToString()
+ {
+ return _builder.WrittenSpan.Trim().ToString();
+ }
+
+ ///
+ public void Dispose()
+ {
+ _builder.Dispose();
+ }
+
+ ///
+ /// Writes raw text to the underlying buffer, adding leading indentation if needed.
+ ///
+ /// The raw text to write.
+ private void WriteRawText(ReadOnlySpan content)
+ {
+ if (_builder.Count == 0 || _builder.WrittenSpan[^1] == DefaultNewLine)
+ {
+ _builder.AddRange(_currentIndentation.AsSpan());
+ }
+
+ _builder.AddRange(content);
+ }
+
+ ///
+ /// A delegate representing a callback to write data into an instance.
+ ///
+ /// The type of data to use.
+ /// The input data to use to write into .
+ /// The instance to write into.
+ public delegate void Callback(T value, IndentedTextWriter writer);
+
+ ///
+ /// Represents an indented block that needs to be closed.
+ ///
+ /// The input instance to wrap.
+ public struct Block(IndentedTextWriter writer) : IDisposable
+ {
+ ///
+ /// The instance to write to.
+ ///
+ private IndentedTextWriter? _writer = writer;
+
+ ///
+ public void Dispose()
+ {
+ IndentedTextWriter? writer = _writer;
+
+ _writer = null;
+
+ if (writer is not null)
+ {
+ writer.DecreaseIndent();
+ writer.WriteLine("}");
+ }
+ }
+ }
+
+ ///
+ /// Provides a handler used by the language compiler to append interpolated strings into instances.
+ ///
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ [InterpolatedStringHandler]
+ public readonly ref struct WriteInterpolatedStringHandler
+ {
+ /// The associated to which to append.
+ private readonly IndentedTextWriter _writer;
+
+ /// Creates a handler used to append an interpolated string into a .
+ /// The number of constant characters outside of interpolation expressions in the interpolated string.
+ /// The number of interpolation expressions in the interpolated string.
+ /// The associated to which to append.
+ /// This is intended to be called only by compiler-generated code. Arguments are not validated as they'd otherwise be for members intended to be used directly.
+ public WriteInterpolatedStringHandler(int literalLength, int formattedCount, IndentedTextWriter writer)
+ {
+ _writer = writer;
+ }
+
+ /// Writes the specified string to the handler.
+ /// The string to write.
+ public void AppendLiteral(string value)
+ {
+ _writer.Write(value);
+ }
+
+ /// Writes the specified value to the handler.
+ /// The value to write.
+ public void AppendFormatted(string? value)
+ {
+ AppendFormatted(value);
+ }
+
+ /// Writes the specified character span to the handler.
+ /// The span to write.
+ public void AppendFormatted(ReadOnlySpan value)
+ {
+ _writer.Write(value);
+ }
+
+ /// Writes the specified value to the handler.
+ /// The value to write.
+ /// The type of the value to write.
+ public void AppendFormatted(T value)
+ {
+ if (value is not null)
+ {
+ _writer.Write(value.ToString()!);
+ }
+ }
+
+ /// Writes the specified value to the handler.
+ /// The value to write.
+ /// The format string.
+ /// The type of the value to write.
+ public void AppendFormatted(T value, string? format)
+ {
+ if (value is IFormattable)
+ {
+ _writer.Write(((IFormattable)value).ToString(format, CultureInfo.InvariantCulture));
+ }
+ else if (value is not null)
+ {
+ _writer.Write(value.ToString()!);
+ }
+ }
+ }
+
+ ///
+ /// Provides a handler used by the language compiler to conditionally append interpolated strings into instances.
+ ///
+ [EditorBrowsable(EditorBrowsableState.Never)]
+ [InterpolatedStringHandler]
+ public readonly ref struct WriteIfInterpolatedStringHandler
+ {
+ /// The associated to use.
+ private readonly WriteInterpolatedStringHandler handler;
+
+ /// Creates a handler used to append an interpolated string into a .
+ /// The number of constant characters outside of interpolation expressions in the interpolated string.
+ /// The number of interpolation expressions in the interpolated string.
+ /// The associated to which to append.
+ /// The condition to use to decide whether or not to write content.
+ /// A value indicating whether formatting should proceed.
+ /// This is intended to be called only by compiler-generated code. Arguments are not validated as they'd otherwise be for members intended to be used directly.
+ public WriteIfInterpolatedStringHandler(int literalLength, int formattedCount, IndentedTextWriter writer, bool condition, out bool shouldAppend)
+ {
+ if (condition)
+ {
+ handler = new WriteInterpolatedStringHandler(literalLength, formattedCount, writer);
+
+ shouldAppend = true;
+ }
+ else
+ {
+ handler = default;
+
+ shouldAppend = false;
+ }
+ }
+
+ ///
+ public void AppendLiteral(string value)
+ {
+ handler.AppendLiteral(value);
+ }
+
+ ///
+ public void AppendFormatted(string? value)
+ {
+ handler.AppendFormatted(value);
+ }
+
+ ///
+ public void AppendFormatted(ReadOnlySpan value)
+ {
+ handler.AppendFormatted(value);
+ }
+
+ ///
+ public void AppendFormatted(T value)
+ {
+ handler.AppendFormatted(value);
+ }
+
+ ///
+ public void AppendFormatted(T value, string? format)
+ {
+ handler.AppendFormatted(value, format);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Authoring/WinRT.SourceGenerator2/Helpers/ObjectPool{T}.cs b/src/Authoring/WinRT.SourceGenerator2/Helpers/ObjectPool{T}.cs
new file mode 100644
index 000000000..04f60a089
--- /dev/null
+++ b/src/Authoring/WinRT.SourceGenerator2/Helpers/ObjectPool{T}.cs
@@ -0,0 +1,154 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+// Ported from Roslyn.
+// See: https://github.com/dotnet/roslyn/blob/main/src/Dependencies/PooledObjects/ObjectPool%601.cs.
+
+using System;
+using System.Runtime.CompilerServices;
+using System.Threading;
+
+#pragma warning disable RS1035
+
+namespace WindowsRuntime.SourceGenerator;
+
+///
+///
+/// Generic implementation of object pooling pattern with predefined pool size limit. The main purpose
+/// is that limited number of frequently used objects can be kept in the pool for further recycling.
+///
+///
+/// Notes:
+///
+/// -
+/// It is not the goal to keep all returned objects. Pool is not meant for storage. If there
+/// is no space in the pool, extra returned objects will be dropped.
+///
+/// -
+/// It is implied that if object was obtained from a pool, the caller will return it back in
+/// a relatively short time. Keeping checked out objects for long durations is ok, but
+/// reduces usefulness of pooling. Just new up your own.
+///
+///
+///
+///
+/// Not returning objects to the pool in not detrimental to the pool's work, but is a bad practice.
+/// Rationale: if there is no intent for reusing the object, do not use pool - just use "new".
+///
+///
+/// The type of objects to pool.
+/// The input factory to produce items.
+///
+/// The factory is stored for the lifetime of the pool. We will call this only when pool needs to
+/// expand. compared to "new T()", Func gives more flexibility to implementers and faster than "new T()".
+///
+/// The pool size to use.
+internal sealed class ObjectPool(Func factory, int size)
+ where T : class
+{
+ ///
+ /// The array of cached items.
+ ///
+ private readonly Element[] _items = new Element[size - 1];
+
+ ///
+ /// Storage for the pool objects. The first item is stored in a dedicated field
+ /// because we expect to be able to satisfy most requests from it.
+ ///
+ private T? _firstItem;
+
+ ///
+ /// Creates a new instance with the specified parameters.
+ ///
+ /// The input factory to produce items.
+ public ObjectPool(Func factory)
+ : this(factory, Environment.ProcessorCount * 2)
+ {
+ }
+
+ ///
+ /// Produces a instance.
+ ///
+ /// The returned item to use.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public T Allocate()
+ {
+ T? item = _firstItem;
+
+ if (item is null || item != Interlocked.CompareExchange(ref _firstItem, null, item))
+ {
+ item = AllocateSlow();
+ }
+
+ return item;
+ }
+
+ ///
+ /// Returns a given instance to the pool.
+ ///
+ /// The instance to return.
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Free(T obj)
+ {
+ if (_firstItem is null)
+ {
+ _firstItem = obj;
+ }
+ else
+ {
+ FreeSlow(obj);
+ }
+ }
+
+ ///
+ /// Allocates a new item.
+ ///
+ /// The returned item to use.
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private T AllocateSlow()
+ {
+ foreach (ref Element element in _items.AsSpan())
+ {
+ T? instance = element.Value;
+
+ if (instance is not null)
+ {
+ if (instance == Interlocked.CompareExchange(ref element.Value, null, instance))
+ {
+ return instance;
+ }
+ }
+ }
+
+ return factory();
+ }
+
+ ///
+ /// Frees a given item.
+ ///
+ /// The item to return to the pool.
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private void FreeSlow(T obj)
+ {
+ foreach (ref Element element in _items.AsSpan())
+ {
+ if (element.Value is null)
+ {
+ element.Value = obj;
+
+ break;
+ }
+ }
+ }
+
+ ///
+ /// A container for a produced item (using a wrapper to avoid covariance checks).
+ ///
+ private struct Element
+ {
+ ///
+ /// The value held at the current element.
+ ///
+ internal T? Value;
+ }
+}
\ No newline at end of file
diff --git a/src/Authoring/WinRT.SourceGenerator2/Helpers/PooledArrayBuilder{T}.cs b/src/Authoring/WinRT.SourceGenerator2/Helpers/PooledArrayBuilder{T}.cs
new file mode 100644
index 000000000..71c5e2506
--- /dev/null
+++ b/src/Authoring/WinRT.SourceGenerator2/Helpers/PooledArrayBuilder{T}.cs
@@ -0,0 +1,355 @@
+// 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 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()
+ {
+ return _writer!.WrittenSpan.ToImmutableArray();
+ }
+
+ ///
+ 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
diff --git a/src/Authoring/WinRT.SourceGenerator2/Models/CustomPropertyInfo.cs b/src/Authoring/WinRT.SourceGenerator2/Models/CustomPropertyInfo.cs
new file mode 100644
index 000000000..c2a567ed9
--- /dev/null
+++ b/src/Authoring/WinRT.SourceGenerator2/Models/CustomPropertyInfo.cs
@@ -0,0 +1,30 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Diagnostics.CodeAnalysis;
+
+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)
+{
+ ///
+ /// Gets whether the current property is an indexer property.
+ ///
+ [MemberNotNullWhen(true, nameof(FullyQualifiedIndexerTypeName))]
+ public bool IsIndexer => FullyQualifiedIndexerTypeName is not null;
+}
diff --git a/src/Authoring/WinRT.SourceGenerator2/Models/CustomPropertyProviderInfo.cs b/src/Authoring/WinRT.SourceGenerator2/Models/CustomPropertyProviderInfo.cs
new file mode 100644
index 000000000..118422c95
--- /dev/null
+++ b/src/Authoring/WinRT.SourceGenerator2/Models/CustomPropertyProviderInfo.cs
@@ -0,0 +1,30 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+namespace WindowsRuntime.SourceGenerator.Models;
+
+///
+/// A model describing a type that implements ICustomPropertyProvider.
+///
+/// The type hierarchy info for the annotated type.
+/// The custom properties to generate code for on the annotated type.
+/// Whether to use Windows.UI.Xaml projections.
+internal sealed record CustomPropertyProviderInfo(
+ HierarchyInfo TypeHierarchy,
+ EquatableArray CustomProperties,
+ bool UseWindowsUIXamlProjections)
+{
+ ///
+ /// Gets the fully qualified name of the ICustomPropertyProvider interface to use.
+ ///
+ public string FullyQualifiedCustomPropertyProviderInterfaceName => UseWindowsUIXamlProjections
+ ? "Windows.UI.Xaml.Data.ICustomPropertyProvider"
+ : "Microsoft.UI.Xaml.Data.ICustomPropertyProvider";
+
+ ///
+ /// Gets the fully qualified name of the ICustomProperty interface to use.
+ ///
+ public string FullyQualifiedCustomPropertyInterfaceName => UseWindowsUIXamlProjections
+ ? "Windows.UI.Xaml.Data.ICustomProperty"
+ : "Microsoft.UI.Xaml.Data.ICustomProperty";
+}
\ No newline at end of file
diff --git a/src/Authoring/WinRT.SourceGenerator2/Models/HierarchyInfo.cs b/src/Authoring/WinRT.SourceGenerator2/Models/HierarchyInfo.cs
new file mode 100644
index 000000000..ff56fc698
--- /dev/null
+++ b/src/Authoring/WinRT.SourceGenerator2/Models/HierarchyInfo.cs
@@ -0,0 +1,139 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+// Ported from ComputeSharp.
+// See: https://github.com/Sergio0694/ComputeSharp/blob/main/src/ComputeSharp.SourceGeneration/Models/HierarchyInfo.cs.
+
+using System;
+using Microsoft.CodeAnalysis;
+using static Microsoft.CodeAnalysis.SymbolDisplayTypeQualificationStyle;
+
+namespace WindowsRuntime.SourceGenerator.Models;
+
+///
+/// A model describing the hierarchy info for a specific type.
+///
+/// The fully qualified metadata name for the current type.
+/// Gets the namespace for the current type.
+/// Gets the sequence of type definitions containing the current type.
+internal sealed partial record HierarchyInfo(string FullyQualifiedMetadataName, string Namespace, EquatableArray Hierarchy)
+{
+ ///
+ /// Creates a new instance from a given .
+ ///
+ /// The input instance to gather info for.
+ /// A instance describing .
+ public static HierarchyInfo From(INamedTypeSymbol typeSymbol)
+ {
+ using PooledArrayBuilder hierarchy = new();
+
+ for (INamedTypeSymbol? parent = typeSymbol;
+ parent is not null;
+ parent = parent.ContainingType)
+ {
+ hierarchy.Add(new TypeInfo(
+ parent.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat),
+ parent.TypeKind,
+ parent.IsRecord));
+ }
+
+ return new(
+ typeSymbol.GetFullyQualifiedMetadataName(),
+ typeSymbol.ContainingNamespace.ToDisplayString(new(typeQualificationStyle: NameAndContainingTypesAndNamespaces)),
+ hierarchy.ToImmutable());
+ }
+
+ ///
+ /// Writes syntax for the current hierarchy into a target writer.
+ ///
+ /// The type of state to pass to callbacks.
+ /// The input state to pass to callbacks.
+ /// The target instance to write text to.
+ /// A list of base types to add to the generated type, if any.
+ /// The callbacks to use to write members into the declared type.
+ public void WriteSyntax(
+ T state,
+ IndentedTextWriter writer,
+ ReadOnlySpan baseTypes,
+ ReadOnlySpan> memberCallbacks)
+ {
+ // Write the generated file header
+ writer.WriteLine("// ");
+ writer.WriteLine("#pragma warning disable");
+ writer.WriteLine();
+
+ // Declare the namespace, if needed
+ if (Namespace.Length > 0)
+ {
+ writer.WriteLine($"namespace {Namespace}");
+ writer.WriteLine("{");
+ writer.IncreaseIndent();
+ }
+
+ // Declare all the opening types until the inner-most one
+ for (int i = Hierarchy.Length - 1; i >= 0; i--)
+ {
+ writer.WriteLine($$"""/// """);
+ writer.Write($$"""partial {{Hierarchy[i].GetTypeKeyword()}} {{Hierarchy[i].QualifiedName}}""");
+
+ // Add any base types, if needed
+ if (i == 0 && !baseTypes.IsEmpty)
+ {
+ writer.Write(" : ");
+ writer.WriteInitializationExpressions(baseTypes, static (item, writer) => writer.Write(item));
+ writer.WriteLine();
+ }
+ else
+ {
+ writer.WriteLine();
+ }
+
+ writer.WriteLine($$"""{""");
+ writer.IncreaseIndent();
+ }
+
+ // Generate all nested members
+ writer.WriteLineSeparatedMembers(memberCallbacks, (callback, writer) => callback(state, writer));
+
+ // Close all scopes and reduce the indentation
+ for (int i = 0; i < Hierarchy.Length; i++)
+ {
+ writer.DecreaseIndent();
+ writer.WriteLine("}");
+ }
+
+ // Close the namespace scope as well, if needed
+ if (Namespace.Length > 0)
+ {
+ writer.DecreaseIndent();
+ writer.WriteLine("}");
+ }
+ }
+
+ ///
+ /// Gets the fully qualified type name for the current instance.
+ ///
+ /// The fully qualified type name for the current instance.
+ public string GetFullyQualifiedTypeName()
+ {
+ using PooledArrayBuilder fullyQualifiedTypeName = new();
+
+ fullyQualifiedTypeName.AddRange("global::".AsSpan());
+
+ if (Namespace.Length > 0)
+ {
+ fullyQualifiedTypeName.AddRange(Namespace.AsSpan());
+ fullyQualifiedTypeName.Add('.');
+ }
+
+ fullyQualifiedTypeName.AddRange(Hierarchy[^1].QualifiedName.AsSpan());
+
+ for (int i = Hierarchy.Length - 2; i >= 0; i--)
+ {
+ fullyQualifiedTypeName.Add('.');
+ fullyQualifiedTypeName.AddRange(Hierarchy[i].QualifiedName.AsSpan());
+ }
+
+ return fullyQualifiedTypeName.ToString();
+ }
+}
\ No newline at end of file
diff --git a/src/Authoring/WinRT.SourceGenerator2/Models/TypeInfo.cs b/src/Authoring/WinRT.SourceGenerator2/Models/TypeInfo.cs
new file mode 100644
index 000000000..b7575e999
--- /dev/null
+++ b/src/Authoring/WinRT.SourceGenerator2/Models/TypeInfo.cs
@@ -0,0 +1,36 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+// Ported from ComputeSharp.
+// See: https://github.com/Sergio0694/ComputeSharp/blob/main/src/ComputeSharp.SourceGeneration/Models/TypeInfo.cs.
+
+using System.Diagnostics.CodeAnalysis;
+using Microsoft.CodeAnalysis;
+
+namespace WindowsRuntime.SourceGenerator.Models;
+
+///
+/// A model describing a type info in a type hierarchy.
+///
+/// The qualified name for the type.
+/// The type of the type in the hierarchy.
+/// Whether the type is a record type.
+internal sealed record TypeInfo(string QualifiedName, TypeKind Kind, bool IsRecord)
+{
+ ///
+ /// Gets the keyword for the current type kind.
+ ///
+ /// The keyword for the current type kind.
+ [SuppressMessage("Style", "IDE0072", Justification = "These are the only relevant cases for type hierarchies.")]
+ public string GetTypeKeyword()
+ {
+ return Kind switch
+ {
+ TypeKind.Struct when IsRecord => "record struct",
+ TypeKind.Struct => "struct",
+ TypeKind.Interface => "interface",
+ TypeKind.Class when IsRecord => "record",
+ _ => "class"
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/Authoring/WinRT.SourceGenerator2/TypeMapAssemblyTargetGenerator.Execute.cs b/src/Authoring/WinRT.SourceGenerator2/TypeMapAssemblyTargetGenerator.Execute.cs
index aba7fa179..eee46f15b 100644
--- a/src/Authoring/WinRT.SourceGenerator2/TypeMapAssemblyTargetGenerator.Execute.cs
+++ b/src/Authoring/WinRT.SourceGenerator2/TypeMapAssemblyTargetGenerator.Execute.cs
@@ -11,7 +11,7 @@
namespace WindowsRuntime.SourceGenerator;
///
-public partial class TypeMapAssemblyTargetGenerator : IIncrementalGenerator
+public partial class TypeMapAssemblyTargetGenerator
{
///
/// Generation methods for .
diff --git a/src/Authoring/WinRT.SourceGenerator2/WinRT.SourceGenerator2.csproj b/src/Authoring/WinRT.SourceGenerator2/WinRT.SourceGenerator2.csproj
index 7485b93f6..72da65c21 100644
--- a/src/Authoring/WinRT.SourceGenerator2/WinRT.SourceGenerator2.csproj
+++ b/src/Authoring/WinRT.SourceGenerator2/WinRT.SourceGenerator2.csproj
@@ -69,6 +69,12 @@
$(SolutionDir)WinRT.Runtime\key.snk
+
+
+
+
+
+
diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props
index 5b1aa5b5c..3d0a6d9dc 100644
--- a/src/Directory.Packages.props
+++ b/src/Directory.Packages.props
@@ -32,6 +32,7 @@
+
diff --git a/src/Tests/FunctionalTests/ClassActivation/ClassActivation.csproj b/src/Tests/FunctionalTests/ClassActivation/ClassActivation.csproj
index f9e366e40..72b523a37 100644
--- a/src/Tests/FunctionalTests/ClassActivation/ClassActivation.csproj
+++ b/src/Tests/FunctionalTests/ClassActivation/ClassActivation.csproj
@@ -6,12 +6,14 @@
x86;x64
win-x86;win-x64
$(MSBuildProjectDirectory)\..\PublishProfiles\win-$(Platform).pubxml
+ true
+
diff --git a/src/Tests/FunctionalTests/ClassActivation/Program.cs b/src/Tests/FunctionalTests/ClassActivation/Program.cs
index 69b6db59a..1779b7782 100644
--- a/src/Tests/FunctionalTests/ClassActivation/Program.cs
+++ b/src/Tests/FunctionalTests/ClassActivation/Program.cs
@@ -3,7 +3,9 @@
using System.Runtime.InteropServices.Marshalling;
using TestComponent;
using TestComponentCSharp;
+using Windows.UI.Xaml.Data;
using WindowsRuntime.InteropServices;
+using WindowsRuntime.Xaml;
CustomDisposableTest customDisposableTest = new();
customDisposableTest.Dispose();
@@ -71,6 +73,28 @@
}
}
+TestCustomPropertyProvider testCustomPropertyProvider = new();
+
+unsafe
+{
+ void* testCustomPropertyProviderUnknownPtr = WindowsRuntimeMarshal.ConvertToUnmanaged(testCustomPropertyProvider);
+ void* customPropertyProviderPtr = null;
+
+ try
+ {
+ // We should be able to get an 'ICustomPropertyProvider' interface pointer
+ Marshal.ThrowExceptionForHR(Marshal.QueryInterface(
+ pUnk: (nint)customPropertyProviderPtr,
+ iid: new Guid("7C925755-3E48-42B4-8677-76372267033F"),
+ ppv: out *(nint*)&customPropertyProviderPtr));
+ }
+ finally
+ {
+ WindowsRuntimeMarshal.Free(testCustomPropertyProviderUnknownPtr);
+ WindowsRuntimeMarshal.Free(customPropertyProviderPtr);
+ }
+}
+
sealed class TestComposable : Composable
{
}
@@ -93,6 +117,22 @@ partial interface IClassicComAction
void Invoke();
}
+[GeneratedCustomPropertyProvider]
+sealed partial class TestCustomPropertyProvider : ICustomPropertyProvider
+{
+ public string Text => "Hello";
+
+ public int Number { get; set; }
+
+ public int this[string key]
+ {
+ get => 0;
+ set { }
+ }
+
+ public static string Info { get; set; }
+}
+
/*
// new RCW / Factory activation
var instance = new Class();
diff --git a/src/Tests/SourceGenerator2Test/Extensions/ReferenceAssembliesExtensions.cs b/src/Tests/SourceGenerator2Test/Extensions/ReferenceAssembliesExtensions.cs
new file mode 100644
index 000000000..79836a3ad
--- /dev/null
+++ b/src/Tests/SourceGenerator2Test/Extensions/ReferenceAssembliesExtensions.cs
@@ -0,0 +1,30 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System;
+using System.IO;
+using Microsoft.CodeAnalysis.Testing;
+
+namespace WindowsRuntime.SourceGenerator;
+
+///
+/// Extensions for the type.
+///
+internal static class ReferenceAssembliesExtensions
+{
+ ///
+ /// The lazy-loaded instance for .NET 10 assemblies.
+ ///
+ private static readonly Lazy Net100 = new(static () => new(
+ targetFramework: "net10.0",
+ referenceAssemblyPackage: new PackageIdentity("Microsoft.NETCore.App.Ref", "10.0.1"),
+ referenceAssemblyPath: Path.Combine("ref", "net10.0")));
+
+ extension(ReferenceAssemblies.Net)
+ {
+ ///
+ /// Gets the value for .NET 10 reference assemblies.
+ ///
+ public static ReferenceAssemblies Net100 => Net100.Value; // TODO: remove when https://github.com/dotnet/roslyn-sdk/issues/1233 is resolved
+ }
+}
\ No newline at end of file
diff --git a/src/Tests/SourceGenerator2Test/Helpers/CSharpAnalyzerTest{TAnalyzer}.cs b/src/Tests/SourceGenerator2Test/Helpers/CSharpAnalyzerTest{TAnalyzer}.cs
new file mode 100644
index 000000000..016020c7e
--- /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.Net100;
+ test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(WindowsRuntimeObject).Assembly.Location));
+
+ return test.RunAsync(CancellationToken.None);
+ }
+}
\ No newline at end of file
diff --git a/src/Tests/SourceGenerator2Test/Helpers/CSharpGeneratorTest{TGenerator}.cs b/src/Tests/SourceGenerator2Test/Helpers/CSharpGeneratorTest{TGenerator}.cs
new file mode 100644
index 000000000..1c70650f2
--- /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, languageVersion, out Compilation compilation, out ImmutableArray diagnostics);
+
+ // 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,
+ LanguageVersion languageVersion,
+ out Compilation compilation,
+ out ImmutableArray diagnostics)
+ {
+ 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
diff --git a/src/Tests/SourceGenerator2Test/Properties/AssemblyInfo.cs b/src/Tests/SourceGenerator2Test/Properties/AssemblyInfo.cs
new file mode 100644
index 000000000..1152878de
--- /dev/null
+++ b/src/Tests/SourceGenerator2Test/Properties/AssemblyInfo.cs
@@ -0,0 +1,6 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+[assembly: Parallelize]
\ No newline at end of file
diff --git a/src/Tests/SourceGenerator2Test/SourceGenerator2Test.csproj b/src/Tests/SourceGenerator2Test/SourceGenerator2Test.csproj
new file mode 100644
index 000000000..baf75d37f
--- /dev/null
+++ b/src/Tests/SourceGenerator2Test/SourceGenerator2Test.csproj
@@ -0,0 +1,18 @@
+
+
+ net10.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Tests/SourceGenerator2Test/Test_CustomPropertyProviderGenerator.cs b/src/Tests/SourceGenerator2Test/Test_CustomPropertyProviderGenerator.cs
new file mode 100644
index 000000000..3a10b898b
--- /dev/null
+++ b/src/Tests/SourceGenerator2Test/Test_CustomPropertyProviderGenerator.cs
@@ -0,0 +1,42 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Threading.Tasks;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using WindowsRuntime.SourceGenerator.Tests.Helpers;
+
+namespace WindowsRuntime.SourceGenerator.Tests;
+
+[TestClass]
+public class Test_CustomPropertyProviderGenerator
+{
+ [TestMethod]
+ public async Task SimpleShader_ComputeShader()
+ {
+ const string source = """
+ using WindowsRuntime.Xaml;
+
+ namespace MyNamespace;
+
+ [GeneratedCustomPropertyProvider]
+ public partial class MyClass
+ {
+ public string Name => "";
+
+ public int Age { get; set; }
+
+ public int this[int index]
+ {
+ get => 0;
+ set { }
+ }
+ }
+ """;
+
+ const string result = """"
+
+ """";
+
+ CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyClass.g.cs", result));
+ }
+}
\ No newline at end of file
diff --git a/src/Tests/SourceGenerator2Test/Test_GeneratedCustomPropertyProviderTargetTypeAnalyzer.cs b/src/Tests/SourceGenerator2Test/Test_GeneratedCustomPropertyProviderTargetTypeAnalyzer.cs
new file mode 100644
index 000000000..07550f0b6
--- /dev/null
+++ b/src/Tests/SourceGenerator2Test/Test_GeneratedCustomPropertyProviderTargetTypeAnalyzer.cs
@@ -0,0 +1,78 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System.Threading.Tasks;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using WindowsRuntime.SourceGenerator.Diagnostics;
+using WindowsRuntime.SourceGenerator.Tests.Helpers;
+
+namespace WindowsRuntime.SourceGenerator.Tests;
+
+[TestClass]
+public class Test_GeneratedCustomPropertyProviderTargetTypeAnalyzer
+{
+ [TestMethod]
+ [DataRow("class")]
+ [DataRow("struct")]
+ public async Task ValidTargetType_DoesNotWarn(string modifier)
+ {
+ string source = $$"""
+ using WindowsRuntime.Xaml;
+
+ [GeneratedCustomPropertyProvider]
+ public partial {{modifier}} MyType;
+ """;
+
+ await CSharpAnalyzerTest.VerifyAnalyzerAsync(source);
+ }
+
+ [TestMethod]
+ [DataRow("abstract class")]
+ [DataRow("static class")]
+ [DataRow("static struct")]
+ [DataRow("ref struct")]
+ public async Task InvalidTargetType_Warns(string modifiers)
+ {
+ string source = $$"""
+ using WindowsRuntime.Xaml;
+
+ [{|CSWINRT2000:GeneratedCustomPropertyProvider|}]
+ public {{modifiers}} MyType;
+ """;
+
+ await CSharpAnalyzerTest.VerifyAnalyzerAsync(source);
+ }
+
+ [TestMethod]
+ [DataRow("class")]
+ [DataRow("struct")]
+ public async Task TypeNotPartial_Warns(string modifier)
+ {
+ string source = $$"""
+ using WindowsRuntime.Xaml;
+
+ [{|CSWINRT2001:GeneratedCustomPropertyProvider|}]
+ public {{modifier}} MyType;
+ """;
+
+ await CSharpAnalyzerTest.VerifyAnalyzerAsync(source);
+ }
+
+ [TestMethod]
+ [DataRow("class")]
+ [DataRow("struct")]
+ public async Task TypeNotInPartialTypeHierarchy_Warns(string modifier)
+ {
+ string source = $$"""
+ using WindowsRuntime.Xaml;
+
+ public class ParentType
+ {
+ [{|CSWINRT2001:GeneratedCustomPropertyProvider|}]
+ public partial {{modifier}} MyType;
+ }
+ """;
+
+ await CSharpAnalyzerTest.VerifyAnalyzerAsync(source);
+ }
+}
\ No newline at end of file
diff --git a/src/WinRT.Runtime2/ABI/WindowsRuntime.InteropServices/Bindables/IBindableIReadOnlyListAdapter.cs b/src/WinRT.Runtime2/ABI/WindowsRuntime.InteropServices/Bindables/BindableIReadOnlyListAdapter.cs
similarity index 94%
rename from src/WinRT.Runtime2/ABI/WindowsRuntime.InteropServices/Bindables/IBindableIReadOnlyListAdapter.cs
rename to src/WinRT.Runtime2/ABI/WindowsRuntime.InteropServices/Bindables/BindableIReadOnlyListAdapter.cs
index f7cfe892a..b05abe7fb 100644
--- a/src/WinRT.Runtime2/ABI/WindowsRuntime.InteropServices/Bindables/IBindableIReadOnlyListAdapter.cs
+++ b/src/WinRT.Runtime2/ABI/WindowsRuntime.InteropServices/Bindables/BindableIReadOnlyListAdapter.cs
@@ -87,11 +87,7 @@ static BindableIReadOnlyListAdapterInterfaceEntriesImpl()
///
/// A custom implementation for .
///
-[Obsolete(WindowsRuntimeConstants.PrivateImplementationDetailObsoleteMessage,
- DiagnosticId = WindowsRuntimeConstants.PrivateImplementationDetailObsoleteDiagnosticId,
- UrlFormat = WindowsRuntimeConstants.CsWinRTDiagnosticsUrlFormat)]
-[EditorBrowsable(EditorBrowsableState.Never)]
-public sealed unsafe class BindableIReadOnlyListAdapterComWrappersMarshallerAttribute : WindowsRuntimeComWrappersMarshallerAttribute
+internal sealed unsafe class BindableIReadOnlyListAdapterComWrappersMarshallerAttribute : WindowsRuntimeComWrappersMarshallerAttribute
{
///
public override void* GetOrCreateComInterfaceForObject(object value)
diff --git a/src/WinRT.Runtime2/InteropServices/Bindables/BindableIReadOnlyListAdapter.cs b/src/WinRT.Runtime2/InteropServices/Bindables/BindableIReadOnlyListAdapter.cs
index 23b2862b0..eba5a8f97 100644
--- a/src/WinRT.Runtime2/InteropServices/Bindables/BindableIReadOnlyListAdapter.cs
+++ b/src/WinRT.Runtime2/InteropServices/Bindables/BindableIReadOnlyListAdapter.cs
@@ -15,6 +15,7 @@ namespace WindowsRuntime.InteropServices;
/// still uses "IReadOnlyList" in its name to match the naming convention of adapter types matching .NET type names.
///
[WindowsRuntimeManagedOnlyType]
+[ABI.WindowsRuntime.InteropServices.BindableIReadOnlyListAdapterComWrappersMarshaller]
[Obsolete(WindowsRuntimeConstants.PrivateImplementationDetailObsoleteMessage,
DiagnosticId = WindowsRuntimeConstants.PrivateImplementationDetailObsoleteDiagnosticId,
UrlFormat = WindowsRuntimeConstants.CsWinRTDiagnosticsUrlFormat)]
diff --git a/src/WinRT.Runtime2/Xaml.Attributes/GeneratedCustomPropertyProvider.cs b/src/WinRT.Runtime2/Xaml.Attributes/GeneratedCustomPropertyProvider.cs
new file mode 100644
index 000000000..1ac293ad4
--- /dev/null
+++ b/src/WinRT.Runtime2/Xaml.Attributes/GeneratedCustomPropertyProvider.cs
@@ -0,0 +1,56 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System;
+
+namespace WindowsRuntime.Xaml;
+
+///
+/// An attribute used to indicate the properties which are bindable, for XAML (WinUI) scenarios.
+///
+///
+/// This attribute will cause binding code to be generated to provide support via the Windows.UI.Xaml.Data.ICustomPropertyProvider
+/// and Microsoft.UI.Xaml.Data.ICustomPropertyProvider infrastructure, for the specified properties on the annotated type.
+///
+///
+///
+[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, Inherited = false, AllowMultiple = false)]
+public sealed class GeneratedCustomPropertyProviderAttribute : Attribute
+{
+ ///
+ /// Creates a new instance.
+ ///
+ ///
+ /// Using this constructor will mark all public properties as bindable.
+ ///
+ public GeneratedCustomPropertyProviderAttribute()
+ {
+ }
+
+ ///
+ /// Creates a new instance with the specified parameters.
+ ///
+ /// The name of the non-indexer public properties to mark as bindable.
+ /// The parameter type of the indexer public properties to mark as bindable.
+ public GeneratedCustomPropertyProviderAttribute(string[] propertyNames, Type[] indexerPropertyTypes)
+ {
+ PropertyNames = propertyNames;
+ IndexerPropertyTypes = indexerPropertyTypes;
+ }
+
+ ///
+ /// Gets the name of the non-indexer public properties to mark as bindable.
+ ///
+ ///
+ /// If , all public properties are considered bindable.
+ ///
+ public string[]? PropertyNames { get; }
+
+ ///
+ /// Gets the parameter type of the indexer public properties to mark as bindable.
+ ///
+ ///
+ /// If , all indexer public properties are considered bindable.
+ ///
+ public Type[]? IndexerPropertyTypes { get; }
+}
\ No newline at end of file
diff --git a/src/cswinrt.slnx b/src/cswinrt.slnx
index 7db15b0a3..fcdc36297 100644
--- a/src/cswinrt.slnx
+++ b/src/cswinrt.slnx
@@ -185,6 +185,7 @@
+