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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ If you are already using other analyzers, you can check [which rules are duplica
|[MA0192](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0192.md)|Usage|Use HasFlag instead of bitwise checks|ℹ️|❌|✔️|
|[MA0193](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0193.md)|Usage|Use an overload with a MidpointRounding argument|ℹ️|✔️|✔️|
|[MA0194](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0194.md)|Usage|Merge is expressions on the same value|ℹ️|✔️|✔️|
|[MA0195](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0195.md)|Usage|Do not use static fields before they are initialized|⚠️|✔️|❌|

<!-- rules -->

Expand Down
7 changes: 7 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@
|[MA0192](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0192.md)|Usage|Use HasFlag instead of bitwise checks|<span title='Info'>ℹ️</span>|❌|✔️|
|[MA0193](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0193.md)|Usage|Use an overload with a MidpointRounding argument|<span title='Info'>ℹ️</span>|✔️|✔️|
|[MA0194](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0194.md)|Usage|Merge is expressions on the same value|<span title='Info'>ℹ️</span>|✔️|✔️|
|[MA0195](https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0195.md)|Usage|Do not use static fields before they are initialized|<span title='Warning'>⚠️</span>|✔️|❌|

|Id|Suppressed rule|Justification|
|--|---------------|-------------|
Expand Down Expand Up @@ -787,6 +788,9 @@ dotnet_diagnostic.MA0193.severity = suggestion

# MA0194: Merge is expressions on the same value
dotnet_diagnostic.MA0194.severity = suggestion

# MA0195: Do not use static fields before they are initialized
dotnet_diagnostic.MA0195.severity = warning
```

# .editorconfig - all rules disabled
Expand Down Expand Up @@ -1367,4 +1371,7 @@ dotnet_diagnostic.MA0193.severity = none

# MA0194: Merge is expressions on the same value
dotnet_diagnostic.MA0194.severity = none

# MA0195: Do not use static fields before they are initialized
dotnet_diagnostic.MA0195.severity = none
```
31 changes: 31 additions & 0 deletions docs/Rules/MA0195.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# MA0195 - Do not use static fields before they are initialized
<!-- sources -->
Source: [DoNotUseNotYetInitializedStaticFieldAnalyzer.cs](https://github.com/meziantou/Meziantou.Analyzer/blob/main/src/Meziantou.Analyzer/Rules/DoNotUseNotYetInitializedStaticFieldAnalyzer.cs)
<!-- sources -->

Static fields are initialized in declaration order. Reading another static field in a field initializer can observe the default value when that field is declared later.

This rule reports references to static fields from static field initializers when:

- the referenced field is declared later in the same partial declaration, or
- the referenced field is declared in another partial declaration of the same type.

Fields without an explicit initializer are ignored.

````csharp
public class Example
{
public static readonly bool[] Both = new[] { P1, P2 }; // MA0195
public static readonly bool P1 = true;
public static readonly bool P2 = false;
}
````

````csharp
public class Example
{
public static readonly bool P1 = true;
public static readonly bool P2 = false;
public static readonly bool[] Both = new[] { P1, P2 }; // compliant
}
````
Original file line number Diff line number Diff line change
Expand Up @@ -577,3 +577,6 @@ dotnet_diagnostic.MA0193.severity = suggestion

# MA0194: Merge is expressions on the same value
dotnet_diagnostic.MA0194.severity = suggestion

# MA0195: Do not use static fields before they are initialized
dotnet_diagnostic.MA0195.severity = warning
3 changes: 3 additions & 0 deletions src/Meziantou.Analyzer.Pack/configuration/none.editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -577,3 +577,6 @@ dotnet_diagnostic.MA0193.severity = none

# MA0194: Merge is expressions on the same value
dotnet_diagnostic.MA0194.severity = none

# MA0195: Do not use static fields before they are initialized
dotnet_diagnostic.MA0195.severity = none
1 change: 1 addition & 0 deletions src/Meziantou.Analyzer/RuleIdentifiers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ internal static class RuleIdentifiers
public const string UseHasFlagMethod = "MA0192";
public const string UseAnOverloadThatHasMidpointRounding = "MA0193";
public const string MergeIsPatternChecks = "MA0194";
public const string DoNotUseNotYetInitializedStaticField = "MA0195";

public static string GetHelpUri(string identifier)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
using System.Collections.Concurrent;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using Meziantou.Analyzer.Internals;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;

namespace Meziantou.Analyzer.Rules;

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public sealed class DoNotUseNotYetInitializedStaticFieldAnalyzer : DiagnosticAnalyzer
{
private static readonly DiagnosticDescriptor Rule = new(
RuleIdentifiers.DoNotUseNotYetInitializedStaticField,
title: "Do not use static fields before they are initialized",
messageFormat: "Static field '{0}' may not be initialized yet",
RuleCategories.Usage,
DiagnosticSeverity.Warning,
isEnabledByDefault: true,
description: "",
helpLinkUri: RuleIdentifiers.GetHelpUri(RuleIdentifiers.DoNotUseNotYetInitializedStaticField));

public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics => ImmutableArray.Create(Rule);

public override void Initialize(AnalysisContext context)
{
context.EnableConcurrentExecution();
context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.ReportDiagnostics);

context.RegisterCompilationStartAction(context =>
{
var fieldDeclarationInfos = new ConcurrentDictionary<IFieldSymbol, FieldDeclarationInfo?>(SymbolEqualityComparer.Default);
context.RegisterOperationAction(context => AnalyzeFieldReference(context, fieldDeclarationInfos), OperationKind.FieldReference);
});
}

private static void AnalyzeFieldReference(OperationAnalysisContext context, ConcurrentDictionary<IFieldSymbol, FieldDeclarationInfo?> fieldDeclarationInfos)
{
var fieldReferenceOperation = (IFieldReferenceOperation)context.Operation;
if (fieldReferenceOperation.IsInNameofOperation())
return;

if (IsInDeferredExecutionContext(fieldReferenceOperation))
return;

var referencedField = fieldReferenceOperation.Field;
if (referencedField is not { IsImplicitlyDeclared: false, IsStatic: true, IsConst: false })
return;

if (!TryGetContainingFieldInitializerField(fieldReferenceOperation, out var currentField))
return;

if (!referencedField.ContainingType.IsEqualTo(currentField.ContainingType))
return;

if (referencedField.IsEqualTo(currentField))
return;

var currentFieldInfo = GetFieldDeclarationInfo(currentField, fieldDeclarationInfos, context.CancellationToken);
if (currentFieldInfo is null)
return;

var referencedFieldInfo = GetFieldDeclarationInfo(referencedField, fieldDeclarationInfos, context.CancellationToken);
if (referencedFieldInfo is null || referencedFieldInfo.Value.Initializer is null)
return;

if (!ShouldReport(currentFieldInfo.Value, referencedFieldInfo.Value))
return;

context.ReportDiagnostic(Rule, fieldReferenceOperation, referencedField.Name);
}

private static bool IsInDeferredExecutionContext(IOperation operation)
{
foreach (var ancestor in operation.Ancestors())
{
if (ancestor is IAnonymousFunctionOperation or ILocalFunctionOperation)
return true;
}

return false;
}

private static bool TryGetContainingFieldInitializerField(IOperation operation, [NotNullWhen(true)] out IFieldSymbol? field)
{
foreach (var ancestor in operation.Ancestors())
{
if (ancestor is IFieldInitializerOperation fieldInitializerOperation)
{
var initializedField = fieldInitializerOperation.InitializedFields.FirstOrDefault(field => field is { IsImplicitlyDeclared: false, IsStatic: true, IsConst: false });
if (initializedField is not null)
{
field = initializedField;
return true;
}
}
}

field = null;
return false;
}

private static bool ShouldReport(FieldDeclarationInfo currentField, FieldDeclarationInfo referencedField)
{
if (!currentField.IsInSamePartialDeclarationAs(referencedField))
return true;

return referencedField.DeclaratorStart > currentField.DeclaratorStart;
}

private static FieldDeclarationInfo? GetFieldDeclarationInfo(IFieldSymbol field, ConcurrentDictionary<IFieldSymbol, FieldDeclarationInfo?> cache, CancellationToken cancellationToken)
{
if (cache.TryGetValue(field, out var result))
return result;

result = CreateFieldDeclarationInfo(field, cancellationToken);
cache.TryAdd(field, result);
return result;
}

private static FieldDeclarationInfo? CreateFieldDeclarationInfo(IFieldSymbol field, CancellationToken cancellationToken)
{
if (field.DeclaringSyntaxReferences is not [var syntaxReference])
return null;

if (syntaxReference.GetSyntax(cancellationToken) is not VariableDeclaratorSyntax variableDeclarator)
return null;

if (variableDeclarator.FirstAncestorOrSelf<TypeDeclarationSyntax>() is not TypeDeclarationSyntax typeDeclaration)
return null;

return new(
variableDeclarator.SyntaxTree,
typeDeclaration.SpanStart,
variableDeclarator.SpanStart,
variableDeclarator.Initializer);
}

private readonly record struct FieldDeclarationInfo(SyntaxTree SyntaxTree, int TypeDeclarationSpanStart, int DeclaratorStart, EqualsValueClauseSyntax? Initializer)
{
public bool IsInSamePartialDeclarationAs(FieldDeclarationInfo other)
{
return TypeDeclarationSpanStart == other.TypeDeclarationSpanStart && SyntaxTree == other.SyntaxTree;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
using Meziantou.Analyzer.Rules;
using TestHelper;

namespace Meziantou.Analyzer.Test.Rules;

public sealed class DoNotUseNotYetInitializedStaticFieldAnalyzerTests
{
private static ProjectBuilder CreateProjectBuilder()
{
return new ProjectBuilder()
.WithAnalyzer<DoNotUseNotYetInitializedStaticFieldAnalyzer>();
}

[Fact]
public async Task ReportDiagnostic_WhenReferencingLaterFieldInSamePart()
{
await CreateProjectBuilder()
.WithSourceCode("""
class Sample
{
private static readonly bool[] Values = new[] { [|P1|], [|P2|] };
private static readonly bool P1 = true;
private static readonly bool P2 = false;
}
""")
.ValidateAsync();
}

[Fact]
public async Task NoDiagnostic_WhenReferencingEarlierFieldInSamePart()
{
await CreateProjectBuilder()
.WithSourceCode("""
class Sample
{
private static readonly bool P1 = true;
private static readonly bool P2 = false;
private static readonly bool[] Values = new[] { P1, P2 };
}
""")
.ValidateAsync();
}

[Fact]
public async Task ReportDiagnostic_WhenReferencingFieldFromAnotherPartialDeclaration()
{
await CreateProjectBuilder()
.WithSourceCode("""
partial class Sample
{
private static readonly bool P1 = true;
}

partial class Sample
{
private static readonly bool[] Values = new[] { [|P1|] };
}
""")
.ValidateAsync();
}

[Fact]
public async Task NoDiagnostic_WhenPartialDeclarationsOnlyReferenceEarlierFieldsInSamePart()
{
await CreateProjectBuilder()
.WithSourceCode("""
partial class Sample
{
private static readonly int P1 = 1;
private static readonly int[] Values1 = new[] { P1 };
}

partial class Sample
{
private static readonly int P2 = 2;
private static readonly int[] Values2 = new[] { P2 };
}
""")
.ValidateAsync();
}

[Fact]
public async Task NoDiagnostic_WhenReferencingFieldFromAnotherPartialDeclarationWithoutInitializer()
{
await CreateProjectBuilder()
.WithSourceCode("""
partial class Sample
{
private static readonly int Other;
}

partial class Sample
{
private static readonly int Value = Other;
}
""")
.ValidateAsync();
}

[Fact]
public async Task NoDiagnostic_WhenReferencedFieldHasNoInitializer()
{
await CreateProjectBuilder()
.WithSourceCode("""
class Sample
{
private static readonly int Value = Other;
private static readonly int Other;
}
""")
.ValidateAsync();
}

[Fact]
public async Task NoDiagnostic_WhenReferenceIsInNameof()
{
await CreateProjectBuilder()
.WithSourceCode("""
class Sample
{
private static readonly string Value = nameof(Other);
private static readonly int Other = 42;
}
""")
.ValidateAsync();
}

[Fact]
public async Task NoDiagnostic_WhenReferenceIsInLambda()
{
await CreateProjectBuilder()
.WithSourceCode("""
class Sample
{
private static readonly System.Func<int> ValueFactory = () => Other;
private static readonly int Other = 42;
}
""")
.ValidateAsync();
}
}
Loading