DiagnosticCatalog 1.0.1

dotnet add package DiagnosticCatalog --version 1.0.1
                    
NuGet\Install-Package DiagnosticCatalog -Version 1.0.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="DiagnosticCatalog" Version="1.0.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="DiagnosticCatalog" Version="1.0.1" />
                    
Directory.Packages.props
<PackageReference Include="DiagnosticCatalog" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add DiagnosticCatalog --version 1.0.1
                    
#r "nuget: DiagnosticCatalog, 1.0.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package DiagnosticCatalog@1.0.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=DiagnosticCatalog&version=1.0.1
                    
Install as a Cake Addin
#tool nuget:?package=DiagnosticCatalog&version=1.0.1
                    
Install as a Cake Tool

DiagnosticCatalog

🌍 Languages:
πŸ‡¬πŸ‡§ English (this file) | πŸ‡«πŸ‡· FranΓ§ais

Declare analyzer diagnostic rules as strongly referenced constants, so that SuppressMessageAttribute takes compile-checked references instead of magic strings.

One package, both halves: the attributes a catalogue is declared with, and the DCAT analyzers and code fixes that check what you write against it.

The problem

Both arguments of SuppressMessageAttribute are magic strings, and nothing validates either one:

[SuppressMessage("Major Code Smell", "S1144", Justification = "...")]

They differ only in how they fail. Get the id wrong β€” a typo, or a rule the vendor later renamed β€” and the suppression silently does nothing: the warning simply stays, with nothing pointing at the cause. Get the category wrong and nothing happens at all, ever: the .NET platform never reads that argument, so no compiler, analyzer, test or tool can tell you. And you would not guess it β€” S1144's category is "Major Code Smell", not "Code Smell" and not "Maintainability".

// Fails the build instead, if the rule is ever renamed or retired.
[SuppressMessage(SonarRule.S1144.Category, SonarRule.S1144.Id, Justification = "...")]

Thirteen analyzers are already packaged, and the ready-made catalogues lists every one of them β€” a partial list here would only tell a reader their analyzer is not covered. This package is what you need to declare a catalogue of your own; referencing any of those already brings it, and the checks it carries, along with them.

Installation

<PackageReference Include="DiagnosticCatalog" Version="1.0.0" />

Do not add PrivateAssets="all" if your project publishes a catalogue for others to consume. One package carries both halves, so hiding it hides both: your consumers lose [DiagnosticRule], which they need to declare rules of their own and which run-time reflection over your catalogue resolves, and they lose the checks along with it β€” a consumer written the ordinary way stops compiling rather than merely going unchecked. Both halves of that are measured against a real restore by tools/packaging/verify-consumption.sh, in the checks a catalogue hiding the foundation delivers no analyzer either and a catalogue hiding the foundation withholds the attribute assembly.

A catalogue also packs build/<its own package id>.props, setting EnableDiagnosticCatalogAnalyzers, and that is what delivers the analyzers to its consumers β€” the check a catalogue delivers the analyzer to its own consumer. NuGet imports a package's build/ folder for a direct reference and for nothing further out, so the checks reach the project that referenced the catalogue and stop there: an application referencing a library that took a catalogue for its own suppressions is not analysed by a catalogue it never chose, and the library writes nothing to arrange that (ADR-0038). Packaging a catalogue has the file.

A consuming project overrules that in either direction with the same property: false keeps the catalogue and declines the analysis, true asks for the checks from further out than a direct reference.

Declaring a rule

A rule is a static, non-generic class marked [DiagnosticRule], exposing two mandatory public constants. The category must reach a constant declared in a class marked [DiagnosticCategory]:

using DiagnosticCatalog;

namespace JustDummies.Analyzers.Suppressions;

[DiagnosticCategory]
internal static class DummiesCategory
{
    public const string Usage = "Usage";
}

public static class Dummies
{
    [DiagnosticRule]
    public static class JD0007
    {
        public const string Id = nameof(JD0007);
        public const string Category = DummiesCategory.Usage;
    }
}

Both members must be const. A property, a static readonly field or a record cannot be used as an attribute argument, which is also why the contract is structural rather than an interface or a base class.

The category class earns its place on a catalogue of any size: very few distinct categories are spread over very many rules, and declaring each once is what keeps a single spelling per value. The marker is what makes that class legible to tooling, so a fix can offer the named constant in place of a literal. A rule reaching its category any other way is reported as DCAT0011.

Keep container names short β€” every use site pays for them twice. One constraint bounds the shortening: never name the container after the first segment of its own namespace. A consumer writing using JustDummies.Analyzers.Suppressions; resolves JustDummies to the namespace, not to the imported container, and every reference fails with CS0234. The consumer cannot work around it.

Using a rule

using System.Diagnostics.CodeAnalysis;
using JustDummies.Analyzers.Suppressions;

[SuppressMessage(
    Dummies.JD0007.Category,
    Dummies.JD0007.Id,
    Justification = "This member is instantiated by the test infrastructure.")]
public sealed class DummyFactory
{
}

Optional metadata

A rule may carry the remaining DiagnosticDescriptor arguments. Every one of these is a plain string, so this adds no dependency beyond this package:

[DiagnosticRule]
public static class JD0007
{
    public const string Id = nameof(JD0007);
    public const string Category = DummiesCategory.Usage;
    public const string Title = "Dummy factories should follow the expected convention";
    public const string MessageFormat = "Type '{0}' does not follow the convention";
    public const string Description = "Explains the condition detected by the analyzer.";
    public const string HelpLinkUri = "https://justdummies.io/analyzers/JD0007";
}

If you own the analyzer, it can then build its descriptor from the very constants its suppressions reference β€” one source of truth for both:

using Microsoft.CodeAnalysis;

private static readonly DiagnosticDescriptor Descriptor = new(
    JD0007.Id, JD0007.Title, JD0007.MessageFormat, JD0007.Category,
    DiagnosticSeverity.Warning, isEnabledByDefault: true,
    description: JD0007.Description, helpLinkUri: JD0007.HelpLinkUri);

DiagnosticSeverity is constant-capable, so a rule can also expose public const DiagnosticSeverity Severity = DiagnosticSeverity.Warning; β€” but unlike the string constants above, that type lives in Microsoft.CodeAnalysis.Common, so a rule declaring it forces a Roslyn dependency on every consumer of the catalogue. Add it only in a project that already references Microsoft.CodeAnalysis, such as your analyzer itself. A standalone catalogue package should stay on plain strings.

Localised text (LocalizableString, resx-backed descriptors) falls outside the const model; resource files remain the right tool for translated strings.

The checks that come with it

The DCAT analyzers and their code fixes ship inside this package, under analyzers/dotnet/cs/ beside lib/. There is nothing else to reference: they arrive with the foundation, and the foundation arrives with every catalogue built on it.

They check two things: that a rule declaration satisfies the structural contract β€” its shape, its Id, its Category, how that category is reached and what its type name says β€” and that a suppression referencing one is coherent: two arguments that do not name one rule's Category and that same rule's Id, a half-migrated suppression mixing a reference with a literal, a literal that a catalogue reference would replace, and an UnconditionalSuppressMessage the trimmer discards.

A project that consumes a catalogue and declares no rules of its own sees the second set only. The declaration diagnostics report on types marked [DiagnosticRule] and return immediately on everything else.

An analysis assembly never becomes a runtime dependency of the consuming application: tools/packaging/verify-consumption.sh restores this package the way a consumer does and asserts that DiagnosticCatalog.Analyzers.dll and DiagnosticCatalog.CodeFixes.dll stay out of the output folder while DiagnosticCatalog.dll reaches it. Applying [DiagnosticRule] adds no runtime behaviour either β€” the runtime resolves attribute types lazily, so DiagnosticCatalog.dll is never loaded unless something reflects over the rule types.

The analyzers never need the attribute type, only its name: they match DiagnosticCatalog.DiagnosticRuleAttribute by its fully qualified metadata name. A project declaring its own internal sealed class DiagnosticRuleAttribute in the DiagnosticCatalog namespace is therefore checked exactly like one that took the package. What that does not do is deliver the analyzers β€” those arrive with this package, and a project that has hidden it has neither.

Migrating an existing codebase

Adopting a catalogue is not a quiet change: the use-site diagnostics are errors by default (DCAT0001, DCAT0006, DCAT0007, DCAT0009 and DCAT0014), so a literal suppression a catalogue reference would replace fails the build rather than warning. The code fix that rewrites it is how a codebase adopts a catalogue in practice:

[SuppressMessage("Major Code Smell", "S1144", Justification = "kept for reflection")]
// becomes
[SuppressMessage(SonarRule.S1144.Category, SonarRule.S1144.Id, Justification = "kept for reflection")]

One more lands on that same build, and it is an error too: DCAT0014 reports a suppression that carries no Justification. It asks that of every suppression, a literal one included, so unlike DCAT0006 it does not wait for a catalogue to recognise the rule. Presence is all it checks β€” the value is read for its length, never for its meaning. Lower it in .editorconfig while you write the reasons, exactly as you lower DCAT0006 while you convert the pairs.

Fix all occurrences applies it across a document, project or solution in one step, and the using the reference needs is added for you. Everything else in the attribute is left exactly as written β€” Justification, Scope, Target and MessageId are yours.

Two behaviours worth knowing before you run it:

  • The friendly-name suffix is dropped. Visual Studio writes "S1144:Unused private members should be removed"; the fix recognises that form and replaces the whole thing with the reference. The prose lived in the suppression only because there was nothing else to hold it β€” the rule's own documentation has it now.
  • When two catalogues describe the same rule, no fix is offered. The diagnostic still appears, so nothing is hidden, but choosing between them is yours to make.

A suppression left half migrated β€” one reference, one literal β€” is reported too, and completed from the rule the migrated argument already names:

[SuppressMessage(SonarRule.S1144.Category, "S1144", Justification = "kept for reflection")]
// becomes
[SuppressMessage(SonarRule.S1144.Category, SonarRule.S1144.Id, Justification = "kept for reflection")]

Only the literal is rewritten; whatever spelling you chose for the other side, an alias included, is left alone. And if the literal names something the referenced rule does not β€” "S9999" beside SonarRule.S1144.Category β€” you get the diagnostic and no fix. Completing that one would silence a different rule than the one silenced today, which is a decision for you and not for a lightbulb.

When the two arguments name different rules

That one gets two fixes and no recommendation:

Use SonarRule.S1144.Id        β€” keep the category, correct the identifier
Use SonarRule.S2094.Category  β€” keep the identifier, correct the category

Only you know which half was the typo, so neither is offered as the default. Worth knowing while you choose: Roslyn matches a suppression on the identifier alone and never looks at the category, so correcting the category leaves what is suppressed exactly as it is, while correcting the identifier changes it.

Fixes for a rule written by hand

A catalogue is normally generated, and generated code satisfies the contract by construction. When you write one yourself, code fixes are there for the mechanical part:

[DiagnosticRule]
public sealed class JD0007                      // β†’ Make 'JD0007' static
{
    private static readonly string Id = "JD0007";   // β†’ Make 'Id' a public constant
                                                    // β†’ Declare 'public const string Category'
}

Each is offered only where the repair is already written in the code. static is not offered to a generic type, to a struct, or to a class holding an instance member β€” the keyword would not compile there, and removing what blocks it is a change to your design rather than a repair of it. A partial class is refused too: the parts the fix cannot see are the ones that decide.

The member repairs correct modifiers and never the value. A const int Id, a blank string, an initialiser that is not constant β€” those are reported with no fix, because the code says nothing about what you meant.

The one to think about before pressing it. Declare 'public const string Category' writes "TODO". That is a real string, so DCAT0004 stops being reported β€” you have swapped a warning for a marker. A category nobody fills in is wrong forever and invisible in every build, because Roslyn matches a suppression on its identifier alone. Id is different: it is written nameof(JD0007), read off the declaration rather than invented.

What the analyzers do not do

They do not validate an arbitrary string. [SuppressMessage("Usage", "S1144", Justification = "…")] with the wrong category matches no known rule, and that pair is reported by nothing β€” DCAT0014 still asks the same line why it exists, which is a different question. The mechanism that makes a wrong category impossible is the constant itself, which the compiler checks. These analyzers get you to the constants and keep you there.

Recording where a catalogue came from

A catalogue that mirrors somebody else's analyzer is a snapshot. CatalogSource records which upstream release it reflects and when, readable from metadata:

[assembly: CatalogSource(
    source:        "SonarAnalyzer.CSharp",
    sourceVersion: "10.31.0.145097",
    generatedOn:   "2026-07-30")]

The date is a string because attribute arguments must be compile-time constants and no date type can be one; the format is ISO 8601, yyyy-MM-dd. A first-party catalogue maintained alongside its own analyzer does not need this β€” the two ship at one version.

See also

Every rule catalogue built on this package is listed in one place, generated from the analyzers' own descriptors rather than hand-written. If you run one of those analyzers, its rules do not need declaring:

The ready-made catalogues

They are also worth reading as worked examples of the contract above: a container of rules, the categories declared once, and the upstream release the whole thing mirrors recorded in [assembly: CatalogSource].

For the contract explained from scratch rather than by example, see the catalogue author's guide.

Documentation

For declaring a catalogue, in the order the work happens:

  • Publishing a catalogue β€” the structural contract, the shape to actually ship, declaring categories once, and the versioning rule that will bite you if you skip it.
  • Closing the loop with your own analyzer β€” feeding your DiagnosticDescriptor from your own catalogue, and the member that would force Roslyn on every consumer.
  • Versioning a catalogue β€” never delete a rule, never rename a member, and what each change does to the number.
  • Packaging a catalogue β€” what to reference, what propagates to your consumers, and what nuget.org does to your README.

For the checks this package brings with it:

  • The DCAT diagnostics β€” every id these analyzers report, what triggers it, why it exists, whether a code fix is offered, and the .editorconfig key that configures it.
  • Configuration β€” severities, the category-wide switch, generated code, and the PrivateAssets mistake that silences everything.
  • Adopting a catalogue on an existing codebase β€” the severity ramp and what order to convert in, when the migration above is large.
  • The rule contract β€” the five requirements a declaration is checked against, and every syntactic form a use site may take.
  • Troubleshooting β€” by symptom, starting with "nothing is reported at all".

The documentation map picks a page by what you are trying to do; every guide exists in English and French. The specification is the normative version of all of it, including the verified platform behaviour the design relies on.

License

Apache-2.0

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • .NETStandard 2.0

    • No dependencies.
  • net10.0

    • No dependencies.

NuGet packages (15)

Showing the top 5 NuGet packages that depend on DiagnosticCatalog:

Package Downloads
DiagnosticCatalog.NetAnalyzers

The .NET code analysis (CA) rules as strongly referenced constants, so that SuppressMessageAttribute takes compile-checked references instead of magic strings. Ids, categories and help links are read from the analyzers' own DiagnosticDescriptor instances.

DiagnosticCatalog.Sonar

The SonarAnalyzer.CSharp rules as strongly referenced constants, so that SuppressMessageAttribute takes compile-checked references instead of magic strings. Ids and categories are read from the analyzers' own DiagnosticDescriptor instances. Unofficial; not affiliated with or endorsed by SonarSource.

DiagnosticCatalog.Trimming

The .NET trimming, Native AOT and single-file warnings (ILxxxx) as strongly referenced constants, so that UnconditionalSuppressMessageAttribute takes compile-checked references instead of magic strings. Ids and categories are read from the ILLink analyzer's own DiagnosticDescriptor instances. For Blazor WebAssembly, MAUI, Native AOT and trimmable libraries. Unofficial; not affiliated with or endorsed by Microsoft.

DiagnosticCatalog.StyleCop

The StyleCop.Analyzers rules as strongly referenced constants, so that SuppressMessageAttribute takes compile-checked references instead of magic strings. Ids, categories and help links are read from the analyzers' own DiagnosticDescriptor instances. Unofficial; not affiliated with the StyleCop.Analyzers project.

DiagnosticCatalog.CodeStyle

The Roslyn IDE code-style rules (IDExxxx) as strongly referenced constants, so that SuppressMessageAttribute takes compile-checked references instead of magic strings. Ids, categories, titles and help links are read from Microsoft.CodeAnalysis.CSharp.CodeStyle's own DiagnosticDescriptor instances. Unofficial; not affiliated with or endorsed by Microsoft.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.1 2,714 8/7/2026