Programmatically Suppress a DiagnosticAnalayzer

Problem Space

We already learned about DiagnosticAnalyzers and their use cases; they could be reported directly by the compiler, or made by other developers as a Roslyn component (DiagnosticAnalyzer) and integrated into the compilation pipeline and is reported then by compiler as part of the compilation! Nevertheless, there are situations were a developer or team might decide they want to suppress a warning or information diagnostic due to various reasons, like different naming convention chosen by the team than the one the Roslyn analyzer is considers!

In the previous post, we learned how to statically configure Roslyn Analyzers; however, in certain scenarios there might be a need to dynamically telling the compiler to whether suppress a specific diagnostic or not, To Warn or Not to Warn, that's the problem! 😉

Let's consider the following example. in which I have defined an abstract class with a private constructor, this means no one can instantiate an instance of this class and no other class could inherit from it, except the inner classes within the scope of abstract GameEvents class:

public abstract record class GameEvents
{
    private GameEvents() { } 
}

Now, I would have a certain number events related to game, and I would define them all as nested classes of this GameEvents class and inheriting from it, so my extend class would look like the following:

public abstract record class GameEvents
{
    private GameEvents() { } 

    public sealed record class GameStarted(Guid GameId) : GameEvents ;
    public sealed record class GameFinished(Guid GameId) : GameEvents ; 
}

So far so good, now, I have a Game class with a method called Apply and this method accepts an event of type GameEvents, we have a switch expression inside this and based on the actual type of the event we would like to do some operations:

public sealed record class Game(Guid GameId, bool IsFinished)
{
    pulbic Game Apply( GameEvents @event )
    {
        return @event switch 
        {
            GameEvents.GameStarted started => new Game ( started.GameId);
            GameEvents.GameFinished finished => this with { IsFinished = true };
        }
    }
}

Now if we compile our application, we would get a warning indicating that the switch expression does not handle all possible inputs (it is not exhaustive) ( CS8509 ) and it offers us via Code Fix Provider to cover default case by using _ => ! But we know that the code is covering all possible cases, so we don't really need any other expression arm!

PS: If we enable <TreatWarningsAsError> on our project, this warning will be treated even as an error and the application would not be compiled at all!

Of course we could use something like #pragma warning disable CS8509 preprocessor directive to disable this warning, but what If for instance we have not had GameEvents.GameFinished covered by our switch expression? In that situation we would definitely wanted to get this warning (CS8509) and then we had to remove #pragma.

Now, nonetheless, we have all possible branches covered for our GameEvents, we do not want this warning to be reported! We need something that is more dynamic and could automatically detect whether the warning should be suppressed or not! What do we need you ask? A DiagnosticSuppressor. 😉


DiagnosticSuppressor

A DiagnosticSuppressor itself is actually a DiagnosticAnalyzer 😅 It inherits from DiagnosticAnalyzer but it returns no SupportedDiagnostics, instead it has an abstract member to be implemented in inherited classes, SupportedSuppressions, and a method, ReportSuppressions, to be implemented for reporting suppressions:

// source: https://source.dot.net/#Microsoft.CodeAnalysis/DiagnosticAnalyzer/DiagnosticSuppressor.cs,8afaec075a92603b
public abstract class DiagnosticSuppressor : DiagnosticAnalyzer
{
    // Disallow suppressors from reporting diagnostics or registering analysis actions.
    public sealed override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics 
        => ImmutableArray<DiagnosticDescriptor>.Empty;

    public abstract ImmutableArray<SuppressionDescriptor> SupportedSuppressions { get; }

    public abstract void ReportSuppressions(SuppressionAnalysisContext context);
}

What we need to do is to create a class that inherits from DiagnosticSuppressor and implements these two abstract members:

[DiagnosticAnalyzer(LanguageNames.CSharp)]
public class SwitchExpressionAnalyzer : DiagnosticSuppressor
{
}

SupportedSupressions property returns an ImmutableArray<SuppressionDescriptor>, and to create that we need a DiagnosticId that is going to be suppressed, an identifier for this diagnostic suppressor SuppressorId, and a justification message!

private const string DiagnosticId = "CS8509";
private const string SuppressorId = "SP8509";

private static readonly LocalizableString Justification = "This is supressed by a DiagnosticSuppressor";

private readonly SuppressionDescriptor _rule = new(SuppressorId, DiagnosticId, Justification);

public override ImmutableArray<SuppressionDescriptor> SupportedSuppressions => [_rule];

The last piece of the puzzle is to override ReportSuppressions method and call context.ReportSuppression by passing a Suppression

public override void ReportSuppressions(SuppressionAnalysisContext context)
{
    context.CancellationToken.ThrowIfCancellationRequested();
		
		// logic for detecting the issue omitted for brevity: check it on the repository linked at resources section

    var suppression = Suppression.Create(_rule, reportedDiagnostic);
    context.ReportSuppression(suppression);
}

How to activate this newly build suppressor on your project is the same as referencing DiagnosticAnalyzer as soon as it is referenced it will be activated!

<ItemGroup>
    <ProjectReference OutputItemType="Analyzer" ReferenceOutputAssembly="false"
        Include="..\Sample.Analyzers\Sample.Analyzers\Sample.Analyzers.csproj"/>
</ItemGroup>

The result would be like the following:

As you could see, as soon as all the branches are covered the diagnostic is automatically suppressed without a need to have _ => arm (cover-all-cases arm 😅) in the switch expression, and as soon as I am removing one or more branches, it is immediately back!

PS: This implementation in the repository does not all possible combinations, but it is show casing how to solve the issue to a good extent!


Testing

To test DiagnosticSuppressor we could use the same approach as testing any other DiagnosticAnalyzer we nedd the following NuGet packages to be referenced:

<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing" Version="1.1.3-beta1.25564.1" />

If you notice, I have changed the version from 1.1.2 to 1.1.3-beta1.25564.1, it is because the way the DiagnosticSuppressor is treated in the testing library has changed, if you are using the 1.1.2 version, it will have a TypeInitialization issue when a suppressor is reported:

To avoid that, you need to add a custom NuGet feed:

<add key="dotnet-tools"
          value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-tools/nuget/v3/index.json" protocolVersion="3" />

and then reference a beta version (at least at the time of writing this post - November 2025 ) of the testing library as mentioned above, for the full discussion check out this thread on GitHub.

So, let's take a look into one of the tests:

[Fact]
public async Task When_all_cases_are_covered_Then_Dont_ReportDiagnostic()
{
    const string code = """
                            #nullable enable

                            namespace Sample.Console;

                            internal sealed record Game
                            {
                                internal Game? Apply(GamesEvents @event)
                                {
                                    return @event {|#0:switch|}
                                    {
                                        GamesEvents.GameCreated created => this,
                                        GamesEvents.PlayerJoined playerJoined => this,
                                        GamesEvents.GameStarted => this,
                                        GamesEvents.GameEnded => this,
                                    };
                                }
                            }

                            internal abstract record GamesEvents
                            {
                                private GamesEvents()
                                {
                                }

                                internal record GameCreated() : GamesEvents;

                                internal record PlayerJoined() : GamesEvents;

                                internal record GameStarted() : GamesEvents;

                                internal record GameEnded() : GamesEvents;
                            }
                            """;
    // omitted code
}

In this case, since all the cases are covered by the branches, we are expecting a DiagnosticResult at the marked location ( location 0 where we have {|#0:switch|} ) with IsSuppressed property set to true, creating the test analyzer and running it to see the result:

var expectedDiagnostic = new DiagnosticResult("CS8509", DiagnosticSeverity.Warning)
    .WithLocation(0)
    .WithIsSuppressed(true);

    var testAnalyzer = new CSharpAnalyzerTest<SwitchExpressionAnalyzer, DefaultVerifier>()
    {
        TestState =
        {
            Sources = { code },
            ReferenceAssemblies = ReferenceAssemblies.Net.Net90,
            AdditionalReferences = { },
        },
        CompilerDiagnostics = CompilerDiagnostics.All,
        ExpectedDiagnostics = { expectedDiagnostic },
    };

    await testAnalyzer.RunAsync();

I think that's it for today, let's wrap up!

Conclusion

There are a couple of ways to configure DiagnosticAnalyzer in your project, and we have covered most of them in To Warn or Not to Warn: The Art of Suppressing and Enforcing Diagnostics! But sometimes we need something more dynamic than always disabling or enabling a certain diagnostic! That's where a DiagnosticSuppressor shines!

A DiagnosticSuppressor is a specialized DiagnosticAnalyzer that has a special property, SupportedSuppressoins , to override and returns an ImmutableArray<SuppressionDescriptor> and a method, ReportSuppression, in which Suppression for specific DiagnosticId will be reported!

At the end, thanks for reading, enjoy coding and Dametoon Garm [**]

Resources

Buy Me a Coffee at ko-fi.com
An error has occurred. This application may no longer respond until reloaded. Reload x