diff --git a/src/Dapr.Workflow.Analyzers/AnalyzerReleases.Unshipped.md b/src/Dapr.Workflow.Analyzers/AnalyzerReleases.Unshipped.md index b1b99aaf2..6ff5c0f5b 100644 --- a/src/Dapr.Workflow.Analyzers/AnalyzerReleases.Unshipped.md +++ b/src/Dapr.Workflow.Analyzers/AnalyzerReleases.Unshipped.md @@ -1,3 +1,9 @@ ; Unshipped analyzer release ; https://github.com/dotnet/roslyn-analyzers/blob/main/src/Microsoft.CodeAnalysis.Analyzers/ReleaseTrackingAnalyzers.Help.md +### New Rules + +Rule ID | Category | Severity | Notes +--------|----------|----------|-------------------- +DAPR1303 | Usage | Warning | The provided input type does not match the target workflow or activity input type +DAPR1304 | Usage | Warning | The requested output type does not match the target workflow or activity output type diff --git a/src/Dapr.Workflow.Analyzers/CompilationExtensions.cs b/src/Dapr.Workflow.Analyzers/CompilationExtensions.cs new file mode 100644 index 000000000..a4cf449e3 --- /dev/null +++ b/src/Dapr.Workflow.Analyzers/CompilationExtensions.cs @@ -0,0 +1,27 @@ +using Microsoft.CodeAnalysis; + +namespace Dapr.Workflow.Analyzers; + +internal static class CompilationExtensions +{ + private const string DaprWorkflowClientMetadataName = "Dapr.Workflow.DaprWorkflowClient"; + private const string IDaprWorkflowClientMetadataName = "Dapr.Workflow.IDaprWorkflowClient"; + private const string WorkflowContextMetadataName = "Dapr.Workflow.WorkflowContext"; + private const string WorkflowBaseMetadataName = "Dapr.Workflow.Workflow`2"; + private const string WorkflowActivityBaseMetadataName = "Dapr.Workflow.WorkflowActivity`2"; + + internal static INamedTypeSymbol? GetDaprWorkflowClientType(this Compilation compilation) => + compilation.GetTypeByMetadataName(DaprWorkflowClientMetadataName); + + internal static INamedTypeSymbol? GetIDaprWorkflowClientType(this Compilation compilation) => + compilation.GetTypeByMetadataName(IDaprWorkflowClientMetadataName); + + internal static INamedTypeSymbol? GetWorkflowContextType(this Compilation compilation) => + compilation.GetTypeByMetadataName(WorkflowContextMetadataName); + + internal static INamedTypeSymbol? GetWorkflowBaseType(this Compilation compilation) => + compilation.GetTypeByMetadataName(WorkflowBaseMetadataName); + + internal static INamedTypeSymbol? GetWorkflowActivityBaseType(this Compilation compilation) => + compilation.GetTypeByMetadataName(WorkflowActivityBaseMetadataName); +} diff --git a/src/Dapr.Workflow.Analyzers/Resources.Designer.cs b/src/Dapr.Workflow.Analyzers/Resources.Designer.cs index 42e524df6..5566fe834 100644 --- a/src/Dapr.Workflow.Analyzers/Resources.Designer.cs +++ b/src/Dapr.Workflow.Analyzers/Resources.Designer.cs @@ -94,5 +94,41 @@ internal static string DAPR1302Title { return ResourceManager.GetString("DAPR1302Title", resourceCulture); } } + + /// + /// Looks up a localized string similar to The provided input type '{0}' does not match the expected input type '{1}' for {2} '{3}'. + /// + internal static string DAPR1303MessageFormat { + get { + return ResourceManager.GetString("DAPR1303MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The provided input type does not match the target workflow or activity input type. + /// + internal static string DAPR1303Title { + get { + return ResourceManager.GetString("DAPR1303Title", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The requested output type '{0}' does not match the declared output type '{1}' for {2} '{3}'. + /// + internal static string DAPR1304MessageFormat { + get { + return ResourceManager.GetString("DAPR1304MessageFormat", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The requested output type does not match the target workflow or activity output type. + /// + internal static string DAPR1304Title { + get { + return ResourceManager.GetString("DAPR1304Title", resourceCulture); + } + } } } diff --git a/src/Dapr.Workflow.Analyzers/Resources.resx b/src/Dapr.Workflow.Analyzers/Resources.resx index 12af45514..f332492bf 100644 --- a/src/Dapr.Workflow.Analyzers/Resources.resx +++ b/src/Dapr.Workflow.Analyzers/Resources.resx @@ -30,4 +30,16 @@ The workflow activity type '{0}' is not registered with the dependency injection provider - \ No newline at end of file + + The provided input type does not match the target workflow or activity input type + + + The provided input type '{0}' does not match the expected input type '{1}' for {2} '{3}' + + + The requested output type does not match the target workflow or activity output type + + + The requested output type '{0}' does not match the declared output type '{1}' for {2} '{3}' + + diff --git a/src/Dapr.Workflow.Analyzers/WorkflowTypeSafetyAnalyzer.cs b/src/Dapr.Workflow.Analyzers/WorkflowTypeSafetyAnalyzer.cs new file mode 100644 index 000000000..4af2f2ed4 --- /dev/null +++ b/src/Dapr.Workflow.Analyzers/WorkflowTypeSafetyAnalyzer.cs @@ -0,0 +1,445 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +namespace Dapr.Workflow.Analyzers; + +/// +/// Validates that the input and output types used with Dapr workflow and workflow activity calls +/// match the declared generic input/output types of the target workflow/activity type. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class WorkflowTypeSafetyAnalyzer : DiagnosticAnalyzer +{ + internal static readonly DiagnosticDescriptor InputTypeMismatchDescriptor = new( + id: "DAPR1303", + title: new LocalizableResourceString(nameof(Resources.DAPR1303Title), Resources.ResourceManager, typeof(Resources)), + messageFormat: new LocalizableResourceString(nameof(Resources.DAPR1303MessageFormat), Resources.ResourceManager, typeof(Resources)), + category: "Usage", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + internal static readonly DiagnosticDescriptor OutputTypeMismatchDescriptor = new( + id: "DAPR1304", + title: new LocalizableResourceString(nameof(Resources.DAPR1304Title), Resources.ResourceManager, typeof(Resources)), + messageFormat: new LocalizableResourceString(nameof(Resources.DAPR1304MessageFormat), Resources.ResourceManager, typeof(Resources)), + category: "Usage", + defaultSeverity: DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + /// + /// Gets the diagnostics supported by this analyzer. + /// + public override ImmutableArray SupportedDiagnostics => + [ + InputTypeMismatchDescriptor, + OutputTypeMismatchDescriptor + ]; + + /// + /// Initializes analyzer actions. + /// + /// The analysis context. + public override void Initialize(AnalysisContext context) + { + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + context.EnableConcurrentExecution(); + + context.RegisterCompilationStartAction(static compilationStartContext => + { + var compilation = compilationStartContext.Compilation; + + var daprWorkflowClientType = compilation.GetDaprWorkflowClientType(); + var iDaprWorkflowClientType = compilation.GetIDaprWorkflowClientType(); + var workflowContextType = compilation.GetWorkflowContextType(); + var workflowBaseType = compilation.GetWorkflowBaseType(); + var workflowActivityBaseType = compilation.GetWorkflowActivityBaseType(); + + if (daprWorkflowClientType is null || + iDaprWorkflowClientType is null || + workflowContextType is null || + workflowBaseType is null || + workflowActivityBaseType is null) + { + return; + } + + compilationStartContext.RegisterOperationAction( + operationContext => AnalyzeInvocation( + operationContext, + daprWorkflowClientType, + iDaprWorkflowClientType, + workflowContextType, + workflowBaseType, + workflowActivityBaseType), + OperationKind.Invocation); + }); + } + + private static void AnalyzeInvocation( + OperationAnalysisContext context, + INamedTypeSymbol daprWorkflowClientType, + INamedTypeSymbol iDaprWorkflowClientType, + INamedTypeSymbol workflowContextType, + INamedTypeSymbol workflowBaseType, + INamedTypeSymbol workflowActivityBaseType) + { + context.CancellationToken.ThrowIfCancellationRequested(); + + var invocation = (IInvocationOperation)context.Operation; + var targetMethod = invocation.TargetMethod.ReducedFrom ?? invocation.TargetMethod; + + if (IsScheduleNewWorkflowAsync(targetMethod, daprWorkflowClientType, iDaprWorkflowClientType)) + { + AnalyzeWorkflowInput(invocation, context, workflowBaseType); + return; + } + + if (IsCallChildWorkflowAsync(targetMethod, workflowContextType)) + { + AnalyzeWorkflowInput(invocation, context, workflowBaseType); + AnalyzeWorkflowOutput(invocation, context, workflowBaseType); + return; + } + + if (IsCallActivityAsync(targetMethod, workflowContextType)) + { + AnalyzeActivityInput(invocation, context, workflowActivityBaseType); + AnalyzeActivityOutput(invocation, context, workflowActivityBaseType); + } + } + + private static bool IsScheduleNewWorkflowAsync( + IMethodSymbol method, + INamedTypeSymbol daprWorkflowClientType, + INamedTypeSymbol iDaprWorkflowClientType) => + method.Name == "ScheduleNewWorkflowAsync" && + (SymbolEqualityComparer.Default.Equals(method.ContainingType, daprWorkflowClientType) || + SymbolEqualityComparer.Default.Equals(method.ContainingType, iDaprWorkflowClientType)); + + private static bool IsCallChildWorkflowAsync( + IMethodSymbol method, + INamedTypeSymbol workflowContextType) => + method.Name == "CallChildWorkflowAsync" && + SymbolEqualityComparer.Default.Equals(method.ContainingType, workflowContextType); + + private static bool IsCallActivityAsync( + IMethodSymbol method, + INamedTypeSymbol workflowContextType) => + method.Name == "CallActivityAsync" && + SymbolEqualityComparer.Default.Equals(method.ContainingType, workflowContextType); + + private static void AnalyzeWorkflowInput( + IInvocationOperation invocation, + OperationAnalysisContext context, + INamedTypeSymbol workflowBaseType) + { + context.CancellationToken.ThrowIfCancellationRequested(); + + if (!TryGetTargetTypeFromNameof(invocation, "name", context.CancellationToken, out var workflowType) && + !TryGetTargetTypeFromNameof(invocation, "workflowName", context.CancellationToken, out workflowType)) + { + return; + } + + if (!TryGetGenericArgumentsFromBaseType(workflowType, workflowBaseType, out var expectedInputType, out _)) + { + return; + } + + var inputArgument = GetArgument(invocation, "input"); + if (inputArgument is null || + !TryGetExpressionType(inputArgument.Value, out var actualInputType)) + { + return; + } + + if (IsCompatible(actualInputType, expectedInputType, context.Compilation)) + { + return; + } + + context.ReportDiagnostic(Diagnostic.Create( + InputTypeMismatchDescriptor, + inputArgument.Syntax.GetLocation(), + ToDisplayString(actualInputType), + ToDisplayString(expectedInputType), + "workflow", + workflowType.Name)); + } + + private static void AnalyzeWorkflowOutput( + IInvocationOperation invocation, + OperationAnalysisContext context, + INamedTypeSymbol workflowBaseType) + { + context.CancellationToken.ThrowIfCancellationRequested(); + + if (!TryGetTargetTypeFromNameof(invocation, "workflowName", context.CancellationToken, out var workflowType)) + { + return; + } + + if (!TryGetGenericArgumentsFromBaseType(workflowType, workflowBaseType, out _, out var declaredOutputType)) + { + return; + } + + if (!TryGetRequestedOutputType(invocation.TargetMethod, out var requestedOutputType)) + { + return; + } + + if (IsCompatible(declaredOutputType, requestedOutputType, context.Compilation)) + { + return; + } + + context.ReportDiagnostic(Diagnostic.Create( + OutputTypeMismatchDescriptor, + GetInvocationNameLocation(invocation), + ToDisplayString(requestedOutputType), + ToDisplayString(declaredOutputType), + "workflow", + workflowType.Name)); + } + + private static void AnalyzeActivityInput( + IInvocationOperation invocation, + OperationAnalysisContext context, + INamedTypeSymbol workflowActivityBaseType) + { + context.CancellationToken.ThrowIfCancellationRequested(); + + if (!TryGetTargetTypeFromNameof(invocation, "name", context.CancellationToken, out var activityType)) + { + return; + } + + if (!TryGetGenericArgumentsFromBaseType(activityType, workflowActivityBaseType, out var expectedInputType, out _)) + { + return; + } + + var inputArgument = GetArgument(invocation, "input"); + if (inputArgument is null || + !TryGetExpressionType(inputArgument.Value, out var actualInputType)) + { + return; + } + + if (IsCompatible(actualInputType, expectedInputType, context.Compilation)) + { + return; + } + + context.ReportDiagnostic(Diagnostic.Create( + InputTypeMismatchDescriptor, + inputArgument.Syntax.GetLocation(), + ToDisplayString(actualInputType), + ToDisplayString(expectedInputType), + "workflow activity", + activityType.Name)); + } + + private static void AnalyzeActivityOutput( + IInvocationOperation invocation, + OperationAnalysisContext context, + INamedTypeSymbol workflowActivityBaseType) + { + context.CancellationToken.ThrowIfCancellationRequested(); + + if (!TryGetTargetTypeFromNameof(invocation, "name", context.CancellationToken, out var activityType)) + { + return; + } + + if (!TryGetGenericArgumentsFromBaseType(activityType, workflowActivityBaseType, out _, out var declaredOutputType)) + { + return; + } + + if (!TryGetRequestedOutputType(invocation.TargetMethod, out var requestedOutputType)) + { + return; + } + + if (IsCompatible(declaredOutputType, requestedOutputType, context.Compilation)) + { + return; + } + + context.ReportDiagnostic(Diagnostic.Create( + OutputTypeMismatchDescriptor, + GetInvocationNameLocation(invocation), + ToDisplayString(requestedOutputType), + ToDisplayString(declaredOutputType), + "workflow activity", + activityType.Name)); + } + + private static bool TryGetTargetTypeFromNameof( + IInvocationOperation invocation, + string parameterName, + CancellationToken cancellationToken, + out INamedTypeSymbol targetType) + { + cancellationToken.ThrowIfCancellationRequested(); + + targetType = null!; + var argument = GetArgument(invocation, parameterName); + if (argument is null) + { + return false; + } + + return TryGetTypeFromNameof(argument.Value, cancellationToken, out targetType); + } + + private static bool TryGetTypeFromNameof( + IOperation operation, + CancellationToken cancellationToken, + out INamedTypeSymbol targetType) + { + cancellationToken.ThrowIfCancellationRequested(); + + targetType = null!; + + if (operation is not INameOfOperation nameOfOperation) + { + return false; + } + + var argumentOperation = nameOfOperation.Argument; + var semanticModel = argumentOperation.SemanticModel; + if (semanticModel is null) + { + return false; + } + + var symbolInfo = semanticModel.GetSymbolInfo(argumentOperation.Syntax, cancellationToken).Symbol; + if (symbolInfo is INamedTypeSymbol namedTypeFromSymbol) + { + targetType = namedTypeFromSymbol; + return true; + } + + if (argumentOperation.Type is INamedTypeSymbol namedType) + { + targetType = namedType; + return true; + } + + return false; + } + + private static IArgumentOperation? GetArgument( + IInvocationOperation invocation, + string parameterName) + { + foreach (var argument in invocation.Arguments) + { + if (argument.Parameter?.Name == parameterName) + { + return argument; + } + } + + return null; + } + + private static bool TryGetGenericArgumentsFromBaseType( + INamedTypeSymbol candidateType, + INamedTypeSymbol genericBaseDefinition, + out ITypeSymbol inputType, + out ITypeSymbol outputType) + { + inputType = null!; + outputType = null!; + + for (INamedTypeSymbol? current = candidateType; current is not null; current = current.BaseType) + { + if (current.IsGenericType && + SymbolEqualityComparer.Default.Equals(current.OriginalDefinition, genericBaseDefinition)) + { + inputType = current.TypeArguments[0]; + outputType = current.TypeArguments[1]; + return true; + } + } + + return false; + } + + private static bool TryGetRequestedOutputType( + IMethodSymbol methodSymbol, + out ITypeSymbol requestedOutputType) + { + requestedOutputType = null!; + + if (!methodSymbol.IsGenericMethod || methodSymbol.TypeArguments.Length != 1) + { + return false; + } + + requestedOutputType = methodSymbol.TypeArguments[0]; + return true; + } + + private static bool TryGetExpressionType( + IOperation operation, + out ITypeSymbol expressionType) + { + expressionType = null!; + + if (operation.ConstantValue.HasValue && operation.ConstantValue.Value is null) + { + return false; + } + + if (operation is IConversionOperation { IsImplicit: true, Operand: { } operand }) + { + if (operand.ConstantValue.HasValue && operand.ConstantValue.Value is null) + { + return false; + } + + expressionType = operand.Type!; + return expressionType is not null; + } + + expressionType = operation.Type!; + return expressionType is not null; + } + + private static bool IsCompatible( + ITypeSymbol actualType, + ITypeSymbol expectedType, + Compilation compilation) + { + if (SymbolEqualityComparer.Default.Equals(actualType, expectedType)) + { + return true; + } + + var conversion = compilation.ClassifyConversion(actualType, expectedType); + return conversion.Exists && conversion.IsImplicit; + } + + private static Location GetInvocationNameLocation(IInvocationOperation invocation) + { + return invocation.Syntax switch + { + Microsoft.CodeAnalysis.CSharp.Syntax.InvocationExpressionSyntax + { + Expression: Microsoft.CodeAnalysis.CSharp.Syntax.MemberAccessExpressionSyntax memberAccess + } => memberAccess.Name.GetLocation(), + _ => invocation.Syntax.GetLocation() + }; + } + + private static string ToDisplayString(ITypeSymbol typeSymbol) => + typeSymbol.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat); +} diff --git a/test/Dapr.Workflow.Analyzers.Test/Utilities.cs b/test/Dapr.Workflow.Analyzers.Test/Utilities.cs index 0b971489a..3845dd160 100644 --- a/test/Dapr.Workflow.Analyzers.Test/Utilities.cs +++ b/test/Dapr.Workflow.Analyzers.Test/Utilities.cs @@ -2,6 +2,7 @@ using Microsoft.CodeAnalysis.Diagnostics; using System.Collections.Immutable; using Dapr.Analyzers.Common; +using Dapr.Common; using Microsoft.Extensions.Hosting; namespace Dapr.Workflow.Analyzers.Test; @@ -11,16 +12,19 @@ internal static class Utilities internal static ImmutableArray GetAnalyzers() => [ new WorkflowRegistrationAnalyzer(), - new WorkflowActivityRegistrationAnalyzer() + new WorkflowActivityRegistrationAnalyzer(), + new WorkflowTypeSafetyAnalyzer() ]; internal static IReadOnlyList GetReferences() { var metadataReferences = TestUtilities.GetAllReferencesNeededForType(typeof(WorkflowActivityRegistrationAnalyzer)).ToList(); metadataReferences.AddRange(TestUtilities.GetAllReferencesNeededForType(typeof(WorkflowRegistrationAnalyzer))); + metadataReferences.AddRange(TestUtilities.GetAllReferencesNeededForType(typeof(WorkflowTypeSafetyAnalyzer))); metadataReferences.AddRange(TestUtilities.GetAllReferencesNeededForType(typeof(TimeSpan))); metadataReferences.AddRange(TestUtilities.GetAllReferencesNeededForType(typeof(Workflow<,>))); metadataReferences.AddRange(TestUtilities.GetAllReferencesNeededForType(typeof(WorkflowActivity<,>))); + metadataReferences.AddRange(TestUtilities.GetAllReferencesNeededForType(typeof(IDaprClient))); metadataReferences.Add(MetadataReference.CreateFromFile(typeof(Task).Assembly.Location)); metadataReferences.Add(MetadataReference.CreateFromFile(typeof(DaprWorkflowClient).Assembly.Location)); metadataReferences.Add(MetadataReference.CreateFromFile(typeof(Microsoft.Extensions.DependencyInjection.ServiceCollection).Assembly.Location)); diff --git a/test/Dapr.Workflow.Analyzers.Test/WorkflowActivityRegistrationCodeFixProviderTests.cs b/test/Dapr.Workflow.Analyzers.Test/WorkflowActivityRegistrationCodeFixProviderTests.cs index 9944a578e..109353883 100644 --- a/test/Dapr.Workflow.Analyzers.Test/WorkflowActivityRegistrationCodeFixProviderTests.cs +++ b/test/Dapr.Workflow.Analyzers.Test/WorkflowActivityRegistrationCodeFixProviderTests.cs @@ -24,7 +24,7 @@ public static void Main() }); } } - + class OrderProcessingWorkflow : Workflow { public override async Task RunAsync(WorkflowContext context, OrderPayload order) @@ -37,9 +37,9 @@ public override async Task RunAsync(WorkflowContext context, OrderP record OrderPayload; record OrderResult(string Message) { }; record Notification { public Notification(string message) { } }; - internal sealed class NotifyActivity : WorkflowActivity + internal sealed class NotifyActivity : WorkflowActivity { - public override async Task RunAsync(WorkflowActivityContext context, string input) + public override async Task RunAsync(WorkflowActivityContext context, Notification input) { await Task.Delay(TimeSpan.FromSeconds(15)); return true; @@ -78,9 +78,9 @@ public override async Task RunAsync(WorkflowContext context, OrderP record OrderPayload; record OrderResult(string Message) { }; record Notification { public Notification(string message) { } }; - internal sealed class NotifyActivity : WorkflowActivity + internal sealed class NotifyActivity : WorkflowActivity { - public override async Task RunAsync(WorkflowActivityContext context, string input) + public override async Task RunAsync(WorkflowActivityContext context, Notification input) { await Task.Delay(TimeSpan.FromSeconds(15)); return true; diff --git a/test/Dapr.Workflow.Analyzers.Test/WorkflowTypeSafetyAnalyzerTests.cs b/test/Dapr.Workflow.Analyzers.Test/WorkflowTypeSafetyAnalyzerTests.cs new file mode 100644 index 000000000..69951ae58 --- /dev/null +++ b/test/Dapr.Workflow.Analyzers.Test/WorkflowTypeSafetyAnalyzerTests.cs @@ -0,0 +1,554 @@ +using Dapr.Analyzers.Common; + +namespace Dapr.Workflow.Analyzers.Test; + +public sealed class WorkflowTypeSafetyAnalyzerTests +{ + [Fact] + public async Task VerifyWorkflowInputTypeMismatch() + { + const string testCode = """ + using Dapr.Workflow; + using System.Threading.Tasks; + + public sealed class OrderWorkflow : Workflow + { + public override Task RunAsync(WorkflowContext context, int input) => Task.FromResult(input.ToString()); + } + + public sealed class WorkflowStarter + { + public Task StartAsync(DaprWorkflowClient client) + { + return client.ScheduleNewWorkflowAsync(nameof(OrderWorkflow), input: "wrong"); + } + } + """; + + var expected = VerifyAnalyzer.Diagnostic(WorkflowTypeSafetyAnalyzer.InputTypeMismatchDescriptor) + .WithSpan(13, 71, 13, 85) + .WithMessage( + "The provided input type 'string' does not match the expected input type 'int' for workflow 'OrderWorkflow'"); + + var analyzer = new VerifyAnalyzer(Utilities.GetReferences()); + await analyzer.VerifyAnalyzerAsync(testCode, expected); + } + + [Fact] + public async Task VerifyWorkflowInputTypeMismatch_WhenUsingWorkflowClientInterface() + { + const string testCode = """ + using Dapr.Workflow; + using System.Threading.Tasks; + + public sealed class OrderWorkflow : Workflow + { + public override Task RunAsync(WorkflowContext context, int input) => Task.FromResult(input.ToString()); + } + + public sealed class WorkflowStarter + { + public Task StartAsync(IDaprWorkflowClient client) + { + return client.ScheduleNewWorkflowAsync(nameof(OrderWorkflow), input: "wrong"); + } + } + """; + + var expected = VerifyAnalyzer.Diagnostic(WorkflowTypeSafetyAnalyzer.InputTypeMismatchDescriptor) + .WithSpan(13, 71, 13, 85) + .WithMessage( + "The provided input type 'string' does not match the expected input type 'int' for workflow 'OrderWorkflow'"); + + var analyzer = new VerifyAnalyzer(Utilities.GetReferences()); + await analyzer.VerifyAnalyzerAsync(testCode, expected); + } + + [Fact] + public async Task VerifyActivityInputTypeMismatch() + { + const string testCode = """ + using Dapr.Workflow; + using System.Threading.Tasks; + + public sealed class NotifyActivity : WorkflowActivity + { + public override Task RunAsync(WorkflowActivityContext context, int input) => Task.FromResult(input.ToString()); + } + + public sealed class ParentWorkflow : Workflow + { + public override async Task RunAsync(WorkflowContext context, string input) + { + return await context.CallActivityAsync(nameof(NotifyActivity), "wrong"); + } + } + """; + + var expected = VerifyAnalyzer.Diagnostic(WorkflowTypeSafetyAnalyzer.InputTypeMismatchDescriptor) + .WithSpan(13, 80, 13, 87) + .WithMessage( + "The provided input type 'string' does not match the expected input type 'int' for workflow activity 'NotifyActivity'"); + + var analyzer = new VerifyAnalyzer(Utilities.GetReferences()); + await analyzer.VerifyAnalyzerAsync(testCode, expected); + } + + [Fact] + public async Task VerifyActivityOutputTypeMismatch() + { + const string testCode = """ + using Dapr.Workflow; + using System.Threading.Tasks; + + public sealed class NotifyActivity : WorkflowActivity + { + public override Task RunAsync(WorkflowActivityContext context, int input) => Task.FromResult(input.ToString()); + } + + public sealed class ParentWorkflow : Workflow + { + public override async Task RunAsync(WorkflowContext context, string input) + { + int result = await context.CallActivityAsync(nameof(NotifyActivity), 42); + return result.ToString(); + } + } + """; + + var expected = VerifyAnalyzer.Diagnostic(WorkflowTypeSafetyAnalyzer.OutputTypeMismatchDescriptor) + .WithSpan(13, 36, 13, 58) + .WithMessage( + "The requested output type 'int' does not match the declared output type 'string' for workflow activity 'NotifyActivity'"); + + var analyzer = new VerifyAnalyzer(Utilities.GetReferences()); + await analyzer.VerifyAnalyzerAsync(testCode, expected); + } + + [Fact] + public async Task VerifyChildWorkflowOutputTypeMismatch() + { + const string testCode = """ + using Dapr.Workflow; + using System.Threading.Tasks; + + public sealed class ChildWorkflow : Workflow + { + public override Task RunAsync(WorkflowContext context, int input) => Task.FromResult(input.ToString()); + } + + public sealed class ParentWorkflow : Workflow + { + public override async Task RunAsync(WorkflowContext context, int input) + { + var result = await context.CallChildWorkflowAsync(nameof(ChildWorkflow), input); + return result.ToString(); + } + } + """; + + var expected = VerifyAnalyzer.Diagnostic(WorkflowTypeSafetyAnalyzer.OutputTypeMismatchDescriptor) + .WithSpan(13, 36, 13, 63) + .WithMessage( + "The requested output type 'int' does not match the declared output type 'string' for workflow 'ChildWorkflow'"); + + var analyzer = new VerifyAnalyzer(Utilities.GetReferences()); + await analyzer.VerifyAnalyzerAsync(testCode, expected); + } + + [Fact] + public async Task VerifyCompatibleTypesDoNotReport() + { + const string testCode = """ + using Dapr.Workflow; + using System.Threading.Tasks; + + public class Notification + { + } + + public sealed class ImportantNotification : Notification + { + } + + public sealed class NotifyActivity : WorkflowActivity + { + public override Task RunAsync(WorkflowActivityContext context, Notification input) => + Task.FromResult(new ImportantNotification()); + } + + public sealed class ChildWorkflow : Workflow + { + public override Task RunAsync(WorkflowContext context, Notification input) => + Task.FromResult(new ImportantNotification()); + } + + public sealed class ParentWorkflow : Workflow + { + public override async Task RunAsync(WorkflowContext context, ImportantNotification input) + { + Notification activityResult = await context.CallActivityAsync(nameof(NotifyActivity), input); + Notification childResult = await context.CallChildWorkflowAsync(nameof(ChildWorkflow), input); + return activityResult ?? childResult; + } + } + + public sealed class WorkflowStarter + { + public Task StartAsync(DaprWorkflowClient client, ImportantNotification input) + { + return client.ScheduleNewWorkflowAsync(nameof(ChildWorkflow), input: input); + } + } + """; + + var analyzer = new VerifyAnalyzer(Utilities.GetReferences()); + await analyzer.VerifyAnalyzerAsync(testCode); + } + + [Fact] + public async Task VerifyWorkflowInputMismatch_WithStringLiteralName_DoesNotReport() + { + const string testCode = """ + using Dapr.Workflow; + using System.Threading.Tasks; + + public sealed class OrderWorkflow : Workflow + { + public override Task RunAsync(WorkflowContext context, int input) => Task.FromResult(input.ToString()); + } + + public sealed class WorkflowStarter + { + public Task StartAsync(DaprWorkflowClient client) + { + return client.ScheduleNewWorkflowAsync("OrderWorkflow", input: "wrong"); + } + } + """; + + var analyzer = new VerifyAnalyzer(Utilities.GetReferences()); + await analyzer.VerifyAnalyzerAsync(testCode); + } + + [Fact] + public async Task VerifyActivityInputMismatch_WithConstStringName_DoesNotReport() + { + const string testCode = """ + using Dapr.Workflow; + using System.Threading.Tasks; + + public sealed class NotifyActivity : WorkflowActivity + { + public override Task RunAsync(WorkflowActivityContext context, int input) => Task.FromResult(input.ToString()); + } + + public sealed class ParentWorkflow : Workflow + { + private const string ActivityName = "NotifyActivity"; + + public override async Task RunAsync(WorkflowContext context, string input) + { + return await context.CallActivityAsync(ActivityName, "wrong"); + } + } + """; + + var analyzer = new VerifyAnalyzer(Utilities.GetReferences()); + await analyzer.VerifyAnalyzerAsync(testCode); + } + + [Fact] + public async Task VerifyWorkflowInputNull_DoesNotReport() + { + const string testCode = """ + using Dapr.Workflow; + using System.Threading.Tasks; + + public sealed class OrderWorkflow : Workflow + { + public override Task RunAsync(WorkflowContext context, int input) => Task.FromResult(input.ToString()); + } + + public sealed class WorkflowStarter + { + public Task StartAsync(DaprWorkflowClient client) + { + return client.ScheduleNewWorkflowAsync(nameof(OrderWorkflow), input: null); + } + } + """; + + var analyzer = new VerifyAnalyzer(Utilities.GetReferences()); + await analyzer.VerifyAnalyzerAsync(testCode); + } + + [Fact] + public async Task VerifyUnrelatedScheduleNewWorkflowAsync_DoesNotReport() + { + const string testCode = """ + using System.Threading.Tasks; + + public sealed class OrderWorkflow + { + } + + public sealed class FakeClient + { + public Task ScheduleNewWorkflowAsync(string name, object input) => Task.CompletedTask; + } + + public sealed class WorkflowStarter + { + public Task StartAsync(FakeClient client) + { + return client.ScheduleNewWorkflowAsync(nameof(OrderWorkflow), "wrong"); + } + } + """; + + var analyzer = new VerifyAnalyzer(Utilities.GetReferences()); + await analyzer.VerifyAnalyzerAsync(testCode); + } + + [Fact] + public async Task VerifyUnrelatedCallActivityAsync_DoesNotReport() + { + const string testCode = """ + using System.Threading.Tasks; + + public sealed class FakeContext + { + public Task CallActivityAsync(string name, object input) => Task.FromResult(default(T)!); + } + + public sealed class ParentWorkflow + { + public async Task RunAsync(FakeContext context) + { + return await context.CallActivityAsync(nameof(ParentWorkflow), "wrong"); + } + } + """; + + var analyzer = new VerifyAnalyzer(Utilities.GetReferences()); + await analyzer.VerifyAnalyzerAsync(testCode); + } + + [Fact] + public async Task VerifyUnrelatedCallChildWorkflowAsync_DoesNotReport() + { + const string testCode = """ + using System.Threading.Tasks; + + public sealed class FakeContext + { + public Task CallChildWorkflowAsync(string workflowName, object input) => Task.FromResult(default(T)!); + } + + public sealed class ChildWorkflow + { + } + + public sealed class ParentWorkflow + { + public async Task RunAsync(FakeContext context, int input) + { + var result = await context.CallChildWorkflowAsync(nameof(ChildWorkflow), input); + return result.ToString(); + } + } + """; + + var analyzer = new VerifyAnalyzer(Utilities.GetReferences()); + await analyzer.VerifyAnalyzerAsync(testCode); + } + + [Fact] + public async Task VerifyActivityOutputDerivedRequestedFromBaseDeclared_Reports() + { + const string testCode = """ + using Dapr.Workflow; + using System.Threading.Tasks; + + public class Notification + { + } + + public sealed class ImportantNotification : Notification + { + } + + public sealed class NotifyActivity : WorkflowActivity + { + public override Task RunAsync(WorkflowActivityContext context, int input) => + Task.FromResult(new Notification()); + } + + public sealed class ParentWorkflow : Workflow + { + public override async Task RunAsync(WorkflowContext context, string input) + { + ImportantNotification result = await context.CallActivityAsync(nameof(NotifyActivity), 42); + return result.ToString(); + } + } + """; + + var expected = VerifyAnalyzer.Diagnostic(WorkflowTypeSafetyAnalyzer.OutputTypeMismatchDescriptor) + .WithSpan(22, 54, 22, 94) + .WithMessage( + "The requested output type 'ImportantNotification' does not match the declared output type 'Notification' for workflow activity 'NotifyActivity'"); + + var analyzer = new VerifyAnalyzer(Utilities.GetReferences()); + await analyzer.VerifyAnalyzerAsync(testCode, expected); + } + + [Fact] + public async Task VerifyWorkflowInputCompatibleBaseType_DoesNotReport() + { + const string testCode = """ + using Dapr.Workflow; + using System.Threading.Tasks; + + public class Notification + { + } + + public sealed class ImportantNotification : Notification + { + } + + public sealed class ChildWorkflow : Workflow + { + public override Task RunAsync(WorkflowContext context, Notification input) => + Task.FromResult("ok"); + } + + public sealed class WorkflowStarter + { + public Task StartAsync(DaprWorkflowClient client, ImportantNotification input) + { + return client.ScheduleNewWorkflowAsync(nameof(ChildWorkflow), input: input); + } + } + """; + + var analyzer = new VerifyAnalyzer(Utilities.GetReferences()); + await analyzer.VerifyAnalyzerAsync(testCode); + } + + [Fact] + public async Task VerifyWorkflowTupleInputCompatible_DoesNotReport() + { + const string testCode = """ + using Dapr.Workflow; + using System.Threading.Tasks; + + public sealed class OrderWorkflow : Workflow<(int Id, string Name), string> + { + public override Task RunAsync(WorkflowContext context, (int Id, string Name) input) + => Task.FromResult(input.Name); + } + + public sealed class WorkflowStarter + { + public Task StartAsync(DaprWorkflowClient client) + { + return client.ScheduleNewWorkflowAsync(nameof(OrderWorkflow), input: (1, "test")); + } + } + """; + + var analyzer = new VerifyAnalyzer(Utilities.GetReferences()); + await analyzer.VerifyAnalyzerAsync(testCode); + } + + [Fact] + public async Task VerifyWorkflowTupleInputMismatch_Reports() + { + const string testCode = """ + using Dapr.Workflow; + using System.Threading.Tasks; + + public sealed class OrderWorkflow : Workflow<(int Id, string Name), string> + { + public override Task RunAsync(WorkflowContext context, (int Id, string Name) input) + => Task.FromResult(input.Name); + } + + public sealed class WorkflowStarter + { + public Task StartAsync(DaprWorkflowClient client) + { + return client.ScheduleNewWorkflowAsync(nameof(OrderWorkflow), input: ("wrong", "test")); + } + } + """; + + var expected = VerifyAnalyzer.Diagnostic(WorkflowTypeSafetyAnalyzer.InputTypeMismatchDescriptor) + .WithSpan(14, 71, 14, 95) + .WithMessage("The provided input type '(string, string)' does not match the expected input type '(int Id, string Name)' for workflow 'OrderWorkflow'"); + + var analyzer = new VerifyAnalyzer(Utilities.GetReferences()); + await analyzer.VerifyAnalyzerAsync(testCode, expected); + } + + [Fact] + public async Task VerifyActivityTupleOutputCompatible_DoesNotReport() + { + const string testCode = """ + using Dapr.Workflow; + using System.Threading.Tasks; + + public sealed class NotifyActivity : WorkflowActivity + { + public override Task<(int Code, string Message)> RunAsync(WorkflowActivityContext context, int input) + => Task.FromResult((input, "ok")); + } + + public sealed class ParentWorkflow : Workflow + { + public override async Task RunAsync(WorkflowContext context, string input) + { + var result = await context.CallActivityAsync<(int Code, string Message)>(nameof(NotifyActivity), 42); + return result.Message; + } + } + """; + + var analyzer = new VerifyAnalyzer(Utilities.GetReferences()); + await analyzer.VerifyAnalyzerAsync(testCode); + } + + [Fact] + public async Task VerifyActivityTupleOutputMismatch_Reports() + { + const string testCode = """ + using Dapr.Workflow; + using System.Threading.Tasks; + + public sealed class NotifyActivity : WorkflowActivity + { + public override Task<(int Code, string Message)> RunAsync(WorkflowActivityContext context, int input) + => Task.FromResult((input, "ok")); + } + + public sealed class ParentWorkflow : Workflow + { + public override async Task RunAsync(WorkflowContext context, string input) + { + var result = await context.CallActivityAsync<(string Code, string Message)>(nameof(NotifyActivity), 42); + return result.Message; + } + } + """; + + var expected = VerifyAnalyzer.Diagnostic(WorkflowTypeSafetyAnalyzer.OutputTypeMismatchDescriptor) + .WithSpan(14, 36, 14, 84) + .WithMessage("The requested output type '(string Code, string Message)' does not match the declared output type '(int Code, string Message)' for workflow activity 'NotifyActivity'"); + + var analyzer = new VerifyAnalyzer(Utilities.GetReferences()); + await analyzer.VerifyAnalyzerAsync(testCode, expected); + } +}