提交 b92db562 编写于 作者: J jasonmalinowski

Add an analyzer to force passing TaskSchedulers when creating Tasks.

If you call Task.Factory.StartNew and use one of the overloads that doesn't take TaskScheduler, the resulting task is scheduled onto TaskScheduler.Current. This is very dangerous in a free-threaded library like Roslyn: if the function runs on the UI thread it's possible the resulting Task will be scheduled on the UI thread accidentally. If later UI code were to wait on that task or a continuation of it, we might deadlock.

The solution is just to ban the APIs -- if somebody wishes to schedule to the current thread, it's up to them to be explicit. Otherwise, TaskScheduler.Default should be used. (changeset 1407143)
上级 4b039cb7
......@@ -74,6 +74,7 @@
<ItemGroup>
<Compile Include="ApiDesign\CancellationTokenMustBeLastCodeFixProvider.cs" />
<Compile Include="Documentation\CSharpDoNotUseVerbatimCrefsAnalyzer.cs" />
<Compile Include="Reliability\CSharpDoNotCreateTasksWithoutTaskSchedulerAnalyzer.cs" />
<Compile Include="Reliability\CSharpConsumePreserveSigAnalyzer.cs" />
<Compile Include="Performance\CSharpDiagnosticDescriptorAccessAnalyzer.cs" />
<Compile Include="Performance\CSharpCodeActionCreateAnalyzer.cs" />
......@@ -91,4 +92,4 @@
<Import Project="..\..\..\..\packages\StyleCop.MSBuild.4.7.48.2\build\StyleCop.MSBuild.Targets" Condition="Exists('..\..\..\..\packages\StyleCop.MSBuild.4.7.48.2\build\StyleCop.MSBuild.Targets')" />
<Import Project="$(SolutionDir)\.nuget\NuGet.targets" Condition="Exists('$(SolutionDir)\.nuget\NuGet.targets')" />
</ImportGroup>
</Project>
</Project>
\ No newline at end of file
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Diagnostics;
namespace Roslyn.Diagnostics.Analyzers.CSharp
{
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class CSharpDoNotCallContinueWithWithoutTaskSchedulerAnalyzer : DoNotCreateTasksWithoutTaskSchedulerAnalyzer<SyntaxKind>
{
protected override SyntaxKind InvocationExpressionSyntaxKind
{
get { return SyntaxKind.InvocationExpression; }
}
}
}
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
namespace Roslyn.Diagnostics.Analyzers
{
public abstract class DoNotCreateTasksWithoutTaskSchedulerAnalyzer<TSyntaxKind> : DiagnosticAnalyzer
where TSyntaxKind : struct
{
private static LocalizableString localizableMessage = new LocalizableResourceString(nameof(RoslynDiagnosticsResources.DoNotCreateTasksWithoutTaskSchedulerMessage), RoslynDiagnosticsResources.ResourceManager, typeof(RoslynDiagnosticsResources));
private static LocalizableString localizableTitle = new LocalizableResourceString(nameof(RoslynDiagnosticsResources.DoNotCreateTasksWithoutTaskSchedulerTitle), RoslynDiagnosticsResources.ResourceManager, typeof(RoslynDiagnosticsResources));
private static LocalizableString localizableDescription = new LocalizableResourceString(nameof(RoslynDiagnosticsResources.DoNotCreateTasksWithoutTaskSchedulerDescription), RoslynDiagnosticsResources.ResourceManager, typeof(RoslynDiagnosticsResources));
internal static readonly DiagnosticDescriptor DoNotCreateTasksWithoutTaskSchedulerAnalyzerDescriptor = new DiagnosticDescriptor(
RoslynDiagnosticIds.DoNotCreateTasksWithoutTaskSchedulerRuleId,
localizableTitle,
localizableMessage,
"Reliability",
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: localizableDescription,
customTags: WellKnownDiagnosticTags.Telemetry);
public sealed override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics
{
get
{
return ImmutableArray.Create(DoNotCreateTasksWithoutTaskSchedulerAnalyzerDescriptor);
}
}
public sealed override void Initialize(AnalysisContext context)
{
context.RegisterCompilationStartAction(compilationContext =>
{
// Check if TPL is available before actually doing the searches
var taskType = compilationContext.Compilation.GetTypeByMetadataName("System.Threading.Tasks.Task");
var taskFactoryType = compilationContext.Compilation.GetTypeByMetadataName("System.Threading.Tasks.TaskFactory");
var taskSchedulerType = compilationContext.Compilation.GetTypeByMetadataName("System.Threading.Tasks.TaskScheduler");
if (taskType != null && taskFactoryType != null && taskSchedulerType != null)
{
compilationContext.RegisterSyntaxNodeAction(syntaxNodeContext => AnalyzeNode(syntaxNodeContext, taskType, taskFactoryType, taskSchedulerType), ImmutableArray.Create(InvocationExpressionSyntaxKind));
}
});
}
protected abstract TSyntaxKind InvocationExpressionSyntaxKind { get; }
private void AnalyzeNode(SyntaxNodeAnalysisContext context, INamedTypeSymbol taskType, INamedTypeSymbol taskFactoryType, INamedTypeSymbol taskSchedulerType)
{
var methodSymbol = context.SemanticModel.GetSymbolInfo(context.Node, context.CancellationToken).Symbol as IMethodSymbol;
if (methodSymbol == null)
{
return;
}
if (!IsMethodOfInterest(methodSymbol, taskType, taskFactoryType))
{
return;
}
// We want to ensure that all overloads called are explicitly taking a task scheduler
if (methodSymbol.Parameters.Any(p => p.Type.Equals(taskSchedulerType)))
{
return;
}
context.ReportDiagnostic(Diagnostic.Create(DoNotCreateTasksWithoutTaskSchedulerAnalyzerDescriptor, context.Node.GetLocation(), methodSymbol.Name));
}
private bool IsMethodOfInterest(IMethodSymbol methodSymbol, INamedTypeSymbol taskType, INamedTypeSymbol taskFactoryType)
{
// Check if it's a method of Task or a derived type (for Task<T>)
if ((methodSymbol.ContainingType.Equals(taskType) ||
taskType.Equals(methodSymbol.ContainingType.BaseType)) &&
methodSymbol.Name == "ContinueWith")
{
return true;
}
if (methodSymbol.ContainingType.Equals(taskFactoryType) &&
methodSymbol.Name == "StartNew")
{
return true;
}
return false;
}
}
}
......@@ -84,6 +84,7 @@
<Compile Include="Performance\SpecializedEnumerableCreationAnalyzer.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Reliability\AttributeHelpers.cs" />
<Compile Include="Reliability\DoNotCreateTasksWithoutTaskSchedulerAnalyzer.cs" />
<Compile Include="Reliability\DirectlyAwaitingTaskAnalyzer.cs" />
<Compile Include="Reliability\DirectlyAwaitingTaskAnalyzerRule.cs" />
<Compile Include="Reliability\DirectlyAwaitingTaskFix.cs" />
......@@ -112,4 +113,4 @@
<Import Project="..\..\..\..\packages\StyleCop.MSBuild.4.7.48.2\build\StyleCop.MSBuild.Targets" Condition="Exists('..\..\..\..\packages\StyleCop.MSBuild.4.7.48.2\build\StyleCop.MSBuild.Targets')" />
<Import Project="$(SolutionDir)\.nuget\NuGet.targets" Condition="Exists('$(SolutionDir)\.nuget\NuGet.targets')" />
</ImportGroup>
</Project>
</Project>
\ No newline at end of file
......@@ -22,5 +22,6 @@ internal static class RoslynDiagnosticIds
public const string ConsumePreserveSigRuleId = "RS0015";
public const string DeclarePublicApiRuleId = "RS0016";
public const string RemoveDeletedApiRuleId = "RS0017";
public const string DoNotCreateTasksWithoutTaskSchedulerRuleId = "RS0018";
}
}
......@@ -186,6 +186,33 @@ internal class RoslynDiagnosticsResources {
}
}
/// <summary>
/// Looks up a localized string similar to Do not create tasks unless you are using one of the overloads that takes a TaskScheduler. The default is to schedule on TaskScheduler.Current, which would lead to deadlocks. Either use TaskScheduler.Default to schedule on the thread pool, or explicitly pass TaskScheduler.Current to make your intentions clear..
/// </summary>
internal static string DoNotCreateTasksWithoutTaskSchedulerDescription {
get {
return ResourceManager.GetString("DoNotCreateTasksWithoutTaskSchedulerDescription", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Do not call {0} without passing a TaskScheduler.
/// </summary>
internal static string DoNotCreateTasksWithoutTaskSchedulerMessage {
get {
return ResourceManager.GetString("DoNotCreateTasksWithoutTaskSchedulerMessage", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Do not create tasks without passing a TaskScheduler.
/// </summary>
internal static string DoNotCreateTasksWithoutTaskSchedulerTitle {
get {
return ResourceManager.GetString("DoNotCreateTasksWithoutTaskSchedulerTitle", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to This collection is directly indexable. Going through LINQ here causes unnecessary allocations and CPU work..
/// </summary>
......
......@@ -237,4 +237,13 @@
<data name="RemoveDeletedApiDescription" xml:space="preserve">
<value>When removing a public type or member the corresponding entry in PublicAPI.txt should also be removed. This draws attention to API changes in the code reviews and source control history, and helps prevent breaking changes.</value>
</data>
<data name="DoNotCreateTasksWithoutTaskSchedulerDescription" xml:space="preserve">
<value>Do not create tasks unless you are using one of the overloads that takes a TaskScheduler. The default is to schedule on TaskScheduler.Current, which would lead to deadlocks. Either use TaskScheduler.Default to schedule on the thread pool, or explicitly pass TaskScheduler.Current to make your intentions clear.</value>
</data>
<data name="DoNotCreateTasksWithoutTaskSchedulerTitle" xml:space="preserve">
<value>Do not create tasks without passing a TaskScheduler</value>
</data>
<data name="DoNotCreateTasksWithoutTaskSchedulerMessage" xml:space="preserve">
<value>Do not call {0} without passing a TaskScheduler</value>
</data>
</root>
\ No newline at end of file
......@@ -100,6 +100,7 @@
<Compile Include="Performance\BasicEmptyArrayCodeFixProvider.vb" />
<Compile Include="Performance\BasicEmptyArrayDiagnosticAnalyzer.vb" />
<Compile Include="Performance\BasicSpecializedEnumerableCreationAnalyzer.vb" />
<Compile Include="Reliability\BasicDoNotCreateTasksWithoutTaskSchedulerAnalyzer.vb" />
<Compile Include="Reliability\BasicConsumePreserveSigAnalyzer.vb" />
<Compile Include="Reliability\BasicDirectlyAwaitingTaskAnalyzer.vb" />
<Compile Include="Reliability\BasicDirectlyAwaitingTaskFix.vb" />
......@@ -113,4 +114,4 @@
<Import Project="..\..\..\..\build\VSL.Imports.Closed.targets" />
<Import Project="$(SolutionDir)\.nuget\NuGet.targets" Condition="Exists('$(SolutionDir)\.nuget\NuGet.targets')" />
</ImportGroup>
</Project>
</Project>
\ No newline at end of file
' Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
Imports Microsoft.CodeAnalysis
Imports Microsoft.CodeAnalysis.Diagnostics
Imports Microsoft.CodeAnalysis.VisualBasic
Namespace Roslyn.Diagnostics.Analyzers.VisualBasic
<DiagnosticAnalyzer(LanguageNames.VisualBasic)>
Public Class BasicDoNotCreateTasksWithoutTaskSchedulerAnalyzer
Inherits DoNotCreateTasksWithoutTaskSchedulerAnalyzer(Of SyntaxKind)
Protected Overrides ReadOnly Property InvocationExpressionSyntaxKind As SyntaxKind
Get
Return SyntaxKind.InvocationExpression
End Get
End Property
End Class
End Namespace
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册