Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions docfx/analyzers/VSTHRD116.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# VSTHRD116 Use `ThreadStaticAttribute` only with static fields

`ThreadStaticAttribute` has no effect when applied to an instance field. Each object already has its own instance field, and the runtime does not create a separate value for each thread.

## Examples of patterns that are flagged by this analyzer

```csharp
class Example
{
[ThreadStatic]
private object value;
}
```

```vb
Class Example
<ThreadStatic>
Private value As Object
End Class
```

## Solution

If the value should be shared by all threads for each object, remove `ThreadStaticAttribute`. If each thread should have its own value, make the field static after confirming that changing it from per-instance state is appropriate.

```csharp
class Example
{
[ThreadStatic]
private static object value;
}
```

```vb
Class Example
<ThreadStatic>
Private Shared value As Object
End Class
```

No code fix is offered because removing the attribute and making the field static have different semantics.

This rule is equivalent to .NET SDK rule [CA2259](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2259). When both analyzer packages are enabled, configure or suppress one of the duplicate diagnostics.
79 changes: 79 additions & 0 deletions docfx/analyzers/VSTHRD117.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# VSTHRD117 Avoid initialization of `ThreadStatic` fields in a type initializer

A static field marked with `ThreadStaticAttribute` has a separate value on each thread. A field initializer or static constructor runs only on the thread that initializes the containing type, so the field has its default value on every other thread. In C#, this also applies to auto-property backing fields marked with `[field: ThreadStatic]`.

## Examples of patterns that are flagged by this analyzer

```csharp
class Example
{
[ThreadStatic]
private static object value = new object();
}
```

```csharp
class Example
{
[ThreadStatic]
private static object value;

static Example()
{
value = new object();
}
}
```

```vb
Class Example
<ThreadStatic>
Private Shared value As Object = New Object()
End Class
```

```vb
Class Example
<ThreadStatic>
Private Shared value As Object

Shared Sub New()
value = New Object()
End Sub
End Class
```

## Solution

Remove the initializer or static-constructor assignment and initialize the value independently on each thread, typically by using lazy initialization at the point of use.

```csharp
class Example
{
[ThreadStatic]
private static object value;

private static object Value => value ??= new object();
}
```

```vb
Class Example
<ThreadStatic>
Private Shared value As Object

Private Shared ReadOnly Property Value As Object
Get
If value Is Nothing Then
value = New Object()
End If

Return value
End Get
End Property
End Class
```

No code fix is offered because the correct per-thread initialization depends on how the field is used.

This rule is equivalent to .NET SDK rule [CA2019](https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca2019). When both analyzer packages are enabled, configure or suppress one of the duplicate diagnostics.
Comment thread
AArnott marked this conversation as resolved.
Resolved
2 changes: 2 additions & 0 deletions docfx/analyzers/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ ID | Title | Severity | Supports | Default diagnostic severity
[VSTHRD113](VSTHRD113.md) | Check for `System.IAsyncDisposable` | Advisory | | Info
[VSTHRD114](VSTHRD114.md) | Avoid returning null from a `Task`-returning method. | Advisory | | Warning
[VSTHRD115](VSTHRD115.md) | Avoid creating a JoinableTaskContext with an explicit `null` `SynchronizationContext` | Advisory | | Warning
[VSTHRD116](VSTHRD116.md) | Use `ThreadStaticAttribute` only with static fields | Advisory | | Warning
[VSTHRD117](VSTHRD117.md) | Avoid initialization of `ThreadStatic` fields in a type initializer | Advisory | | Warning
[VSTHRD200](VSTHRD200.md) | Use `Async` naming convention | Guideline | [VSTHRD103](VSTHRD103.md) | Warning

## Severity descriptions
Expand Down
2 changes: 2 additions & 0 deletions docfx/analyzers/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,6 @@ items:
- href: VSTHRD113.md
- href: VSTHRD114.md
- href: VSTHRD115.md
- href: VSTHRD116.md
- href: VSTHRD117.md
- href: VSTHRD200.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Collections.Concurrent;
using System.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;

namespace Microsoft.VisualStudio.Threading.Analyzers;

/// <summary>
/// Verifies that <see cref="System.ThreadStaticAttribute"/> is applied to static fields that are not initialized by the type initializer.
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class CSharpThreadStaticAnalyzer : ThreadStaticAnalyzer
{
/// <inheritdoc />
protected override void InitializeLanguageSpecific(AnalysisContext context)
{
context.RegisterCompilationStartAction(startContext =>
{
INamedTypeSymbol? threadStaticAttribute = startContext.Compilation.GetTypeByMetadataName(Types.ThreadStaticAttribute.FullName);
if (threadStaticAttribute is null)
{
return;
}

var threadStaticEvents = new ConcurrentDictionary<ISymbol, byte>(SymbolEqualityComparer.Default);
var eventAssignments = new ConcurrentQueue<(IEventSymbol Event, Location Location)>();
startContext.RegisterSyntaxNodeAction(
Utils.DebuggableWrapper(context => AnalyzeEventFieldDeclaration(context, threadStaticAttribute, threadStaticEvents)),
SyntaxKind.EventFieldDeclaration);
startContext.RegisterOperationAction(
Utils.DebuggableWrapper(context => AnalyzeEventAssignment(context, eventAssignments)),
OperationKind.EventAssignment);
startContext.RegisterCompilationEndAction(context => ReportEventAssignments(context, threadStaticEvents, eventAssignments));
});
}

private static void AnalyzeEventFieldDeclaration(
SyntaxNodeAnalysisContext context,
INamedTypeSymbol threadStaticAttribute,
ConcurrentDictionary<ISymbol, byte> threadStaticEvents)
{
var eventDeclaration = (EventFieldDeclarationSyntax)context.Node;
bool isThreadStatic = eventDeclaration.AttributeLists.Any(attributeList =>
attributeList.Target?.Identifier.ValueText == "field"
&& attributeList.Attributes.Any(attribute =>
context.SemanticModel.GetSymbolInfo(attribute, context.CancellationToken).Symbol is IMethodSymbol constructor
&& SymbolEqualityComparer.Default.Equals(constructor.ContainingType, threadStaticAttribute)));
if (!isThreadStatic)
{
return;
}

foreach (VariableDeclaratorSyntax variable in eventDeclaration.Declaration.Variables)
{
if (context.SemanticModel.GetDeclaredSymbol(variable, context.CancellationToken) is not IEventSymbol eventSymbol)
{
continue;
}

threadStaticEvents.TryAdd(eventSymbol, 0);
if (!eventSymbol.IsStatic)
{
context.ReportDiagnostic(Diagnostic.Create(GetNonStaticFieldDescriptor(), variable.Identifier.GetLocation()));
}
}
}

private static void AnalyzeEventAssignment(
OperationAnalysisContext context,
ConcurrentQueue<(IEventSymbol Event, Location Location)> eventAssignments)
{
var assignment = (IEventAssignmentOperation)context.Operation;
if (assignment.EventReference is IEventReferenceOperation { Event: { IsStatic: true } eventSymbol }
&& IsExecutedByTypeInitializer(context))
{
eventAssignments.Enqueue((eventSymbol, assignment.Syntax.GetLocation()));
}
}

private static void ReportEventAssignments(
CompilationAnalysisContext context,
ConcurrentDictionary<ISymbol, byte> threadStaticEvents,
ConcurrentQueue<(IEventSymbol Event, Location Location)> eventAssignments)
{
foreach ((IEventSymbol eventSymbol, Location location) in eventAssignments)
{
if (threadStaticEvents.ContainsKey(eventSymbol))
{
context.ReportDiagnostic(Diagnostic.Create(GetTypeInitializerAssignmentDescriptor(), location));
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;

namespace Microsoft.VisualStudio.Threading.Analyzers;

/// <summary>
/// Verifies that <see cref="System.ThreadStaticAttribute"/> is applied to static fields that are not initialized by the type initializer.
/// </summary>
[DiagnosticAnalyzer(LanguageNames.VisualBasic)]
public sealed class VisualBasicThreadStaticAnalyzer : ThreadStaticAnalyzer
{
}
12 changes: 12 additions & 0 deletions src/Microsoft.VisualStudio.Threading.Analyzers/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -353,4 +353,16 @@ Start the work within this context, or use JoinableTaskFactory.RunAsync to start
<data name="VSTHRD115_CodeFix_UseFactory_Title" xml:space="preserve">
<value>Use 'JoinableTaskContext.CreateNoOpContext' instead.</value>
</data>
<data name="VSTHRD116_Title" xml:space="preserve">
<value>Use ThreadStaticAttribute only with static fields</value>
</data>
<data name="VSTHRD116_MessageFormat" xml:space="preserve">
<value>ThreadStaticAttribute has no effect on a non-static field</value>
</data>
<data name="VSTHRD117_Title" xml:space="preserve">
<value>Avoid initialization of ThreadStatic fields in a type initializer</value>
</data>
<data name="VSTHRD117_MessageFormat" xml:space="preserve">
<value>ThreadStatic fields should not be initialized by a field initializer or static constructor because the value is only assigned on the type-initializing thread</value>
</data>
</root>
Loading
Loading