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