File: src\Analyzers\CSharp\Analyzers\RemoveUnnecessaryNullableDirective\CSharpRemoveUnnecessaryNullableDirectiveDiagnosticAnalyzer.cs
Web Access
Project: src\src\Features\CSharp\Portable\Microsoft.CodeAnalysis.CSharp.Features.csproj (Microsoft.CodeAnalysis.CSharp.Features)
// 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.Concurrent;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
using Microsoft.CodeAnalysis.CodeStyle;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Analyzers.RemoveUnnecessaryNullableDirective;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Shared.Collections;
using Microsoft.CodeAnalysis.Text;
using Roslyn.Utilities;
 
namespace Microsoft.CodeAnalysis.RemoveUnnecessaryNullableDirective;
 
[DiagnosticAnalyzer(LanguageNames.CSharp)]
internal sealed class CSharpRemoveUnnecessaryNullableDirectiveDiagnosticAnalyzer
    : AbstractBuiltInUnnecessaryCodeStyleDiagnosticAnalyzer
{
    public CSharpRemoveUnnecessaryNullableDirectiveDiagnosticAnalyzer()
        : base(IDEDiagnosticIds.RemoveUnnecessaryNullableDirectiveDiagnosticId,
               EnforceOnBuildValues.RemoveUnnecessaryNullableDirective,
               option: null,
               fadingOption: null,
               new LocalizableResourceString(nameof(CSharpAnalyzersResources.Remove_unnecessary_nullable_directive), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)),
               new LocalizableResourceString(nameof(CSharpAnalyzersResources.Nullable_directive_is_unnecessary), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)))
    {
    }
 
    public override DiagnosticAnalyzerCategory GetAnalyzerCategory()
        => DiagnosticAnalyzerCategory.SemanticDocumentAnalysis;
 
    protected override void InitializeWorker(AnalysisContext context)
    {
        context.RegisterCompilationStartAction(AnalyzeCompilation);
    }
 
    private void AnalyzeCompilation(CompilationStartAnalysisContext context)
    {
        var analyzer = new AnalyzerImpl(this);
        context.RegisterCodeBlockAction(analyzer.AnalyzeCodeBlock);
        context.RegisterSemanticModelAction(analyzer.AnalyzeSemanticModel);
    }
 
    /// <summary>
    /// Determine if a code block is eligible for analysis by <see cref="AnalyzeCodeBlock"/>.
    /// </summary>
    /// <param name="codeBlock">The syntax node provided via <see cref="CodeBlockAnalysisContext.CodeBlock"/>.</param>
    /// <returns><see langword="true"/> if the code block should be analyzed by <see cref="AnalyzeCodeBlock"/>;
    /// otherwise, <see langword="false"/> to skip analysis of the block. If a block is skipped, one or more child
    /// blocks may be analyzed by <see cref="AnalyzeCodeBlock"/>, and any remaining spans can be analyzed by
    /// <see cref="AnalyzeSemanticModel"/>.</returns>
    private static bool IsIgnoredCodeBlock(SyntaxNode codeBlock)
    {
        // Avoid analysis of compilation units and types in AnalyzeCodeBlock. These nodes appear in code block
        // callbacks when they include attributes, but analysis of the node at this level would block more efficient
        // analysis of descendant members.
        return codeBlock.Kind() is
            SyntaxKind.CompilationUnit or
            SyntaxKind.ClassDeclaration or
            SyntaxKind.RecordDeclaration or
            SyntaxKind.StructDeclaration or
            SyntaxKind.RecordStructDeclaration or
            SyntaxKind.InterfaceDeclaration or
            SyntaxKind.DelegateDeclaration or
            SyntaxKind.EnumDeclaration;
    }
 
    private static bool IsReducing([NotNullWhen(true)] NullableContextOptions? oldOptions, [NotNullWhen(true)] NullableContextOptions? newOptions)
    {
        return oldOptions is { } oldOptionsValue
            && newOptions is { } newOptionsValue
            && newOptionsValue != oldOptionsValue
            && (oldOptionsValue & newOptionsValue) == newOptionsValue;
    }
 
    private static ImmutableArray<TextSpan> AnalyzeCodeBlock(CodeBlockAnalysisContext context, int positionOfFirstReducingNullableDirective)
    {
        using var simplifier = new NullableImpactingSpanWalker(context.SemanticModel, positionOfFirstReducingNullableDirective, ignoredSpans: null, context.CancellationToken);
        simplifier.Visit(context.CodeBlock);
        return simplifier.Spans;
    }
 
    private ImmutableArray<Diagnostic> AnalyzeSemanticModel(SemanticModelAnalysisContext context, int positionOfFirstReducingNullableDirective, TextSpanMutableIntervalTree? codeBlockIntervalTree, TextSpanMutableIntervalTree? possibleNullableImpactIntervalTree)
    {
        var root = context.SemanticModel.SyntaxTree.GetCompilationUnitRoot(context.CancellationToken);
 
        using (var simplifier = new NullableImpactingSpanWalker(context.SemanticModel, positionOfFirstReducingNullableDirective, ignoredSpans: codeBlockIntervalTree, context.CancellationToken))
        {
            simplifier.Visit(root);
            possibleNullableImpactIntervalTree ??= new TextSpanMutableIntervalTree();
            foreach (var interval in simplifier.Spans)
            {
                possibleNullableImpactIntervalTree.AddIntervalInPlace(interval);
            }
        }
 
        using var diagnostics = TemporaryArray<Diagnostic>.Empty;
 
        var compilationOptions = ((CSharpCompilationOptions)context.SemanticModel.Compilation.Options).NullableContextOptions;
 
        NullableDirectiveTriviaSyntax? previousRetainedDirective = null;
        NullableContextOptions? retainedOptions = compilationOptions;
 
        NullableDirectiveTriviaSyntax? currentOptionsDirective = null;
        var currentOptions = retainedOptions;
 
        for (var directive = root.GetFirstDirective(); directive is not null; directive = directive.GetNextDirective())
        {
            context.CancellationToken.ThrowIfCancellationRequested();
 
            if (directive is NullableDirectiveTriviaSyntax nullableDirectiveTrivia)
            {
                // Once we reach a new directive, check to see if we can remove the previous directive
                var removedCurrent = false;
                if (IsReducing(retainedOptions, currentOptions))
                {
                    // We can't have found a reducing directive and not know which directive it was
                    Contract.ThrowIfNull(currentOptionsDirective);
 
                    if (possibleNullableImpactIntervalTree is null
                        || !possibleNullableImpactIntervalTree.HasIntervalThatOverlapsWith(currentOptionsDirective.Span.End, nullableDirectiveTrivia.SpanStart - currentOptionsDirective.Span.End))
                    {
                        diagnostics.Add(Diagnostic.Create(Descriptor, currentOptionsDirective.GetLocation()));
                    }
                }
 
                if (!removedCurrent)
                {
                    previousRetainedDirective = currentOptionsDirective;
                    retainedOptions = currentOptions;
                }
 
                currentOptionsDirective = nullableDirectiveTrivia;
                currentOptions = CSharpRemoveRedundantNullableDirectiveDiagnosticAnalyzer.GetNullableContextOptions(compilationOptions, currentOptions, nullableDirectiveTrivia);
            }
            else if (directive.Kind() is
                SyntaxKind.IfDirectiveTrivia or
                SyntaxKind.ElifDirectiveTrivia or
                SyntaxKind.ElseDirectiveTrivia)
            {
                possibleNullableImpactIntervalTree ??= new TextSpanMutableIntervalTree();
                possibleNullableImpactIntervalTree.AddIntervalInPlace(directive.Span);
            }
        }
 
        // Once we reach the end of the file, check to see if we can remove the last directive
        if (IsReducing(retainedOptions, currentOptions))
        {
            // We can't have found a reducing directive and not know which directive it was
            Contract.ThrowIfNull(currentOptionsDirective);
 
            if (possibleNullableImpactIntervalTree is null
                || !possibleNullableImpactIntervalTree.HasIntervalThatOverlapsWith(currentOptionsDirective.Span.End, root.Span.End - currentOptionsDirective.Span.End))
            {
                diagnostics.Add(Diagnostic.Create(Descriptor, currentOptionsDirective.GetLocation()));
            }
        }
 
        return diagnostics.ToImmutableAndClear();
    }
 
    private sealed class SyntaxTreeState
    {
        private SyntaxTreeState(bool completed, int? positionOfFirstReducingNullableDirective)
        {
            Completed = completed;
            PositionOfFirstReducingNullableDirective = positionOfFirstReducingNullableDirective;
            if (!completed)
            {
                IntervalTree = new TextSpanMutableIntervalTree();
                PossibleNullableImpactIntervalTree = new TextSpanMutableIntervalTree();
            }
        }
 
        [MemberNotNullWhen(false, nameof(PositionOfFirstReducingNullableDirective), nameof(IntervalTree), nameof(PossibleNullableImpactIntervalTree))]
        public bool Completed { get; private set; }
        public int? PositionOfFirstReducingNullableDirective { get; }
        public TextSpanMutableIntervalTree? IntervalTree { get; }
        public TextSpanMutableIntervalTree? PossibleNullableImpactIntervalTree { get; }
 
        public static SyntaxTreeState Create(bool defaultCompleted, NullableContextOptions compilationOptions, SyntaxTree tree, CancellationToken cancellationToken)
        {
            var root = tree.GetCompilationUnitRoot(cancellationToken);
 
            // This analyzer only needs to process syntax trees that contain at least one #nullable directive that
            // reduces the nullable analysis scope.
            int? positionOfFirstReducingNullableDirective = null;
 
            NullableContextOptions? currentOptions = compilationOptions;
            for (var directive = root.GetFirstDirective(); directive is not null; directive = directive.GetNextDirective())
            {
                cancellationToken.ThrowIfCancellationRequested();
 
                if (directive is NullableDirectiveTriviaSyntax nullableDirectiveTrivia)
                {
                    var newOptions = CSharpRemoveRedundantNullableDirectiveDiagnosticAnalyzer.GetNullableContextOptions(compilationOptions, currentOptions, nullableDirectiveTrivia);
                    if (IsReducing(currentOptions, newOptions))
                    {
                        positionOfFirstReducingNullableDirective = directive.SpanStart;
                        break;
                    }
 
                    currentOptions = newOptions;
                }
            }
 
            return new SyntaxTreeState(completed: defaultCompleted || positionOfFirstReducingNullableDirective is null, positionOfFirstReducingNullableDirective);
        }
 
        [MemberNotNullWhen(true, nameof(PositionOfFirstReducingNullableDirective), nameof(IntervalTree), nameof(PossibleNullableImpactIntervalTree))]
        public bool TryProceedWithInterval(TextSpan span)
            => TryProceedOrReportNullableImpactingSpans(span, nullableImpactingSpans: null);
 
        [MemberNotNullWhen(true, nameof(PositionOfFirstReducingNullableDirective), nameof(IntervalTree), nameof(PossibleNullableImpactIntervalTree))]
        public bool TryReportNullableImpactingSpans(TextSpan span, ImmutableArray<TextSpan> nullableImpactingSpans)
            => TryProceedOrReportNullableImpactingSpans(span, nullableImpactingSpans);
 
        [MemberNotNullWhen(true, nameof(PositionOfFirstReducingNullableDirective), nameof(IntervalTree), nameof(PossibleNullableImpactIntervalTree))]
        private bool TryProceedOrReportNullableImpactingSpans(TextSpan span, ImmutableArray<TextSpan>? nullableImpactingSpans)
        {
            if (Completed)
                return false;
 
            lock (this)
            {
                if (Completed)
                    return false;
 
                if (IntervalTree.HasIntervalThatOverlapsWith(span.Start, span.End))
                    return false;
 
                if (nullableImpactingSpans is { } spans)
                {
                    foreach (var nullableImpactingSpan in spans)
                        PossibleNullableImpactIntervalTree.AddIntervalInPlace(nullableImpactingSpan);
                }
 
                return true;
            }
        }
 
        internal void MarkComplete()
        {
            if (Completed)
                return;
 
            lock (this)
            {
                Completed = true;
            }
        }
    }
 
    private sealed class AnalyzerImpl(CSharpRemoveUnnecessaryNullableDirectiveDiagnosticAnalyzer analyzer)
    {
        private readonly CSharpRemoveUnnecessaryNullableDirectiveDiagnosticAnalyzer _analyzer = analyzer;
 
        /// <summary>
        /// Tracks the analysis state of syntax trees in a compilation.
        /// </summary>
        private readonly ConcurrentDictionary<SyntaxTree, SyntaxTreeState> _codeBlockIntervals = [];
 
        public void AnalyzeCodeBlock(CodeBlockAnalysisContext context)
        {
            if (_analyzer.ShouldSkipAnalysis(context, notification: null)
                || IsIgnoredCodeBlock(context.CodeBlock))
            {
                return;
            }
 
            var root = context.GetAnalysisRoot(findInTrivia: true);
 
            // Bail out if the root contains no nullable directives.
            if (!root.ContainsDirective(SyntaxKind.NullableDirectiveTrivia))
                return;
 
            var syntaxTreeState = GetOrCreateSyntaxTreeState(context.CodeBlock.SyntaxTree, defaultCompleted: false, context.SemanticModel, context.CancellationToken);
            if (!syntaxTreeState.TryProceedWithInterval(context.CodeBlock.FullSpan))
                return;
 
            var nullableImpactingSpans = CSharpRemoveUnnecessaryNullableDirectiveDiagnosticAnalyzer.AnalyzeCodeBlock(context, syntaxTreeState.PositionOfFirstReducingNullableDirective.Value);
 
            // After this point, cancellation is not allowed due to possible state alteration
            syntaxTreeState.TryReportNullableImpactingSpans(context.CodeBlock.FullSpan, nullableImpactingSpans);
        }
 
        public void AnalyzeSemanticModel(SemanticModelAnalysisContext context)
        {
            if (_analyzer.ShouldSkipAnalysis(context, notification: null))
                return;
 
            var root = context.GetAnalysisRoot(findInTrivia: true);
 
            // Bail out if the root contains no nullable directives.
            if (!root.ContainsDirective(SyntaxKind.NullableDirectiveTrivia))
                return;
 
            // Get the state information for the syntax tree. If the state information is not available, it is
            // initialized directly to a completed state, ensuring that concurrent (or future) calls to
            // AnalyzeCodeBlock will always read completed==true, and intervalTree does not need to be initialized
            // to a non-null value.
            var syntaxTreeState = GetOrCreateSyntaxTreeState(context.SemanticModel.SyntaxTree, defaultCompleted: true, context.SemanticModel, context.CancellationToken);
 
            syntaxTreeState.MarkComplete();
 
            if (syntaxTreeState.PositionOfFirstReducingNullableDirective is not { } positionOfFirstReducingNullableDirective)
                return;
 
            var diagnostics = _analyzer.AnalyzeSemanticModel(context, positionOfFirstReducingNullableDirective, syntaxTreeState.IntervalTree, syntaxTreeState.PossibleNullableImpactIntervalTree);
 
            // After this point, cancellation is not allowed due to possible state alteration
            foreach (var diagnostic in diagnostics)
            {
                context.ReportDiagnostic(diagnostic);
            }
        }
 
        private SyntaxTreeState GetOrCreateSyntaxTreeState(SyntaxTree tree, bool defaultCompleted, SemanticModel semanticModel, CancellationToken cancellationToken)
        {
            return _codeBlockIntervals.GetOrAdd(
                tree,
                static (tree, arg) => SyntaxTreeState.Create(arg.defaultCompleted, arg.options, tree, arg.cancellationToken),
                (defaultCompleted, options: ((CSharpCompilationOptions)semanticModel.Compilation.Options).NullableContextOptions, cancellationToken));
        }
    }
}