Search code examples
c#roslynsuppress-warnings

Can I suppress custom compiler errors with Roslyn?


I work with a platform that heavily uses reflection and code generation to initialize members of certain special types. It looks like this:

public class MyGraph : PXGraph<MyGraph>
{
    public PXSelect<DBTable> view;
}

I want to add nullable reference types analysis to my code. However, I can't find an elegant solution which would avoid multiple warnings about non-nullable fields being uninitialized. They are displayed either on fields or on the type constructor. The platform initializes them

I know that I can suppress warnings from the nullable analysis. However, I don't like the way it looks. I don't like pragmas because they clutter the code, and I don't like suppression attributes even more.

I don't like the following declaration because it looks like nonsense to me:

public class MyGraph : PXGraph<MyGraph>
{
    public PXSelect<DBTable> view = null!; // or = default!
}

I also tried nullable attributes without much benefit.

I tried to use the required keyword like this:

public class MyGraph : PXGraph<MyGraph>
{
    public required PXSelect<DBTable> view;
}

I like this declaration the most but I receive compiler error about uninitialized field.

Is there a way to suppress compiler warnings in a more sophisticated way? Can I use Roslyn to somehow affect compiler diagnostics?


Solution

  • As @marc-gravell mentioned there is a DiagnosticSuppressor API in Roslyn. I have used it for similar things. It even works if you add the analyzer with the suppressor to your project as a nuget package.

    There is a downside though, Rider IDE does not support this Roslyn functionality. So, use it at your own risk.

    As for the code, it should look like this:

    using System;
    using System.Collections.Generic;
    using System.Collections.Immutable;
    using System.Linq;
    using System.Threading;
    
    using Microsoft.CodeAnalysis;
    using Microsoft.CodeAnalysis.CSharp;
    using Microsoft.CodeAnalysis.CSharp.Syntax;
    using Microsoft.CodeAnalysis.Diagnostics;
    
    namespace NullableDiagnosticHider
    {
        /// <summary>
        /// Customized version of CS8524 diagnostic which is the one reported by C# compiler in this case.
        /// </summary>
        [DiagnosticAnalyzer(LanguageNames.CSharp)]
        public class NullableDiagnosticHider : DiagnosticSuppressor
        {
            public static readonly SuppressionDescriptor NonNullableUninitializedAfterConstructorDiagnostic =
                new SuppressionDescriptor("PXS001", "CS8618", "<Describe the reason for suppression>");
       
            public override ImmutableArray<SuppressionDescriptor> SupportedSuppressions { get; } = ImmutableArray.Create(NonNullableUninitializedAfterConstructorDiagnostic);
        
            public override void ReportSuppressions(SuppressionAnalysisContext context)
            {
                if (context.ReportedDiagnostics.IsDefaultOrEmpty)
                    return;
        
                foreach (Diagnostic diagnostic in context.ReportedDiagnostics)
                {
                    context.CancellationToken.ThrowIfCancellationRequested();
                    SuppressNullableDiagnostic(context, diagnostic);
                }
            }
    
            private static void SuppressNullableDiagnostic(SuppressionAnalysisContext context, Diagnostic diagnostic)
            {
                if (diagnostic.IsSuppressed || diagnostic.Location.SourceTree is not { } syntaxTree)
                    return;
    
                SyntaxNode? root = syntaxTree.GetRoot(context.CancellationToken);
                var diagnosticNode = root?.FindNode(diagnostic.Location.SourceSpan);
    
                // For the given case it makes sense to check if the syntax node is one of the possible member declaration nodes that may have the compiler warning 
                if (diagnosticNode is not (ConstructorDeclarationSyntax or VariableDeclaratorSyntax or PropertyDeclarationSyntax) ||
                    context.GetSemanticModel(syntaxTree) is not SemanticModel semanticModel)
                {
                    return;
                }
    
                if (semanticModel.GetDeclaredSymbol(diagnosticNode, context.CancellationToken) is not { } memberSymbol)
                {
                    return;
                }
                
                if (MemberMeetsSuppressionConditions(memberSymbol, diagnostic, context.CancellationToken))
                {
                    var suppression = Suppression.Create(NonNullableUninitializedAfterConstructorDiagnostic, diagnostic);
                    context.ReportSuppression(suppression);
                }
            }
        
            private static bool MemberMeetsSuppressionConditions(ISymbol memberSymbol, Diagnostic diagnostic, CancellationToken cancellation)
            {
                // Put your logic to recognize the symbol to be not suppressed here.
                // For special types this is usually a check for base types and implemented interfaces 
                // For diagnostics on constructors you may need to use AdditionalLocations property from the `diagnostic`
               // that contains the location of the uninitialized field/property
            }
        }
    }