diff --git a/build/AzurePipelineTemplates/CsWinRT-Test-Steps.yml b/build/AzurePipelineTemplates/CsWinRT-Test-Steps.yml index c89ba88e43..a9b2308f8a 100644 --- a/build/AzurePipelineTemplates/CsWinRT-Test-Steps.yml +++ b/build/AzurePipelineTemplates/CsWinRT-Test-Steps.yml @@ -22,6 +22,18 @@ steps: --no-build testRunTitle: Unit Tests +# Run Source Generator 2 Tests + - task: DotNetCoreCLI@2 + displayName: Run Source Generator 2 Tests + condition: and(succeeded(), or(eq(variables['BuildPlatform'], 'x86'), eq(variables['BuildPlatform'], 'x64'))) + inputs: + command: test + projects: 'src/Tests/SourceGenerator2Test/SourceGenerator2Test.csproj' + arguments: > + /p:platform=$(BuildPlatform);configuration=$(BuildConfiguration) + --no-build + testRunTitle: Source Generator 2 Tests + # Run Host Tests - task: CmdLine@2 displayName: Run Host Tests diff --git a/src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Shipped.md b/src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Shipped.md new file mode 100644 index 0000000000..d6343c4c03 --- /dev/null +++ b/src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Shipped.md @@ -0,0 +1,17 @@ +; 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 +CSWINRT2003 | WindowsRuntime.SourceGenerator | Error | Existing 'ICustomPropertyProvider' member implementation +CSWINRT2004 | WindowsRuntime.SourceGenerator | Error | Null property name in '[GeneratedCustomPropertyProvider]' +CSWINRT2005 | WindowsRuntime.SourceGenerator | Error | Null indexer type in '[GeneratedCustomPropertyProvider]' +CSWINRT2006 | WindowsRuntime.SourceGenerator | Error | Property name not found for '[GeneratedCustomPropertyProvider]' +CSWINRT2007 | WindowsRuntime.SourceGenerator | Error | Indexer type not found for '[GeneratedCustomPropertyProvider]' +CSWINRT2008 | WindowsRuntime.SourceGenerator | Error | Static indexer for '[GeneratedCustomPropertyProvider]' \ No newline at end of file diff --git a/src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Unshipped.md b/src/Authoring/WinRT.SourceGenerator2/AnalyzerReleases.Unshipped.md new file mode 100644 index 0000000000..7634792eb7 --- /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/AuthoringExportTypesGenerator.Execute.cs b/src/Authoring/WinRT.SourceGenerator2/AuthoringExportTypesGenerator.Execute.cs index 0eb151d997..a2b215bf52 100644 --- a/src/Authoring/WinRT.SourceGenerator2/AuthoringExportTypesGenerator.Execute.cs +++ b/src/Authoring/WinRT.SourceGenerator2/AuthoringExportTypesGenerator.Execute.cs @@ -112,7 +112,7 @@ public static void EmitManagedExports(SourceProductionContext context, Authoring return; } - IndentedTextWriter writer = new(literalLength: 0, formattedCount: 0); + IndentedTextWriter writer = new(literalLength: 0, formattedCount: 0); // TODO: adjust the literal length // Emit the '[WindowsRuntimeComponentAssemblyExportsType]' attribute so other tooling (including this same generator) // can reliably find the generated export types from other assemblies, which is needed when merging activation factories. @@ -134,14 +134,14 @@ namespace ABI.{{info.AssemblyName.EscapeIdentifierName()}}; /// /// Contains the managed exports for activating types from the current project in an authoring scenario. /// - """); + """, isMultiline: true); // Emit the standard generated attributes, and also mark the type as hidden, since it's generated as a public type writer.WriteGeneratedAttributes(nameof(AuthoringExportTypesGenerator), useFullyQualifiedTypeNames: false); writer.WriteLine($$""" [EditorBrowsable(EditorBrowsableState.Never)] public static unsafe class ManagedExports - """); + """, isMultiline: true); // Indent via a block, as we'll also need to emit custom logic for the activation factory using (writer.WriteBlock()) @@ -155,7 +155,7 @@ public static unsafe class ManagedExports /// The class identifier that is associated with an activatable runtime class. /// The resulting pointer to the activation factory that corresponds with the class specified by . public static void* GetActivationFactory(ReadOnlySpan activatableClassId) - """); + """, isMultiline: true); using (writer.WriteBlock()) { @@ -167,7 +167,7 @@ public static unsafe class ManagedExports static extern void* AuthoringGetActivationFactory( [UnsafeAccessorType("ABI.{info.AssemblyName.EscapeIdentifierName()}.ManagedExports, WinRT.Authoring")] object? _, ReadOnlySpan activatableClassId); - """); + """, isMultiline: true); } // Emit the specialized code to redirect the activation @@ -181,7 +181,7 @@ public static unsafe class ManagedExports void* activationFactory = AuthoringGetActivationFactory(null, activatableClassId); return activationFactory ?? ReferencedManagedExports.GetActivationFactory(activatableClassId); - """); + """, isMultiline: true); } else if (info.Options.MergeReferencedActivationFactories) { @@ -198,7 +198,7 @@ public static nint GetActivationFactory(string activatableClassId) { return (nint)GetActivationFactory(activatableClassId.AsSpan()); } - """); + """, isMultiline: true); } // Emit a helper type with the logic for merging activaton factories, if needed @@ -209,7 +209,7 @@ public static nint GetActivationFactory(string activatableClassId) /// /// Contains the logic for activating types from transitively referenced Windows Runtime components. /// - """); + """, isMultiline: true); writer.WriteGeneratedAttributes(nameof(AuthoringExportTypesGenerator), useFullyQualifiedTypeNames: false); writer.WriteLine("file static unsafe class ReferencedManagedExports"); @@ -222,7 +222,7 @@ public static nint GetActivationFactory(string activatableClassId) /// The class identifier that is associated with an activatable runtime class. /// The resulting pointer to the activation factory that corresponds with the class specified by . public static void* GetActivationFactory(ReadOnlySpan activatableClassId) - """); + """, isMultiline: true); using (writer.WriteBlock()) { @@ -239,7 +239,7 @@ public static nint GetActivationFactory(string activatableClassId) { return activationFactory; } - """); + """, isMultiline: true); } // No match across the referenced factories, we can't do anything else @@ -290,7 +290,7 @@ namespace ABI.{info.AssemblyName.EscapeIdentifierName()}; /// /// Contains the native exports for activating types from the current project in an authoring scenario. /// - """); + """, isMultiline: true); // Emit the attributes to mark the code as generated, and to exclude it from code coverage as well. We also use a // file-scoped P/Invoke, so we don't take a dependency on private implementation detail types from 'WinRT.Runtime.dll'. @@ -383,7 +383,7 @@ file static class WindowsRuntimeImports [SupportedOSPlatform("windows6.2")] public static extern char* WindowsGetStringRawBuffer(HSTRING @string, uint* length); } - """); + """, isMultiline: true); context.AddSource("NativeExports.g.cs", writer.ToStringAndClear()); } diff --git a/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Emit.cs b/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Emit.cs new file mode 100644 index 0000000000..c63a1979e9 --- /dev/null +++ b/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Emit.cs @@ -0,0 +1,463 @@ +// 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) + { + const int ApproximateTypeDeclarationLength = 2048; + + // Approximate a close enough starting length to reduce copies + int approximateLiteralLength = ApproximateTypeDeclarationLength + (ApproximateTypeDeclarationLength * info.CustomProperties.Length); + + IndentedTextWriter writer = new(literalLength: approximateLiteralLength, formattedCount: 0); + + // Emit the implementation on the annotated type + info.TypeHierarchy.WriteSyntax( + state: info, + writer: ref writer, + baseTypes: [info.FullyQualifiedCustomPropertyProviderInterfaceName], + memberCallbacks: [ + WriteCustomPropertyProviderType, + WriteCustomPropertyProviderGetCustomProperty, + WriteCustomPropertyProviderGetIndexedProperty, + WriteCustomPropertyProviderGetStringRepresentation]); + + // Emit the additional property implementation types, if needed + WriteCustomPropertyImplementationTypes(info, ref writer); + + // Add the source file for the annotated type + context.AddSource($"{info.TypeHierarchy.FullyQualifiedMetadataName}.g.cs", writer.ToStringAndClear()); + } + + /// + /// Writes the ICustomPropertyProvider.Type implementation. + /// + /// + /// + private static void WriteCustomPropertyProviderType(CustomPropertyProviderInfo info, ref IndentedTextWriter writer) + { + writer.WriteLine("/// "); + writer.WriteGeneratedAttributes(nameof(CustomPropertyProviderGenerator), includeNonUserCodeAttributes: false); + writer.WriteLine($"global::System.Type {info.FullyQualifiedCustomPropertyProviderInterfaceName}.Type => typeof({info.TypeHierarchy.Hierarchy[0].QualifiedName});"); + } + + /// + /// Writes the ICustomPropertyProvider.GetCustomProperty implementation. + /// + /// + /// + private static void WriteCustomPropertyProviderGetCustomProperty(CustomPropertyProviderInfo info, ref IndentedTextWriter writer) + { + writer.WriteLine("/// "); + writer.WriteGeneratedAttributes(nameof(CustomPropertyProviderGenerator), includeNonUserCodeAttributes: false); + writer.WriteLine($""" + {info.FullyQualifiedCustomPropertyInterfaceName} {info.FullyQualifiedCustomPropertyProviderInterfaceName}.GetCustomProperty(string name) + """, isMultiline: true); + + 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"); + writer.WriteLine("{"); + writer.IncreaseIndent(); + + // 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"); + writer.DecreaseIndent(); + writer.WriteLine("};"); + } + } + + /// + /// Writes the ICustomPropertyProvider.GetIndexedProperty implementation. + /// + /// + /// + private static void WriteCustomPropertyProviderGetIndexedProperty(CustomPropertyProviderInfo info, ref IndentedTextWriter writer) + { + writer.WriteLine("/// "); + writer.WriteGeneratedAttributes(nameof(CustomPropertyProviderGenerator), includeNonUserCodeAttributes: false); + writer.WriteLine($""" + {info.FullyQualifiedCustomPropertyInterfaceName} {info.FullyQualifiedCustomPropertyProviderInterfaceName}.GetIndexedProperty(string name, global::System.Type type) + """, isMultiline: true); + + 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; + } + + string implementationTypeName = $"{info.TypeHierarchy.Hierarchy[0].QualifiedName}_this_{propertyInfo.FullyQualifiedIndexerTypeName.Replace("global::", "").EscapeIdentifierName()}"; + + // If we have a match, return the cached property implementation for the current indexer + writer.WriteLine(skipIfPresent: true); + writer.WriteLine($$""" + if (type == typeof({{propertyInfo.FullyQualifiedIndexerTypeName}})) + { + return global::WindowsRuntime.Xaml.Generated.{{implementationTypeName}}.Instance; + } + """, isMultiline: true); + } + + // If there's no matching property, just return 'null' + writer.WriteLine(skipIfPresent: true); + writer.WriteLine("return null;"); + } + } + + /// + /// Writes the ICustomPropertyProvider.GetStringRepresentation implementation. + /// + /// + /// + private static void WriteCustomPropertyProviderGetStringRepresentation(CustomPropertyProviderInfo info, ref IndentedTextWriter writer) + { + writer.WriteLine("/// "); + writer.WriteGeneratedAttributes(nameof(CustomPropertyProviderGenerator), includeNonUserCodeAttributes: false); + writer.WriteLine($$""" + string {{info.FullyQualifiedCustomPropertyProviderInterfaceName}}.GetStringRepresentation() + { + return ToString(); + } + """, isMultiline: true); + } + + /// + /// Writes the ICustomProperty implementation types. + /// + /// + /// + private static void WriteCustomPropertyImplementationTypes(CustomPropertyProviderInfo info, ref IndentedTextWriter writer) + { + // If we have no custom properties, we don't need to emit any additional code + if (info.CustomProperties.IsEmpty) + { + 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 namespaces we can use with simple names + writer.WriteLine("using global::System;"); + writer.WriteLine("using global::System.CodeDom.Compiler;"); + writer.WriteLine("using global::System.Diagnostics;"); + writer.WriteLine("using global::System.Diagnostics.CodeAnalysis;"); + writer.WriteLine($"using global::{info.FullyQualifiedCustomPropertyProviderInterfaceName.Replace(".ICustomPropertyProvider", "")};"); + 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, ref writer); + } + else + { + WriteNonIndexedCustomPropertyImplementationType(info, propertyInfo, ref writer); + } + } + } + } + + /// + /// Writes a single non indexed ICustomProperty implementation type. + /// + /// + /// The input instance for the property to generate the implementation type for. + /// + private static void WriteNonIndexedCustomPropertyImplementationType(CustomPropertyProviderInfo info, CustomPropertyInfo propertyInfo, ref IndentedTextWriter writer) + { + string userTypeName = info.TypeHierarchy.GetFullyQualifiedTypeName().Replace("global::", ""); + string implementationTypeName = $"{info.TypeHierarchy.Hierarchy[0].QualifiedName}_{propertyInfo.Name}"; + + // Emit a type as follows: + // + // file sealed class : ICustomProperty + writer.WriteLine($""" + /// + /// The implementation for . + /// + """, isMultiline: true); + writer.WriteGeneratedAttributes(nameof(CustomPropertyProviderGenerator), useFullyQualifiedTypeNames: false); + writer.WriteLine($"file sealed class {implementationTypeName} : ICustomProperty"); + + 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); + + writer.WriteLine(); + + // Emit 'GetValue' depending on whether the property is readable and whether it's static + if (propertyInfo.CanRead && propertyInfo.IsStatic) + { + writer.WriteLine($$""" + /// + public object GetValue(object target) + { + return {{info.TypeHierarchy.GetFullyQualifiedTypeName()}}.{{propertyInfo.Name}}; + } + """, isMultiline: true); + } + else if (propertyInfo.CanRead) + { + writer.WriteLine($$""" + /// + public object GetValue(object target) + { + return (({{info.TypeHierarchy.GetFullyQualifiedTypeName()}})target).{{propertyInfo.Name}}; + } + """, isMultiline: true); + } + else + { + writer.WriteLine(""" + /// + public object GetValue(object target) + { + throw new NotSupportedException(); + } + """, isMultiline: true); + } + + writer.WriteLine(); + + // Emit 'SetValue' depending on whether the property is writable and whether it's static + if (propertyInfo.CanWrite && propertyInfo.IsStatic) + { + writer.WriteLine($$""" + /// + public void SetValue(object target, object value) + { + {{info.TypeHierarchy.GetFullyQualifiedTypeName()}}.{{propertyInfo.Name}} = ({{propertyInfo.FullyQualifiedTypeName}})value; + } + """, isMultiline: true); + } + else if (propertyInfo.CanWrite) + { + writer.WriteLine($$""" + /// + public void SetValue(object target, object value) + { + (({{info.TypeHierarchy.GetFullyQualifiedTypeName()}})target).{{propertyInfo.Name}} = ({{propertyInfo.FullyQualifiedTypeName}})value; + } + """, isMultiline: true); + } + else + { + writer.WriteLine(""" + /// + public void SetValue(object target, object value) + { + throw new NotSupportedException(); + } + """, isMultiline: true); + } + + // Emit the property accessors (indexer properties can only be instance properties) + writer.WriteLine(); + 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); + } + } + + /// + /// 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, ref IndentedTextWriter writer) + { + string userTypeName = info.TypeHierarchy.GetFullyQualifiedTypeName().Replace("global::", ""); + string indexerTypeName = propertyInfo.FullyQualifiedIndexerTypeName!.Replace("global::", ""); + string implementationTypeName = $"{info.TypeHierarchy.Hierarchy[0].QualifiedName}_this_{indexerTypeName.EscapeIdentifierName()}"; + + // Emit the implementation type, same as above + writer.WriteLine($""" + /// + /// The implementation for 's indexer. + /// + """, isMultiline: true); + writer.WriteGeneratedAttributes(nameof(CustomPropertyProviderGenerator), useFullyQualifiedTypeNames: false); + writer.WriteLine($"file sealed class {implementationTypeName} : ICustomProperty"); + + using (writer.WriteBlock()) + { + // Emit all 'ICustomProperty' members for a normal property, 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 => "this"; + + /// + public Type Type => typeof({{propertyInfo.FullyQualifiedTypeName}}); + """, isMultiline: true); + + // This is an indexed property, so non indexed ones will always throw + writer.WriteLine(); + writer.WriteLine($$""" + /// + public object GetValue(object target) + { + throw new NotSupportedException(); + } + + /// + public void SetValue(object target, object value) + { + throw new NotSupportedException(); + } + """, isMultiline: true); + + // Emit the indexer property accessors, conditionally based on CanRead/CanWrite + writer.WriteLine(); + + if (propertyInfo.CanRead) + { + writer.WriteLine($$""" + /// + public object GetIndexedValue(object target, object index) + { + return (({{info.TypeHierarchy.GetFullyQualifiedTypeName()}})target)[({{propertyInfo.FullyQualifiedIndexerTypeName}})index]; + } + """, isMultiline: true); + } + else + { + writer.WriteLine(""" + /// + public object GetIndexedValue(object target, object index) + { + throw new NotSupportedException(); + } + """, isMultiline: true); + } + + writer.WriteLine(); + + if (propertyInfo.CanWrite) + { + writer.WriteLine($$""" + /// + public void SetIndexedValue(object target, object value, object index) + { + (({{info.TypeHierarchy.GetFullyQualifiedTypeName()}})target)[({{propertyInfo.FullyQualifiedIndexerTypeName}})index] = ({{propertyInfo.FullyQualifiedTypeName}})value; + } + """, isMultiline: true); + } + else + { + writer.WriteLine(""" + /// + public void SetIndexedValue(object target, object value, object index) + { + throw new NotSupportedException(); + } + """, isMultiline: true); + } + } + } + } +} \ 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 0000000000..1063c06976 --- /dev/null +++ b/src/Authoring/WinRT.SourceGenerator2/CustomPropertyProviderGenerator.Execute.cs @@ -0,0 +1,197 @@ +// 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 generate the 'ICustomPropertyProvider' implementation if the type is 'partial'. + // Additionally, all parent type declarations must also be 'partial', for generation to work. + if (!((MemberDeclarationSyntax)node).IsPartialAndWithinPartialTypeHierarchy) + { + 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.GetCsWinRTUseWindowsUIXamlProjections(); + + token.ThrowIfCancellationRequested(); + + string customPropertyProviderMetadataName = useWindowsUIXamlProjections + ? "Windows.UI.Xaml.Data.ICustomPropertyProvider" + : "Microsoft.UI.Xaml.Data.ICustomPropertyProvider"; + + // Make sure that the target interface types are available. This is mostly because when UWP XAML projections + // are not used, the target project must be referencing the WinUI package to get the right interface type. + // If we can't find it, we just stop here. A separate diagnostic analyzer will emit the right diagnostic. + if (context.SemanticModel.Compilation.GetTypeByMetadataName(customPropertyProviderMetadataName) is not { } customPropertyProviderType) + { + 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; + } + + // If the annotated type already implements 'ICustomPropertyProvider' and has or inherits any + // member implementations, we can't generate the implementation. A separate diagnostic analyzer + // will emit the right diagnostic for this case. + if (typeSymbol.HasAnyImplementedMembersForInterface(customPropertyProviderType)) + { + return null; + } + + token.ThrowIfCancellationRequested(); + + // Get the type hierarchy (needed to correctly generate sources for nested types too) + HierarchyInfo typeHierarchy = HierarchyInfo.From(typeSymbol); + + 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 0000000000..e342f16129 --- /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/GeneratedCustomPropertyProviderAttributeArgumentAnalyzer.cs b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/GeneratedCustomPropertyProviderAttributeArgumentAnalyzer.cs new file mode 100644 index 0000000000..c9e8c04693 --- /dev/null +++ b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/GeneratedCustomPropertyProviderAttributeArgumentAnalyzer.cs @@ -0,0 +1,203 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace WindowsRuntime.SourceGenerator.Diagnostics; + +/// +/// A diagnostic analyzer that validates the arguments of [GeneratedCustomPropertyProvider] when +/// using the constructor taking explicit property names and indexer types. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class GeneratedCustomPropertyProviderAttributeArgumentAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = [ + DiagnosticDescriptors.GeneratedCustomPropertyProviderNullPropertyName, + DiagnosticDescriptors.GeneratedCustomPropertyProviderNullIndexerType, + DiagnosticDescriptors.GeneratedCustomPropertyProviderPropertyNameNotFound, + DiagnosticDescriptors.GeneratedCustomPropertyProviderIndexerTypeNotFound, + DiagnosticDescriptors.GeneratedCustomPropertyProviderStaticIndexer]; + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static context => + { + // Get the '[GeneratedCustomPropertyProvider]' symbol + if (context.Compilation.GetTypeByMetadataName("WindowsRuntime.Xaml.GeneratedCustomPropertyProviderAttribute") is not { } attributeType) + { + return; + } + + context.RegisterSymbolAction(context => + { + // Only classes and structs can be targets of the attribute + if (context.Symbol is not INamedTypeSymbol { TypeKind: TypeKind.Class or TypeKind.Struct } typeSymbol) + { + return; + } + + // Get the attribute instance, if present + if (!typeSymbol.TryGetAttributeWithType(attributeType, out AttributeData? attribute)) + { + return; + } + + // Only validate when using the constructor with explicit property names and indexer types + if (attribute.ConstructorArguments is not [ + { Kind: TypedConstantKind.Array, Values: var typedPropertyNames }, + { Kind: TypedConstantKind.Array, Values: var typedIndexerTypes }]) + { + return; + } + + // Validate all property name arguments + foreach (TypedConstant typedName in typedPropertyNames) + { + // Check that we have a valid 'string' value + if (typedName.Value is not string propertyName) + { + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.GeneratedCustomPropertyProviderNullPropertyName, + typeSymbol.Locations.FirstOrDefault(), + typeSymbol)); + + continue; + } + + // Check whether any public, non-override, non-partial-impl, non-indexer property has this name + if (!HasAccessiblePropertyWithName(typeSymbol, propertyName)) + { + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.GeneratedCustomPropertyProviderPropertyNameNotFound, + typeSymbol.Locations.FirstOrDefault(), + propertyName, + typeSymbol)); + } + } + + // Validate all indexer type arguments + foreach (TypedConstant typedType in typedIndexerTypes) + { + // Check that we have a valid 'Type' value (the parameter type of the target indexer) + if (typedType.Value is not ITypeSymbol indexerType) + { + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.GeneratedCustomPropertyProviderNullIndexerType, + typeSymbol.Locations.FirstOrDefault(), + typeSymbol)); + + continue; + } + + // Check whether there's a static indexer with a matching parameter type (which wouldn't be usable) + if (HasStaticIndexerWithParameterType(typeSymbol, indexerType)) + { + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.GeneratedCustomPropertyProviderStaticIndexer, + typeSymbol.Locations.FirstOrDefault(), + indexerType, + typeSymbol)); + } + else if (!HasAccessibleIndexerWithParameterType(typeSymbol, indexerType)) + { + // No matching instance or static indexer at all + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.GeneratedCustomPropertyProviderIndexerTypeNotFound, + typeSymbol.Locations.FirstOrDefault(), + indexerType, + typeSymbol)); + } + } + }, SymbolKind.NamedType); + }); + } + + /// + /// Checks whether a type has an accessible (public, non-override, non-indexer) property with a given name. + /// + /// The type to inspect. + /// The property name to look for. + /// Whether a matching property exists. + private static bool HasAccessiblePropertyWithName(INamedTypeSymbol typeSymbol, string propertyName) + { + foreach (ISymbol symbol in typeSymbol.EnumerateAllMembers()) + { + // Filter to public properties that we might care about (we ignore indexers here) + if (symbol is not IPropertySymbol { DeclaredAccessibility: Accessibility.Public, IsOverride: false, PartialDefinitionPart: null, IsIndexer: false } property) + { + continue; + } + + if (property.Name.Equals(propertyName, StringComparison.Ordinal)) + { + return true; + } + } + + return false; + } + + /// + /// Checks whether a type has an accessible (public, non-override, non-static, single-parameter) indexer + /// with a parameter matching a given type. + /// + /// The type to inspect. + /// The indexer parameter type to look for. + /// Whether a matching indexer exists. + private static bool HasAccessibleIndexerWithParameterType(INamedTypeSymbol typeSymbol, ITypeSymbol indexerType) + { + foreach (ISymbol symbol in typeSymbol.EnumerateAllMembers()) + { + // Same filtering as above, but we also exclude static members, and we only look for indexers + if (symbol is not IPropertySymbol { DeclaredAccessibility: Accessibility.Public, IsOverride: false, PartialDefinitionPart: null, IsIndexer: true, IsStatic: false } property) + { + continue; + } + + // Check that we have a single parameter, that matches theone we're looking for + if (property.Parameters is [{ } parameter] && SymbolEqualityComparer.Default.Equals(parameter.Type, indexerType)) + { + return true; + } + } + + return false; + } + + /// + /// Checks whether a type has a static indexer with a parameter matching a given type. + /// This is used to provide a more specific diagnostic when the indexer exists but is static. + /// + /// The type to inspect. + /// The indexer parameter type to look for. + /// Whether a matching static indexer exists. + private static bool HasStaticIndexerWithParameterType(INamedTypeSymbol typeSymbol, ITypeSymbol indexerType) + { + foreach (ISymbol symbol in typeSymbol.EnumerateAllMembers()) + { + // Same filtering as above, but this time including static properties + if (symbol is not IPropertySymbol { DeclaredAccessibility: Accessibility.Public, IsOverride: false, PartialDefinitionPart: null, IsIndexer: true, IsStatic: true } property) + { + continue; + } + + // Validate the parameter type (same as above) + if (property.Parameters is [{ } parameter] && SymbolEqualityComparer.Default.Equals(parameter.Type, indexerType)) + { + return true; + } + } + + return false; + } +} diff --git a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/GeneratedCustomPropertyProviderExistingMemberImplementationAnalyzer.cs b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/GeneratedCustomPropertyProviderExistingMemberImplementationAnalyzer.cs new file mode 100644 index 0000000000..7b2cfaefc8 --- /dev/null +++ b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/GeneratedCustomPropertyProviderExistingMemberImplementationAnalyzer.cs @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace WindowsRuntime.SourceGenerator.Diagnostics; + +/// +/// A diagnostic analyzer that validates when [GeneratedCustomPropertyProvider] is used on a type +/// that already has or inherits implementations for one or more ICustomPropertyProvider members. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class GeneratedCustomPropertyProviderExistingMemberImplementationAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = [DiagnosticDescriptors.GeneratedCustomPropertyProviderExistingMemberImplementation]; + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static context => + { + // Get the '[GeneratedCustomPropertyProvider]' symbol + if (context.Compilation.GetTypeByMetadataName("WindowsRuntime.Xaml.GeneratedCustomPropertyProviderAttribute") is not { } attributeType) + { + return; + } + + // Try to get any 'ICustomPropertyProvider' symbol + INamedTypeSymbol? windowsUIXamlCustomPropertyProviderType = context.Compilation.GetTypeByMetadataName("Windows.UI.Xaml.Data.ICustomPropertyProvider"); + INamedTypeSymbol? microsoftUIXamlCustomPropertyProviderType = context.Compilation.GetTypeByMetadataName("Microsoft.UI.Xaml.Data.ICustomPropertyProvider"); + + // If we have neither, a different analyzer will handle that + if (windowsUIXamlCustomPropertyProviderType is null && microsoftUIXamlCustomPropertyProviderType is null) + { + return; + } + + // Resolve the '[GeneratedCode]' attribute type, used to skip generated implementations + INamedTypeSymbol generatedCodeAttributeType = context.Compilation.GetTypeByMetadataName("System.CodeDom.Compiler.GeneratedCodeAttribute")!; + + context.RegisterSymbolAction(context => + { + // Only classes and structs can be targets of the attribute + 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; + } + + // Helper to check whether a symbol has any generated interface members that are not emitted by this generator. his covers + // both manually implemented members and those produced by other generators, and explicitly interface implementations too. + static bool HasNonGeneratedImplementedMembers( + INamedTypeSymbol typeSymbol, + INamedTypeSymbol? interfaceType, + INamedTypeSymbol generatedCodeAttributeType) + { + if (interfaceType is null) + { + return false; + } + + ISymbol[] implementedMembers = [.. typeSymbol.EnumerateImplementedMembersForInterface(interfaceType)]; + + // Check that we have at least one implemented member, and that not all of them are produced by our generator + return implementedMembers.Length > 0 && !implementedMembers.AreAllImplementedByGenerator(generatedCodeAttributeType, nameof(CustomPropertyProviderGenerator)); + } + + // Check whether the type has or inherits any 'ICustomPropertyProvider' member implementations. + // We don't warn if all implementations are generated by 'CustomPropertyProviderGenerator' itself, + // so that only user-authored implementations trigger this diagnostic. + if (HasNonGeneratedImplementedMembers(typeSymbol, windowsUIXamlCustomPropertyProviderType, generatedCodeAttributeType) || + HasNonGeneratedImplementedMembers(typeSymbol, microsoftUIXamlCustomPropertyProviderType, generatedCodeAttributeType)) + { + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.GeneratedCustomPropertyProviderExistingMemberImplementation, + typeSymbol.Locations.FirstOrDefault(), + typeSymbol)); + } + }, SymbolKind.NamedType); + }); + } +} \ 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 0000000000..e9b2327bfb --- /dev/null +++ b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/GeneratedCustomPropertyProviderNoAvailableInterfaceTypeAnalyzer.cs @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace WindowsRuntime.SourceGenerator.Diagnostics; + +/// +/// A diagnostic analyzer that validates when [GeneratedCustomPropertyProvider] is used but no interface is available. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class GeneratedCustomPropertyProviderNoAvailableInterfaceTypeAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = [DiagnosticDescriptors.GeneratedCustomPropertyProviderNoAvailableInterfaceType]; + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static context => + { + // Get the '[GeneratedCustomPropertyProvider]' symbol + if (context.Compilation.GetTypeByMetadataName("WindowsRuntime.Xaml.GeneratedCustomPropertyProviderAttribute") is not { } attributeType) + { + return; + } + + // Try to get any 'ICustomPropertyProvider' symbol + INamedTypeSymbol? windowsUIXamlCustomPropertyProviderType = context.Compilation.GetTypeByMetadataName("Windows.UI.Xaml.Data.ICustomPropertyProvider"); + INamedTypeSymbol? microsoftUIXamlCustomPropertyProviderType = context.Compilation.GetTypeByMetadataName("Microsoft.UI.Xaml.Data.ICustomPropertyProvider"); + + // If we have either of them, we'll never need to report any diagnostics + if (windowsUIXamlCustomPropertyProviderType is not null || microsoftUIXamlCustomPropertyProviderType is not null) + { + return; + } + + context.RegisterSymbolAction(context => + { + // Only classes and structs can be targets of the attribute + if (context.Symbol is not INamedTypeSymbol { TypeKind: TypeKind.Class or TypeKind.Struct } typeSymbol) + { + return; + } + + // Emit a diagnostic if the type has the attribute, as it can't be used now + if (typeSymbol.HasAttributeWithType(attributeType)) + { + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.GeneratedCustomPropertyProviderNoAvailableInterfaceType, + typeSymbol.Locations.FirstOrDefault(), + typeSymbol)); + } + }, SymbolKind.NamedType); + }); + } +} \ No newline at end of file diff --git a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/GeneratedCustomPropertyProviderTargetTypeAnalyzer.cs b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/GeneratedCustomPropertyProviderTargetTypeAnalyzer.cs new file mode 100644 index 0000000000..38e3078fbc --- /dev/null +++ b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/Analyzers/GeneratedCustomPropertyProviderTargetTypeAnalyzer.cs @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace WindowsRuntime.SourceGenerator.Diagnostics; + +/// +/// A diagnostic analyzer that validates target types for [GeneratedCustomPropertyProvider]. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class GeneratedCustomPropertyProviderTargetTypeAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = [ + DiagnosticDescriptors.GeneratedCustomPropertyProviderInvalidTargetType, + DiagnosticDescriptors.GeneratedCustomPropertyProviderMissingPartialModifier]; + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static context => + { + // Get the '[GeneratedCustomPropertyProvider]' symbol + if (context.Compilation.GetTypeByMetadataName("WindowsRuntime.Xaml.GeneratedCustomPropertyProviderAttribute") is not { } attributeType) + { + return; + } + + context.RegisterSymbolAction(context => + { + // Only classes and structs can be targets of the attribute + if (context.Symbol is not INamedTypeSymbol { TypeKind: TypeKind.Class or TypeKind.Struct } typeSymbol) + { + return; + } + + // Immediately bail if the type doesn't have the attribute + if (!typeSymbol.HasAttributeWithType(attributeType)) + { + return; + } + + // If the type is static, abstract, or 'ref', it isn't valid + if (typeSymbol.IsAbstract || typeSymbol.IsStatic || typeSymbol.IsRefLikeType) + { + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.GeneratedCustomPropertyProviderInvalidTargetType, + typeSymbol.Locations.FirstOrDefault(), + typeSymbol)); + } + + // Try to get a syntax reference for the symbol, to resolve the syntax node for it + if (typeSymbol.DeclaringSyntaxReferences.FirstOrDefault() is SyntaxReference syntaxReference) + { + SyntaxNode typeNode = syntaxReference.GetSyntax(context.CancellationToken); + + // If there's no 'partial' modifier in the type hierarchy, the target type isn't valid + if (!((MemberDeclarationSyntax)typeNode).IsPartialAndWithinPartialTypeHierarchy) + { + context.ReportDiagnostic(Diagnostic.Create( + DiagnosticDescriptors.GeneratedCustomPropertyProviderMissingPartialModifier, + typeSymbol.Locations.FirstOrDefault(), + typeSymbol)); + } + } + }, SymbolKind.NamedType); + }); + } +} \ No newline at end of file diff --git a/src/Authoring/WinRT.SourceGenerator2/Diagnostics/DiagnosticDescriptors.cs b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/DiagnosticDescriptors.cs new file mode 100644 index 0000000000..a5f73dd6ad --- /dev/null +++ b/src/Authoring/WinRT.SourceGenerator2/Diagnostics/DiagnosticDescriptors.cs @@ -0,0 +1,129 @@ +// 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 'Microsoft.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"); + + /// + /// Gets a for when [GeneratedCustomPropertyProvider] is used on a type that already implements ICustomPropertyProvider members. + /// + public static readonly DiagnosticDescriptor GeneratedCustomPropertyProviderExistingMemberImplementation = new( + id: "CSWINRT2003", + title: "Existing 'ICustomPropertyProvider' member implementation", + messageFormat: """The type '{0}' cannot use '[GeneratedCustomPropertyProvider]' because it already has or inherits implementations for one or more 'ICustomPropertyProvider' members""", + category: "WindowsRuntime.SourceGenerator", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "Types annotated with '[GeneratedCustomPropertyProvider]' must not already have or inherit implementations for any 'ICustomPropertyProvider' members, as the generator will provide them.", + helpLinkUri: "https://github.com/microsoft/CsWinRT"); + + /// + /// Gets a for when a property name is specified in [GeneratedCustomPropertyProvider]. + /// + public static readonly DiagnosticDescriptor GeneratedCustomPropertyProviderNullPropertyName = new( + id: "CSWINRT2004", + title: "Null property name in '[GeneratedCustomPropertyProvider]'", + messageFormat: """A null property name was specified in '[GeneratedCustomPropertyProvider]' on type '{0}'""", + category: "WindowsRuntime.SourceGenerator", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "Property names specified in '[GeneratedCustomPropertyProvider]' must not be null.", + helpLinkUri: "https://github.com/microsoft/CsWinRT"); + + /// + /// Gets a for when a indexer type is specified in [GeneratedCustomPropertyProvider]. + /// + public static readonly DiagnosticDescriptor GeneratedCustomPropertyProviderNullIndexerType = new( + id: "CSWINRT2005", + title: "Null indexer type in '[GeneratedCustomPropertyProvider]'", + messageFormat: """A null indexer type was specified in '[GeneratedCustomPropertyProvider]' on type '{0}'""", + category: "WindowsRuntime.SourceGenerator", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "Indexer types specified in '[GeneratedCustomPropertyProvider]' must not be null.", + helpLinkUri: "https://github.com/microsoft/CsWinRT"); + + /// + /// Gets a for when a property name in [GeneratedCustomPropertyProvider] doesn't match any accessible property. + /// + public static readonly DiagnosticDescriptor GeneratedCustomPropertyProviderPropertyNameNotFound = new( + id: "CSWINRT2006", + title: "Property name not found for '[GeneratedCustomPropertyProvider]'", + messageFormat: """The property name '{0}' specified in '[GeneratedCustomPropertyProvider]' on type '{1}' does not match any accessible property""", + category: "WindowsRuntime.SourceGenerator", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "Property names specified in '[GeneratedCustomPropertyProvider]' must match the name of a public, non-override, non-indexer property on the annotated type.", + helpLinkUri: "https://github.com/microsoft/CsWinRT"); + + /// + /// Gets a for when an indexer type in [GeneratedCustomPropertyProvider] doesn't match any accessible indexer. + /// + public static readonly DiagnosticDescriptor GeneratedCustomPropertyProviderIndexerTypeNotFound = new( + id: "CSWINRT2007", + title: "Indexer type not found for '[GeneratedCustomPropertyProvider]'", + messageFormat: """The indexer type '{0}' specified in '[GeneratedCustomPropertyProvider]' on type '{1}' does not match any accessible indexer""", + category: "WindowsRuntime.SourceGenerator", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "Indexer types specified in '[GeneratedCustomPropertyProvider]' must match the parameter type of a public, non-override, non-static, single-parameter indexer on the annotated type.", + helpLinkUri: "https://github.com/microsoft/CsWinRT"); + + /// + /// Gets a for when an indexer type in [GeneratedCustomPropertyProvider] matches a static indexer. + /// + public static readonly DiagnosticDescriptor GeneratedCustomPropertyProviderStaticIndexer = new( + id: "CSWINRT2008", + title: "Static indexer for '[GeneratedCustomPropertyProvider]'", + messageFormat: """The indexer type '{0}' specified in '[GeneratedCustomPropertyProvider]' on type '{1}' matches a static indexer, which is not supported""", + category: "WindowsRuntime.SourceGenerator", + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "Indexers used with '[GeneratedCustomPropertyProvider]' must be instance indexers, not static indexers.", + helpLinkUri: "https://github.com/microsoft/CsWinRT"); +} \ No newline at end of file diff --git a/src/Authoring/WinRT.SourceGenerator2/Extensions/ISymbolExtensions.cs b/src/Authoring/WinRT.SourceGenerator2/Extensions/ISymbolExtensions.cs index 117fcd2cdf..59441b65cb 100644 --- a/src/Authoring/WinRT.SourceGenerator2/Extensions/ISymbolExtensions.cs +++ b/src/Authoring/WinRT.SourceGenerator2/Extensions/ISymbolExtensions.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using Microsoft.CodeAnalysis; @@ -16,6 +17,24 @@ internal static class ISymbolExtensions /// The input instance. extension(ISymbol symbol) { + /// + /// Gets the fully qualified name for a given symbol. + /// + /// The fully qualified name for . + public string GetFullyQualifiedName() + { + return symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + } + + /// + /// Gets the fully qualified name for a given symbol, including nullability annotations + /// + /// The fully qualified name for . + public string GetFullyQualifiedNameWithNullabilityAnnotations() + { + return symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat.AddMiscellaneousOptions(SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier)); + } + /// /// Checks whether a type has an attribute with a specified type. /// @@ -59,4 +78,56 @@ public bool IsAccessibleFromCompilationAssembly(Compilation compilation) return compilation.IsSymbolAccessibleWithin(symbol, compilation.Assembly); } } + + /// The input sequence of instances. + extension(IEnumerable symbols) + { + /// + /// Checks whether all symbols in the sequence are generated by a specific generator. + /// + /// The for [GeneratedCode]. + /// The generator name to match against the first constructor argument of [GeneratedCode]. + /// Whether all symbols in have a [GeneratedCode] attribute matching . + public bool AreAllImplementedByGenerator(INamedTypeSymbol generatedCodeAttributeType, string generatorName) + { + bool hasAtLeastOneImplementedMember = false; + + foreach (ISymbol symbol in symbols) + { + hasAtLeastOneImplementedMember = true; + + ISymbol currentSymbol = symbol; + + // First, try to get the attribute directly on the symbol (this should work in most cases) + if (!currentSymbol.TryGetAttributeWithType(generatedCodeAttributeType, out AttributeData? attributeData)) + { + // If the current symbol is some property accessor, move to the associated property instead. + // This is also needed because usually '[GeneratedCode]' would only be on the property. + if (currentSymbol is IMethodSymbol { AssociatedSymbol: { } associatedSymbol }) + { + currentSymbol = associatedSymbol; + } + + // Stop if the symbol doesn't have '[GeneratedCode]' on it. Note, we want to traverse the symbol + // hierarchy and find the attribute on the closest parent (so we can also handle e.g. accessors). + for (ISymbol? parentSymbol = currentSymbol; parentSymbol is not null; parentSymbol = parentSymbol.ContainingSymbol) + { + if (parentSymbol.TryGetAttributeWithType(generatedCodeAttributeType, out attributeData)) + { + break; + } + } + } + + // Check that the symbol was specifically generated by the target generator + if (attributeData is not { ConstructorArguments: [{ Value: string name }, ..] } || name != generatorName) + { + return false; + } + } + + // If the input set is empty, we want to return 'false' to indicate that there are no implemented members + return hasAtLeastOneImplementedMember; + } + } } \ No newline at end of file diff --git a/src/Authoring/WinRT.SourceGenerator2/Extensions/ITypeSymbolExtensions.cs b/src/Authoring/WinRT.SourceGenerator2/Extensions/ITypeSymbolExtensions.cs new file mode 100644 index 0000000000..30cd258ea2 --- /dev/null +++ b/src/Authoring/WinRT.SourceGenerator2/Extensions/ITypeSymbolExtensions.cs @@ -0,0 +1,153 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +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; + } + } + } + + /// + /// Enumerates all members of a specified interface that have implementations on the given . + /// + /// The interface type to enumerate member implementations for. + /// The sequence of all implemented members for on . + public IEnumerable EnumerateImplementedMembersForInterface(INamedTypeSymbol interfaceType) + { + if (!symbol.AllInterfaces.Contains(interfaceType, SymbolEqualityComparer.Default)) + { + yield break; + } + + foreach (ISymbol member in interfaceType.GetMembers()) + { + if (symbol.FindImplementationForInterfaceMember(member) is { } implementation) + { + yield return implementation; + } + } + } + + /// + /// Checks whether the given implements a specified interface + /// and has or inherits implementations for any of its members. + /// + /// The interface type to check for member implementations. + /// Whether has any implemented members for . + public bool HasAnyImplementedMembersForInterface(INamedTypeSymbol interfaceType) + { + return symbol.EnumerateImplementedMembersForInterface(interfaceType).Any(); + } + + /// + /// 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 0000000000..3f94b3183c --- /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; +} \ No newline at end of file diff --git a/src/Authoring/WinRT.SourceGenerator2/Extensions/IncrementalValuesProviderExtensions.cs b/src/Authoring/WinRT.SourceGenerator2/Extensions/IncrementalValuesProviderExtensions.cs new file mode 100644 index 0000000000..3c49e1f14a --- /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)!; + } +} \ No newline at end of file diff --git a/src/Authoring/WinRT.SourceGenerator2/Extensions/IndentedTextWriterExtensions.cs b/src/Authoring/WinRT.SourceGenerator2/Extensions/IndentedTextWriterExtensions.cs index 21cfeef592..a0deebb335 100644 --- a/src/Authoring/WinRT.SourceGenerator2/Extensions/IndentedTextWriterExtensions.cs +++ b/src/Authoring/WinRT.SourceGenerator2/Extensions/IndentedTextWriterExtensions.cs @@ -5,6 +5,8 @@ // See: https://github.com/Sergio0694/ComputeSharp/blob/main/src/ComputeSharp.SourceGeneration/Helpers/IndentedTextWriter.cs. using System; +using System.Collections.Generic; +using System.Linq; namespace WindowsRuntime.SourceGenerator; @@ -56,4 +58,74 @@ public static void WriteGeneratedAttributes( } } } + + /// + /// Writes a sequence of using directives, sorted correctly. + /// + /// The instance to write into. + /// The sequence of using directives to write. + public static void WriteSortedUsingDirectives(this ref IndentedTextWriter writer, IEnumerable usingDirectives) + { + // Add the System directives first, in the correct order + foreach (string usingDirective in usingDirectives.Where(static name => name.StartsWith("global::System", StringComparison.InvariantCulture)).OrderBy(static name => name)) + { + writer.WriteLine($"using {usingDirective};"); + } + + // Add the other directives, also sorted in the correct order + foreach (string usingDirective in usingDirectives.Where(static name => !name.StartsWith("global::System", StringComparison.InvariantCulture)).OrderBy(static name => name)) + { + writer.WriteLine($"using {usingDirective};"); + } + + // Leave a trailing blank line if at least one using directive has been written. + // This is so that any members will correctly have a leading blank line before. + writer.WriteLineIf(usingDirectives.Any()); + } + + /// + /// Writes a series of members separated by one line between each of them. + /// + /// The type of input items to process. + /// The instance to write into. + /// The input items to process. + /// The instance to invoke for each item. + public static void WriteLineSeparatedMembers( + this scoped ref IndentedTextWriter writer, + scoped ReadOnlySpan items, + IndentedTextWriter.Callback callback) + { + for (int i = 0; i < items.Length; i++) + { + if (i > 0) + { + writer.WriteLine(); + } + + callback(items[i], ref 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 scoped ref IndentedTextWriter writer, + scoped ReadOnlySpan items, + IndentedTextWriter.Callback callback) + { + for (int i = 0; i < items.Length; i++) + { + callback(items[i], ref 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 0000000000..cc510ce9d3 --- /dev/null +++ b/src/Authoring/WinRT.SourceGenerator2/Extensions/MemberDeclarationSyntaxExtensions.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace WindowsRuntime.SourceGenerator; + +/// +/// Extensions for member declaration syntax types. +/// +internal static class MemberDeclarationSyntaxExtensions +{ + extension(MemberDeclarationSyntax node) + { + /// + /// Gets whether the input member declaration is partial. + /// + public bool IsPartial => node.Modifiers.Any(SyntaxKind.PartialKeyword); + + /// + /// Gets whether the input member declaration is partial and + /// all of its parent type declarations are also partial. + /// + public bool IsPartialAndWithinPartialTypeHierarchy + { + get + { + // If the target node is not partial, stop immediately + if (!node.IsPartial) + { + return false; + } + + // Walk all parent type declarations, stop if any of them is not partial + foreach (SyntaxNode ancestor in node.Ancestors()) + { + if (ancestor is BaseTypeDeclarationSyntax { IsPartial: false }) + { + return false; + } + } + + return true; + } + } + } +} \ No newline at end of file diff --git a/src/Authoring/WinRT.SourceGenerator2/Extensions/SyntaxExtensions.cs b/src/Authoring/WinRT.SourceGenerator2/Extensions/SyntaxExtensions.cs new file mode 100644 index 0000000000..4af55419b0 --- /dev/null +++ b/src/Authoring/WinRT.SourceGenerator2/Extensions/SyntaxExtensions.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace WindowsRuntime.SourceGenerator; + +/// +/// Extensions for syntax types. +/// +internal static class SyntaxExtensions +{ + extension(SyntaxNode node) + { + /// + /// Determines if is of any of the specified kinds. + /// + /// The syntax kinds to test for. + /// Whether the input node is of any of the specified kinds. + public bool IsAnyKind(params ReadOnlySpan kinds) + { + foreach (SyntaxKind kind in kinds) + { + if (node.IsKind(kind)) + { + return true; + } + } + + return false; + } + } + + extension(SyntaxTokenList list) + { + /// + /// Tests whether a list contains any token of particular kinds. + /// + /// The syntax kinds to test for. + /// Whether the input list contains any of the specified kinds. + public bool ContainsAny(params ReadOnlySpan kinds) + { + foreach (SyntaxKind kind in kinds) + { + if (list.IndexOf(kind) >= 0) + { + return true; + } + } + + return false; + } + } +} \ No newline at end of file diff --git a/src/Authoring/WinRT.SourceGenerator2/Helpers/EquatableArray{T}.cs b/src/Authoring/WinRT.SourceGenerator2/Helpers/EquatableArray{T}.cs index 223d8a6b54..384a904e1f 100644 --- a/src/Authoring/WinRT.SourceGenerator2/Helpers/EquatableArray{T}.cs +++ b/src/Authoring/WinRT.SourceGenerator2/Helpers/EquatableArray{T}.cs @@ -75,19 +75,37 @@ public bool IsEmpty get => AsImmutableArray().IsEmpty; } - /// + /// + /// Gets a value indicating whether the current array is default or empty. + /// + public bool IsDefaultOrEmpty + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => AsImmutableArray().IsDefaultOrEmpty; + } + + /// + /// Gets the length of the current array. + /// + public int Length + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => AsImmutableArray().Length; + } + + /// public bool Equals(EquatableArray array) { return AsSpan().SequenceEqual(array.AsSpan()); } - /// + /// public override bool Equals([NotNullWhen(true)] object? obj) { return obj is EquatableArray array && Equals(array); } - /// + /// public override int GetHashCode() { if (_array is not T[] array) @@ -152,13 +170,13 @@ public ImmutableArray.Enumerator GetEnumerator() return AsImmutableArray().GetEnumerator(); } - /// + /// IEnumerator IEnumerable.GetEnumerator() { return ((IEnumerable)AsImmutableArray()).GetEnumerator(); } - /// + /// IEnumerator IEnumerable.GetEnumerator() { return ((IEnumerable)AsImmutableArray()).GetEnumerator(); diff --git a/src/Authoring/WinRT.SourceGenerator2/Helpers/IndentedTextWriter.cs b/src/Authoring/WinRT.SourceGenerator2/Helpers/IndentedTextWriter.cs index 64ef7c1046..840dd22cde 100644 --- a/src/Authoring/WinRT.SourceGenerator2/Helpers/IndentedTextWriter.cs +++ b/src/Authoring/WinRT.SourceGenerator2/Helpers/IndentedTextWriter.cs @@ -100,12 +100,13 @@ public void DecreaseIndent() /// Writes a block to the underlying buffer. /// /// A value to close the open block with. + [UnscopedRef] public Block WriteBlock() { WriteLine("{"); IncreaseIndent(); - return new(this); + return new(ref this); } /// @@ -142,6 +143,14 @@ public void Write(scoped ReadOnlySpan content, bool isMultiline = false) { ReadOnlySpan line = content[..newLineIndex]; + // Remove the trailing 'CR' character, if present. This ensures that the resulting + // text will be correctly normalized with 'LF' newlines, regardless of the line + // endings used in source files. In fact, this whole repo uses 'CRLF' line endings. + if (line is [.. var trim, '\r']) + { + line = trim; + } + // Write the current line (if it's empty, we can skip writing the text entirely). // This ensures that raw multiline string literals with blank lines don't have // extra whitespace at the start of those lines, which would otherwise happen. @@ -225,9 +234,14 @@ public readonly void WriteIf(bool condition, [InterpolatedStringHandlerArgument( /// Indicates whether to skip adding the line if there already is one. public void WriteLine(bool skipIfPresent = false) { - if (skipIfPresent && _handler.Text is [.., '\n', '\n']) + if (skipIfPresent) { - return; + ReadOnlySpan trimmedText = _handler.Text.TrimEnd(' '); + + if (trimmedText is [.., '\n', '\n'] or [.., '{', '\n']) + { + return; + } } _handler.AppendFormatted(DefaultNewLine); @@ -366,33 +380,31 @@ private void WriteRawText(scoped ReadOnlySpan content) /// The type of data to use. /// The input data to use to write into . /// The instance to write into. - public delegate void Callback(T value, IndentedTextWriter writer); + public delegate void Callback(T value, ref IndentedTextWriter writer); /// /// Represents an indented block that needs to be closed. /// /// The input instance to wrap. - public ref struct Block(IndentedTextWriter writer) : IDisposable + public unsafe ref struct Block(ref IndentedTextWriter writer) : IDisposable { /// /// The instance to write to. /// - private IndentedTextWriter _writer = writer; + private IndentedTextWriter* _writer = (IndentedTextWriter*)Unsafe.AsPointer(ref writer); /// public void Dispose() { - IndentedTextWriter writer = _writer; + IndentedTextWriter* writer = _writer; - _writer = default; + _writer = null; - // We check the indentation as a way of knowing if we have reset the field before. - // The field itself can't be 'null', but if we have assigned 'default' to it, then - // that field will be 'null' even though it's always set from the constructor. - if (writer._currentIndentation is not null) + // Make sure to only close the block the first time this value is disposed + if (writer is not null) { - writer.DecreaseIndent(); - writer.WriteLine("}"); + writer->DecreaseIndent(); + writer->WriteLine("}"); } } } @@ -402,26 +414,26 @@ public void Dispose() /// [EditorBrowsable(EditorBrowsableState.Never)] [InterpolatedStringHandler] - public readonly ref struct WriteInterpolatedStringHandler + public readonly unsafe ref struct WriteInterpolatedStringHandler { /// The associated to which to append. - private readonly IndentedTextWriter _writer; + private readonly IndentedTextWriter* _writer; /// Creates a handler used to append an interpolated string into a . /// The number of constant characters outside of interpolation expressions in the interpolated string. /// The number of interpolation expressions in the interpolated string. /// The associated to which to append. /// This is intended to be called only by compiler-generated code. Arguments are not validated as they'd otherwise be for members intended to be used directly. - public WriteInterpolatedStringHandler(int literalLength, int formattedCount, IndentedTextWriter writer) + public WriteInterpolatedStringHandler(int literalLength, int formattedCount, in IndentedTextWriter writer) { - _writer = writer; + _writer = (IndentedTextWriter*)Unsafe.AsPointer(ref Unsafe.AsRef(in writer)); } /// Writes the specified string to the handler. /// The string to write. public void AppendLiteral(string value) { - _writer.Write(value); + _writer->Write(value); } /// Writes the specified value to the handler. @@ -435,7 +447,7 @@ public void AppendFormatted(string? value) /// The span to write. public void AppendFormatted(scoped ReadOnlySpan value) { - _writer.Write(value); + _writer->Write(value); } /// Writes the specified value to the handler. @@ -445,7 +457,7 @@ public void AppendFormatted(T? value) { if (value is not null) { - _writer.Write(value.ToString()!); + _writer->Write(value.ToString()!); } } @@ -458,11 +470,11 @@ public void AppendFormatted(T? value, string? format) { if (value is IFormattable) { - _writer.Write(((IFormattable)value).ToString(format, CultureInfo.InvariantCulture)); + _writer->Write(((IFormattable)value).ToString(format, CultureInfo.InvariantCulture)); } else if (value is not null) { - _writer.Write(value.ToString()!); + _writer->Write(value.ToString()!); } } } @@ -484,11 +496,11 @@ public readonly ref struct WriteIfInterpolatedStringHandler /// The condition to use to decide whether or not to write content. /// A value indicating whether formatting should proceed. /// This is intended to be called only by compiler-generated code. Arguments are not validated as they'd otherwise be for members intended to be used directly. - public WriteIfInterpolatedStringHandler(int literalLength, int formattedCount, IndentedTextWriter writer, bool condition, out bool shouldAppend) + public WriteIfInterpolatedStringHandler(int literalLength, int formattedCount, in IndentedTextWriter writer, bool condition, out bool shouldAppend) { if (condition) { - _writer = new WriteInterpolatedStringHandler(literalLength, formattedCount, writer); + _writer = new WriteInterpolatedStringHandler(literalLength, formattedCount, in writer); shouldAppend = true; } diff --git a/src/Authoring/WinRT.SourceGenerator2/Helpers/ObjectPool{T}.cs b/src/Authoring/WinRT.SourceGenerator2/Helpers/ObjectPool{T}.cs new file mode 100644 index 0000000000..04f60a0894 --- /dev/null +++ b/src/Authoring/WinRT.SourceGenerator2/Helpers/ObjectPool{T}.cs @@ -0,0 +1,154 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Ported from Roslyn. +// See: https://github.com/dotnet/roslyn/blob/main/src/Dependencies/PooledObjects/ObjectPool%601.cs. + +using System; +using System.Runtime.CompilerServices; +using System.Threading; + +#pragma warning disable RS1035 + +namespace WindowsRuntime.SourceGenerator; + +/// +/// +/// Generic implementation of object pooling pattern with predefined pool size limit. The main purpose +/// is that limited number of frequently used objects can be kept in the pool for further recycling. +/// +/// +/// Notes: +/// +/// +/// It is not the goal to keep all returned objects. Pool is not meant for storage. If there +/// is no space in the pool, extra returned objects will be dropped. +/// +/// +/// It is implied that if object was obtained from a pool, the caller will return it back in +/// a relatively short time. Keeping checked out objects for long durations is ok, but +/// reduces usefulness of pooling. Just new up your own. +/// +/// +/// +/// +/// Not returning objects to the pool in not detrimental to the pool's work, but is a bad practice. +/// Rationale: if there is no intent for reusing the object, do not use pool - just use "new". +/// +/// +/// The type of objects to pool. +/// The input factory to produce items. +/// +/// The factory is stored for the lifetime of the pool. We will call this only when pool needs to +/// expand. compared to "new T()", Func gives more flexibility to implementers and faster than "new T()". +/// +/// The pool size to use. +internal sealed class ObjectPool(Func factory, int size) + where T : class +{ + /// + /// The array of cached items. + /// + private readonly Element[] _items = new Element[size - 1]; + + /// + /// Storage for the pool objects. The first item is stored in a dedicated field + /// because we expect to be able to satisfy most requests from it. + /// + private T? _firstItem; + + /// + /// Creates a new instance with the specified parameters. + /// + /// The input factory to produce items. + public ObjectPool(Func factory) + : this(factory, Environment.ProcessorCount * 2) + { + } + + /// + /// Produces a instance. + /// + /// The returned item to use. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public T Allocate() + { + T? item = _firstItem; + + if (item is null || item != Interlocked.CompareExchange(ref _firstItem, null, item)) + { + item = AllocateSlow(); + } + + return item; + } + + /// + /// Returns a given instance to the pool. + /// + /// The instance to return. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Free(T obj) + { + if (_firstItem is null) + { + _firstItem = obj; + } + else + { + FreeSlow(obj); + } + } + + /// + /// Allocates a new item. + /// + /// The returned item to use. + [MethodImpl(MethodImplOptions.NoInlining)] + private T AllocateSlow() + { + foreach (ref Element element in _items.AsSpan()) + { + T? instance = element.Value; + + if (instance is not null) + { + if (instance == Interlocked.CompareExchange(ref element.Value, null, instance)) + { + return instance; + } + } + } + + return factory(); + } + + /// + /// Frees a given item. + /// + /// The item to return to the pool. + [MethodImpl(MethodImplOptions.NoInlining)] + private void FreeSlow(T obj) + { + foreach (ref Element element in _items.AsSpan()) + { + if (element.Value is null) + { + element.Value = obj; + + break; + } + } + } + + /// + /// A container for a produced item (using a wrapper to avoid covariance checks). + /// + private struct Element + { + /// + /// The value held at the current element. + /// + internal T? Value; + } +} \ No newline at end of file diff --git a/src/Authoring/WinRT.SourceGenerator2/Helpers/PooledArrayBuilder{T}.cs b/src/Authoring/WinRT.SourceGenerator2/Helpers/PooledArrayBuilder{T}.cs new file mode 100644 index 0000000000..2fe6d5cbb8 --- /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 pooled 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 0000000000..fd58c9cdc5 --- /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; +} \ No newline at end of file diff --git a/src/Authoring/WinRT.SourceGenerator2/Models/CustomPropertyProviderInfo.cs b/src/Authoring/WinRT.SourceGenerator2/Models/CustomPropertyProviderInfo.cs new file mode 100644 index 0000000000..118422c955 --- /dev/null +++ b/src/Authoring/WinRT.SourceGenerator2/Models/CustomPropertyProviderInfo.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace WindowsRuntime.SourceGenerator.Models; + +/// +/// A model describing a type that implements ICustomPropertyProvider. +/// +/// The type hierarchy info for the annotated type. +/// The custom properties to generate code for on the annotated type. +/// Whether to use Windows.UI.Xaml projections. +internal sealed record CustomPropertyProviderInfo( + HierarchyInfo TypeHierarchy, + EquatableArray CustomProperties, + bool UseWindowsUIXamlProjections) +{ + /// + /// Gets the fully qualified name of the ICustomPropertyProvider interface to use. + /// + public string FullyQualifiedCustomPropertyProviderInterfaceName => UseWindowsUIXamlProjections + ? "Windows.UI.Xaml.Data.ICustomPropertyProvider" + : "Microsoft.UI.Xaml.Data.ICustomPropertyProvider"; + + /// + /// Gets the fully qualified name of the ICustomProperty interface to use. + /// + public string FullyQualifiedCustomPropertyInterfaceName => UseWindowsUIXamlProjections + ? "Windows.UI.Xaml.Data.ICustomProperty" + : "Microsoft.UI.Xaml.Data.ICustomProperty"; +} \ No newline at end of file diff --git a/src/Authoring/WinRT.SourceGenerator2/Models/HierarchyInfo.cs b/src/Authoring/WinRT.SourceGenerator2/Models/HierarchyInfo.cs new file mode 100644 index 0000000000..e044ca0b97 --- /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, + scoped ref IndentedTextWriter writer, + scoped ReadOnlySpan baseTypes, + scoped 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, ref writer) => writer.Write(item)); + writer.WriteLine(); + } + else + { + writer.WriteLine(); + } + + writer.WriteLine($$"""{"""); + writer.IncreaseIndent(); + } + + // Generate all nested members + writer.WriteLineSeparatedMembers(memberCallbacks, (callback, ref writer) => callback(state, ref writer)); + + // Close all scopes and reduce the indentation + for (int i = 0; i < Hierarchy.Length; i++) + { + writer.DecreaseIndent(); + writer.WriteLine("}"); + } + + // Close the namespace scope as well, if needed + if (Namespace.Length > 0) + { + writer.DecreaseIndent(); + writer.WriteLine("}"); + } + } + + /// + /// Gets the fully qualified type name for the current instance. + /// + /// The fully qualified type name for the current instance. + public string GetFullyQualifiedTypeName() + { + using PooledArrayBuilder fullyQualifiedTypeName = new(); + + fullyQualifiedTypeName.AddRange("global::".AsSpan()); + + if (Namespace.Length > 0) + { + fullyQualifiedTypeName.AddRange(Namespace.AsSpan()); + fullyQualifiedTypeName.Add('.'); + } + + fullyQualifiedTypeName.AddRange(Hierarchy[^1].QualifiedName.AsSpan()); + + for (int i = Hierarchy.Length - 2; i >= 0; i--) + { + fullyQualifiedTypeName.Add('.'); + fullyQualifiedTypeName.AddRange(Hierarchy[i].QualifiedName.AsSpan()); + } + + return fullyQualifiedTypeName.ToString(); + } +} \ No newline at end of file diff --git a/src/Authoring/WinRT.SourceGenerator2/Models/TypeInfo.cs b/src/Authoring/WinRT.SourceGenerator2/Models/TypeInfo.cs new file mode 100644 index 0000000000..b7575e9990 --- /dev/null +++ b/src/Authoring/WinRT.SourceGenerator2/Models/TypeInfo.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// Ported from ComputeSharp. +// See: https://github.com/Sergio0694/ComputeSharp/blob/main/src/ComputeSharp.SourceGeneration/Models/TypeInfo.cs. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; + +namespace WindowsRuntime.SourceGenerator.Models; + +/// +/// A model describing a type info in a type hierarchy. +/// +/// The qualified name for the type. +/// The type of the type in the hierarchy. +/// Whether the type is a record type. +internal sealed record TypeInfo(string QualifiedName, TypeKind Kind, bool IsRecord) +{ + /// + /// Gets the keyword for the current type kind. + /// + /// The keyword for the current type kind. + [SuppressMessage("Style", "IDE0072", Justification = "These are the only relevant cases for type hierarchies.")] + public string GetTypeKeyword() + { + return Kind switch + { + TypeKind.Struct when IsRecord => "record struct", + TypeKind.Struct => "struct", + TypeKind.Interface => "interface", + TypeKind.Class when IsRecord => "record", + _ => "class" + }; + } +} \ No newline at end of file diff --git a/src/Authoring/WinRT.SourceGenerator2/TypeMapAssemblyTargetGenerator.Execute.cs b/src/Authoring/WinRT.SourceGenerator2/TypeMapAssemblyTargetGenerator.Execute.cs index cee19ae3e5..c93be3bf56 100644 --- a/src/Authoring/WinRT.SourceGenerator2/TypeMapAssemblyTargetGenerator.Execute.cs +++ b/src/Authoring/WinRT.SourceGenerator2/TypeMapAssemblyTargetGenerator.Execute.cs @@ -2,7 +2,6 @@ // Licensed under the MIT License. using System.Collections.Immutable; -using System.Text; using System.Threading; using Microsoft.CodeAnalysis; @@ -91,25 +90,32 @@ public static void EmitPrivateProjectionsTypeMapAssemblyTargetAttributes(SourceP return; } - StringBuilder builder = new(); + const int ApproximateHeaderLength = 64; + const int ApproximateAttributesLength = 512; - _ = builder.AppendLine($""" + // Approximate a close enough starting length to reduce copies + int approximateLiteralLength = ApproximateHeaderLength + (ApproximateAttributesLength * assemblyNames.Length); + + IndentedTextWriter builder = new(literalLength: approximateLiteralLength, formattedCount: 0); + + // Add the auto-generated header and disable warnings for the generated file + builder.Write($""" // #pragma warning disable - - """); + """, isMultiline: true); // Add a '[TypeMapAssemblyTarget]' entry for each assembly foreach (string assemblyName in assemblyNames) { - _ = builder.AppendLine($""" + builder.WriteLine(); + builder.Write($""" [assembly: global::System.Runtime.InteropServices.TypeMapAssemblyTarget("{assemblyName}")] [assembly: global::System.Runtime.InteropServices.TypeMapAssemblyTarget("{assemblyName}")] [assembly: global::System.Runtime.InteropServices.TypeMapAssemblyTarget("{assemblyName}")] - """); + """, isMultiline: true); } - context.AddSource("TypeMapAssemblyTarget.PrivateProjections.g.cs", builder.ToString()); + context.AddSource("TypeMapAssemblyTarget.PrivateProjections.g.cs", builder.ToStringAndClear()); } /// diff --git a/src/Authoring/WinRT.SourceGenerator2/WinRT.SourceGenerator2.csproj b/src/Authoring/WinRT.SourceGenerator2/WinRT.SourceGenerator2.csproj index 556e838190..01151f646d 100644 --- a/src/Authoring/WinRT.SourceGenerator2/WinRT.SourceGenerator2.csproj +++ b/src/Authoring/WinRT.SourceGenerator2/WinRT.SourceGenerator2.csproj @@ -6,11 +6,11 @@ true + Suppress the following warning: + "RS1041: This compiler extension should not be implemented in an assembly with target framework + '.NET 10.0'. References to other target frameworks will cause the compiler to behave unpredictably." + Using .NET 10 is fine for the scenarios we need to support. + --> $(NoWarn);RS1041 @@ -41,6 +41,9 @@ true + + $(NoWarn);CS8500 + strict @@ -69,6 +72,12 @@ ..\..\WinRT.Runtime2\key.snk + + + + + + diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index fc050eeb09..995432db31 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -17,8 +17,8 @@ - - + + diff --git a/src/Projections/WinAppSDK/WinAppSDK.csproj b/src/Projections/WinAppSDK/WinAppSDK.csproj index 44d39464f9..188de263f2 100644 --- a/src/Projections/WinAppSDK/WinAppSDK.csproj +++ b/src/Projections/WinAppSDK/WinAppSDK.csproj @@ -21,14 +21,9 @@ -include Microsoft # The current WinUI nuget incorrectly references several Windows.* types that should be # Microsoft.* types instead. Temporarily include these to enable the build - -include Windows.UI.Xaml.Interop.Type - -include Windows.UI.Xaml.Interop.NotifyCollectionChangedAction - -include Windows.UI.Xaml.Markup.ContentPropertyAttribute -include Windows.UI.Xaml.StyleTypedPropertyAttribute -include Windows.UI.Xaml.TemplatePartAttribute -include Windows.UI.Xaml.TemplateVisualStateAttribute - -include Windows.UI.Xaml.Data.BindableAttribute - -include Windows.UI.Xaml.Markup.ContentPropertyAttribute -include Windows.UI.Xaml.Markup.FullXamlMetadataProviderAttribute -include Windows.UI.Xaml.Markup.MarkupExtensionReturnTypeAttribute -include Windows.UI.Xaml.Media.Animation.ConditionallyIndependentlyAnimatableAttribute @@ -53,4 +48,4 @@ - \ No newline at end of file + diff --git a/src/Projections/Windows.UI.Xaml/Windows.UI.Xaml.csproj b/src/Projections/Windows.UI.Xaml/Windows.UI.Xaml.csproj index 64204b7ff3..1ade586088 100644 --- a/src/Projections/Windows.UI.Xaml/Windows.UI.Xaml.csproj +++ b/src/Projections/Windows.UI.Xaml/Windows.UI.Xaml.csproj @@ -19,6 +19,17 @@ -exclude Windows -include Windows.UI.Xaml +-include Windows.UI.Colors +-include Windows.UI.ColorHelper +-include Windows.UI.IColorHelper +-include Windows.UI.IColors +-include Windows.UI.Text.FontWeights +-include Windows.UI.Text.IFontWeights +-include Windows.ApplicationModel.Store.Preview.WebAuthenticationCoreManagerHelper +-include Windows.ApplicationModel.Store.Preview.IWebAuthenticationCoreManagerHelper +-exclude Windows.UI.Xaml.Interop +-exclude Windows.UI.Xaml.Data.BindableAttribute +-exclude Windows.UI.Xaml.Markup.ContentPropertyAttribute diff --git a/src/Projections/Windows/Windows.csproj b/src/Projections/Windows/Windows.csproj index 2dc1c974cb..ee46556aa7 100644 --- a/src/Projections/Windows/Windows.csproj +++ b/src/Projections/Windows/Windows.csproj @@ -14,26 +14,22 @@ - -include Windows -# Exclude Windows.UI, Windows.UI.Text, Windows.UI.Xaml per Microsoft.Windows.SDK.WinUI.Contracts NuGet --include Windows.UI.Popups -exclude Windows.UI.Colors -exclude Windows.UI.IColors -exclude Windows.UI.ColorHelper -exclude Windows.UI.IColorHelper -exclude Windows.UI.IColorHelperStatics -exclude Windows.UI.IColorHelperStatics2 -#-exclude Windows.UI.Text (must include Windows.UI.Text to work around WinUI nuget issues) +-exclude Windows.UI.Text.FontWeights +-exclude Windows.UI.Text.IFontWeights -exclude Windows.UI.Xaml --exclude Windows.ApplicationModel.Store.Preview -# Allow Windows.UI.Text, Windows.UI.Xaml types used in other namespaces --include Windows.UI.Text.FontStretch --include Windows.UI.Text.FontStyle --include Windows.UI.Text.FontWeight --include Windows.UI.Text.IFontWeights --include Windows.UI.Text.UnderlineType +-exclude Windows.ApplicationModel.Store.Preview.WebAuthenticationCoreManagerHelper +-exclude Windows.ApplicationModel.Store.Preview.IWebAuthenticationCoreManagerHelper +-include Windows.UI.Xaml.Interop +-include Windows.UI.Xaml.Data.BindableAttribute +-include Windows.UI.Xaml.Markup.ContentPropertyAttribute diff --git a/src/Tests/AuthoringWuxTest/AuthoringWuxTest.csproj b/src/Tests/AuthoringWuxTest/AuthoringWuxTest.csproj index 1f131e8256..24e964724b 100644 --- a/src/Tests/AuthoringWuxTest/AuthoringWuxTest.csproj +++ b/src/Tests/AuthoringWuxTest/AuthoringWuxTest.csproj @@ -10,10 +10,10 @@ true - + - + diff --git a/src/Tests/FunctionalTests/ClassActivation/ClassActivation.csproj b/src/Tests/FunctionalTests/ClassActivation/ClassActivation.csproj index f9e366e402..72b523a375 100644 --- a/src/Tests/FunctionalTests/ClassActivation/ClassActivation.csproj +++ b/src/Tests/FunctionalTests/ClassActivation/ClassActivation.csproj @@ -6,12 +6,14 @@ x86;x64 win-x86;win-x64 $(MSBuildProjectDirectory)\..\PublishProfiles\win-$(Platform).pubxml + true + diff --git a/src/Tests/FunctionalTests/ClassActivation/Program.cs b/src/Tests/FunctionalTests/ClassActivation/Program.cs index 2df28d0f6a..f9d2a3d7ae 100644 --- a/src/Tests/FunctionalTests/ClassActivation/Program.cs +++ b/src/Tests/FunctionalTests/ClassActivation/Program.cs @@ -11,7 +11,9 @@ using TestComponentCSharp; using Windows.Foundation; using Windows.Foundation.Collections; +using Windows.UI.Xaml.Data; using WindowsRuntime.InteropServices; +using WindowsRuntime.Xaml; CustomDisposableTest customDisposableTest = new(); customDisposableTest.Dispose(); @@ -179,6 +181,25 @@ } } +TestCustomPropertyProvider testCustomPropertyProvider = new(); + +unsafe +{ + void* testCustomPropertyProviderUnknownPtr = WindowsRuntimeMarshal.ConvertToUnmanaged(testCustomPropertyProvider); + + try + { + // We should be able to get an 'ICustomPropertyProvider' interface pointer + ComHelpers.EnsureQueryInterface( + unknownPtr: testCustomPropertyProviderUnknownPtr, + iids: [new Guid("7C925755-3E48-42B4-8677-76372267033F")]); + } + finally + { + WindowsRuntimeMarshal.Free(testCustomPropertyProviderUnknownPtr); + } +} + sealed class TestComposable : Composable { } @@ -194,6 +215,13 @@ public void Dispose() } } +[Guid("3C832AA5-5F7E-46EE-B1BF-7FE03AE866AF")] +[GeneratedComInterface] +partial interface IClassicComAction +{ + void Invoke(); +} + class GenericBaseType : IEnumerable, IDisposable { public void Dispose() @@ -261,6 +289,22 @@ IEnumerator> IEnumerable>.GetEnumerato } } +[GeneratedCustomPropertyProvider] +sealed partial class TestCustomPropertyProvider +{ + public string Text => "Hello"; + + public int Number { get; set; } + + public int this[string key] + { + get => 0; + set { } + } + + public static string Info { get; set; } +} + class GenericFactory { // This method is caling a generic one, which then constructs a generic type. @@ -290,13 +334,6 @@ public static IAsyncOperation MakeAsyncOperation() } } -[Guid("3C832AA5-5F7E-46EE-B1BF-7FE03AE866AF")] -[GeneratedComInterface] -partial interface IClassicComAction -{ - void Invoke(); -} - file static class ComHelpers { [SupportedOSPlatform("windows6.3")] diff --git a/src/Tests/FunctionalTests/Directory.Build.props b/src/Tests/FunctionalTests/Directory.Build.props index 7c0c7c3ac2..bf8b4dc243 100644 --- a/src/Tests/FunctionalTests/Directory.Build.props +++ b/src/Tests/FunctionalTests/Directory.Build.props @@ -42,5 +42,16 @@ true + + + + false + + + + + + + diff --git a/src/Tests/SourceGenerator2Test/Directory.Build.props b/src/Tests/SourceGenerator2Test/Directory.Build.props new file mode 100644 index 0000000000..629129c597 --- /dev/null +++ b/src/Tests/SourceGenerator2Test/Directory.Build.props @@ -0,0 +1,9 @@ + + + + true + + + + + diff --git a/src/Tests/SourceGenerator2Test/Extensions/ReferenceAssembliesExtensions.cs b/src/Tests/SourceGenerator2Test/Extensions/ReferenceAssembliesExtensions.cs new file mode 100644 index 0000000000..43ef97bfbb --- /dev/null +++ b/src/Tests/SourceGenerator2Test/Extensions/ReferenceAssembliesExtensions.cs @@ -0,0 +1,37 @@ +// 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 () => + { + // Given we use a different nuget feed, we pass nuget.config. + string nugetConfigFilePath = Path.Combine(Path.GetDirectoryName(typeof(ReferenceAssembliesExtensions).Assembly.Location), "nuget.config"); + + ReferenceAssemblies referenceAssembly = new( + targetFramework: "net10.0", + referenceAssemblyPackage: new PackageIdentity("Microsoft.NETCore.App.Ref", "10.0.1"), + referenceAssemblyPath: Path.Combine("ref", "net10.0")); + return referenceAssembly.WithNuGetConfigFilePath(nugetConfigFilePath); + }); + + extension(ReferenceAssemblies.Net) + { + /// + /// 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 0000000000..d1976d465e --- /dev/null +++ b/src/Tests/SourceGenerator2Test/Helpers/CSharpAnalyzerTest{TAnalyzer}.cs @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +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; +using Microsoft.UI.Xaml.Controls; +using Windows.ApplicationModel.Core; + +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 parameters. + /// + /// 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. + /// The list of expected diagnostic for the test (used as alternative to the markdown syntax). + /// Whether to enable unsafe blocks. + /// The language version to use to run the test. + public static Task VerifyAnalyzerAsync( + string source, + ReadOnlySpan expectedDiagnostics = default, + bool allowUnsafeBlocks = true, + LanguageVersion languageVersion = LanguageVersion.CSharp14) + { + CSharpAnalyzerTest test = new(allowUnsafeBlocks, languageVersion) { TestCode = source }; + + test.TestState.ReferenceAssemblies = ReferenceAssemblies.Net.Net100; + test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(WindowsRuntimeObject).Assembly.Location)); + test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(CoreApplication).Assembly.Location)); + test.TestState.AdditionalReferences.Add(MetadataReference.CreateFromFile(typeof(Button).Assembly.Location)); + test.TestState.ExpectedDiagnostics.AddRange([.. expectedDiagnostics]); + + 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 0000000000..15a75a1d0d --- /dev/null +++ b/src/Tests/SourceGenerator2Test/Helpers/CSharpGeneratorTest{TGenerator}.cs @@ -0,0 +1,96 @@ +// 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.UI.Xaml.Controls; +using Windows.ApplicationModel.Core; + +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. + // Also normalize line endings to 'LF', so the test files don't have to worry about that. + string expectedText = result.Source.Replace("", $"\"{typeof(TGenerator).Assembly.GetName().Version}\"").Replace("\r\n", "\n"); + string actualText = compilation.SyntaxTrees.Single(tree => Path.GetFileName(tree.FilePath) == result.Filename).ToString(); + + Assert.AreEqual(expectedText, actualText); + } + + /// + /// 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), + MetadataReference.CreateFromFile(typeof(CoreApplication).Assembly.Location), + MetadataReference.CreateFromFile(typeof(Button).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 0000000000..d1e038ca49 --- /dev/null +++ b/src/Tests/SourceGenerator2Test/Properties/AssemblyInfo.cs @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +[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 0000000000..48c194ae68 --- /dev/null +++ b/src/Tests/SourceGenerator2Test/SourceGenerator2Test.csproj @@ -0,0 +1,55 @@ + + + Exe + net10.0 + x64;x86 + false + + + + win-x86 + x86 + + + + win-x64 + x64 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Always + + + + diff --git a/src/Tests/SourceGenerator2Test/Test_CustomPropertyProviderGenerator.cs b/src/Tests/SourceGenerator2Test/Test_CustomPropertyProviderGenerator.cs new file mode 100644 index 0000000000..8a5aeadca8 --- /dev/null +++ b/src/Tests/SourceGenerator2Test/Test_CustomPropertyProviderGenerator.cs @@ -0,0 +1,1064 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using WindowsRuntime.SourceGenerator.Tests.Helpers; + +namespace WindowsRuntime.SourceGenerator.Tests; + +[TestClass] +public class Test_CustomPropertyProviderGenerator +{ + [TestMethod] + public async Task ValidClass_MixedProperties() + { + 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 = """ + // + #pragma warning disable + + namespace MyNamespace + { + /// + partial class MyClass : Microsoft.UI.Xaml.Data.ICustomPropertyProvider + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CustomPropertyProviderGenerator", )] + global::System.Type Microsoft.UI.Xaml.Data.ICustomPropertyProvider.Type => typeof(MyClass); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CustomPropertyProviderGenerator", )] + Microsoft.UI.Xaml.Data.ICustomProperty Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetCustomProperty(string name) + { + return name switch + { + nameof(Name) => global::WindowsRuntime.Xaml.Generated.MyClass_Name.Instance, + nameof(Age) => global::WindowsRuntime.Xaml.Generated.MyClass_Age.Instance, + _ => null + }; + } + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CustomPropertyProviderGenerator", )] + Microsoft.UI.Xaml.Data.ICustomProperty Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetIndexedProperty(string name, global::System.Type type) + { + if (type == typeof(int)) + { + return global::WindowsRuntime.Xaml.Generated.MyClass_this__int.Instance; + } + + return null; + } + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CustomPropertyProviderGenerator", )] + string Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetStringRepresentation() + { + return ToString(); + } + } + } + + namespace WindowsRuntime.Xaml.Generated + { + using global::System; + using global::System.CodeDom.Compiler; + using global::System.Diagnostics; + using global::System.Diagnostics.CodeAnalysis; + using global::Microsoft.UI.Xaml.Data; + + /// + /// The implementation for . + /// + [GeneratedCode("CustomPropertyProviderGenerator", )] + [DebuggerNonUserCode] + [ExcludeFromCodeCoverage] + file sealed class MyClass_Name : ICustomProperty + { + /// + /// Gets the singleton instance for this custom property. + /// + public static readonly MyClass_Name Instance = new(); + + /// + public bool CanRead => true; + + /// + public bool CanWrite => false; + + /// + public string Name => "Name"; + + /// + public Type Type => typeof(string); + + /// + public object GetValue(object target) + { + return ((global::MyNamespace.MyClass)target).Name; + } + + /// + public void SetValue(object target, object value) + { + throw new NotSupportedException(); + } + + /// + public object GetIndexedValue(object target, object index) + { + throw new NotSupportedException(); + } + + /// + public void SetIndexedValue(object target, object value, object index) + { + throw new NotSupportedException(); + } + } + + /// + /// The implementation for . + /// + [GeneratedCode("CustomPropertyProviderGenerator", )] + [DebuggerNonUserCode] + [ExcludeFromCodeCoverage] + file sealed class MyClass_Age : ICustomProperty + { + /// + /// Gets the singleton instance for this custom property. + /// + public static readonly MyClass_Age Instance = new(); + + /// + public bool CanRead => true; + + /// + public bool CanWrite => true; + + /// + public string Name => "Age"; + + /// + public Type Type => typeof(int); + + /// + public object GetValue(object target) + { + return ((global::MyNamespace.MyClass)target).Age; + } + + /// + public void SetValue(object target, object value) + { + ((global::MyNamespace.MyClass)target).Age = (int)value; + } + + /// + public object GetIndexedValue(object target, object index) + { + throw new NotSupportedException(); + } + + /// + public void SetIndexedValue(object target, object value, object index) + { + throw new NotSupportedException(); + } + } + + /// + /// The implementation for 's indexer. + /// + [GeneratedCode("CustomPropertyProviderGenerator", )] + [DebuggerNonUserCode] + [ExcludeFromCodeCoverage] + file sealed class MyClass_this__int : ICustomProperty + { + /// + /// Gets the singleton instance for this custom property. + /// + public static readonly MyClass_this__int Instance = new(); + + /// + public bool CanRead => true; + + /// + public bool CanWrite => true; + + /// + public string Name => "this"; + + /// + public Type Type => typeof(int); + + /// + public object GetValue(object target) + { + throw new NotSupportedException(); + } + + /// + public void SetValue(object target, object value) + { + throw new NotSupportedException(); + } + + /// + public object GetIndexedValue(object target, object index) + { + return ((global::MyNamespace.MyClass)target)[(int)index]; + } + + /// + public void SetIndexedValue(object target, object value, object index) + { + ((global::MyNamespace.MyClass)target)[(int)index] = (int)value; + } + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyClass.g.cs", result)); + } + + [TestMethod] + public async Task ValidClass_NoProperties() + { + const string source = """ + using WindowsRuntime.Xaml; + + namespace MyNamespace; + + [GeneratedCustomPropertyProvider] + public partial class MyClass + { + } + """; + + const string result = """ + // + #pragma warning disable + + namespace MyNamespace + { + /// + partial class MyClass : Microsoft.UI.Xaml.Data.ICustomPropertyProvider + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CustomPropertyProviderGenerator", )] + global::System.Type Microsoft.UI.Xaml.Data.ICustomPropertyProvider.Type => typeof(MyClass); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CustomPropertyProviderGenerator", )] + Microsoft.UI.Xaml.Data.ICustomProperty Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetCustomProperty(string name) + { + return null; + } + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CustomPropertyProviderGenerator", )] + Microsoft.UI.Xaml.Data.ICustomProperty Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetIndexedProperty(string name, global::System.Type type) + { + return null; + } + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CustomPropertyProviderGenerator", )] + string Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetStringRepresentation() + { + return ToString(); + } + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyClass.g.cs", result)); + } + + [TestMethod] + public async Task ValidClass_NormalPropertiesOnly() + { + const string source = """ + using WindowsRuntime.Xaml; + + namespace MyNamespace; + + [GeneratedCustomPropertyProvider] + public partial class MyClass + { + public string Name => ""; + + public int Age { get; set; } + } + """; + + const string result = """ + // + #pragma warning disable + + namespace MyNamespace + { + /// + partial class MyClass : Microsoft.UI.Xaml.Data.ICustomPropertyProvider + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CustomPropertyProviderGenerator", )] + global::System.Type Microsoft.UI.Xaml.Data.ICustomPropertyProvider.Type => typeof(MyClass); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CustomPropertyProviderGenerator", )] + Microsoft.UI.Xaml.Data.ICustomProperty Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetCustomProperty(string name) + { + return name switch + { + nameof(Name) => global::WindowsRuntime.Xaml.Generated.MyClass_Name.Instance, + nameof(Age) => global::WindowsRuntime.Xaml.Generated.MyClass_Age.Instance, + _ => null + }; + } + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CustomPropertyProviderGenerator", )] + Microsoft.UI.Xaml.Data.ICustomProperty Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetIndexedProperty(string name, global::System.Type type) + { + return null; + } + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CustomPropertyProviderGenerator", )] + string Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetStringRepresentation() + { + return ToString(); + } + } + } + + namespace WindowsRuntime.Xaml.Generated + { + using global::System; + using global::System.CodeDom.Compiler; + using global::System.Diagnostics; + using global::System.Diagnostics.CodeAnalysis; + using global::Microsoft.UI.Xaml.Data; + + /// + /// The implementation for . + /// + [GeneratedCode("CustomPropertyProviderGenerator", )] + [DebuggerNonUserCode] + [ExcludeFromCodeCoverage] + file sealed class MyClass_Name : ICustomProperty + { + /// + /// Gets the singleton instance for this custom property. + /// + public static readonly MyClass_Name Instance = new(); + + /// + public bool CanRead => true; + + /// + public bool CanWrite => false; + + /// + public string Name => "Name"; + + /// + public Type Type => typeof(string); + + /// + public object GetValue(object target) + { + return ((global::MyNamespace.MyClass)target).Name; + } + + /// + public void SetValue(object target, object value) + { + throw new NotSupportedException(); + } + + /// + public object GetIndexedValue(object target, object index) + { + throw new NotSupportedException(); + } + + /// + public void SetIndexedValue(object target, object value, object index) + { + throw new NotSupportedException(); + } + } + + /// + /// The implementation for . + /// + [GeneratedCode("CustomPropertyProviderGenerator", )] + [DebuggerNonUserCode] + [ExcludeFromCodeCoverage] + file sealed class MyClass_Age : ICustomProperty + { + /// + /// Gets the singleton instance for this custom property. + /// + public static readonly MyClass_Age Instance = new(); + + /// + public bool CanRead => true; + + /// + public bool CanWrite => true; + + /// + public string Name => "Age"; + + /// + public Type Type => typeof(int); + + /// + public object GetValue(object target) + { + return ((global::MyNamespace.MyClass)target).Age; + } + + /// + public void SetValue(object target, object value) + { + ((global::MyNamespace.MyClass)target).Age = (int)value; + } + + /// + public object GetIndexedValue(object target, object index) + { + throw new NotSupportedException(); + } + + /// + public void SetIndexedValue(object target, object value, object index) + { + throw new NotSupportedException(); + } + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyClass.g.cs", result)); + } + + [TestMethod] + public async Task ValidClass_IndexerPropertiesOnly() + { + const string source = """ + using WindowsRuntime.Xaml; + + namespace MyNamespace; + + [GeneratedCustomPropertyProvider] + public partial class MyClass + { + public int this[int index] + { + get => 0; + set { } + } + } + """; + + const string result = """ + // + #pragma warning disable + + namespace MyNamespace + { + /// + partial class MyClass : Microsoft.UI.Xaml.Data.ICustomPropertyProvider + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CustomPropertyProviderGenerator", )] + global::System.Type Microsoft.UI.Xaml.Data.ICustomPropertyProvider.Type => typeof(MyClass); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CustomPropertyProviderGenerator", )] + Microsoft.UI.Xaml.Data.ICustomProperty Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetCustomProperty(string name) + { + return null; + } + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CustomPropertyProviderGenerator", )] + Microsoft.UI.Xaml.Data.ICustomProperty Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetIndexedProperty(string name, global::System.Type type) + { + if (type == typeof(int)) + { + return global::WindowsRuntime.Xaml.Generated.MyClass_this__int.Instance; + } + + return null; + } + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CustomPropertyProviderGenerator", )] + string Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetStringRepresentation() + { + return ToString(); + } + } + } + + namespace WindowsRuntime.Xaml.Generated + { + using global::System; + using global::System.CodeDom.Compiler; + using global::System.Diagnostics; + using global::System.Diagnostics.CodeAnalysis; + using global::Microsoft.UI.Xaml.Data; + + /// + /// The implementation for 's indexer. + /// + [GeneratedCode("CustomPropertyProviderGenerator", )] + [DebuggerNonUserCode] + [ExcludeFromCodeCoverage] + file sealed class MyClass_this__int : ICustomProperty + { + /// + /// Gets the singleton instance for this custom property. + /// + public static readonly MyClass_this__int Instance = new(); + + /// + public bool CanRead => true; + + /// + public bool CanWrite => true; + + /// + public string Name => "this"; + + /// + public Type Type => typeof(int); + + /// + public object GetValue(object target) + { + throw new NotSupportedException(); + } + + /// + public void SetValue(object target, object value) + { + throw new NotSupportedException(); + } + + /// + public object GetIndexedValue(object target, object index) + { + return ((global::MyNamespace.MyClass)target)[(int)index]; + } + + /// + public void SetIndexedValue(object target, object value, object index) + { + ((global::MyNamespace.MyClass)target)[(int)index] = (int)value; + } + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyClass.g.cs", result)); + } + + [TestMethod] + public async Task ValidClass_ReadOnlyProperty() + { + const string source = """ + using WindowsRuntime.Xaml; + + namespace MyNamespace; + + [GeneratedCustomPropertyProvider] + public partial class MyClass + { + public string Name => ""; + } + """; + + const string result = """ + // + #pragma warning disable + + namespace MyNamespace + { + /// + partial class MyClass : Microsoft.UI.Xaml.Data.ICustomPropertyProvider + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CustomPropertyProviderGenerator", )] + global::System.Type Microsoft.UI.Xaml.Data.ICustomPropertyProvider.Type => typeof(MyClass); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CustomPropertyProviderGenerator", )] + Microsoft.UI.Xaml.Data.ICustomProperty Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetCustomProperty(string name) + { + return name switch + { + nameof(Name) => global::WindowsRuntime.Xaml.Generated.MyClass_Name.Instance, + _ => null + }; + } + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CustomPropertyProviderGenerator", )] + Microsoft.UI.Xaml.Data.ICustomProperty Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetIndexedProperty(string name, global::System.Type type) + { + return null; + } + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CustomPropertyProviderGenerator", )] + string Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetStringRepresentation() + { + return ToString(); + } + } + } + + namespace WindowsRuntime.Xaml.Generated + { + using global::System; + using global::System.CodeDom.Compiler; + using global::System.Diagnostics; + using global::System.Diagnostics.CodeAnalysis; + using global::Microsoft.UI.Xaml.Data; + + /// + /// The implementation for . + /// + [GeneratedCode("CustomPropertyProviderGenerator", )] + [DebuggerNonUserCode] + [ExcludeFromCodeCoverage] + file sealed class MyClass_Name : ICustomProperty + { + /// + /// Gets the singleton instance for this custom property. + /// + public static readonly MyClass_Name Instance = new(); + + /// + public bool CanRead => true; + + /// + public bool CanWrite => false; + + /// + public string Name => "Name"; + + /// + public Type Type => typeof(string); + + /// + public object GetValue(object target) + { + return ((global::MyNamespace.MyClass)target).Name; + } + + /// + public void SetValue(object target, object value) + { + throw new NotSupportedException(); + } + + /// + public object GetIndexedValue(object target, object index) + { + throw new NotSupportedException(); + } + + /// + public void SetIndexedValue(object target, object value, object index) + { + throw new NotSupportedException(); + } + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyClass.g.cs", result)); + } + + [TestMethod] + public async Task ValidClass_WriteOnlyProperty() + { + const string source = """ + using WindowsRuntime.Xaml; + + namespace MyNamespace; + + [GeneratedCustomPropertyProvider] + public partial class MyClass + { + public string Name { set { } } + } + """; + + const string result = """ + // + #pragma warning disable + + namespace MyNamespace + { + /// + partial class MyClass : Microsoft.UI.Xaml.Data.ICustomPropertyProvider + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CustomPropertyProviderGenerator", )] + global::System.Type Microsoft.UI.Xaml.Data.ICustomPropertyProvider.Type => typeof(MyClass); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CustomPropertyProviderGenerator", )] + Microsoft.UI.Xaml.Data.ICustomProperty Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetCustomProperty(string name) + { + return name switch + { + nameof(Name) => global::WindowsRuntime.Xaml.Generated.MyClass_Name.Instance, + _ => null + }; + } + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CustomPropertyProviderGenerator", )] + Microsoft.UI.Xaml.Data.ICustomProperty Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetIndexedProperty(string name, global::System.Type type) + { + return null; + } + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CustomPropertyProviderGenerator", )] + string Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetStringRepresentation() + { + return ToString(); + } + } + } + + namespace WindowsRuntime.Xaml.Generated + { + using global::System; + using global::System.CodeDom.Compiler; + using global::System.Diagnostics; + using global::System.Diagnostics.CodeAnalysis; + using global::Microsoft.UI.Xaml.Data; + + /// + /// The implementation for . + /// + [GeneratedCode("CustomPropertyProviderGenerator", )] + [DebuggerNonUserCode] + [ExcludeFromCodeCoverage] + file sealed class MyClass_Name : ICustomProperty + { + /// + /// Gets the singleton instance for this custom property. + /// + public static readonly MyClass_Name Instance = new(); + + /// + public bool CanRead => false; + + /// + public bool CanWrite => true; + + /// + public string Name => "Name"; + + /// + public Type Type => typeof(string); + + /// + public object GetValue(object target) + { + throw new NotSupportedException(); + } + + /// + public void SetValue(object target, object value) + { + ((global::MyNamespace.MyClass)target).Name = (string)value; + } + + /// + public object GetIndexedValue(object target, object index) + { + throw new NotSupportedException(); + } + + /// + public void SetIndexedValue(object target, object value, object index) + { + throw new NotSupportedException(); + } + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyClass.g.cs", result)); + } + + [TestMethod] + public async Task ValidClass_StaticProperty() + { + const string source = """ + using WindowsRuntime.Xaml; + + namespace MyNamespace; + + [GeneratedCustomPropertyProvider] + public partial class MyClass + { + public static int Count { get; set; } + } + """; + + const string result = """ + // + #pragma warning disable + + namespace MyNamespace + { + /// + partial class MyClass : Microsoft.UI.Xaml.Data.ICustomPropertyProvider + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CustomPropertyProviderGenerator", )] + global::System.Type Microsoft.UI.Xaml.Data.ICustomPropertyProvider.Type => typeof(MyClass); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CustomPropertyProviderGenerator", )] + Microsoft.UI.Xaml.Data.ICustomProperty Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetCustomProperty(string name) + { + return name switch + { + nameof(Count) => global::WindowsRuntime.Xaml.Generated.MyClass_Count.Instance, + _ => null + }; + } + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CustomPropertyProviderGenerator", )] + Microsoft.UI.Xaml.Data.ICustomProperty Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetIndexedProperty(string name, global::System.Type type) + { + return null; + } + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CustomPropertyProviderGenerator", )] + string Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetStringRepresentation() + { + return ToString(); + } + } + } + + namespace WindowsRuntime.Xaml.Generated + { + using global::System; + using global::System.CodeDom.Compiler; + using global::System.Diagnostics; + using global::System.Diagnostics.CodeAnalysis; + using global::Microsoft.UI.Xaml.Data; + + /// + /// The implementation for . + /// + [GeneratedCode("CustomPropertyProviderGenerator", )] + [DebuggerNonUserCode] + [ExcludeFromCodeCoverage] + file sealed class MyClass_Count : ICustomProperty + { + /// + /// Gets the singleton instance for this custom property. + /// + public static readonly MyClass_Count Instance = new(); + + /// + public bool CanRead => true; + + /// + public bool CanWrite => true; + + /// + public string Name => "Count"; + + /// + public Type Type => typeof(int); + + /// + public object GetValue(object target) + { + return global::MyNamespace.MyClass.Count; + } + + /// + public void SetValue(object target, object value) + { + global::MyNamespace.MyClass.Count = (int)value; + } + + /// + public object GetIndexedValue(object target, object index) + { + throw new NotSupportedException(); + } + + /// + public void SetIndexedValue(object target, object value, object index) + { + throw new NotSupportedException(); + } + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyClass.g.cs", result)); + } + + [TestMethod] + public async Task ValidClass_ReadOnlyIndexer() + { + const string source = """ + using WindowsRuntime.Xaml; + + namespace MyNamespace; + + [GeneratedCustomPropertyProvider] + public partial class MyClass + { + public string this[int index] => ""; + } + """; + + const string result = """ + // + #pragma warning disable + + namespace MyNamespace + { + /// + partial class MyClass : Microsoft.UI.Xaml.Data.ICustomPropertyProvider + { + /// + [global::System.CodeDom.Compiler.GeneratedCode("CustomPropertyProviderGenerator", )] + global::System.Type Microsoft.UI.Xaml.Data.ICustomPropertyProvider.Type => typeof(MyClass); + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CustomPropertyProviderGenerator", )] + Microsoft.UI.Xaml.Data.ICustomProperty Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetCustomProperty(string name) + { + return null; + } + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CustomPropertyProviderGenerator", )] + Microsoft.UI.Xaml.Data.ICustomProperty Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetIndexedProperty(string name, global::System.Type type) + { + if (type == typeof(int)) + { + return global::WindowsRuntime.Xaml.Generated.MyClass_this__int.Instance; + } + + return null; + } + + /// + [global::System.CodeDom.Compiler.GeneratedCode("CustomPropertyProviderGenerator", )] + string Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetStringRepresentation() + { + return ToString(); + } + } + } + + namespace WindowsRuntime.Xaml.Generated + { + using global::System; + using global::System.CodeDom.Compiler; + using global::System.Diagnostics; + using global::System.Diagnostics.CodeAnalysis; + using global::Microsoft.UI.Xaml.Data; + + /// + /// The implementation for 's indexer. + /// + [GeneratedCode("CustomPropertyProviderGenerator", )] + [DebuggerNonUserCode] + [ExcludeFromCodeCoverage] + file sealed class MyClass_this__int : ICustomProperty + { + /// + /// Gets the singleton instance for this custom property. + /// + public static readonly MyClass_this__int Instance = new(); + + /// + public bool CanRead => true; + + /// + public bool CanWrite => false; + + /// + public string Name => "this"; + + /// + public Type Type => typeof(string); + + /// + public object GetValue(object target) + { + throw new NotSupportedException(); + } + + /// + public void SetValue(object target, object value) + { + throw new NotSupportedException(); + } + + /// + public object GetIndexedValue(object target, object index) + { + return ((global::MyNamespace.MyClass)target)[(int)index]; + } + + /// + public void SetIndexedValue(object target, object value, object index) + { + throw new NotSupportedException(); + } + } + } + """; + + CSharpGeneratorTest.VerifySources(source, ("MyNamespace.MyClass.g.cs", result)); + } +} \ No newline at end of file diff --git a/src/Tests/SourceGenerator2Test/Test_GeneratedCustomPropertyProviderAttributeArgumentAnalyzer.cs b/src/Tests/SourceGenerator2Test/Test_GeneratedCustomPropertyProviderAttributeArgumentAnalyzer.cs new file mode 100644 index 0000000000..715feb3539 --- /dev/null +++ b/src/Tests/SourceGenerator2Test/Test_GeneratedCustomPropertyProviderAttributeArgumentAnalyzer.cs @@ -0,0 +1,394 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using Microsoft.CodeAnalysis.Testing; +using WindowsRuntime.SourceGenerator.Diagnostics; +using WindowsRuntime.SourceGenerator.Tests.Helpers; +using VerifyCS = Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier< + WindowsRuntime.SourceGenerator.Diagnostics.GeneratedCustomPropertyProviderAttributeArgumentAnalyzer, + Microsoft.CodeAnalysis.Testing.DefaultVerifier>; + +namespace WindowsRuntime.SourceGenerator.Tests; + +[TestClass] +public class Test_GeneratedCustomPropertyProviderAttributeArgumentAnalyzer +{ + [TestMethod] + [DataRow("class")] + [DataRow("struct")] + public async Task DefaultConstructor_DoesNotWarn(string modifier) + { + string source = $$""" + using WindowsRuntime.Xaml; + + [GeneratedCustomPropertyProvider] + public partial {{modifier}} MyType + { + public string Name { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source); + } + + [TestMethod] + public async Task EmptyArrays_DoesNotWarn() + { + string source = """ + using System; + using WindowsRuntime.Xaml; + + [GeneratedCustomPropertyProvider(new string[] { }, new Type[] { })] + public partial class MyType + { + public string Name { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source); + } + + [TestMethod] + public async Task ValidPropertyName_DoesNotWarn() + { + string source = """ + using System; + using WindowsRuntime.Xaml; + + [GeneratedCustomPropertyProvider(new string[] { "Name" }, new Type[] { })] + public partial class MyType + { + public string Name { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source); + } + + [TestMethod] + public async Task ValidIndexerType_DoesNotWarn() + { + string source = """ + using System; + using WindowsRuntime.Xaml; + + [GeneratedCustomPropertyProvider(new string[] { }, new Type[] { typeof(int) })] + public partial class MyType + { + public string this[int index] => ""; + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source); + } + + [TestMethod] + public async Task ValidPropertyNameAndIndexerType_DoesNotWarn() + { + string source = """ + using System; + using WindowsRuntime.Xaml; + + [GeneratedCustomPropertyProvider(new string[] { "Name" }, new Type[] { typeof(int) })] + public partial class MyType + { + public string Name { get; set; } + public string this[int index] => ""; + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source); + } + + [TestMethod] + public async Task InheritedPropertyName_DoesNotWarn() + { + string source = """ + using System; + using WindowsRuntime.Xaml; + + public class BaseType + { + public string Name { get; set; } + } + + [GeneratedCustomPropertyProvider(new string[] { "Name" }, new Type[] { })] + public partial class MyType : BaseType; + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source); + } + + [TestMethod] + public async Task InheritedIndexerType_DoesNotWarn() + { + string source = """ + using System; + using WindowsRuntime.Xaml; + + public class BaseType + { + public string this[int index] => ""; + } + + [GeneratedCustomPropertyProvider(new string[] { }, new Type[] { typeof(int) })] + public partial class MyType : BaseType; + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source); + } + + [TestMethod] + public async Task MultipleValidPropertyNames_DoesNotWarn() + { + string source = """ + using System; + using WindowsRuntime.Xaml; + + [GeneratedCustomPropertyProvider(new string[] { "Name", "Age" }, new Type[] { })] + public partial class MyType + { + public string Name { get; set; } + public int Age { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source); + } + + [TestMethod] + public async Task OverriddenPropertyName_DoesNotWarn() + { + string source = """ + using System; + using WindowsRuntime.Xaml; + + public class BaseType + { + public virtual string Name { get; set; } + } + + [GeneratedCustomPropertyProvider(new string[] { "Name" }, new Type[] { })] + public partial class MyType : BaseType + { + public override string Name { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source); + } + + [TestMethod] + public async Task NullPropertyName_Warns() + { + string source = """ + using System; + using WindowsRuntime.Xaml; + + [GeneratedCustomPropertyProvider(new string[] { null }, new Type[] { })] + public partial class {|CSWINRT2004:MyType|} + { + public string Name { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source); + } + + [TestMethod] + public async Task NullAmongValidPropertyNames_Warns() + { + string source = """ + using System; + using WindowsRuntime.Xaml; + + [GeneratedCustomPropertyProvider(new string[] { "Name", null }, new Type[] { })] + public partial class {|CSWINRT2004:MyType|} + { + public string Name { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source); + } + + [TestMethod] + public async Task NullIndexerType_Warns() + { + string source = """ + using System; + using WindowsRuntime.Xaml; + + [GeneratedCustomPropertyProvider(new string[] { }, new Type[] { null })] + public partial class {|CSWINRT2005:MyType|} + { + public string this[int index] => ""; + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source); + } + + [TestMethod] + public async Task NullAmongValidIndexerTypes_Warns() + { + string source = """ + using System; + using WindowsRuntime.Xaml; + + [GeneratedCustomPropertyProvider(new string[] { }, new Type[] { typeof(int), null })] + public partial class {|CSWINRT2005:MyType|} + { + public string this[int index] => ""; + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source); + } + + [TestMethod] + public async Task PropertyNameNotFound_Warns() + { + string source = """ + using System; + using WindowsRuntime.Xaml; + + [GeneratedCustomPropertyProvider(new string[] { "Missing" }, new Type[] { })] + public partial class {|CSWINRT2006:MyType|} + { + public string Name { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source); + } + + [TestMethod] + public async Task PropertyNameMatchesPrivateProperty_Warns() + { + string source = """ + using System; + using WindowsRuntime.Xaml; + + [GeneratedCustomPropertyProvider(new string[] { "Secret" }, new Type[] { })] + public partial class {|CSWINRT2006:MyType|} + { + private string Secret { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source); + } + + [TestMethod] + public async Task MultiplePropertyNamesNotFound_WarnsForEach() + { + string source = """ + using System; + using WindowsRuntime.Xaml; + + [GeneratedCustomPropertyProvider(new string[] { "Missing1", "Missing2" }, new Type[] { })] + public partial class MyType + { + public string Name { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, expectedDiagnostics: [ + VerifyCS.Diagnostic("CSWINRT2006").WithSpan(5, 22, 5, 28).WithArguments("Missing1", "MyType"), + VerifyCS.Diagnostic("CSWINRT2006").WithSpan(5, 22, 5, 28).WithArguments("Missing2", "MyType")]); + } + + [TestMethod] + public async Task IndexerTypeNotFound_WrongType_Warns() + { + string source = """ + using System; + using WindowsRuntime.Xaml; + + [GeneratedCustomPropertyProvider(new string[] { }, new Type[] { typeof(string) })] + public partial class {|CSWINRT2007:MyType|} + { + public string this[int index] => ""; + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source); + } + + [TestMethod] + public async Task IndexerTypeNotFound_NoIndexer_Warns() + { + string source = """ + using System; + using WindowsRuntime.Xaml; + + [GeneratedCustomPropertyProvider(new string[] { }, new Type[] { typeof(int) })] + public partial class {|CSWINRT2007:MyType|} + { + public string Name { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source); + } + + [TestMethod] + public async Task IndexerTypeMatchesMultiParameterIndexer_Warns() + { + string source = """ + using System; + using WindowsRuntime.Xaml; + + [GeneratedCustomPropertyProvider(new string[] { }, new Type[] { typeof(int) })] + public partial class {|CSWINRT2007:MyType|} + { + public string this[int row, int col] => ""; + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source); + } + + [TestMethod] + public async Task MultipleIndexerTypesNotFound_WarnsForEach() + { + string source = """ + using System; + using WindowsRuntime.Xaml; + + [GeneratedCustomPropertyProvider(new string[] { }, new Type[] { typeof(string), typeof(double) })] + public partial class MyType + { + public string this[int index] => ""; + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, expectedDiagnostics: [ + VerifyCS.Diagnostic("CSWINRT2007").WithSpan(5, 22, 5, 28).WithArguments("double", "MyType"), + VerifyCS.Diagnostic("CSWINRT2007").WithSpan(5, 22, 5, 28).WithArguments("string", "MyType")]); + } + + [TestMethod] + public async Task MixedInvalidArguments_WarnsForEach() + { + string source = """ + using System; + using WindowsRuntime.Xaml; + + [GeneratedCustomPropertyProvider(new string[] { "Missing", null }, new Type[] { typeof(string), null })] + public partial class MyType + { + public string Name { get; set; } + public string this[int index] => ""; + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, expectedDiagnostics: [ + VerifyCS.Diagnostic("CSWINRT2004").WithSpan(5, 22, 5, 28).WithArguments("MyType"), + VerifyCS.Diagnostic("CSWINRT2005").WithSpan(5, 22, 5, 28).WithArguments("MyType"), + VerifyCS.Diagnostic("CSWINRT2006").WithSpan(5, 22, 5, 28).WithArguments("Missing", "MyType"), + VerifyCS.Diagnostic("CSWINRT2007").WithSpan(5, 22, 5, 28).WithArguments("string", "MyType")]); + } +} diff --git a/src/Tests/SourceGenerator2Test/Test_GeneratedCustomPropertyProviderExistingMemberImplementationAnalyzer.cs b/src/Tests/SourceGenerator2Test/Test_GeneratedCustomPropertyProviderExistingMemberImplementationAnalyzer.cs new file mode 100644 index 0000000000..9810434058 --- /dev/null +++ b/src/Tests/SourceGenerator2Test/Test_GeneratedCustomPropertyProviderExistingMemberImplementationAnalyzer.cs @@ -0,0 +1,232 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using Microsoft; +using Microsoft.CodeAnalysis.Testing; +using WindowsRuntime.SourceGenerator.Diagnostics; +using WindowsRuntime.SourceGenerator.Tests.Helpers; +using VerifyCS = Microsoft.CodeAnalysis.CSharp.Testing.CSharpAnalyzerVerifier< + WindowsRuntime.SourceGenerator.Diagnostics.GeneratedCustomPropertyProviderExistingMemberImplementationAnalyzer, + Microsoft.CodeAnalysis.Testing.DefaultVerifier>; + +namespace WindowsRuntime.SourceGenerator.Tests; + +[TestClass] +public class Test_GeneratedCustomPropertyProviderExistingMemberImplementationAnalyzer +{ + [TestMethod] + [DataRow("class")] + [DataRow("struct")] + public async Task ValidType_NoInterface_DoesNotWarn(string modifier) + { + string source = $$""" + using WindowsRuntime.Xaml; + + [GeneratedCustomPropertyProvider] + public partial {{modifier}} MyType + { + public string Name { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source); + } + + [TestMethod] + public async Task ValidType_NoMembers_DoesNotWarn() + { + string source = $$""" + using Microsoft.UI.Xaml.Data; + using WindowsRuntime.Xaml; + + [GeneratedCustomPropertyProvider] + public partial class MyType : ICustomPropertyProvider + { + public string Name { get; set; } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, expectedDiagnostics: [ + // /0/Test0.cs(5,31): error CS0535: 'MyType' does not implement interface member 'ICustomPropertyProvider.GetCustomProperty(string)' + DiagnosticResult.CompilerError("CS0535").WithSpan(5, 31, 5, 54).WithArguments("MyType", "Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetCustomProperty(string)"), + // /0/Test0.cs(5,31): error CS0535: 'MyType' does not implement interface member 'ICustomPropertyProvider.GetIndexedProperty(string, Type)' + DiagnosticResult.CompilerError("CS0535").WithSpan(5, 31, 5, 54).WithArguments("MyType", "Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetIndexedProperty(string, System.Type)"), + // /0/Test0.cs(5,31): error CS0535: 'MyType' does not implement interface member 'ICustomPropertyProvider.GetStringRepresentation()' + DiagnosticResult.CompilerError("CS0535").WithSpan(5, 31, 5, 54).WithArguments("MyType", "Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetStringRepresentation()"), + // /0/Test0.cs(5,31): error CS0535: 'MyType' does not implement interface member 'ICustomPropertyProvider.Type' + DiagnosticResult.CompilerError("CS0535").WithSpan(5, 31, 5, 54).WithArguments("MyType", "Microsoft.UI.Xaml.Data.ICustomPropertyProvider.Type")]); + } + + [TestMethod] + public async Task TypeWithExplicitInterfaceImplementation_Warns() + { + string source = """ + using System; + using Microsoft.UI.Xaml.Data; + using WindowsRuntime.Xaml; + + [GeneratedCustomPropertyProvider] + public partial class {|CSWINRT2003:MyType|} : ICustomPropertyProvider + { + Type ICustomPropertyProvider.Type => typeof(MyType); + ICustomProperty ICustomPropertyProvider.GetCustomProperty(string name) => null; + ICustomProperty ICustomPropertyProvider.GetIndexedProperty(string name, Type type) => null; + string ICustomPropertyProvider.GetStringRepresentation() => ""; + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source); + } + + [TestMethod] + public async Task TypeWithExplicitInterfaceImplementation_Incomplete_Warns() + { + string source = """ + using System; + using Microsoft.UI.Xaml.Data; + using WindowsRuntime.Xaml; + + [GeneratedCustomPropertyProvider] + public partial class MyType : ICustomPropertyProvider + { + Type ICustomPropertyProvider.Type => typeof(MyType); + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, expectedDiagnostics: [ + // /0/Test0.cs(6,22): error CSWINRT2003: The type 'MyType' cannot use '[GeneratedCustomPropertyProvider]' because it already has or inherits implementations for one or more 'ICustomPropertyProvider' members + VerifyCS.Diagnostic().WithSpan(6, 22, 6, 28).WithArguments("MyType"), + // /0/Test0.cs(6,31): error CS0535: 'MyType' does not implement interface member 'ICustomPropertyProvider.GetCustomProperty(string)' + DiagnosticResult.CompilerError("CS0535").WithSpan(6, 31, 6, 54).WithArguments("MyType", "Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetCustomProperty(string)"), + // /0/Test0.cs(6,31): error CS0535: 'MyType' does not implement interface member 'ICustomPropertyProvider.GetIndexedProperty(string, Type)' + DiagnosticResult.CompilerError("CS0535").WithSpan(6, 31, 6, 54).WithArguments("MyType", "Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetIndexedProperty(string, System.Type)"), + // /0/Test0.cs(6,31): error CS0535: 'MyType' does not implement interface member 'ICustomPropertyProvider.GetStringRepresentation()' + DiagnosticResult.CompilerError("CS0535").WithSpan(6, 31, 6, 54).WithArguments("MyType", "Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetStringRepresentation()")]); + } + + [TestMethod] + public async Task TypeWithImplicitInterfaceImplementation_Warns() + { + string source = """ + using System; + using Microsoft.UI.Xaml.Data; + using WindowsRuntime.Xaml; + + [GeneratedCustomPropertyProvider] + public partial class {|CSWINRT2003:MyType|} : ICustomPropertyProvider + { + public Type Type => typeof(MyType); + public ICustomProperty GetCustomProperty(string name) => null; + public ICustomProperty GetIndexedProperty(string name, Type type) => null; + public string GetStringRepresentation() => ""; + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source); + } + + [TestMethod] + public async Task TypeWithImplicitInterfaceImplementation_Incomplete_Warns() + { + string source = """ + using System; + using Microsoft.UI.Xaml.Data; + using WindowsRuntime.Xaml; + + [GeneratedCustomPropertyProvider] + public partial class MyType : ICustomPropertyProvider + { + public Type Type => typeof(MyType); + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source, expectedDiagnostics: [ + // /0/Test0.cs(6,22): error CSWINRT2003: The type 'MyType' cannot use '[GeneratedCustomPropertyProvider]' because it already has or inherits implementations for one or more 'ICustomPropertyProvider' members + VerifyCS.Diagnostic().WithSpan(6, 22, 6, 28).WithArguments("MyType"), + // /0/Test0.cs(6,31): error CS0535: 'MyType' does not implement interface member 'ICustomPropertyProvider.GetCustomProperty(string)' + DiagnosticResult.CompilerError("CS0535").WithSpan(6, 31, 6, 54).WithArguments("MyType", "Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetCustomProperty(string)"), + // /0/Test0.cs(6,31): error CS0535: 'MyType' does not implement interface member 'ICustomPropertyProvider.GetIndexedProperty(string, Type)' + DiagnosticResult.CompilerError("CS0535").WithSpan(6, 31, 6, 54).WithArguments("MyType", "Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetIndexedProperty(string, System.Type)"), + // /0/Test0.cs(6,31): error CS0535: 'MyType' does not implement interface member 'ICustomPropertyProvider.GetStringRepresentation()' + DiagnosticResult.CompilerError("CS0535").WithSpan(6, 31, 6, 54).WithArguments("MyType", "Microsoft.UI.Xaml.Data.ICustomPropertyProvider.GetStringRepresentation()")]); + } + + [TestMethod] + public async Task TypeInheritingFromImplementingBase_Warns() + { + string source = """ + using System; + using Microsoft.UI.Xaml.Data; + using WindowsRuntime.Xaml; + + public class BaseType : ICustomPropertyProvider + { + Type ICustomPropertyProvider.Type => typeof(BaseType); + ICustomProperty ICustomPropertyProvider.GetCustomProperty(string name) => null; + ICustomProperty ICustomPropertyProvider.GetIndexedProperty(string name, Type type) => null; + string ICustomPropertyProvider.GetStringRepresentation() => ""; + } + + [GeneratedCustomPropertyProvider] + public partial class {|CSWINRT2003:MyType|} : BaseType; + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source); + } + + [TestMethod] + public async Task TypeWithGeneratedExplicitInterfaceImplementation_DoesNotWarn() + { + string source = """ + using System; + using System.CodeDom.Compiler; + using Microsoft.UI.Xaml.Data; + using WindowsRuntime.Xaml; + + [GeneratedCustomPropertyProvider] + public partial class MyType : ICustomPropertyProvider + { + public string Name { get; set; } + + [GeneratedCode("CustomPropertyProviderGenerator", "1.0.0")] + Type ICustomPropertyProvider.Type => typeof(MyType); + [GeneratedCode("CustomPropertyProviderGenerator", "1.0.0")] + ICustomProperty ICustomPropertyProvider.GetCustomProperty(string name) => null; + [GeneratedCode("CustomPropertyProviderGenerator", "1.0.0")] + ICustomProperty ICustomPropertyProvider.GetIndexedProperty(string name, Type type) => null; + [GeneratedCode("CustomPropertyProviderGenerator", "1.0.0")] + string ICustomPropertyProvider.GetStringRepresentation() => ""; + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source); + } + + [TestMethod] + public async Task TypeWithMixedGeneratedAndUserImplementation_Warns() + { + string source = """ + using System; + using System.CodeDom.Compiler; + using Microsoft.UI.Xaml.Data; + using WindowsRuntime.Xaml; + + [GeneratedCustomPropertyProvider] + public partial class {|CSWINRT2003:MyType|} : ICustomPropertyProvider + { + public string Name { get; set; } + + Type ICustomPropertyProvider.Type => typeof(MyType); + [GeneratedCode("CustomPropertyProviderGenerator", "1.0.0")] + ICustomProperty ICustomPropertyProvider.GetCustomProperty(string name) => null; + [GeneratedCode("CustomPropertyProviderGenerator", "1.0.0")] + ICustomProperty ICustomPropertyProvider.GetIndexedProperty(string name, Type type) => null; + [GeneratedCode("CustomPropertyProviderGenerator", "1.0.0")] + string ICustomPropertyProvider.GetStringRepresentation() => ""; + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source); + } +} diff --git a/src/Tests/SourceGenerator2Test/Test_GeneratedCustomPropertyProviderTargetTypeAnalyzer.cs b/src/Tests/SourceGenerator2Test/Test_GeneratedCustomPropertyProviderTargetTypeAnalyzer.cs new file mode 100644 index 0000000000..421242ce8d --- /dev/null +++ b/src/Tests/SourceGenerator2Test/Test_GeneratedCustomPropertyProviderTargetTypeAnalyzer.cs @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Threading.Tasks; +using WindowsRuntime.SourceGenerator.Diagnostics; +using WindowsRuntime.SourceGenerator.Tests.Helpers; + +namespace WindowsRuntime.SourceGenerator.Tests; + +[TestClass] +public class Test_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("class")] + [DataRow("struct")] + public async Task ValidTargetType_InValidHierarchy_DoesNotWarn(string modifier) + { + string source = $$""" + using WindowsRuntime.Xaml; + + public partial struct A + { + public partial class B + { + [GeneratedCustomPropertyProvider] + public partial {{modifier}} MyType; + } + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source); + } + + [TestMethod] + [DataRow("abstract partial class")] + [DataRow("static partial class")] + [DataRow("ref partial struct")] + public async Task InvalidTargetType_Warns(string modifiers) + { + string source = $$""" + using WindowsRuntime.Xaml; + + [GeneratedCustomPropertyProvider] + public {{modifiers}} {|CSWINRT2000:MyType|}; + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source); + } + + [TestMethod] + [DataRow("class")] + [DataRow("struct")] + public async Task TypeNotPartial_Warns(string modifier) + { + string source = $$""" + using WindowsRuntime.Xaml; + + [GeneratedCustomPropertyProvider] + public {{modifier}} {|CSWINRT2001: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 + { + [GeneratedCustomPropertyProvider] + public partial {{modifier}} {|CSWINRT2001:MyType|}; + } + """; + + await CSharpAnalyzerTest.VerifyAnalyzerAsync(source); + } +} \ No newline at end of file diff --git a/src/WinRT.Runtime2/Xaml.Attributes/GeneratedCustomPropertyProvider.cs b/src/WinRT.Runtime2/Xaml.Attributes/GeneratedCustomPropertyProvider.cs new file mode 100644 index 0000000000..1ac293ad4e --- /dev/null +++ b/src/WinRT.Runtime2/Xaml.Attributes/GeneratedCustomPropertyProvider.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; + +namespace WindowsRuntime.Xaml; + +/// +/// An attribute used to indicate the properties which are bindable, for XAML (WinUI) scenarios. +/// +/// +/// This attribute will cause binding code to be generated to provide support via the Windows.UI.Xaml.Data.ICustomPropertyProvider +/// and Microsoft.UI.Xaml.Data.ICustomPropertyProvider infrastructure, for the specified properties on the annotated type. +/// +/// +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, Inherited = false, AllowMultiple = false)] +public sealed class GeneratedCustomPropertyProviderAttribute : Attribute +{ + /// + /// Creates a new instance. + /// + /// + /// Using this constructor will mark all public properties as bindable. + /// + public GeneratedCustomPropertyProviderAttribute() + { + } + + /// + /// Creates a new instance with the specified parameters. + /// + /// The name of the non-indexer public properties to mark as bindable. + /// The parameter type of the indexer public properties to mark as bindable. + public GeneratedCustomPropertyProviderAttribute(string[] propertyNames, Type[] indexerPropertyTypes) + { + PropertyNames = propertyNames; + IndexerPropertyTypes = indexerPropertyTypes; + } + + /// + /// Gets the name of the non-indexer public properties to mark as bindable. + /// + /// + /// If , all public properties are considered bindable. + /// + public string[]? PropertyNames { get; } + + /// + /// Gets the parameter type of the indexer public properties to mark as bindable. + /// + /// + /// If , all indexer public properties are considered bindable. + /// + public Type[]? IndexerPropertyTypes { get; } +} \ No newline at end of file diff --git a/src/build.cmd b/src/build.cmd index c151eeb0a7..404bb60f87 100644 --- a/src/build.cmd +++ b/src/build.cmd @@ -189,6 +189,16 @@ if ErrorLevel 1 ( exit /b !ErrorLevel! ) +:sourcegenerator2test +rem Running Source Generator 2 Unit Tests +echo Running source generator 2 tests for %cswinrt_platform% %cswinrt_configuration% +call :exec %dotnet_exe% test --verbosity normal --no-build --logger trx;LogFilePath=%~dp0sourcegenerator2test_%cswinrt_version_string%.trx %this_dir%Tests\SourceGenerator2Test\SourceGenerator2Test.csproj /nologo /m /p:platform=%cswinrt_platform%;configuration=%cswinrt_configuration% -- RunConfiguration.TreatNoTestsAsError=true +if ErrorLevel 1 ( + echo. + echo ERROR: Source generator 2 unit test failed, skipping NuGet pack + exit /b !ErrorLevel! +) + :hosttest rem Run WinRT.Host tests echo Running cswinrt host tests for %cswinrt_platform% %cswinrt_configuration% diff --git a/src/cswinrt.slnx b/src/cswinrt.slnx index af6d7709c9..d4087f6519 100644 --- a/src/cswinrt.slnx +++ b/src/cswinrt.slnx @@ -206,6 +206,14 @@ + + + + + + + +