From 03038fa519670b583eabcef1417eacd55c3e44c8 Mon Sep 17 00:00:00 2001 From: Manuel Menegazzo <65919883+m3nax@users.noreply.github.com> Date: Mon, 28 Oct 2024 15:20:31 +0100 Subject: [PATCH] Incremental source generator for actors (#1334) * Samples - Add k8s deployment yaml to DemoActor sample (#1308) * up Signed-off-by: Manuel Menegazzo * Fixed build Signed-off-by: Manuel Menegazzo * Added scripts for image build Signed-off-by: Manuel Menegazzo * Added readme Build and push Docker image Signed-off-by: Manuel Menegazzo * Added demo-actor.yaml Signed-off-by: Manuel Menegazzo * Fixed typo Signed-off-by: Manuel Menegazzo * Updated guide, fixed invocation throw curl Signed-off-by: Manuel Menegazzo * Removed dockerfile, updated readme, removed ps1 and sh scripts Signed-off-by: Manuel Menegazzo * Updated base image Signed-off-by: Manuel Menegazzo <65919883+m3nax@users.noreply.github.com> Signed-off-by: Manuel Menegazzo * Update demo-actor.yaml Signed-off-by: Manuel Menegazzo <65919883+m3nax@users.noreply.github.com> Signed-off-by: Manuel Menegazzo * Added overload for DaprClient DI registration (#1289) * Added overload for DaprClient DI registration allowing the consumer to easily use values from injected services (e.g. IConfiguration). Signed-off-by: Whit Waldo * Added supporting unit test Signed-off-by: Whit Waldo --------- Signed-off-by: Whit Waldo Co-authored-by: Phillip Hoff Signed-off-by: Manuel Menegazzo * Merge `release-1.13` back into `master` (#1285) * Update protos and related use for Dapr 1.13. (#1236) * Update protos and related use. Signed-off-by: Phillip Hoff * Update Dapr runtime version. Signed-off-by: Phillip Hoff * Init properties. Signed-off-by: Phillip Hoff --------- Signed-off-by: Phillip Hoff * Update artifact action versions. (#1240) Signed-off-by: Phillip Hoff * Make recursive true as default (#1243) Signed-off-by: Shivam Kumar * Fix for secret key transformation in multi-value scenarios (#1274) * Add repro test. Signed-off-by: Phillip Hoff * Fix for secret key transformation in multi-value scenarios. Signed-off-by: Phillip Hoff --------- Signed-off-by: Phillip Hoff * Update Dapr version numbers used during testing. Signed-off-by: Phillip Hoff --------- Signed-off-by: Phillip Hoff Signed-off-by: Shivam Kumar Co-authored-by: Shivam Kumar Signed-off-by: Manuel Menegazzo --------- Signed-off-by: Manuel Menegazzo Signed-off-by: Manuel Menegazzo <65919883+m3nax@users.noreply.github.com> Signed-off-by: Whit Waldo Signed-off-by: Phillip Hoff Signed-off-by: Shivam Kumar Co-authored-by: Whit Waldo Co-authored-by: Phillip Hoff Co-authored-by: Shivam Kumar Signed-off-by: Manuel Menegazzo * Aligned nuget version Signed-off-by: Manuel Menegazzo * UP Signed-off-by: Manuel Menegazzo * UP Signed-off-by: Manuel Menegazzo * Debug profile added Signed-off-by: Manuel Menegazzo * Updated implementation Signed-off-by: Manuel Menegazzo * Emitted DAPR001 Diagnostic warning Signed-off-by: Manuel Menegazzo * Added DAPR002 diagnostic Signed-off-by: Manuel Menegazzo * Cleaun Signed-off-by: Manuel Menegazzo * UP Signed-off-by: Manuel Menegazzo * Added summaries Signed-off-by: Manuel Menegazzo * Added base interface to ActorClient Signed-off-by: Manuel Menegazzo * Updated Signed-off-by: Manuel Menegazzo * Added ctor Signed-off-by: Manuel Menegazzo * Added nullable directive Signed-off-by: Manuel Menegazzo * Added null check for actorproxy ctor parameter Signed-off-by: Manuel Menegazzo * Moved DiagnoticException in a dedicate cs file Signed-off-by: Manuel Menegazzo * Moved generator costants to dedicated class Signed-off-by: Manuel Menegazzo * Added ActorReference creation from the ActorBase class informations (#1277) * Handled creation of ActorReference from Actor base class Signed-off-by: Manuel Menegazzo * Updated null check Signed-off-by: Manuel Menegazzo * Added unit test for GetActorReference from null actore and actor proxy Signed-off-by: Manuel Menegazzo * Added test for ActorReference created inside Actor implementation Signed-off-by: Manuel Menegazzo * Updated description Signed-off-by: Manuel Menegazzo * Fixed test method naming Signed-off-by: Manuel Menegazzo * Added unit test for exception generated in case the type is not convertible to an ActorReference Signed-off-by: Manuel Menegazzo --------- Signed-off-by: Manuel Menegazzo * Added overload to support SDK supplying query string on invoked URL (#1310) * Refactored extensions and their tests into separate directories Signed-off-by: Whit Waldo * Added overload to method invocation to allow query string parameters to be passed in via the SDK instead of being uncermoniously added to the end of the produced HttpRequestMessage URI Signed-off-by: Whit Waldo * Added unit tests to support implementation Signed-off-by: Whit Waldo * Marking HttpExtensions as internal to prevent external usage and updating to work against Uri instead of HttpRequestMessage. Signed-off-by: Whit Waldo * Updated unit tests to match new extension purpose Signed-off-by: Whit Waldo * Resolved an ambiguous method invocation wherein it was taking the query string and passing it as the payload for a request. Removed the offending method and reworked the remaining configurations so there's no API impact. Signed-off-by: Whit Waldo --------- Signed-off-by: Whit Waldo Signed-off-by: Manuel Menegazzo * Fixed actorProxy argument null check Signed-off-by: Manuel Menegazzo * Moved ActorClientDesciptor into separta cs file Signed-off-by: Manuel Menegazzo * Moved textual templates to dedicated class Signed-off-by: Manuel Menegazzo * Updated comments, property names Signed-off-by: Manuel Menegazzo * Added argument null check to SyntaxFactoryHelpers Signed-off-by: Manuel Menegazzo * Added comments Signed-off-by: Manuel Menegazzo * Removed obsolete testing packages https://github.com/dotnet/roslyn-sdk/blob/main/src/Microsoft.CodeAnalysis.Testing/README.md#obsolete-packages Signed-off-by: Manuel Menegazzo * Adapted existing unit test to new source generated code Signed-off-by: Manuel Menegazzo * Up Signed-off-by: Manuel Menegazzo * Added tests for SyntaxFactoryHelpers Signed-off-by: Manuel Menegazzo * Updated generation of ArgumentNullException Signed-off-by: Manuel Menegazzo * Updated nullability Signed-off-by: Manuel Menegazzo * Fixed internal methods tests Signed-off-by: Manuel Menegazzo * Added test to IEnumerableExtensions Signed-off-by: Manuel Menegazzo * Unittested GetSyntaxKinds from Accessibility Signed-off-by: Manuel Menegazzo * UP Signed-off-by: Manuel Menegazzo * Updated assignment implementation of ctor body Signed-off-by: Manuel Menegazzo * Improved unit test Signed-off-by: Manuel Menegazzo * Added implementation of method generation Signed-off-by: Manuel Menegazzo * Fixed ArgumentNullException invocation Signed-off-by: Manuel Menegazzo * Added test for NameOfExpression Signed-off-by: Manuel Menegazzo * Fixed ActorProxy method invocation Signed-off-by: Manuel Menegazzo * Simplified proxy argument definition Signed-off-by: Manuel Menegazzo * Explicit generic arguments of the proxy call during generation Signed-off-by: Manuel Menegazzo * Handled cancellation token with default value Signed-off-by: Manuel Menegazzo * Fixed typo Signed-off-by: Manuel Menegazzo * Configured eol used in NormalizeWhitespace function Signed-off-by: Manuel Menegazzo * Normalized expected source Signed-off-by: Manuel Menegazzo * Moved to constat the ActorProxyTypeName Signed-off-by: Manuel Menegazzo * Fix typo Signed-off-by: Manuel Menegazzo * Created ActorProxyInvokeMethodAsync SyntaxFactoryHelper Signed-off-by: Manuel Menegazzo * Removed custom concat implementation Signed-off-by: Manuel Menegazzo * fix (#1329) Signed-off-by: Hannah Hunter Signed-off-by: Manuel Menegazzo * link to non-dapr endpoint howto (#1335) Signed-off-by: Hannah Hunter Signed-off-by: Manuel Menegazzo * Merge 1.14 release branch back into `master`. (#1337) Signed-off-by: Manuel Menegazzo * Fixed merge errors Signed-off-by: Manuel Menegazzo Signed-off-by: Manuel Menegazzo * Updated some summaries Signed-off-by: Manuel Menegazzo * Added some missing summaries Signed-off-by: Manuel Menegazzo * Fixed typo Signed-off-by: Manuel Menegazzo * Improved some summary text Signed-off-by: Manuel Menegazzo * Improved summaries Signed-off-by: Manuel Menegazzo * Handled review requests Signed-off-by: Manuel Menegazzo * Changed SyntaxFactoryHelpers accessor to internal Signed-off-by: Manuel Menegazzo --------- Signed-off-by: Manuel Menegazzo Signed-off-by: Manuel Menegazzo <65919883+m3nax@users.noreply.github.com> Signed-off-by: Whit Waldo Signed-off-by: Phillip Hoff Signed-off-by: Shivam Kumar Signed-off-by: Hannah Hunter Co-authored-by: Whit Waldo Co-authored-by: Phillip Hoff Co-authored-by: Shivam Kumar Co-authored-by: Hannah Hunter <94493363+hhunter-ms@users.noreply.github.com> --- Directory.Packages.props | 2 +- .../ActorClient/ActorClient.csproj | 41 +- .../ActorClient/IClientActor.cs | 2 +- .../ActorClientGenerator.cs | 454 +++++++++--------- .../AnalyzerReleases.Shipped.md | 8 + .../AnalyzerReleases.Unshipped.md | 3 + src/Dapr.Actors.Generators/Constants.cs | 38 ++ .../Dapr.Actors.Generators.csproj | 64 +-- ...CancellationTokensMustBeTheLastArgument.cs | 25 + ...tOptionallyFollowedByACancellationToken.cs | 25 + .../DiagnosticsException.cs | 25 + .../Extensions/IEnumerableExtensions.cs | 34 ++ .../Helpers/SyntaxFactoryHelpers.cs | 159 ++++++ .../Models/ActorClientDescriptor.cs | 46 ++ .../Properties/launchSettings.json | 8 + src/Dapr.Actors.Generators/Templates.cs | 87 ++++ .../ActorClientGeneratorTests.cs | 292 +++++------ .../CSharpSourceGeneratorVerifier.cs | 23 +- .../Dapr.Actors.Generators.Test.csproj | 5 +- .../Extensions/IEnumerableExtensionsTests.cs | 52 ++ .../Helpers/SyntaxFactoryHelpersTests.cs | 133 +++++ 21 files changed, 1090 insertions(+), 436 deletions(-) create mode 100644 src/Dapr.Actors.Generators/AnalyzerReleases.Shipped.md create mode 100644 src/Dapr.Actors.Generators/AnalyzerReleases.Unshipped.md create mode 100644 src/Dapr.Actors.Generators/Constants.cs create mode 100644 src/Dapr.Actors.Generators/Diagnostics/CancellationTokensMustBeTheLastArgument.cs create mode 100644 src/Dapr.Actors.Generators/Diagnostics/MethodMustOnlyHaveASingleArgumentOptionallyFollowedByACancellationToken.cs create mode 100644 src/Dapr.Actors.Generators/DiagnosticsException.cs create mode 100644 src/Dapr.Actors.Generators/Extensions/IEnumerableExtensions.cs create mode 100644 src/Dapr.Actors.Generators/Helpers/SyntaxFactoryHelpers.cs create mode 100644 src/Dapr.Actors.Generators/Models/ActorClientDescriptor.cs create mode 100644 src/Dapr.Actors.Generators/Properties/launchSettings.json create mode 100644 src/Dapr.Actors.Generators/Templates.cs create mode 100644 test/Dapr.Actors.Generators.Test/Extensions/IEnumerableExtensionsTests.cs create mode 100644 test/Dapr.Actors.Generators.Test/Helpers/SyntaxFactoryHelpersTests.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 3c1459b5d..332939a5b 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -21,7 +21,7 @@ - + diff --git a/examples/GeneratedActor/ActorClient/ActorClient.csproj b/examples/GeneratedActor/ActorClient/ActorClient.csproj index 73b5c2027..88f75663d 100644 --- a/examples/GeneratedActor/ActorClient/ActorClient.csproj +++ b/examples/GeneratedActor/ActorClient/ActorClient.csproj @@ -1,22 +1,29 @@ - + - - Exe - net6 - 10.0 - enable - enable - + + Exe + net6 + 10.0 + enable + enable - - - - + + true + + + + + - - - + + + + + + + + diff --git a/examples/GeneratedActor/ActorClient/IClientActor.cs b/examples/GeneratedActor/ActorClient/IClientActor.cs index c5c732cb9..c687ecf03 100644 --- a/examples/GeneratedActor/ActorClient/IClientActor.cs +++ b/examples/GeneratedActor/ActorClient/IClientActor.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2023 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/src/Dapr.Actors.Generators/ActorClientGenerator.cs b/src/Dapr.Actors.Generators/ActorClientGenerator.cs index f95fc4224..001604d53 100644 --- a/src/Dapr.Actors.Generators/ActorClientGenerator.cs +++ b/src/Dapr.Actors.Generators/ActorClientGenerator.cs @@ -11,7 +11,13 @@ // limitations under the License. // ------------------------------------------------------------------------ +using System.Collections.Immutable; +using Dapr.Actors.Generators.Diagnostics; +using Dapr.Actors.Generators.Extensions; +using Dapr.Actors.Generators.Helpers; +using Dapr.Actors.Generators.Models; using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; namespace Dapr.Actors.Generators; @@ -20,283 +26,265 @@ namespace Dapr.Actors.Generators; /// Generates strongly-typed actor clients that use the non-remoting actor proxy. /// [Generator] -public sealed class ActorClientGenerator : ISourceGenerator +public sealed class ActorClientGenerator : IIncrementalGenerator { - private const string GeneratorsNamespace = "Dapr.Actors.Generators"; - - private const string ActorMethodAttributeTypeName = "ActorMethodAttribute"; - private const string ActorMethodAttributeFullTypeName = GeneratorsNamespace + "." + ActorMethodAttributeTypeName; - - private const string GenerateActorClientAttribute = "GenerateActorClientAttribute"; - private const string GenerateActorClientAttributeFullTypeName = GeneratorsNamespace + "." + GenerateActorClientAttribute; - - private const string ActorMethodAttributeText = $@" - // - - #nullable enable - - using System; - - namespace {GeneratorsNamespace} - {{ - [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] - internal sealed class ActorMethodAttribute : Attribute - {{ - public string? Name {{ get; set; }} - }} - }}"; - - private const string GenerateActorClientAttributeText = $@" - // - - #nullable enable - - using System; - - namespace {GeneratorsNamespace} - {{ - [AttributeUsage(AttributeTargets.Interface, AllowMultiple = false, Inherited = false)] - internal sealed class GenerateActorClientAttribute : Attribute - {{ - public string? Name {{ get; set; }} - - public string? Namespace {{ get; set; }} - }} - }}"; - - private sealed class ActorInterfaceSyntaxReceiver : ISyntaxContextReceiver + /// + public void Initialize(IncrementalGeneratorInitializationContext context) { - private readonly List models = new(); - - public IEnumerable Models => this.models; - - #region ISyntaxContextReceiver Members - - public void OnVisitSyntaxNode(GeneratorSyntaxContext context) + // Register the source output that generates the attribute definitions for ActorMethodAttribute and GenerateActorClientAttribute. + context.RegisterPostInitializationOutput(context => { - if (context.Node is not InterfaceDeclarationSyntax interfaceDeclarationSyntax - || interfaceDeclarationSyntax.AttributeLists.Count == 0) - { - return; - } - - var interfaceSymbol = context.SemanticModel.GetDeclaredSymbol(interfaceDeclarationSyntax) as INamedTypeSymbol; - - if (interfaceSymbol is null - || !interfaceSymbol.GetAttributes().Any(a => a.AttributeClass?.ToString() == GenerateActorClientAttributeFullTypeName)) - { - return; - } - - this.models.Add(interfaceSymbol); - } - - #endregion + context.AddSource( + $"{Constants.ActorMethodAttributeFullTypeName}.g.cs", + Templates.ActorMethodAttributeSourceText(Constants.GeneratorsNamespace)); + + context.AddSource( + $"{Constants.GenerateActorClientAttributeFullTypeName}.g.cs", + Templates.GenerateActorClientAttributeSourceText(Constants.GeneratorsNamespace)); + }); + + // Register the value provider that triggers the generation of actor clients when detecting the GenerateActorClientAttribute. + IncrementalValuesProvider actorClientsToGenerate = context.SyntaxProvider + .ForAttributeWithMetadataName( + Constants.GenerateActorClientAttributeFullTypeName, + predicate: static (_, _) => true, + transform: static (gasc, cancellationToken) => CreateActorClientDescriptor(gasc, cancellationToken)); + + // Register the source output that generates the actor clients. + context.RegisterSourceOutput(actorClientsToGenerate, GenerateActorClientCode); } - #region ISourceGenerator Members - - /// - public void Execute(GeneratorExecutionContext context) + /// + /// Returns the descriptor for the actor client to generate. + /// + /// Current generator syntax context passed from generator pipeline. + /// Cancellation token used to interrupt the generation. + /// Returns the descriptor of actor client to generate. + private static ActorClientDescriptor CreateActorClientDescriptor( + GeneratorAttributeSyntaxContext context, + CancellationToken cancellationToken) { - if (context.SyntaxContextReceiver is not ActorInterfaceSyntaxReceiver actorInterfaceSyntaxReceiver) - { - return; - } - - var actorMethodAttributeSymbol = context.Compilation.GetTypeByMetadataName(ActorMethodAttributeFullTypeName) ?? throw new InvalidOperationException("Could not find ActorMethodAttribute."); - var generateActorClientAttributeSymbol = context.Compilation.GetTypeByMetadataName(GenerateActorClientAttributeFullTypeName) ?? throw new InvalidOperationException("Could not find GenerateActorClientAttribute."); - var cancellationTokenSymbol = context.Compilation.GetTypeByMetadataName("System.Threading.CancellationToken") ?? throw new InvalidOperationException("Could not find CancellationToken."); - - foreach (var interfaceSymbol in actorInterfaceSyntaxReceiver.Models) - { - try - { - var fullyQualifiedActorInterfaceTypeName = interfaceSymbol.ToString(); - - var attributeData = interfaceSymbol.GetAttributes().Single(a => a.AttributeClass?.Equals(generateActorClientAttributeSymbol, SymbolEqualityComparer.Default) == true); - - var accessibility = GetClientAccessibility(interfaceSymbol); - var clientTypeName = GetClientName(interfaceSymbol, attributeData); - var namespaceName = attributeData.NamedArguments.SingleOrDefault(kvp => kvp.Key == "Namespace").Value.Value?.ToString() ?? interfaceSymbol.ContainingNamespace.ToDisplayString(); - - var members = interfaceSymbol.GetMembers().OfType().Where(m => m.MethodKind == MethodKind.Ordinary).ToList(); + // Return the attribute data of GenerateActorClientAttribute, which is the attribute that triggered this generator + // and is expected to be the only attribute in the list of matching attributes. + var attributeData = context.Attributes.Single(); - var methodImplementations = String.Join("\n", members.Select(member => GenerateMethodImplementation(member, actorMethodAttributeSymbol, cancellationTokenSymbol))); + var actorInterfaceSymbol = (INamedTypeSymbol)context.TargetSymbol; - var source = $@" -// + // Use the namespace specified in the GenerateActorClientAttribute, or the namespace of the actor interface if not specified. + var namespaceName = attributeData.NamedArguments.SingleOrDefault(kvp => kvp.Key == "Namespace").Value.Value?.ToString() + ?? actorInterfaceSymbol.ContainingNamespace.ToDisplayString(); -namespace {namespaceName} -{{ - {accessibility} sealed class {clientTypeName} : {fullyQualifiedActorInterfaceTypeName} - {{ - private readonly Dapr.Actors.Client.ActorProxy actorProxy; + // Use the name specified in the GenerateActorClientAttribute, or the name of the actor interface with a "Client" suffix if not specified. + var clientName = attributeData.NamedArguments.SingleOrDefault(kvp => kvp.Key == "Name").Value.Value?.ToString() + ?? $"{(actorInterfaceSymbol.Name.StartsWith("I") ? actorInterfaceSymbol.Name.Substring(1) : actorInterfaceSymbol.Name)}Client"; - public {clientTypeName}(Dapr.Actors.Client.ActorProxy actorProxy) - {{ - this.actorProxy = actorProxy; - }} + // Actor member to generate the client for. + var members = actorInterfaceSymbol + .GetMembers() + .OfType() + .Where(m => m.MethodKind == MethodKind.Ordinary) + .ToImmutableArray(); - {methodImplementations} - }} -}} -"; - // Add the source code to the compilation - context.AddSource($"{namespaceName}.{clientTypeName}.g.cs", source); - } - catch (DiagnosticsException e) - { - foreach (var diagnostic in e.Diagnostics) - { - context.ReportDiagnostic(diagnostic); - } - } - } + return new ActorClientDescriptor + { + NamespaceName = namespaceName, + ClientTypeName = clientName, + Methods = members, + Accessibility = actorInterfaceSymbol.DeclaredAccessibility, + InterfaceType = actorInterfaceSymbol, + Compilation = context.SemanticModel.Compilation, + }; } - /// - public void Initialize(GeneratorInitializationContext context) + /// + /// Generates the actor client code based on the specified descriptor. + /// + /// Context passed from the source generator when it has registered an output. + /// Descriptor of actor client to generate. + /// Throws when one or more required symbols assembly are missing. + private static void GenerateActorClientCode(SourceProductionContext context, ActorClientDescriptor descriptor) { - /* - while (!Debugger.IsAttached) + try { - System.Threading.Thread.Sleep(500); - } - */ - - context.RegisterForPostInitialization( - i => - { - i.AddSource($"{ActorMethodAttributeFullTypeName}.g.cs", ActorMethodAttributeText); - i.AddSource($"{GenerateActorClientAttributeFullTypeName}.g.cs", GenerateActorClientAttributeText); - }); + var actorMethodAttributeSymbol = descriptor.Compilation.GetTypeByMetadataName(Constants.ActorMethodAttributeFullTypeName) + ?? throw new InvalidOperationException("Could not find ActorMethodAttribute type."); - context.RegisterForSyntaxNotifications(() => new ActorInterfaceSyntaxReceiver()); - } + var cancellationTokenSymbol = descriptor.Compilation.GetTypeByMetadataName("System.Threading.CancellationToken") + ?? throw new InvalidOperationException("Could not find CancellationToken type."); - #endregion + var actorClientBaseInterface = SyntaxFactory.SimpleBaseType(SyntaxFactory.ParseTypeName(descriptor.InterfaceType.ToString())); + var autoGeneratedComment = SyntaxFactory.Comment("// "); + var nullableAnnotation = SyntaxFactory.Trivia(SyntaxFactory.NullableDirectiveTrivia(SyntaxFactory.Token(SyntaxKind.EnableKeyword), true)); + var actorProxyTypeSyntax = SyntaxFactory.ParseTypeName(Constants.ActorProxyTypeName); - private static string GetClientAccessibility(INamedTypeSymbol interfaceSymbol) - { - return interfaceSymbol.DeclaredAccessibility switch + // Generate the actor proxy field to store the actor proxy instance. + var actorProxyFieldDeclaration = SyntaxFactory.FieldDeclaration(SyntaxFactory.VariableDeclaration(actorProxyTypeSyntax) + .WithVariables(SyntaxFactory.SingletonSeparatedList(SyntaxFactory.VariableDeclarator(SyntaxFactory.Identifier("actorProxy"))))) + .WithModifiers(SyntaxFactory.TokenList(new[] + { + SyntaxFactory.Token(SyntaxKind.PrivateKeyword), + SyntaxFactory.Token(SyntaxKind.ReadOnlyKeyword) + })); + + // Generate the constructor for the actor client. + var actorCtor = SyntaxFactory.ConstructorDeclaration(SyntaxFactory.Identifier(descriptor.ClientTypeName)) + .WithModifiers(SyntaxFactory.TokenList(SyntaxFactory.Token(SyntaxKind.PublicKeyword))) + .WithParameterList(SyntaxFactory.ParameterList(SyntaxFactory.SeparatedList(new[] + { + SyntaxFactory.Parameter(SyntaxFactory.Identifier("actorProxy")).WithType(actorProxyTypeSyntax) + }))) + .WithBody(SyntaxFactory.Block(SyntaxFactory.List(new StatementSyntax[] + { + SyntaxFactoryHelpers.ThrowIfArgumentNull("actorProxy"), + SyntaxFactory.ExpressionStatement(SyntaxFactory.AssignmentExpression( + SyntaxKind.SimpleAssignmentExpression, + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.ThisExpression(), + SyntaxFactory.IdentifierName("actorProxy")), + SyntaxFactory.IdentifierName("actorProxy")) + ), + }))); + + var actorMethods = descriptor.Methods + .OrderBy(member => member.DeclaredAccessibility) + .ThenBy(member => member.Name) + .Select(member => GenerateMethodImplementation(member, actorMethodAttributeSymbol, cancellationTokenSymbol)); + + var actorMembers = new List() + .Append(actorProxyFieldDeclaration) + .Append(actorCtor) + .Concat(actorMethods); + + var actorClientClassModifiers = new List() + .Concat(SyntaxFactoryHelpers.GetSyntaxKinds(descriptor.Accessibility)) + .Append(SyntaxKind.SealedKeyword) + .Select(sk => SyntaxFactory.Token(sk)); + + var actorClientClassDeclaration = SyntaxFactory.ClassDeclaration(descriptor.ClientTypeName) + .WithModifiers(SyntaxFactory.TokenList(actorClientClassModifiers)) + .WithMembers(SyntaxFactory.List(actorMembers)) + .WithBaseList(SyntaxFactory.BaseList( + SyntaxFactory.Token(SyntaxKind.ColonToken), + SyntaxFactory.SeparatedList(new[] { actorClientBaseInterface }))); + + var namespaceDeclaration = SyntaxFactory.NamespaceDeclaration(SyntaxFactory.ParseName(descriptor.NamespaceName)) + .WithMembers(SyntaxFactory.List(new[] { actorClientClassDeclaration })) + .WithLeadingTrivia(SyntaxFactory.TriviaList(new[] { + autoGeneratedComment, + nullableAnnotation, + })); + + var compilationOutput = SyntaxFactory.CompilationUnit() + .WithMembers(SyntaxFactory.SingletonList(namespaceDeclaration)) + .NormalizeWhitespace() + .ToFullString(); + + context.AddSource($"{descriptor.FullyQualifiedTypeName}.g.cs", compilationOutput); + } + catch (DiagnosticsException e) { - Accessibility.Public => "public", - Accessibility.Internal => "internal", - Accessibility.Private => "private", - Accessibility.Protected => "protected", - Accessibility.ProtectedAndInternal => "protected internal", - _ => throw new InvalidOperationException("Unexpected accessibility.") - }; - } - - private static string GetClientName(INamedTypeSymbol interfaceSymbol, AttributeData attributeData) - { - string? clientName = attributeData.NamedArguments.SingleOrDefault(kvp => kvp.Key == "Name").Value.Value?.ToString(); - - clientName ??= $"{(interfaceSymbol.Name.StartsWith("I") ? interfaceSymbol.Name.Substring(1) : interfaceSymbol.Name)}Client"; - - return clientName; + foreach (var diagnostic in e.Diagnostics) + { + context.ReportDiagnostic(diagnostic); + } + } } - private static string GenerateMethodImplementation(IMethodSymbol method, INamedTypeSymbol generateActorClientAttributeSymbol, INamedTypeSymbol cancellationTokenSymbol) + /// + /// Generates the method implementation for the specified method. + /// + /// + /// MethodSymbol extracted from the actor interface representing the method to generate. + /// + /// + /// ActorMethodAttribute symbol used to extract the original actor method name to use when making runtime calls. + /// + /// Symbol used to search the position of cancellationToken between method parameters. + /// Returns a of the generated method. + private static MethodDeclarationSyntax GenerateMethodImplementation( + IMethodSymbol method, + INamedTypeSymbol generateActorClientAttributeSymbol, + INamedTypeSymbol cancellationTokenSymbol) { int cancellationTokenIndex = method.Parameters.IndexOf(p => p.Type.Equals(cancellationTokenSymbol, SymbolEqualityComparer.Default)); var cancellationTokenParameter = cancellationTokenIndex != -1 ? method.Parameters[cancellationTokenIndex] : null; + var diagnostics = new List(); if (cancellationTokenParameter is not null && cancellationTokenIndex != method.Parameters.Length - 1) { - throw new DiagnosticsException(new[] - { - Diagnostic.Create( - new DiagnosticDescriptor( - "DAPR0001", - "Invalid method signature.", - "Cancellation tokens must be the last argument.", - "Dapr.Actors.Generators", - DiagnosticSeverity.Error, - true), - cancellationTokenParameter.Locations.First()) - }); + diagnostics.Add(CancellationTokensMustBeTheLastArgument.CreateDiagnostic(cancellationTokenParameter)); } - if ((method.Parameters.Length > 1 && cancellationTokenIndex == -1) - || (method.Parameters.Length > 2)) + if ((method.Parameters.Length > 1 && cancellationTokenIndex == -1) || (method.Parameters.Length > 2)) { - throw new DiagnosticsException(new[] - { - Diagnostic.Create( - new DiagnosticDescriptor( - "DAPR0002", - "Invalid method signature.", - "Only methods with a single argument or a single argument followed by a cancellation token are supported.", - "Dapr.Actors.Generators", - DiagnosticSeverity.Error, - true), - method.Locations.First()) - }); + diagnostics.Add(MethodMustOnlyHaveASingleArgumentOptionallyFollowedByACancellationToken.CreateDiagnostic(method)); } - var attributeData = method.GetAttributes().SingleOrDefault(a => a.AttributeClass?.Equals(generateActorClientAttributeSymbol, SymbolEqualityComparer.Default) == true); - - string? actualMethodName = attributeData?.NamedArguments.SingleOrDefault(kvp => kvp.Key == "Name").Value.Value?.ToString() ?? method.Name; - - var requestParameter = method.Parameters.Length > 0 && cancellationTokenIndex != 0 ? method.Parameters[0] : null; - - var returnTypeArgument = (method.ReturnType as INamedTypeSymbol)?.TypeArguments.FirstOrDefault(); - - string argumentDefinitions = String.Join(", ", method.Parameters.Select(p => $"{p.Type} {p.Name}")); - - if (cancellationTokenParameter is not null - && cancellationTokenParameter.IsOptional - && cancellationTokenParameter.HasExplicitDefaultValue - && cancellationTokenParameter.ExplicitDefaultValue is null) + // If there are any diagnostics, throw an exception to report them and stop the generation. + if (diagnostics.Any()) { - argumentDefinitions = argumentDefinitions + " = default"; + throw new DiagnosticsException(diagnostics); } - string argumentList = String.Join(", ", new[] { $@"""{actualMethodName}""" }.Concat(method.Parameters.Select(p => p.Name))); + // Get the ActorMethodAttribute data for the method, if it exists. + var attributeData = method.GetAttributes() + .SingleOrDefault(a => a.AttributeClass?.Equals(generateActorClientAttributeSymbol, SymbolEqualityComparer.Default) == true); - string templateArgs = - returnTypeArgument is not null - ? $"<{(requestParameter is not null ? $"{requestParameter.Type}, " : "")}{returnTypeArgument}>" - : ""; + // Generate the method name to use for the Dapr actor method invocation, using the Name property of ActorMethodAttribute if specified, + // or the original method name otherwise. + var daprMethodName = attributeData?.NamedArguments.SingleOrDefault(kvp => kvp.Key == "Name").Value.Value?.ToString() ?? method.Name; - return - $@"public {method.ReturnType} {method.Name}({argumentDefinitions}) - {{ - return this.actorProxy.InvokeMethodAsync{templateArgs}({argumentList}); - }}"; - } -} + var methodModifiers = new List() + .Concat(SyntaxFactoryHelpers.GetSyntaxKinds(method.DeclaredAccessibility)) + .Select(sk => SyntaxFactory.Token(sk)); -internal static class Extensions -{ - public static int IndexOf(this IEnumerable source, Func predicate) - { - int index = 0; + // Define the parameters to pass to the actor proxy method invocation. + // Exclude the CancellationToken parameter if it exists, because it need to be handled separately. + var methodParameters = method.Parameters + .Where(p => p.Type is not INamedTypeSymbol { Name: "CancellationToken" }) + .Select(p => SyntaxFactory.Parameter(SyntaxFactory.Identifier(p.Name)).WithType(SyntaxFactory.ParseTypeName(p.Type.ToString()))); - foreach (var item in source) + // Append the CancellationToken parameter if it exists, handling the case where it is optional and has no default value. + if (cancellationTokenParameter is not null) { - if (predicate(item)) + if (cancellationTokenParameter.IsOptional + && cancellationTokenParameter.HasExplicitDefaultValue + && cancellationTokenParameter.ExplicitDefaultValue is null) { - return index; + methodParameters = methodParameters.Append( + SyntaxFactory.Parameter(SyntaxFactory.Identifier(cancellationTokenParameter.Name)) + .WithDefault(SyntaxFactory.EqualsValueClause(SyntaxFactory.LiteralExpression(SyntaxKind.DefaultLiteralExpression))) + .WithType(SyntaxFactory.ParseTypeName(cancellationTokenParameter.Type.ToString()))); + } + else + { + methodParameters = methodParameters.Append( + SyntaxFactory.Parameter(SyntaxFactory.Identifier(cancellationTokenParameter.Name)) + .WithType(SyntaxFactory.ParseTypeName(cancellationTokenParameter.Type.ToString()))); } - - index++; } - return -1; - } -} + // Extract the return type of the original method. + var methodReturnType = (INamedTypeSymbol)method.ReturnType; -internal sealed class DiagnosticsException : Exception -{ - public DiagnosticsException(IEnumerable diagnostics) - : base(String.Join("\n", diagnostics.Select(d => d.ToString()))) - { - this.Diagnostics = diagnostics.ToArray(); + // Generate the method implementation. + var generatedMethod = SyntaxFactory.MethodDeclaration(SyntaxFactory.ParseTypeName(method.ReturnType.ToString()), method.Name) + .WithModifiers(SyntaxFactory.TokenList(methodModifiers)) + .WithParameterList(SyntaxFactory.ParameterList(SyntaxFactory.SeparatedList(methodParameters))) + .WithBody(SyntaxFactory.Block(SyntaxFactory.List(new StatementSyntax[] + { + SyntaxFactory.ReturnStatement(SyntaxFactoryHelpers.ActorProxyInvokeMethodAsync( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.ThisExpression(), + SyntaxFactory.IdentifierName("actorProxy")), + daprMethodName, + method.Parameters, + methodReturnType.TypeArguments + )), + }))); + + return generatedMethod; } - - public IEnumerable Diagnostics { get; } } diff --git a/src/Dapr.Actors.Generators/AnalyzerReleases.Shipped.md b/src/Dapr.Actors.Generators/AnalyzerReleases.Shipped.md new file mode 100644 index 000000000..62b61ac2c --- /dev/null +++ b/src/Dapr.Actors.Generators/AnalyzerReleases.Shipped.md @@ -0,0 +1,8 @@ +## Release 1.14 + +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|-------------------- +DAPR0001| Usage | Error | Cancellation tokens must be the last argument +DAPR0002| Usage | Error | Only methods with a single argument or a single argument followed by a cancellation token are supported \ No newline at end of file diff --git a/src/Dapr.Actors.Generators/AnalyzerReleases.Unshipped.md b/src/Dapr.Actors.Generators/AnalyzerReleases.Unshipped.md new file mode 100644 index 000000000..b1b99aaf2 --- /dev/null +++ b/src/Dapr.Actors.Generators/AnalyzerReleases.Unshipped.md @@ -0,0 +1,3 @@ +; Unshipped analyzer release +; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md + diff --git a/src/Dapr.Actors.Generators/Constants.cs b/src/Dapr.Actors.Generators/Constants.cs new file mode 100644 index 000000000..392def4ef --- /dev/null +++ b/src/Dapr.Actors.Generators/Constants.cs @@ -0,0 +1,38 @@ +namespace Dapr.Actors.Generators +{ + /// + /// Constants used by the code generator. + /// + internal static class Constants + { + /// + /// The namespace used by the generated code. + /// + public const string GeneratorsNamespace = "Dapr.Actors.Generators"; + + /// + /// The name of the attribute used to mark actor interfaces. + /// + public const string ActorMethodAttributeTypeName = "ActorMethodAttribute"; + + /// + /// The full type name of the attribute used to mark actor interfaces. + /// + public const string ActorMethodAttributeFullTypeName = GeneratorsNamespace + "." + ActorMethodAttributeTypeName; + + /// + /// The name of the attribute used to mark actor interfaces. + /// + public const string GenerateActorClientAttributeTypeName = "GenerateActorClientAttribute"; + + /// + /// The full type name of the attribute used to mark actor interfaces. + /// + public const string GenerateActorClientAttributeFullTypeName = GeneratorsNamespace + "." + GenerateActorClientAttributeTypeName; + + /// + /// Actor proxy type name. + /// + public const string ActorProxyTypeName = "Dapr.Actors.Client.ActorProxy"; + } +} diff --git a/src/Dapr.Actors.Generators/Dapr.Actors.Generators.csproj b/src/Dapr.Actors.Generators/Dapr.Actors.Generators.csproj index 370d422f1..b1f73383a 100644 --- a/src/Dapr.Actors.Generators/Dapr.Actors.Generators.csproj +++ b/src/Dapr.Actors.Generators/Dapr.Actors.Generators.csproj @@ -1,45 +1,55 @@ - + - - enable - enable - + + enable + enable + true + - - true - + + true + - - - - netstandard2.0 - + + + netstandard2.0 + - - false + + false - - true + + true - - false + + false - - This package contains source generators for interacting with Actor services using Dapr. - $(PackageTags);Actors - + + This package contains source generators for interacting with Actor services using Dapr. + $(PackageTags);Actors + - - - - + + + + + + + + + + + + + diff --git a/src/Dapr.Actors.Generators/Diagnostics/CancellationTokensMustBeTheLastArgument.cs b/src/Dapr.Actors.Generators/Diagnostics/CancellationTokensMustBeTheLastArgument.cs new file mode 100644 index 000000000..376bb360f --- /dev/null +++ b/src/Dapr.Actors.Generators/Diagnostics/CancellationTokensMustBeTheLastArgument.cs @@ -0,0 +1,25 @@ +using Microsoft.CodeAnalysis; + +namespace Dapr.Actors.Generators.Diagnostics +{ + internal static class CancellationTokensMustBeTheLastArgument + { + public const string DiagnosticId = "DAPR0001"; + public const string Title = "Invalid method signature"; + public const string MessageFormat = "Cancellation tokens must be the last argument"; + public const string Category = "Usage"; + + private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor( + DiagnosticId, + Title, + MessageFormat, + Category, + DiagnosticSeverity.Error, + isEnabledByDefault: true); + + internal static Diagnostic CreateDiagnostic(ISymbol symbol) => Diagnostic.Create( + Rule, + symbol.Locations.First(), + symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); + } +} diff --git a/src/Dapr.Actors.Generators/Diagnostics/MethodMustOnlyHaveASingleArgumentOptionallyFollowedByACancellationToken.cs b/src/Dapr.Actors.Generators/Diagnostics/MethodMustOnlyHaveASingleArgumentOptionallyFollowedByACancellationToken.cs new file mode 100644 index 000000000..c82b20630 --- /dev/null +++ b/src/Dapr.Actors.Generators/Diagnostics/MethodMustOnlyHaveASingleArgumentOptionallyFollowedByACancellationToken.cs @@ -0,0 +1,25 @@ +using Microsoft.CodeAnalysis; + +namespace Dapr.Actors.Generators.Diagnostics +{ + internal static class MethodMustOnlyHaveASingleArgumentOptionallyFollowedByACancellationToken + { + public const string DiagnosticId = "DAPR0002"; + public const string Title = "Invalid method signature"; + public const string MessageFormat = "Only methods with a single argument or a single argument followed by a cancellation token are supported"; + public const string Category = "Usage"; + + private static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor( + DiagnosticId, + Title, + MessageFormat, + Category, + DiagnosticSeverity.Error, + isEnabledByDefault: true); + + internal static Diagnostic CreateDiagnostic(ISymbol symbol) => Diagnostic.Create( + Rule, + symbol.Locations.First(), + symbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)); + } +} diff --git a/src/Dapr.Actors.Generators/DiagnosticsException.cs b/src/Dapr.Actors.Generators/DiagnosticsException.cs new file mode 100644 index 000000000..d196f8484 --- /dev/null +++ b/src/Dapr.Actors.Generators/DiagnosticsException.cs @@ -0,0 +1,25 @@ +using Microsoft.CodeAnalysis; + +namespace Dapr.Actors.Generators +{ + /// + /// Exception thrown when diagnostics are encountered during code generation. + /// + internal sealed class DiagnosticsException : Exception + { + /// + /// Initializes a new instance of the class. + /// + /// List of diagnostics generated. + public DiagnosticsException(IEnumerable diagnostics) + : base(string.Join("\n", diagnostics.Select(d => d.ToString()))) + { + this.Diagnostics = diagnostics.ToArray(); + } + + /// + /// Diagnostics encountered during code generation. + /// + public ICollection Diagnostics { get; } + } +} diff --git a/src/Dapr.Actors.Generators/Extensions/IEnumerableExtensions.cs b/src/Dapr.Actors.Generators/Extensions/IEnumerableExtensions.cs new file mode 100644 index 000000000..6b45e86f3 --- /dev/null +++ b/src/Dapr.Actors.Generators/Extensions/IEnumerableExtensions.cs @@ -0,0 +1,34 @@ +namespace Dapr.Actors.Generators.Extensions +{ + internal static class IEnumerableExtensions + { + /// + /// Returns the index of the first item in the sequence that satisfies the predicate. If no item satisfies the predicate, -1 is returned. + /// + /// The type of objects in the . + /// in which to search. + /// Function performed to check whether an item satisfies the condition. + /// Return the zero-based index of the first occurrence of an element that satisfies the condition, if found; otherwise, -1. + internal static int IndexOf(this IEnumerable source, Func predicate) + { + if (predicate is null) + { + throw new ArgumentNullException(nameof(predicate)); + } + + int index = 0; + + foreach (var item in source) + { + if (predicate(item)) + { + return index; + } + + index++; + } + + return -1; + } + } +} diff --git a/src/Dapr.Actors.Generators/Helpers/SyntaxFactoryHelpers.cs b/src/Dapr.Actors.Generators/Helpers/SyntaxFactoryHelpers.cs new file mode 100644 index 000000000..36df7b280 --- /dev/null +++ b/src/Dapr.Actors.Generators/Helpers/SyntaxFactoryHelpers.cs @@ -0,0 +1,159 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace Dapr.Actors.Generators.Helpers +{ + /// + /// Syntax factory helpers for generating syntax. + /// + internal static partial class SyntaxFactoryHelpers + { + /// + /// Generates a syntax for an based on the given argument name. + /// + /// Name of the argument that generated the exception. + /// Returns used to throw an . + public static ThrowExpressionSyntax ThrowArgumentNullException(string argumentName) + { + return SyntaxFactory.ThrowExpression( + SyntaxFactory.Token(SyntaxKind.ThrowKeyword), + SyntaxFactory.ObjectCreationExpression( + SyntaxFactory.Token(SyntaxKind.NewKeyword), + SyntaxFactory.ParseTypeName("System.ArgumentNullException"), + SyntaxFactory.ArgumentList(SyntaxFactory.SeparatedList(new[] + { + SyntaxFactory.Argument(NameOfExpression(argumentName)) + })), + default + ) + ); + } + + /// + /// Generates a syntax for null check for the given argument name. + /// + /// Name of the argument whose null check is to be generated. + /// Returns representing an argument null check. + public static IfStatementSyntax ThrowIfArgumentNull(string argumentName) + { + return SyntaxFactory.IfStatement( + SyntaxFactory.BinaryExpression( + SyntaxKind.IsExpression, + SyntaxFactory.IdentifierName(argumentName), + SyntaxFactory.LiteralExpression(SyntaxKind.NullLiteralExpression) + ), + SyntaxFactory.Block(SyntaxFactory.List(new StatementSyntax[] + { + SyntaxFactory.ExpressionStatement(ThrowArgumentNullException(argumentName)) + })) + ); + } + + /// + /// Generates a syntax for nameof expression for the given argument name. + /// + /// Name of the argument from which the syntax is to be generated. + /// Return a representing a NameOf expression. + public static ExpressionSyntax NameOfExpression(string argumentName) + { + var nameofIdentifier = SyntaxFactory.Identifier( + SyntaxFactory.TriviaList(), + SyntaxKind.NameOfKeyword, + "nameof", + "nameof", + SyntaxFactory.TriviaList()); + + return SyntaxFactory.InvocationExpression( + SyntaxFactory.IdentifierName(nameofIdentifier), + SyntaxFactory.ArgumentList(SyntaxFactory.SeparatedList(new[] + { + SyntaxFactory.Argument(SyntaxFactory.IdentifierName(argumentName)) + })) + ); + } + + /// + /// Generates the invocation syntax to call a remote method with the actor proxy. + /// + /// Member syntax to access actorProxy member. + /// Name of remote method to invoke. + /// Remote method parameters. + /// Return types of remote method invocation. + /// Returns the representing a call to the actor proxy. + public static InvocationExpressionSyntax ActorProxyInvokeMethodAsync( + MemberAccessExpressionSyntax actorProxyMemberSyntax, + string remoteMethodName, + IEnumerable remoteMethodParameters, + IEnumerable remoteMethodReturnTypes) + { + // Define the type arguments to pass to the actor proxy method invocation. + var proxyInvocationTypeArguments = new List() + .Concat(remoteMethodParameters + .Where(p => p.Type is not { Name: "CancellationToken" }) + .Select(p => SyntaxFactory.ParseTypeName(p.Type.ToString()))) + .Concat(remoteMethodReturnTypes + .Select(a => SyntaxFactory.ParseTypeName(a.OriginalDefinition.ToString()))); + + // Define the arguments to pass to the actor proxy method invocation. + var proxyInvocationArguments = new List() + // Name of remote method to invoke. + .Append(SyntaxFactory.Argument(SyntaxFactory.LiteralExpression(SyntaxKind.StringLiteralExpression, SyntaxFactory.Literal(remoteMethodName)))) + // Actor method arguments, including the CancellationToken if it exists. + .Concat(remoteMethodParameters.Select(p => SyntaxFactory.Argument(SyntaxFactory.IdentifierName(p.Name)))); + + // If the invocation has return types or input parameters, we need to use the generic version of the method. + SimpleNameSyntax invokeAsyncSyntax = proxyInvocationTypeArguments.Any() + ? SyntaxFactory.GenericName( + SyntaxFactory.Identifier("InvokeMethodAsync"), + SyntaxFactory.TypeArgumentList(SyntaxFactory.SeparatedList(proxyInvocationTypeArguments))) + : SyntaxFactory.IdentifierName("InvokeMethodAsync"); + + // Generate the invocation syntax. + var generatedInvocationSyntax = SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + actorProxyMemberSyntax, + invokeAsyncSyntax + )) + .WithArgumentList(SyntaxFactory.ArgumentList(SyntaxFactory.SeparatedList(proxyInvocationArguments))); + + return generatedInvocationSyntax; + } + + /// + /// Returns the for the specified accessibility. + /// + /// Accessibility to convert into a . + /// Returns the collection of representing the given accessibility. + /// Throws when un unexpected accessibility is passed. + public static ICollection GetSyntaxKinds(Accessibility accessibility) + { + var syntaxKinds = new List(); + + switch (accessibility) + { + case Accessibility.Public: + syntaxKinds.Add(SyntaxKind.PublicKeyword); + break; + case Accessibility.Internal: + syntaxKinds.Add(SyntaxKind.InternalKeyword); + break; + case Accessibility.Private: + syntaxKinds.Add(SyntaxKind.PrivateKeyword); + break; + case Accessibility.Protected: + syntaxKinds.Add(SyntaxKind.ProtectedKeyword); + break; + case Accessibility.ProtectedAndInternal: + syntaxKinds.Add(SyntaxKind.ProtectedKeyword); + syntaxKinds.Add(SyntaxKind.InternalKeyword); + break; + default: + throw new InvalidOperationException("Unexpected accessibility"); + } + + return syntaxKinds; + } + } +} diff --git a/src/Dapr.Actors.Generators/Models/ActorClientDescriptor.cs b/src/Dapr.Actors.Generators/Models/ActorClientDescriptor.cs new file mode 100644 index 000000000..e1f54fac4 --- /dev/null +++ b/src/Dapr.Actors.Generators/Models/ActorClientDescriptor.cs @@ -0,0 +1,46 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; + +namespace Dapr.Actors.Generators.Models +{ + /// + /// Describes an actor client to generate. + /// + internal record class ActorClientDescriptor : IEquatable + { + /// + /// Gets or sets the symbol representing the actor interface. + /// + public INamedTypeSymbol InterfaceType { get; set; } = null!; + + /// + /// Accessibility of the generated client. + /// + public Accessibility Accessibility { get; set; } + + /// + /// Namespace of the generated client. + /// + public string NamespaceName { get; set; } = string.Empty; + + /// + /// Name of the generated client. + /// + public string ClientTypeName { get; set; } = string.Empty; + + /// + /// Fully qualified type name of the generated client. + /// + public string FullyQualifiedTypeName => $"{NamespaceName}.{ClientTypeName}"; + + /// + /// Methods to generate in the client. + /// + public ImmutableArray Methods { get; set; } = Array.Empty().ToImmutableArray(); + + /// + /// Compilation to use for generating the client. + /// + public Compilation Compilation { get; set; } = null!; + } +} diff --git a/src/Dapr.Actors.Generators/Properties/launchSettings.json b/src/Dapr.Actors.Generators/Properties/launchSettings.json new file mode 100644 index 000000000..f146e6195 --- /dev/null +++ b/src/Dapr.Actors.Generators/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "Debug": { + "commandName": "DebugRoslynComponent", + "targetProject": "..\\..\\examples\\GeneratedActor\\ActorClient\\ActorClient.csproj" + } + } +} \ No newline at end of file diff --git a/src/Dapr.Actors.Generators/Templates.cs b/src/Dapr.Actors.Generators/Templates.cs new file mode 100644 index 000000000..6cc4c9f87 --- /dev/null +++ b/src/Dapr.Actors.Generators/Templates.cs @@ -0,0 +1,87 @@ +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Text; + +namespace Dapr.Actors.Generators +{ + /// + /// Templates for generating source code. + /// + internal static partial class Templates + { + /// + /// Returns the for the ActorMethodAttribute. + /// + /// Namespace where to generate attribute. + /// The representing the ActorMethodAttribute. + /// Throws when destinationNamespace is null. + public static SourceText ActorMethodAttributeSourceText(string destinationNamespace) + { + if (destinationNamespace is null) + { + throw new ArgumentNullException(nameof(destinationNamespace)); + } + + var source = $@" +// + +#nullable enable + +using System; + +namespace {destinationNamespace} +{{ + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] + internal sealed class {Constants.ActorMethodAttributeTypeName} : Attribute + {{ + public string? Name {{ get; set; }} + }} +}}"; + + return SourceText.From( + SyntaxFactory.ParseCompilationUnit(source) + .NormalizeWhitespace() + .ToFullString(), + Encoding.UTF8); + } + + /// + /// Returns the for the GenerateActorClientAttribute. + /// + /// Namespace where to generate attribute. + /// The representing the ActorMethodAttribute. + /// Throws when destinationNamespace is null. + public static SourceText GenerateActorClientAttributeSourceText(string destinationNamespace) + { + if (destinationNamespace is null) + { + throw new ArgumentNullException(nameof(destinationNamespace)); + } + + string source = $@" +// + +#nullable enable + +using System; + +namespace {destinationNamespace} +{{ + [AttributeUsage(AttributeTargets.Interface, AllowMultiple = false, Inherited = false)] + internal sealed class {Constants.GenerateActorClientAttributeTypeName} : Attribute + {{ + public string? Name {{ get; set; }} + + public string? Namespace {{ get; set; }} + }} +}}"; + + return SourceText.From( + SyntaxFactory.ParseCompilationUnit(source) + .NormalizeWhitespace() + .ToFullString(), + Encoding.UTF8); + } + } +} diff --git a/test/Dapr.Actors.Generators.Test/ActorClientGeneratorTests.cs b/test/Dapr.Actors.Generators.Test/ActorClientGeneratorTests.cs index ce4c0accd..4c0ef194e 100644 --- a/test/Dapr.Actors.Generators.Test/ActorClientGeneratorTests.cs +++ b/test/Dapr.Actors.Generators.Test/ActorClientGeneratorTests.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2023 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -21,43 +21,40 @@ namespace Dapr.Actors.Generators; public sealed class ActorClientGeneratorTests { - private const string ActorMethodAttributeText = $@" - // - - #nullable enable - - using System; - - namespace Dapr.Actors.Generators - {{ - [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] - internal sealed class ActorMethodAttribute : Attribute - {{ - public string? Name {{ get; set; }} - }} - }}"; - - private static readonly (string, SourceText) ActorMethodAttributeSource = ("Dapr.Actors.Generators/Dapr.Actors.Generators.ActorClientGenerator/Dapr.Actors.Generators.ActorMethodAttribute.g.cs", SourceText.From(ActorMethodAttributeText, Encoding.UTF8)); - - private const string GenerateActorClientAttributeText = $@" - // - - #nullable enable - - using System; - - namespace Dapr.Actors.Generators - {{ - [AttributeUsage(AttributeTargets.Interface, AllowMultiple = false, Inherited = false)] - internal sealed class GenerateActorClientAttribute : Attribute - {{ - public string? Name {{ get; set; }} - - public string? Namespace {{ get; set; }} - }} - }}"; - - private static readonly (string, SourceText) GenerateActorClientAttributeSource = ("Dapr.Actors.Generators/Dapr.Actors.Generators.ActorClientGenerator/Dapr.Actors.Generators.GenerateActorClientAttribute.g.cs", SourceText.From(GenerateActorClientAttributeText, Encoding.UTF8)); + private const string ActorMethodAttributeText = $@"// +#nullable enable +using System; + +namespace Dapr.Actors.Generators +{{ + [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] + internal sealed class ActorMethodAttribute : Attribute + {{ + public string? Name {{ get; set; }} + }} +}}"; + + private static readonly (string, SourceText) ActorMethodAttributeSource = ( + Path.Combine("Dapr.Actors.Generators", "Dapr.Actors.Generators.ActorClientGenerator", "Dapr.Actors.Generators.ActorMethodAttribute.g.cs"), + SourceText.From(ActorMethodAttributeText, Encoding.UTF8)); + + private const string GenerateActorClientAttributeText = $@"// +#nullable enable +using System; + +namespace Dapr.Actors.Generators +{{ + [AttributeUsage(AttributeTargets.Interface, AllowMultiple = false, Inherited = false)] + internal sealed class GenerateActorClientAttribute : Attribute + {{ + public string? Name {{ get; set; }} + public string? Namespace {{ get; set; }} + }} +}}"; + + private static readonly (string, SourceText) GenerateActorClientAttributeSource = ( + Path.Combine("Dapr.Actors.Generators", "Dapr.Actors.Generators.ActorClientGenerator", "Dapr.Actors.Generators.GenerateActorClientAttribute.g.cs"), + SourceText.From(GenerateActorClientAttributeText, Encoding.UTF8)); private static VerifyCS.Test CreateTest(string originalSource, string? generatedName = null, string? generatedSource = null) { @@ -77,7 +74,9 @@ private static VerifyCS.Test CreateTest(string originalSource, string? generated if (generatedName is not null && generatedSource is not null) { - test.TestState.GeneratedSources.Add(($"Dapr.Actors.Generators/Dapr.Actors.Generators.ActorClientGenerator/{generatedName}", SourceText.From(generatedSource, Encoding.UTF8))); + test.TestState.GeneratedSources.Add(( + Path.Combine("Dapr.Actors.Generators", "Dapr.Actors.Generators.ActorClientGenerator", generatedName), + SourceText.From(generatedSource, Encoding.UTF8))); } return test; @@ -97,20 +96,22 @@ public interface ITestActor { Task TestMethod(); } -} -"; - - var generatedSource = @" -// +}"; + var generatedSource = @"// +#nullable enable namespace Test { public sealed class TestActorClient : Test.ITestActor { private readonly Dapr.Actors.Client.ActorProxy actorProxy; - public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) { + if (actorProxy is null) + { + throw new System.ArgumentNullException(nameof(actorProxy)); + } + this.actorProxy = actorProxy; } @@ -119,8 +120,7 @@ public System.Threading.Tasks.Task TestMethod() return this.actorProxy.InvokeMethodAsync(""TestMethod""); } } -} -"; +}"; await CreateTest(originalSource, "Test.TestActorClient.g.cs", generatedSource).RunAsync(); } @@ -139,20 +139,22 @@ internal interface ITestActor { Task TestMethod(); } -} -"; - - var generatedSource = @" -// +}"; + var generatedSource = @"// +#nullable enable namespace Test { internal sealed class TestActorClient : Test.ITestActor { private readonly Dapr.Actors.Client.ActorProxy actorProxy; - public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) { + if (actorProxy is null) + { + throw new System.ArgumentNullException(nameof(actorProxy)); + } + this.actorProxy = actorProxy; } @@ -161,8 +163,7 @@ public System.Threading.Tasks.Task TestMethod() return this.actorProxy.InvokeMethodAsync(""TestMethod""); } } -} -"; +}"; await CreateTest(originalSource, "Test.TestActorClient.g.cs", generatedSource).RunAsync(); } @@ -181,20 +182,22 @@ internal interface ITestActor { Task TestMethod(); } -} -"; - - var generatedSource = @" -// +}"; + var generatedSource = @"// +#nullable enable namespace Test { internal sealed class MyTestActorClient : Test.ITestActor { private readonly Dapr.Actors.Client.ActorProxy actorProxy; - public MyTestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) { + if (actorProxy is null) + { + throw new System.ArgumentNullException(nameof(actorProxy)); + } + this.actorProxy = actorProxy; } @@ -203,8 +206,7 @@ public System.Threading.Tasks.Task TestMethod() return this.actorProxy.InvokeMethodAsync(""TestMethod""); } } -} -"; +}"; await CreateTest(originalSource, "Test.MyTestActorClient.g.cs", generatedSource).RunAsync(); } @@ -223,20 +225,22 @@ internal interface ITestActor { Task TestMethod(); } -} -"; - - var generatedSource = @" -// +}"; + var generatedSource = @"// +#nullable enable namespace MyTest { internal sealed class TestActorClient : Test.ITestActor { private readonly Dapr.Actors.Client.ActorProxy actorProxy; - public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) { + if (actorProxy is null) + { + throw new System.ArgumentNullException(nameof(actorProxy)); + } + this.actorProxy = actorProxy; } @@ -245,8 +249,7 @@ public System.Threading.Tasks.Task TestMethod() return this.actorProxy.InvokeMethodAsync(""TestMethod""); } } -} -"; +}"; await CreateTest(originalSource, "MyTest.TestActorClient.g.cs", generatedSource).RunAsync(); } @@ -269,17 +272,20 @@ public interface ITestActor } "; - var generatedSource = @" -// - + var generatedSource = @"// +#nullable enable namespace Test { public sealed class TestActorClient : Test.ITestActor { private readonly Dapr.Actors.Client.ActorProxy actorProxy; - public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) { + if (actorProxy is null) + { + throw new System.ArgumentNullException(nameof(actorProxy)); + } + this.actorProxy = actorProxy; } @@ -288,8 +294,7 @@ public System.Threading.Tasks.Task TestMethod() return this.actorProxy.InvokeMethodAsync(""MyTestMethod""); } } -} -"; +}"; await CreateTest(originalSource, "Test.TestActorClient.g.cs", generatedSource).RunAsync(); } @@ -313,27 +318,29 @@ public interface ITestActor } "; - var generatedSource = @" -// - + var generatedSource = @"// +#nullable enable namespace Test { public sealed class TestActorClient : Test.ITestActor { private readonly Dapr.Actors.Client.ActorProxy actorProxy; - public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) { + if (actorProxy is null) + { + throw new System.ArgumentNullException(nameof(actorProxy)); + } + this.actorProxy = actorProxy; } public System.Threading.Tasks.Task TestMethod(Test.TestValue value) { - return this.actorProxy.InvokeMethodAsync(""TestMethod"", value); + return this.actorProxy.InvokeMethodAsync(""TestMethod"", value); } } -} -"; +}"; await CreateTest(originalSource, "Test.TestActorClient.g.cs", generatedSource).RunAsync(); } @@ -354,20 +361,22 @@ public interface ITestActor { Task TestMethodAsync(); } -} -"; - - var generatedSource = @" -// +}"; + var generatedSource = @"// +#nullable enable namespace Test { public sealed class TestActorClient : Test.ITestActor { private readonly Dapr.Actors.Client.ActorProxy actorProxy; - public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) { + if (actorProxy is null) + { + throw new System.ArgumentNullException(nameof(actorProxy)); + } + this.actorProxy = actorProxy; } @@ -376,8 +385,7 @@ public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) return this.actorProxy.InvokeMethodAsync(""TestMethodAsync""); } } -} -"; +}"; await CreateTest(originalSource, "Test.TestActorClient.g.cs", generatedSource).RunAsync(); } @@ -400,20 +408,22 @@ public interface ITestActor { Task TestMethodAsync(TestRequestValue value); } -} -"; - - var generatedSource = @" -// +}"; + var generatedSource = @"// +#nullable enable namespace Test { public sealed class TestActorClient : Test.ITestActor { private readonly Dapr.Actors.Client.ActorProxy actorProxy; - public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) { + if (actorProxy is null) + { + throw new System.ArgumentNullException(nameof(actorProxy)); + } + this.actorProxy = actorProxy; } @@ -422,8 +432,7 @@ public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) return this.actorProxy.InvokeMethodAsync(""TestMethodAsync"", value); } } -} -"; +}"; await CreateTest(originalSource, "Test.TestActorClient.g.cs", generatedSource).RunAsync(); } @@ -443,20 +452,22 @@ public interface ITestActor { Task TestMethodAsync(CancellationToken cancellationToken); } -} -"; - - var generatedSource = @" -// +}"; + var generatedSource = @"// +#nullable enable namespace Test { public sealed class TestActorClient : Test.ITestActor { private readonly Dapr.Actors.Client.ActorProxy actorProxy; - public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) { + if (actorProxy is null) + { + throw new System.ArgumentNullException(nameof(actorProxy)); + } + this.actorProxy = actorProxy; } @@ -465,8 +476,7 @@ public System.Threading.Tasks.Task TestMethodAsync(System.Threading.Cancellation return this.actorProxy.InvokeMethodAsync(""TestMethodAsync"", cancellationToken); } } -} -"; +}"; await CreateTest(originalSource, "Test.TestActorClient.g.cs", generatedSource).RunAsync(); } @@ -486,20 +496,22 @@ public interface ITestActor { Task TestMethodAsync(CancellationToken cancellationToken = default); } -} -"; - - var generatedSource = @" -// +}"; + var generatedSource = @"// +#nullable enable namespace Test { public sealed class TestActorClient : Test.ITestActor { private readonly Dapr.Actors.Client.ActorProxy actorProxy; - public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) { + if (actorProxy is null) + { + throw new System.ArgumentNullException(nameof(actorProxy)); + } + this.actorProxy = actorProxy; } @@ -508,8 +520,7 @@ public System.Threading.Tasks.Task TestMethodAsync(System.Threading.Cancellation return this.actorProxy.InvokeMethodAsync(""TestMethodAsync"", cancellationToken); } } -} -"; +}"; await CreateTest(originalSource, "Test.TestActorClient.g.cs", generatedSource).RunAsync(); } @@ -534,27 +545,29 @@ public interface ITestActor } "; - var generatedSource = @" -// - + var generatedSource = @"// +#nullable enable namespace Test { public sealed class TestActorClient : Test.ITestActor { private readonly Dapr.Actors.Client.ActorProxy actorProxy; - public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) { + if (actorProxy is null) + { + throw new System.ArgumentNullException(nameof(actorProxy)); + } + this.actorProxy = actorProxy; } public System.Threading.Tasks.Task TestMethodAsync(Test.TestValue value, System.Threading.CancellationToken cancellationToken) { - return this.actorProxy.InvokeMethodAsync(""TestMethodAsync"", value, cancellationToken); + return this.actorProxy.InvokeMethodAsync(""TestMethodAsync"", value, cancellationToken); } } -} -"; +}"; await CreateTest(originalSource, "Test.TestActorClient.g.cs", generatedSource).RunAsync(); } @@ -579,27 +592,29 @@ public interface ITestActor } "; - var generatedSource = @" -// - + var generatedSource = @"// +#nullable enable namespace Test { public sealed class TestActorClient : Test.ITestActor { private readonly Dapr.Actors.Client.ActorProxy actorProxy; - public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) { + if (actorProxy is null) + { + throw new System.ArgumentNullException(nameof(actorProxy)); + } + this.actorProxy = actorProxy; } public System.Threading.Tasks.Task TestMethodAsync(Test.TestValue value, System.Threading.CancellationToken cancellationToken = default) { - return this.actorProxy.InvokeMethodAsync(""TestMethodAsync"", value, cancellationToken); + return this.actorProxy.InvokeMethodAsync(""TestMethodAsync"", value, cancellationToken); } } -} -"; +}"; await CreateTest(originalSource, "Test.TestActorClient.g.cs", generatedSource).RunAsync(); } @@ -621,15 +636,14 @@ public interface ITestActor { Task TestMethodAsync(CancellationToken cancellationToken, int value); } -} -"; +}"; var test = CreateTest(originalSource); test.TestState.ExpectedDiagnostics.Add( new DiagnosticResult("DAPR0001", DiagnosticSeverity.Error) .WithSpan(13, 48, 13, 65) - .WithMessage("Cancellation tokens must be the last argument.")); + .WithMessage("Cancellation tokens must be the last argument")); await test.RunAsync(); } @@ -651,15 +665,14 @@ public interface ITestActor { Task TestMethodAsync(int value1, int value2); } -} -"; +}"; var test = CreateTest(originalSource); test.TestState.ExpectedDiagnostics.Add( new DiagnosticResult("DAPR0002", DiagnosticSeverity.Error) .WithSpan(13, 14, 13, 29) - .WithMessage("Only methods with a single argument or a single argument followed by a cancellation token are supported.")); + .WithMessage("Only methods with a single argument or a single argument followed by a cancellation token are supported")); await test.RunAsync(); } @@ -681,16 +694,15 @@ public interface ITestActor { Task TestMethodAsync(int value1, int value2, CancellationToken cancellationToken); } -} -"; +}"; var test = CreateTest(originalSource); test.TestState.ExpectedDiagnostics.Add( new DiagnosticResult("DAPR0002", DiagnosticSeverity.Error) .WithSpan(13, 14, 13, 29) - .WithMessage("Only methods with a single argument or a single argument followed by a cancellation token are supported.")); + .WithMessage("Only methods with a single argument or a single argument followed by a cancellation token are supported")); await test.RunAsync(); } -} \ No newline at end of file +} diff --git a/test/Dapr.Actors.Generators.Test/CSharpSourceGeneratorVerifier.cs b/test/Dapr.Actors.Generators.Test/CSharpSourceGeneratorVerifier.cs index 2b1046e1a..c64fd3427 100644 --- a/test/Dapr.Actors.Generators.Test/CSharpSourceGeneratorVerifier.cs +++ b/test/Dapr.Actors.Generators.Test/CSharpSourceGeneratorVerifier.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2023 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -16,28 +16,25 @@ using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Testing; using Microsoft.CodeAnalysis.Testing; -using Microsoft.CodeAnalysis.Testing.Verifiers; /// /// From Roslyn Source Generators Cookbook: https://github.com/dotnet/roslyn/blob/main/docs/features/source-generators.cookbook.md#unit-testing-of-generators /// internal static class CSharpSourceGeneratorVerifier - where TSourceGenerator : ISourceGenerator, new() + where TSourceGenerator : IIncrementalGenerator, new() { -#pragma warning disable CS0618 // Type or member is obsolete - public class Test : CSharpSourceGeneratorTest -#pragma warning restore CS0618 // Type or member is obsolete + public class Test : CSharpSourceGeneratorTest { public Test() { int frameworkVersion = - #if NET6_0 +#if NET6_0 6; - #elif NET7_0 +#elif NET7_0 7; - #elif NET8_0 +#elif NET8_0 8; - #endif +#endif // // NOTE: Ordinarily we'd use the following: @@ -58,10 +55,10 @@ public Test() protected override CompilationOptions CreateCompilationOptions() { - var compilationOptions = base.CreateCompilationOptions(); + var compilationOptions = base.CreateCompilationOptions(); - return compilationOptions - .WithSpecificDiagnosticOptions(compilationOptions.SpecificDiagnosticOptions.SetItems(GetNullableWarningsFromCompiler())); + return compilationOptions + .WithSpecificDiagnosticOptions(compilationOptions.SpecificDiagnosticOptions.SetItems(GetNullableWarningsFromCompiler())); } public LanguageVersion LanguageVersion { get; set; } = LanguageVersion.Default; diff --git a/test/Dapr.Actors.Generators.Test/Dapr.Actors.Generators.Test.csproj b/test/Dapr.Actors.Generators.Test/Dapr.Actors.Generators.Test.csproj index 80a79cafe..9e9a9e4db 100644 --- a/test/Dapr.Actors.Generators.Test/Dapr.Actors.Generators.Test.csproj +++ b/test/Dapr.Actors.Generators.Test/Dapr.Actors.Generators.Test.csproj @@ -13,9 +13,7 @@ - - - + @@ -27,7 +25,6 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - diff --git a/test/Dapr.Actors.Generators.Test/Extensions/IEnumerableExtensionsTests.cs b/test/Dapr.Actors.Generators.Test/Extensions/IEnumerableExtensionsTests.cs new file mode 100644 index 000000000..97dbcfe1e --- /dev/null +++ b/test/Dapr.Actors.Generators.Test/Extensions/IEnumerableExtensionsTests.cs @@ -0,0 +1,52 @@ +using Dapr.Actors.Generators.Extensions; + +namespace Dapr.Actors.Generators.Test.Extensions +{ + public class IEnumerableExtensionsTests + { + [Fact] + public void IndexOf_WhenPredicateIsNull_ThrowsArgumentNullException() + { + // Arrange + var source = new[] { 1, 2, 3, 4, 5 }; + Func predicate = null!; + + // Act + Action act = () => source.IndexOf(predicate); + + // Assert + Assert.Throws(act); + } + + [Theory] + [InlineData(new int[] { }, 3, -1)] + [InlineData(new[] { 1, 2, 3, 4, 5 }, 6, -1)] + public void IndexOf_WhenItemDoesNotExist_ReturnsMinusOne(int[] source, int item, int expected) + { + // Arrange + Func predicate = (x) => x == item; + + // Act + var index = source.IndexOf(predicate); + + // Assert + Assert.Equal(expected, index); + } + + [Theory] + [InlineData(new[] { 1, 2, 3, 4, 5 }, 3, 2)] + [InlineData(new[] { 1, 2, 3, 4, 5 }, 1, 0)] + [InlineData(new[] { 1, 2, 3, 4, 5 }, 5, 4)] + public void IndexOf_WhenItemExists_ReturnsIndexOfItem(int[] source, int item, int expected) + { + // Arrange + Func predicate = (x) => x == item; + + // Act + var index = source.IndexOf(predicate); + + // Assert + Assert.Equal(expected, index); + } + } +} diff --git a/test/Dapr.Actors.Generators.Test/Helpers/SyntaxFactoryHelpersTests.cs b/test/Dapr.Actors.Generators.Test/Helpers/SyntaxFactoryHelpersTests.cs new file mode 100644 index 000000000..807bd7469 --- /dev/null +++ b/test/Dapr.Actors.Generators.Test/Helpers/SyntaxFactoryHelpersTests.cs @@ -0,0 +1,133 @@ +using Dapr.Actors.Generators.Helpers; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace Dapr.Actors.Generators.Test.Helpers +{ + public class SyntaxFactoryHelpersTests + { + [Fact] + public void ThrowArgumentNullException_GenerateThrowArgumentNullExceptionSyntaxWithGivenArgumentName() + { + // Arrange + var argumentName = "arg0"; + var expectedSource = $@"throw new System.ArgumentNullException(nameof(arg0));"; + var expectedSourceNormalized = SyntaxFactory.ParseSyntaxTree(expectedSource) + .GetRoot() + .NormalizeWhitespace() + .ToFullString(); + + // Act + var generatedSource = SyntaxFactory.ExpressionStatement(SyntaxFactoryHelpers.ThrowArgumentNullException(argumentName)) + .SyntaxTree + .GetRoot() + .NormalizeWhitespace() + .ToFullString(); + + // Assert + Assert.Equal(expectedSourceNormalized, generatedSource); + } + + [Fact] + public void ThrowIfArgumentNullException_GivesNullCheckSyntaxWithGivenArgumentName() + { + // Arrange + var argumentName = "arg0"; + var expectedSource = $@"if (arg0 is null) +{{ + throw new System.ArgumentNullException(nameof(arg0)); +}}"; + var expectedSourceNormalized = SyntaxFactory.ParseSyntaxTree(expectedSource) + .GetRoot() + .NormalizeWhitespace() + .ToFullString(); + + // Act + var generatedSource = SyntaxFactoryHelpers.ThrowIfArgumentNull(argumentName) + .SyntaxTree + .GetRoot() + .NormalizeWhitespace() + .ToFullString(); + + // Assert + Assert.Equal(expectedSourceNormalized, generatedSource); + } + + [Fact] + public void ActorProxyInvokeMethodAsync_WithoutReturnTypeAndParamters_ReturnNonGenericInvokeMethodAsync() + { + // Arrange + var remoteMethodName = "RemoteMethodToCall"; + var remoteMethodParameters = Array.Empty(); + var remoteMethodReturnTypes = Array.Empty(); + var actorProxMemberAccessSyntax = SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.ThisExpression(), + SyntaxFactory.IdentifierName("actorProxy") + ); + var expectedSource = $@"this.actorProxy.InvokeMethodAsync(""RemoteMethodToCall"")"; + var expectedSourceNormalized = SyntaxFactory.ParseSyntaxTree(expectedSource) + .GetRoot() + .NormalizeWhitespace() + .ToFullString(); + + // Act + var generatedSource = SyntaxFactoryHelpers.ActorProxyInvokeMethodAsync( + actorProxMemberAccessSyntax, + remoteMethodName, + remoteMethodParameters, + remoteMethodReturnTypes) + .SyntaxTree + .GetRoot() + .NormalizeWhitespace() + .ToFullString(); ; + + // Assert + Assert.Equal(expectedSourceNormalized, generatedSource); + } + + [Fact] + public void NameOfExpression() + { + // Arrange + var argumentName = "arg0"; + var expectedSource = $@"nameof(arg0)"; + var expectedSourceNormalized = SyntaxFactory.ParseSyntaxTree(expectedSource) + .GetRoot() + .NormalizeWhitespace() + .ToFullString(); + + // Act + var generatedSource = SyntaxFactoryHelpers.NameOfExpression(argumentName) + .SyntaxTree + .GetRoot() + .NormalizeWhitespace() + .ToFullString(); + + // Assert + Assert.Equal(expectedSourceNormalized, generatedSource); + } + + [Theory] + [InlineData(Accessibility.Public, new[] { SyntaxKind.PublicKeyword })] + [InlineData(Accessibility.Internal, new[] { SyntaxKind.InternalKeyword })] + [InlineData(Accessibility.Private, new[] { SyntaxKind.PrivateKeyword })] + [InlineData(Accessibility.Protected, new[] { SyntaxKind.ProtectedKeyword })] + [InlineData(Accessibility.ProtectedAndInternal, new[] { SyntaxKind.ProtectedKeyword, SyntaxKind.InternalKeyword })] + public void GetSyntaxKinds_GenerateSyntaxForGivenAccessibility(Accessibility accessibility, ICollection expectedSyntaxKinds) + { + // Arrange + + // Act + var generatedSyntaxKinds = SyntaxFactoryHelpers.GetSyntaxKinds(accessibility); + + // Assert + foreach (var expectedSyntaxKind in expectedSyntaxKinds) + { + Assert.Contains(expectedSyntaxKind, generatedSyntaxKinds); + } + + Assert.Equal(expectedSyntaxKinds.Count, generatedSyntaxKinds.Count); + } + } +}