| 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; } } } }