{"id":22544323,"url":"https://github.com/sator-imaging/csharp-staticfieldanalyzer","last_synced_at":"2025-04-09T23:50:44.547Z","repository":{"id":242246154,"uuid":"808844717","full_name":"sator-imaging/CSharp-StaticFieldAnalyzer","owner":"sator-imaging","description":"Roslyn-based analyzer to diagnose static fields and properties initialization.","archived":false,"fork":false,"pushed_at":"2024-12-07T01:02:42.000Z","size":1471,"stargazers_count":4,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-04-09T23:50:37.507Z","etag":null,"topics":["analyzer","analyzers","roslyn","roslyn-analyzer","roslyn-analyzers","roslyn-code-analysis","roslyn-diagnostics"],"latest_commit_sha":null,"homepage":"","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/sator-imaging.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE.md","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null},"funding":{"github":["sator-imaging"]}},"created_at":"2024-06-01T00:40:09.000Z","updated_at":"2024-12-08T15:49:32.000Z","dependencies_parsed_at":"2024-06-01T17:53:49.857Z","dependency_job_id":"e9a7a909-da08-4d28-901b-41a363bb36d9","html_url":"https://github.com/sator-imaging/CSharp-StaticFieldAnalyzer","commit_stats":null,"previous_names":["sator-imaging/csharp-staticfieldanalyzer"],"tags_count":7,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sator-imaging%2FCSharp-StaticFieldAnalyzer","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sator-imaging%2FCSharp-StaticFieldAnalyzer/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sator-imaging%2FCSharp-StaticFieldAnalyzer/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/sator-imaging%2FCSharp-StaticFieldAnalyzer/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/sator-imaging","download_url":"https://codeload.github.com/sator-imaging/CSharp-StaticFieldAnalyzer/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248131454,"owners_count":21052819,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["analyzer","analyzers","roslyn","roslyn-analyzer","roslyn-analyzers","roslyn-code-analysis","roslyn-diagnostics"],"created_at":"2024-12-07T14:06:57.278Z","updated_at":"2025-04-09T23:50:44.528Z","avatar_url":"https://github.com/sator-imaging.png","language":"C#","funding_links":["https://github.com/sponsors/sator-imaging"],"categories":[],"sub_categories":[],"readme":"# Static Field Analyzer for C# / .NET\n\n[![NuGet](https://img.shields.io/nuget/v/SatorImaging.StaticMemberAnalyzer)](https://www.nuget.org/packages/SatorImaging.StaticMemberAnalyzer)\n\u0026nbsp;\u003csup\u003e[Devnote / TODO](#devnote)\u003c/sup\u003e\n\nRoslyn-based analyzer to provide diagnostics of static fields and properties initialization and more.\n\n- Wrong order of static field and property declaration\n- Partial type member reference across files\n- [Cross-Referencing Problem](#cross-referencing-problem) of static field across type\n- [`Enum` type analysis](#enum-analyzer-and-code-fix-provider) to prevent user-level value conversion \u0026 [more](#kotlin-like-enum-pattern)\n- `struct` parameter-less constructor misuse analysis\n- [`Disposable` analyzer](#disposable-analyzer) to detect missing using statement\n- `TSelf` generic type argument \u0026 type constraint analysis\n- Annotating and underlining field, property or etc with custom message\n- Find out all diagnostic rules: [RULES.md](RULES.md)\n\n\n## Static Field Analysis\n\n![Analyzer in Action](https://raw.githubusercontent.com/sator-imaging/CSharp-StaticFieldAnalyzer/main/assets/InAction.gif)\n\n## Enum Type Analysis\n\nRestrict both cast from/to integer number! Disallow user-level enum value conversion completely!!\n\n![Enum Analyzer](https://raw.githubusercontent.com/sator-imaging/CSharp-StaticFieldAnalyzer/main/assets/EnumAnalyzer.png)\n\n## `TSelf` Type Argument Analysis\n\nAnalyze `TSelf` type argument mismatch and `where` clause mismatch.\n\n![TSelf Type Argument](https://raw.githubusercontent.com/sator-imaging/CSharp-StaticFieldAnalyzer/main/assets/GenericTypeArgTSelf.png)\n\n\n\n## Annotation for Type, Field and Property 💯\n\nThere 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.\n\nSee [the following section](#annotating--underlining) for details.\n\n\n![Draw Underline](https://raw.githubusercontent.com/sator-imaging/CSharp-StaticFieldAnalyzer/main/assets/DrawUnderline.png)\n\n\n\n\n\n# Installation\n\n- NuGet\n\t- https://www.nuget.org/packages/SatorImaging.StaticMemberAnalyzer\n    - ```\n      PM\u003e Install-Package SatorImaging.StaticMemberAnalyzer\n      ```\n\n\n## Visual Studio 2019 or Earlier\n\nAnalyzer is tested on Visual Studio 2022.\n\nYou 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.\n\n\n\n\n\n# Unity Integration\n\nThis analyzer can be used with Unity 2020.2 or above. See the following page for detail.\n\n[SatorImaging.StaticMemberAnalyzer.Unity/](SatorImaging.StaticMemberAnalyzer.Unity)\n\n\n\n\n\n# Cross-Referencing Problem\n\nIt is a design bug makes all things complex. Not only that but also it causes initialization error only when meet a specific condition.\n\nSo 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!!\n\n\n```cs\nclass A {\n    public static int Value = B.Other;\n    public static int Other = 310;\n}\n\nclass B {\n    public static int Other = 620;\n    public static int Value = A.Other;  // will be '0' not '310'\n}\n\npublic static class Test\n{\n    public static void Main()\n    {\n        System.Console.WriteLine(A.Value);  // 620\n        System.Console.WriteLine(A.Other);  // 310\n        System.Console.WriteLine(B.Value);  // 0   👈👈👈\n        System.Console.WriteLine(B.Other);  // 620\n\n        // when changing class member access order, it works correctly 🤣\n        // see the following section for detailed explanation\n        //System.Console.WriteLine(B.Value);  // 310  👈 correct!!\n        //System.Console.WriteLine(B.Other);  // 620\n        //System.Console.WriteLine(A.Value);  // 620\n        //System.Console.WriteLine(A.Other);  // 310\n    }\n}\n```\n\n\n**C# Compiler Initialization Sequence**\n\n- `A.Value = B.Other;`\n    - // 'B' initialization is started by member access\n    - `B.Other = 620;`\n    - `B.Value = A.Other;`  // BUG: B.Value will be 0 because reading uninitialized `A.Other`\n    - // then, assign `B.Other` value (620) to `A.Value`\n- `A.Other = 310;`  // initialized here!! this value is not assigned to B.Value\n\n\nWhen reading B value first, initialization order is changed and resulting value is also changed accordingly:\n\n- `B.Other = 620;`\n- `B.Value = A.Other;`\n    - // 'A' initialization is started by member access\n    - `A.Value = B.Other;`  // correct: B.Other is initialized before reading value\n    - `A.Other = 310;`\n\n\n\n\n\n# `Enum` Analyzer and Code Fix Provider\n\nEnum 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.\n\nThis analyzer will help centerizing and encapsulating enum handling in app's central enum utility.\n\n![Enum Analyzer](https://raw.githubusercontent.com/sator-imaging/CSharp-StaticFieldAnalyzer/main/assets/EnumAnalyzer.png)\n\n\n## Excluding Enum Type from Obfuscation\n\nHelpful annotation and code fix for enum types to prevent modification of string representation by obfuscation tool.\n\n![Enum Code Fix](https://raw.githubusercontent.com/sator-imaging/CSharp-StaticFieldAnalyzer/main/assets/EnumCodeFix.png)\n\n\u003e [!NOTE]\n\u003e `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.\n\n\n## Kotlin-like Enum Pattern\n\nAnalysis to help implementing Kotlin-style enum class.\n\nEnum-like type requirements:\n- `MyEnumLike[]` or `ReadOnlyMemory\u003cMyEnumLike\u003e` field(s) exist\n    - analyzer will check field initializer correctness if name is starting with `Entries` (case-sensitive) or ending with `entries` (case-insensitive)\n- `sealed` modifier on type\n- `private` constructor only\n- `public static` member called `Entries` exists\n- `public bool Equals` method should not be declared/overridden\n\n\n```cs\npublic class EnumLike\n//           ~~~~~~~~ WARN: no `sealed` modifier on type and public constructor exists\n//                          * this warning appears only if type has member called 'Entries'\n{\n    public static readonly EnumLike A = new(\"A\");\n    public static readonly EnumLike B = new(\"B\");\n\n    public static ReadOnlySpan\u003cEnumLike\u003e Entries =\u003e EntriesAsMemory.Span;\n\n    // 'Entries' must have all of 'public static readonly' fields in declared order\n    static readonly EnumLike[] _entries = new[] { B, A };\n    //                                    ~~~~~~~~~~~~~~ wrong order!!\n\n    // 'ReadOnlyMemory\u003cT\u003e' can be used instead of array\n    public static readonly ReadOnlyMemory\u003cEnumLike\u003e EntriesAsMemory = new(new[] { A, B });\n\n\n    /* ===  Kotlin style enum template  === */\n\n    static int AUTO_INCREMENT = 0;  // iota\n\n    public readonly int Ordinal;\n    public readonly string Name;\n\n    private EnumLike(string name) { Ordinal = AUTO_INCREMENT++; Name = name; }\n\n    public override string ToString()\n    {\n        const string SEP = \": \";\n        Span\u003cchar\u003e span = stackalloc char[Name.Length + 32];\n\n        Ordinal.TryFormat(span, out var written);\n        SEP.AsSpan().CopyTo(span.Slice(written));\n        written += SEP.Length;\n        Name.AsSpan().CopyTo(span.Slice(written));\n        written += Name.Length;\n\n        return span.Slice(0, written).ToString();\n    }\n}\n```\n\n\n### Benefits\n\n\u003cp\u003e\u003cdetails lang=\"en\" --open\u003e\u003csummary\u003eBenefits\u003c/summary\u003e\n\nKotlin-like enum (algebraic data type) can prevent invalid value creation.\n\n```cs\nvar invalid = Activator.CreateInstance(typeof(EnumLike));\n\nif (EnumLike.A == invalid || EnumLike.B == invalid)\n{\n    // this code path won't be reached\n    // each enum like entry is a class instance and ReferenceEquals match required\n}\n```\n\n\nUnfortunately, use in `switch` statement is a bit weird.\n\n```cs\nvar val = EnumLike.A;\n\nswitch (val)\n{\n    // pattern matching with case guard...!!\n    case EnumLike when val == EnumLike.A:\n        System.Console.WriteLine(val);\n        break;\n\n    case EnumLike when val == EnumLike.B:\n        System.Console.WriteLine(val);\n        break;\n}\n\n// this pattern generates same AOT compiled code\nswitch (val)\n{\n    // typeless case guard\n    case {} when val == EnumLike.A:\n        System.Console.WriteLine(val);\n        break;\n\n    case {} when val == EnumLike.B:\n        System.Console.WriteLine(val);\n        break;\n}\n```\n\n\u003c!------- End of Details EN Tag -------\u003e\u003c/details\u003e\u003c/p\u003e\n\n\n\n\n\n# Disposable Analyzer\n\n```cs\nvar d = new Disposable();\n//      ~~~~~~~~~~~~~~~~ no `using` statement found\n\nd = (new object()) as IDisposable;\n//  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ cast from/to disposable\n```\n\n\nAnalyzer won't show warning in the following condition:\n- instance is created on `return` statement\n    - `return new Disposable();`\n- assign instance to field or property\n    - `m_field = new Disposable();`\n- cast between disposable types\n    - `var x = myDisposable as IDisposable;`\n\n\n\n## Suppress `Disposable` Analysis\n\nTo suppress analysis for specified types, declare attribute named `DisposableAnalyzerSuppressor` and add it to assembly.\n\n```cs\n[assembly: DisposableAnalyzerSuppressor(typeof(Task), typeof(Task\u003c\u003e))]  // Task and Task\u003cT\u003e are ignored by default\n\n[Conditional(\"DEBUG\"), AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)]\nsealed class DisposableAnalyzerSuppressor : Attribute\n{\n    public DisposableAnalyzerSuppressor(params Type[] _) { }\n}\n```\n\n\n\n\n\n# Annotating / Underlining\n\nThere is optional feature to draw underline on selected types, fields, properties, generic type/method arguments and parameters of method, delegate and lambda function.\n\nAs 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.\n\n\n![Draw Underline](https://raw.githubusercontent.com/sator-imaging/CSharp-StaticFieldAnalyzer/main/assets/DrawUnderline.png)\n\n\u003e [!TIP]\n\u003e `!`-starting message will add warning annotation on keyword instead of info diagnostic annotation.\n\n\n## How to Use\n\nTo avoid dependency to this analyzer, required attribute for underlining is chosen from builtin `System.ComponentModel` assembly so that syntax is little bit weird.\n\nAnalyzer 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.\n\n\n\u003e [!TIP]\n\u003e `CategoryAttribute` can be used instead of `DescriptionAttribute`.\n\u003e\n\u003e 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.\n\n\n```cs\nusing System.ComponentModel;\n\n[DescriptionAttribute(\"Draw underline for IDE environment and show this message\")]\n//          ^^^^^^^^^ `Attribute` suffix is required to draw underline\npublic class WithUnderline\n{\n    [DescriptionAttribute]  // parameter-less will draw underline with default message\n    public static void Method() { }\n}\n\n// C# language spec allows to omit `Attribute` suffix but when omitted, underline won't be drawn\n// to avoid conflict with originally designed usage for VS form designer\n[Description(\"No Underline\")]\npublic class NoUnderline { }\n\n// underline won't be drawn when namespace is specified\n[System.ComponentModel.DescriptionAttribute(\"...\")]\npublic static int Underline_Not_Drawn = 0;\n\n// this code will draw underline. 'Trivia' is allowed to being added in attribute syntax\n[ /**/  DescriptionAttribute   (   \"Underline will be drawn\" )   /* hello, world. */   ]\npublic static int Underline_Drawn = 310;\n```\n\n\n\n## Verbosity Control\n\nThere are 4 types of underline, line head, line leading, line end and keyword.\n\nBy default, static field analyzer will draw most verbose underline.\nYou can omit specific type of underline by using `#pragma` preprocessor directive or adding `SuppressMessage` attribute or etc.\n\n\n![Verbosity Control](https://raw.githubusercontent.com/sator-imaging/CSharp-StaticFieldAnalyzer/main/assets/VerbosityControl.png)\n\n\n\n## Unity Tips\n\nUnderlining 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.\n\nTo remove unnecessary attribute from Unity build, add the following `link.xml` file in Unity project's `Assets` folder.\n\n```xml\n\u003clinker\u003e\n    \u003cassembly fullname=\"System.ComponentModel\"\u003e\n        \u003ctype fullname=\"System.ComponentModel.DescriptionAttribute\" preserve=\"nothing\"/\u003e\n    \u003c/assembly\u003e\n\u003c/linker\u003e\n```\n\n\n\n\u0026nbsp;  \n\u0026nbsp;  \n\n# Devnote\n\nSteps to publish new version of nuget package\n- update nuget package version in `.props`\n- upload source code to github\n- run build action for test\n- merge pull request sent from build action\n- create github release\n- run nuget packaging action to push new version\n\n\n## TODO\n\n### Disposable Analyzer\n\n#### Known Misdetections\n\n- lambda return statement\n    - `MethodArg(() =\u003e DisposableProperty);`\n    - `MethodArg(() =\u003e { return DisposableProperty; });`\n- `?:` operator\n    - `DisposableProperty = condition ? null : disposableList[index];` \n\n\n### Enum Analyzer Features\n- implicit cast suppressor attribute\n    - `[assembly: EnumAnalyzer(SuppressImplicitCast = true)]`\n        - ***DO NOT*** suppress cast to `object` `Enum` `string` `int` or other blittable types\n        - (implicit cast operator is designed function in almost cases. it should be suppressed by default?)\n- allow internal only entry for Enum-like types\n  ```cs\n  sealed class MyEnumLike\n  {\n      public static readonly MyEnumLike PublicEntry = new();\n      internal static readonly MyEnumLike ForDebuggingPurpose = new();\n  }\n  ```\n\n\n### Underlining Analyzer\n\n- features not supported\n    - `ITypeParameterObjectCreationOperation`\n    - `IDefaultValueOperation`\n- unnecessary optimization...??\n    - `ts_singleLocation` --\u003e `ImmutableArray.Create(loc)`\n    - https://github.com/dotnet/runtime/blob/main/src/libraries/System.Collections.Immutable/src/System/Collections/Immutable/ImmutableArray.cs#L37\n- entry method has many `if` statements. seems that ready to be separated\n    - underlining by `CategoryAttribute`\n    - lambda analysis\n    - ...and other if statements can be made more simple by separating analyzer action registration\n\n\n### Optimization\n\n- Implement `IViewTaggerProvider` for underlining analyzer.\n\n\n\n\n\n\u003c!--\n\n\u0026nbsp;  \n\u0026nbsp;  \n\n\n# Off-topic: Why not `const`?\n\n## Effective C#\n\nIn Effective C#, it describes that runtime constant `readonly static` is better than compile-time constant `const`.\n\nFor example, when there are 2 libralies, MyLib.dll and External.dll\n- External.dll has public constant value `10.1f`\n- MyLib.dll read that value and compiled as managed assembly\n- Then, replacing External.dll which has updated constant value `20.2f`\n\nIn this case, MyLib.dll will continue to use it's compile-time constant value `10.1f` read from old External.dll, until it is recompiled.\n\n\u003e ie. constant values are \"burned\" into compiled assembly.\n\n\nSo, using runtime constant is better than `const` in shared libraries.\n\n\n\n## `const string` can be easily listed up\n\nWhen you store your api end point (costs each access) or api key or something secret as `const string`, those are easily retrieved by `strings YourApp.exe` command, or by C# decompilers when compiled as managed code assembly.\n\nOf course using `readonly static string` won't solve the problem perfectly, but worth to consider use to obfuscate secrets keys/values.\n\n--\u003e\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsator-imaging%2Fcsharp-staticfieldanalyzer","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsator-imaging%2Fcsharp-staticfieldanalyzer","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsator-imaging%2Fcsharp-staticfieldanalyzer/lists"}