Repository navigation
Add ThreadStatic correctness analyzers #1646
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Andrew Arnott (AArnott)
merged 10 commits into
main
from
aarnott-add-threadstatic-analyzer
Aug 22, 2026
Merged
Changes from 5 commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
3139ca0
Add ThreadStatic correctness analyzers
AArnott 888011f
Remove unnecessary analyzer doc redirects
AArnott 7accd4e
Rename shared ThreadStatic analyzer
AArnott 7afa052
Wrap ThreadStatic analyzer callbacks
AArnott c0d8038
Cover ThreadStatic static constructor assignments
AArnott 60050f5
Refine type initializer assignment analysis
AArnott 991430f
Handle additional ThreadStatic writes
AArnott f8fd6e2
Handle ThreadStatic instance events
AArnott 1093317
Cover remaining ThreadStatic writes
AArnott ee62376
Cover indirect ThreadStatic writes
AArnott File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
154 changes: 154 additions & 0 deletions
154
src/Microsoft.VisualStudio.Threading.Analyzers/ThreadStaticAnalyzer.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | ||
|
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); | ||
|
AArnott marked this conversation as resolved.
Resolved
|
||
| startContext.RegisterOperationAction( | ||
| Utils.DebuggableWrapper(operationContext => AnalyzeAssignment(operationContext, threadStaticAttribute)), | ||
| OperationKind.SimpleAssignment, | ||
|
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 }) | ||
|
AArnott marked this conversation as resolved.
Resolved
|
||
| { | ||
| return; | ||
| } | ||
|
|
||
| var assignment = (IAssignmentOperation)context.Operation; | ||
| if (assignment.Target is IFieldReferenceOperation { Field: { IsStatic: true } field } | ||
|
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)); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.