-
Notifications
You must be signed in to change notification settings - Fork 86
[Experiment] AppServices library #438
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
e3027d1
Create blank AppServices experiment
Sergio0694 270a042
Rename AppServices project file
Sergio0694 e9ddcf0
Set correct package id
Sergio0694 2113bc7
Remove imported global usings
Sergio0694 fd1d3ab
Add source generator project
Sergio0694 d0f6b20
Add .targets and packaging step for generator
Sergio0694 9c81f65
Port source generator code
Sergio0694 da8c657
Update AppServices .csproj
Sergio0694 b24bb46
Port main AppServices code
Sergio0694 77fd1f7
Update docs
Sergio0694 158454f
Remove DotNet.ReproducibleBuilds package
Sergio0694 3ef160f
Check for cancellation more often in generators
Sergio0694 3892d65
Improve ImmutableArrayBuilder<T> type
Sergio0694 061d9c9
Suppress sample warning and fix file header
Sergio0694 99e86d6
Removed unused sample markdown
Arlodotexe 0375d1e
Resolve symbol in compilation action in analyzer
Sergio0694 ed4fc13
Update progress/token parameters to bool
Sergio0694 2a4b9f8
Update tooling to latest main
Arlodotexe fa4f01e
Disabled WinUI for AppServices
Arlodotexe 00aecde
Remove unnecessary global using directive
Sergio0694 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
16 changes: 16 additions & 0 deletions
16
...vices/CommunityToolkit.AppServices.SourceGenerators/AnalyzerReleases.Shipped.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | |
2 changes: 2 additions & 0 deletions
2
...ces/CommunityToolkit.AppServices.SourceGenerators/AnalyzerReleases.Unshipped.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| ; Unshipped analyzer release | ||
| ; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md |
276 changes: 276 additions & 0 deletions
276
...ppServices/CommunityToolkit.AppServices.SourceGenerators/AppServiceGenerator.Component.cs
Large diffs are not rendered by default.
Oops, something went wrong.
27 changes: 27 additions & 0 deletions
27
.../AppServices/CommunityToolkit.AppServices.SourceGenerators/AppServiceGenerator.Helpers.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
|
|
||
| /// <inheritdoc/> | ||
| partial class AppServiceGenerator : IIncrementalGenerator | ||
| { | ||
| /// <summary> | ||
| /// Shader generators logic for app service hosts and components. | ||
| /// </summary> | ||
| private static class Helpers | ||
| { | ||
| /// <summary> | ||
| /// Gets whether the current target is a UWP application. | ||
| /// </summary> | ||
| /// <param name="compilation">The input <see cref="Compilation"/> instance to inspect.</param> | ||
| /// <returns>Whether the current target is a UWP application.</returns> | ||
| public static bool IsUwpTarget(Compilation compilation) | ||
| { | ||
| return compilation.Options.OutputKind == OutputKind.WindowsRuntimeApplication; | ||
| } | ||
| } | ||
| } |
291 changes: 291 additions & 0 deletions
291
...nts/AppServices/CommunityToolkit.AppServices.SourceGenerators/AppServiceGenerator.Host.cs
Large diffs are not rendered by default.
Oops, something went wrong.
133 changes: 133 additions & 0 deletions
133
components/AppServices/CommunityToolkit.AppServices.SourceGenerators/AppServiceGenerator.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,133 @@ | ||
| // 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 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; | ||
|
|
||
| /// <summary> | ||
| /// A source generator for the <c>AppServiceAttribute</c> type. | ||
| /// </summary> | ||
| [Generator(LanguageNames.CSharp)] | ||
| public sealed partial class AppServiceGenerator : IIncrementalGenerator | ||
| { | ||
| /// <inheritdoc/> | ||
| 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; | ||
| } | ||
|
|
||
| token.ThrowIfCancellationRequested(); | ||
|
|
||
| // 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); | ||
|
|
||
| token.ThrowIfCancellationRequested(); | ||
|
|
||
| ImmutableArray<MethodInfo> methods = MethodInfo.From(serviceSymbol, token); | ||
|
|
||
| token.ThrowIfCancellationRequested(); | ||
|
|
||
| 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<MemberDeclarationSyntax>(constructorSyntax), | ||
| ImmutableArray.Create<BaseTypeSyntax>(SimpleBaseType(IdentifierName("global::CommunityToolkit.AppServices.AppServiceComponent"))), | ||
| "/// <inheritdoc/>"); | ||
|
|
||
| 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; | ||
| } | ||
|
|
||
| token.ThrowIfCancellationRequested(); | ||
|
|
||
| INamedTypeSymbol typeSymbol = (INamedTypeSymbol)context.TargetSymbol; | ||
|
|
||
| // Get the info on the host implementation | ||
| HierarchyInfo hierarchy = HierarchyInfo.From(typeSymbol, typeSymbol.Name.Substring(1)); | ||
|
|
||
| token.ThrowIfCancellationRequested(); | ||
|
|
||
| ImmutableArray<MethodInfo> methods = MethodInfo.From(typeSymbol, token); | ||
|
|
||
| token.ThrowIfCancellationRequested(); | ||
|
|
||
| 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<MethodDeclarationSyntax> methodDeclarations = Host.GetMethodDeclarationsSyntax(item.Info); | ||
| CompilationUnitSyntax compilationUnit = item.Hierarchy.GetCompilationUnit( | ||
| ImmutableArray.Create<MemberDeclarationSyntax>(constructorSyntax).AddRange(methodDeclarations), | ||
| ImmutableArray.Create<BaseTypeSyntax>( | ||
| SimpleBaseType(IdentifierName("global::CommunityToolkit.AppServices.AppServiceHost")), | ||
| SimpleBaseType(IdentifierName(item.Info.InterfaceFullyQualifiedName))), | ||
| $"/// <summary>A generated host implementation for the <see cref=\"{item.Info.InterfaceFullyQualifiedName}\"/> interface.</summary>"); | ||
|
|
||
| context.AddSource($"{item.Hierarchy.FilenameHint}.g.cs", compilationUnit.GetText(Encoding.UTF8)); | ||
| }); | ||
| } | ||
| } | ||
30 changes: 30 additions & 0 deletions
30
...Toolkit.AppServices.SourceGenerators/CommunityToolkit.AppServices.SourceGenerators.csproj
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| <Project Sdk="Microsoft.NET.Sdk"> | ||
| <PropertyGroup> | ||
| <TargetFramework>netstandard2.0</TargetFramework> | ||
| <IsPackable>false</IsPackable> | ||
| <AllowUnsafeBlocks>true</AllowUnsafeBlocks> | ||
|
|
||
| <!-- | ||
| Suppress ref safety warnings in unsafe contexts (see https://github.com/dotnet/csharplang/issues/6476). | ||
| This is used eg. to replace Unsafe.SizeOf<T>() calls with just sizeof(T), or to just use raw pointers to | ||
| reinterpret references to managed objects when it is safe to do so. The warnings are not necessary in this | ||
| context, since in order to use these APIs the caller already has to be in an unsafe context. | ||
| --> | ||
| <NoWarn>$(NoWarn);CS8500</NoWarn> | ||
| </PropertyGroup> | ||
|
|
||
| <ItemGroup> | ||
| <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.3.1" PrivateAssets="all" Pack="false" /> | ||
| <PackageReference Include="PolySharp" Version="1.13.2" PrivateAssets="All" /> | ||
| </ItemGroup> | ||
|
|
||
| <ItemGroup> | ||
| <AdditionalFiles Include="AnalyzerReleases.Shipped.md" /> | ||
| <AdditionalFiles Include="AnalyzerReleases.Unshipped.md" /> | ||
| </ItemGroup> | ||
|
|
||
| <!-- Remove imported global usings --> | ||
| <ItemGroup> | ||
| <Compile Remove="$(ToolingDirectory)\GlobalUsings.cs" /> | ||
| </ItemGroup> | ||
| </Project> |
127 changes: 127 additions & 0 deletions
127
...it.AppServices.SourceGenerators/Diagnostics/Analyzers/InvalidAppServicesMemberAnalyzer.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,127 @@ | ||
| // 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; | ||
|
|
||
| /// <summary> | ||
| /// A diagnostic analyzer that emits diagnostics whenever an app service has an invalid member. | ||
| /// </summary> | ||
| [DiagnosticAnalyzer(LanguageNames.CSharp)] | ||
| public sealed class InvalidAppServicesMemberAnalyzer : DiagnosticAnalyzer | ||
| { | ||
| /// <inheritdoc/> | ||
| public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create( | ||
| InvalidAppServicesMemberType, | ||
| InvalidAppServicesMethodReturnType, | ||
| InvalidAppServicesMethodParameterType, | ||
| InvalidRepeatedAppServicesMethodIProgressParameter, | ||
| InvalidRepeatedAppServicesMethodCancellationTokenParameter); | ||
|
|
||
| /// <inheritdoc/> | ||
| public override void Initialize(AnalysisContext context) | ||
| { | ||
| context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics); | ||
| context.EnableConcurrentExecution(); | ||
|
|
||
| context.RegisterCompilationStartAction(static context => | ||
| { | ||
| // Get the symbol for [AppService] | ||
| if (context.Compilation.GetTypeByMetadataName("CommunityToolkit.AppServices.AppServiceAttribute") is not INamedTypeSymbol appServicesAttributeSymbol) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| // Register a callback for all named type symbols (ie. user defined types) | ||
| context.RegisterSymbolAction(context => | ||
| { | ||
| // The symbol must be an interface | ||
| if (context.Symbol is not INamedTypeSymbol { TypeKind: TypeKind.Interface } interfaceSymbol) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| // 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)); | ||
| } | ||
|
|
||
| bool isProgressParameterFound = false; | ||
| bool isCancellationTokenParameterFound = 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()) | ||
| { | ||
| context.ReportDiagnostic(Diagnostic.Create(InvalidAppServicesMethodParameterType, parameter.Locations.FirstOrDefault(), parameter.Name, methodSymbol, interfaceSymbol, parameter.Type)); | ||
|
|
||
| continue; | ||
| } | ||
|
|
||
| // Then check that the type is not an IProgress<T>, if one has already been discovered | ||
| if (parameterType.HasFlag(ParameterOrReturnType.IProgressOfT)) | ||
| { | ||
| 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)) | ||
| { | ||
| if (isCancellationTokenParameterFound) | ||
| { | ||
| context.ReportDiagnostic(Diagnostic.Create(InvalidRepeatedAppServicesMethodCancellationTokenParameter, parameter.Locations.FirstOrDefault(), parameter.Name, methodSymbol, interfaceSymbol, parameter.Type)); | ||
| } | ||
|
|
||
| isCancellationTokenParameterFound = true; | ||
| } | ||
| } | ||
| } | ||
| }, SymbolKind.NamedType); | ||
| }); | ||
| } | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.