Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
2e99471
Add GeneratedCustomPropertyProviderAttribute class
Sergio0694 Dec 8, 2025
b1b3759
Rename IBindableIReadOnlyListAdapter to BindableIReadOnlyListAdapter
Sergio0694 Dec 8, 2025
976f8fc
Update marshaller attribute for BindableIReadOnlyListAdapter
Sergio0694 Dec 8, 2025
47314a2
Remove IIncrementalGenerator implementation
Sergio0694 Dec 8, 2025
659a476
Add SyntaxExtensions with helper methods for Roslyn
Sergio0694 Dec 8, 2025
a7622b4
Add extension for attribute analysis with config options
Sergio0694 Dec 8, 2025
ac97abd
Add MemberDeclarationSyntaxExtensions for partial checks
Sergio0694 Dec 8, 2025
f4fd8ab
WIP
Sergio0694 Dec 8, 2025
cecbaa6
Add generic object pool implementation
Sergio0694 Dec 8, 2025
9204686
Add PooledArrayBuilder<T> helper for pooled arrays
Sergio0694 Dec 8, 2025
07a9d9b
Add IndentedTextWriter helper and update PooledArrayBuilder
Sergio0694 Dec 8, 2025
821ee26
Comment out bindable custom property generation logic
Sergio0694 Dec 8, 2025
c969b25
Add IsDefaultOrEmpty and Length properties to EquatableArray
Sergio0694 Dec 8, 2025
cfab2d1
Add ITypeSymbol extension methods for metadata names
Sergio0694 Dec 8, 2025
214ee6c
Add IndentedTextWriter extension methods
Sergio0694 Dec 8, 2025
2200f3b
Add HierarchyInfo and TypeInfo models
Sergio0694 Dec 8, 2025
e3b36de
Disallow ICustomPropertyProvider on ref types
Sergio0694 Dec 8, 2025
2aa2fa4
Add SkipNullValues extension for IncrementalValuesProvider
Sergio0694 Dec 9, 2025
0092358
Add EnumerateAllMembers extension for ITypeSymbol
Sergio0694 Dec 9, 2025
c6e4352
Add 'this' modifier to SkipNullValues extension method
Sergio0694 Dec 9, 2025
3cdf30b
Add methods for fully qualified symbol names
Sergio0694 Dec 9, 2025
ec9da7c
WIP
Sergio0694 Dec 9, 2025
51191e0
Refactor ToImmutable to use ToImmutableArray
Sergio0694 Dec 9, 2025
017cc14
Refactor GetCustomPropertyInfo for clarity and filtering
Sergio0694 Dec 9, 2025
a4d7eda
Refactor CustomPropertyProvider models and implementation
Sergio0694 Dec 9, 2025
fb40484
Refactor CustomPropertyProviderGenerator emit logic
Sergio0694 Dec 9, 2025
24000bd
Add CanBeBoxed property to ITypeSymbolExtensions
Sergio0694 Dec 10, 2025
2c74f20
Add IsIndexer property to CustomPropertyInfo record
Sergio0694 Dec 10, 2025
710c1a6
Skip static indexer properties in generator
Sergio0694 Dec 10, 2025
ee9cddc
Add code generation for ICustomProperty implementation types
Sergio0694 Dec 10, 2025
65ee305
Add ICustomPropertyProvider test and XAML references
Sergio0694 Dec 10, 2025
b66ae97
Add diagnostic descriptors for custom property provider
Sergio0694 Dec 11, 2025
6a9d8c6
Add analyzer release tracking and test implementation
Sergio0694 Dec 11, 2025
3783382
Add analyzer for GeneratedCustomPropertyProvider targets
Sergio0694 Dec 11, 2025
9117d59
Add analyzer for missing ICustomPropertyProvider interface
Sergio0694 Dec 11, 2025
7f134b1
Add SourceGenerator2Test project to solution
Sergio0694 Dec 17, 2025
dc0cd5c
Suppress CS8620 warning in generator file
Sergio0694 Dec 17, 2025
b7b78d3
Set VersionOverride for CSharp.Workspaces package
Sergio0694 Dec 17, 2025
0ca9b0e
Add MSTest package to dependencies
Sergio0694 Dec 17, 2025
8f6601d
Add AssemblyInfo with Parallelize attribute to tests
Sergio0694 Dec 17, 2025
001f450
Add CSharpGeneratorTest helper for source generator tests
Sergio0694 Dec 17, 2025
2929203
Add test for CustomPropertyProviderGenerator
Sergio0694 Dec 17, 2025
f26704a
Add custom CSharpAnalyzerTest helper for analyzer tests
Sergio0694 Dec 17, 2025
f1ce103
Refactor RunGenerator parameter order and default
Sergio0694 Dec 17, 2025
dd62b22
Add tests for GeneratedCustomPropertyProvider analyzer
Sergio0694 Dec 17, 2025
2ce2c78
Add .NET 10 reference assemblies support for tests
Sergio0694 Dec 17, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Shipped.md
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
--------|----------|----------|-------

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -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;

/// <inheritdoc cref="CustomPropertyProviderGenerator"/>
public partial class CustomPropertyProviderGenerator
{
/// <summary>
/// Generation methods for <see cref="CustomPropertyProviderGenerator"/>.
/// </summary>
private static class Execute
{
/// <summary>
/// Checks whether a target node needs the <c>ICustomPropertyProvider</c> implementation.
/// </summary>
/// <param name="node">The target <see cref="SyntaxNode"/> instance to check.</param>
/// <param name="token">The cancellation token for the operation.</param>
/// <returns>Whether <paramref name="node"/> is a valid target for the <c>ICustomPropertyProvider</c> implementation.</returns>
[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'.
Copy link

Copilot AI Dec 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Corrected spelling of 'generated' to 'generate' in the comment.

Suggested change
// We can only generated the 'ICustomPropertyProvider' implementation if the type is 'partial'.
// We can only generate the 'ICustomPropertyProvider' implementation if the type is 'partial'.

Copilot uses AI. Check for mistakes.
// Additionally, all parent type declarations must also be 'partial', for generation to work.
if (!((MemberDeclarationSyntax)node).IsPartialAndWithinPartialTypeHierarchy)
{
return false;
}

return true;
}

/// <summary>
/// Tries to get the <see cref="CustomPropertyProviderInfo"/> instance for a given annotated symbol.
/// </summary>
/// <param name="context">The <see cref="GeneratorAttributeSyntaxContextWithOptions"/> value to use.</param>
/// <param name="token">The cancellation token for the operation.</param>
/// <returns>The resulting <see cref="CustomPropertyProviderInfo"/> instance, if processed successfully.</returns>
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<CustomPropertyInfo> customProperties = GetCustomPropertyInfo(typeSymbol, context.Attributes[0], token);

token.ThrowIfCancellationRequested();

return new(
TypeHierarchy: typeHierarchy,
CustomProperties: customProperties,
UseWindowsUIXamlProjections: useWindowsUIXamlProjections);
}

/// <summary>
/// Gets the <see cref="CustomPropertyInfo"/> values for all applicable properties of a target type.
/// </summary>
/// <param name="typeSymbol">The annotated type.</param>
/// <param name="attribute">The attribute to trigger generation.</param>
/// <param name="token">The cancellation token for the operation.</param>
/// <returns>The resulting <see cref="CustomPropertyInfo"/> values for <paramref name="typeSymbol"/>.</returns>
private static EquatableArray<CustomPropertyInfo> 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> 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();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using Microsoft.CodeAnalysis;
using WindowsRuntime.SourceGenerator.Models;

namespace WindowsRuntime.SourceGenerator;

/// <summary>
/// A generator to emit <c>ICustomPropertyProvider</c> implementations for annotated types.
/// </summary>
[Generator]
public sealed partial class CustomPropertyProviderGenerator : IIncrementalGenerator
{
/// <inheritdoc/>
public void Initialize(IncrementalGeneratorInitializationContext context)
{
// Gather the info on all types annotated with '[GeneratedCustomPropertyProvider]'.
IncrementalValuesProvider<CustomPropertyProviderInfo> 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);
}
}
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// A diagnostic analyzer that validates when <c>[GeneratedCustomPropertyProvider]</c> is used but no interface is available.
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class GeneratedCustomPropertyProviderNoAvailableInterfaceTypeAnalyzer : DiagnosticAnalyzer
{
/// <inheritdoc/>
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = [DiagnosticDescriptors.GeneratedCustomPropertyProviderNoAvailableInterfaceType];

/// <inheritdoc/>
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);
});
}
}
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// A diagnostic analyzer that validates target types for <c>[GeneratedCustomPropertyProvider]</c>.
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class GeneratedCustomPropertyProviderTargetTypeAnalyzer : DiagnosticAnalyzer
{
/// <inheritdoc/>
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = [
DiagnosticDescriptors.GeneratedCustomPropertyProviderInvalidTargetType,
DiagnosticDescriptors.GeneratedCustomPropertyProviderMissingPartialModifier];

/// <inheritdoc/>
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);
});
}
}
Loading
Loading