From e3027d16aaf2280633f67085f40f896afcdd1c62 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 31 May 2023 18:21:15 +0200 Subject: [PATCH 01/20] Create blank AppServices experiment --- components/AppServices/OpenSolution.bat | 3 + .../samples/AppServices.Samples.csproj | 8 +++ components/AppServices/samples/AppServices.md | 64 +++++++++++++++++++ .../AppServices/samples/MultiTarget.props | 9 +++ .../AppServices/src/AdditionalAssemblyInfo.cs | 13 ++++ ...yToolkit.WinUI.Controls.AppServices.csproj | 13 ++++ components/AppServices/src/MultiTarget.props | 9 +++ .../tests/AppServices.Tests.projitems | 11 ++++ .../tests/AppServices.Tests.shproj | 13 ++++ 9 files changed, 143 insertions(+) create mode 100644 components/AppServices/OpenSolution.bat create mode 100644 components/AppServices/samples/AppServices.Samples.csproj create mode 100644 components/AppServices/samples/AppServices.md create mode 100644 components/AppServices/samples/MultiTarget.props create mode 100644 components/AppServices/src/AdditionalAssemblyInfo.cs create mode 100644 components/AppServices/src/CommunityToolkit.WinUI.Controls.AppServices.csproj create mode 100644 components/AppServices/src/MultiTarget.props create mode 100644 components/AppServices/tests/AppServices.Tests.projitems create mode 100644 components/AppServices/tests/AppServices.Tests.shproj diff --git a/components/AppServices/OpenSolution.bat b/components/AppServices/OpenSolution.bat new file mode 100644 index 000000000..814a56d4b --- /dev/null +++ b/components/AppServices/OpenSolution.bat @@ -0,0 +1,3 @@ +@ECHO OFF + +powershell ..\..\tooling\ProjectHeads\GenerateSingleSampleHeads.ps1 -componentPath %CD% %* \ No newline at end of file diff --git a/components/AppServices/samples/AppServices.Samples.csproj b/components/AppServices/samples/AppServices.Samples.csproj new file mode 100644 index 000000000..9d6b70dbf --- /dev/null +++ b/components/AppServices/samples/AppServices.Samples.csproj @@ -0,0 +1,8 @@ + + + AppServices + + + + + diff --git a/components/AppServices/samples/AppServices.md b/components/AppServices/samples/AppServices.md new file mode 100644 index 000000000..59ff6fb12 --- /dev/null +++ b/components/AppServices/samples/AppServices.md @@ -0,0 +1,64 @@ +--- +title: AppServices +author: githubaccount +description: TODO: Your experiment's description here +keywords: AppServices, Control, Layout +dev_langs: + - csharp +category: Controls +subcategory: Layout +discussion-id: 0 +issue-id: 0 +--- + + + + + + + + + +# AppServices + +TODO: Fill in information about this experiment and how to get started here... + +## Custom Control + +You can inherit from an existing component as well, like `Panel`, this example shows a control without a +XAML Style that will be more light-weight to consume by an app developer: + +> [!Sample AppServicesCustomSample] + +## Templated Controls + +The Toolkit is built with templated controls. This provides developers a flexible way to restyle components +easily while still inheriting the general functionality a control provides. The examples below show +how a component can use a default style and then get overridden by the end developer. + +TODO: Two types of templated control building methods are shown. Delete these if you're building a custom component. +Otherwise, pick one method for your component and delete the files related to the unchosen `_ClassicBinding` or `_xBind` +classes (and the custom non-suffixed one as well). Then, rename your component to just be your component name. + +The `_ClassicBinding` class shows the traditional method used to develop components with best practices. + +### Implict style + +> [!SAMPLE AppServicesTemplatedSample] + +### Custom style + +> [!SAMPLE AppServicesTemplatedStyleCustomSample] + +## Templated Controls with x:Bind + +This is an _experimental_ new way to define components which allows for the use of x:Bind within the style. + +### Implict style + +> [!SAMPLE AppServicesXbindBackedSample] + +### Custom style + +> [!SAMPLE AppServicesXbindBackedStyleCustomSample] + diff --git a/components/AppServices/samples/MultiTarget.props b/components/AppServices/samples/MultiTarget.props new file mode 100644 index 000000000..0c55bf4f7 --- /dev/null +++ b/components/AppServices/samples/MultiTarget.props @@ -0,0 +1,9 @@ + + + + uwp; + + diff --git a/components/AppServices/src/AdditionalAssemblyInfo.cs b/components/AppServices/src/AdditionalAssemblyInfo.cs new file mode 100644 index 000000000..1a32f7ed2 --- /dev/null +++ b/components/AppServices/src/AdditionalAssemblyInfo.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Runtime.CompilerServices; + +// These `InternalsVisibleTo` calls are intended to make it easier for +// for any internal code to be testable in all the different test projects +// used with the Labs infrastructure. +[assembly: InternalsVisibleTo("AppServices.Tests.Uwp")] +[assembly: InternalsVisibleTo("AppServices.Tests.WinAppSdk")] +[assembly: InternalsVisibleTo("CommunityToolkit.Tests.Uwp")] +[assembly: InternalsVisibleTo("CommunityToolkit.Tests.WinAppSdk")] diff --git a/components/AppServices/src/CommunityToolkit.WinUI.Controls.AppServices.csproj b/components/AppServices/src/CommunityToolkit.WinUI.Controls.AppServices.csproj new file mode 100644 index 000000000..81981d4b7 --- /dev/null +++ b/components/AppServices/src/CommunityToolkit.WinUI.Controls.AppServices.csproj @@ -0,0 +1,13 @@ + + + AppServices + This package contains AppServices. + 0.0.1 + + + CommunityToolkit.WinUI.Controls.AppServicesRns + + + + + diff --git a/components/AppServices/src/MultiTarget.props b/components/AppServices/src/MultiTarget.props new file mode 100644 index 000000000..c8f516392 --- /dev/null +++ b/components/AppServices/src/MultiTarget.props @@ -0,0 +1,9 @@ + + + + uwp;netstandard; + + diff --git a/components/AppServices/tests/AppServices.Tests.projitems b/components/AppServices/tests/AppServices.Tests.projitems new file mode 100644 index 000000000..ab41406d2 --- /dev/null +++ b/components/AppServices/tests/AppServices.Tests.projitems @@ -0,0 +1,11 @@ + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + true + A3106AE5-5AA9-4307-9041-E9C4232AA7F2 + + + AppServicesExperiment.Tests + + \ No newline at end of file diff --git a/components/AppServices/tests/AppServices.Tests.shproj b/components/AppServices/tests/AppServices.Tests.shproj new file mode 100644 index 000000000..3da05fab3 --- /dev/null +++ b/components/AppServices/tests/AppServices.Tests.shproj @@ -0,0 +1,13 @@ + + + + A3106AE5-5AA9-4307-9041-E9C4232AA7F2 + 14.0 + + + + + + + + From 270a0422fd8254951f0eba2e27e05c96a5ef1706 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 31 May 2023 18:27:06 +0200 Subject: [PATCH 02/20] Rename AppServices project file --- ...AppServices.csproj => CommunityToolkit.AppServices.csproj} | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) rename components/AppServices/src/{CommunityToolkit.WinUI.Controls.AppServices.csproj => CommunityToolkit.AppServices.csproj} (63%) diff --git a/components/AppServices/src/CommunityToolkit.WinUI.Controls.AppServices.csproj b/components/AppServices/src/CommunityToolkit.AppServices.csproj similarity index 63% rename from components/AppServices/src/CommunityToolkit.WinUI.Controls.AppServices.csproj rename to components/AppServices/src/CommunityToolkit.AppServices.csproj index 81981d4b7..970547da5 100644 --- a/components/AppServices/src/CommunityToolkit.WinUI.Controls.AppServices.csproj +++ b/components/AppServices/src/CommunityToolkit.AppServices.csproj @@ -3,9 +3,7 @@ AppServices This package contains AppServices. 0.0.1 - - - CommunityToolkit.WinUI.Controls.AppServicesRns + CommunityToolkit.AppServices From e9ddcf0cb03e088a8c9f8d72456806b2c33fa6cf Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 31 May 2023 18:29:04 +0200 Subject: [PATCH 03/20] Set correct package id --- components/AppServices/src/CommunityToolkit.AppServices.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/components/AppServices/src/CommunityToolkit.AppServices.csproj b/components/AppServices/src/CommunityToolkit.AppServices.csproj index 970547da5..12c2a693b 100644 --- a/components/AppServices/src/CommunityToolkit.AppServices.csproj +++ b/components/AppServices/src/CommunityToolkit.AppServices.csproj @@ -4,6 +4,7 @@ This package contains AppServices. 0.0.1 CommunityToolkit.AppServices + $(PackageIdPrefix).$(ToolkitComponentName) From 2113bc7c46ae1a9de521307a82c3c18c536b5329 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 31 May 2023 18:33:09 +0200 Subject: [PATCH 04/20] Remove imported global usings --- .../AppServices/src/CommunityToolkit.AppServices.csproj | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/components/AppServices/src/CommunityToolkit.AppServices.csproj b/components/AppServices/src/CommunityToolkit.AppServices.csproj index 12c2a693b..93d1761c3 100644 --- a/components/AppServices/src/CommunityToolkit.AppServices.csproj +++ b/components/AppServices/src/CommunityToolkit.AppServices.csproj @@ -9,4 +9,10 @@ + + + + + + From fd1d3ab96032d83b819ff94c93a0b1237b81bd04 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 31 May 2023 18:34:13 +0200 Subject: [PATCH 05/20] Add source generator project --- ...ommunityToolkit.AppServices.SourceGenerators.csproj | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 components/AppServices/CommunityToolkit.AppServices.SourceGenerators/CommunityToolkit.AppServices.SourceGenerators.csproj diff --git a/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/CommunityToolkit.AppServices.SourceGenerators.csproj b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/CommunityToolkit.AppServices.SourceGenerators.csproj new file mode 100644 index 000000000..45e69e9c3 --- /dev/null +++ b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/CommunityToolkit.AppServices.SourceGenerators.csproj @@ -0,0 +1,10 @@ + + + netstandard2.0 + + + + + + + From d0f6b2063a70bd59ba82bff8a9ee1c7e9dbad6bf Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 31 May 2023 18:37:53 +0200 Subject: [PATCH 06/20] Add .targets and packaging step for generator --- .../src/CommunityToolkit.AppServices.csproj | 13 +++++ .../src/CommunityToolkit.AppServices.targets | 55 +++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 components/AppServices/src/CommunityToolkit.AppServices.targets diff --git a/components/AppServices/src/CommunityToolkit.AppServices.csproj b/components/AppServices/src/CommunityToolkit.AppServices.csproj index 93d1761c3..6a6411fa0 100644 --- a/components/AppServices/src/CommunityToolkit.AppServices.csproj +++ b/components/AppServices/src/CommunityToolkit.AppServices.csproj @@ -15,4 +15,17 @@ + + + + + + + + + + + + + diff --git a/components/AppServices/src/CommunityToolkit.AppServices.targets b/components/AppServices/src/CommunityToolkit.AppServices.targets new file mode 100644 index 000000000..78089e0a2 --- /dev/null +++ b/components/AppServices/src/CommunityToolkit.AppServices.targets @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + @(CommunityToolkitAppServicesCurrentCompilerAssemblyIdentity->'%(Version)') + + + true + + + + + + + + + + + + + + + + + + + + From 9c81f6501ed4cbd617284e5d73b8b9d1b6d09445 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 31 May 2023 18:42:52 +0200 Subject: [PATCH 07/20] Port source generator code --- .../AnalyzerReleases.Shipped.md | 16 + .../AnalyzerReleases.Unshipped.md | 2 + .../AppServiceGenerator.Component.cs | 276 +++++++++++++++++ .../AppServiceGenerator.Helpers.cs | 27 ++ .../AppServiceGenerator.Host.cs | 291 ++++++++++++++++++ .../AppServiceGenerator.cs | 119 +++++++ ...oolkit.AppServices.SourceGenerators.csproj | 20 ++ .../InvalidAppServicesMemberAnalyzer.cs | 120 ++++++++ .../InvalidValueSetSerializerUseAnalyzer.cs | 81 +++++ .../Diagnostics/DiagnosticDescriptors.cs | 120 ++++++++ .../Diagnostics/SuppressionDescriptors.cs | 21 ++ ...ousAppServiceMethodDiagnosticSuppressor.cs | 85 +++++ .../Extensions/EnumExtensions.cs | 105 +++++++ .../Extensions/IMethodSymbolExtensions.cs | 106 +++++++ .../Extensions/INamedTypeSymbolExtensions.cs | 172 +++++++++++ .../Extensions/IParameterSymbolExtensions.cs | 34 ++ .../Extensions/ISymbolExtensions.cs | 91 ++++++ .../Extensions/ITypeSymbolExtensions.cs | 283 +++++++++++++++++ .../ParameterOrReturnTypeExtensions.cs | 35 +++ .../Extensions/SyntaxNodeExtensions.cs | 34 ++ .../Extensions/SyntaxTokenExtensions.cs | 26 ++ .../TypeDeclarationSyntaxExtensions.cs | 41 +++ .../Helpers/EquatableArray{T}.cs | 177 +++++++++++ .../Helpers/ImmutableArrayBuilder{T}.cs | 221 +++++++++++++ .../Helpers/ObjectPool{T}.cs | 163 ++++++++++ .../Models/AppServiceInfo.cs | 15 + .../Models/HierarchyInfo.Syntax.cs | 110 +++++++ .../Models/HierarchyInfo.cs | 88 ++++++ .../Models/MethodInfo.cs | 83 +++++ .../Models/ParameterInfo.cs | 157 ++++++++++ .../Models/ParameterOrReturnType.cs | 153 +++++++++ .../Models/TypeInfo.cs | 46 +++ 32 files changed, 3318 insertions(+) create mode 100644 components/AppServices/CommunityToolkit.AppServices.SourceGenerators/AnalyzerReleases.Shipped.md create mode 100644 components/AppServices/CommunityToolkit.AppServices.SourceGenerators/AnalyzerReleases.Unshipped.md create mode 100644 components/AppServices/CommunityToolkit.AppServices.SourceGenerators/AppServiceGenerator.Component.cs create mode 100644 components/AppServices/CommunityToolkit.AppServices.SourceGenerators/AppServiceGenerator.Helpers.cs create mode 100644 components/AppServices/CommunityToolkit.AppServices.SourceGenerators/AppServiceGenerator.Host.cs create mode 100644 components/AppServices/CommunityToolkit.AppServices.SourceGenerators/AppServiceGenerator.cs create mode 100644 components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Diagnostics/Analyzers/InvalidAppServicesMemberAnalyzer.cs create mode 100644 components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Diagnostics/Analyzers/InvalidValueSetSerializerUseAnalyzer.cs create mode 100644 components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs create mode 100644 components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Diagnostics/SuppressionDescriptors.cs create mode 100644 components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Diagnostics/Suppressors/SynchronousAppServiceMethodDiagnosticSuppressor.cs create mode 100644 components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Extensions/EnumExtensions.cs create mode 100644 components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Extensions/IMethodSymbolExtensions.cs create mode 100644 components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Extensions/INamedTypeSymbolExtensions.cs create mode 100644 components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Extensions/IParameterSymbolExtensions.cs create mode 100644 components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Extensions/ISymbolExtensions.cs create mode 100644 components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Extensions/ITypeSymbolExtensions.cs create mode 100644 components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Extensions/ParameterOrReturnTypeExtensions.cs create mode 100644 components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Extensions/SyntaxNodeExtensions.cs create mode 100644 components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Extensions/SyntaxTokenExtensions.cs create mode 100644 components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Extensions/TypeDeclarationSyntaxExtensions.cs create mode 100644 components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Helpers/EquatableArray{T}.cs create mode 100644 components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Helpers/ImmutableArrayBuilder{T}.cs create mode 100644 components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Helpers/ObjectPool{T}.cs create mode 100644 components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Models/AppServiceInfo.cs create mode 100644 components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Models/HierarchyInfo.Syntax.cs create mode 100644 components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Models/HierarchyInfo.cs create mode 100644 components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Models/MethodInfo.cs create mode 100644 components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Models/ParameterInfo.cs create mode 100644 components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Models/ParameterOrReturnType.cs create mode 100644 components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Models/TypeInfo.cs diff --git a/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/AnalyzerReleases.Shipped.md b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/AnalyzerReleases.Shipped.md new file mode 100644 index 000000000..d846c9b2d --- /dev/null +++ b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/AnalyzerReleases.Shipped.md @@ -0,0 +1,16 @@ +; Shipped analyzer releases +; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md + +## Release 1.0 + +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|------- +APPSRVSPR0001 | CommunityToolkit.AppServices.SourceGenerators.InvalidAppServicesMemberAnalyzer | Error | +APPSRVSPR0002 | CommunityToolkit.AppServices.SourceGenerators.InvalidAppServicesMemberAnalyzer | Error | +APPSRVSPR0003 | CommunityToolkit.AppServices.SourceGenerators.InvalidAppServicesMemberAnalyzer | Error | +APPSRVSPR0004 | CommunityToolkit.AppServices.SourceGenerators.InvalidAppServicesMemberAnalyzer | Error | +APPSRVSPR0005 | CommunityToolkit.AppServices.SourceGenerators.InvalidAppServicesMemberAnalyzer | Error | +APPSRVSPR0006 | CommunityToolkit.AppServices.SourceGenerators.InvalidValueSetSerializerUseAnalyzer | Error | +APPSRVSPR0007 | CommunityToolkit.AppServices.SourceGenerators.InvalidValueSetSerializerUseAnalyzer | Warning | diff --git a/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/AnalyzerReleases.Unshipped.md b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/AnalyzerReleases.Unshipped.md new file mode 100644 index 000000000..f2b7fad65 --- /dev/null +++ b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/AnalyzerReleases.Unshipped.md @@ -0,0 +1,2 @@ +; Unshipped analyzer release +; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md diff --git a/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/AppServiceGenerator.Component.cs b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/AppServiceGenerator.Component.cs new file mode 100644 index 000000000..edae6e6c2 --- /dev/null +++ b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/AppServiceGenerator.Component.cs @@ -0,0 +1,276 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Threading; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using CommunityToolkit.AppServices.SourceGenerators.Extensions; +using CommunityToolkit.AppServices.SourceGenerators.Helpers; +using CommunityToolkit.AppServices.SourceGenerators.Models; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace CommunityToolkit.AppServices.SourceGenerators; + +/// +partial class AppServiceGenerator : IIncrementalGenerator +{ + /// + /// Generator logic for app service components. + /// + private static class Component + { + /// + /// Gathers info on a component. + /// + /// The input instance to inspect. + /// The cancellation token for the operation. + /// The interface type and service name, or . + public static (INamedTypeSymbol? ServiceSymbol, string? ServiceName) GetInfo(INamedTypeSymbol symbol, CancellationToken token) + { + foreach (INamedTypeSymbol interfaceSymbol in symbol.Interfaces) + { + token.ThrowIfCancellationRequested(); + + if (interfaceSymbol.TryGetAppServicesNameFromAttribute(out string? serviceName)) + { + return (interfaceSymbol, serviceName); + } + } + + return default; + } + + /// + /// Gets a registering all available service endpoints. + /// + /// The input hierarchy for the component. + /// The app service info. + /// The for the component. + public static ConstructorDeclarationSyntax GetSyntax(HierarchyInfo hierarchy, AppServiceInfo info) + { + using ImmutableArrayBuilder registrationStatements = ImmutableArrayBuilder.Rent(); + + // Prepare the endpoint registrations + foreach (MethodInfo methodInfo in info.Methods) + { + if (methodInfo.Parameters.IsEmpty) + { + using ImmutableArrayBuilder endpointArguments = ImmutableArrayBuilder.Rent(); + + if (methodInfo.HasCustomValueSetSerializer) + { + // This adds the serializer expression: + // + // new () + endpointArguments.Add(Argument(ObjectCreationExpression(IdentifierName(methodInfo.FullyQualifiedValueSetSerializerTypeName)).AddArgumentListArguments())); + } + + // Add the common arguments: + // + // , "" + endpointArguments.Add(Argument(MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, ThisExpression(), IdentifierName(methodInfo.MethodName)))); + endpointArguments.Add(Argument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(methodInfo.MethodName)))); + + // This creates a registration for a parameterless endpoint: + // + // base.RegisterEndpoint(); + registrationStatements.Add( + ExpressionStatement( + InvocationExpression( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + BaseExpression(), + IdentifierName("RegisterEndpoint"))) + .AddArgumentListArguments(endpointArguments.ToArray()))); + } + else + { + using ImmutableArrayBuilder endpointStatements = ImmutableArrayBuilder.Rent(); + using ImmutableArrayBuilder endpointArguments = ImmutableArrayBuilder.Rent(); + + foreach (ParameterInfo parameterInfo in methodInfo.Parameters) + { + if (parameterInfo.Type.HasFlag(ParameterOrReturnType.IProgressOfT)) + { + using ImmutableArrayBuilder progressArguments = ImmutableArrayBuilder.Rent(); + + if (parameterInfo.HasCustomValueSetSerializer) + { + // This adds the serializer expression, like above: + // + // new () + progressArguments.Add(Argument(ObjectCreationExpression(IdentifierName(parameterInfo.FullyQualifiedValueSetSerializerTypeName)).AddArgumentListArguments())); + } + + // Add the common argument: + // + // out + progressArguments.Add( + Argument( + DeclarationExpression( + parameterInfo.GetSyntax(), + SingleVariableDesignation(Identifier(parameterInfo.Name)))) + .WithRefOrOutKeyword(Token(SyntaxKind.OutKeyword))); + + // This code prepares an argument retrieval statement for an IProgress parameter: + // + // parameters.GetProgress(); + endpointStatements.Add( + ExpressionStatement( + InvocationExpression( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName("parameters"), + IdentifierName("GetProgress"))) + .AddArgumentListArguments(progressArguments.ToArray()))); + } + else if (parameterInfo.Type.HasFlag(ParameterOrReturnType.CancellationToken)) + { + // This code prepares an argument retrieval statement for a CancellationToken parameter: + // + // parameters.GetCancellationToken(out ); + endpointStatements.Add( + ExpressionStatement( + InvocationExpression( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName("parameters"), + IdentifierName("GetCancellationToken"))) + .AddArgumentListArguments( + Argument( + DeclarationExpression( + parameterInfo.GetSyntax(), + SingleVariableDesignation(Identifier(parameterInfo.Name)))) + .WithRefOrOutKeyword(Token(SyntaxKind.OutKeyword))))); + } + else + { + using ImmutableArrayBuilder parameterRetrievalArguments = ImmutableArrayBuilder.Rent(); + + if (parameterInfo.HasCustomValueSetSerializer) + { + // This adds the serializer expression, like above: + // + // new () + parameterRetrievalArguments.Add(Argument(ObjectCreationExpression(IdentifierName(parameterInfo.FullyQualifiedValueSetSerializerTypeName)).AddArgumentListArguments())); + } + + // Add the common arguments: + // + // out , "" + parameterRetrievalArguments.Add( + Argument( + DeclarationExpression( + parameterInfo.GetSyntax(), + SingleVariableDesignation(Identifier(parameterInfo.Name)))) + .WithRefOrOutKeyword(Token(SyntaxKind.OutKeyword))); + parameterRetrievalArguments.Add(Argument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(parameterInfo.Name)))); + + // This code prepares an argument retrieval statement for a generic named parameter with a custom serializer: + // + // parameters.GetParameter(); + endpointStatements.Add( + ExpressionStatement( + InvocationExpression( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName("parameters"), + IdentifierName("GetParameter"))) + .AddArgumentListArguments(parameterRetrievalArguments.ToArray()))); + } + + // Also create the parameter argument syntax + endpointArguments.Add(Argument(IdentifierName(parameterInfo.Name))); + } + + // Add the await and optional return statement for the endpoint stub. This generates code as follows: + if (methodInfo.ReturnType == ParameterOrReturnType.Task) + { + // await (); + endpointStatements.Add( + ExpressionStatement( + AwaitExpression( + InvocationExpression(MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, ThisExpression(), IdentifierName(methodInfo.MethodName))) + .AddArgumentListArguments(endpointArguments.ToArray())))); + } + else + { + // return await (); + endpointStatements.Add( + ReturnStatement( + AwaitExpression( + InvocationExpression(MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression, ThisExpression(), IdentifierName(methodInfo.MethodName))) + .AddArgumentListArguments(endpointArguments.ToArray())))); + } + + using ImmutableArrayBuilder endpointRegistrationArguments = ImmutableArrayBuilder.Rent(); + + // Add the return type custom serializer, if needed + if (methodInfo.HasCustomValueSetSerializer) + { + endpointRegistrationArguments.Add(Argument(ObjectCreationExpression(IdentifierName(methodInfo.FullyQualifiedValueSetSerializerTypeName)).AddArgumentListArguments())); + } + + // Add the fixed arguments that are common for all cases. + // This creates a registration for an endpoint with parameters: + // + // ..., async parameters => + // { + // + // + // }, "" + endpointRegistrationArguments.Add(Argument( + SimpleLambdaExpression(Parameter(Identifier("parameters"))) + .WithAsyncKeyword(Token(SyntaxKind.AsyncKeyword)) + .AddBlockStatements(endpointStatements.ToArray()))); + endpointRegistrationArguments.Add(Argument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(methodInfo.MethodName)))); + + // This creates a registration for an endpoint with parameters: + // + // base.RegisterEndpoint(); + registrationStatements.Add( + ExpressionStatement( + InvocationExpression( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + BaseExpression(), + IdentifierName("RegisterEndpoint"))) + .AddArgumentListArguments(endpointRegistrationArguments.ToArray()))); + } + } + + + // This code produces the constructor declaration as follows: + // + // /// Creates a new instance. + // [global::System.CodeDom.Compiler.GeneratedCode("...", "...")] + // [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + // public () + // : base("") + // { + // + // } + return + ConstructorDeclaration(Identifier(hierarchy.MetadataName)) + .AddModifiers(Token(SyntaxKind.PublicKeyword)) + .WithInitializer( + ConstructorInitializer( + SyntaxKind.BaseConstructorInitializer, + ArgumentList(SingletonSeparatedList( + Argument(LiteralExpression( + SyntaxKind.StringLiteralExpression, + Literal(info.AppServiceName))))))) + .AddAttributeLists( + AttributeList(SingletonSeparatedList( + Attribute(IdentifierName("global::System.CodeDom.Compiler.GeneratedCode")) + .AddArgumentListArguments( + AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(AppServiceGenerator).FullName))), + AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(AppServiceGenerator).Assembly.GetName().Version.ToString())))))) + .WithOpenBracketToken(Token(TriviaList(Comment($"/// Creates a new instance.")), SyntaxKind.OpenBracketToken, TriviaList())), + AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage"))))) + .AddBodyStatements(registrationStatements.ToArray()); + } + } +} diff --git a/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/AppServiceGenerator.Helpers.cs b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/AppServiceGenerator.Helpers.cs new file mode 100644 index 000000000..94c333763 --- /dev/null +++ b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/AppServiceGenerator.Helpers.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CodeAnalysis; + +namespace CommunityToolkit.AppServices.SourceGenerators; + +/// +partial class AppServiceGenerator : IIncrementalGenerator +{ + /// + /// Shader generators logic for app service hosts and components. + /// + private static class Helpers + { + /// + /// Gets whether the current target is a UWP application. + /// + /// The input instance to inspect. + /// Whether the current target is a UWP application. + public static bool IsUwpTarget(Compilation compilation) + { + return compilation.Options.OutputKind == OutputKind.WindowsRuntimeApplication; + } + } +} diff --git a/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/AppServiceGenerator.Host.cs b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/AppServiceGenerator.Host.cs new file mode 100644 index 000000000..bc2551a7d --- /dev/null +++ b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/AppServiceGenerator.Host.cs @@ -0,0 +1,291 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using CommunityToolkit.AppServices.SourceGenerators.Helpers; +using CommunityToolkit.AppServices.SourceGenerators.Models; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace CommunityToolkit.AppServices.SourceGenerators; + +/// +partial class AppServiceGenerator : IIncrementalGenerator +{ + /// + /// Generator logic for app service hosts. + /// + private static class Host + { + /// + /// Gets the app service name for a host interface. + /// + /// The data for the attribute over the service interface. + /// The service name, or . + public static bool TryGetAppServiceName(AttributeData attributeData, [NotNullWhen(true)] out string? appServiceName) + { + if (attributeData.ConstructorArguments[0].Value is string { Length: > 0 } name) + { + appServiceName = name; + + return true; + } + + appServiceName = null; + + return false; + } + + /// + /// Gets a registering the service name. + /// + /// The input hierarchy for the host. + /// The app service info. + /// The for the host. + public static ConstructorDeclarationSyntax GetConstructorSyntax(HierarchyInfo hierarchy, AppServiceInfo info) + { + + // This code produces the constructor declaration as follows: + // + // /// Creates a new instance. + // [global::System.CodeDom.Compiler.GeneratedCode("...", "...")] + // [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + // public () + // : base("") + // { + // } + return + ConstructorDeclaration(Identifier(hierarchy.MetadataName)) + .AddModifiers(Token(SyntaxKind.PublicKeyword)) + .WithInitializer( + ConstructorInitializer( + SyntaxKind.BaseConstructorInitializer, + ArgumentList(SingletonSeparatedList( + Argument(LiteralExpression( + SyntaxKind.StringLiteralExpression, + Literal(info.AppServiceName))))))) + .AddAttributeLists( + AttributeList(SingletonSeparatedList( + Attribute(IdentifierName("global::System.CodeDom.Compiler.GeneratedCode")) + .AddArgumentListArguments( + AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(AppServiceGenerator).FullName))), + AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(AppServiceGenerator).Assembly.GetName().Version.ToString())))))) + .WithOpenBracketToken(Token(TriviaList(Comment($"/// Creates a new instance.")), SyntaxKind.OpenBracketToken, TriviaList())), + AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage"))))) + .WithBody(Block()); + } + + /// + /// Gets a collection with all host methods for a given service. + /// + /// The app service info. + /// The collection for the host. + public static ImmutableArray GetMethodDeclarationsSyntax(AppServiceInfo info) + { + using ImmutableArrayBuilder methodDeclarations = ImmutableArrayBuilder.Rent(); + + // Prepare the method declarations + foreach (MethodInfo methodInfo in info.Methods) + { + using ImmutableArrayBuilder requestStatements = ImmutableArrayBuilder.Rent(); + using ImmutableArrayBuilder methodParameters = ImmutableArrayBuilder.Rent(); + + // This code produces a statement creating the request: + // + // var request = base.CreateAppServiceRequest(); + requestStatements.Add( + LocalDeclarationStatement( + VariableDeclaration( + IdentifierName(Identifier(TriviaList(), SyntaxKind.VarKeyword, "var", "var", TriviaList()))) + .AddVariables( + VariableDeclarator(Identifier("request")) + .WithInitializer(EqualsValueClause( + InvocationExpression( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + BaseExpression(), + IdentifierName("CreateAppServiceRequest")))))))); + + // Add the parameters and the optional progress, if any + foreach (ParameterInfo parameterInfo in methodInfo.Parameters) + { + if (parameterInfo.Type.HasFlag(ParameterOrReturnType.IProgressOfT)) + { + using ImmutableArrayBuilder progressArguments = ImmutableArrayBuilder.Rent(); + + if (parameterInfo.HasCustomValueSetSerializer) + { + // This adds the serializer expression, like above: + // + // new () + progressArguments.Add(Argument(ObjectCreationExpression(IdentifierName(parameterInfo.FullyQualifiedValueSetSerializerTypeName)).AddArgumentListArguments())); + } + + // Add the common argument: + // + // + progressArguments.Add(Argument(IdentifierName(parameterInfo.Name))); + + // This produces the following expression: + // + // request = request.WithProgress(); + requestStatements.Add( + ExpressionStatement( + AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + IdentifierName("request"), + InvocationExpression( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName("request"), + IdentifierName("WithProgress"))) + .AddArgumentListArguments(progressArguments.ToArray())))); + } + else if (parameterInfo.Type.HasFlag(ParameterOrReturnType.CancellationToken)) + { + // This produces the following expression: + // + // request = request.WithCancellationToken(); + requestStatements.Add( + ExpressionStatement( + AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + IdentifierName("request"), + InvocationExpression( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName("request"), + IdentifierName("WithCancellationToken"))) + .AddArgumentListArguments( + Argument(IdentifierName(parameterInfo.Name)))))); + } + else + { + using ImmutableArrayBuilder parameterArguments = ImmutableArrayBuilder.Rent(); + + if (parameterInfo.HasCustomValueSetSerializer) + { + // This adds the serializer expression: + // + // new () + parameterArguments.Add(Argument(ObjectCreationExpression(IdentifierName(parameterInfo.FullyQualifiedValueSetSerializerTypeName)).AddArgumentListArguments())); + } + + // Add the common arguments: + // + // , "" + parameterArguments.Add(Argument(IdentifierName(parameterInfo.Name))); + parameterArguments.Add(Argument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(parameterInfo.Name)))); + + // This produces the following expression: + // + // request = request.WithParameter(); + requestStatements.Add( + ExpressionStatement( + AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + IdentifierName("request"), + InvocationExpression( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName("request"), + IdentifierName("WithParameter"))) + .AddArgumentListArguments(parameterArguments.ToArray())))); + } + + // Also prepare the method parameter + methodParameters.Add( + Parameter(Identifier(parameterInfo.Name)) + .WithType(parameterInfo.GetSyntax())); + } + + TypeSyntax returnType; + + // Send the request, with the following code: + if (methodInfo.ReturnType == ParameterOrReturnType.Task) + { + // global::System.Threading.Tasks.Task + returnType = IdentifierName(Identifier("global::System.Threading.Tasks.Task")); + + // return request.SendAndWaitForResultAsync(); + requestStatements.Add( + ReturnStatement( + InvocationExpression( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName("request"), + IdentifierName(Identifier("SendAndWaitForResultAsync")))))); + } + else + { + // global::System.Threading.Tasks.Task<> + returnType = + GenericName(Identifier("global::System.Threading.Tasks.Task")) + .AddTypeArgumentListArguments(ParameterInfo.GetSyntax(methodInfo.ReturnType, methodInfo.FullyQualifiedReturnTypeName)); + + // Prepare the serializer, if available: + // + // new (), or nothing + ArgumentSyntax[] arguments = methodInfo.HasCustomValueSetSerializer switch + { + true => new[] { Argument(ObjectCreationExpression(IdentifierName(methodInfo.FullyQualifiedValueSetSerializerTypeName)).AddArgumentListArguments()) }, + false => Array.Empty() + }; + + // Prepare the type arguments for the method invocation: + // + // <, > + TypeSyntax[] typeArguents = methodInfo.HasCustomValueSetSerializer switch + { + true => new[] { IdentifierName(methodInfo.FullyQualifiedValueSetSerializerTypeName), ParameterInfo.GetSyntax(methodInfo.ReturnType, methodInfo.FullyQualifiedReturnTypeName) }, + false => new[] { ParameterInfo.GetSyntax(methodInfo.ReturnType, methodInfo.FullyQualifiedReturnTypeName) } + }; + + // return request.SendAndWaitForResultAsync<>(); + requestStatements.Add( + ReturnStatement( + InvocationExpression( + MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + IdentifierName("request"), + GenericName(Identifier("SendAndWaitForResultAsync")) + .AddTypeArgumentListArguments(typeArguents))) + .AddArgumentListArguments(arguments))); + } + + // Prepare the method declaration: + // + // /// + // [global::System.CodeDom.Compiler.GeneratedCode("...", "...")] + // [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] + // public () + // { + // + // } + methodDeclarations.Add( + MethodDeclaration( + returnType, + Identifier(methodInfo.MethodName)) + .AddModifiers(Token(SyntaxKind.PublicKeyword)) + .AddParameterListParameters(methodParameters.ToArray()) + .AddBodyStatements(requestStatements.ToArray()) + .AddAttributeLists( + AttributeList(SingletonSeparatedList( + Attribute(IdentifierName("global::System.CodeDom.Compiler.GeneratedCode")) + .AddArgumentListArguments( + AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(AppServiceGenerator).FullName))), + AttributeArgument(LiteralExpression(SyntaxKind.StringLiteralExpression, Literal(typeof(AppServiceGenerator).Assembly.GetName().Version.ToString())))))) + .WithOpenBracketToken(Token(TriviaList(Comment("/// ")), SyntaxKind.OpenBracketToken, TriviaList())), + AttributeList(SingletonSeparatedList(Attribute(IdentifierName("global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage")))))); + } + + + return methodDeclarations.ToImmutable(); + } + } +} diff --git a/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/AppServiceGenerator.cs b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/AppServiceGenerator.cs new file mode 100644 index 000000000..7c24796ae --- /dev/null +++ b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/AppServiceGenerator.cs @@ -0,0 +1,119 @@ +// ------------------------------------------------------ +// Copyright (C) Microsoft. All rights reserved. +// ------------------------------------------------------ + +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using CommunityToolkit.AppServices.SourceGenerators.Extensions; +using CommunityToolkit.AppServices.SourceGenerators.Models; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace CommunityToolkit.AppServices.SourceGenerators; + +/// +/// A source generator for the AppServiceAttribute type. +/// +[Generator(LanguageNames.CSharp)] +public sealed partial class AppServiceGenerator : IIncrementalGenerator +{ + /// + public void Initialize(IncrementalGeneratorInitializationContext context) + { + // Get all app service class implementations, and only enable this branch if the target is not a UWP app (the component) + IncrementalValuesProvider<(HierarchyInfo Hierarchy, AppServiceInfo Info)> appServiceComponentInfo = + context.SyntaxProvider + .CreateSyntaxProvider( + static (node, _) => node is ClassDeclarationSyntax classDeclaration && classDeclaration.HasOrPotentiallyHasBaseTypes(), + static (context, token) => + { + // Only retrieve host info if the target is not a UWP application + if (Helpers.IsUwpTarget(context.SemanticModel.Compilation)) + { + return default; + } + + INamedTypeSymbol typeSymbol = (INamedTypeSymbol)context.SemanticModel.GetDeclaredSymbol(context.Node, token)!; + + // Only select the first declaration of a given item, to avoid issues with partial types + if (!context.Node.IsFirstSyntaxDeclarationForSymbol(typeSymbol)) + { + return default; + } + + // Try to get the info on the current component + (INamedTypeSymbol? serviceSymbol, string? appServiceName) = Component.GetInfo(typeSymbol, token); + + // If there's no app service interface, do nothing + if (serviceSymbol is null) + { + return default; + } + + HierarchyInfo hierarchy = HierarchyInfo.From(typeSymbol); + ImmutableArray methods = MethodInfo.From(serviceSymbol); + + return (Hierarchy: hierarchy, new AppServiceInfo(methods, appServiceName!, typeSymbol.GetFullyQualifiedName())); + }) + .Where(static pair => pair.Hierarchy is not null); + + // Produce the component type + context.RegisterSourceOutput(appServiceComponentInfo, static (context, item) => + { + ConstructorDeclarationSyntax constructorSyntax = Component.GetSyntax(item.Hierarchy, item.Info); + CompilationUnitSyntax compilationUnit = item.Hierarchy.GetCompilationUnit( + ImmutableArray.Create(constructorSyntax), + ImmutableArray.Create(SimpleBaseType(IdentifierName("global::CommunityToolkit.AppServices.AppServiceComponent"))), + "/// "); + + context.AddSource($"{item.Hierarchy.FilenameHint}.g.cs", compilationUnit.GetText(Encoding.UTF8)); + }); + + // Gather all interfaces, and only enable this branch if the target is a UWP app (the host) + IncrementalValuesProvider<(HierarchyInfo Hierarchy, AppServiceInfo Info)> appServiceHostInfo = + context.SyntaxProvider + .ForAttributeWithMetadataName( + "CommunityToolkit.AppServices.AppServiceAttribute", + static (node, _) => node is InterfaceDeclarationSyntax, + static (context, token) => + { + // Only retrieve host info if the target is a UWP application + if (!Helpers.IsUwpTarget(context.SemanticModel.Compilation)) + { + return default; + } + + // Check if the current interface is in fact an app service type + if (!Host.TryGetAppServiceName(context.Attributes[0], out string? appServiceName)) + { + return default; + } + + INamedTypeSymbol typeSymbol = (INamedTypeSymbol)context.TargetSymbol; + + // Get the info on the host implementation + HierarchyInfo hierarchy = HierarchyInfo.From(typeSymbol, typeSymbol.Name.Substring(1)); + ImmutableArray methods = MethodInfo.From(typeSymbol); + + return (Hierarchy: hierarchy, new AppServiceInfo(methods, appServiceName, typeSymbol.GetFullyQualifiedName())); + }) + .Where(static item => item.Hierarchy is not null); + + // Produce the host type + context.RegisterSourceOutput(appServiceHostInfo, static (context, item) => + { + ConstructorDeclarationSyntax constructorSyntax = Host.GetConstructorSyntax(item.Hierarchy, item.Info); + ImmutableArray methodDeclarations = Host.GetMethodDeclarationsSyntax(item.Info); + CompilationUnitSyntax compilationUnit = item.Hierarchy.GetCompilationUnit( + ImmutableArray.Create(constructorSyntax).AddRange(methodDeclarations), + ImmutableArray.Create( + SimpleBaseType(IdentifierName("global::CommunityToolkit.AppServices.AppServiceHost")), + SimpleBaseType(IdentifierName(item.Info.InterfaceFullyQualifiedName))), + $"/// A generated host implementation for the interface."); + + context.AddSource($"{item.Hierarchy.FilenameHint}.g.cs", compilationUnit.GetText(Encoding.UTF8)); + }); + } +} diff --git a/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/CommunityToolkit.AppServices.SourceGenerators.csproj b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/CommunityToolkit.AppServices.SourceGenerators.csproj index 45e69e9c3..64d80c88a 100644 --- a/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/CommunityToolkit.AppServices.SourceGenerators.csproj +++ b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/CommunityToolkit.AppServices.SourceGenerators.csproj @@ -1,8 +1,28 @@ netstandard2.0 + false + true + + + $(NoWarn);CS8500 + + + + + + + + + + diff --git a/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Diagnostics/Analyzers/InvalidAppServicesMemberAnalyzer.cs b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Diagnostics/Analyzers/InvalidAppServicesMemberAnalyzer.cs new file mode 100644 index 000000000..6aea09d12 --- /dev/null +++ b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Diagnostics/Analyzers/InvalidAppServicesMemberAnalyzer.cs @@ -0,0 +1,120 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using CommunityToolkit.AppServices.SourceGenerators.Extensions; +using CommunityToolkit.AppServices.SourceGenerators.Models; +using static CommunityToolkit.AppServices.SourceGenerators.Diagnostics.DiagnosticDescriptors; + +namespace CommunityToolkit.AppServices.SourceGenerators; + +/// +/// A diagnostic analyzer that emits diagnostics whenever an app service has an invalid member. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class InvalidAppServicesMemberAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create( + InvalidAppServicesMemberType, + InvalidAppServicesMethodReturnType, + InvalidAppServicesMethodParameterType, + InvalidRepeatedAppServicesMethodIProgressParameter, + InvalidRepeatedAppServicesMethodCancellationTokenParameter); + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.EnableConcurrentExecution(); + + // Register a callback for all named type symbols (ie. user defined types) + context.RegisterSymbolAction(static context => + { + // The symbol must be an interface + if (context.Symbol is not INamedTypeSymbol { TypeKind: TypeKind.Interface } interfaceSymbol) + { + return; + } + + INamedTypeSymbol appServicesAttributeSymbol = context.Compilation.GetTypeByMetadataName("CommunityToolkit.AppServices.AppServiceAttribute")!; + + // Check whether the interface is an app services interface + if (!interfaceSymbol.HasOrInheritsAttribute(appServicesAttributeSymbol)) + { + return; + } + + // Go through all interface members to analyze them. Here we need to go through all members, not just the ones immediately + // declared, as it's possible an interface with an invalid member will be inherited by another one that adds [AppServices]. + // In that case, the base interface will not be analyzed (as it doesn't have [AppServices]), so the derived one will need + // to also go through inherited members to ensure that all members that the generator will process will actually be valid. + foreach (ISymbol memberSymbol in interfaceSymbol.GetAllMembers()) + { + // If a method is not abstract nor virtual (ie. a DIM or static non-virtual interface member), it can just be ignored. + // The generated service type will not have to consider it as far as registering endpoints and generating members goes. + if (memberSymbol.IsIgnoredAppServicesMember()) + { + continue; + } + + // All remaining members must be non-generic instance methods, which the generator will emit + if (memberSymbol is not IMethodSymbol { IsStatic: false, IsGenericMethod: false, ReturnType: INamedTypeSymbol returnTypeSymbol } methodSymbol) + { + context.ReportDiagnostic(Diagnostic.Create(InvalidAppServicesMemberType, memberSymbol.Locations.FirstOrDefault(), memberSymbol, interfaceSymbol)); + + continue; + } + + // Validate the return type for the current method + if (!methodSymbol.TryGetParameterOrReturnType(out ParameterOrReturnType returnType) || + !returnType.IsValidReturnType()) + { + context.ReportDiagnostic(Diagnostic.Create(InvalidAppServicesMethodReturnType, memberSymbol.Locations.FirstOrDefault(), methodSymbol, interfaceSymbol, returnTypeSymbol)); + } + + int progressParametersCount = 0; + int cancellationTokenParametersCount = 0; + + // Validate the method parameters + foreach (IParameterSymbol parameter in methodSymbol.Parameters) + { + // First validate types that could possibly be allowed at all (ie. valid types) + if (!parameter.TryGetParameterOrReturnType(out ParameterOrReturnType parameterType) || + !parameterType.IsValidParameterType()) + { + context.ReportDiagnostic(Diagnostic.Create(InvalidAppServicesMethodParameterType, parameter.Locations.FirstOrDefault(), parameter.Name, methodSymbol, interfaceSymbol, parameter.Type)); + + continue; + } + + // Then check that the type is not an IProgress, if one has already been discovered + if (parameterType.HasFlag(ParameterOrReturnType.IProgressOfT)) + { + progressParametersCount++; + + if (progressParametersCount > 1) + { + context.ReportDiagnostic(Diagnostic.Create(InvalidRepeatedAppServicesMethodIProgressParameter, parameter.Locations.FirstOrDefault(), parameter.Name, methodSymbol, interfaceSymbol, parameter.Type)); + } + } + + // Lastly, check that the type is not a CancellationToken, if one has already been discovered + if (parameterType.HasFlag(ParameterOrReturnType.CancellationToken)) + { + cancellationTokenParametersCount++; + + if (cancellationTokenParametersCount > 1) + { + context.ReportDiagnostic(Diagnostic.Create(InvalidRepeatedAppServicesMethodCancellationTokenParameter, parameter.Locations.FirstOrDefault(), parameter.Name, methodSymbol, interfaceSymbol, parameter.Type)); + } + } + } + } + }, SymbolKind.NamedType); + } +} diff --git a/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Diagnostics/Analyzers/InvalidValueSetSerializerUseAnalyzer.cs b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Diagnostics/Analyzers/InvalidValueSetSerializerUseAnalyzer.cs new file mode 100644 index 000000000..97b5369a2 --- /dev/null +++ b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Diagnostics/Analyzers/InvalidValueSetSerializerUseAnalyzer.cs @@ -0,0 +1,81 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using CommunityToolkit.AppServices.SourceGenerators.Extensions; +using static CommunityToolkit.AppServices.SourceGenerators.Diagnostics.DiagnosticDescriptors; + +namespace CommunityToolkit.AppServices.SourceGenerators; + +/// +/// A diagnostic analyzer that emits diagnostics whenever a value set serializer type is not valid. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class InvalidValueSetSerializerUseAnalyzer : DiagnosticAnalyzer +{ + /// + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create(InvalidValueSetSerializerType, InvalidValueSetSerializerLocation); + + /// + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); + context.EnableConcurrentExecution(); + + // Register a callback for all named type symbols (ie. user defined types) + context.RegisterSyntaxNodeAction(static context => + { + // Try to get the associated symbol for the current node. There are two cases we are interested in: + // - int Foo([Attribute] int bar), ie. an attribute on a parameter node + // - [return: Attribute] int Foo(), ie. an attribute on a return value + ISymbol? associatedSymbol = context.Node switch + { + { Parent.Parent: ParameterSyntax parameter } + => context.SemanticModel.GetDeclaredSymbol(parameter, context.CancellationToken), + { Parent: AttributeListSyntax { Target.Identifier: SyntaxToken(SyntaxKind.ReturnKeyword), Parent: MethodDeclarationSyntax method } } + => context.SemanticModel.GetDeclaredSymbol(method, context.CancellationToken), + _ => null + }; + + // We're only looking for methods and parameters + if (associatedSymbol is not (IMethodSymbol or IParameterSymbol)) + { + return; + } + + // We only care about cases where [ValueSetSerializer] is used + if (!associatedSymbol.TryGetValueSetSerializerTypeFromAttribute(out INamedTypeSymbol? serializerType)) + { + return; + } + + // Validate the attribute location + if ((associatedSymbol as IMethodSymbol ?? ((IParameterSymbol)associatedSymbol).ContainingSymbol) is not IMethodSymbol methodSymbol || + methodSymbol.ContainingType is not INamedTypeSymbol { TypeKind: TypeKind.Interface } interfaceSymbol || + !interfaceSymbol.TryGetAppServicesNameFromAttribute(out _)) + { + // If the location is invalid, the attribute has no effect, so we emit this diagnostic and just stop here + context.ReportDiagnostic(Diagnostic.Create(InvalidValueSetSerializerLocation, context.Node.GetLocation(), associatedSymbol)); + + return; + } + + // Look for a public parameterless constructor + foreach (IMethodSymbol constructor in serializerType.InstanceConstructors) + { + if (constructor is { DeclaredAccessibility: Accessibility.Public, Parameters.IsEmpty: true }) + { + return; + } + } + + // The type isn't valid, emit a diagnostic + context.ReportDiagnostic(Diagnostic.Create(InvalidValueSetSerializerType, context.Node.GetLocation(), serializerType)); + }, ImmutableArray.Create(SyntaxKind.Attribute)); + } +} diff --git a/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs new file mode 100644 index 000000000..b71b275c6 --- /dev/null +++ b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Diagnostics/DiagnosticDescriptors.cs @@ -0,0 +1,120 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CodeAnalysis; + +#pragma warning disable IDE0090 // Use 'new(...)' for field initializers, suppressed as it breaks a Roslyn analyzer + +namespace CommunityToolkit.AppServices.SourceGenerators.Diagnostics; + +/// +/// A container for all instances for errors reported by analyzers in this project. +/// +internal static class DiagnosticDescriptors +{ + /// + /// Gets a indicating when an app services interface declares a member of an invalid type. + /// + /// Format: "Cannot declare member "{0}" in interface {1}, as it is not a valid member type for an [AppServices] interface type (only non-generic instance methods or static non-virtual DIMs are allowed)". + /// + /// + public static readonly DiagnosticDescriptor InvalidAppServicesMemberType = new DiagnosticDescriptor( + id: "APPSRVSPR0001", + title: "Invalid [AppServices] interface member declaration", + messageFormat: "Cannot declare member \"{0}\" in interface {1}, as it is not a valid member type for an [AppServices] interface type (only non-generic instance methods or static non-virtual DIMs are allowed)", + category: typeof(InvalidAppServicesMemberAnalyzer).FullName, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "Only non-generic instance methods or static non-virtual DIMs are allowed as members for interfaces annotated with the [AppServices] attribute."); + + /// + /// Gets a indicating when an app services interface declares a method with an invalid return type. + /// + /// Format: "Method "{0}" in interface {1} has an invalid return type ({2}) for an [AppServices] method (only Task and Task<T> types, where T is a supported primitive or enum type, foundation type or SZ array with a supported element type, or has a custom serializer specified, are allowed)". + /// + /// + public static readonly DiagnosticDescriptor InvalidAppServicesMethodReturnType = new DiagnosticDescriptor( + id: "APPSRVSPR0002", + title: "Invalid return type in [AppServices] interface method", + messageFormat: "Method \"{0}\" in interface {1} has an invalid return type ({2}) for an [AppServices] method (only Task and Task types, where T is a supported primitive or enum type, foundation type or SZ array with a supported element type, or has a custom serializer specified, are allowed)", + category: typeof(InvalidAppServicesMemberAnalyzer).FullName, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "Only Task and Task with valid result types are allowed as return types for [AppServices] methods."); + + /// + /// Gets a indicating when an app services interface declares a method with an invalid parameter type. + /// + /// Format: "Parameter "{0}" in method "{1}" in interface {2} has an invalid type ({3}) for an [AppServices] method (only supported primitive or enum types, foundation types and SZ arrays with a supported element type, IProgress<T>, CancellationToken or types with a custom serializer specified, are allowed)". + /// + /// + public static readonly DiagnosticDescriptor InvalidAppServicesMethodParameterType = new DiagnosticDescriptor( + id: "APPSRVSPR0003", + title: "Invalid parameter type in [AppServices] interface method", + messageFormat: "Parameter \"{0}\" in method \"{1}\" in interface {2} has an invalid type ({3}) for an [AppServices] method (only supported primitive or enum types, foundation types and SZ arrays with a supported element type, IProgress, CancellationToken or types with a custom serializer specified, are allowed)", + category: typeof(InvalidAppServicesMemberAnalyzer).FullName, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "Only supported primitive types, foundation types and SZ arrays with these types as element types, IProgress, CancellationToken or types with a custom serializer specified, are allowed as parameter types for [AppServices] methods."); + + /// + /// Gets a indicating when an app services interface declares a method that takes more than an IProgress<T> parameter. + /// + /// Format: "Parameter "{0}" in method "{1}" in interface {2} has an invalid type ({3}) for an [AppServices] method, as its containing method already has an IProgress<T> parameter (only one is allowed)". + /// + /// + public static readonly DiagnosticDescriptor InvalidRepeatedAppServicesMethodIProgressParameter = new DiagnosticDescriptor( + id: "APPSRVSPR0004", + title: "Invalid parameter type in [AppServices] interface method", + messageFormat: "Parameter \"{0}\" in method \"{1}\" in interface {2} has an invalid type ({3}) for an [AppServices] method, as its containing method already has an IProgress parameter (only one is allowed)", + category: typeof(InvalidAppServicesMemberAnalyzer).FullName, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "Only a single IProgress parameter can be used in an [AppServices] method."); + + /// + /// Gets a indicating when an app services interface declares a method that takes more than a CancellationToken parameter. + /// + /// Format: "Parameter "{0}" in method "{1}" in interface {2} has an invalid type ({3}) for an [AppServices] method, as its containing method already has a CancellationToken parameter (only one is allowed)". + /// + /// + public static readonly DiagnosticDescriptor InvalidRepeatedAppServicesMethodCancellationTokenParameter = new DiagnosticDescriptor( + id: "APPSRVSPR0005", + title: "Invalid parameter type in [AppServices] interface method", + messageFormat: "Parameter \"{0}\" in method \"{1}\" in interface {2} has an invalid type ({3}) for an [AppServices] method, as its containing method already has a CancellationToken parameter (only one is allowed)", + category: typeof(InvalidAppServicesMemberAnalyzer).FullName, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "Only a single CancellationToken parameter can be used in an [AppServices] method."); + + /// + /// Gets a indicating when a value set serializer type is invalid. + /// + /// Format: "Type {0} cannot be used as a custom ValueSet serializer type, as it lacks a public parameterless constructor". + /// + /// + public static readonly DiagnosticDescriptor InvalidValueSetSerializerType = new DiagnosticDescriptor( + id: "APPSRVSPR0006", + title: "Invalid ValueSet serializer type", + messageFormat: "Type {0} cannot be used as a custom serializer type, as it lacks a public parameterless constructor", + category: typeof(InvalidValueSetSerializerUseAnalyzer).FullName, + defaultSeverity: DiagnosticSeverity.Error, + isEnabledByDefault: true, + description: "Only types with a public parameterless constructor can be used as custom ValueSet serializer types."); + + /// + /// Gets a indicating when a value set serializer use is invalid. + /// + /// Format: "Method or parameter named "{0}" cannot request a custom ValueSet serializer, as this is only enabled for methods and parameters in an [AppServices] interface". + /// + /// + public static readonly DiagnosticDescriptor InvalidValueSetSerializerLocation = new DiagnosticDescriptor( + id: "APPSRVSPR0007", + title: "Invalid ValueSet serializer use", + messageFormat: "Method or parameter named \"{0}\" cannot request a custom ValueSet serializer, as this is only enabled for methods and parameters in an [AppServices] interface", + category: typeof(InvalidValueSetSerializerUseAnalyzer).FullName, + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true, + description: "Only methods and method parameters of methods in an [AppServices] interface should request a custom ValueSet serializer."); +} diff --git a/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Diagnostics/SuppressionDescriptors.cs b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Diagnostics/SuppressionDescriptors.cs new file mode 100644 index 000000000..a49c25435 --- /dev/null +++ b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Diagnostics/SuppressionDescriptors.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CodeAnalysis; + +namespace CommunityToolkit.AppServices.SourceGenerators.Diagnostics; + +/// +/// A container for all instances for suppressed diagnostics by analyzers in this project. +/// +internal static class SuppressionDescriptors +{ + /// + /// Gets a for a synchronous AppService method using the async modifier. + /// + public static readonly SuppressionDescriptor SynchronousAppServiceMethod = new( + id: "APPSRVSPR0001", + suppressedDiagnosticId: "CS1998", + justification: "All AppService methods must return a Task, but components implementing them might not need them to be asynchronous (but making them so simplifies the code and normalizes exceptions)"); +} diff --git a/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Diagnostics/Suppressors/SynchronousAppServiceMethodDiagnosticSuppressor.cs b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Diagnostics/Suppressors/SynchronousAppServiceMethodDiagnosticSuppressor.cs new file mode 100644 index 000000000..e58ceb2ba --- /dev/null +++ b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Diagnostics/Suppressors/SynchronousAppServiceMethodDiagnosticSuppressor.cs @@ -0,0 +1,85 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using CommunityToolkit.AppServices.SourceGenerators.Extensions; +using static CommunityToolkit.AppServices.SourceGenerators.Diagnostics.SuppressionDescriptors; + +namespace CommunityToolkit.AppServices.SourceGenerators; + +/// +/// +/// A diagnostic suppressor to suppress CS1998 warnings for synchronous AppService methods using the async modifier. +/// +/// +/// That is, this diagnostic suppressor will suppress the following diagnostic: +/// +/// public partial class MyAppService : IMyAppService +/// { +/// public async Task<string> GreetUserAsync() +/// { +/// return "Hello world"; +/// } +/// } +/// +/// +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class SynchronousAppServiceMethodDiagnosticSuppressor : DiagnosticSuppressor +{ + /// + public override ImmutableArray SupportedSuppressions => ImmutableArray.Create(SynchronousAppServiceMethod); + + /// + public override void ReportSuppressions(SuppressionAnalysisContext context) + { + foreach (Diagnostic diagnostic in context.ReportedDiagnostics) + { + SuppressDiagnosticIfNeeded(context, diagnostic); + } + } + + /// + /// Suppresses a given diagnostic, if applicable. + /// + /// The instance currently in use. + /// The candidate to suppress, if needed. + private static void SuppressDiagnosticIfNeeded(SuppressionAnalysisContext context, Diagnostic diagnostic) + { + SyntaxNode? syntaxNode = diagnostic.Location.SourceTree?.GetRoot(context.CancellationToken).FindNode(diagnostic.Location.SourceSpan); + + // The target node has to be a method declaration inside a class + if (syntaxNode is MethodDeclarationSyntax { Parent: ClassDeclarationSyntax }) + { + // Get the symbol for the current syntax node + ISymbol? declaredSymbol = context.GetSemanticModel(syntaxNode.SyntaxTree).GetDeclaredSymbol(syntaxNode, context.CancellationToken); + + // Get the method symbol and the type symbol for the containing type + if (declaredSymbol is IMethodSymbol { ContainingType: INamedTypeSymbol classSymbol } methodSymbol) + { + foreach (INamedTypeSymbol interfaceSymbol in classSymbol.Interfaces) + { + // Check whether this interface implemented by the containing type is an AppService interface + if (interfaceSymbol.TryGetAppServicesNameFromAttribute(out _)) + { + foreach (IMethodSymbol interfaceMethodSymbol in interfaceSymbol.GetMembers().OfType()) + { + // For each interface method, get the implementation in the current class and check if it's the same as the current method + if (classSymbol.FindImplementationForInterfaceMember(interfaceMethodSymbol) is IMethodSymbol implementedMethodSymbol && + SymbolEqualityComparer.Default.Equals(methodSymbol, implementedMethodSymbol)) + { + context.ReportSuppression(Suppression.Create(SynchronousAppServiceMethod, diagnostic)); + + return; + } + } + } + } + } + } + } +} diff --git a/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Extensions/EnumExtensions.cs b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Extensions/EnumExtensions.cs new file mode 100644 index 000000000..11f0980b4 --- /dev/null +++ b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Extensions/EnumExtensions.cs @@ -0,0 +1,105 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace CommunityToolkit.AppServices.SourceGenerators.Extensions; + +/// +/// Extension methods for types. +/// +internal static class EnumExtensions +{ + /// + /// Checks whether a given enum has a flag, without boxing. + /// + /// The type. + /// The input value. + /// The flag. + /// + /// This is needed over because source generators run on a .NET Framework host, + /// and that API is not a JIT intrinsic there, meaning it will box the input value every single time it is called. + /// + public static unsafe bool HasFlag(this T value, T flag) + where T : unmanaged, Enum + { + if (sizeof(T) == sizeof(byte)) + { + byte value8 = *(byte*)&value; + byte flag8 = *(byte*)&flag; + + return (value8 & flag8) == flag8; + } + + if (sizeof(T) == sizeof(ushort)) + { + ushort value16 = *(ushort*)&value; + ushort flag16 = *(ushort*)&flag; + + return (value16 & flag16) == flag16; + } + + if (sizeof(T) == sizeof(uint)) + { + uint value32 = *(uint*)&value; + uint flag32 = *(uint*)&flag; + + return (value32 & flag32) == flag32; + } + + if (sizeof(T) == sizeof(ulong)) + { + ulong value64 = *(ulong*)&value; + ulong flag64 = *(ulong*)&flag; + + return (value64 & flag64) == flag64; + } + + throw new ArgumentException("Invalid enum type."); + } + + /// + /// Checks whether a given enum has any of the input flags, without boxing. + /// + /// The type. + /// The input value. + /// The flags. + public static unsafe bool HasAnyFlags(this T value, T flags) + where T : unmanaged, Enum + { + if (sizeof(T) == sizeof(byte)) + { + byte value8 = *(byte*)&value; + byte flags8 = *(byte*)&flags; + + return (value8 & flags8) != 0; + } + + if (sizeof(T) == sizeof(ushort)) + { + ushort value16 = *(ushort*)&value; + ushort flags16 = *(ushort*)&flags; + + return (value16 & flags16) != 0; + } + + if (sizeof(T) == sizeof(uint)) + { + uint value32 = *(uint*)&value; + uint flags32 = *(uint*)&flags; + + return (value32 & flags32) != 0; + } + + if (sizeof(T) == sizeof(ulong)) + { + ulong value64 = *(ulong*)&value; + ulong flags64 = *(ulong*)&flags; + + return (value64 & flags64) != 0; + } + + throw new ArgumentException("Invalid enum type."); + } +} \ No newline at end of file diff --git a/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Extensions/IMethodSymbolExtensions.cs b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Extensions/IMethodSymbolExtensions.cs new file mode 100644 index 000000000..5ab7d61bc --- /dev/null +++ b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Extensions/IMethodSymbolExtensions.cs @@ -0,0 +1,106 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CodeAnalysis; +using CommunityToolkit.AppServices.SourceGenerators.Models; + +namespace CommunityToolkit.AppServices.SourceGenerators.Extensions; + +/// +/// Extension methods for the type. +/// +internal static partial class IMethodSymbolExtensions +{ + /// + /// Tries to parse the value from an input method symbol. + /// + /// The input instance. + /// The for , if valid. + /// Whether could successfully be retrieved from . + public static bool TryGetParameterOrReturnType(this IMethodSymbol methodSymbol, out ParameterOrReturnType parameterOrReturnType) + { + // Check if the method has a custom serializer for its return + if (methodSymbol.TryGetValueSetSerializerTypeFromAttribute(out INamedTypeSymbol? serializerType)) + { + // If the custom serializer doesn't match the return type, the return type is invalid + if (!serializerType.IsValidValueSetSerializerTypeForReturnType(methodSymbol)) + { + parameterOrReturnType = default; + + return false; + } + + // Otherwise, the return type has to be a Task with a custom serializer, which is valid + parameterOrReturnType = ParameterOrReturnType.TaskOfT | ParameterOrReturnType.CustomSerializerType; + + return true; + } + + // If there is no custom serializer, the usual logic is used + return methodSymbol.ReturnType.TryGetParameterOrReturnType(out parameterOrReturnType); + } + + /// + /// Gets all member symbols from a given instance, including inherited ones. + /// + /// The input instance. + /// A sequence of all member symbols for . + /// + /// The logic here is very similar to that of , with the difference being + /// that the logic here is simple as no diagnostics need to be generated, the method only has to check for correctness. + /// + public static bool IsValidAppServicesMethod(this IMethodSymbol methodSymbol) + { + // All [AppServices] methods must be non-generic instance methods + if (methodSymbol is not { IsStatic: false, IsGenericMethod: false, ReturnType: INamedTypeSymbol }) + { + return false; + } + + // Validate the return type + if (!methodSymbol.TryGetParameterOrReturnType(out ParameterOrReturnType returnType) || + !returnType.IsValidReturnType()) + { + return false; + } + + bool hasProgress = false; + bool hasCancellationToken = false; + + // Validate the method parameters + foreach (IParameterSymbol parameter in methodSymbol.Parameters) + { + // First validate types that could possibly be allowed at all (ie. valid types) + if (!parameter.TryGetParameterOrReturnType(out ParameterOrReturnType parameterType) || + !parameterType.IsValidParameterType()) + { + return false; + } + + // Then check that the type is not an IProgress, if one has already been discovered + if (parameterType.HasFlag(ParameterOrReturnType.IProgressOfT)) + { + if (hasProgress) + { + return false; + } + + hasProgress = true; + } + + // Lastly, check that the type is not a CancellationToken, if one has already been discovered + if (parameterType.HasFlag(ParameterOrReturnType.CancellationToken)) + { + if (hasCancellationToken) + { + return false; + } + + hasCancellationToken = true; + } + } + + return true; + } +} diff --git a/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Extensions/INamedTypeSymbolExtensions.cs b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Extensions/INamedTypeSymbolExtensions.cs new file mode 100644 index 000000000..2c39ca95c --- /dev/null +++ b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Extensions/INamedTypeSymbolExtensions.cs @@ -0,0 +1,172 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; +using CommunityToolkit.AppServices.SourceGenerators.Models; + +namespace CommunityToolkit.AppServices.SourceGenerators.Extensions; + +/// +/// Extension methods for the type. +/// +internal static class INamedTypeSymbolExtensions +{ + /// + /// Gets all member symbols from a given instance, including inherited ones. + /// + /// The input instance. + /// A sequence of all member symbols for . + public static IEnumerable GetAllMembers(this INamedTypeSymbol typeSymbol) + { + for (INamedTypeSymbol? currentSymbol = typeSymbol; currentSymbol is { SpecialType: not SpecialType.System_Object }; currentSymbol = currentSymbol.BaseType) + { + foreach (ISymbol memberSymbol in currentSymbol.GetMembers()) + { + yield return memberSymbol; + } + } + } + + /// + /// Checks whether or not a given symbol has an attribute of a specified type. + /// + /// The input instance. + /// The attribute type to look for. + /// Whether or not has an attribute of type . + public static bool HasOrInheritsAttribute(this INamedTypeSymbol typeSymbol, INamedTypeSymbol attributeSymbol) + { + for (INamedTypeSymbol? currentSymbol = typeSymbol; currentSymbol is { SpecialType: not SpecialType.System_Object }; currentSymbol = currentSymbol.BaseType) + { + foreach (AttributeData attributeData in typeSymbol.GetAttributes()) + { + if (SymbolEqualityComparer.Default.Equals(attributeData.AttributeClass, attributeSymbol)) + { + return true; + } + } + } + + return false; + } + + /// + /// Checks whether an input object represents a valid serializer type for a return type. + /// + /// The input instance. + /// The whose return type should be validated. + /// Whether is a valid serializer type for 's return type. + public static bool IsValidValueSetSerializerTypeForReturnType(this INamedTypeSymbol typeSymbol, IMethodSymbol methodSymbol) + { + // Only if the return is a Task, the input serializer could potentially be valid + if (methodSymbol.ReturnType is INamedTypeSymbol + { + Name: "Task", + ContainingNamespace: { Name: "Tasks", ContainingNamespace: { Name: "Threading", ContainingNamespace: { Name: "System", ContainingNamespace.IsGlobalNamespace: true } } }, + IsGenericType: true, + TypeArguments.Length: 1 + } genericType && + genericType.TypeArguments[0] is INamedTypeSymbol typeArgumentSymbol) + { + return typeSymbol.IsValidValueSetSerializerTypeForType(typeArgumentSymbol); + } + + return false; + } + + /// + /// Checks whether an input object represents a valid serializer type for a parameter type. + /// + /// The input instance. + /// The whose return type should be validated. + /// The for , if valid. + /// Whether is a valid serializer type for 's type. + public static bool IsValidValueSetSerializerTypeForParameterType( + this INamedTypeSymbol typeSymbol, + IParameterSymbol parameterSymbol, + out ParameterOrReturnType parameterOrReturnType) + { + // Any INamedTypeSymbol matching the serializer is allowed + if (parameterSymbol.Type is INamedTypeSymbol parameterTypeSymbol) + { + // Special case for IProgress values, where the custom serializer applies to the inner T values + if (parameterTypeSymbol is + { + Name: "IProgress", + ContainingNamespace: { Name: "System", ContainingNamespace.IsGlobalNamespace: true }, + IsGenericType: true, + TypeArguments.Length: 1 + }) + { + // For this to be allowed, the type argument has to be an INamedTypeSymbol + if (parameterTypeSymbol.TypeArguments[0] is INamedTypeSymbol progressTypeSymbol && + IsValidValueSetSerializerTypeForType(typeSymbol, progressTypeSymbol)) + { + parameterOrReturnType = ParameterOrReturnType.IProgressOfT | ParameterOrReturnType.CustomSerializerType; + + return true; + } + + goto Failure; + } + + // Handle all other cases normally (ie. the serializer applies directly to the parameter type) + if (IsValidValueSetSerializerTypeForType(typeSymbol, parameterTypeSymbol)) + { + parameterOrReturnType = ParameterOrReturnType.CustomSerializerType; + + return true; + } + } + + Failure: + parameterOrReturnType = default; + + return false; + } + + /// + /// Checks whether an input object represents a valid serializer type for a target type. + /// + /// The input instance. + /// The to validate. + /// Whether is a valid serializer type for . + public static bool IsValidValueSetSerializerTypeForType(this INamedTypeSymbol typeSymbol, INamedTypeSymbol targetTypeSymbol) + { + foreach (INamedTypeSymbol interfaceSymbol in typeSymbol.AllInterfaces) + { + if (interfaceSymbol.TryGetValueSetSerializerType(out INamedTypeSymbol? resultingSymbol)) + { + if (SymbolEqualityComparer.Default.Equals(targetTypeSymbol, resultingSymbol)) + { + return true; + } + } + } + + return false; + } + + /// + /// Tries to get the target type symbol from an interface, if it's IValueSetSerializer<T<>. + /// + /// The input instance to check. + /// The target type symbol, if available. + /// Whether was IValueSetSerializer<T<> and could be retrieved. + public static bool TryGetValueSetSerializerType(this INamedTypeSymbol typeSymbol, [NotNullWhen(true)] out INamedTypeSymbol? resultingSymbol) + { + if (typeSymbol is { Name: "IValueSetSerializer", ContainingNamespace: { Name: "AppServices", ContainingNamespace: { Name: "CommunityToolkit", ContainingNamespace.IsGlobalNamespace: true } }, IsGenericType: true, TypeArguments.Length: 1 } && + typeSymbol.TypeArguments[0] is INamedTypeSymbol typeArgumentSymbol) + { + resultingSymbol = typeArgumentSymbol; + + return true; + } + + resultingSymbol = null; + + return false; + } +} diff --git a/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Extensions/IParameterSymbolExtensions.cs b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Extensions/IParameterSymbolExtensions.cs new file mode 100644 index 000000000..c3a551557 --- /dev/null +++ b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Extensions/IParameterSymbolExtensions.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CodeAnalysis; +using CommunityToolkit.AppServices.SourceGenerators.Models; + +namespace CommunityToolkit.AppServices.SourceGenerators.Extensions; + +/// +/// Extension methods for the type. +/// +internal static partial class IParameterSymbolExtensions +{ + /// + /// Tries to parse the value from an input parameter symbol. + /// + /// The input instance. + /// The for , if valid. + /// Whether could successfully be retrieved from . + public static bool TryGetParameterOrReturnType(this IParameterSymbol parameterSymbol, out ParameterOrReturnType parameterOrReturnType) + { + // Check if the parameter has a custom serializer specified + if (parameterSymbol.TryGetValueSetSerializerTypeFromAttribute(out INamedTypeSymbol? serializerType)) + { + // If that is the case, validate the serializer against the current parameter type. + // This includes logic to also validate IProgress parameters with custom serializers. + return serializerType.IsValidValueSetSerializerTypeForParameterType(parameterSymbol, out parameterOrReturnType); + } + + // If there is no custom serializer, the usual logic is used + return parameterSymbol.Type.TryGetParameterOrReturnType(out parameterOrReturnType); + } +} diff --git a/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Extensions/ISymbolExtensions.cs b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Extensions/ISymbolExtensions.cs new file mode 100644 index 000000000..ec9317935 --- /dev/null +++ b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Extensions/ISymbolExtensions.cs @@ -0,0 +1,91 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; + +namespace CommunityToolkit.AppServices.SourceGenerators.Extensions; + +/// +/// Extension methods for the type. +/// +internal static class ISymbolExtensions +{ + /// + /// Gets the fully qualified name for a given symbol. + /// + /// The input instance. + /// The fully qualified name for . + public static string GetFullyQualifiedName(this ISymbol symbol) + { + return symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat); + } + + /// + /// Tries to get the app service name from a given symbol for the AppServices attribute. + /// + /// The input instance to check. + /// The app service name from the retrieved attribute, if found. + /// Whether the attribute was found. + public static bool TryGetAppServicesNameFromAttribute(this ISymbol symbol, [NotNullWhen(true)] out string? appServiceName) + { + foreach (AttributeData attribute in symbol.GetAttributes()) + { + if (attribute.AttributeClass is { Name: "AppServiceAttribute", ContainingNamespace: { Name: "AppServices", ContainingNamespace: { Name: "CommunityToolkit", ContainingNamespace.IsGlobalNamespace: true } } }) + { + appServiceName = (string)attribute.ConstructorArguments[0].Value!; + + return true; + } + } + + appServiceName = null; + + return false; + } + + /// + /// Tries to get the serializer type from a given symbol for the ValueSetSerializer attribute. + /// + /// The input instance to check. + /// The serializer type from the retrieved attribute, if found. + /// Whether the attribute was found. + public static bool TryGetValueSetSerializerTypeFromAttribute(this ISymbol symbol, [NotNullWhen(true)] out INamedTypeSymbol? serializerType) + { + // Get either the attributes from the correct location based on symbol type + ImmutableArray attributes = symbol switch + { + IMethodSymbol methodSymbol => methodSymbol.GetReturnTypeAttributes(), + IParameterSymbol parameterSymbol => parameterSymbol.GetAttributes(), + _ => throw new ArgumentException("Invalid symbol type.") + }; + + foreach (AttributeData attribute in attributes) + { + if (attribute.AttributeClass is { Name: "ValueSetSerializerAttribute", ContainingNamespace: { Name: "AppServices", ContainingNamespace: { Name: "CommunityToolkit", ContainingNamespace.IsGlobalNamespace: true } } }) + { + serializerType = (INamedTypeSymbol)attribute.ConstructorArguments[0].Value!; + + return true; + } + } + + serializerType = null; + + return false; + } + + /// + /// Checks whether a given member is an ignored member for [AppServices]. + /// + /// The input instance to check. + /// Whether is an ignored [AppServices] member. + /// Interface member that are not abstract nor virtual can be ignored (eg. DIMs or static, non-abstract, non-virtual interface members). + public static bool IsIgnoredAppServicesMember(this ISymbol symbol) + { + return symbol is { IsAbstract: false, IsVirtual: false }; + } +} diff --git a/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Extensions/ITypeSymbolExtensions.cs b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Extensions/ITypeSymbolExtensions.cs new file mode 100644 index 000000000..6566b15d5 --- /dev/null +++ b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Extensions/ITypeSymbolExtensions.cs @@ -0,0 +1,283 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using Microsoft.CodeAnalysis; +using CommunityToolkit.AppServices.SourceGenerators.Helpers; +using CommunityToolkit.AppServices.SourceGenerators.Models; + +namespace CommunityToolkit.AppServices.SourceGenerators.Extensions; + +/// +/// Extension methods for the type. +/// +internal static class ITypeSymbolExtensions +{ + /// + /// Gets a valid filename for a given instance. + /// + /// The input instance. + /// The full metadata name for that is also a valid filename. + public static string GetFullMetadataNameForFileName(this ITypeSymbol typeSymbol) + { + using ImmutableArrayBuilder builder = ImmutableArrayBuilder.Rent(); + + static void BuildFrom(ISymbol? symbol, in ImmutableArrayBuilder 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(typeSymbol, in builder); + + return builder.ToString(); + } + + /// + /// Tries to parse the value from an input type symbol. + /// + /// The input instance. + /// The for , if valid. + /// Whether could successfully be retrieved from . + public static bool TryGetParameterOrReturnType(this ITypeSymbol typeSymbol, out ParameterOrReturnType parameterOrReturnType) + { + return TryGetParameterOrReturnType(typeSymbol, associatedTypeSymbol: null, out parameterOrReturnType); + } + + /// + /// Tries to parse the value from an input type symbol. + /// + /// The input instance. + /// The associated type symbol (ie. the containing type, if the parent is generic). + /// The for , if valid. + /// Whether could successfully be retrieved from . + private static bool TryGetParameterOrReturnType(this ITypeSymbol typeSymbol, ITypeSymbol? associatedTypeSymbol, out ParameterOrReturnType parameterOrReturnType) + { + // There are only two allowed kinds of type symbols: + // - IArrayTypeSymbol, for SZ arrays + // - INamedTypeSymbol, for all other possible types. + if (typeSymbol is not (INamedTypeSymbol or IArrayTypeSymbol)) + { + goto Failure; + } + + // First, consider the case where the type symbol is an array type + if (typeSymbol is IArrayTypeSymbol arrayTypeSymbol) + { + // In order to be valid, an array type must match these conditions: + // - It must be either a top level parameter, or its associated type symbol must be Task + // - It has to be an SZ array + // - The element type must be one of the simple types supported (eg. not an array, Task or other complex types) + if ((associatedTypeSymbol is null || IsTaskOfT(associatedTypeSymbol)) && + arrayTypeSymbol is { IsSZArray: true, ElementType: INamedTypeSymbol elementTypeSymbol } && + TryGetParameterOrReturnType(elementTypeSymbol, associatedTypeSymbol: typeSymbol, out ParameterOrReturnType elementType)) + { + parameterOrReturnType = elementType | ParameterOrReturnType.Array; + + return true; + } + + goto Failure; + } + + // Try to match against all allowed types that have a corresponding special type + ParameterOrReturnType? parameterOrReturnTypeFromSpecialType = typeSymbol.SpecialType switch + { + SpecialType.System_Byte => ParameterOrReturnType.UInt8, + SpecialType.System_Int16 => ParameterOrReturnType.Int16, + SpecialType.System_UInt16 => ParameterOrReturnType.UInt16, + SpecialType.System_Int32 => ParameterOrReturnType.Int32, + SpecialType.System_UInt32 => ParameterOrReturnType.UInt32, + SpecialType.System_Int64 => ParameterOrReturnType.Int64, + SpecialType.System_UInt64 => ParameterOrReturnType.UInt64, + SpecialType.System_Single => ParameterOrReturnType.Single, + SpecialType.System_Double => ParameterOrReturnType.Double, + SpecialType.System_Char => ParameterOrReturnType.Char16, + SpecialType.System_Boolean => ParameterOrReturnType.Boolean, + SpecialType.System_String => ParameterOrReturnType.String, + SpecialType.System_DateTime => ParameterOrReturnType.DateTime, + _ => null + }; + + // If a type has been found, we can stop here + if (parameterOrReturnTypeFromSpecialType is not null) + { + parameterOrReturnType = parameterOrReturnTypeFromSpecialType.Value; + + return true; + } + + // If the type is an enum, that is also explicitly allowed + if (typeSymbol.TypeKind == TypeKind.Enum) + { + parameterOrReturnType = ParameterOrReturnType.Enum; + + return true; + } + + // The following types have to be checked explicitly, as they don't have a special type: + // - System.TimeSpan + // - System.Guid + // - Windows.Foundation.Size + // - Windows.Foundation.Point + // - Windows.Foundation.Rect + if (typeSymbol is { Name: "TimeSpan", ContainingNamespace: { Name: "System", ContainingNamespace.IsGlobalNamespace: true } }) + { + parameterOrReturnType = ParameterOrReturnType.TimeSpan; + + return true; + } + + // System.Guid + if (typeSymbol is { Name: "Guid", ContainingNamespace: { Name: "System", ContainingNamespace.IsGlobalNamespace: true } }) + { + parameterOrReturnType = ParameterOrReturnType.Guid; + + return true; + } + + // Windows.Foundation.Size + if (typeSymbol is { Name: "Size", ContainingNamespace: { Name: "Foundation", ContainingNamespace: { Name: "Windows", ContainingNamespace.IsGlobalNamespace: true } } }) + { + parameterOrReturnType = ParameterOrReturnType.Size; + + return true; + } + + // Windows.Foundation.Point + if (typeSymbol is { Name: "Point", ContainingNamespace: { Name: "Foundation", ContainingNamespace: { Name: "Windows", ContainingNamespace.IsGlobalNamespace: true } } }) + { + parameterOrReturnType = ParameterOrReturnType.Point; + + return true; + } + + // Windows.Foundation.Rect + if (typeSymbol is { Name: "Rect", ContainingNamespace: { Name: "Foundation", ContainingNamespace: { Name: "Windows", ContainingNamespace.IsGlobalNamespace: true } } }) + { + parameterOrReturnType = ParameterOrReturnType.Rect; + + return true; + } + + // This is the end of the possible types that are allowed if an associated type is present. + // That is, if there is an associated type available, it means matching has failed. + // If that's not the case, the following types must be checked manually after that: + // - T[] arrays + // - System.Threading.Tasks.Task + // - System.Threading.Tasks.Task + // - System.IProgress + // - System.Threading.CancellationToken + if (associatedTypeSymbol is not null) + { + goto Failure; + } + + // System.Threading.Tasks.Task (this has to be tested first for correctness, as System.Threading.Tasks.Task would also match) + if (IsTaskOfT(typeSymbol)) + { + // Only validate a Task type if the type argument is actually allowed + if (((INamedTypeSymbol)typeSymbol).TypeArguments[0] is INamedTypeSymbol taskResultTypeSymbol && + TryGetParameterOrReturnType(taskResultTypeSymbol, associatedTypeSymbol: typeSymbol, out ParameterOrReturnType resultType)) + { + parameterOrReturnType = resultType | ParameterOrReturnType.TaskOfT; + + return true; + } + + goto Failure; + } + + // System.Threading.Tasks.Task (this branch is also taken for Task as the type name without type parameters is the same) + if (typeSymbol is { Name: "Task", ContainingNamespace: { Name: "Tasks", ContainingNamespace: { Name: "Threading", ContainingNamespace: { Name: "System", ContainingNamespace.IsGlobalNamespace: true } } } }) + { + parameterOrReturnType = ParameterOrReturnType.Task; + + return true; + } + + // System.IProgress + if (typeSymbol is INamedTypeSymbol + { + Name: "IProgress", + ContainingNamespace: { Name: "System", ContainingNamespace.IsGlobalNamespace: true }, + IsGenericType: true, + TypeArguments.Length: 1 + }) + { + // If the type argument is valid, then confirm the IProgress parameter + if (((INamedTypeSymbol)typeSymbol).TypeArguments[0] is INamedTypeSymbol progressTypeSymbol && + TryGetParameterOrReturnType(progressTypeSymbol, associatedTypeSymbol: typeSymbol, out ParameterOrReturnType progressType)) + { + parameterOrReturnType = progressType | ParameterOrReturnType.IProgressOfT; + + return true; + } + + goto Failure; + } + + // System.Threading.CancellationToken + if (typeSymbol is { Name: "CancellationToken", ContainingNamespace: { Name: "Threading", ContainingNamespace: { Name: "System", ContainingNamespace.IsGlobalNamespace: true } } }) + { + parameterOrReturnType = ParameterOrReturnType.CancellationToken; + + return true; + } + + Failure: + parameterOrReturnType = default; + + return false; + } + + /// + /// Checks whether the input represents the type. + /// + /// The input instance to check. + /// Whether represents the type. + private static bool IsTaskOfT(ITypeSymbol typeSymbol) + { + return typeSymbol is INamedTypeSymbol + { + Name: "Task", + ContainingNamespace: { Name: "Tasks", ContainingNamespace: { Name: "Threading", ContainingNamespace: { Name: "System", ContainingNamespace.IsGlobalNamespace: true } } }, + IsGenericType: true, + TypeArguments.Length: 1 + }; + } +} \ No newline at end of file diff --git a/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Extensions/ParameterOrReturnTypeExtensions.cs b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Extensions/ParameterOrReturnTypeExtensions.cs new file mode 100644 index 000000000..09f67d93c --- /dev/null +++ b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Extensions/ParameterOrReturnTypeExtensions.cs @@ -0,0 +1,35 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.AppServices.SourceGenerators.Models; + +namespace CommunityToolkit.AppServices.SourceGenerators.Extensions; + +/// +/// Extension methods for the type. +/// +internal static partial class ParameterOrReturnTypeExtensions +{ + /// + /// Checks whether an input value can be used as a parameter type. + /// + /// The value to check. + /// Whether can be used as a return type. + public static bool IsValidParameterType(this ParameterOrReturnType parameterType) + { + // The sets of valid return types and parameter types are disjointed, so we can just invert the condition and + // reuse the same logic. That is, return types only allow Task and Task, which can't be used as parameters. + return !IsValidReturnType(parameterType); + } + + /// + /// Checks whether an input value can be used as a return type. + /// + /// The value to check. + /// Whether can be used as a return type. + public static bool IsValidReturnType(this ParameterOrReturnType returnType) + { + return returnType.HasAnyFlags(ParameterOrReturnType.Task | ParameterOrReturnType.TaskOfT); + } +} diff --git a/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Extensions/SyntaxNodeExtensions.cs b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Extensions/SyntaxNodeExtensions.cs new file mode 100644 index 000000000..2906efc35 --- /dev/null +++ b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Extensions/SyntaxNodeExtensions.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CodeAnalysis; + +namespace CommunityToolkit.AppServices.SourceGenerators.Extensions; + +/// +/// Extension methods for the type. +/// +internal static class SyntaxNodeExtensions +{ + /// + /// Checks whether a given represents the first (partial) declaration of a given symbol. + /// + /// The input instance. + /// The target instance to check the syntax declaration for. + /// Whether is the first (partial) declaration for . + /// + /// This extension can be used to avoid accidentally generating repeated members for types that have multiple partial declarations. + /// In order to keep this check efficient and without the need to collect all items and build some sort of hashset from them to + /// remove duplicates, each syntax node is symply compared against the available declaring syntax references for the target symbol. + /// If the syntax node matches the first syntax reference for the symbol, it is kept, otherwise it is considered a duplicate. + /// + public static bool IsFirstSyntaxDeclarationForSymbol(this SyntaxNode syntaxNode, ISymbol symbol) + { + return + symbol.DeclaringSyntaxReferences.Length > 0 && + symbol.DeclaringSyntaxReferences[0] is SyntaxReference syntaxReference && + syntaxReference.SyntaxTree == syntaxNode.SyntaxTree && + syntaxReference.Span == syntaxNode.Span; + } +} diff --git a/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Extensions/SyntaxTokenExtensions.cs b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Extensions/SyntaxTokenExtensions.cs new file mode 100644 index 000000000..6e360f48b --- /dev/null +++ b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Extensions/SyntaxTokenExtensions.cs @@ -0,0 +1,26 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.ComponentModel; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace CommunityToolkit.AppServices.SourceGenerators.Extensions; + +/// +/// Extension methods for the type. +/// +internal static partial class SyntaxTokenExtensions +{ + /// + /// Deconstructs a into its value. + /// + /// The input instance. + /// The for . + [EditorBrowsable(EditorBrowsableState.Never)] + public static void Deconstruct(this SyntaxToken token, out SyntaxKind kind) + { + kind = token.Kind(); + } +} diff --git a/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Extensions/TypeDeclarationSyntaxExtensions.cs b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Extensions/TypeDeclarationSyntaxExtensions.cs new file mode 100644 index 000000000..e92d0a366 --- /dev/null +++ b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Extensions/TypeDeclarationSyntaxExtensions.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace CommunityToolkit.AppServices.SourceGenerators.Extensions; + +/// +/// Extension methods for the type. +/// +internal static class TypeDeclarationSyntaxExtensions +{ + /// + /// Checks whether a given has or could possibly have any base types, using only syntax. + /// + /// The input instance to check. + /// Whether has or could possibly have any base types. + public static bool HasOrPotentiallyHasBaseTypes(this TypeDeclarationSyntax typeDeclaration) + { + // If the base types list is not empty, the type can definitely has implemented interfaces + if (typeDeclaration.BaseList is { Types.Count: > 0 }) + { + return true; + } + + // If the base types list is empty, check if the type is partial. If it is, it means + // that there could be another partial declaration with a non-empty base types list. + foreach (SyntaxToken modifier in typeDeclaration.Modifiers) + { + if (modifier.IsKind(SyntaxKind.PartialKeyword)) + { + return true; + } + } + + return false; + } +} diff --git a/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Helpers/EquatableArray{T}.cs b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Helpers/EquatableArray{T}.cs new file mode 100644 index 000000000..ba92e0334 --- /dev/null +++ b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Helpers/EquatableArray{T}.cs @@ -0,0 +1,177 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +// This file is ported and adapted from ComputeSharp (Sergio0694/ComputeSharp), +// more info in ThirdPartyNotices.txt in the root of the project. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Runtime.CompilerServices; + +namespace CommunityToolkit.AppServices.SourceGenerators.Helpers; + +/// +/// An imutable, equatable array. This is equivalent to but with value equality support. +/// +/// The type of values in the array. +internal readonly struct EquatableArray : IEquatable>, IEnumerable + where T : IEquatable +{ + /// + /// The underlying array. + /// + private readonly T[]? array; + + /// + /// Creates a new instance. + /// + /// The input to wrap. + public unsafe EquatableArray(ImmutableArray array) + { + this.array = *(T[]?*)&array; + } + + /// + /// Gets a reference to an item at a specified position within the array. + /// + /// The index of the item to retrieve a reference to. + /// A reference to an item at a specified position within the array. + public T this[int index] + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => AsImmutableArray()[index]; + } + + /// + /// Gets a value indicating whether the current array is empty. + /// + public bool IsEmpty + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => AsImmutableArray().IsEmpty; + } + + /// + public bool Equals(EquatableArray array) + { + return AsSpan().SequenceEqual(array.AsSpan()); + } + + /// + public override bool Equals(object? obj) + { + return obj is EquatableArray array && Equals(this, array); + } + + /// + public override unsafe int GetHashCode() + { + if (this.array is not T[] array) + { + return 0; + } + + int hashCode = 0; + + foreach (T value in array) + { + hashCode = unchecked((hashCode * (int)0xA5555529) + value.GetHashCode()); + } + + return hashCode; + } + + /// + /// Gets an instance from the current . + /// + /// The from the current . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public ImmutableArray AsImmutableArray() + { + return Unsafe.As>(ref Unsafe.AsRef(in this.array)); + } + + /// + /// Creates an instance from a given . + /// + /// The input instance. + /// An instance from a given . + public static EquatableArray FromImmutableArray(ImmutableArray array) + { + return new(array); + } + + /// + /// Returns a wrapping the current items. + /// + /// A wrapping the current items. + public ReadOnlySpan AsSpan() + { + return AsImmutableArray().AsSpan(); + } + + /// + /// Gets an value to traverse items in the current array. + /// + /// An value to traverse items in the current array. + public ImmutableArray.Enumerator GetEnumerator() + { + return AsImmutableArray().GetEnumerator(); + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)AsImmutableArray()).GetEnumerator(); + } + + /// + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)AsImmutableArray()).GetEnumerator(); + } + + /// + /// Implicitly converts an to . + /// + /// An instance from a given . + public static implicit operator EquatableArray(ImmutableArray array) + { + return FromImmutableArray(array); + } + + /// + /// Implicitly converts an to . + /// + /// An instance from a given . + public static implicit operator ImmutableArray(EquatableArray array) + { + return array.AsImmutableArray(); + } + + /// + /// Checks whether two values are the same. + /// + /// The first value. + /// The second value. + /// Whether and are equal. + public static bool operator ==(EquatableArray left, EquatableArray right) + { + return left.Equals(right); + } + + /// + /// Checks whether two values are not the same. + /// + /// The first value. + /// The second value. + /// Whether and are not equal. + public static bool operator !=(EquatableArray left, EquatableArray right) + { + return !left.Equals(right); + } +} \ No newline at end of file diff --git a/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Helpers/ImmutableArrayBuilder{T}.cs b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Helpers/ImmutableArrayBuilder{T}.cs new file mode 100644 index 000000000..6aa377d39 --- /dev/null +++ b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Helpers/ImmutableArrayBuilder{T}.cs @@ -0,0 +1,221 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +// This file is ported and adapted from ComputeSharp (Sergio0694/ComputeSharp), +// more info in ThirdPartyNotices.txt in the root of the project. + +using System; +using System.Collections.Immutable; +using System.Runtime.CompilerServices; + +namespace CommunityToolkit.AppServices.SourceGenerators.Helpers; + +/// +/// A helper type to build sequences of values with pooled buffers. +/// +/// The type of items to create sequences for. +internal struct ImmutableArrayBuilder : IDisposable +{ + /// + /// The shared instance to share objects. + /// + private static readonly ObjectPool SharedObjectPool = new(static () => new Writer()); + + /// + /// The rented instance to use. + /// + private Writer? writer; + + /// + /// Creates a value with a pooled underlying data writer. + /// + /// A instance to write data to. + public static ImmutableArrayBuilder Rent() + { + return new(SharedObjectPool.Allocate()); + } + + /// + /// Creates a new object with the specified parameters. + /// + /// The target data writer to use. + private ImmutableArrayBuilder(Writer writer) + { + this.writer = writer; + } + + /// + /// Gets the data written to the underlying buffer so far, as a . + /// + public readonly ReadOnlySpan WrittenSpan + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.writer!.WrittenSpan; + } + + /// + public readonly void Add(T item) + { + this.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) + { + this.writer!.AddRange(items); + } + + /// + public readonly unsafe ImmutableArray ToImmutable() + { + T[] array = this.writer!.WrittenSpan.ToArray(); + + return *(ImmutableArray*)&array; + } + + /// + public readonly T[] ToArray() + { + return this.writer!.WrittenSpan.ToArray(); + } + + /// + public override readonly string ToString() + { + return this.writer!.WrittenSpan.ToString(); + } + + /// + public void Dispose() + { + Writer? writer = this.writer; + + this.writer = null; + + if (writer is not null) + { + writer.Clear(); + + SharedObjectPool.Free(writer); + } + } + + /// + /// A class handling the actual buffer writing. + /// + private sealed class Writer + { + /// + /// The underlying array. + /// + private T[] array; + + /// + /// The starting offset within . + /// + private int index; + + /// + /// Creates a new instance with the specified parameters. + /// + public Writer() + { + if (typeof(T) == typeof(char)) + { + this.array = new T[1024]; + } + else + { + this.array = new T[8]; + } + + this.index = 0; + } + + /// + public ReadOnlySpan WrittenSpan + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => new(this.array, 0, this.index); + } + + /// + public void Add(T value) + { + EnsureCapacity(1); + + this.array[this.index++] = value; + } + + /// + public void AddRange(ReadOnlySpan items) + { + EnsureCapacity(items.Length); + + items.CopyTo(this.array.AsSpan(this.index)); + + this.index += items.Length; + } + + /// + /// Clears the items in the current writer. + /// + public void Clear() + { + if (typeof(T) != typeof(char)) + { + this.array.AsSpan(0, this.index).Clear(); + } + + this.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 > this.array.Length - this.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 = this.index + sizeHint; + int requestedSize = Math.Max(this.array.Length * 2, minimumSize); + + T[] newArray = new T[requestedSize]; + + Array.Copy(this.array, newArray, this.index); + + this.array = newArray; + } + } +} + +/// +/// Private helpers for the type. +/// +internal static class ImmutableArrayBuilder +{ + /// + /// Throws an for "index". + /// + public static void ThrowArgumentOutOfRangeExceptionForIndex() + { + throw new ArgumentOutOfRangeException("index"); + } +} \ No newline at end of file diff --git a/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Helpers/ObjectPool{T}.cs b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Helpers/ObjectPool{T}.cs new file mode 100644 index 000000000..8156ea6c0 --- /dev/null +++ b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Helpers/ObjectPool{T}.cs @@ -0,0 +1,163 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +// 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; + +namespace CommunityToolkit.AppServices.SourceGenerators.Helpers; + +/// +/// +/// 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. +internal sealed class ObjectPool + where T : class +{ + /// + /// 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()". + /// + private readonly Func factory; + + /// + /// The array of cached items. + /// + private readonly Element[] items; + + /// + /// 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) + { + } + + /// + /// Creates a new instance with the specified parameters. + /// + /// The input factory to produce items. + /// The pool size to use. + public ObjectPool(Func factory, int size) + { + this.factory = factory; + this.items = new Element[size - 1]; + } + + /// + /// Produces a instance. + /// + /// The returned item to use. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public T Allocate() + { + T? item = this.firstItem; + + if (item is null || item != Interlocked.CompareExchange(ref this.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 (this.firstItem is null) + { + this.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 this.items.AsSpan()) + { + T? instance = element.Value; + + if (instance is not null) + { + if (instance == Interlocked.CompareExchange(ref element.Value, null, instance)) + { + return instance; + } + } + } + + return this.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 this.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/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Models/AppServiceInfo.cs b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Models/AppServiceInfo.cs new file mode 100644 index 000000000..83a144d96 --- /dev/null +++ b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Models/AppServiceInfo.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using CommunityToolkit.AppServices.SourceGenerators.Helpers; + +namespace CommunityToolkit.AppServices.SourceGenerators.Models; + +/// +/// A model with gathered info on a given app service (either host or component). +/// +/// The methods in this app service. +/// The name of the app service. +/// The fully qualified name of the AppService interface. +internal sealed record AppServiceInfo(EquatableArray Methods, string AppServiceName, string InterfaceFullyQualifiedName); \ No newline at end of file diff --git a/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Models/HierarchyInfo.Syntax.cs b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Models/HierarchyInfo.Syntax.cs new file mode 100644 index 000000000..5e30f3126 --- /dev/null +++ b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Models/HierarchyInfo.Syntax.cs @@ -0,0 +1,110 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +// This file is ported and adapted from ComputeSharp (Sergio0694/ComputeSharp), +// more info in ThirdPartyNotices.txt in the root of the project. + +using System.Collections.Immutable; +using System.Linq; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace CommunityToolkit.AppServices.SourceGenerators.Models; + +/// +partial record HierarchyInfo +{ + /// + /// Creates a instance wrapping the given members. + /// + /// The input instances to use. + /// The collection of instances to add to generated types. + /// The optional comment for the type. + /// A object wrapping . + public CompilationUnitSyntax GetCompilationUnit( + ImmutableArray memberDeclarations, + ImmutableArray baseList, + string? comment = null) + { + SyntaxToken[] typeModifierTokens = Modifiers.AsImmutableArray().Select(static m => Token((SyntaxKind)m)).ToArray(); + + if (comment is not null) + { + // Add the comment, if needed + typeModifierTokens[0] = + Token( + TriviaList(Comment(comment)), + typeModifierTokens[0].Kind(), + TriviaList()); + } + + // Create the partial type declaration with the given member declarations. + // This code produces a class declaration as follows: + // + // TYPE_NAME> + // { + // + // } + TypeDeclarationSyntax typeDeclarationSyntax = + Hierarchy[0].GetSyntax() + .AddModifiers(typeModifierTokens) + .AddMembers(memberDeclarations.ToArray()); + + // Add the base list, if present + if (baseList.Length > 0) + { + typeDeclarationSyntax = (TypeDeclarationSyntax)typeDeclarationSyntax.AddBaseListTypes(baseList.ToArray()); + } + + // Add all parent types in ascending order, if any + foreach (TypeInfo parentType in Hierarchy.AsSpan().Slice(1)) + { + typeDeclarationSyntax = + parentType.GetSyntax() + .AddModifiers(Token(SyntaxKind.PartialKeyword)) + .AddMembers(typeDeclarationSyntax); + } + + // Prepare the leading trivia for the generated compilation unit. + // This will produce code as follows: + // + // + // #pragma warning disable + // #nullable enable + SyntaxTriviaList syntaxTriviaList = TriviaList( + Comment("// "), + Trivia(PragmaWarningDirectiveTrivia(Token(SyntaxKind.DisableKeyword), true)), + Trivia(NullableDirectiveTrivia(Token(SyntaxKind.EnableKeyword), true))); + + if (Namespace is "") + { + // If there is no namespace, attach the pragma directly to the declared type, + // and skip the namespace declaration. This will produce code as follows: + // + // + // + return + CompilationUnit() + .AddMembers(typeDeclarationSyntax.WithLeadingTrivia(syntaxTriviaList)) + .NormalizeWhitespace(); + } + + // Create the compilation unit with disabled warnings, target namespace and generated type. + // This will produce code as follows: + // + // + // namespace + // { + // + // } + return + CompilationUnit().AddMembers( + NamespaceDeclaration(IdentifierName(Namespace)) + .WithLeadingTrivia(syntaxTriviaList) + .AddMembers(typeDeclarationSyntax)) + .NormalizeWhitespace(); + } +} diff --git a/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Models/HierarchyInfo.cs b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Models/HierarchyInfo.cs new file mode 100644 index 000000000..f8a3f7029 --- /dev/null +++ b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Models/HierarchyInfo.cs @@ -0,0 +1,88 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +// This file is ported and adapted from ComputeSharp (Sergio0694/ComputeSharp), +// more info in ThirdPartyNotices.txt in the root of the project. + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using CommunityToolkit.AppServices.SourceGenerators.Extensions; +using CommunityToolkit.AppServices.SourceGenerators.Helpers; +using static Microsoft.CodeAnalysis.SymbolDisplayTypeQualificationStyle; + +namespace CommunityToolkit.AppServices.SourceGenerators.Models; + +/// +/// A model describing the hierarchy info for a specific type. +/// +/// The filename hint for the current type. +/// The metadata name for the current type. +/// The modifiers for the type declaration. +/// Gets the namespace for the current type. +/// Gets the sequence of type definitions containing the current type. +internal sealed partial record HierarchyInfo( + string FilenameHint, + string MetadataName, + EquatableArray Modifiers, + 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 ImmutableArrayBuilder hierarchy = ImmutableArrayBuilder.Rent(); + + 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.GetFullMetadataNameForFileName(), + typeSymbol.MetadataName, + ImmutableArray.Create((ushort)SyntaxKind.PartialKeyword), + typeSymbol.ContainingNamespace.ToDisplayString(new(typeQualificationStyle: NameAndContainingTypesAndNamespaces)), + hierarchy.ToImmutable()); + } + + /// + /// Creates a new instance from a given . + /// + /// The input instance to gather info for. + /// The name of the new type to describe. + /// A instance describing . + public static HierarchyInfo From(INamedTypeSymbol typeSymbol, string metadataName) + { + using ImmutableArrayBuilder hierarchy = ImmutableArrayBuilder.Rent(); + + hierarchy.Add(new TypeInfo(metadataName, TypeKind.Class, IsRecord: false)); + + for (INamedTypeSymbol? parent = typeSymbol.ContainingType; + parent is not null; + parent = parent.ContainingType) + { + hierarchy.Add(new TypeInfo( + parent.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat), + parent.TypeKind, + parent.IsRecord)); + } + + return new( + typeSymbol.GetFullMetadataNameForFileName(), + metadataName, + ImmutableArray.Create((ushort)SyntaxKind.PublicKeyword, (ushort)SyntaxKind.SealedKeyword), + typeSymbol.ContainingNamespace.ToDisplayString(new(typeQualificationStyle: NameAndContainingTypesAndNamespaces)), + hierarchy.ToImmutable()); + } +} diff --git a/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Models/MethodInfo.cs b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Models/MethodInfo.cs new file mode 100644 index 000000000..a2c927eb2 --- /dev/null +++ b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Models/MethodInfo.cs @@ -0,0 +1,83 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; +using CommunityToolkit.AppServices.SourceGenerators.Extensions; +using CommunityToolkit.AppServices.SourceGenerators.Helpers; + +namespace CommunityToolkit.AppServices.SourceGenerators.Models; + +/// +/// A model with gathered info on a given method. +/// +/// The name of the target method. +/// The method parameters, if any. +/// The return type of the method. +/// The fully qualified type name of the return type being serialized, if any. +/// The fully qualified type name of the custom ValueSet serializer to use, if any. +internal sealed record MethodInfo( + string MethodName, + EquatableArray Parameters, + ParameterOrReturnType ReturnType, + string? FullyQualifiedReturnTypeName, + string? FullyQualifiedValueSetSerializerTypeName) +{ + /// + /// Creates instances from methods in a given . + /// + /// The input instance to gather info for. + /// A collection of instances from . + public static ImmutableArray From(INamedTypeSymbol typeSymbol) + { + using ImmutableArrayBuilder builder = ImmutableArrayBuilder.Rent(); + + foreach (ISymbol symbol in typeSymbol.GetMembers()) + { + if (symbol.IsIgnoredAppServicesMember()) + { + continue; + } + + // If the current method is not valid, also just skip it. This will avoid crashes in the generator + // and it will keep the rest of the code simpler. The analyzer will just emit the correct diagnostics + // separately, so the user will easily understand why these methods have not been generated. + if (symbol is not IMethodSymbol methodSymbol || + !methodSymbol.IsValidAppServicesMethod()) + { + continue; + } + + // Get the return type (this will always succeed, as the call above will validate the whole signature) + _ = methodSymbol.TryGetParameterOrReturnType(out ParameterOrReturnType returnType); + + // Get the return type name, if needed (for custom serializer types, and for enum types) + string? fullyQualifiedReturnTypeName = returnType.HasAnyFlags(ParameterOrReturnType.CustomSerializerType | ParameterOrReturnType.Enum) switch + { + true => ((INamedTypeSymbol)methodSymbol.ReturnType).TypeArguments[0].GetFullyQualifiedName(), + false => null + }; + + // Try to get the serializer name, in case there is one (validation has already been performed) + _ = methodSymbol.TryGetValueSetSerializerTypeFromAttribute(out INamedTypeSymbol? serializerType); + + builder.Add(new MethodInfo( + MethodName: symbol.Name, + Parameters: ParameterInfo.From(methodSymbol), + ReturnType: returnType, + FullyQualifiedReturnTypeName: fullyQualifiedReturnTypeName, + FullyQualifiedValueSetSerializerTypeName: serializerType?.GetFullyQualifiedName())); + } + + return builder.ToImmutable(); + } + + /// + /// Gets whether or not the method has a custom serializer. + /// + [MemberNotNullWhen(true, nameof(FullyQualifiedReturnTypeName))] + [MemberNotNullWhen(true, nameof(FullyQualifiedValueSetSerializerTypeName))] + public bool HasCustomValueSetSerializer => FullyQualifiedValueSetSerializerTypeName is not null; +} diff --git a/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Models/ParameterInfo.cs b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Models/ParameterInfo.cs new file mode 100644 index 000000000..1320bae91 --- /dev/null +++ b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Models/ParameterInfo.cs @@ -0,0 +1,157 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.CSharp; +using CommunityToolkit.AppServices.SourceGenerators.Extensions; +using CommunityToolkit.AppServices.SourceGenerators.Helpers; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace CommunityToolkit.AppServices.SourceGenerators.Models; + +/// +/// A model for a method parameter. +/// +/// The parameter name. +/// The parameter type. +/// The fully qualified type name of the type being serialized, if any. +/// The fully qualified type name of the custom ValueSet serializer to use, if any. +internal sealed record ParameterInfo( + string Name, + ParameterOrReturnType Type, + string? FullyQualifiedTypeName, + string? FullyQualifiedValueSetSerializerTypeName) +{ + /// + /// Creates instances from methods in a given . + /// + /// The input instance to gather info for. + /// A collection of instances from . + public static ImmutableArray From(IMethodSymbol methodSymbol) + { + using ImmutableArrayBuilder builder = ImmutableArrayBuilder.Rent(); + + foreach (IParameterSymbol parameterInfo in methodSymbol.Parameters) + { + // This method is only invoked from MethodInfo.From, which already does parameter validation first + _ = parameterInfo.TryGetParameterOrReturnType(out ParameterOrReturnType type); + + // Get the parameter type name, if a serializer is used or if the type is an enum + string? fullyQualifiedTypeName = type.HasAnyFlags(ParameterOrReturnType.CustomSerializerType | ParameterOrReturnType.Enum) switch + { + true => parameterInfo.Type.GetFullyQualifiedName(), + false => null + }; + + // Same as above for the ValueSet serializer, if any + _ = parameterInfo.TryGetValueSetSerializerTypeFromAttribute(out INamedTypeSymbol? serializerType); + + builder.Add(new ParameterInfo( + Name: parameterInfo.Name, + Type: type, + FullyQualifiedTypeName: fullyQualifiedTypeName, + FullyQualifiedValueSetSerializerTypeName: serializerType?.GetFullyQualifiedName())); + } + + return builder.ToImmutable(); + } + + /// + /// Creates a instance representing a given parameter. + /// + /// The parameter type. + /// The fully qualified type name, in case a custom serializer is used or the type is an enum. + /// A instance representing a given parameter. + /// Thrown if the type is not valid. + public static TypeSyntax GetSyntax(ParameterOrReturnType type, string? fullyQualifiedTypeName) + { + // If a custom serializer is used or if the type is an enum, return the fully qualified type name directly + if (fullyQualifiedTypeName is not null) + { + return IdentifierName(fullyQualifiedTypeName); + } + + // If the type is a cancellation token, handle it first as it has no element type + if (type.HasFlag(ParameterOrReturnType.CancellationToken)) + { + return + QualifiedName( + QualifiedName( + AliasQualifiedName( + IdentifierName(Token(SyntaxKind.GlobalKeyword)), + IdentifierName("System")), + IdentifierName("Threading")), + IdentifierName("CancellationToken")); + } + + // Get the type syntax for the inner parameter type + TypeSyntax typeSyntax = (type & ParameterOrReturnType.ElementTypeMask) switch + { + ParameterOrReturnType.UInt8 => PredefinedType(Token(SyntaxKind.ByteKeyword)), + ParameterOrReturnType.Int16 => PredefinedType(Token(SyntaxKind.ShortKeyword)), + ParameterOrReturnType.UInt16 => PredefinedType(Token(SyntaxKind.UShortKeyword)), + ParameterOrReturnType.Int32 => PredefinedType(Token(SyntaxKind.IntKeyword)), + ParameterOrReturnType.UInt32 => PredefinedType(Token(SyntaxKind.UIntKeyword)), + ParameterOrReturnType.Int64 => PredefinedType(Token(SyntaxKind.LongKeyword)), + ParameterOrReturnType.UInt64 => PredefinedType(Token(SyntaxKind.ULongKeyword)), + ParameterOrReturnType.Single => PredefinedType(Token(SyntaxKind.FloatKeyword)), + ParameterOrReturnType.Double => PredefinedType(Token(SyntaxKind.DoubleKeyword)), + ParameterOrReturnType.Char16 => PredefinedType(Token(SyntaxKind.CharKeyword)), + ParameterOrReturnType.Boolean => PredefinedType(Token(SyntaxKind.BoolKeyword)), + ParameterOrReturnType.String => PredefinedType(Token(SyntaxKind.StringKeyword)), + ParameterOrReturnType.DateTime => QualifiedName(AliasQualifiedName(IdentifierName(Token(SyntaxKind.GlobalKeyword)), IdentifierName("System")), IdentifierName("DateTime")), + ParameterOrReturnType.TimeSpan => QualifiedName(AliasQualifiedName(IdentifierName(Token(SyntaxKind.GlobalKeyword)), IdentifierName("System")), IdentifierName("TimeSpan")), + ParameterOrReturnType.Guid => QualifiedName(AliasQualifiedName(IdentifierName(Token(SyntaxKind.GlobalKeyword)), IdentifierName("System")), IdentifierName("Guid")), + ParameterOrReturnType.Point => QualifiedName(QualifiedName(AliasQualifiedName(IdentifierName(Token(SyntaxKind.GlobalKeyword)), IdentifierName("Windows")), IdentifierName("Foundation")), IdentifierName("Point")), + ParameterOrReturnType.Size => QualifiedName(QualifiedName(AliasQualifiedName(IdentifierName(Token(SyntaxKind.GlobalKeyword)), IdentifierName("Windows")), IdentifierName("Foundation")), IdentifierName("Size")), + ParameterOrReturnType.Rect => QualifiedName(QualifiedName(AliasQualifiedName(IdentifierName(Token(SyntaxKind.GlobalKeyword)), IdentifierName("Windows")), IdentifierName("Foundation")), IdentifierName("Rect")), + _ => throw new ArgumentOutOfRangeException("Invalid special type.") + }; + + if (type.HasFlag(ParameterOrReturnType.Array)) + { + // Return an array type where the element type is the current type syntax + return + ArrayType(typeSyntax) + .AddRankSpecifiers(ArrayRankSpecifier( + SingletonSeparatedList( + OmittedArraySizeExpression()))); + } + + if (type.HasFlag(ParameterOrReturnType.IProgressOfT)) + { + // Return an IProgress type where the type argument is the current type syntax + return + QualifiedName( + AliasQualifiedName( + IdentifierName(Token(SyntaxKind.GlobalKeyword)), + IdentifierName("System")), + GenericName(Identifier("IProgress")) + .AddTypeArgumentListArguments(typeSyntax)); + } + + return typeSyntax; + } + + /// + /// Gets whether or not the parameter has a custom serializer. + /// + [MemberNotNullWhen(true, nameof(FullyQualifiedTypeName))] + [MemberNotNullWhen(true, nameof(FullyQualifiedValueSetSerializerTypeName))] + public bool HasCustomValueSetSerializer => FullyQualifiedValueSetSerializerTypeName is not null; + + /// + /// Creates a instance representing the current parameter. + /// + /// A instance representing the current parameter. + /// Thrown if the type is not valid. + public TypeSyntax GetSyntax() + { + return GetSyntax(Type, FullyQualifiedTypeName); + } +} \ No newline at end of file diff --git a/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Models/ParameterOrReturnType.cs b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Models/ParameterOrReturnType.cs new file mode 100644 index 000000000..793fbbdf4 --- /dev/null +++ b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Models/ParameterOrReturnType.cs @@ -0,0 +1,153 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace CommunityToolkit.AppServices.SourceGenerators.Models; + +/// +/// Indicates a type of parameter or return value that is supported for an app service method signature. +/// +/// The allowed element types are taken from . +[Flags] +internal enum ParameterOrReturnType +{ + /// + /// A value. + /// + UInt8 = 0x1 << 0, + + /// + /// A value. + /// + Int16 = 0x1 << 1, + + /// + /// An value. + /// + UInt16 = 0x1 << 2, + + /// + /// An value. + /// + Int32 = 0x1 << 3, + + /// + /// A value. + /// + UInt32 = 0x1 << 4, + + /// + /// A value. + /// + Int64 = 0x1 << 5, + + /// + /// A value. + /// + UInt64 = 0x1 << 6, + + /// + /// A value. + /// + Single = 0x1 << 7, + + /// + /// A value. + /// + Double = 0x1 << 8, + + /// + /// A value. + /// + Char16 = 0x1 << 9, + + /// + /// A value. + /// + Boolean = 0x1 << 10, + + /// + /// A value. + /// + String = 0x1 << 11, + + /// + /// A value. + /// + DateTime = 0x1 << 12, + + /// + /// A value. + /// + TimeSpan = 0x1 << 13, + + /// + /// A value. + /// + Guid = 0x1 << 14, + + /// + /// A Windows.Foundation.Point value. + /// + Point = 0x1 << 15, + + /// + /// A Windows.Foundation.Size value. + /// + Size = 0x1 << 16, + + /// + /// A Windows.Foundation.Rect value. + /// + Rect = 0x1 << 17, + + /// + /// An type. + /// + Enum = 0x1 << 18, + + /// + /// Gets a mask for all the existing element types. + /// + ElementTypeMask = UInt8 | Int16 | UInt16 | Int32 | UInt32 | Int64 | UInt64 | Single | Double | Char16 | Boolean | String | DateTime | TimeSpan | Guid | Point | Size | Rect | Enum, + + /// + /// An array type (when this flag is set, one of the flags above this must also be set). + /// + Array = 0x1 << 19, + + /// + /// A value (only valid for returns). + /// + Task = 0x1 << 20, + + /// + /// A value (only valid for returns). + /// + TaskOfT = 0x1 << 21, + + /// + /// An value (only valid for parameters, and must have a value flag set as well). + /// + IProgressOfT = 0x1 << 22, + + /// + /// A value (only valid for parameters). + /// + CancellationToken = 0x1 << 23, + + /// + /// A type that relies on a custom serializer in order to be marshalled across processes. + /// + /// + /// This is applied as follows: + /// + /// If the type is a parameter of type, the serializer applies to the inner progress values. + /// If the type is a parameter in all other cases, the serializer applies to the whole type (eg. if the type is an array, the serializer must be for T[] values, not T values. + /// If the type is in a return, the serializer applies to the type argument of the returned value. + /// + /// + CustomSerializerType = 0x1 << 24 +} \ No newline at end of file diff --git a/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Models/TypeInfo.cs b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Models/TypeInfo.cs new file mode 100644 index 000000000..2f9b2145a --- /dev/null +++ b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Models/TypeInfo.cs @@ -0,0 +1,46 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using static Microsoft.CodeAnalysis.CSharp.SyntaxFactory; + +namespace CommunityToolkit.AppServices.SourceGenerators.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) +{ + /// + /// Creates a instance for the current info. + /// + /// A instance for the current info. + public TypeDeclarationSyntax GetSyntax() + { + // Create the partial type declaration with the kind. + // This code produces a class declaration as follows: + // + // + // { + // } + // + // Note that specifically for record declarations, we also need to explicitly add the open + // and close brace tokens, otherwise member declarations will not be formatted correctly. + return Kind switch + { + TypeKind.Struct => StructDeclaration(QualifiedName), + TypeKind.Interface => InterfaceDeclaration(QualifiedName), + TypeKind.Class when IsRecord => + RecordDeclaration(Token(SyntaxKind.RecordKeyword), QualifiedName) + .WithOpenBraceToken(Token(SyntaxKind.OpenBraceToken)) + .WithCloseBraceToken(Token(SyntaxKind.CloseBraceToken)), + _ => ClassDeclaration(QualifiedName) + }; + } +} From da8c6576fa057905e836618b4d2d3f69acb88c57 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 31 May 2023 18:54:15 +0200 Subject: [PATCH 08/20] Update AppServices .csproj --- .../AppServices/src/AdditionalAssemblyInfo.cs | 13 ------- .../src/CommunityToolkit.AppServices.csproj | 39 +++++++++++++++++-- 2 files changed, 36 insertions(+), 16 deletions(-) delete mode 100644 components/AppServices/src/AdditionalAssemblyInfo.cs diff --git a/components/AppServices/src/AdditionalAssemblyInfo.cs b/components/AppServices/src/AdditionalAssemblyInfo.cs deleted file mode 100644 index 1a32f7ed2..000000000 --- a/components/AppServices/src/AdditionalAssemblyInfo.cs +++ /dev/null @@ -1,13 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System.Runtime.CompilerServices; - -// These `InternalsVisibleTo` calls are intended to make it easier for -// for any internal code to be testable in all the different test projects -// used with the Labs infrastructure. -[assembly: InternalsVisibleTo("AppServices.Tests.Uwp")] -[assembly: InternalsVisibleTo("AppServices.Tests.WinAppSdk")] -[assembly: InternalsVisibleTo("CommunityToolkit.Tests.Uwp")] -[assembly: InternalsVisibleTo("CommunityToolkit.Tests.WinAppSdk")] diff --git a/components/AppServices/src/CommunityToolkit.AppServices.csproj b/components/AppServices/src/CommunityToolkit.AppServices.csproj index 6a6411fa0..787dd42ff 100644 --- a/components/AppServices/src/CommunityToolkit.AppServices.csproj +++ b/components/AppServices/src/CommunityToolkit.AppServices.csproj @@ -10,10 +10,37 @@ - - - + + + + + + + System.Diagnostics.CodeAnalysis.NotNullAttribute; + System.Diagnostics.CodeAnalysis.NotNullWhenAttribute; + System.Diagnostics.CodeAnalysis.NotNullIfNotNullAttribute; + System.Diagnostics.CodeAnalysis.MemberNotNullAttribute; + System.Diagnostics.CodeAnalysis.DoesNotReturnAttribute; + System.Runtime.CompilerServices.CallerArgumentExpressionAttribute; + + + + + + + Windows Desktop Extensions for the UWP + + + + + + + + + + + @@ -28,4 +55,10 @@ + + + + + + From b24bb46b3a872d93ecbb678a655b143487be7dd0 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 31 May 2023 19:02:01 +0200 Subject: [PATCH 09/20] Port main AppServices code --- ...oolkit.AppServices.SourceGenerators.csproj | 1 + .../AppServices/src/AppServiceAttribute.cs | 28 + .../AppServices/src/AppServiceComponent.cs | 728 +++++++++++++++++ .../AppServices/src/AppServiceException.cs | 40 + components/AppServices/src/AppServiceHost.cs | 748 ++++++++++++++++++ .../AppServices/src/AppServiceStatus.cs | 76 ++ .../src/CommunityToolkit.AppServices.csproj | 4 +- .../src/Helpers/ValueSetMarshaller.cs | 194 +++++ .../AppServices/src/IValueSetSerializer{}.cs | 34 + .../src/ValueSetSerializerAttribute.cs | 28 + 10 files changed, 1880 insertions(+), 1 deletion(-) create mode 100644 components/AppServices/src/AppServiceAttribute.cs create mode 100644 components/AppServices/src/AppServiceComponent.cs create mode 100644 components/AppServices/src/AppServiceException.cs create mode 100644 components/AppServices/src/AppServiceHost.cs create mode 100644 components/AppServices/src/AppServiceStatus.cs create mode 100644 components/AppServices/src/Helpers/ValueSetMarshaller.cs create mode 100644 components/AppServices/src/IValueSetSerializer{}.cs create mode 100644 components/AppServices/src/ValueSetSerializerAttribute.cs diff --git a/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/CommunityToolkit.AppServices.SourceGenerators.csproj b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/CommunityToolkit.AppServices.SourceGenerators.csproj index 64d80c88a..f5e94b304 100644 --- a/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/CommunityToolkit.AppServices.SourceGenerators.csproj +++ b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/CommunityToolkit.AppServices.SourceGenerators.csproj @@ -14,6 +14,7 @@ + diff --git a/components/AppServices/src/AppServiceAttribute.cs b/components/AppServices/src/AppServiceAttribute.cs new file mode 100644 index 000000000..c22367bc0 --- /dev/null +++ b/components/AppServices/src/AppServiceAttribute.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace CommunityToolkit.AppServices; + +/// +/// An attribute that can be used to annotate an interface to generate app service connection points. +/// +[AttributeUsage(AttributeTargets.Interface, AllowMultiple = false, Inherited = false)] +public sealed class AppServiceAttribute : Attribute +{ + /// + /// Creates a new instance with the specified parameters. + /// + /// The name of the app service. + public AppServiceAttribute(string appServiceName) + { + AppServiceName = appServiceName; + } + + /// + /// Gets the name of the app service. + /// + public string AppServiceName { get; } +} diff --git a/components/AppServices/src/AppServiceComponent.cs b/components/AppServices/src/AppServiceComponent.cs new file mode 100644 index 000000000..26ec1d5a3 --- /dev/null +++ b/components/AppServices/src/AppServiceComponent.cs @@ -0,0 +1,728 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Threading.Tasks; +using Windows.Foundation.Collections; +using Windows.ApplicationModel.AppService; +using Windows.ApplicationModel; +using System.Runtime.CompilerServices; +using System.Collections.Generic; +using System.Threading; +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; +using CommunityToolkit.AppServices.Helpers; + +#pragma warning disable CA2213, CA1063 + +namespace CommunityToolkit.AppServices; + +/// +/// A base type for an app service component (replying to requests from a host). +/// +public abstract class AppServiceComponent : IDisposable +{ + /// + /// The name of the app service. + /// + private readonly string _appServiceName; + + /// + /// The mapping of available endpoints for this component. + /// + private readonly Dictionary>> _endpoints = new(); + + /// + /// + /// The mapping of cancellation keys to instances. This is used to associate + /// an executing request with its cancellation key, so that if the host sends a cancellation request, the component + /// can lookup its associated instance and propagate the cancellation. + /// + /// + /// This mapping is populated when a request is received from the host, and cleared right after it completes or is + /// canceled. Each individual request is responsible for adding and removing its own cancellation pair. + /// + /// + private readonly ConcurrentDictionary _cancellationSources = new(); + + /// + /// The instance to use. + /// + private AppServiceConnection? _connection; + + /// + /// Raised whenever an app service connection fails to connect. + /// + /// When this event is raised, usually a full trust process is expected to terminate. + public event EventHandler? ConnectionFailed; + + /// + /// Raised whenever an app service connection is closed. + /// + /// When this event is raised, usually a full trust process is expected to terminate. + public event EventHandler? ConnectionClosed; + + /// + /// Creates a new instance with the specified parameters. + /// + /// The name of the app service. + protected AppServiceComponent(string appServiceName) + { + _appServiceName = appServiceName; + } + + /// + /// Initializes the app service. + /// + public async Task InitializeAppService() + { + Dispose(); + + AppServiceConnection appServiceConnection = new() + { + PackageFamilyName = Package.Current.Id.FamilyName, + AppServiceName = _appServiceName, + }; + + appServiceConnection.RequestReceived += Connection_RequestReceived; + + AppServiceConnectionStatus status = await appServiceConnection.OpenAsync(); + + if (status != AppServiceConnectionStatus.Success) + { + ConnectionFailed?.Invoke(this, EventArgs.Empty); + } + else + { + // Resets the connection and closes the application + void Connection_ServiceClosed(AppServiceConnection sender, AppServiceClosedEventArgs args) + { + _ = Interlocked.CompareExchange(ref _connection, null, sender); + + ConnectionClosed?.Invoke(this, EventArgs.Empty); + } + + appServiceConnection.ServiceClosed += Connection_ServiceClosed; + + _connection = appServiceConnection; + } + } + + /// + public void Dispose() + { + AppServiceConnection? connection = Interlocked.CompareExchange(ref _connection, null, null); + + if (connection is not null) + { + connection.Dispose(); + } + } + + /// + /// Registers a synchronous endpoint. + /// + /// The endpoint function. + /// + /// The endpoint name (it uses targeting , + /// so a method group expression can be used to automatically pick up the method name as endpoint name). + /// + protected void RegisterEndpoint(Action endpoint, [CallerArgumentExpression("endpoint")] string? endpointName = null) + { + _endpoints.Add(endpointName!, _ => + { + try + { + endpoint(); + + return Task.FromResult(0); + } + catch (Exception e) + { + // Normalize exceptions for callers invoking the endpoint + return Task.FromException(e); + } + }); + } + + /// + /// Registers a synchronous endpoint. + /// + /// The type of return value for the endpoint. + /// The endpoint function. + /// + /// The endpoint name (it uses targeting , + /// so a method group expression can be used to automatically pick up the method name as endpoint name). + /// + protected void RegisterEndpoint(Func endpoint, [CallerArgumentExpression("endpoint")] string? endpointName = null) + { + _endpoints.Add(endpointName!, _ => + { + try + { + return Task.FromResult(ValueSetMarshaller.ToObject(endpoint())); + } + catch (Exception e) + { + // Normalize exceptions for callers invoking the endpoint + return Task.FromException(e); + } + }); + } + + /// + /// Registers a synchronous endpoint. + /// + /// The type of serializer to use. + /// The type of return value for the endpoint. + /// The serializer to use to serialize the return value. + /// The endpoint function. + /// + /// The endpoint name (it uses targeting , + /// so a method group expression can be used to automatically pick up the method name as endpoint name). + /// + protected void RegisterEndpoint(TSerializer serializer, Func endpoint, [CallerArgumentExpression("endpoint")] string? endpointName = null) + where TSerializer : IValueSetSerializer + { + _endpoints.Add(endpointName!, _ => + { + try + { + return Task.FromResult(serializer.Serialize(endpoint())); + } + catch (Exception e) + { + // Normalize exceptions for callers invoking the endpoint + return Task.FromException(e); + } + }); + } + + /// + /// Registers a synchronous endpoint. + /// + /// The endpoint function. + /// + /// The endpoint name (it uses targeting , + /// so a method group expression can be used to automatically pick up the method name as endpoint name). + /// + protected void RegisterEndpoint(Action endpoint, [CallerArgumentExpression("endpoint")] string? endpointName = null) + { + _endpoints.Add(endpointName!, parameters => + { + try + { + endpoint(parameters); + + return Task.FromResult(0); + } + catch (Exception e) + { + // Normalize exceptions for callers invoking the endpoint + return Task.FromException(e); + } + }); + } + + /// + /// Registers a synchronous endpoint. + /// + /// The type of return value for the endpoint. + /// The endpoint function. + /// + /// The endpoint name (it uses targeting , + /// so a method group expression can be used to automatically pick up the method name as endpoint name). + /// + protected void RegisterEndpoint(Func endpoint, [CallerArgumentExpression("endpoint")] string? endpointName = null) + { + _endpoints.Add(endpointName!, parameters => + { + try + { + return Task.FromResult(ValueSetMarshaller.ToObject(endpoint(parameters))); + } + catch (Exception e) + { + // Normalize exceptions for callers invoking the endpoint + return Task.FromException(e); + } + }); + } + + /// + /// Registers a synchronous endpoint. + /// + /// The type of serializer to use. + /// The type of return value for the endpoint. + /// The serializer to use to serialize the return value. + /// The endpoint function. + /// + /// The endpoint name (it uses targeting , + /// so a method group expression can be used to automatically pick up the method name as endpoint name). + /// + protected void RegisterEndpoint(TSerializer serializer, Func endpoint, [CallerArgumentExpression("endpoint")] string? endpointName = null) + where TSerializer : IValueSetSerializer + { + _endpoints.Add(endpointName!, parameters => + { + try + { + return Task.FromResult(serializer.Serialize(endpoint(parameters))); + } + catch (Exception e) + { + // Normalize exceptions for callers invoking the endpoint + return Task.FromException(e); + } + }); + } + + /// + /// Registers an asynchronous endpoint. + /// + /// The endpoint function. + /// + /// The endpoint name (it uses targeting , + /// so a method group expression can be used to automatically pick up the method name as endpoint name). + /// + protected void RegisterEndpoint(Func endpoint, [CallerArgumentExpression("endpoint")] string? endpointName = null) + { + _endpoints.Add(endpointName!, async _ => + { + await endpoint(); + + return 0; + }); + } + + /// + /// Registers an asynchronous endpoint. + /// + /// The type of return value for the endpoint. + /// The endpoint function. + /// + /// The endpoint name (it uses targeting , + /// so a method group expression can be used to automatically pick up the method name as endpoint name). + /// + protected void RegisterEndpoint(Func> endpoint, [CallerArgumentExpression("endpoint")] string? endpointName = null) + { + _endpoints.Add(endpointName!, async _ => ValueSetMarshaller.ToObject(await endpoint())); + } + + /// + /// Registers an asynchronous endpoint. + /// + /// The type of serializer to use. + /// The type of return value for the endpoint. + /// The serializer to use to serialize the return value. + /// The endpoint function. + /// + /// The endpoint name (it uses targeting , + /// so a method group expression can be used to automatically pick up the method name as endpoint name). + /// + protected void RegisterEndpoint(TSerializer serializer, Func> endpoint, [CallerArgumentExpression("endpoint")] string? endpointName = null) + where TSerializer : IValueSetSerializer + { + _endpoints.Add(endpointName!, async _ => serializer.Serialize(await endpoint())); + } + + /// + /// Registers an asynchronous endpoint. + /// + /// The endpoint function. + /// + /// The endpoint name (it uses targeting , + /// so a method group expression can be used to automatically pick up the method name as endpoint name). + /// + protected void RegisterEndpoint(Func endpoint, [CallerArgumentExpression("endpoint")] string? endpointName = null) + { + _endpoints.Add(endpointName!, async parameters => + { + await endpoint(parameters); + + return 0; + }); + } + + /// + /// Registers an asynchronous endpoint. + /// + /// The type of return value for the endpoint. + /// The endpoint function. + /// + /// The endpoint name (it uses targeting , + /// so a method group expression can be used to automatically pick up the method name as endpoint name). + /// + protected void RegisterEndpoint(Func> endpoint, [CallerArgumentExpression("endpoint")] string? endpointName = null) + { + _endpoints.Add(endpointName!, async parameters => ValueSetMarshaller.ToObject(await endpoint(parameters))); + } + + /// + /// Registers an asynchronous endpoint. + /// + /// The type of serializer to use. + /// The type of return value for the endpoint. + /// The serializer to use to serialize the return value. + /// The endpoint function. + /// + /// The endpoint name (it uses targeting , + /// so a method group expression can be used to automatically pick up the method name as endpoint name). + /// + protected void RegisterEndpoint(TSerializer serializer, Func> endpoint, [CallerArgumentExpression("endpoint")] string? endpointName = null) + where TSerializer : IValueSetSerializer + { + _endpoints.Add(endpointName!, async parameters => serializer.Serialize(await endpoint(parameters))); + } + + /// + /// Handles an incoming app service request. + /// + /// The instance for the request. + /// The request arguments. + private async void Connection_RequestReceived(AppServiceConnection sender, AppServiceRequestReceivedEventArgs args) + { + // Check if this is a cancellation request for a pending operation, and not an actual request. + // If that is the case, request cancellation and then return, as there is no more work to do. + try + { + if (args.Request.Message.TryGetValue(AppServiceHost.CancellationKey, out object? cancellationTokenKeyObj) && + cancellationTokenKeyObj is Guid cancellationKey) + { + RemoveCancellationSource(cancellationKey, requestCancellation: true); + + return; + } + } + catch + { + // Exceptions shouldn't happen when just requesting cancellation, but if they do, they can just be ignored + return; + } + + AppServiceDeferral deferral = args.GetDeferral(); + + try + { + // Extract the command name and the ValueSet instance with the arguments + if (args.Request.Message.TryGetValue(AppServiceHost.CommandKey, out object? command) && command is string commandStr && + args.Request.Message.TryGetValue(AppServiceHost.ArgsKey, out var argsObj) && argsObj is ValueSet parameters) + { + object? response; + + try + { + // Try to get the registered endpoint with the command name, and invoke it + if (_endpoints.TryGetValue(commandStr, out Func> endpoint)) + { + response = await endpoint(new AppServiceParameters(this, sender, parameters)); + } + else + { + throw new EntryPointNotFoundException(); + } + } + catch (EntryPointNotFoundException ex) + { + // The endpoint was not registered + ValueSet errorResponseContainer = new() + { + [AppServiceHost.StatusKey] = (int)AppServiceStatus.ActionNotFound, + [AppServiceHost.ReasonKey] = ex.Message + }; + + _ = await args.Request.SendResponseAsync(errorResponseContainer); + + return; + } + catch (OperationCanceledException) + { + // The operation was canceled, so it shouldn't be reported as an error + ValueSet errorResponseContainer = new() { [AppServiceHost.StatusKey] = (int)AppServiceStatus.Canceled }; + + _ = await args.Request.SendResponseAsync(errorResponseContainer); + + return; + } + catch (Exception e) + { + // Some exception was thrown by one of the registered endpoints + ValueSet errorResponseContainer = new() + { + [AppServiceHost.StatusKey] = (int)AppServiceStatus.Error, + [AppServiceHost.ReasonKey] = e.Message, + [AppServiceHost.HResultKey] = e.HResult + }; + + _ = await args.Request.SendResponseAsync(errorResponseContainer); + + return; + } + + // The endpoint was found and invoked successfully, return its result + ValueSet responseContainer = new() + { + [AppServiceHost.StatusKey] = (int)AppServiceStatus.Ok, + [AppServiceHost.ValueKey] = response + }; + + _ = await args.Request.SendResponseAsync(responseContainer); + } + else + { + // The input arguments to invoke the command were not present + ValueSet errorResponseContainer = new() + { + [AppServiceHost.StatusKey] = (int)AppServiceStatus.InvalidRequest + }; + + _ = await args.Request.SendResponseAsync(errorResponseContainer); + } + } + catch (Exception ex) + { + // Some unknown exception was thrown in the whole response handling + ValueSet errorResponseContainer = new() + { + [AppServiceHost.StatusKey] = (int)AppServiceStatus.Error, + [AppServiceHost.ReasonKey] = ex.Message, + [AppServiceHost.HResultKey] = ex.HResult + }; + + _ = await args.Request.SendResponseAsync(errorResponseContainer); + } + finally + { + // Regardless of the result, also remove the cancellation source, if one was available + if (args.Request.Message.TryGetValue(AppServiceHost.CancellationKey, out object? cancellationTokenKeyObj) && + cancellationTokenKeyObj is Guid cancellationKey) + { + RemoveCancellationSource(cancellationKey, requestCancellation: false); + } + + deferral.Complete(); + } + } + + /// + /// Adds a new cancellation source to the set managed by this component. + /// + /// The cancellation source id. + /// The instance. + private void AddCancellationSource(Guid id, CancellationTokenSource cancellationTokenSource) + { + _ = _cancellationSources.TryAdd(id, cancellationTokenSource); + } + + /// + /// Removes a cancellation source from the set managed by this component and optionally cancels it. + /// + /// The cancellation source id. + /// Whether to request cancellation when removing the cancellation source. + private void RemoveCancellationSource(Guid id, bool requestCancellation) + { + if (_cancellationSources.TryRemove(id, out CancellationTokenSource? cancellationTokenSource) && requestCancellation) + { + cancellationTokenSource.Cancel(); + } + } + + /// + /// An object that can be used to retrieve parameters + /// + protected readonly struct AppServiceParameters + { + /// + /// The owner for the current parameters. + /// + private readonly AppServiceComponent _component; + + /// + /// The instance used for the current request. + /// + private readonly AppServiceConnection _connection; + + /// + /// The object with the available parameters. + /// + private readonly ValueSet _valueSet; + + /// + /// Creates a new instance with the specified parameters. + /// + /// The owner for the current parameters. + /// The instance used for the current request. + /// The object with the available parameters. + public AppServiceParameters(AppServiceComponent component, AppServiceConnection connection, ValueSet valueSet) + { + _component = component; + _connection = connection; + _valueSet = valueSet; + } + + /// + /// Gets a parameter of a specified type. + /// + /// The type of parameter to retrieve. + /// The resulting parameter. + /// The parameter name. + /// Thrown if there is no parameter with the specified name. + public void GetParameter(out T parameter, [CallerArgumentExpression("parameter")] string? parameterName = null) + { + FixupParameterName(ref parameterName); + + // Try to get the requested parameter + if (!_valueSet.TryGetValue(parameterName, out object? value) || + !ValueSetMarshaller.TryGetValue(value, out T? result)) + { + throw new InvalidOperationException($"Failed to retrieve parameter with name \"{parameterName}\" and type \"{typeof(T)}\"."); + } + + parameter = result; + } + + /// + /// Gets a parameter of a specified type. + /// + /// The type of serializer to use. + /// The type of parameter to retrieve. + /// The serializer to use to load . + /// The resulting parameter. + /// The parameter name. + /// Thrown if there is no parameter with the specified name. + /// Thrown if the deserialization failed. + public void GetParameter(TSerializer serializer, out TParameter? parameter, [CallerArgumentExpression("parameter")] string? parameterName = null) + where TSerializer: IValueSetSerializer + { + FixupParameterName(ref parameterName); + + // Try to get the requested parameter + if (!_valueSet.TryGetValue(parameterName, out object? value) || + value is not (null or ValueSet)) + { + throw new InvalidOperationException($"Failed to retrieve parameter with name \"{parameterName}\" and type \"{typeof(ValueSet)}\"."); + } + + parameter = serializer.Deserialize((ValueSet?)value); + } + + /// + /// Gets an instance of a specified type. + /// + /// The type of progress values being used. + /// The resulting instance. + /// Thrown if the current request has no progress support. + public void GetProgress(out IProgress progress) + { + // Try to get the progress id to send values back to the host + if (!_valueSet.TryGetValue(AppServiceHost.ProgressKey, out object? progressKeyObj) || + progressKeyObj is not Guid progressKey) + { + throw new InvalidOperationException("The current request does not support progress reporting."); + } + + AppServiceConnection connection = _connection; + + async void ReportProgress(T value) + { + try + { + await connection.SendMessageAsync(new ValueSet + { + [AppServiceHost.ProgressKey] = progressKey, + [AppServiceHost.ProgressValue] = ValueSetMarshaller.ToObject(value) + }); + } + catch + { + // If a progress message is lost, it can just be ignored. The operation + // should not fail nor the component should crash if that happens. + } + } + + progress = new Progress(ReportProgress); + } + + /// + /// Gets an instance of a specified type. + /// + /// The type of serializer to use. + /// The type of return value for the endpoint. + /// The serializer to use to serialize the return value. + /// The resulting instance. + /// Thrown if the current request has no progress support. + public void GetProgress(TSerializer serializer, out IProgress progress) + where TSerializer : IValueSetSerializer + { + // Try to get the progress id to send values back to the host + if (!_valueSet.TryGetValue(AppServiceHost.ProgressKey, out object? progressKeyObj) || + progressKeyObj is not Guid progressKey) + { + throw new InvalidOperationException("The current request does not support progress reporting."); + } + + AppServiceConnection connection = _connection; + + async void ReportProgress(TResult? value) + { + try + { + await connection.SendMessageAsync(new ValueSet + { + [AppServiceHost.ProgressKey] = progressKey, + [AppServiceHost.ProgressValue] = serializer.Serialize(value) + }); + } + catch + { + // If a progress message is lost, it can just be ignored. The operation + // should not fail nor the component should crash if that happens. + } + } + + progress = new Progress(ReportProgress); + } + + /// + /// Gets a instance from the current parameters. + /// + /// The resulting instance. + /// Thrown if the current request has no cancellation support. + public void GetCancellationToken(out CancellationToken cancellationToken) + { + // Try to get the cancellation id to lookup the local CancellationTokenSource instance + if (!_valueSet.TryGetValue(AppServiceHost.CancellationKey, out object? cancellationTokenKeyObj) || + cancellationTokenKeyObj is not Guid cancellationTokenKey) + { + throw new InvalidOperationException("The current request does not support cancellation."); + } + + CancellationTokenSource cancellationTokenSource = new(); + + _component.AddCancellationSource(cancellationTokenKey, cancellationTokenSource); + + cancellationToken = cancellationTokenSource.Token; + } + + /// + /// Applies the necessary fixup to a parameter name. + /// + /// The parameter name to fixup. + private static void FixupParameterName([NotNull] ref string? parameterName) + { + // If the parameter name has no spaces, it means it's an explicit name. In that case + // the valuer doesn't need to be parsed, and it can be used directly as value name. + if (parameterName!.IndexOf(' ') != -1) + { + int leftSeparatorIndex = parameterName.TrimEnd().LastIndexOf(' '); + int rightSeparatorIndex = parameterName.TrimEnd().Length - 1; + + // The input expression will be in the form: "out ". We need to get the part. + // So we trim the end and get the last index of ' ' (ie. the space on the left), and then also + // trim and calculate the actual end of the parameter name. Then we slice the input name with that. + parameterName = parameterName!.Substring(leftSeparatorIndex + 1, rightSeparatorIndex - leftSeparatorIndex); + } + } + } +} diff --git a/components/AppServices/src/AppServiceException.cs b/components/AppServices/src/AppServiceException.cs new file mode 100644 index 000000000..1ba6f70b2 --- /dev/null +++ b/components/AppServices/src/AppServiceException.cs @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace CommunityToolkit.AppServices; + +/// +/// An that is thrown by a host or component when an app service operation fails. +/// +public sealed class AppServiceException : Exception +{ + /// + /// Creates a new with the specified parameters. + /// + /// The for the exception. + /// The exception message. + /// The error code for the exception. + internal AppServiceException(AppServiceStatus status, string message, int hresult) + : base(message) + { + Status = status; + HResult = hresult; + } + + /// + /// Creates a new with the specified parameters. + /// + /// The for the exception. + internal AppServiceException(AppServiceStatus status) + { + Status = status; + } + + /// + /// Gets the status associated with the current exception. + /// + public AppServiceStatus Status { get; } +} \ No newline at end of file diff --git a/components/AppServices/src/AppServiceHost.cs b/components/AppServices/src/AppServiceHost.cs new file mode 100644 index 000000000..e1ab1b0e0 --- /dev/null +++ b/components/AppServices/src/AppServiceHost.cs @@ -0,0 +1,748 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Windows.ApplicationModel; +using Windows.ApplicationModel.Activation; +using Windows.ApplicationModel.AppService; +using Windows.ApplicationModel.Background; +using Windows.Foundation.Collections; +using Windows.Foundation.Metadata; +using Windows.System.Profile; +using CommunityToolkit.AppServices.Helpers; + +#pragma warning disable CA1068 + +namespace CommunityToolkit.AppServices; + +/// +/// A base type for an app service host (sending requests to a component). +/// +public abstract class AppServiceHost +{ + internal const string CommandKey = "__endpoint"; + internal const string ArgsKey = "__args"; + internal const string StatusKey = "__status"; + internal const string ReasonKey = "__reason"; + internal const string ValueKey = "__value"; + internal const string HResultKey = "__HRESULT"; + internal const string ProgressKey = "__progressKey"; + internal const string ProgressValue = "__progressValue"; + internal const string CancellationKey = "__cancellationKey"; + + /// + /// The name of the app service. + /// + private readonly string _appServiceName; + private readonly SemaphoreSlim _semaphoreConnection = new(0, 1); + private readonly SemaphoreSlim _lockConnection = new(1, 1); + + /// + /// + /// The mapping of progress keys to instances. This is used to associate + /// incoming progress messages with the instance passed by callers, so + /// the current progress amount can be forwarded to the right one. + /// + /// + /// This mapping is populated when a request is started, and cleared right after it completes. + /// Each individual request is responsible for adding and removing its own progress pair. + /// + /// + private readonly ConcurrentDictionary> _progressTrackers = new(); + private BackgroundTaskDeferral? _appServiceDeferral; + private AppServiceConnection? _appServiceConnection; + + /// + /// Creates a new instance with the specified parameters. + /// + /// The name of the app service. + protected AppServiceHost(string appServiceName) + { + _appServiceName = appServiceName; + } + + /// + /// Gets a value indicating whether the app service functionality can be used. + /// + private static bool CanUseAppServiceFunctionality { get; } = AnalyticsInfo.VersionInfo.DeviceFamily == "Windows.Desktop" && ApiInformation.IsApiContractPresent("Windows.ApplicationModel.FullTrustAppContract", 1, 0); + + /// + /// Handles the app service activation when is invoked. + /// + /// The args for the background activation. + /// Whether this activation was an app service connection that could be handled by this host. + /// + /// + /// When this method returns , no further work should be done by the caller. + /// + /// + /// This method should be used as follows (from App.xaml.cs): + /// + /// protected override void OnBackgroundActivated(BackgroundActivatedEventArgs args) + /// { + /// base.OnBackgroundActivated(args); + /// + /// if (DesktopExtension.OnBackgroundActivated(args)) + /// { + /// return; + /// } + /// + /// // Any other work, if needed + /// } + /// + /// + /// + public bool OnBackgroundActivated(BackgroundActivatedEventArgs args) + { + IBackgroundTaskInstance backgroundTaskInstance = args.TaskInstance; + + // Check that the activation is an app service connection response + if (backgroundTaskInstance.TriggerDetails is not AppServiceTriggerDetails appServiceTriggerDetails) + { + return false; + } + + // Check if the connection is from the same package + if (!appServiceTriggerDetails.CallerPackageFamilyName.Equals(Package.Current.Id.FamilyName, StringComparison.InvariantCultureIgnoreCase)) + { + return false; + } + + // Check that the app service name matches the one for this host instance + if (!appServiceTriggerDetails.AppServiceConnection.AppServiceName.Equals(_appServiceName, StringComparison.InvariantCultureIgnoreCase)) + { + return false; + } + + BackgroundTaskDeferral? previousAppServiceDeferral = _appServiceDeferral; + + _appServiceDeferral = backgroundTaskInstance.GetDeferral(); + + bool hadNoConnection = _appServiceConnection is null; + + _appServiceConnection = appServiceTriggerDetails.AppServiceConnection; + + appServiceTriggerDetails.AppServiceConnection.ServiceClosed += AppServiceConnection_ServiceClosed; + appServiceTriggerDetails.AppServiceConnection.RequestReceived += AppServiceConnection_RequestReceived; + + if (hadNoConnection) + { + _semaphoreConnection.Release(); + } + + previousAppServiceDeferral?.Complete(); + + return true; + } + + /// + /// Creates a new for a given operation. + /// + /// The name of the request to prepare. + /// An instance to construct a request to send. + protected AppServiceRequest CreateAppServiceRequest([CallerMemberName] string? requestName = null) + { + return new(this, requestName!); + } + + /// + /// Closes a connection when a service is closed. + /// + /// The that was closed. + /// The arguments for the operation. + private void AppServiceConnection_ServiceClosed(AppServiceConnection sender, AppServiceClosedEventArgs args) + { + CloseConnection(sender); + } + + /// + /// Closes a target instance. + /// + /// The instance to close. + private void CloseConnection(AppServiceConnection appServiceConnection) + { + if (appServiceConnection is null) + { + return; + } + + appServiceConnection.ServiceClosed -= AppServiceConnection_ServiceClosed; + appServiceConnection.RequestReceived -= AppServiceConnection_RequestReceived; + + if (_appServiceConnection == appServiceConnection) + { + _appServiceConnection = null; + + try + { + _appServiceDeferral?.Complete(); + } + catch + { + } + } + } + + /// + /// Handles an incoming app service request. + /// + /// The instance that received the request. + /// The request arguments. + private void AppServiceConnection_RequestReceived(AppServiceConnection sender, AppServiceRequestReceivedEventArgs args) + { + // If this is a request to report progress, try to retrieve the progress tracker and invoke it. + // If the progress is not present, it means its associated request has already completed, so it + // is safe to ignore it. Unhandled exceptions in the progress handler will bubble up normally. + if (args.Request.Message.TryGetValue(ProgressKey, out object? progressKey) && + args.Request.Message.TryGetValue(ProgressValue, out object? progressValue) && + progressKey is Guid id && + _progressTrackers.TryGetValue(id, out IProgress progress)) + { + progress.Report(progressValue); + } + } + + /// + /// Tries to get an instance to send request to. + /// + /// The connection timeout. + /// A producing the connection, if successful. + /// Thrown if trying to create or open a connection failed. + /// Thrown if the full trust process fails to launch. + private async Task GetConnectionAsync(TimeSpan timeout) + { + if (_appServiceConnection is null) + { + await _lockConnection.WaitAsync(); + + try + { + if (_appServiceConnection is null) + { + if (await StartAppServiceAsync()) + { + if (_appServiceConnection is null && !await _semaphoreConnection.WaitAsync(timeout)) + { + if (_appServiceConnection is null) + { + throw new AppServiceException(AppServiceStatus.Timeout); + } + } + } + else + { + throw new AppServiceException(AppServiceStatus.CantStart); + } + } + } + finally + { + _lockConnection.Release(); + } + } + + return _appServiceConnection; + } + + /// + /// Starts the app service connection. + /// + /// Whether starting the connection was successful. + private static async Task StartAppServiceAsync() + { + if (!CanUseAppServiceFunctionality) + { + return false; + } + + try + { + await FullTrustProcessLauncher.LaunchFullTrustProcessForCurrentAppAsync(); + + return true; + } + catch + { + return false; + } + } + + /// + /// Sends an app service connection message. + /// + /// The request parameters. + /// The id to notify cancellation, if available. + /// The instance to use. + /// The timeout duration. + /// Whether the operation can be attempted again in case of failure. + /// The response for the request. + private async Task SendMessageAsync( + ValueSet request, + Guid cancellationKey, + CancellationToken cancellationToken, + TimeSpan timeout, + bool canRetry = true) + { + if (await GetConnectionAsync(timeout) is not { } connection) + { + return null; + } + + AppServiceResponse response; + CancellationTokenRegistration registration; + + // If there is a valid cancellation token, handle the possible cases. + // Otherwise, just use a dummy registration token that does nothing. + if (cancellationKey == Guid.Empty) + { + registration = default; + } + else + { + // If the token has already been canceled, just fail immediately + if (cancellationToken.IsCancellationRequested) + { + throw new AppServiceException(AppServiceStatus.Canceled); + } + + // Otherwise, register the token cancellation to notify the component + registration = cancellationToken.Register(async () => + { + try + { + await connection.SendMessageAsync(new ValueSet { [CancellationKey] = cancellationKey }); + } + catch + { + // If a cancellation request is lost, just ignore it. This shouldn't really fail anyway + // given that at this point the connection has already been established with the component. + // If that wasn't the case, it'd mean that there would not be a request to cancel at all. + } + }); + } + + // Ensure the cancellation registration is unregistered once the request completes + using (registration) + { + response = await connection.SendMessageAsync(request); + } + + if (response?.Status == AppServiceResponseStatus.Failure && canRetry) + { + // When the application process is paused by the OS, it also stops the full trust (extension) process + // without raising AppServiceConnection.ServiceClosed. To mitigate the scenario, we create a new + // appServiceConnection (including launching the full trust process) and try one more time. + CloseConnection(connection); + + return await SendMessageAsync(request, cancellationKey, cancellationToken, timeout, false); + } + + return response; + } + + /// + /// Adds a new progress tracker to the set managed by this host. + /// + /// The progress tracker id. + /// The instance. + private void AddProgressTracker(Guid id, IProgress progress) + { + _ = _progressTrackers.TryAdd(id, progress); + } + + /// + /// Removes a progress tracker from the set managed by this host. + /// + /// The progress tracker id. + private void RemoveProgressTracker(Guid id) + { + _ = _progressTrackers.TryRemove(id, out _); + } + + /// + /// Sends an app service request. + /// + /// The type of response to expect. + /// The name of the endpoint to invoke. + /// The request arguments. + /// The id to notify cancellation, if available. + /// The instance to use. + /// The optional serializer for the return value. + /// The timeout duration. + /// The result for the request. + /// Thrown if the request fails. + private async Task SendAppServiceRequestAsync( + string commandName, + ValueSet args, + Guid cancellationKey, + CancellationToken cancellationToken, + IValueSetSerializer? serializer, + TimeSpan timeout) + { + if (!CanUseAppServiceFunctionality) + { + throw new AppServiceException(AppServiceStatus.AppServiceNotCompatible); + } + + ValueSet request = new() { [CommandKey] = commandName, [ArgsKey] = args }; + AppServiceResponse? answer = await SendMessageAsync(request, cancellationKey, cancellationToken, timeout); + + if (answer?.Status == AppServiceResponseStatus.Success) + { + if (answer.Message.TryGetValue(StatusKey, out object? statusValue)) + { + if (statusValue is not int rawStatus) + { + throw new AppServiceException(AppServiceStatus.InvalidResponse); + } + + AppServiceStatus status = (AppServiceStatus)rawStatus; + + if (status == AppServiceStatus.Ok) + { + // The response must contain the known return value + if (!answer.Message.TryGetValue(ValueKey, out object? value)) + { + throw new AppServiceException(AppServiceStatus.InvalidResponse); + } + + T? result = default; + + // The response must match the expected return value: + // - If a serializer is available, it must be null or a ValueSet + // - If there is no serializer, it must be a valid T instance + if ((serializer is not null && value is not (null or ValueSet)) || + (serializer is null && (value is null || !ValueSetMarshaller.TryGetValue(value, out result)))) + { + throw new AppServiceException(AppServiceStatus.MismatchedResponseType); + } + + // If there is a serializer, invoke it to produce the return value + if (serializer is not null) + { + try + { + return serializer.Deserialize((ValueSet?)value); + } + catch + { + throw new AppServiceException(AppServiceStatus.SerializationError); + } + } + + // Otherwise return the value directly + return result; + } + else if (status is AppServiceStatus.InvalidRequest or AppServiceStatus.Canceled) + { + // If the operation is canceled or an invalid request was sent, no other exception info is needed + throw new AppServiceException(status); + } + + // Report an error on the component side + throw new AppServiceException( + status, + (string)answer.Message[ReasonKey], + answer.Message.TryGetValue(HResultKey, out object? hr) ? (int)hr : 0); + } + else + { + throw new AppServiceException(AppServiceStatus.NoResponse); + } + } + else + { + throw new AppServiceException(AppServiceStatus.CantSend); + } + } + + /// + /// An object that can be used to build app service requests. + /// + protected sealed class AppServiceRequest + { + /// + /// The source instance. + /// + private readonly AppServiceHost _host; + + /// + /// The request name. + /// + private readonly string _requestName; + + /// + /// The instance with the arguments for the current request. + /// + private readonly ValueSet _valueSet; + + /// + /// The instance to use, if available; + /// + private IProgress? _progress; + + /// + /// The id of the instance to use, if available. + /// + private Guid _progressId; + + /// + /// The instance to use, if available. + /// + private CancellationToken _cancellationToken; + + /// + /// The id of the instance to use, if available. + /// + private Guid _cancellationTokenId; + + /// + /// Creates a new instance with the specified parameters. + /// + /// The source instance. + /// The request name. + public AppServiceRequest(AppServiceHost host, string requestName) + { + _host = host; + _requestName = requestName; + _valueSet = new ValueSet(); + } + + /// + /// Adds a new parameter to the request. + /// + /// The type of the parameter. + /// The parameter to add to the request. + /// The parameter name. + /// The current instance. + public AppServiceRequest WithParameter(T parameter, [CallerArgumentExpression("parameter")] string? parameterName = null) + { + _valueSet.Add(parameterName!, ValueSetMarshaller.ToObject(parameter)); + + return this; + } + + /// + /// Adds a new parameter to the request. + /// + /// The type of serializer to use. + /// The type of parameter to retrieve. + /// The serializer to use to load . + /// The parameter to add to the request. + /// The parameter name. + /// The current instance. + public AppServiceRequest WithParameter(TSerializer serializer, TParameter? parameter, [CallerArgumentExpression("parameter")] string? parameterName = null) + where TSerializer : IValueSetSerializer + { + _valueSet.Add(parameterName!, serializer.Serialize(parameter)); + + return this; + } + + /// + /// Adds an parameter to the request. + /// + /// The progress value to use. + /// The instance for the request. + /// The current instance. + /// Thrown if is . + /// Thrown if an instance has already been registered on this request. + /// A request can only have a single object associated with it. + [MemberNotNull(nameof(_progress))] + public AppServiceRequest WithProgress(IProgress progress) + { + if (progress is null) + { + throw new ArgumentNullException(nameof(progress)); + } + + if (_progressId != Guid.Empty) + { + throw new InvalidOperationException("Only one IProgress instance can be used per AppService request."); + } + + _progress = new Progress(value => progress.Report(ValueSetMarshaller.ToValue(value))); + _progressId = Guid.NewGuid(); + + return this; + } + + /// + /// Adds an parameter to the request. + /// + /// The type of serializer to use. + /// The type of return value for the endpoint. + /// The serializer to use to serialize the return value. + /// The instance for the request. + /// The current instance. + /// Thrown if is . + /// Thrown if an instance has already been registered on this request. + /// A request can only have a single object associated with it. + [MemberNotNull(nameof(_progress))] + public AppServiceRequest WithProgress(TSerializer serializer, IProgress progress) + where TSerializer : IValueSetSerializer + { + if (progress is null) + { + throw new ArgumentNullException(nameof(progress)); + } + + if (_progressId != Guid.Empty) + { + throw new InvalidOperationException("Only one IProgress instance can be used per AppService request."); + } + + _progress = new Progress(value => progress.Report(serializer.Deserialize((ValueSet?)value))); + _progressId = Guid.NewGuid(); + + return this; + } + + /// + /// Adds a parameter to the request. + /// + /// The instance for the request. + /// The current instance. + /// Thrown if has already been called on this request. + /// The can only be called once per request. + public AppServiceRequest WithCancellationToken(CancellationToken cancellationToken) + { + if (_cancellationTokenId != Guid.Empty) + { + throw new InvalidOperationException("Only one CancellationToken instance can be used per AppService request."); + } + + _cancellationToken = cancellationToken; + _cancellationTokenId = Guid.NewGuid(); + + return this; + } + + /// + /// Sends the request, waits for a response and tries to convert the result to a specified type. + /// + /// A with the expected result, if successful. + /// This overload will use a default timeout value of 5 seconds. + /// Thrown if the request fails. + public Task SendAndWaitForResultAsync() + { + return SendAndWaitForResultAsync(serializer: null, TimeSpan.FromSeconds(5)); + } + + /// + /// Sends the request, waits for a response and tries to convert the result to a specified type. + /// + /// The type of result to expect. + /// A with the expected result, if successful. + /// This overload will use a default timeout value of 5 seconds. + /// Thrown if the request fails. + public Task SendAndWaitForResultAsync() + { + return SendAndWaitForResultAsync(serializer: null, TimeSpan.FromSeconds(5))!; + } + + /// + /// Sends the request, waits for a response and tries to convert the result to a specified type. + /// + /// The type of serializer to use. + /// The type of return value for the endpoint. + /// The serializer to use to serialize the return value. + /// A with the expected result, if successful. + /// This overload will use a default timeout value of 5 seconds. + /// Thrown if the request fails. + public Task SendAndWaitForResultAsync(TSerializer serializer) + where TSerializer : IValueSetSerializer + { + return SendAndWaitForResultAsync(serializer, TimeSpan.FromSeconds(5)); + } + + /// + /// Sends the request, waits for a response and tries to convert the result to a specified type. + /// + /// The timeout to start and wait for a service connection. + /// A with the expected result, if successful. + /// Thrown if the request fails. + public Task SendAndWaitForResultAsync(TimeSpan timeout) + { + return SendAndWaitForResultAsync(serializer: null, timeout); + } + + /// + /// Sends the request, waits for a response and tries to convert the result to a specified type. + /// + /// The type of result to expect. + /// The timeout to start and wait for a service connection. + /// A with the expected result, if successful. + /// Thrown if the request fails. + public Task SendAndWaitForResultAsync(TimeSpan timeout) + { + return SendAndWaitForResultAsync(serializer: null, timeout)!; + } + + /// + /// Sends the request, waits for a response and tries to convert the result to a specified type. + /// + /// The type of serializer to use. + /// The type of return value for the endpoint. + /// The serializer to use to serialize the return value. + /// The timeout to start and wait for a service connection. + /// A with the expected result, if successful. + /// Thrown if the request fails. + public Task SendAndWaitForResultAsync(TSerializer serializer, TimeSpan timeout) + where TSerializer : IValueSetSerializer + { + return SendAndWaitForResultAsync(serializer, timeout); + } + + /// + /// Sends the request, waits for a response and tries to convert the result to a specified type. + /// + /// The type of result to expect. + /// The serializer, if available. + /// The timeout to start and wait for a service connection. + /// A with the expected result, if successful. + /// Thrown if the request fails. + private async Task SendAndWaitForResultAsync(IValueSetSerializer? serializer, TimeSpan timeout) + { + // If a progress has been configured, add it to the trackers set for the current host. + // The id also needs to be sent, as it'll be used by the component to report progress back. + if (_progressId != Guid.Empty) + { + _valueSet.Add(ProgressKey, _progressId); + + _host.AddProgressTracker(_progressId, _progress!); + } + + // If a cancellation token has been configured + if (_cancellationTokenId != Guid.Empty) + { + _valueSet.Add(CancellationKey, _cancellationTokenId); + } + + try + { + return await _host.SendAppServiceRequestAsync( + _requestName, + _valueSet, + _cancellationTokenId, + _cancellationToken, + serializer, + timeout); + } + finally + { + // Remove the progress tracker when the request is complete + if (_progress is not null) + { + _host.RemoveProgressTracker(_progressId); + } + } + } + } +} \ No newline at end of file diff --git a/components/AppServices/src/AppServiceStatus.cs b/components/AppServices/src/AppServiceStatus.cs new file mode 100644 index 000000000..b6b848202 --- /dev/null +++ b/components/AppServices/src/AppServiceStatus.cs @@ -0,0 +1,76 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace CommunityToolkit.AppServices; + +/// +/// Indicates the status of an app service operation. +/// +public enum AppServiceStatus +{ + /// + /// The operation completed successfully. + /// + Ok, + + /// + /// The operation failed with an error. + /// + Error, + + /// + /// The entry point for the operation was not found. + /// + ActionNotFound, + + /// + /// No response was received for the operation. + /// + NoResponse, + + /// + /// A request was received, but it was invalid. + /// + InvalidRequest, + + /// + /// A response was received, but it was invalid. + /// + InvalidResponse, + + /// + /// A response result was received, but it was of a mismatched type. + /// + MismatchedResponseType, + + /// + /// An error occurred while trying to deserialize the response with a custom serializer. + /// + SerializationError, + + /// + /// There was an error and it wasn't possible to send a request. + /// + CantSend, + + /// + /// The app service is not compatible. + /// + AppServiceNotCompatible, + + /// + /// The operation timed out. + /// + Timeout, + + /// + /// The operation was canceled. + /// + Canceled, + + /// + /// The operation failed to start. + /// + CantStart +} diff --git a/components/AppServices/src/CommunityToolkit.AppServices.csproj b/components/AppServices/src/CommunityToolkit.AppServices.csproj index 787dd42ff..8a9564058 100644 --- a/components/AppServices/src/CommunityToolkit.AppServices.csproj +++ b/components/AppServices/src/CommunityToolkit.AppServices.csproj @@ -1,7 +1,8 @@ AppServices - This package contains AppServices. + This package contains AppServices, to easily communicate between UWP apps and Win32 extensions. + true 0.0.1 CommunityToolkit.AppServices $(PackageIdPrefix).$(ToolkitComponentName) @@ -11,6 +12,7 @@ + diff --git a/components/AppServices/src/Helpers/ValueSetMarshaller.cs b/components/AppServices/src/Helpers/ValueSetMarshaller.cs new file mode 100644 index 000000000..cfe152e5b --- /dev/null +++ b/components/AppServices/src/Helpers/ValueSetMarshaller.cs @@ -0,0 +1,194 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Diagnostics.CodeAnalysis; + +namespace CommunityToolkit.AppServices.Helpers; + +/// +/// A base type for an app service host (sending requests to a component). +/// +internal static class ValueSetMarshaller +{ + /// + /// Converts a value into an . + /// + /// The type of input value to convert. + /// The input value to convert. + /// An conversion of compatible with . + /// Thrown if is not a valid type. + [return: NotNullIfNotNull(nameof(value))] + public static object? ToObject(T value) + { + if (typeof(T).IsEnum) + { + Type underlyingType = typeof(T).GetEnumUnderlyingType(); + + if (underlyingType == typeof(byte)) + { + return (byte)(object)value!; + } + + if (underlyingType == typeof(sbyte)) + { + return (sbyte)(object)value!; + } + + if (underlyingType == typeof(short)) + { + return (short)(object)value!; + } + + if (underlyingType == typeof(ushort)) + { + return (ushort)(object)value!; + } + + if (underlyingType == typeof(int)) + { + return (int)(object)value!; + } + + if (underlyingType == typeof(uint)) + { + return (uint)(object)value!; + } + + if (underlyingType == typeof(long)) + { + return (long)(object)value!; + } + + if (underlyingType == typeof(ulong)) + { + return (ulong)(object)value!; + } + + ThrowArgumentExceptionForInvalidEnumType(); + } + + return value; + } + + /// + /// Converts an instance into a value. + /// + /// The type of value to convert. + /// The input instance to convert. + /// The resulting converted value. + /// Thrown if is not a valid type. + public static T ToValue(object data) + { + if (!TryGetValue(data, out T? result)) + { + ThrowArgumentExceptionForInvalidDataType(); + } + + return result; + } + + /// + /// Tries to convert an instance into a value. + /// + /// The type of value to convert. + /// The input instance to convert. + /// The resulting value, if the conversion was successful. + /// Whether the conversion was successful.. + /// Thrown if is not a valid type. + public static bool TryGetValue(object data, [NotNullWhen(true)] out T? value) + { + if (typeof(T).IsEnum) + { + Type underlyingType = typeof(T).GetEnumUnderlyingType(); + + if (underlyingType == typeof(byte)) + { + value = (T)(object)(byte)data; + + return true; + } + + if (underlyingType == typeof(sbyte)) + { + value = (T)(object)(sbyte)data; + + return true; + } + + if (underlyingType == typeof(short)) + { + value = (T)(object)(short)data; + + return true; + } + + if (underlyingType == typeof(ushort)) + { + value = (T)(object)(ushort)data; + + return true; + } + + if (underlyingType == typeof(int)) + { + value = (T)(object)(int)data; + + return true; + } + + if (underlyingType == typeof(uint)) + { + value = (T)(object)(uint)data; + + return true; + } + + if (underlyingType == typeof(long)) + { + value = (T)(object)(long)data; + + return true; + } + + if (underlyingType == typeof(ulong)) + { + value = (T)(object)(ulong)data; + + return true; + } + + ThrowArgumentExceptionForInvalidEnumType(); + } + + if (data is T matchingValue) + { + value = matchingValue; + + return true; + } + + value = default; + + return false; + } + + /// + /// Throws an for an enum with an invalid type. + /// + [DoesNotReturn] + private static void ThrowArgumentExceptionForInvalidEnumType() + { + throw new ArgumentException("The input enum type is not valid."); + } + + /// + /// Throws an for input data with an invalid value. + /// + [DoesNotReturn] + private static void ThrowArgumentExceptionForInvalidDataType() + { + throw new ArgumentException("The input data is not valid."); + } +} \ No newline at end of file diff --git a/components/AppServices/src/IValueSetSerializer{}.cs b/components/AppServices/src/IValueSetSerializer{}.cs new file mode 100644 index 000000000..d5364075a --- /dev/null +++ b/components/AppServices/src/IValueSetSerializer{}.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Diagnostics.CodeAnalysis; +using Windows.Foundation.Collections; + +namespace CommunityToolkit.AppServices; + +/// +/// An interface for a custom serializer for parameters marshalled through objects. +/// +/// The type of values being serialized. +public interface IValueSetSerializer +{ + /// + /// Serializes an input value into a object. + /// + /// The input object to serialize. + /// The object representing . + /// Thrown if serializing failed. + [return: NotNullIfNotNull("value")] + ValueSet? Serialize(T? value); + + /// + /// Deserializes an input object into a value. + /// + /// The input object to deserialize. + /// The deserialized value. + /// Thrown if deserializing failed. + [return: NotNullIfNotNull("valueSet")] + T? Deserialize(ValueSet? valueSet); +} diff --git a/components/AppServices/src/ValueSetSerializerAttribute.cs b/components/AppServices/src/ValueSetSerializerAttribute.cs new file mode 100644 index 000000000..0510f4bde --- /dev/null +++ b/components/AppServices/src/ValueSetSerializerAttribute.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace CommunityToolkit.AppServices; + +/// +/// An attribute that can be used to annotate an interface to generate app service connection points. +/// +[AttributeUsage(AttributeTargets.ReturnValue | AttributeTargets.Parameter, AllowMultiple = false, Inherited = false)] +public sealed class ValueSetSerializerAttribute : Attribute +{ + /// + /// Creates a new instance with the specified parameters. + /// + /// The type of to use. + public ValueSetSerializerAttribute(Type valueSetSerializerType) + { + ValueSetSerializerType = valueSetSerializerType; + } + + /// + /// Gets the type of serializer to use. + /// + public Type ValueSetSerializerType { get; } +} From 77fd1f76a308a7689ed43b4cb7e16f160874d005 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 31 May 2023 19:06:04 +0200 Subject: [PATCH 10/20] Update docs --- components/AppServices/samples/AppServices.md | 62 +++---------------- 1 file changed, 7 insertions(+), 55 deletions(-) diff --git a/components/AppServices/samples/AppServices.md b/components/AppServices/samples/AppServices.md index 59ff6fb12..b1370cedb 100644 --- a/components/AppServices/samples/AppServices.md +++ b/components/AppServices/samples/AppServices.md @@ -1,64 +1,16 @@ --- title: AppServices -author: githubaccount -description: TODO: Your experiment's description here -keywords: AppServices, Control, Layout +author: Sergio0694 +description: AppServices is a library to easily communicate between UWP apps and Win32 extensions +keywords: AppServices, uwp, xaml, win32, fulltrust, extension, rpc dev_langs: - csharp -category: Controls -subcategory: Layout -discussion-id: 0 +category: Extensions +subcategory: None +discussion-id: 301 issue-id: 0 --- - - - - - - - - # AppServices -TODO: Fill in information about this experiment and how to get started here... - -## Custom Control - -You can inherit from an existing component as well, like `Panel`, this example shows a control without a -XAML Style that will be more light-weight to consume by an app developer: - -> [!Sample AppServicesCustomSample] - -## Templated Controls - -The Toolkit is built with templated controls. This provides developers a flexible way to restyle components -easily while still inheriting the general functionality a control provides. The examples below show -how a component can use a default style and then get overridden by the end developer. - -TODO: Two types of templated control building methods are shown. Delete these if you're building a custom component. -Otherwise, pick one method for your component and delete the files related to the unchosen `_ClassicBinding` or `_xBind` -classes (and the custom non-suffixed one as well). Then, rename your component to just be your component name. - -The `_ClassicBinding` class shows the traditional method used to develop components with best practices. - -### Implict style - -> [!SAMPLE AppServicesTemplatedSample] - -### Custom style - -> [!SAMPLE AppServicesTemplatedStyleCustomSample] - -## Templated Controls with x:Bind - -This is an _experimental_ new way to define components which allows for the use of x:Bind within the style. - -### Implict style - -> [!SAMPLE AppServicesXbindBackedSample] - -### Custom style - -> [!SAMPLE AppServicesXbindBackedStyleCustomSample] - +AppServices is a library to easily communicate between UWP apps and Win32 extensions From 158454fdeecb369030a894c3068c200b21bd1d75 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 31 May 2023 19:35:14 +0200 Subject: [PATCH 11/20] Remove DotNet.ReproducibleBuilds package --- .../CommunityToolkit.AppServices.SourceGenerators.csproj | 1 - components/AppServices/src/CommunityToolkit.AppServices.csproj | 1 - 2 files changed, 2 deletions(-) diff --git a/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/CommunityToolkit.AppServices.SourceGenerators.csproj b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/CommunityToolkit.AppServices.SourceGenerators.csproj index f5e94b304..64d80c88a 100644 --- a/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/CommunityToolkit.AppServices.SourceGenerators.csproj +++ b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/CommunityToolkit.AppServices.SourceGenerators.csproj @@ -14,7 +14,6 @@ - diff --git a/components/AppServices/src/CommunityToolkit.AppServices.csproj b/components/AppServices/src/CommunityToolkit.AppServices.csproj index 8a9564058..5f1174109 100644 --- a/components/AppServices/src/CommunityToolkit.AppServices.csproj +++ b/components/AppServices/src/CommunityToolkit.AppServices.csproj @@ -12,7 +12,6 @@ - From 3ef160f404e067c9e920d977aa5bc2b3e14ac6e0 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 31 May 2023 19:59:53 +0200 Subject: [PATCH 12/20] Check for cancellation more often in generators Same as https://github.com/CommunityToolkit/dotnet/pull/704 --- .../AppServiceGenerator.cs | 20 ++++++++++++++++--- .../Models/MethodInfo.cs | 6 +++++- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/AppServiceGenerator.cs b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/AppServiceGenerator.cs index 7c24796ae..8380098a6 100644 --- a/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/AppServiceGenerator.cs +++ b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/AppServiceGenerator.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------ +// ------------------------------------------------------ // Copyright (C) Microsoft. All rights reserved. // ------------------------------------------------------ @@ -43,6 +43,8 @@ public void Initialize(IncrementalGeneratorInitializationContext context) return default; } + token.ThrowIfCancellationRequested(); + // Try to get the info on the current component (INamedTypeSymbol? serviceSymbol, string? appServiceName) = Component.GetInfo(typeSymbol, token); @@ -53,7 +55,12 @@ public void Initialize(IncrementalGeneratorInitializationContext context) } HierarchyInfo hierarchy = HierarchyInfo.From(typeSymbol); - ImmutableArray methods = MethodInfo.From(serviceSymbol); + + token.ThrowIfCancellationRequested(); + + ImmutableArray methods = MethodInfo.From(serviceSymbol, token); + + token.ThrowIfCancellationRequested(); return (Hierarchy: hierarchy, new AppServiceInfo(methods, appServiceName!, typeSymbol.GetFullyQualifiedName())); }) @@ -91,11 +98,18 @@ public void Initialize(IncrementalGeneratorInitializationContext context) return default; } + token.ThrowIfCancellationRequested(); + INamedTypeSymbol typeSymbol = (INamedTypeSymbol)context.TargetSymbol; // Get the info on the host implementation HierarchyInfo hierarchy = HierarchyInfo.From(typeSymbol, typeSymbol.Name.Substring(1)); - ImmutableArray methods = MethodInfo.From(typeSymbol); + + token.ThrowIfCancellationRequested(); + + ImmutableArray methods = MethodInfo.From(typeSymbol, token); + + token.ThrowIfCancellationRequested(); return (Hierarchy: hierarchy, new AppServiceInfo(methods, appServiceName, typeSymbol.GetFullyQualifiedName())); }) diff --git a/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Models/MethodInfo.cs b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Models/MethodInfo.cs index a2c927eb2..dedccfac7 100644 --- a/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Models/MethodInfo.cs +++ b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Models/MethodInfo.cs @@ -4,6 +4,7 @@ using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; +using System.Threading; using Microsoft.CodeAnalysis; using CommunityToolkit.AppServices.SourceGenerators.Extensions; using CommunityToolkit.AppServices.SourceGenerators.Helpers; @@ -29,13 +30,16 @@ internal sealed record MethodInfo( /// Creates instances from methods in a given . /// /// The input instance to gather info for. + /// The cancellation token for the operation. /// A collection of instances from . - public static ImmutableArray From(INamedTypeSymbol typeSymbol) + public static ImmutableArray From(INamedTypeSymbol typeSymbol, CancellationToken token) { using ImmutableArrayBuilder builder = ImmutableArrayBuilder.Rent(); foreach (ISymbol symbol in typeSymbol.GetMembers()) { + token.ThrowIfCancellationRequested(); + if (symbol.IsIgnoredAppServicesMember()) { continue; From 3892d65e53266807c57bdeb2edf0fd89a7029d0e Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 31 May 2023 20:03:10 +0200 Subject: [PATCH 13/20] Improve ImmutableArrayBuilder type Same as https://github.com/CommunityToolkit/dotnet/pull/701 --- .../Helpers/ImmutableArrayBuilder{T}.cs | 101 +++++------ .../Helpers/ObjectPool{T}.cs | 163 ------------------ 2 files changed, 44 insertions(+), 220 deletions(-) delete mode 100644 components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Helpers/ObjectPool{T}.cs diff --git a/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Helpers/ImmutableArrayBuilder{T}.cs b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Helpers/ImmutableArrayBuilder{T}.cs index 6aa377d39..d46e2088f 100644 --- a/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Helpers/ImmutableArrayBuilder{T}.cs +++ b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Helpers/ImmutableArrayBuilder{T}.cs @@ -6,7 +6,9 @@ // more info in ThirdPartyNotices.txt in the root of the project. using System; +using System.Buffers; using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; namespace CommunityToolkit.AppServices.SourceGenerators.Helpers; @@ -15,13 +17,8 @@ namespace CommunityToolkit.AppServices.SourceGenerators.Helpers; /// A helper type to build sequences of values with pooled buffers. /// /// The type of items to create sequences for. -internal struct ImmutableArrayBuilder : IDisposable +internal ref struct ImmutableArrayBuilder { - /// - /// The shared instance to share objects. - /// - private static readonly ObjectPool SharedObjectPool = new(static () => new Writer()); - /// /// The rented instance to use. /// @@ -33,7 +30,7 @@ internal struct ImmutableArrayBuilder : IDisposable /// A instance to write data to. public static ImmutableArrayBuilder Rent() { - return new(SharedObjectPool.Allocate()); + return new(new Writer()); } /// @@ -45,9 +42,17 @@ private ImmutableArrayBuilder(Writer writer) this.writer = writer; } + /// + public readonly int Count + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.writer!.Count; + } + /// /// Gets the data written to the underlying buffer so far, as a . /// + [UnscopedRef] public readonly ReadOnlySpan WrittenSpan { [MethodImpl(MethodImplOptions.AggressiveInlining)] @@ -64,17 +69,17 @@ public readonly void Add(T 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) + public readonly void AddRange(scoped ReadOnlySpan items) { this.writer!.AddRange(items); } /// - public readonly unsafe ImmutableArray ToImmutable() + public readonly ImmutableArray ToImmutable() { T[] array = this.writer!.WrittenSpan.ToArray(); - return *(ImmutableArray*)&array; + return Unsafe.As>(ref array); } /// @@ -89,30 +94,25 @@ public override readonly string ToString() return this.writer!.WrittenSpan.ToString(); } - /// + /// public void Dispose() { Writer? writer = this.writer; this.writer = null; - if (writer is not null) - { - writer.Clear(); - - SharedObjectPool.Free(writer); - } + writer?.Dispose(); } /// /// A class handling the actual buffer writing. /// - private sealed class Writer + private sealed class Writer : IDisposable { /// /// The underlying array. /// - private T[] array; + private T?[]? array; /// /// The starting offset within . @@ -124,23 +124,22 @@ private sealed class Writer /// public Writer() { - if (typeof(T) == typeof(char)) - { - this.array = new T[1024]; - } - else - { - this.array = new T[8]; - } - + this.array = ArrayPool.Shared.Rent(typeof(T) == typeof(char) ? 1024 : 8); this.index = 0; } + /// + public int Count + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get => this.index; + } + /// public ReadOnlySpan WrittenSpan { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get => new(this.array, 0, this.index); + get => new(this.array!, 0, this.index); } /// @@ -148,7 +147,7 @@ public void Add(T value) { EnsureCapacity(1); - this.array[this.index++] = value; + this.array![this.index++] = value; } /// @@ -156,22 +155,22 @@ public void AddRange(ReadOnlySpan items) { EnsureCapacity(items.Length); - items.CopyTo(this.array.AsSpan(this.index)); + items.CopyTo(this.array.AsSpan(this.index)!); this.index += items.Length; } - /// - /// Clears the items in the current writer. - /// - public void Clear() + /// + public void Dispose() { - if (typeof(T) != typeof(char)) + T?[]? array = this.array; + + this.array = null; + + if (array is not null) { - this.array.AsSpan(0, this.index).Clear(); + ArrayPool.Shared.Return(array, clearArray: typeof(T) != typeof(char)); } - - this.index = 0; } /// @@ -181,7 +180,7 @@ public void Clear() [MethodImpl(MethodImplOptions.AggressiveInlining)] private void EnsureCapacity(int requestedSize) { - if (requestedSize > this.array.Length - this.index) + if (requestedSize > this.array!.Length - this.index) { ResizeBuffer(requestedSize); } @@ -195,27 +194,15 @@ private void EnsureCapacity(int requestedSize) private void ResizeBuffer(int sizeHint) { int minimumSize = this.index + sizeHint; - int requestedSize = Math.Max(this.array.Length * 2, minimumSize); - T[] newArray = new T[requestedSize]; + T?[] oldArray = this.array!; + T?[] newArray = ArrayPool.Shared.Rent(minimumSize); - Array.Copy(this.array, newArray, this.index); + Array.Copy(oldArray, newArray, this.index); this.array = newArray; + + ArrayPool.Shared.Return(oldArray, clearArray: typeof(T) != typeof(char)); } } } - -/// -/// Private helpers for the type. -/// -internal static class ImmutableArrayBuilder -{ - /// - /// Throws an for "index". - /// - public static void ThrowArgumentOutOfRangeExceptionForIndex() - { - throw new ArgumentOutOfRangeException("index"); - } -} \ No newline at end of file diff --git a/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Helpers/ObjectPool{T}.cs b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Helpers/ObjectPool{T}.cs deleted file mode 100644 index 8156ea6c0..000000000 --- a/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Helpers/ObjectPool{T}.cs +++ /dev/null @@ -1,163 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -// 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; - -namespace CommunityToolkit.AppServices.SourceGenerators.Helpers; - -/// -/// -/// 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. -internal sealed class ObjectPool - where T : class -{ - /// - /// 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()". - /// - private readonly Func factory; - - /// - /// The array of cached items. - /// - private readonly Element[] items; - - /// - /// 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) - { - } - - /// - /// Creates a new instance with the specified parameters. - /// - /// The input factory to produce items. - /// The pool size to use. - public ObjectPool(Func factory, int size) - { - this.factory = factory; - this.items = new Element[size - 1]; - } - - /// - /// Produces a instance. - /// - /// The returned item to use. - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public T Allocate() - { - T? item = this.firstItem; - - if (item is null || item != Interlocked.CompareExchange(ref this.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 (this.firstItem is null) - { - this.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 this.items.AsSpan()) - { - T? instance = element.Value; - - if (instance is not null) - { - if (instance == Interlocked.CompareExchange(ref element.Value, null, instance)) - { - return instance; - } - } - } - - return this.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 this.items.AsSpan()) - { - if (element.Value is null) - { - element.Value = obj; - - break; - } - } - } - - /// - /// A container for a produced item (using a wrapper to avoid covariance checks). - /// - private struct Element - { - /// - /// The value held at the current element. - /// - internal T? Value; - } -} \ No newline at end of file From 061d9c9e4737273909a774489ed8ba37789ff3da Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 31 May 2023 21:19:14 +0200 Subject: [PATCH 14/20] Suppress sample warning and fix file header --- .../AppServiceGenerator.cs | 6 +++--- components/AppServices/samples/AppServices.Samples.csproj | 7 +++++++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/AppServiceGenerator.cs b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/AppServiceGenerator.cs index 8380098a6..466f313bc 100644 --- a/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/AppServiceGenerator.cs +++ b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/AppServiceGenerator.cs @@ -1,6 +1,6 @@ -// ------------------------------------------------------ -// Copyright (C) Microsoft. All rights reserved. -// ------------------------------------------------------ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. using System.Collections.Immutable; using System.Linq; diff --git a/components/AppServices/samples/AppServices.Samples.csproj b/components/AppServices/samples/AppServices.Samples.csproj index 9d6b70dbf..be47c6001 100644 --- a/components/AppServices/samples/AppServices.Samples.csproj +++ b/components/AppServices/samples/AppServices.Samples.csproj @@ -1,6 +1,13 @@ AppServices + + + $(NoWarn);TKSMPL0014 From 99e86d6d47a163b8108c56a10d1cbbc83dbcbfc3 Mon Sep 17 00:00:00 2001 From: Arlo Godfrey Date: Wed, 31 May 2023 14:57:52 -0500 Subject: [PATCH 15/20] Removed unused sample markdown --- .../samples/AppServices.Samples.csproj | 7 ------- components/AppServices/samples/AppServices.md | 16 ---------------- 2 files changed, 23 deletions(-) delete mode 100644 components/AppServices/samples/AppServices.md diff --git a/components/AppServices/samples/AppServices.Samples.csproj b/components/AppServices/samples/AppServices.Samples.csproj index be47c6001..9d6b70dbf 100644 --- a/components/AppServices/samples/AppServices.Samples.csproj +++ b/components/AppServices/samples/AppServices.Samples.csproj @@ -1,13 +1,6 @@ AppServices - - - $(NoWarn);TKSMPL0014 diff --git a/components/AppServices/samples/AppServices.md b/components/AppServices/samples/AppServices.md deleted file mode 100644 index b1370cedb..000000000 --- a/components/AppServices/samples/AppServices.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -title: AppServices -author: Sergio0694 -description: AppServices is a library to easily communicate between UWP apps and Win32 extensions -keywords: AppServices, uwp, xaml, win32, fulltrust, extension, rpc -dev_langs: - - csharp -category: Extensions -subcategory: None -discussion-id: 301 -issue-id: 0 ---- - -# AppServices - -AppServices is a library to easily communicate between UWP apps and Win32 extensions From 0375d1e9e20fc87e832c69ef1aac2674c5722453 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 31 May 2023 22:03:36 +0200 Subject: [PATCH 16/20] Resolve symbol in compilation action in analyzer --- .../InvalidAppServicesMemberAnalyzer.cs | 117 ++++++++++-------- 1 file changed, 62 insertions(+), 55 deletions(-) diff --git a/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Diagnostics/Analyzers/InvalidAppServicesMemberAnalyzer.cs b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Diagnostics/Analyzers/InvalidAppServicesMemberAnalyzer.cs index 6aea09d12..f3ca6e332 100644 --- a/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Diagnostics/Analyzers/InvalidAppServicesMemberAnalyzer.cs +++ b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Diagnostics/Analyzers/InvalidAppServicesMemberAnalyzer.cs @@ -32,89 +32,96 @@ public override void Initialize(AnalysisContext context) context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); context.EnableConcurrentExecution(); - // Register a callback for all named type symbols (ie. user defined types) - context.RegisterSymbolAction(static context => + context.RegisterCompilationStartAction(static context => { - // The symbol must be an interface - if (context.Symbol is not INamedTypeSymbol { TypeKind: TypeKind.Interface } interfaceSymbol) + // Get the symbol for [AppService] + if (context.Compilation.GetTypeByMetadataName("CommunityToolkit.AppServices.AppServiceAttribute") is not INamedTypeSymbol appServicesAttributeSymbol) { return; } - INamedTypeSymbol appServicesAttributeSymbol = context.Compilation.GetTypeByMetadataName("CommunityToolkit.AppServices.AppServiceAttribute")!; - - // Check whether the interface is an app services interface - if (!interfaceSymbol.HasOrInheritsAttribute(appServicesAttributeSymbol)) - { - return; - } - - // Go through all interface members to analyze them. Here we need to go through all members, not just the ones immediately - // declared, as it's possible an interface with an invalid member will be inherited by another one that adds [AppServices]. - // In that case, the base interface will not be analyzed (as it doesn't have [AppServices]), so the derived one will need - // to also go through inherited members to ensure that all members that the generator will process will actually be valid. - foreach (ISymbol memberSymbol in interfaceSymbol.GetAllMembers()) + // Register a callback for all named type symbols (ie. user defined types) + context.RegisterSymbolAction(context => { - // If a method is not abstract nor virtual (ie. a DIM or static non-virtual interface member), it can just be ignored. - // The generated service type will not have to consider it as far as registering endpoints and generating members goes. - if (memberSymbol.IsIgnoredAppServicesMember()) + // The symbol must be an interface + if (context.Symbol is not INamedTypeSymbol { TypeKind: TypeKind.Interface } interfaceSymbol) { - continue; + return; } - // All remaining members must be non-generic instance methods, which the generator will emit - if (memberSymbol is not IMethodSymbol { IsStatic: false, IsGenericMethod: false, ReturnType: INamedTypeSymbol returnTypeSymbol } methodSymbol) + // Check whether the interface is an app services interface + if (!interfaceSymbol.HasOrInheritsAttribute(appServicesAttributeSymbol)) { - context.ReportDiagnostic(Diagnostic.Create(InvalidAppServicesMemberType, memberSymbol.Locations.FirstOrDefault(), memberSymbol, interfaceSymbol)); - - continue; + return; } - // Validate the return type for the current method - if (!methodSymbol.TryGetParameterOrReturnType(out ParameterOrReturnType returnType) || - !returnType.IsValidReturnType()) + // Go through all interface members to analyze them. Here we need to go through all members, not just the ones immediately + // declared, as it's possible an interface with an invalid member will be inherited by another one that adds [AppServices]. + // In that case, the base interface will not be analyzed (as it doesn't have [AppServices]), so the derived one will need + // to also go through inherited members to ensure that all members that the generator will process will actually be valid. + foreach (ISymbol memberSymbol in interfaceSymbol.GetAllMembers()) { - context.ReportDiagnostic(Diagnostic.Create(InvalidAppServicesMethodReturnType, memberSymbol.Locations.FirstOrDefault(), methodSymbol, interfaceSymbol, returnTypeSymbol)); - } - - int progressParametersCount = 0; - int cancellationTokenParametersCount = 0; + // If a method is not abstract nor virtual (ie. a DIM or static non-virtual interface member), it can just be ignored. + // The generated service type will not have to consider it as far as registering endpoints and generating members goes. + if (memberSymbol.IsIgnoredAppServicesMember()) + { + continue; + } - // Validate the method parameters - foreach (IParameterSymbol parameter in methodSymbol.Parameters) - { - // First validate types that could possibly be allowed at all (ie. valid types) - if (!parameter.TryGetParameterOrReturnType(out ParameterOrReturnType parameterType) || - !parameterType.IsValidParameterType()) + // All remaining members must be non-generic instance methods, which the generator will emit + if (memberSymbol is not IMethodSymbol { IsStatic: false, IsGenericMethod: false, ReturnType: INamedTypeSymbol returnTypeSymbol } methodSymbol) { - context.ReportDiagnostic(Diagnostic.Create(InvalidAppServicesMethodParameterType, parameter.Locations.FirstOrDefault(), parameter.Name, methodSymbol, interfaceSymbol, parameter.Type)); + context.ReportDiagnostic(Diagnostic.Create(InvalidAppServicesMemberType, memberSymbol.Locations.FirstOrDefault(), memberSymbol, interfaceSymbol)); continue; } - // Then check that the type is not an IProgress, if one has already been discovered - if (parameterType.HasFlag(ParameterOrReturnType.IProgressOfT)) + // Validate the return type for the current method + if (!methodSymbol.TryGetParameterOrReturnType(out ParameterOrReturnType returnType) || + !returnType.IsValidReturnType()) { - progressParametersCount++; + context.ReportDiagnostic(Diagnostic.Create(InvalidAppServicesMethodReturnType, memberSymbol.Locations.FirstOrDefault(), methodSymbol, interfaceSymbol, returnTypeSymbol)); + } + + int progressParametersCount = 0; + int cancellationTokenParametersCount = 0; - if (progressParametersCount > 1) + // Validate the method parameters + foreach (IParameterSymbol parameter in methodSymbol.Parameters) + { + // First validate types that could possibly be allowed at all (ie. valid types) + if (!parameter.TryGetParameterOrReturnType(out ParameterOrReturnType parameterType) || + !parameterType.IsValidParameterType()) { - context.ReportDiagnostic(Diagnostic.Create(InvalidRepeatedAppServicesMethodIProgressParameter, parameter.Locations.FirstOrDefault(), parameter.Name, methodSymbol, interfaceSymbol, parameter.Type)); + context.ReportDiagnostic(Diagnostic.Create(InvalidAppServicesMethodParameterType, parameter.Locations.FirstOrDefault(), parameter.Name, methodSymbol, interfaceSymbol, parameter.Type)); + + continue; } - } - // Lastly, check that the type is not a CancellationToken, if one has already been discovered - if (parameterType.HasFlag(ParameterOrReturnType.CancellationToken)) - { - cancellationTokenParametersCount++; + // Then check that the type is not an IProgress, if one has already been discovered + if (parameterType.HasFlag(ParameterOrReturnType.IProgressOfT)) + { + progressParametersCount++; + + if (progressParametersCount > 1) + { + context.ReportDiagnostic(Diagnostic.Create(InvalidRepeatedAppServicesMethodIProgressParameter, parameter.Locations.FirstOrDefault(), parameter.Name, methodSymbol, interfaceSymbol, parameter.Type)); + } + } - if (cancellationTokenParametersCount > 1) + // Lastly, check that the type is not a CancellationToken, if one has already been discovered + if (parameterType.HasFlag(ParameterOrReturnType.CancellationToken)) { - context.ReportDiagnostic(Diagnostic.Create(InvalidRepeatedAppServicesMethodCancellationTokenParameter, parameter.Locations.FirstOrDefault(), parameter.Name, methodSymbol, interfaceSymbol, parameter.Type)); + cancellationTokenParametersCount++; + + if (cancellationTokenParametersCount > 1) + { + context.ReportDiagnostic(Diagnostic.Create(InvalidRepeatedAppServicesMethodCancellationTokenParameter, parameter.Locations.FirstOrDefault(), parameter.Name, methodSymbol, interfaceSymbol, parameter.Type)); + } } } } - } - }, SymbolKind.NamedType); + }, SymbolKind.NamedType); + }); } } From ed4fc13495b93ed981e3c30b1cf8f39d42546a77 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Wed, 31 May 2023 22:09:27 +0200 Subject: [PATCH 17/20] Update progress/token parameters to bool --- .../InvalidAppServicesMemberAnalyzer.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Diagnostics/Analyzers/InvalidAppServicesMemberAnalyzer.cs b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Diagnostics/Analyzers/InvalidAppServicesMemberAnalyzer.cs index f3ca6e332..5168f00b4 100644 --- a/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Diagnostics/Analyzers/InvalidAppServicesMemberAnalyzer.cs +++ b/components/AppServices/CommunityToolkit.AppServices.SourceGenerators/Diagnostics/Analyzers/InvalidAppServicesMemberAnalyzer.cs @@ -83,8 +83,8 @@ public override void Initialize(AnalysisContext context) context.ReportDiagnostic(Diagnostic.Create(InvalidAppServicesMethodReturnType, memberSymbol.Locations.FirstOrDefault(), methodSymbol, interfaceSymbol, returnTypeSymbol)); } - int progressParametersCount = 0; - int cancellationTokenParametersCount = 0; + bool isProgressParameterFound = false; + bool isCancellationTokenParameterFound = false; // Validate the method parameters foreach (IParameterSymbol parameter in methodSymbol.Parameters) @@ -101,23 +101,23 @@ public override void Initialize(AnalysisContext context) // Then check that the type is not an IProgress, if one has already been discovered if (parameterType.HasFlag(ParameterOrReturnType.IProgressOfT)) { - progressParametersCount++; - - if (progressParametersCount > 1) + if (isProgressParameterFound) { context.ReportDiagnostic(Diagnostic.Create(InvalidRepeatedAppServicesMethodIProgressParameter, parameter.Locations.FirstOrDefault(), parameter.Name, methodSymbol, interfaceSymbol, parameter.Type)); } + + isProgressParameterFound = true; } // Lastly, check that the type is not a CancellationToken, if one has already been discovered if (parameterType.HasFlag(ParameterOrReturnType.CancellationToken)) { - cancellationTokenParametersCount++; - - if (cancellationTokenParametersCount > 1) + if (isCancellationTokenParameterFound) { context.ReportDiagnostic(Diagnostic.Create(InvalidRepeatedAppServicesMethodCancellationTokenParameter, parameter.Locations.FirstOrDefault(), parameter.Name, methodSymbol, interfaceSymbol, parameter.Type)); } + + isCancellationTokenParameterFound = true; } } } From 2a4b9f88e3d32cadab6ea62721e164c15af162a5 Mon Sep 17 00:00:00 2001 From: Arlo Godfrey Date: Wed, 31 May 2023 23:49:15 -0500 Subject: [PATCH 18/20] Update tooling to latest main --- tooling | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tooling b/tooling index 60f4ca99f..35c03dff2 160000 --- a/tooling +++ b/tooling @@ -1 +1 @@ -Subproject commit 60f4ca99f5623b4a66bcdb4884c2d6432a8d8aea +Subproject commit 35c03dff2df6a1423860710e6633faf65cf3e788 From fa4f01eca77ba0585871af40ae1b8ba6190e56ee Mon Sep 17 00:00:00 2001 From: Arlo Godfrey Date: Wed, 31 May 2023 23:49:40 -0500 Subject: [PATCH 19/20] Disabled WinUI for AppServices --- components/AppServices/src/CommunityToolkit.AppServices.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/components/AppServices/src/CommunityToolkit.AppServices.csproj b/components/AppServices/src/CommunityToolkit.AppServices.csproj index 5f1174109..96f35200e 100644 --- a/components/AppServices/src/CommunityToolkit.AppServices.csproj +++ b/components/AppServices/src/CommunityToolkit.AppServices.csproj @@ -6,6 +6,7 @@ 0.0.1 CommunityToolkit.AppServices $(PackageIdPrefix).$(ToolkitComponentName) + false From 00aecde02151c50aef81b34841bfdd0e005a1429 Mon Sep 17 00:00:00 2001 From: Sergio Pedri Date: Thu, 1 Jun 2023 15:10:40 +0200 Subject: [PATCH 20/20] Remove unnecessary global using directive --- components/AppServices/src/CommunityToolkit.AppServices.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/components/AppServices/src/CommunityToolkit.AppServices.csproj b/components/AppServices/src/CommunityToolkit.AppServices.csproj index 96f35200e..697bfc876 100644 --- a/components/AppServices/src/CommunityToolkit.AppServices.csproj +++ b/components/AppServices/src/CommunityToolkit.AppServices.csproj @@ -61,6 +61,5 @@ -