File: Microsoft.NetCore.Analyzers\Runtime\CallGCSuppressFinalizeCorrectly.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 Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Operations;

namespace Microsoft.NetCore.Analyzers.Runtime
{
    using static MicrosoftNetCoreAnalyzersResources;

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

        private static readonly LocalizableString s_localizableTitle = CreateLocalizableResourceString(nameof(CallGCSuppressFinalizeCorrectlyTitle));
        private static readonly LocalizableString s_localizableDescription = CreateLocalizableResourceString(nameof(CallGCSuppressFinalizeCorrectlyDescription));

        internal static readonly DiagnosticDescriptor NotCalledWithFinalizerRule = DiagnosticDescriptorHelper.Create(
            RuleId,
            s_localizableTitle,
            CreateLocalizableResourceString(nameof(CallGCSuppressFinalizeCorrectlyMessageNotCalledWithFinalizer)),
            DiagnosticCategory.Usage,
            RuleLevel.BuildWarningCandidate,
            description: s_localizableDescription,
            isPortedFxCopRule: true,
            isDataflowRule: false);

        internal static readonly DiagnosticDescriptor NotCalledRule = DiagnosticDescriptorHelper.Create(
            RuleId,
            s_localizableTitle,
            CreateLocalizableResourceString(nameof(CallGCSuppressFinalizeCorrectlyMessageNotCalled)),
            DiagnosticCategory.Usage,
            RuleLevel.BuildWarningCandidate,
            description: s_localizableDescription,
            isPortedFxCopRule: true,
            isDataflowRule: false);

        internal static readonly DiagnosticDescriptor NotPassedThisRule = DiagnosticDescriptorHelper.Create(
            RuleId,
            s_localizableTitle,
            CreateLocalizableResourceString(nameof(CallGCSuppressFinalizeCorrectlyMessageNotPassedThis)),
            DiagnosticCategory.Usage,
            RuleLevel.BuildWarningCandidate,
            description: s_localizableDescription,
            isPortedFxCopRule: true,
            isDataflowRule: false);

        internal static readonly DiagnosticDescriptor OutsideDisposeRule = DiagnosticDescriptorHelper.Create(
            RuleId,
            s_localizableTitle,
            CreateLocalizableResourceString(nameof(CallGCSuppressFinalizeCorrectlyMessageOutsideDispose)),
            DiagnosticCategory.Usage,
            RuleLevel.BuildWarningCandidate,
            description: s_localizableDescription,
            isPortedFxCopRule: true,
            isDataflowRule: false);

        public override ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get; } = ImmutableArray.Create(NotCalledWithFinalizerRule, NotCalledRule, NotPassedThisRule, OutsideDisposeRule);

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

            context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None);

            context.RegisterCompilationStartAction(compilationContext =>
            {
                var gcSuppressFinalizeMethodSymbol = compilationContext.Compilation
                                                        .GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemGC)
                                                        ?.GetMembers("SuppressFinalize")
                                                        .OfType<IMethodSymbol>()
                                                        .FirstOrDefault();

                if (gcSuppressFinalizeMethodSymbol == null)
                {
                    return;
                }

                compilationContext.RegisterOperationBlockStartAction(operationBlockContext =>
                {
                    if (operationBlockContext.OwningSymbol.Kind != SymbolKind.Method)
                    {
                        return;
                    }

                    var methodSymbol = (IMethodSymbol)operationBlockContext.OwningSymbol;
                    if (methodSymbol.IsExtern || methodSymbol.IsAbstract)
                    {
                        return;
                    }

                    var analyzer = new SuppressFinalizeAnalyzer(methodSymbol, gcSuppressFinalizeMethodSymbol, compilationContext.Compilation);

                    operationBlockContext.RegisterOperationAction(analyzer.Analyze, OperationKind.Invocation);
                    operationBlockContext.RegisterOperationBlockEndAction(analyzer.OperationBlockEndAction);
                });
            });

        }

        private class SuppressFinalizeAnalyzer
        {
            private enum SuppressFinalizeUsage
            {
                CanCall,
                MustCall,
                MustNotCall
            }

            private readonly Compilation _compilation;
            private readonly IMethodSymbol _containingMethodSymbol;
            private readonly IMethodSymbol _gcSuppressFinalizeMethodSymbol;
            private readonly SuppressFinalizeUsage _expectedUsage;

            private bool _suppressFinalizeCalled;

            public SuppressFinalizeAnalyzer(IMethodSymbol methodSymbol, IMethodSymbol gcSuppressFinalizeMethodSymbol, Compilation compilation)
            {
                this._compilation = compilation;
                this._containingMethodSymbol = methodSymbol;
                this._gcSuppressFinalizeMethodSymbol = gcSuppressFinalizeMethodSymbol;

                this._expectedUsage = GetAllowedSuppressFinalizeUsage(_containingMethodSymbol);
            }

            public void Analyze(OperationAnalysisContext analysisContext)
            {
                var invocationExpression = (IInvocationOperation)analysisContext.Operation;
                if (invocationExpression.TargetMethod.OriginalDefinition.Equals(_gcSuppressFinalizeMethodSymbol))
                {
                    _suppressFinalizeCalled = true;

                    // Check for GC.SuppressFinalize outside of IDisposable.Dispose()
                    if (_expectedUsage == SuppressFinalizeUsage.MustNotCall)
                    {
                        analysisContext.ReportDiagnostic(invocationExpression.Syntax.CreateDiagnostic(
                            OutsideDisposeRule,
                            _containingMethodSymbol.ToDisplayString(SymbolDisplayFormats.ShortSymbolDisplayFormat),
                            _gcSuppressFinalizeMethodSymbol.ToDisplayString(SymbolDisplayFormats.ShortSymbolDisplayFormat)));
                    }

                    // Checks for GC.SuppressFinalize(this)
                    if (!invocationExpression.Arguments.HasExactly(1))
                    {
                        return;
                    }

                    if (invocationExpression.SemanticModel!.GetSymbolInfo(invocationExpression.Arguments.Single().Value.Syntax, analysisContext.CancellationToken).Symbol is not IParameterSymbol parameterSymbol || !parameterSymbol.IsThis)
                    {
                        analysisContext.ReportDiagnostic(invocationExpression.Syntax.CreateDiagnostic(
                            NotPassedThisRule,
                            _containingMethodSymbol.ToDisplayString(SymbolDisplayFormats.ShortSymbolDisplayFormat),
                            _gcSuppressFinalizeMethodSymbol.ToDisplayString(SymbolDisplayFormats.ShortSymbolDisplayFormat)));
                    }
                }
            }

            public void OperationBlockEndAction(OperationBlockAnalysisContext context)
            {
                // Check for absence of GC.SuppressFinalize
                if (!_suppressFinalizeCalled && _expectedUsage == SuppressFinalizeUsage.MustCall)
                {
                    var descriptor = _containingMethodSymbol.ContainingType.HasFinalizer() ? NotCalledWithFinalizerRule : NotCalledRule;
                    context.ReportDiagnostic(_containingMethodSymbol.CreateDiagnostic(
                        descriptor,
                        _containingMethodSymbol.ToDisplayString(SymbolDisplayFormats.ShortSymbolDisplayFormat),
                        _gcSuppressFinalizeMethodSymbol.ToDisplayString(SymbolDisplayFormats.ShortSymbolDisplayFormat)));
                }
            }

            private SuppressFinalizeUsage GetAllowedSuppressFinalizeUsage(IMethodSymbol method)
            {
                // We allow constructors in sealed types to call GC.SuppressFinalize.
                // This allows types that derive from Component (such SqlConnection)
                // to prevent the finalizer they inherit from Component from ever
                // being called.
                if (method.ContainingType.IsSealed && method.IsConstructor() && !method.IsStatic)
                {
                    return SuppressFinalizeUsage.CanCall;
                }

                if (!method.IsDisposeImplementation(_compilation) && !method.IsAsyncDisposeImplementation(_compilation))
                {
                    return SuppressFinalizeUsage.MustNotCall;
                }

                // If the Dispose method is declared in a sealed type, we do
                // not require that the method calls GC.SuppressFinalize
                var hasFinalizer = method.ContainingType.HasFinalizer();
                if (method.ContainingType.IsSealed && !hasFinalizer)
                {
                    return SuppressFinalizeUsage.CanCall;
                }

                // We don't require that non-public types call GC.SuppressFinalize
                // if they don't have a finalizer as the owner of the assembly can
                // control whether any finalizable types derive from them.
                if (method.ContainingType.DeclaredAccessibility != Accessibility.Public && !hasFinalizer)
                {
                    return SuppressFinalizeUsage.CanCall;
                }

                // Even if the Dispose method is declared on a type without a
                // finalizer, we still require it to call GC.SuppressFinalize to
                // prevent derived finalizable types from having to reimplement
                // IDisposable.Dispose just to call it.
                return SuppressFinalizeUsage.MustCall;
            }
        }
    }
}