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 @@ +