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