File: src\Analyzers\CSharp\Analyzers\RemoveUnreachableCode\CSharpRemoveUnreachableCodeDiagnosticAnalyzer.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.Immutable;
using System.Linq;
using Microsoft.CodeAnalysis.CodeStyle;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.Utilities;
 
namespace Microsoft.CodeAnalysis.CSharp.RemoveUnreachableCode;
 
[DiagnosticAnalyzer(LanguageNames.CSharp)]
internal sealed class CSharpRemoveUnreachableCodeDiagnosticAnalyzer : AbstractBuiltInUnnecessaryCodeStyleDiagnosticAnalyzer
{
    private const string CS0162 = nameof(CS0162); // Unreachable code detected
 
    public const string IsSubsequentSection = nameof(IsSubsequentSection);
    private static readonly ImmutableDictionary<string, string?> s_subsequentSectionProperties = ImmutableDictionary<string, string?>.Empty.Add(IsSubsequentSection, "");
 
    public CSharpRemoveUnreachableCodeDiagnosticAnalyzer()
        : base(IDEDiagnosticIds.RemoveUnreachableCodeDiagnosticId,
               EnforceOnBuildValues.RemoveUnreachableCode,
               option: null,
               new LocalizableResourceString(nameof(CSharpAnalyzersResources.Unreachable_code_detected), CSharpAnalyzersResources.ResourceManager, typeof(CSharpAnalyzersResources)),
               configurable: false)
    {
    }
 
    public override DiagnosticAnalyzerCategory GetAnalyzerCategory()
        => DiagnosticAnalyzerCategory.SemanticSpanAnalysis;
 
    protected override void InitializeWorker(AnalysisContext context)
        => context.RegisterSemanticModelAction(AnalyzeSemanticModel);
 
    private void AnalyzeSemanticModel(SemanticModelAnalysisContext context)
    {
        if (ShouldSkipAnalysis(context, notification: null))
            return;
 
        var semanticModel = context.SemanticModel;
        var cancellationToken = context.CancellationToken;
 
        // There is no good existing API to check if a statement is unreachable in an efficient
        // manner.  While there is SemanticModel.AnalyzeControlFlow, it can only operate on a 
        // statement at a time, and it will reanalyze and allocate on each call.  
        //
        // To avoid this, we simply ask the semantic model for all the diagnostics for this
        // block and we look for any reported "unreachable code detected" diagnostics.
        //
        // This is actually quite fast to do because the compiler does not actually need to
        // recompile things to determine the diagnostics.  It will have already stashed the
        // binding diagnostics directly on the SourceMethodSymbol containing this block, and
        // so it can retrieve the diagnostics at practically no cost.
        var root = semanticModel.SyntaxTree.GetRoot(cancellationToken);
        var diagnostics = semanticModel.GetDiagnostics(context.FilterSpan, cancellationToken);
        foreach (var diagnostic in diagnostics)
        {
            cancellationToken.ThrowIfCancellationRequested();
 
            if (diagnostic.Id == CS0162)
            {
                ProcessUnreachableDiagnostic(context, root, diagnostic.Location.SourceSpan);
            }
        }
    }
 
    public static StatementSyntax? TryGetContainingStatement(SyntaxNode node)
    {
        var firstUnreachableStatement = node.FirstAncestorOrSelf<StatementSyntax>();
        return firstUnreachableStatement != null && firstUnreachableStatement.SpanStart == node.SpanStart
            ? firstUnreachableStatement
            : null;
    }
 
    private void ProcessUnreachableDiagnostic(
        SemanticModelAnalysisContext context, SyntaxNode root, TextSpan sourceSpan)
    {
        var node = root.FindNode(sourceSpan);
 
        // Note: this approach works as the language only supports the concept of 
        // unreachable statements.  If we ever get unreachable subexpressions, then
        // we'll need to revise this code accordingly.
        var firstUnreachableStatement = TryGetContainingStatement(node);
        if (firstUnreachableStatement is null)
            return;
 
        // At a high level, we can think about us wanting to fade out a "section" of unreachable
        // statements.  However, the compiler only reports the first statement in that "section".
        // We want to figure out what other statements are in that section and fade them all out
        // along with the first statement.  This is made somewhat tricky due to the fact that
        // subsequent sibling statements possibly being reachable due to explicit gotos+labels.
        //
        // On top of this, an unreachable section might not be contiguous.  This is possible 
        // when there is unreachable code that contains a local function declaration in-situ.
        // This is legal, and the local function declaration may be called from other reachable code.
        //
        // As such, it's not possible to just get first unreachable statement, and the last, and
        // then report that whole region as unreachable.  Instead, when we are told about an
        // unreachable statement, we simply determine which other statements are also unreachable
        // and bucket them into contiguous chunks. 
        //
        // We then fade each of these contiguous chunks, while also having each diagnostic we
        // report point back to the first unreachable statement so that we can easily determine
        // what to remove if the user fixes the issue.  (The fix itself has to go recompute this
        // as the total set of statements to remove may be larger than the actual faded code
        // that that diagnostic corresponds to).
 
        // Get the location of this first unreachable statement.  It will be given to all
        // the diagnostics we create off of this single compiler diagnostic so that we always
        // know how to find it regardless of which of our diagnostics the user invokes the 
        // fix off of.
        var firstStatementLocation = root.SyntaxTree.GetLocation(firstUnreachableStatement.FullSpan);
 
        // 'additionalLocations' is how we always pass along the locaiton of the first unreachable
        // statement in this group.
        var additionalLocations = ImmutableArray.Create(firstStatementLocation);
 
        context.ReportDiagnostic(DiagnosticHelper.CreateWithLocationTags(
            Descriptor,
            firstStatementLocation,
            NotificationOption2.ForSeverity(Descriptor.DefaultSeverity),
            context.Options,
            additionalLocations: [],
            additionalUnnecessaryLocations: additionalLocations));
 
        var sections = RemoveUnreachableCodeHelpers.GetSubsequentUnreachableSections(firstUnreachableStatement);
        foreach (var section in sections)
        {
            var span = TextSpan.FromBounds(section[0].FullSpan.Start, section.Last().FullSpan.End);
            var location = root.SyntaxTree.GetLocation(span);
 
            // Mark subsequent sections as being 'cascaded'.  We don't need to actually process them
            // when doing a fix-all as they'll be scooped up when we process the fix for the first
            // section.
            var additionalUnnecessaryLocations = ImmutableArray.Create(location);
 
            context.ReportDiagnostic(DiagnosticHelper.CreateWithLocationTags(
                Descriptor,
                location,
                NotificationOption2.ForSeverity(Descriptor.DefaultSeverity),
                context.Options,
                additionalLocations,
                additionalUnnecessaryLocations,
                s_subsequentSectionProperties));
        }
    }
}