https://github.com/sator-imaging/csharp-staticfieldanalyzer
Roslyn-based analyzer to diagnose static fields and properties initialization.
https://github.com/sator-imaging/csharp-staticfieldanalyzer
analyzer analyzers roslyn roslyn-analyzer roslyn-analyzers roslyn-code-analysis roslyn-diagnostics
Last synced: 19 days ago
JSON representation
Roslyn-based analyzer to diagnose static fields and properties initialization.
- Host: GitHub
- URL: https://github.com/sator-imaging/csharp-staticfieldanalyzer
- Owner: sator-imaging
- License: mit
- Created: 2024-06-01T00:40:09.000Z (11 months ago)
- Default Branch: main
- Last Pushed: 2024-12-07T01:02:42.000Z (5 months ago)
- Last Synced: 2025-04-09T23:50:37.507Z (19 days ago)
- Topics: analyzer, analyzers, roslyn, roslyn-analyzer, roslyn-analyzers, roslyn-code-analysis, roslyn-diagnostics
- Language: C#
- Homepage:
- Size: 1.4 MB
- Stars: 4
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE.md
Awesome Lists containing this project
README
# Static Field Analyzer for C# / .NET
[](https://www.nuget.org/packages/SatorImaging.StaticMemberAnalyzer)
[Devnote / TODO](#devnote)Roslyn-based analyzer to provide diagnostics of static fields and properties initialization and more.
- Wrong order of static field and property declaration
- Partial type member reference across files
- [Cross-Referencing Problem](#cross-referencing-problem) of static field across type
- [`Enum` type analysis](#enum-analyzer-and-code-fix-provider) to prevent user-level value conversion & [more](#kotlin-like-enum-pattern)
- `struct` parameter-less constructor misuse analysis
- [`Disposable` analyzer](#disposable-analyzer) to detect missing using statement
- `TSelf` generic type argument & type constraint analysis
- Annotating and underlining field, property or etc with custom message
- Find out all diagnostic rules: [RULES.md](RULES.md)## Static Field Analysis

## Enum Type Analysis
Restrict both cast from/to integer number! Disallow user-level enum value conversion completely!!

## `TSelf` Type Argument Analysis
Analyze `TSelf` type argument mismatch and `where` clause mismatch.

## Annotation for Type, Field and Property 💯
There is fancy extra feature to take your attention while coding in Visual Studio. No more need to use `Obsolete` attribute in case of annotating types, methods, fields and properties.
See [the following section](#annotating--underlining) for details.

# Installation
- NuGet
- https://www.nuget.org/packages/SatorImaging.StaticMemberAnalyzer
- ```
PM> Install-Package SatorImaging.StaticMemberAnalyzer
```## Visual Studio 2019 or Earlier
Analyzer is tested on Visual Studio 2022.
You could use this analyzer on older versions of Visual Studio. To do so, update `Vsix` project file by following instructions written in memo and build project.
# Unity Integration
This analyzer can be used with Unity 2020.2 or above. See the following page for detail.
[SatorImaging.StaticMemberAnalyzer.Unity/](SatorImaging.StaticMemberAnalyzer.Unity)
# Cross-Referencing Problem
It is a design bug makes all things complex. Not only that but also it causes initialization error only when meet a specific condition.
So it must be fixed even if app works correctly at a moment, to prevent simple but complicated potential bug which is hard to find in large code base by hand. As you know static fields will never report error when initialization failed!!
```cs
class A {
public static int Value = B.Other;
public static int Other = 310;
}class B {
public static int Other = 620;
public static int Value = A.Other; // will be '0' not '310'
}public static class Test
{
public static void Main()
{
System.Console.WriteLine(A.Value); // 620
System.Console.WriteLine(A.Other); // 310
System.Console.WriteLine(B.Value); // 0 👈👈👈
System.Console.WriteLine(B.Other); // 620// when changing class member access order, it works correctly 🤣
// see the following section for detailed explanation
//System.Console.WriteLine(B.Value); // 310 👈 correct!!
//System.Console.WriteLine(B.Other); // 620
//System.Console.WriteLine(A.Value); // 620
//System.Console.WriteLine(A.Other); // 310
}
}
```**C# Compiler Initialization Sequence**
- `A.Value = B.Other;`
- // 'B' initialization is started by member access
- `B.Other = 620;`
- `B.Value = A.Other;` // BUG: B.Value will be 0 because reading uninitialized `A.Other`
- // then, assign `B.Other` value (620) to `A.Value`
- `A.Other = 310;` // initialized here!! this value is not assigned to B.ValueWhen reading B value first, initialization order is changed and resulting value is also changed accordingly:
- `B.Other = 620;`
- `B.Value = A.Other;`
- // 'A' initialization is started by member access
- `A.Value = B.Other;` // correct: B.Other is initialized before reading value
- `A.Other = 310;`# `Enum` Analyzer and Code Fix Provider
Enum type handling is really headaching. To make enum operation under control, good to avoid user-level enum handling such as converting to integer or string, parse from string and etc.
This analyzer will help centerizing and encapsulating enum handling in app's central enum utility.

## Excluding Enum Type from Obfuscation
Helpful annotation and code fix for enum types to prevent modification of string representation by obfuscation tool.

> [!NOTE]
> `Obfuscation` attribute is from C# base library and it does NOT provide feature to obfuscate compiled assembly. It just provides configuration option to obfuscation tools which recognizing this attribute.## Kotlin-like Enum Pattern
Analysis to help implementing Kotlin-style enum class.
Enum-like type requirements:
- `MyEnumLike[]` or `ReadOnlyMemory` field(s) exist
- analyzer will check field initializer correctness if name is starting with `Entries` (case-sensitive) or ending with `entries` (case-insensitive)
- `sealed` modifier on type
- `private` constructor only
- `public static` member called `Entries` exists
- `public bool Equals` method should not be declared/overridden```cs
public class EnumLike
// ~~~~~~~~ WARN: no `sealed` modifier on type and public constructor exists
// * this warning appears only if type has member called 'Entries'
{
public static readonly EnumLike A = new("A");
public static readonly EnumLike B = new("B");public static ReadOnlySpan Entries => EntriesAsMemory.Span;
// 'Entries' must have all of 'public static readonly' fields in declared order
static readonly EnumLike[] _entries = new[] { B, A };
// ~~~~~~~~~~~~~~ wrong order!!// 'ReadOnlyMemory' can be used instead of array
public static readonly ReadOnlyMemory EntriesAsMemory = new(new[] { A, B });/* === Kotlin style enum template === */
static int AUTO_INCREMENT = 0; // iota
public readonly int Ordinal;
public readonly string Name;private EnumLike(string name) { Ordinal = AUTO_INCREMENT++; Name = name; }
public override string ToString()
{
const string SEP = ": ";
Span span = stackalloc char[Name.Length + 32];Ordinal.TryFormat(span, out var written);
SEP.AsSpan().CopyTo(span.Slice(written));
written += SEP.Length;
Name.AsSpan().CopyTo(span.Slice(written));
written += Name.Length;return span.Slice(0, written).ToString();
}
}
```### Benefits
Benefits
Kotlin-like enum (algebraic data type) can prevent invalid value creation.
```cs
var invalid = Activator.CreateInstance(typeof(EnumLike));if (EnumLike.A == invalid || EnumLike.B == invalid)
{
// this code path won't be reached
// each enum like entry is a class instance and ReferenceEquals match required
}
```Unfortunately, use in `switch` statement is a bit weird.
```cs
var val = EnumLike.A;switch (val)
{
// pattern matching with case guard...!!
case EnumLike when val == EnumLike.A:
System.Console.WriteLine(val);
break;case EnumLike when val == EnumLike.B:
System.Console.WriteLine(val);
break;
}// this pattern generates same AOT compiled code
switch (val)
{
// typeless case guard
case {} when val == EnumLike.A:
System.Console.WriteLine(val);
break;case {} when val == EnumLike.B:
System.Console.WriteLine(val);
break;
}
```# Disposable Analyzer
```cs
var d = new Disposable();
// ~~~~~~~~~~~~~~~~ no `using` statement foundd = (new object()) as IDisposable;
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ cast from/to disposable
```Analyzer won't show warning in the following condition:
- instance is created on `return` statement
- `return new Disposable();`
- assign instance to field or property
- `m_field = new Disposable();`
- cast between disposable types
- `var x = myDisposable as IDisposable;`## Suppress `Disposable` Analysis
To suppress analysis for specified types, declare attribute named `DisposableAnalyzerSuppressor` and add it to assembly.
```cs
[assembly: DisposableAnalyzerSuppressor(typeof(Task), typeof(Task<>))] // Task and Task are ignored by default[Conditional("DEBUG"), AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]
sealed class DisposableAnalyzerSuppressor : Attribute
{
public DisposableAnalyzerSuppressor(params Type[] _) { }
}
```# Annotating / Underlining
There is optional feature to draw underline on selected types, fields, properties, generic type/method arguments and parameters of method, delegate and lambda function.
As of Visual Studio's UX design, `Info` severity diagnostic underlines are drawn only on a few leading chars, not drawn whole marked area. So for workaround, underline on keyword is dashed.

> [!TIP]
> `!`-starting message will add warning annotation on keyword instead of info diagnostic annotation.## How to Use
To avoid dependency to this analyzer, required attribute for underlining is chosen from builtin `System.ComponentModel` assembly so that syntax is little bit weird.
Analyzer is checking identifier keyword in C# source code, not checking actual C# type. `DescriptionAttribute` in C# attribute syntax is the only keyword to draw underline. Omitting `Attribute` or adding namespace are not recognized.
> [!TIP]
> `CategoryAttribute` can be used instead of `DescriptionAttribute`.
>
> By contrast from Description, CategoryAttribute draws underline only on exact type reference and constructors including `base()`. Any inherited types, variables, fields and properties don't get underline.```cs
using System.ComponentModel;[DescriptionAttribute("Draw underline for IDE environment and show this message")]
// ^^^^^^^^^ `Attribute` suffix is required to draw underline
public class WithUnderline
{
[DescriptionAttribute] // parameter-less will draw underline with default message
public static void Method() { }
}// C# language spec allows to omit `Attribute` suffix but when omitted, underline won't be drawn
// to avoid conflict with originally designed usage for VS form designer
[Description("No Underline")]
public class NoUnderline { }// underline won't be drawn when namespace is specified
[System.ComponentModel.DescriptionAttribute("...")]
public static int Underline_Not_Drawn = 0;// this code will draw underline. 'Trivia' is allowed to being added in attribute syntax
[ /**/ DescriptionAttribute ( "Underline will be drawn" ) /* hello, world. */ ]
public static int Underline_Drawn = 310;
```## Verbosity Control
There are 4 types of underline, line head, line leading, line end and keyword.
By default, static field analyzer will draw most verbose underline.
You can omit specific type of underline by using `#pragma` preprocessor directive or adding `SuppressMessage` attribute or etc.
## Unity Tips
Underlining is achieved by using [Description](https://learn.microsoft.com/dotnet/api/system.componentmodel.descriptionattribute) attribute designed for Visual Studio's visual designer, formerly known as form designer.
To remove unnecessary attribute from Unity build, add the following `link.xml` file in Unity project's `Assets` folder.
```xml
```
# Devnote
Steps to publish new version of nuget package
- update nuget package version in `.props`
- upload source code to github
- run build action for test
- merge pull request sent from build action
- create github release
- run nuget packaging action to push new version## TODO
### Disposable Analyzer
#### Known Misdetections
- lambda return statement
- `MethodArg(() => DisposableProperty);`
- `MethodArg(() => { return DisposableProperty; });`
- `?:` operator
- `DisposableProperty = condition ? null : disposableList[index];`### Enum Analyzer Features
- implicit cast suppressor attribute
- `[assembly: EnumAnalyzer(SuppressImplicitCast = true)]`
- ***DO NOT*** suppress cast to `object` `Enum` `string` `int` or other blittable types
- (implicit cast operator is designed function in almost cases. it should be suppressed by default?)
- allow internal only entry for Enum-like types
```cs
sealed class MyEnumLike
{
public static readonly MyEnumLike PublicEntry = new();
internal static readonly MyEnumLike ForDebuggingPurpose = new();
}
```### Underlining Analyzer
- features not supported
- `ITypeParameterObjectCreationOperation`
- `IDefaultValueOperation`
- unnecessary optimization...??
- `ts_singleLocation` --> `ImmutableArray.Create(loc)`
- https://github.com/dotnet/runtime/blob/main/src/libraries/System.Collections.Immutable/src/System/Collections/Immutable/ImmutableArray.cs#L37
- entry method has many `if` statements. seems that ready to be separated
- underlining by `CategoryAttribute`
- lambda analysis
- ...and other if statements can be made more simple by separating analyzer action registration### Optimization
- Implement `IViewTaggerProvider` for underlining analyzer.