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
8 changes: 7 additions & 1 deletion docfx/analyzers/VSTHRD117.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

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]`.

## Severity

An inline field or auto-property initializer that explicitly assigns the default value for its type, such as `null`, `0`, or `default`, produces an informational diagnostic. The initializer is redundant but does not create different initial values across threads.

All other initializers and assignments reported by this rule produce a warning because they can initialize the field differently on the type-initializing thread.

## Examples of patterns that are flagged by this analyzer

```csharp
Expand Down Expand Up @@ -45,7 +51,7 @@ 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.
Remove an initializer that assigns the default value. For other initializers or static-constructor assignments, initialize the value independently on each thread, typically by using lazy initialization at the point of use.

```csharp
class Example
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -170,21 +170,78 @@ private static void AnalyzeFieldLikeSymbol(SymbolAnalysisContext context, INamed
private static void AnalyzeFieldInitializer(OperationAnalysisContext context, INamedTypeSymbol threadStaticAttribute)
{
var initializer = (IFieldInitializerOperation)context.Operation;
if (initializer.InitializedFields.Any(field => field.IsStatic && HasThreadStaticAttribute(field, threadStaticAttribute)))
IFieldSymbol? field = initializer.InitializedFields.FirstOrDefault(field => field.IsStatic && HasThreadStaticAttribute(field, threadStaticAttribute));
if (field is not null)
{
context.ReportDiagnostic(Diagnostic.Create(TypeInitializerAssignmentDescriptor, initializer.Syntax.GetLocation()));
context.ReportDiagnostic(CreateTypeInitializerAssignmentDiagnostic(initializer.Syntax.GetLocation(), IsDefaultValue(initializer.Value, field.Type)));
}
}

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)))
IPropertySymbol? property = initializer.InitializedProperties.FirstOrDefault(property => property.IsStatic && GetField(property) is IFieldSymbol field && HasThreadStaticAttribute(field, threadStaticAttribute));
if (property is not null)
{
context.ReportDiagnostic(Diagnostic.Create(TypeInitializerAssignmentDescriptor, initializer.Syntax.GetLocation()));
context.ReportDiagnostic(CreateTypeInitializerAssignmentDiagnostic(initializer.Syntax.GetLocation(), IsDefaultValue(initializer.Value, property.Type)));
}
}

private static Diagnostic CreateTypeInitializerAssignmentDiagnostic(Location location, bool isDefaultValue)
=> Diagnostic.Create(
TypeInitializerAssignmentDescriptor,
location,
isDefaultValue ? DiagnosticSeverity.Info : TypeInitializerAssignmentDescriptor.DefaultSeverity,
additionalLocations: null,
properties: null);

private static bool IsDefaultValue(IOperation value, ITypeSymbol targetType)
{
while (value is IConversionOperation { IsImplicit: true } conversion)
{
value = conversion.Operand;
}

if (value is IDefaultValueOperation)
{
return true;
}

Optional<object?> constant = value.ConstantValue;
if (!constant.HasValue)
{
return false;
}

if (constant.Value is null)
{
return targetType.IsReferenceType
|| targetType.TypeKind is TypeKind.Pointer or TypeKind.FunctionPointer
|| targetType.OriginalDefinition.SpecialType == SpecialType.System_Nullable_T;
}

SpecialType specialType = targetType.TypeKind == TypeKind.Enum
? ((INamedTypeSymbol)targetType).EnumUnderlyingType!.SpecialType
: targetType.SpecialType;
return specialType switch
{
SpecialType.System_Boolean => constant.Value is false,
SpecialType.System_Char => constant.Value is '\0',
SpecialType.System_SByte => constant.Value is sbyte sbyteValue && sbyteValue == 0,
SpecialType.System_Byte => constant.Value is byte byteValue && byteValue == 0,
SpecialType.System_Int16 => constant.Value is short int16Value && int16Value == 0,
SpecialType.System_UInt16 => constant.Value is ushort uint16Value && uint16Value == 0,
SpecialType.System_Int32 => constant.Value is int int32Value && int32Value == 0,
SpecialType.System_UInt32 => constant.Value is uint uint32Value && uint32Value == 0,
SpecialType.System_Int64 => constant.Value is long int64Value && int64Value == 0,
SpecialType.System_UInt64 => constant.Value is ulong uint64Value && uint64Value == 0,
SpecialType.System_Single => constant.Value is float singleValue && singleValue == 0,
SpecialType.System_Double => constant.Value is double doubleValue && doubleValue == 0,
SpecialType.System_Decimal => constant.Value is decimal decimalValue && decimalValue == 0,
_ => false,
};
}

private static void AnalyzeAssignment(OperationAnalysisContext context, INamedTypeSymbol threadStaticAttribute)
{
if (!IsExecutedByTypeInitializer(context))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,42 +76,48 @@ class Test
await CSVerify.VerifyAnalyzerAsync(test);
}

[Fact]
public async Task AutoPropertyInlineInitialization_CSharp()
[Theory]
[InlineData("new object()", DiagnosticSeverity.Warning)]
[InlineData("null", DiagnosticSeverity.Info)]
public async Task AutoPropertyInlineInitialization_CSharp(string value, DiagnosticSeverity severity)
{
const string test = """
string test = $$"""
using System;

class Test
{
[field: ThreadStatic]
private static object Property { get; set; } {|VSTHRD117:= new object()|};
private static object Property { get; set; } {|#0:= {{value}}|};
}
""";

await CSVerify.VerifyAnalyzerAsync(test);
await CSVerify.VerifyAnalyzerAsync(test, new DiagnosticResult(ThreadStaticAnalyzer.TypeInitializerAssignmentId, severity).WithLocation(0));
}

[Theory]
[InlineData("object", "new object()")]
[InlineData("object", "default")]
[InlineData("object", "null")]
[InlineData("int", "42")]
[InlineData("int", "default")]
[InlineData("int", "0")]
public async Task InlineInitialization_CSharp(string type, string value)
[InlineData("object", "new object()", DiagnosticSeverity.Warning)]
[InlineData("object", "default", DiagnosticSeverity.Info)]
[InlineData("object", "null", DiagnosticSeverity.Info)]
[InlineData("int", "42", DiagnosticSeverity.Warning)]
[InlineData("int", "default", DiagnosticSeverity.Info)]
[InlineData("int", "0", DiagnosticSeverity.Info)]
[InlineData("bool", "false", DiagnosticSeverity.Info)]
[InlineData("char", "'\\0'", DiagnosticSeverity.Info)]
[InlineData("int?", "null", DiagnosticSeverity.Info)]
[InlineData("object", "0", DiagnosticSeverity.Warning)]
public async Task InlineInitialization_CSharp(string type, string value, DiagnosticSeverity severity)
{
string test = $$"""
using System;

class Test
{
[ThreadStatic]
private static {{type}} field {|VSTHRD117:= {{value}}|};
private static {{type}} field {|#0:= {{value}}|};
}
""";

await CSVerify.VerifyAnalyzerAsync(test);
await CSVerify.VerifyAnalyzerAsync(test, new DiagnosticResult(ThreadStaticAnalyzer.TypeInitializerAssignmentId, severity).WithLocation(0));
}

[Fact]
Expand Down Expand Up @@ -366,22 +372,24 @@ End Class
}

[Theory]
[InlineData("Object", "New Object()")]
[InlineData("Object", "Nothing")]
[InlineData("Integer", "42")]
[InlineData("Integer", "0")]
public async Task InlineInitialization_VisualBasic(string type, string value)
[InlineData("Object", "New Object()", DiagnosticSeverity.Warning)]
[InlineData("Object", "Nothing", DiagnosticSeverity.Info)]
[InlineData("Integer", "42", DiagnosticSeverity.Warning)]
[InlineData("Integer", "0", DiagnosticSeverity.Info)]
[InlineData("Boolean", "False", DiagnosticSeverity.Info)]
[InlineData("Object", "0", DiagnosticSeverity.Warning)]
public async Task InlineInitialization_VisualBasic(string type, string value, DiagnosticSeverity severity)
{
string test = $$"""
Imports System

Class Test
<ThreadStatic>
Private Shared field As {{type}} {|VSTHRD117:= {{value}}|}
Private Shared field As {{type}} {|#0:= {{value}}|}
End Class
""";

await VBVerify.VerifyAnalyzerAsync(test);
await VBVerify.VerifyAnalyzerAsync(test, new DiagnosticResult(ThreadStaticAnalyzer.TypeInitializerAssignmentId, severity).WithLocation(0));
}

[Fact]
Expand Down
Loading