diff --git a/src/Analyzers/AnalyzerReleases.Unshipped.md b/src/Analyzers/AnalyzerReleases.Unshipped.md index 690cd6a4..c2109dea 100644 --- a/src/Analyzers/AnalyzerReleases.Unshipped.md +++ b/src/Analyzers/AnalyzerReleases.Unshipped.md @@ -8,6 +8,9 @@ Rule ID | Category | Severity | Notes DURABLE0001 | Orchestration | Warning | DateTimeOrchestrationAnalyzer DURABLE0002 | Orchestration | Warning | GuidOrchestrationAnalyzer DURABLE0003 | Orchestration | Warning | DelayOrchestrationAnalyzer +DURABLE0004 | Orchestration | Warning | ThreadTaskOrchestrationAnalyzer +DURABLE0005 | Orchestration | Warning | IOOrchestrationAnalyzer +DURABLE0006 | Orchestration | Warning | EnvironmentOrchestrationAnalyzer DURABLE0007 | Orchestration | Warning | CancellationTokenOrchestrationAnalyzer DURABLE0008 | Orchestration | Warning | OtherBindingsOrchestrationAnalyzer DURABLE1001 | Attribute Binding | Error | OrchestrationTriggerBindingAnalyzer diff --git a/src/Analyzers/KnownTypeSymbols.Azure.cs b/src/Analyzers/KnownTypeSymbols.Azure.cs new file mode 100644 index 00000000..fb0ce41b --- /dev/null +++ b/src/Analyzers/KnownTypeSymbols.Azure.cs @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.CodeAnalysis; + +namespace Microsoft.DurableTask.Analyzers; + +/// +/// Provides a set of well-known types that are used by the analyzers. +/// Inspired by KnownTypeSymbols class in +/// System.Text.Json.SourceGeneration source code. +/// Lazy initialization is used to avoid the the initialization of all types during class construction, since not all symbols are used by all analyzers. +/// +public sealed partial class KnownTypeSymbols +{ + INamedTypeSymbol? blobServiceClient; + INamedTypeSymbol? blobContainerClient; + INamedTypeSymbol? blobClient; + INamedTypeSymbol? queueServiceClient; + INamedTypeSymbol? queueClient; + INamedTypeSymbol? tableServiceClient; + INamedTypeSymbol? tableClient; + INamedTypeSymbol? cosmosClient; + INamedTypeSymbol? sqlConnection; + + /// + /// Gets a BlobServiceClient type symbol. + /// + public INamedTypeSymbol? BlobServiceClient => this.GetOrResolveFullyQualifiedType("Azure.Storage.Blobs.BlobServiceClient", ref this.blobServiceClient); + + /// + /// Gets a BlobContainerClient type symbol. + /// + public INamedTypeSymbol? BlobContainerClient => this.GetOrResolveFullyQualifiedType("Azure.Storage.Blobs.BlobContainerClient", ref this.blobContainerClient); + + /// + /// Gets a BlobClient type symbol. + /// + public INamedTypeSymbol? BlobClient => this.GetOrResolveFullyQualifiedType("Azure.Storage.Blobs.BlobClient", ref this.blobClient); + + /// + /// Gets a QueueServiceClient type symbol. + /// + public INamedTypeSymbol? QueueServiceClient => this.GetOrResolveFullyQualifiedType("Azure.Storage.Queues.QueueServiceClient", ref this.queueServiceClient); + + /// + /// Gets a QueueClient type symbol. + /// + public INamedTypeSymbol? QueueClient => this.GetOrResolveFullyQualifiedType("Azure.Storage.Queues.QueueClient", ref this.queueClient); + + /// + /// Gets a TableServiceClient type symbol. + /// + public INamedTypeSymbol? TableServiceClient => this.GetOrResolveFullyQualifiedType("Azure.Data.Tables.TableServiceClient", ref this.tableServiceClient); + + /// + /// Gets a TableClient type symbol. + /// + public INamedTypeSymbol? TableClient => this.GetOrResolveFullyQualifiedType("Azure.Data.Tables.TableClient", ref this.tableClient); + + /// + /// Gets a CosmosClient type symbol. + /// + public INamedTypeSymbol? CosmosClient => this.GetOrResolveFullyQualifiedType("Microsoft.Azure.Cosmos.CosmosClient", ref this.cosmosClient); + + /// + /// Gets a SqlConnection type symbol. + /// + public INamedTypeSymbol? SqlConnection => this.GetOrResolveFullyQualifiedType("Microsoft.Data.SqlClient.SqlConnection", ref this.sqlConnection); +} diff --git a/src/Analyzers/KnownTypeSymbols.Net.cs b/src/Analyzers/KnownTypeSymbols.Net.cs index 1e2ebc4a..9803273a 100644 --- a/src/Analyzers/KnownTypeSymbols.Net.cs +++ b/src/Analyzers/KnownTypeSymbols.Net.cs @@ -17,7 +17,12 @@ public sealed partial class KnownTypeSymbols INamedTypeSymbol? thread; INamedTypeSymbol? task; INamedTypeSymbol? taskT; + INamedTypeSymbol? taskFactory; + INamedTypeSymbol? taskContinuationOptions; + INamedTypeSymbol? taskFactoryT; INamedTypeSymbol? cancellationToken; + INamedTypeSymbol? environment; + INamedTypeSymbol? httpClient; /// /// Gets a Guid type symbol. @@ -39,8 +44,35 @@ public sealed partial class KnownTypeSymbols /// public INamedTypeSymbol? TaskT => this.GetOrResolveFullyQualifiedType(typeof(Task<>).FullName, ref this.taskT); + /// + /// Gets a TaskFactory type symbol. + /// + public INamedTypeSymbol? TaskFactory => this.GetOrResolveFullyQualifiedType(typeof(TaskFactory).FullName, ref this.taskFactory); + + /// + /// Gets a TaskFactory<T> type symbol. + /// + public INamedTypeSymbol? TaskFactoryT => this.GetOrResolveFullyQualifiedType(typeof(TaskFactory<>).FullName, ref this.taskFactoryT); + + /// + /// Gets a TaskContinuationOptions type symbol. + /// + public INamedTypeSymbol? TaskContinuationOptions => this.GetOrResolveFullyQualifiedType(typeof(TaskContinuationOptions).FullName, ref this.taskContinuationOptions); + /// /// Gets a CancellationToken type symbol. /// public INamedTypeSymbol? CancellationToken => this.GetOrResolveFullyQualifiedType(typeof(CancellationToken).FullName, ref this.cancellationToken); + +#pragma warning disable RS1035 // Environment Variables are not supposed to be used in Analyzers, but here we just reference the API, never using it. + /// + /// Gets an Environment type symbol. + /// + public INamedTypeSymbol? Environment => this.GetOrResolveFullyQualifiedType(typeof(Environment).FullName, ref this.environment); +#pragma warning restore RS1035 + + /// + /// Gets an HttpClient type symbol. + /// + public INamedTypeSymbol? HttpClient => this.GetOrResolveFullyQualifiedType(typeof(HttpClient).FullName, ref this.httpClient); } diff --git a/src/Analyzers/Orchestration/EnvironmentOrchestrationAnalyzer.cs b/src/Analyzers/Orchestration/EnvironmentOrchestrationAnalyzer.cs new file mode 100644 index 00000000..3e4fb773 --- /dev/null +++ b/src/Analyzers/Orchestration/EnvironmentOrchestrationAnalyzer.cs @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; +using static Microsoft.DurableTask.Analyzers.Orchestration.EnvironmentOrchestrationAnalyzer; + +namespace Microsoft.DurableTask.Analyzers.Orchestration; + +#pragma warning disable RS1035 // Environment Variables are not supposed to be used in Analyzers, but here we just reference the API, never using it. + +/// +/// Analyzer that reports usage of APIs in orchestrations. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class EnvironmentOrchestrationAnalyzer : OrchestrationAnalyzer +{ + /// + /// Diagnostic ID supported for the analyzer. + /// + public const string DiagnosticId = "DURABLE0006"; + + static readonly LocalizableString Title = new LocalizableResourceString(nameof(Resources.EnvironmentOrchestrationAnalyzerTitle), Resources.ResourceManager, typeof(Resources)); + static readonly LocalizableString MessageFormat = new LocalizableResourceString(nameof(Resources.EnvironmentOrchestrationAnalyzerMessageFormat), Resources.ResourceManager, typeof(Resources)); + + static readonly DiagnosticDescriptor Rule = new( + DiagnosticId, + Title, + MessageFormat, + AnalyzersCategories.Orchestration, + DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + /// + public override ImmutableArray SupportedDiagnostics => [Rule]; + + /// + /// Visitor that inspects the method body for retrievals of Environment Variables through the type. + /// + public sealed class EnvironmentOrchestrationVisitor : MethodProbeOrchestrationVisitor + { + /// + public override bool Initialize() + { + return this.KnownTypeSymbols.Environment != null; + } + + /// + protected override void VisitMethod(SemanticModel semanticModel, SyntaxNode methodSyntax, IMethodSymbol methodSymbol, string orchestrationName, Action reportDiagnostic) + { + IOperation? methodOperation = semanticModel.GetOperation(methodSyntax); + if (methodOperation is null) + { + return; + } + + foreach (IInvocationOperation invocation in methodOperation.Descendants().OfType()) + { + IMethodSymbol targetMethod = invocation.TargetMethod; + + if (targetMethod.ContainingType.Equals(this.KnownTypeSymbols.Environment, SymbolEqualityComparer.Default) && + targetMethod.Name is nameof(Environment.GetEnvironmentVariable) or nameof(Environment.GetEnvironmentVariables) or nameof(Environment.ExpandEnvironmentVariables)) + { + string invocationName = targetMethod.ToDisplayString(SymbolDisplayFormat.CSharpShortErrorMessageFormat); + + // e.g.: "The method 'Method1' uses environment variables through 'Environment.GetEnvironmentVariable()' that may cause non-deterministic behavior when invoked from orchestration 'MyOrchestrator'" + reportDiagnostic(RoslynExtensions.BuildDiagnostic(Rule, invocation, methodSymbol.Name, invocationName, orchestrationName)); + } + } + } + } +} diff --git a/src/Analyzers/Orchestration/IOOrchestrationAnalyzer.cs b/src/Analyzers/Orchestration/IOOrchestrationAnalyzer.cs new file mode 100644 index 00000000..6051ccc5 --- /dev/null +++ b/src/Analyzers/Orchestration/IOOrchestrationAnalyzer.cs @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; + +using static Microsoft.DurableTask.Analyzers.Orchestration.IOOrchestrationAnalyzer; + +namespace Microsoft.DurableTask.Analyzers.Orchestration; + +/// +/// Analyzer that reports usage of I/O APIs in orchestrations. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class IOOrchestrationAnalyzer : OrchestrationAnalyzer +{ + /// + /// Diagnostic ID supported for the analyzer. + /// + public const string DiagnosticId = "DURABLE0005"; + + static readonly LocalizableString Title = new LocalizableResourceString(nameof(Resources.IOOrchestrationAnalyzerTitle), Resources.ResourceManager, typeof(Resources)); + static readonly LocalizableString MessageFormat = new LocalizableResourceString(nameof(Resources.IOOrchestrationAnalyzerMessageFormat), Resources.ResourceManager, typeof(Resources)); + + static readonly DiagnosticDescriptor Rule = new( + DiagnosticId, + Title, + MessageFormat, + AnalyzersCategories.Orchestration, + DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + /// + public override ImmutableArray SupportedDiagnostics => [Rule]; + + /// + /// Visitor that inspects the method body for I/O operations by searching for specific types. + /// + public sealed class IOOrchestrationVisitor : MethodProbeOrchestrationVisitor + { + ImmutableArray bannedTypes; + + /// + public override bool Initialize() + { + List candidateSymbols = [ + this.KnownTypeSymbols.HttpClient, + this.KnownTypeSymbols.BlobServiceClient, + this.KnownTypeSymbols.BlobContainerClient, + this.KnownTypeSymbols.BlobClient, + this.KnownTypeSymbols.QueueServiceClient, + this.KnownTypeSymbols.QueueClient, + this.KnownTypeSymbols.TableServiceClient, + this.KnownTypeSymbols.TableClient, + this.KnownTypeSymbols.CosmosClient, + this.KnownTypeSymbols.SqlConnection, + ]; + + // filter out null values, since some of them may not be available during compilation: + this.bannedTypes = candidateSymbols.Where(s => s is not null).ToImmutableArray()!; + + return this.bannedTypes.Length > 0; + } + + /// + protected override void VisitMethod(SemanticModel semanticModel, SyntaxNode methodSyntax, IMethodSymbol methodSymbol, string orchestrationName, Action reportDiagnostic) + { + IOperation? methodOperation = semanticModel.GetOperation(methodSyntax); + if (methodOperation is null) + { + return; + } + + foreach (IOperation operation in methodOperation.Descendants()) + { + if (operation.Type is not null) + { + if (this.bannedTypes.Contains(operation.Type, SymbolEqualityComparer.Default)) + { + string typeName = operation.Type.ToDisplayString(SymbolDisplayFormat.CSharpShortErrorMessageFormat); + + // e.g.: "The method 'Method1' performs I/O through 'HttpClient' that may cause non-deterministic behavior when invoked from orchestration 'MyOrchestrator'" + reportDiagnostic(RoslynExtensions.BuildDiagnostic(Rule, operation, methodSymbol.Name, typeName, orchestrationName)); + } + } + } + } + } +} diff --git a/src/Analyzers/Orchestration/ThreadTaskOrchestrationAnalyzer.cs b/src/Analyzers/Orchestration/ThreadTaskOrchestrationAnalyzer.cs new file mode 100644 index 00000000..8d640ed1 --- /dev/null +++ b/src/Analyzers/Orchestration/ThreadTaskOrchestrationAnalyzer.cs @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Operations; +using static Microsoft.DurableTask.Analyzers.Orchestration.ThreadTaskOrchestrationAnalyzer; + +namespace Microsoft.DurableTask.Analyzers.Orchestration; + +/// +/// Analyzer that detects usage of non-deterministic / operations in orchestrations. +/// +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public sealed class ThreadTaskOrchestrationAnalyzer : OrchestrationAnalyzer +{ + /// + /// Diagnostic ID supported for the analyzer. + /// + public const string DiagnosticId = "DURABLE0004"; + + static readonly LocalizableString Title = new LocalizableResourceString(nameof(Resources.ThreadTaskOrchestrationAnalyzerTitle), Resources.ResourceManager, typeof(Resources)); + static readonly LocalizableString MessageFormat = new LocalizableResourceString(nameof(Resources.ThreadTaskOrchestrationAnalyzerMessageFormat), Resources.ResourceManager, typeof(Resources)); + + static readonly DiagnosticDescriptor Rule = new( + DiagnosticId, + Title, + MessageFormat, + AnalyzersCategories.Orchestration, + DiagnosticSeverity.Warning, + isEnabledByDefault: true); + + /// + public override ImmutableArray SupportedDiagnostics => [Rule]; + + /// + /// Visitor that inspects the orchestration methods for non-deterministic / operations. + /// + public sealed class ThreadTaskOrchestrationVisitor : MethodProbeOrchestrationVisitor + { + /// + public override bool Initialize() + { + return this.KnownTypeSymbols.Thread != null && + this.KnownTypeSymbols.Task != null && + this.KnownTypeSymbols.TaskT != null && + this.KnownTypeSymbols.TaskFactory != null; + } + + /// + protected override void VisitMethod(SemanticModel semanticModel, SyntaxNode methodSyntax, IMethodSymbol methodSymbol, string orchestrationName, Action reportDiagnostic) + { + IOperation? methodOperation = semanticModel.GetOperation(methodSyntax); + if (methodOperation is null) + { + return; + } + + // reports usage of Thread.Start, Task.Run, Task.ContinueWith and Task.Factory.StartNew + foreach (IInvocationOperation invocation in methodOperation.Descendants().OfType()) + { + IMethodSymbol targetMethod = invocation.TargetMethod; + + if (targetMethod.IsEqualTo(this.KnownTypeSymbols.Thread, nameof(Thread.Start)) || + targetMethod.IsEqualTo(this.KnownTypeSymbols.Task, nameof(Task.Run)) || + targetMethod.IsEqualTo(this.KnownTypeSymbols.TaskT, nameof(Task.Run)) || + targetMethod.IsEqualTo(this.KnownTypeSymbols.Task, nameof(Task.ContinueWith)) || + targetMethod.IsEqualTo(this.KnownTypeSymbols.TaskT, nameof(Task.ContinueWith)) || + targetMethod.IsEqualTo(this.KnownTypeSymbols.TaskFactory, nameof(TaskFactory.StartNew)) || + targetMethod.IsEqualTo(this.KnownTypeSymbols.TaskFactoryT, nameof(TaskFactory.StartNew))) + { + string invocationName = targetMethod.ToDisplayString(SymbolDisplayFormat.CSharpShortErrorMessageFormat); + + // e.g.: "The method 'Method1' uses non-deterministic Threads/Tasks operations by the invocation of 'Thread.Start' in orchestration 'MyOrchestrator'" + reportDiagnostic(RoslynExtensions.BuildDiagnostic(Rule, invocation, methodSymbol.Name, invocationName, orchestrationName)); + } + } + } + } +} diff --git a/src/Analyzers/Resources.resx b/src/Analyzers/Resources.resx index 7f3481a0..6af3d76f 100644 --- a/src/Analyzers/Resources.resx +++ b/src/Analyzers/Resources.resx @@ -165,4 +165,22 @@ OrchestrationTrigger methods must not use any other bindings + + The method '{0}' uses environment variables through '{1}' that may cause non-deterministic behavior when invoked from orchestration '{2}' + + + Environment variables must not be accessed inside an orchestrator function + + + The method '{0}' performs I/O through '{1}' that may cause non-deterministic behavior when invoked from orchestration '{2}' + + + I/O operations are not allowed inside an orchestrator function + + + The method '{0}' uses non-deterministic Threads/Tasks operations by the invocation of '{1}' in orchestration '{2}' + + + Thread and Task calls must be deterministic inside an orchestrator function + \ No newline at end of file diff --git a/src/Analyzers/RoslynExtensions.cs b/src/Analyzers/RoslynExtensions.cs index 9ecaba5f..e1920524 100644 --- a/src/Analyzers/RoslynExtensions.cs +++ b/src/Analyzers/RoslynExtensions.cs @@ -118,7 +118,8 @@ public static IEnumerable GetSyntaxNodes(this IMethodSy /// True if the method symbol is contained in the type symbol and has the method name, false otherwise. public static bool IsEqualTo(this IMethodSymbol methodSymbol, INamedTypeSymbol? typeSymbol, string methodName) { - return methodSymbol.ContainingType.Equals(typeSymbol, SymbolEqualityComparer.Default) && + return (methodSymbol.ContainingType.Equals(typeSymbol, SymbolEqualityComparer.Default) || + methodSymbol.ContainingType.OriginalDefinition.Equals(typeSymbol, SymbolEqualityComparer.Default)) && methodSymbol.Name.Equals(methodName, StringComparison.Ordinal); } diff --git a/test/Analyzers.Tests/Orchestration/EnvironmentOrchestrationAnalyzerTests.cs b/test/Analyzers.Tests/Orchestration/EnvironmentOrchestrationAnalyzerTests.cs new file mode 100644 index 00000000..55c2113a --- /dev/null +++ b/test/Analyzers.Tests/Orchestration/EnvironmentOrchestrationAnalyzerTests.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.CodeAnalysis.Testing; +using Microsoft.DurableTask.Analyzers.Orchestration; + +using VerifyCS = Microsoft.DurableTask.Analyzers.Tests.Verifiers.CSharpAnalyzerVerifier; + +namespace Microsoft.DurableTask.Analyzers.Tests.Orchestration; + +public class EnvironmentOrchestrationAnalyzerTest +{ + [Fact] + public async Task EmptyCodeHasNoDiag() + { + string code = @""; + + await VerifyCS.VerifyDurableTaskAnalyzerAsync(code); + } + + [Fact] + public async Task GettingEnvironmentVariablesAreNotAllowedInAzureFunctionsOrchestrations() + { + string code = Wrapper.WrapDurableFunctionOrchestration(@" +[Function(""Run"")] +void Method([OrchestrationTrigger] TaskOrchestrationContext context) +{ + {|#0:Environment.GetEnvironmentVariable(""PATH"")|}; + {|#1:Environment.GetEnvironmentVariables()|}; + {|#2:Environment.ExpandEnvironmentVariables(""PATH"")|}; +} +"); + string[] methods = [ + "Environment.GetEnvironmentVariable(string)", + "Environment.GetEnvironmentVariables()", + "Environment.ExpandEnvironmentVariables(string)", + ]; + + DiagnosticResult[] expected = methods.Select( + (method, i) => BuildDiagnostic().WithLocation(i).WithArguments("Method", method, "Run")).ToArray(); + + await VerifyCS.VerifyDurableTaskAnalyzerAsync(code, expected); + } + + static DiagnosticResult BuildDiagnostic() + { + return VerifyCS.Diagnostic(EnvironmentOrchestrationAnalyzer.DiagnosticId); + } +} \ No newline at end of file diff --git a/test/Analyzers.Tests/Orchestration/IOOrchestrationTests.cs b/test/Analyzers.Tests/Orchestration/IOOrchestrationTests.cs new file mode 100644 index 00000000..249fd89f --- /dev/null +++ b/test/Analyzers.Tests/Orchestration/IOOrchestrationTests.cs @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.CodeAnalysis.Testing; +using Microsoft.DurableTask.Analyzers.Orchestration; + +using VerifyCS = Microsoft.DurableTask.Analyzers.Tests.Verifiers.CSharpAnalyzerVerifier; + +namespace Microsoft.DurableTask.Analyzers.Tests.Orchestration; + +public class IOOrchestrationTests +{ + [Fact] + public async Task EmptyCodeHasNoDiag() + { + string code = @""; + + await VerifyCS.VerifyDurableTaskAnalyzerAsync(code); + } + + [Fact] + public async Task IOTypesAreBannedWithinAzureFunctionOrchestrations() + { + string code = Wrapper.WrapDurableFunctionOrchestration(@" +[Function(""Run"")] +void Method([OrchestrationTrigger] TaskOrchestrationContext context) +{ + var http1 = {|#0:new HttpClient()|}; + + var blob1 = {|#1:new BlobServiceClient(""test"")|}; + var blob2 = {|#2:new BlobContainerClient(""test"",""test"")|}; + var blob3 = {|#3:new BlobClient(""test"",""test"",""test"")|}; + + var queue1 = {|#4:new QueueServiceClient(""test"")|}; + var queue2 = {|#5:new QueueClient(""test"",""test"")|}; + + var table1 = {|#6:new TableServiceClient(""test"")|}; + var table2 = {|#7:new TableClient(""test"",""test"")|}; + + var cosmos1 = {|#8:new CosmosClient(""test"")|}; + + var sql1 = {|#9:new SqlConnection()|}; +} +"); + string[] types = [ + "HttpClient", + "BlobServiceClient", "BlobContainerClient", "BlobClient", + "QueueServiceClient", "QueueClient", + "TableServiceClient", "TableClient", + "CosmosClient", + "SqlConnection", + ]; + + DiagnosticResult[] expected = types.Select( + (type, i) => BuildDiagnostic().WithLocation(i).WithArguments("Method", type, "Run")).ToArray(); + + await VerifyCS.VerifyDurableTaskAnalyzerAsync(code, expected); + } + + static DiagnosticResult BuildDiagnostic() + { + return VerifyCS.Diagnostic(IOOrchestrationAnalyzer.DiagnosticId); + } +} diff --git a/test/Analyzers.Tests/Orchestration/ThreadTaskOrchestrationAnalyzerTests.cs b/test/Analyzers.Tests/Orchestration/ThreadTaskOrchestrationAnalyzerTests.cs new file mode 100644 index 00000000..0021e6e4 --- /dev/null +++ b/test/Analyzers.Tests/Orchestration/ThreadTaskOrchestrationAnalyzerTests.cs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Microsoft.CodeAnalysis.Testing; +using Microsoft.DurableTask.Analyzers.Orchestration; + +using VerifyCS = Microsoft.DurableTask.Analyzers.Tests.Verifiers.CSharpAnalyzerVerifier; + +namespace Microsoft.DurableTask.Analyzers.Tests.Orchestration; + +public class ThreadTaskOrchestrationAnalyzerTests +{ + [Fact] + public async Task EmptyCodeHasNoDiag() + { + string code = @""; + + await VerifyCS.VerifyDurableTaskAnalyzerAsync(code); + } + + [Fact] + public async Task StartingThreadsTasksAreBannedWithinAzureFunctionOrchestrations() + { + string code = Wrapper.WrapDurableFunctionOrchestration(@" +[Function(""Run"")] +async Task Method([OrchestrationTrigger] TaskOrchestrationContext context) +{ + {|#0:new Thread(() => { }).Start()|}; + + Task t1 = {|#1:Task.Run(() => 0)|}; + await {|#2:t1.ContinueWith(task => 0)|}; + await {|#3:Task.Factory.StartNew(() => 0)|}; + + Task t2 = {|#4:Task.Run(() => { })|}; + await {|#5:t2.ContinueWith(task => { })|}; + await {|#6:Task.Factory.StartNew(() => { })|}; +} +"); + string[] invocations = [ + "Thread.Start()", + "Task.Run(Func)", + "Task.ContinueWith(Func, int>)", + "TaskFactory.StartNew(Func)", + "Task.Run(Action)", + "Task.ContinueWith(Action)", + "TaskFactory.StartNew(Action)", + ]; + + DiagnosticResult[] expected = invocations.Select( + (invocation, i) => BuildDiagnostic().WithLocation(i).WithArguments("Method", invocation, "Run")).ToArray(); + + await VerifyCS.VerifyDurableTaskAnalyzerAsync(code, expected); + } + + static DiagnosticResult BuildDiagnostic() + { + return VerifyCS.Diagnostic(ThreadTaskOrchestrationAnalyzer.DiagnosticId); + } +} diff --git a/test/Analyzers.Tests/Verifiers/CSharpAnalyzerVerifier.Durable.cs b/test/Analyzers.Tests/Verifiers/CSharpAnalyzerVerifier.Durable.cs index 6d035e77..71939778 100644 --- a/test/Analyzers.Tests/Verifiers/CSharpAnalyzerVerifier.Durable.cs +++ b/test/Analyzers.Tests/Verifiers/CSharpAnalyzerVerifier.Durable.cs @@ -22,8 +22,13 @@ public static async Task VerifyDurableTaskAnalyzerAsync(string source, Action