File: Microsoft.CodeQuality.Analyzers\ApiDesignGuidelines\ImplementIDisposableCorrectly.cs
Web Access
Project: src\sdk\src\Microsoft.CodeAnalysis.NetAnalyzers\src\Microsoft.CodeAnalysis.NetAnalyzers\Microsoft.CodeAnalysis.NetAnalyzers.csproj (Microsoft.CodeAnalysis.NetAnalyzers)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Immutable;
using System.Linq;
using Analyzer.Utilities;
using Analyzer.Utilities.Extensions;
using Analyzer.Utilities.Lightup;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;

namespace Microsoft.CodeQuality.Analyzers.ApiDesignGuidelines
{
    using static MicrosoftCodeQualityAnalyzersResources;

    /// <summary>
    /// CA1063: <inheritdoc cref="ImplementIDisposableCorrectlyTitle"/>
    /// </summary>
    [DiagnosticAnalyzer(LanguageNames.CSharp, LanguageNames.VisualBasic)]
    public sealed class ImplementIDisposableCorrectlyAnalyzer : DiagnosticAnalyzer
    {
        internal const string RuleId = "CA1063";

        private const string DisposeMethodName = "Dispose";
        private const string GarbageCollectorTypeName = "System.GC";
        private const string SuppressFinalizeMethodName = "SuppressFinalize";

        private static readonly LocalizableString s_localizableTitle = CreateLocalizableResourceString(nameof(ImplementIDisposableCorrectlyTitle));
        private static readonly LocalizableString s_localizableDescription = CreateLocalizableResourceString(nameof(ImplementIDisposableCorrectlyDescription));

        // Rules disabled by default until https://github.com/dotnet/docs/issues/8463 is resolved.

        internal static readonly DiagnosticDescriptor IDisposableReimplementationRule = DiagnosticDescriptorHelper.Create(
            RuleId,
            s_localizableTitle,
            CreateLocalizableResourceString(nameof(ImplementIDisposableCorrectlyMessageIDisposableReimplementation)),
            DiagnosticCategory.Design,
            RuleLevel.Disabled,
            description: s_localizableDescription,
            isPortedFxCopRule: true,
            isDataflowRule: false);

        internal static readonly DiagnosticDescriptor FinalizeOverrideRule = DiagnosticDescriptorHelper.Create(
            RuleId,
            s_localizableTitle,
            CreateLocalizableResourceString(nameof(ImplementIDisposableCorrectlyMessageFinalizeOverride)),
            DiagnosticCategory.Design,
            RuleLevel.Disabled,
            description: s_localizableDescription,
            isPortedFxCopRule: true,
            isDataflowRule: false);

        internal static readonly DiagnosticDescriptor DisposeOverrideRule = DiagnosticDescriptorHelper.Create(
            RuleId,
            s_localizableTitle,
            CreateLocalizableResourceString(nameof(ImplementIDisposableCorrectlyMessageDisposeOverride)),
            DiagnosticCategory.Design,
            RuleLevel.Disabled,
            description: s_localizableDescription,
            isPortedFxCopRule: true,
            isDataflowRule: false);

        internal static readonly DiagnosticDescriptor DisposeSignatureRule = DiagnosticDescriptorHelper.Create(
            RuleId,
            s_localizableTitle,
            CreateLocalizableResourceString(nameof(ImplementIDisposableCorrectlyMessageDisposeSignature)),
            DiagnosticCategory.Design,
            RuleLevel.Disabled,
            description: s_localizableDescription,
            isPortedFxCopRule: true,
            isDataflowRule: false);

        internal static readonly DiagnosticDescriptor RenameDisposeRule = DiagnosticDescriptorHelper.Create(
            RuleId,
            s_localizableTitle,
            CreateLocalizableResourceString(nameof(ImplementIDisposableCorrectlyMessageRenameDispose)),
            DiagnosticCategory.Design,
            RuleLevel.Disabled,
            description: s_localizableDescription,
            isPortedFxCopRule: true,
            isDataflowRule: false);

        internal static readonly DiagnosticDescriptor DisposeBoolSignatureRule = DiagnosticDescriptorHelper.Create(
            RuleId,
            s_localizableTitle,
            CreateLocalizableResourceString(nameof(ImplementIDisposableCorrectlyMessageDisposeBoolSignature)),
            DiagnosticCategory.Design,
            RuleLevel.Disabled,
            description: s_localizableDescription,
            isPortedFxCopRule: true,
            isDataflowRule: false);

        internal static readonly DiagnosticDescriptor DisposeImplementationRule = DiagnosticDescriptorHelper.Create(
            RuleId,
            s_localizableTitle,
            CreateLocalizableResourceString(nameof(ImplementIDisposableCorrectlyMessageDisposeImplementation)),
            DiagnosticCategory.Design,
            RuleLevel.Disabled,
            description: s_localizableDescription,
            isPortedFxCopRule: true,
            isDataflowRule: false);

        internal static readonly DiagnosticDescriptor FinalizeImplementationRule = DiagnosticDescriptorHelper.Create(
            RuleId,
            s_localizableTitle,
            CreateLocalizableResourceString(nameof(ImplementIDisposableCorrectlyMessageFinalizeImplementation)),
            DiagnosticCategory.Design,
            RuleLevel.Disabled,
            description: s_localizableDescription,
            isPortedFxCopRule: true,
            isDataflowRule: false);

        internal static readonly DiagnosticDescriptor ProvideDisposeBoolRule = DiagnosticDescriptorHelper.Create(
            RuleId,
            s_localizableTitle,
            CreateLocalizableResourceString(nameof(ImplementIDisposableCorrectlyMessageProvideDisposeBool)),
            DiagnosticCategory.Design,
            RuleLevel.Disabled,
            description: s_localizableDescription,
            isPortedFxCopRule: true,
            isDataflowRule: false);

        public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } =
            ImmutableArray.Create(IDisposableReimplementationRule, FinalizeOverrideRule, DisposeOverrideRule, DisposeSignatureRule, RenameDisposeRule, DisposeBoolSignatureRule, DisposeImplementationRule, FinalizeImplementationRule, ProvideDisposeBoolRule);

        public override void Initialize(AnalysisContext context)
        {
            context.EnableConcurrentExecution();
            context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);

            context.RegisterCompilationStartAction(
                context =>
                {
                    INamedTypeSymbol? disposableType = context.Compilation.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemIDisposable);
                    if (disposableType == null)
                    {
                        return;
                    }

                    if (disposableType.GetMembers(DisposeMethodName).FirstOrDefault() is not IMethodSymbol disposeInterfaceMethod)
                    {
                        return;
                    }

                    INamedTypeSymbol? garbageCollectorType = context.Compilation.GetOrCreateTypeByMetadataName(GarbageCollectorTypeName);
                    if (garbageCollectorType == null)
                    {
                        return;
                    }

                    if (garbageCollectorType.GetMembers(SuppressFinalizeMethodName).FirstOrDefault() is not IMethodSymbol suppressFinalizeMethod)
                    {
                        return;
                    }

                    var analyzer = new PerCompilationAnalyzer(disposableType, disposeInterfaceMethod, suppressFinalizeMethod);
                    analyzer.Initialize(context);
                });
        }

        /// <summary>
        /// Analyzes single instance of compilation.
        /// </summary>
        private class PerCompilationAnalyzer
        {
            private readonly INamedTypeSymbol _disposableType;
            private readonly IMethodSymbol _disposeInterfaceMethod;
            private readonly IMethodSymbol _suppressFinalizeMethod;

            public PerCompilationAnalyzer(INamedTypeSymbol disposableType, IMethodSymbol disposeInterfaceMethod, IMethodSymbol suppressFinalizeMethod)
            {
                _disposableType = disposableType;
                _disposeInterfaceMethod = disposeInterfaceMethod;
                _suppressFinalizeMethod = suppressFinalizeMethod;
            }

            public void Initialize(CompilationStartAnalysisContext context)
            {
                context.RegisterSymbolAction(AnalyzeNamedTypeSymbol, SymbolKind.NamedType);
                context.RegisterOperationBlockAction(AnalyzeOperationBlock);
            }

            private void AnalyzeNamedTypeSymbol(SymbolAnalysisContext context)
            {
                var type = (INamedTypeSymbol)context.Symbol;
                // Note all the descriptors/rules for this analyzer have the same ID and category and hence
                // will always have identical configured visibility.
                if (type.TypeKind == TypeKind.Class &&
                    context.Options.MatchesConfiguredVisibility(IDisposableReimplementationRule, type, context.Compilation))
                {
                    bool implementsDisposableInBaseType = ImplementsDisposableInBaseType(type);

                    if (ImplementsDisposableDirectly(type))
                    {
                        if (type.Interfaces.Contains(_disposableType))
                        {
                            // This differs from FxCop implementation
                            // Reports violation when type redundantly declares IDisposable as implemented interface
                            CheckIDisposableReimplementationRule(type, context, implementsDisposableInBaseType);
                        }

                        IMethodSymbol? disposeMethod = FindDisposeMethod(type);
                        if (disposeMethod != null)
                        {
                            CheckDisposeSignatureRule(disposeMethod, type, context);
                            CheckRenameDisposeRule(disposeMethod, type, context);

                            if (!type.IsSealed)
                            {
                                IMethodSymbol disposeBoolMethod = FindDisposeBoolMethod(type);
                                if (disposeBoolMethod != null)
                                {
                                    CheckDisposeBoolSignatureRule(disposeBoolMethod, type, context);
                                }
                                else
                                {
                                    CheckProvideDisposeBoolRule(type, context);
                                }
                            }
                        }
                    }

                    if (implementsDisposableInBaseType && FindInheritedDisposeBoolMethod(type) != null)
                    {
                        foreach (IMethodSymbol method in type.GetMembers().OfType<IMethodSymbol>())
                        {
                            CheckDisposeOverrideRule(method, type, context);
                        }

                        CheckFinalizeOverrideRule(type, context);
                    }
                }
            }

            private void AnalyzeOperationBlock(OperationBlockAnalysisContext context)
            {
                if (context.OwningSymbol is not IMethodSymbol method)
                {
                    return;
                }

                bool isFinalizerMethod = method.IsFinalizer();
                bool isDisposeMethod = method.Name == DisposeMethodName;
                if (isFinalizerMethod || isDisposeMethod)
                {
                    // Note all the descriptors/rules for this analyzer have the same ID and category and hence
                    // will always have identical configured visibility.
                    INamedTypeSymbol type = method.ContainingType;
                    if (type != null && type.TypeKind == TypeKind.Class &&
                        !type.IsSealed && context.Options.MatchesConfiguredVisibility(IDisposableReimplementationRule, type, context.Compilation))
                    {
                        if (ImplementsDisposableDirectly(type))
                        {
                            IMethodSymbol? disposeMethod = FindDisposeMethod(type);
                            if (disposeMethod != null)
                            {
                                if (method.Equals(disposeMethod))
                                {
                                    CheckDisposeImplementationRule(method, type, context.OperationBlocks, context);
                                }
                                else if (isFinalizerMethod)
                                {
                                    CheckFinalizeImplementationRule(method, type, context.OperationBlocks, context);
                                }
                            }
                        }
                        else if (isFinalizerMethod &&
                            ImplementsDisposableInBaseType(type) &&
                            FindInheritedDisposeBoolMethod(type) != null)
                        {
                            // Finalizer must invoke Dispose(false) if any of its base type has a Dispose(bool) implementation.
                            CheckFinalizeImplementationRule(method, type, context.OperationBlocks, context);
                        }
                    }
                }
            }

            /// <summary>
            /// Check rule: Remove IDisposable from the list of interfaces implemented by {0} as it is already implemented by base type {1}.
            /// </summary>
            private static void CheckIDisposableReimplementationRule(INamedTypeSymbol type, SymbolAnalysisContext context, bool implementsDisposableInBaseType)
            {
                if (implementsDisposableInBaseType)
                {
                    context.ReportDiagnostic(type.CreateDiagnostic(IDisposableReimplementationRule, type.Name, type.BaseType!.Name));
                }
            }

            /// <summary>
            /// Checks rule: Ensure that {0} is declared as public and sealed.
            /// </summary>
            private static void CheckDisposeSignatureRule(IMethodSymbol method, INamedTypeSymbol type, SymbolAnalysisContext context)
            {
                if (!method.IsPublic() ||
                    method.IsAbstract || method.IsVirtual || (method.IsOverride && !method.IsSealed))
                {
                    context.ReportDiagnostic(method.CreateDiagnostic(DisposeSignatureRule, $"{type.Name}.{method.Name}"));
                }
            }

            /// <summary>
            /// Checks rule: Rename {0} to 'Dispose' and ensure that it is declared as public and sealed.
            /// </summary>
            private static void CheckRenameDisposeRule(IMethodSymbol method, INamedTypeSymbol type, SymbolAnalysisContext context)
            {
                if (method.Name != DisposeMethodName)
                {
                    context.ReportDiagnostic(method.CreateDiagnostic(RenameDisposeRule, $"{type.Name}.{method.Name}"));
                }
            }

            /// <summary>
            /// Checks rule: Remove {0}, override Dispose(bool disposing), and put the dispose logic in the code path where 'disposing' is true.
            /// </summary>
            private void CheckDisposeOverrideRule(IMethodSymbol method, INamedTypeSymbol type, SymbolAnalysisContext context)
            {
                if (method.MethodKind == MethodKind.Ordinary && method.IsOverride && method.ReturnsVoid && method.Parameters.IsEmpty)
                {
                    bool isDisposeOverride = false;
                    for (IMethodSymbol? m = method.OverriddenMethod; m != null; m = m.OverriddenMethod)
                    {
                        if (Equals(m, FindDisposeMethod(m.ContainingType)))
                        {
                            isDisposeOverride = true;
                            break;
                        }
                    }

                    if (isDisposeOverride)
                    {
                        context.ReportDiagnostic(method.CreateDiagnostic(DisposeOverrideRule, $"{type.Name}.{method.Name}"));
                    }
                }
            }

            /// <summary>
            /// Checks rule: Remove the finalizer from type {0}, override Dispose(bool disposing), and put the finalization logic in the code path where 'disposing' is false. Otherwise, it might lead to duplicate Dispose invocations as the Base type '{1}' also provides a finalizer.
            /// </summary>
            private static void CheckFinalizeOverrideRule(INamedTypeSymbol type, SymbolAnalysisContext context)
            {
                if (type.HasFinalizer())
                {
                    // Flag the finalizer if there is any base type with a finalizer, this can cause duplicate Dispose(false) invocations.
                    var baseTypeWithFinalizer = GetFirstBaseTypeWithFinalizerOrDefault(type);
                    if (baseTypeWithFinalizer != null)
                    {
                        context.ReportDiagnostic(type.CreateDiagnostic(FinalizeOverrideRule, type.Name, baseTypeWithFinalizer.Name));
                    }
                }
            }

            /// <summary>
            /// Checks rule: Provide an overridable implementation of Dispose(bool) on {0} or mark the type as sealed. A call to Dispose(false) should only clean up native resources. A call to Dispose(true) should clean up both managed and native resources.
            /// </summary>
            private static void CheckProvideDisposeBoolRule(INamedTypeSymbol type, SymbolAnalysisContext context)
            {
                context.ReportDiagnostic(type.CreateDiagnostic(ProvideDisposeBoolRule, type.Name));
            }

            /// <summary>
            /// Checks rule: Ensure that {0} is declared as protected, virtual, and unsealed.
            /// </summary>
            private static void CheckDisposeBoolSignatureRule(IMethodSymbol method, INamedTypeSymbol type, SymbolAnalysisContext context)
            {
                if (method.DeclaredAccessibility != Accessibility.Protected ||
                    !(method.IsVirtual || method.IsAbstract || method.IsOverride) || method.IsSealed)
                {
                    context.ReportDiagnostic(method.CreateDiagnostic(DisposeBoolSignatureRule, $"{type.Name}.{method.Name}"));
                }
            }

            /// <summary>
            /// Checks rule: Modify {0} so that it calls Dispose(true), then calls GC.SuppressFinalize on the current object instance ('this' or 'Me' in Visual Basic), and then returns.
            /// </summary>
            private void CheckDisposeImplementationRule(IMethodSymbol method, INamedTypeSymbol type, ImmutableArray<IOperation> operationBlocks, OperationBlockAnalysisContext context)
            {
                var validator = new DisposeImplementationValidator(_suppressFinalizeMethod, type);
                if (!validator.Validate(operationBlocks))
                {
                    context.ReportDiagnostic(method.CreateDiagnostic(DisposeImplementationRule, $"{type.Name}.{method.Name}"));
                }
            }

            /// <summary>
            /// Checks rule: Modify {0} so that it calls Dispose(false) and then returns.
            /// </summary>
            private static void CheckFinalizeImplementationRule(IMethodSymbol method, INamedTypeSymbol type, ImmutableArray<IOperation> operationBlocks, OperationBlockAnalysisContext context)
            {
                // Bail out if any base type also provides a finalizer - we will fire CheckFinalizeOverrideRule for that case.
                if (GetFirstBaseTypeWithFinalizerOrDefault(type) != null)
                {
                    return;
                }

                var validator = new FinalizeImplementationValidator(type);
                if (!validator.Validate(operationBlocks))
                {
                    context.ReportDiagnostic(method.CreateDiagnostic(FinalizeImplementationRule, $"{type.Name}.{method.Name}"));
                }
            }

            private static INamedTypeSymbol GetFirstBaseTypeWithFinalizerOrDefault(INamedTypeSymbol type)
                => type.GetBaseTypes().FirstOrDefault(baseType => baseType.SpecialType != SpecialType.System_Object && baseType.HasFinalizer());

            /// <summary>
            /// Checks if type implements IDisposable interface or an interface inherited from IDisposable.
            /// Only direct implementation is taken into account, implementation in base type is ignored.
            /// </summary>
            private bool ImplementsDisposableDirectly(ITypeSymbol type)
            {
                return type.Interfaces.Any(i => i.Inherits(_disposableType));
            }

            /// <summary>
            /// Checks if base type implements IDisposable interface directly or indirectly.
            /// </summary>
            private bool ImplementsDisposableInBaseType(ITypeSymbol type)
            {
                return type.BaseType != null && type.BaseType.AllInterfaces.Contains(_disposableType);
            }

            /// <summary>
            /// Returns method that implements IDisposable.Dispose operation.
            /// Only direct implementation is taken into account, implementation in base type is ignored.
            /// </summary>
            private IMethodSymbol? FindDisposeMethod(INamedTypeSymbol type)
            {
                if (type.FindImplementationForInterfaceMember(_disposeInterfaceMethod) is IMethodSymbol disposeMethod && Equals(disposeMethod.ContainingType, type))
                {
                    return disposeMethod;
                }

                return null;
            }

            /// <summary>
            /// Returns method: void Dispose(bool)
            /// </summary>
            private static IMethodSymbol FindDisposeBoolMethod(INamedTypeSymbol type)
            {
                return type.GetMembers(DisposeMethodName).OfType<IMethodSymbol>().FirstOrDefault(m => m.HasDisposeBoolMethodSignature());
            }

            /// <summary>
            /// Returns method defined in the nearest ancestor: void Dispose(bool)
            /// </summary>
            private IMethodSymbol? FindInheritedDisposeBoolMethod(INamedTypeSymbol? type)
            {
                IMethodSymbol? method = null;

                while (type != null && method == null && ImplementsDisposableInBaseType(type))
                {
                    type = type.BaseType!;
                    method = FindDisposeBoolMethod(type);
                }

                return method;
            }
        }

        private static bool IsDisposeBoolCall(IInvocationOperation invocationExpression, INamedTypeSymbol type, bool expectedValue)
        {
            if (!invocationExpression.TargetMethod.HasDisposeBoolMethodSignature())
            {
                return false;
            }

            if (invocationExpression.Instance == null)
            {
                if (!type.Equals(invocationExpression.TargetMethod.ContainingType))
                {
                    return false;
                }
            }
            else if (invocationExpression.Instance.Kind != OperationKind.InstanceReference ||
                !type.Equals(invocationExpression.Instance.Type))
            {
                return false;
            }

            if (invocationExpression.Arguments.Length != 1)
            {
                return false;
            }

            IArgumentOperation argument = invocationExpression.Arguments[0];
            if (argument.Value.Kind != OperationKind.Literal)
            {
                return false;
            }

            var literal = (ILiteralOperation)argument.Value;
            if (!literal.ConstantValue.HasValue || !expectedValue.Equals(literal.ConstantValue.Value))
            {
                return false;
            }

            return true;
        }

        /// <summary>
        /// Validates implementation of Dispose method. The method must call Dispose(true) and then GC.SuppressFinalize(this).
        /// </summary>
        private sealed class DisposeImplementationValidator
        {
            private readonly IMethodSymbol _suppressFinalizeMethod;
            private readonly INamedTypeSymbol _type;
            private bool _callsDisposeBool;
            private bool _callsSuppressFinalize;

            public DisposeImplementationValidator(IMethodSymbol suppressFinalizeMethod, INamedTypeSymbol type)
            {
                _callsDisposeBool = false;
                _callsSuppressFinalize = false;
                _suppressFinalizeMethod = suppressFinalizeMethod;
                _type = type;
            }

            public bool Validate(ImmutableArray<IOperation> operations)
            {
                _callsDisposeBool = false;
                _callsSuppressFinalize = false;

                if (ValidateOperations(operations))
                {
                    return _callsDisposeBool && (_callsSuppressFinalize || !_type.HasFinalizer());
                }

                return false;
            }

            private bool ValidateOperations(ImmutableArray<IOperation> operations)
            {
                foreach (IOperation operation in operations)
                {
                    if (!operation.IsImplicit && operation.Kind != OperationKindEx.Attribute && !ValidateOperation(operation))
                    {
                        return false;
                    }
                }

                return true;
            }

            private bool ValidateOperation(IOperation operation)
            {
                switch (operation.Kind)
                {
                    case OperationKind.Empty:
                    case OperationKind.Labeled:
                        return true;
                    case OperationKind.Block:
                        var blockStatement = (IBlockOperation)operation;
                        return ValidateOperations(blockStatement.Operations);
                    case OperationKind.ExpressionStatement:
                        var expressionStatement = (IExpressionStatementOperation)operation;
                        return ValidateExpression(expressionStatement);
                    default:
                        // Ignore operation roots with no IOperation API support (OperationKind.None)
                        return operation.IsOperationNoneRoot();
                }
            }

            private bool ValidateExpression(IExpressionStatementOperation expressionStatement)
            {
                if (expressionStatement.Operation == null || expressionStatement.Operation.Kind != OperationKind.Invocation)
                {
                    return false;
                }

                var invocationExpression = (IInvocationOperation)expressionStatement.Operation;
                if (!_callsDisposeBool)
                {
                    bool result = IsDisposeBoolCall(invocationExpression, _type, expectedValue: true);
                    if (result)
                    {
                        _callsDisposeBool = true;
                    }

                    return result;
                }
                else if (!_callsSuppressFinalize)
                {
                    bool result = IsSuppressFinalizeCall(invocationExpression);
                    if (result)
                    {
                        _callsSuppressFinalize = true;
                    }

                    return result;
                }

                return false;
            }

            private bool IsSuppressFinalizeCall(IInvocationOperation invocationExpression)
            {
                if (!Equals(invocationExpression.TargetMethod, _suppressFinalizeMethod))
                {
                    return false;
                }

                if (invocationExpression.Arguments.Length != 1)
                {
                    return false;
                }

                IOperation argumentValue = invocationExpression.Arguments[0].Value;
                if (argumentValue.Kind != OperationKind.Conversion)
                {
                    return false;
                }

                var conversion = (IConversionOperation)argumentValue;
                if (conversion.Operand == null || conversion.Operand.Kind != OperationKind.InstanceReference)
                {
                    return false;
                }

                if (!_type.Equals(conversion.Operand.Type))
                {
                    return false;
                }

                return true;
            }
        }

        /// <summary>
        /// Validates implementation of the finalizer. This method must call Dispose(false) and then return
        /// </summary>
        private sealed class FinalizeImplementationValidator
        {
            private readonly INamedTypeSymbol _type;
            private bool _callDispose;

            public FinalizeImplementationValidator(INamedTypeSymbol type)
            {
                _type = type;
                _callDispose = false;
            }

            public bool Validate(ImmutableArray<IOperation> operations)
            {
                _callDispose = false;

                if (ValidateOperations(operations))
                {
                    return _callDispose;
                }

                return false;
            }

            private bool ValidateOperations(ImmutableArray<IOperation> operations)
            {
                foreach (var operation in operations)
                {
                    // We need to analyze implicit try statements. This is because if the base type has
                    // a finalizer, C# will create a try/finally statement to wrap the finalizer, with a
                    // call to the base finalizer in the finally section. We need to validate the contents
                    // of the try block
                    // Also analyze the implicit expression statement created for expression bodied implementation.
                    var shouldAnalyze = (!operation.IsImplicit && operation.Kind != OperationKindEx.Attribute) || operation.Kind == OperationKind.Try || operation.Kind == OperationKind.ExpressionStatement;
                    if (shouldAnalyze && !ValidateOperation(operation))
                    {
                        return false;
                    }
                }

                return true;
            }

            private bool ValidateOperation(IOperation operation)
            {
                return operation.Kind switch
                {
                    OperationKind.Empty or OperationKind.Labeled => true,
                    OperationKind.Block => ValidateOperations(((IBlockOperation)operation).Operations),
                    OperationKind.ExpressionStatement => ValidateExpression((IExpressionStatementOperation)operation),
                    OperationKind.Try => ValidateTryOperation((ITryOperation)operation),
                    _ => operation.IsOperationNoneRoot(),// Ignore operation roots with no IOperation API support (OperationKind.None)
                };
            }

            private bool ValidateExpression(IExpressionStatementOperation expressionStatement)
            {
                if (expressionStatement.Operation?.Kind != OperationKind.Invocation)
                {
                    return false;
                }

                var invocation = (IInvocationOperation)expressionStatement.Operation;

                // Valid calls are either to Dispose(false), or to the Finalize method of the base type
                if (!_callDispose)
                {
                    bool result = IsDisposeBoolCall(invocation, _type, expectedValue: false);
                    if (result)
                    {
                        _callDispose = true;
                    }

                    return result;
                }
                else if (_type.BaseType != null && invocation.Instance != null && invocation.Instance.Kind == OperationKind.InstanceReference)
                {
                    IMethodSymbol methodSymbol = invocation.TargetMethod;
                    IInstanceReferenceOperation receiver = (IInstanceReferenceOperation)invocation.Instance;

                    return methodSymbol.IsFinalizer() && Equals(receiver.Type?.OriginalDefinition, _type.BaseType.OriginalDefinition);
                }

                return false;
            }

            private bool ValidateTryOperation(ITryOperation tryOperation)
            {
                // The try operation must have been implicit, as we still analyze it if it isn't implicit
                if (!tryOperation.IsImplicit)
                {
                    return false;
                }

                // There is no way to pass this check without the finally block being correct,
                // as this try-finally is generated by the compiler. No need to verify
                // the contents of the finally.
                if (tryOperation.Finally == null || !tryOperation.Finally.IsImplicit)
                {
                    return false;
                }

                // The try statement is otherwise correct, so validate the main body
                return Validate(tryOperation.Body.Operations);
            }
        }
    }
}