Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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. 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
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>
154 changes: 154 additions & 0 deletions src/Microsoft.VisualStudio.Threading.Analyzers/ThreadStaticAnalyzer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
// 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.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis;
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 inline.
/// </summary>
[DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
public class ThreadStaticAnalyzer : DiagnosticAnalyzer
{
/// <summary>
/// The diagnostic ID for <see cref="System.ThreadStaticAttribute"/> applied to a non-static field.
/// </summary>
public const string NonStaticFieldId = "VSTHRD116";

/// <summary>
/// The diagnostic ID for a <see cref="System.ThreadStaticAttribute"/> field initialized by the type initializer.
/// </summary>
public const string TypeInitializerAssignmentId = "VSTHRD117";

/// <summary>
/// The descriptor for <see cref="System.ThreadStaticAttribute"/> applied to a non-static field.
/// </summary>
internal static readonly DiagnosticDescriptor NonStaticFieldDescriptor = new DiagnosticDescriptor(
id: NonStaticFieldId,
title: new LocalizableResourceString(nameof(Strings.VSTHRD116_Title), Strings.ResourceManager, typeof(Strings)),
messageFormat: new LocalizableResourceString(nameof(Strings.VSTHRD116_MessageFormat), Strings.ResourceManager, typeof(Strings)),
description: null,
helpLinkUri: Utils.GetHelpLink(NonStaticFieldId),
category: "Usage",
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true);

/// <summary>
/// The descriptor for a <see cref="System.ThreadStaticAttribute"/> field initialized by the type initializer.
/// </summary>
internal static readonly DiagnosticDescriptor TypeInitializerAssignmentDescriptor = new DiagnosticDescriptor(
id: TypeInitializerAssignmentId,
title: new LocalizableResourceString(nameof(Strings.VSTHRD117_Title), Strings.ResourceManager, typeof(Strings)),
messageFormat: new LocalizableResourceString(nameof(Strings.VSTHRD117_MessageFormat), Strings.ResourceManager, typeof(Strings)),
description: null,
helpLinkUri: Utils.GetHelpLink(TypeInitializerAssignmentId),
category: "Usage",
defaultSeverity: DiagnosticSeverity.Warning,
isEnabledByDefault: true);

/// <inheritdoc />
public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(NonStaticFieldDescriptor, TypeInitializerAssignmentDescriptor);

/// <inheritdoc />
public override void Initialize(AnalysisContext context)
{
context.EnableConcurrentExecution();
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze);

context.RegisterCompilationStartAction(startContext =>
{
INamedTypeSymbol? threadStaticAttribute = startContext.Compilation.GetTypeByMetadataName(Types.ThreadStaticAttribute.FullName);
if (threadStaticAttribute is null)
{
return;
}

startContext.RegisterSymbolAction(
Utils.DebuggableWrapper(symbolContext => AnalyzeFieldLikeSymbol(symbolContext, threadStaticAttribute)),
SymbolKind.Field,
SymbolKind.Property);
Comment thread
AArnott marked this conversation as resolved.
Resolved
startContext.RegisterOperationAction(
Utils.DebuggableWrapper(operationContext => AnalyzeFieldInitializer(operationContext, threadStaticAttribute)),
OperationKind.FieldInitializer);
startContext.RegisterOperationAction(
Utils.DebuggableWrapper(operationContext => AnalyzePropertyInitializer(operationContext, threadStaticAttribute)),
OperationKind.PropertyInitializer);
Comment thread
AArnott marked this conversation as resolved.
Resolved
startContext.RegisterOperationAction(
Utils.DebuggableWrapper(operationContext => AnalyzeAssignment(operationContext, threadStaticAttribute)),
OperationKind.SimpleAssignment,
Comment thread
AArnott marked this conversation as resolved.
Resolved
OperationKind.CompoundAssignment,
OperationKind.CoalesceAssignment);
});
}

private static void AnalyzeFieldLikeSymbol(SymbolAnalysisContext context, INamedTypeSymbol threadStaticAttribute)
{
if (context.Symbol.IsStatic)
{
return;
}

IFieldSymbol? field = GetField(context.Symbol);
if (field is not null && HasThreadStaticAttribute(field, threadStaticAttribute))
{
Location? location = context.Symbol.Locations.FirstOrDefault(static location => location.IsInSource);
if (location is not null)
{
context.ReportDiagnostic(Diagnostic.Create(NonStaticFieldDescriptor, location));
}
}
}

private static void AnalyzeFieldInitializer(OperationAnalysisContext context, INamedTypeSymbol threadStaticAttribute)
{
var initializer = (IFieldInitializerOperation)context.Operation;
if (initializer.InitializedFields.Any(field => field.IsStatic && HasThreadStaticAttribute(field, threadStaticAttribute)))
{
context.ReportDiagnostic(Diagnostic.Create(TypeInitializerAssignmentDescriptor, initializer.Syntax.GetLocation()));
}
}

private static void AnalyzePropertyInitializer(OperationAnalysisContext context, INamedTypeSymbol threadStaticAttribute)
{
var initializer = (IPropertyInitializerOperation)context.Operation;
if (initializer.InitializedProperties.Any(property => property.IsStatic && GetField(property) is IFieldSymbol field && HasThreadStaticAttribute(field, threadStaticAttribute)))
{
context.ReportDiagnostic(Diagnostic.Create(TypeInitializerAssignmentDescriptor, initializer.Syntax.GetLocation()));
}
}

private static void AnalyzeAssignment(OperationAnalysisContext context, INamedTypeSymbol threadStaticAttribute)
{
if (context.ContainingSymbol is not IMethodSymbol { MethodKind: MethodKind.StaticConstructor })
Comment thread
AArnott marked this conversation as resolved.
Resolved
{
return;
}

var assignment = (IAssignmentOperation)context.Operation;
if (assignment.Target is IFieldReferenceOperation { Field: { IsStatic: true } field }
Comment thread
AArnott marked this conversation as resolved.
Resolved
&& HasThreadStaticAttribute(field, threadStaticAttribute))
{
context.ReportDiagnostic(Diagnostic.Create(TypeInitializerAssignmentDescriptor, assignment.Syntax.GetLocation()));
}
}

private static IFieldSymbol? GetField(ISymbol symbol)
{
if (symbol is IFieldSymbol field)
{
return field;
}

return symbol.ContainingType?.GetMembers()
.OfType<IFieldSymbol>()
.FirstOrDefault(field => SymbolEqualityComparer.Default.Equals(field.AssociatedSymbol, symbol));
}

private static bool HasThreadStaticAttribute(IFieldSymbol field, INamedTypeSymbol threadStaticAttribute)
=> field.GetAttributes().Any(attribute => SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, threadStaticAttribute));
}
11 changes: 11 additions & 0 deletions src/Microsoft.VisualStudio.Threading.Analyzers/Types.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,17 @@ public static class Types
{
private const string VSThreadingNamespace = "Microsoft.VisualStudio.Threading";

/// <summary>
/// Contains descriptors for <see cref="System.ThreadStaticAttribute"/>.
/// </summary>
public static class ThreadStaticAttribute
{
/// <summary>
/// The full name of <see cref="System.ThreadStaticAttribute"/>.
/// </summary>
public const string FullName = "System.ThreadStaticAttribute";
}

public static class BclAsyncDisposable
{
public const string FullName = "System.IAsyncDisposable";
Expand Down
Loading
Loading