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);
+ }
+ }
+}