File: src\RoslynAnalyzers\Utilities\FlowAnalysis\FlowAnalysis\Analysis\DisposeAnalysis\DisposeAnalysisHelper.cs
Web Access
Project: src\src\RoslynAnalyzers\Microsoft.CodeAnalysis.AnalyzerUtilities\Microsoft.CodeAnalysis.AnalyzerUtilities.csproj (Microsoft.CodeAnalysis.AnalyzerUtilities)
// 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;
using System.Collections.Concurrent;
using System.Collections.Immutable;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Threading;
using Analyzer.Utilities.Extensions;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.FlowAnalysis.DataFlow;
using Microsoft.CodeAnalysis.FlowAnalysis.DataFlow.DisposeAnalysis;
using Microsoft.CodeAnalysis.FlowAnalysis.DataFlow.PointsToAnalysis;
using Microsoft.CodeAnalysis.Operations;
using Microsoft.CodeAnalysis.PooledObjects;
using Roslyn.Utilities;
 
namespace Analyzer.Utilities
{
    /// <summary>
    /// Helper for DisposeAnalysis.
    /// </summary>
    internal sealed class DisposeAnalysisHelper
    {
        private static readonly string[] s_disposeOwnershipTransferLikelyTypes =
            [
                "System.IO.Stream",
                "System.IO.TextReader",
                "System.IO.TextWriter",
                "System.Resources.IResourceReader",
            ];
        private static readonly BoundedCacheWithFactory<Compilation, DisposeAnalysisHelper> s_DisposeHelperCache = new();
 
        private static readonly ImmutableHashSet<OperationKind> s_DisposableCreationKinds = ImmutableHashSet.Create(
            OperationKind.ObjectCreation,
            OperationKind.TypeParameterObjectCreation,
            OperationKind.DynamicObjectCreation,
            OperationKind.Invocation);
 
        private readonly WellKnownTypeProvider _wellKnownTypeProvider;
        private readonly ImmutableHashSet<INamedTypeSymbol> _disposeOwnershipTransferLikelyTypes;
        private ConcurrentDictionary<INamedTypeSymbol, ImmutableHashSet<IFieldSymbol>>? _lazyDisposableFieldsMap;
        public INamedTypeSymbol? IDisposable { get; }
        public INamedTypeSymbol? IAsyncDisposable { get; }
        public INamedTypeSymbol? ConfiguredAsyncDisposable { get; }
        public INamedTypeSymbol? Task { get; }
        public INamedTypeSymbol? ValueTask { get; }
        public INamedTypeSymbol? ConfiguredValueTaskAwaitable { get; }
        public INamedTypeSymbol? StringReader { get; }
        public INamedTypeSymbol? MemoryStream { get; }
 
        private DisposeAnalysisHelper(Compilation compilation)
        {
            _wellKnownTypeProvider = WellKnownTypeProvider.GetOrCreate(compilation);
 
            IDisposable = _wellKnownTypeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemIDisposable);
            IAsyncDisposable = _wellKnownTypeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemIAsyncDisposable);
            ConfiguredAsyncDisposable = _wellKnownTypeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemRuntimeCompilerServicesConfiguredAsyncDisposable);
            Task = _wellKnownTypeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemThreadingTasksTask);
            ValueTask = _wellKnownTypeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemThreadingTasksValueTask);
            ConfiguredValueTaskAwaitable = _wellKnownTypeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemRuntimeCompilerServicesConfiguredValueTaskAwaitable);
            StringReader = _wellKnownTypeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemIOStringReader);
            MemoryStream = _wellKnownTypeProvider.GetOrCreateTypeByMetadataName(WellKnownTypeNames.SystemIOMemoryStream);
 
            _disposeOwnershipTransferLikelyTypes = IDisposable != null ?
                GetDisposeOwnershipTransferLikelyTypes(compilation) :
                ImmutableHashSet<INamedTypeSymbol>.Empty;
        }
 
        private static ImmutableHashSet<INamedTypeSymbol> GetDisposeOwnershipTransferLikelyTypes(Compilation compilation)
        {
            var builder = PooledHashSet<INamedTypeSymbol>.GetInstance();
            foreach (var typeName in s_disposeOwnershipTransferLikelyTypes)
            {
                INamedTypeSymbol? typeSymbol = compilation.GetOrCreateTypeByMetadataName(typeName);
                if (typeSymbol != null)
                {
                    builder.Add(typeSymbol);
                }
            }
 
            return builder.ToImmutableAndFree();
        }
 
        private void EnsureDisposableFieldsMap()
        {
            if (_lazyDisposableFieldsMap == null)
            {
                Interlocked.CompareExchange(ref _lazyDisposableFieldsMap, new ConcurrentDictionary<INamedTypeSymbol, ImmutableHashSet<IFieldSymbol>>(), null);
            }
        }
 
        public static bool TryGetOrCreate(Compilation compilation, [NotNullWhen(returnValue: true)] out DisposeAnalysisHelper? disposeHelper)
        {
            disposeHelper = s_DisposeHelperCache.GetOrCreateValue(compilation, CreateDisposeAnalysisHelper);
            if (disposeHelper.IDisposable == null)
            {
                disposeHelper = null;
                return false;
            }
 
            return true;
 
            // Local functions
            static DisposeAnalysisHelper CreateDisposeAnalysisHelper(Compilation compilation)
                => new(compilation);
        }
 
        public static Func<ITypeSymbol?, bool> GetIsDisposableDelegate(Compilation compilation)
        {
            if (TryGetOrCreate(compilation, out var disposeAnalysisHelper))
            {
                return disposeAnalysisHelper.IsDisposable;
            }
 
            return _ => false;
        }
 
        public bool TryGetOrComputeResult(
            ImmutableArray<IOperation> operationBlocks,
            IMethodSymbol containingMethod,
            AnalyzerOptions analyzerOptions,
            DiagnosticDescriptor rule,
            PointsToAnalysisKind defaultPointsToAnalysisKind,
            bool trackInstanceFields,
            bool trackExceptionPaths,
            [NotNullWhen(returnValue: true)] out DisposeAnalysisResult? disposeAnalysisResult,
            [NotNullWhen(returnValue: true)] out PointsToAnalysisResult? pointsToAnalysisResult,
            InterproceduralAnalysisPredicate? interproceduralAnalysisPredicate = null,
            bool defaultDisposeOwnershipTransferAtConstructor = false)
        {
            var cfg = operationBlocks.GetControlFlowGraph();
            if (cfg != null && IDisposable != null)
            {
                disposeAnalysisResult = DisposeAnalysis.TryGetOrComputeResult(cfg, containingMethod, _wellKnownTypeProvider,
                    analyzerOptions, rule, _disposeOwnershipTransferLikelyTypes, defaultPointsToAnalysisKind, trackInstanceFields,
                    trackExceptionPaths, out pointsToAnalysisResult, interproceduralAnalysisPredicate: interproceduralAnalysisPredicate,
                    defaultDisposeOwnershipTransferAtConstructor: defaultDisposeOwnershipTransferAtConstructor);
                if (disposeAnalysisResult != null)
                {
                    RoslynDebug.Assert(pointsToAnalysisResult is object);
                    return true;
                }
            }
 
            disposeAnalysisResult = null;
            pointsToAnalysisResult = null;
            return false;
        }
 
        private bool HasDisposableOwnershipTransferForConstructorParameter(IMethodSymbol containingMethod) =>
            containingMethod.MethodKind == MethodKind.Constructor &&
            containingMethod.Parameters.Any(p => _disposeOwnershipTransferLikelyTypes.Contains(p.Type));
 
        private bool IsDisposableCreation(IOperation operation)
            => (s_DisposableCreationKinds.Contains(operation.Kind) ||
                operation.Parent is IArgumentOperation argument && argument.Parameter?.RefKind == RefKind.Out) &&
               IsDisposable(operation.Type);
 
        public bool HasAnyDisposableCreationDescendant(ImmutableArray<IOperation> operationBlocks, IMethodSymbol containingMethod)
        {
            return operationBlocks.HasAnyOperationDescendant(IsDisposableCreation) ||
                HasDisposableOwnershipTransferForConstructorParameter(containingMethod);
        }
 
        public bool IsDisposableTypeNotRequiringToBeDisposed(ITypeSymbol typeSymbol) =>
            // Common case doesn't require dispose. https://learn.microsoft.com/dotnet/api/system.threading.tasks.task.dispose
            typeSymbol.DerivesFrom(Task, baseTypesOnly: true) ||
            // StringReader doesn't need to be disposed: https://learn.microsoft.com/dotnet/api/system.io.stringreader
            SymbolEqualityComparer.Default.Equals(typeSymbol, StringReader) ||
            // MemoryStream doesn't need to be disposed. https://learn.microsoft.com/dotnet/api/system.io.memorystream
            // Subclasses *might* need to be disposed, but that is the less common case,
            // and the common case is a huge source of noisy warnings.
            SymbolEqualityComparer.Default.Equals(typeSymbol, MemoryStream);
 
        public ImmutableHashSet<IFieldSymbol> GetDisposableFields(INamedTypeSymbol namedType)
        {
            EnsureDisposableFieldsMap();
            RoslynDebug.Assert(_lazyDisposableFieldsMap != null);
 
            if (_lazyDisposableFieldsMap.TryGetValue(namedType, out ImmutableHashSet<IFieldSymbol> disposableFields))
            {
                return disposableFields;
            }
 
            if (!namedType.IsDisposable(IDisposable, IAsyncDisposable, ConfiguredAsyncDisposable))
            {
                disposableFields = ImmutableHashSet<IFieldSymbol>.Empty;
            }
            else
            {
                disposableFields = namedType.GetMembers()
                    .OfType<IFieldSymbol>()
                    .Where(f => IsDisposable(f.Type) && !IsDisposableTypeNotRequiringToBeDisposed(f.Type))
                    .ToImmutableHashSet();
            }
 
            return _lazyDisposableFieldsMap.GetOrAdd(namedType, disposableFields);
        }
 
        /// <summary>
        /// Returns true if the given <paramref name="location"/> was created for an allocation in the <paramref name="containingMethod"/>
        /// or represents a location created for a constructor parameter whose type indicates dispose ownership transfer.
        /// </summary>
        public bool IsDisposableCreationOrDisposeOwnershipTransfer(AbstractLocation location, IMethodSymbol containingMethod)
        {
            if (location.Creation == null)
            {
                return location.Symbol?.Kind == SymbolKind.Parameter &&
                    HasDisposableOwnershipTransferForConstructorParameter(containingMethod);
            }
 
            return IsDisposableCreation(location.Creation);
        }
 
        public bool IsDisposable([NotNullWhen(returnValue: true)] ITypeSymbol? type)
            => type != null && type.IsDisposable(IDisposable, IAsyncDisposable, ConfiguredAsyncDisposable);
 
        public DisposeMethodKind GetDisposeMethodKind(IMethodSymbol method)
            => method.GetDisposeMethodKind(IDisposable, IAsyncDisposable, ConfiguredAsyncDisposable, Task, ValueTask, ConfiguredValueTaskAwaitable);
    }
}