File: src\Analyzers\CSharp\Analyzers\RemoveUnnecessaryNullableDirective\CSharpRemoveRedundantNullableDirectiveDiagnosticAnalyzer.cs
Web Access
Project: src\src\CodeStyle\CSharp\Analyzers\Microsoft.CodeAnalysis.CSharp.CodeStyle.csproj (Microsoft.CodeAnalysis.CSharp.CodeStyle)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
using System.Collections.Generic;
using Microsoft.CodeAnalysis.CodeStyle;
using Microsoft.CodeAnalysis.CSharp.LanguageService;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Shared.Extensions;
 
namespace Microsoft.CodeAnalysis.CSharp.Analyzers.RemoveUnnecessaryNullableDirective;
 
[DiagnosticAnalyzer(LanguageNames.CSharp)]
internal sealed class CSharpRemoveRedundantNullableDirectiveDiagnosticAnalyzer
    : AbstractBuiltInUnnecessaryCodeStyleDiagnosticAnalyzer
{
    public CSharpRemoveRedundantNullableDirectiveDiagnosticAnalyzer()
        : base(
            IDEDiagnosticIds.RemoveRedundantNullableDirectiveDiagnosticId,
            EnforceOnBuildValues.RemoveRedundantNullableDirective,
            option: null,
            new LocalizableResourceString(nameof(CSharpAnalyzersResources.Remove_redundant_nullable_directive), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)),
            new LocalizableResourceString(nameof(CSharpAnalyzersResources.Nullable_directive_is_redundant), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)))
    {
    }
 
    public override DiagnosticAnalyzerCategory GetAnalyzerCategory()
        => DiagnosticAnalyzerCategory.SyntaxTreeWithoutSemanticsAnalysis;
 
    protected override void InitializeWorker(AnalysisContext context)
        => context.RegisterCompilationStartAction(context =>
        {
            var compilation = (CSharpCompilation)context.Compilation;
            if (compilation.LanguageVersion < LanguageVersion.CSharp8)
            {
                // Compilation does not support nullable directives
                return;
            }
 
            var compilationOptions = compilation.Options;
            context.RegisterSyntaxTreeAction(context => ProcessSyntaxTree(compilationOptions, context));
        });
 
    private void ProcessSyntaxTree(
        CSharpCompilationOptions compilationOptions,
        SyntaxTreeAnalysisContext context)
    {
        if (ShouldSkipAnalysis(context, compilationOptions, notification: null))
            return;
 
        var root = context.GetAnalysisRoot(findInTrivia: true);
 
        // Bail out if the root contains no nullable directives.
        if (!root.ContainsDirective(SyntaxKind.NullableDirectiveTrivia))
            return;
 
        var defaultNullableContext = compilationOptions.NullableContextOptions;
        NullableContextOptions? currentState = context.Tree.IsGeneratedCode(context.Options, CSharpSyntaxFacts.Instance, context.CancellationToken)
            ? NullableContextOptions.Disable
            : defaultNullableContext;
 
        using var pooledStack = SharedPools.Default<Stack<SyntaxNodeOrToken>>().GetPooledObject();
        var stack = pooledStack.Object;
 
        stack.Push(root);
 
        while (stack.TryPop(out var current))
        {
            if (!current.ContainsDirectives)
                continue;
 
            if (current.AsNode(out var childNode))
            {
                // Add the nodes in reverse so we continue walking in a depth-first fashion.
                foreach (var child in childNode.ChildNodesAndTokens().Reverse())
                    stack.Push(child);
            }
            else if (current.IsToken)
            {
                foreach (var trivia in current.AsToken().LeadingTrivia)
                    ProcessTrivia(trivia);
            }
        }
 
        return;
 
        void ProcessTrivia(SyntaxTrivia trivia)
        {
            if (!trivia.IsDirective)
                return;
 
            var directive = trivia.GetStructure()!;
            switch (directive.Kind())
            {
                case SyntaxKind.NullableDirectiveTrivia:
                    {
                        // Report if the nullable directive puts us in the same state we're already in.
                        var newState = GetNullableContextOptions(defaultNullableContext, currentState, (NullableDirectiveTriviaSyntax)directive);
                        if (newState == currentState)
                            context.ReportDiagnostic(Diagnostic.Create(Descriptor, directive.GetLocation()));
 
                        currentState = newState;
                        return;
                    }
 
                case SyntaxKind.IfDirectiveTrivia or
                     SyntaxKind.ElifDirectiveTrivia or
                     SyntaxKind.ElseDirectiveTrivia or
                     SyntaxKind.EndIfDirectiveTrivia:
                    // Reset the known nullable state when crossing a conditional compilation boundary
                    currentState = null;
                    return;
            }
        }
    }
 
    internal static NullableContextOptions? GetNullableContextOptions(NullableContextOptions compilationOptions, NullableContextOptions? options, NullableDirectiveTriviaSyntax directive)
    {
        if (!directive.TargetToken.IsKind(SyntaxKind.None))
        {
            if (options is not { } knownState)
            {
                return null;
            }
 
            NullableContextOptions flagToChange;
            if (directive.TargetToken.IsKind(SyntaxKind.AnnotationsKeyword))
            {
                flagToChange = NullableContextOptions.Annotations;
            }
            else if (directive.TargetToken.IsKind(SyntaxKind.WarningsKeyword))
            {
                flagToChange = NullableContextOptions.Warnings;
            }
            else
            {
                return null;
            }
 
            if (directive.SettingToken.IsKind(SyntaxKind.EnableKeyword))
            {
                return knownState | flagToChange;
            }
            else if (directive.SettingToken.IsKind(SyntaxKind.DisableKeyword))
            {
                return knownState & (~flagToChange);
            }
            else
            {
                return null;
            }
        }
 
        if (directive.SettingToken.IsKind(SyntaxKind.EnableKeyword))
        {
            return NullableContextOptions.Annotations | NullableContextOptions.Warnings;
        }
        else if (directive.SettingToken.IsKind(SyntaxKind.DisableKeyword))
        {
            return NullableContextOptions.Disable;
        }
        else if (directive.SettingToken.IsKind(SyntaxKind.RestoreKeyword))
        {
            return compilationOptions;
        }
        else
        {
            return null;
        }
    }
}