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