File: src\Analyzers\Core\Analyzers\PopulateSwitch\AbstractPopulateSwitchDiagnosticAnalyzer.cs
Web Access
Project: src\src\Features\Core\Portable\Microsoft.CodeAnalysis.Features.csproj (Microsoft.CodeAnalysis.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.Generic;
using System.Collections.Immutable;
using Microsoft.CodeAnalysis.CodeStyle;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Shared.Extensions;
 
namespace Microsoft.CodeAnalysis.PopulateSwitch;
 
internal abstract class AbstractPopulateSwitchDiagnosticAnalyzer<TSwitchOperation, TSwitchSyntax>(
    string diagnosticId,
    EnforceOnBuild enforceOnBuild)
    : AbstractBuiltInCodeStyleDiagnosticAnalyzer(diagnosticId,
        enforceOnBuild,
        option: null,
        s_localizableTitle, s_localizableMessage)
    where TSwitchOperation : IOperation
    where TSwitchSyntax : SyntaxNode
{
    private static readonly LocalizableString s_localizableTitle = new LocalizableResourceString(nameof(AnalyzersResources.Add_missing_cases), AnalyzersResources.ResourceManager, typeof(AnalyzersResources));
    private static readonly LocalizableString s_localizableMessage = new LocalizableResourceString(nameof(AnalyzersResources.Populate_switch), AnalyzersResources.ResourceManager, typeof(AnalyzersResources));
 
    protected abstract OperationKind OperationKind { get; }
 
    protected abstract bool IsSwitchTypeUnknown(TSwitchOperation operation);
    protected abstract IOperation GetValueOfSwitchOperation(TSwitchOperation operation);
 
    protected abstract bool IsKnownToBeExhaustive(TSwitchOperation switchOperation);
 
    protected abstract bool HasConstantCase(TSwitchOperation operation, object? value);
    protected abstract ICollection<ISymbol> GetMissingEnumMembers(TSwitchOperation operation);
    protected abstract bool HasDefaultCase(TSwitchOperation operation);
    protected abstract bool HasExhaustiveNullAndTypeCheckCases(TSwitchOperation operation);
    protected abstract Location GetDiagnosticLocation(TSwitchSyntax switchBlock);
 
    public sealed override DiagnosticAnalyzerCategory GetAnalyzerCategory()
        => DiagnosticAnalyzerCategory.SemanticSpanAnalysis;
 
    protected sealed override void InitializeWorker(AnalysisContext context)
        => context.RegisterOperationAction(AnalyzeOperation, OperationKind);
 
    private void AnalyzeOperation(OperationAnalysisContext context)
    {
        if (ShouldSkipAnalysis(context, notification: null))
            return;
 
        var switchOperation = (TSwitchOperation)context.Operation;
        if (switchOperation.Syntax is not TSwitchSyntax switchBlock || IsSwitchTypeUnknown(switchOperation))
            return;
 
        if (HasExhaustiveNullAndTypeCheckCases(switchOperation))
            return;
 
        var value = GetValueOfSwitchOperation(switchOperation);
        var type = value.Type;
        if (type is null)
            return;
 
        var (missingCases, missingDefaultCase) = AnalyzeSwitch(switchOperation, type);
        if (!missingCases && !missingDefaultCase)
            return;
 
        if (switchBlock.SyntaxTree.OverlapsHiddenPosition(switchBlock.Span, context.CancellationToken))
            return;
 
        var properties = ImmutableDictionary<string, string?>.Empty
            .Add(PopulateSwitchStatementHelpers.MissingCases, missingCases.ToString())
            .Add(PopulateSwitchStatementHelpers.MissingDefaultCase, missingDefaultCase.ToString());
        var diagnostic = Diagnostic.Create(
            Descriptor,
            GetDiagnosticLocation(switchBlock),
            properties: properties,
            additionalLocations: [switchBlock.GetLocation()]);
        context.ReportDiagnostic(diagnostic);
    }
 
    private (bool missingCases, bool missingDefaultCase) AnalyzeSwitch(TSwitchOperation switchOperation, ITypeSymbol type)
    {
        var typeWithoutNullable = type.RemoveNullableIfPresent();
 
        // We treat enum switches specially.  Specifically, even if exhaustive (because of a 'default' case),
        // we still want to let users use the feature to fill in missing enum members.  That way if they add
        // new enum members, they can quickly find and fix the switches that aren't explicitly handling those cases.
        //
        // Note: this should likely be a refactoring instead of an analyzer.  However, for historical reasons,
        // we shipped in this fashion.
        if (typeWithoutNullable.TypeKind == TypeKind.Enum)
            return AnalyzeEnumSwitch(switchOperation, type);
 
        // For all other types, we don't want to offer the user anything if the switch is already exhaustive.
        if (this.IsKnownToBeExhaustive(switchOperation))
            return default;
 
        if (typeWithoutNullable.SpecialType == SpecialType.System_Boolean)
            return AnalyzeBooleanSwitch(switchOperation, type);
 
        return (missingCases: false, missingDefaultCase: !HasDefaultCase(switchOperation));
    }
 
    private (bool missingCases, bool missingDefaultCase) AnalyzeBooleanSwitch(TSwitchOperation operation, ITypeSymbol type)
    {
        if (type.RemoveNullableIfPresent() is not { SpecialType: SpecialType.System_Boolean })
            return default;
 
        // Doesn't have a default.  We don't want to offer that if they're already complete.
        var hasAllCases = HasConstantCase(operation, true) && HasConstantCase(operation, false);
        if (type.IsNullable())
            hasAllCases = hasAllCases && HasConstantCase(operation, null);
 
        // If the switch already has a default case or already has all cases, then we don't have to offer the user anything.
        if (HasDefaultCase(operation) || hasAllCases)
            return default;
 
        return (missingCases: false, missingDefaultCase: true);
    }
 
    private (bool missingCases, bool missingDefaultCase) AnalyzeEnumSwitch(TSwitchOperation operation, ITypeSymbol type)
    {
        if (type.RemoveNullableIfPresent()?.TypeKind != TypeKind.Enum)
            return default;
 
        var missingEnumMembers = GetMissingEnumMembers(operation);
 
        return (missingCases: missingEnumMembers.Count > 0, missingDefaultCase: !HasDefaultCase(operation));
    }
 
    protected static bool ConstantValueEquals(Optional<object?> constantValue, object? value)
        => constantValue.HasValue && Equals(constantValue.Value, value);
}