// 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.Generic; using System.Collections.Immutable; using System.Diagnostics; using System.Linq; using Analyzer.Utilities; using Analyzer.Utilities.Extensions; using Microsoft.CodeAnalysis.FlowAnalysis.DataFlow.PointsToAnalysis; using Microsoft.CodeAnalysis.Operations; using Microsoft.CodeAnalysis.Shared.Extensions; using Roslyn.Utilities; namespace Microsoft.CodeAnalysis.FlowAnalysis.DataFlow.DisposeAnalysis { using DisposeAnalysisData = DictionaryAnalysisData<AbstractLocation, DisposeAbstractValue>; public partial class DisposeAnalysis : ForwardDataFlowAnalysis<DisposeAnalysisData, DisposeAnalysisContext, DisposeAnalysisResult, DisposeBlockAnalysisResult, DisposeAbstractValue> { /// <summary> /// Operation visitor to flow the dispose values across a given statement in a basic block. /// </summary> private sealed class DisposeDataFlowOperationVisitor : AbstractLocationDataFlowOperationVisitor<DisposeAnalysisData, DisposeAnalysisContext, DisposeAnalysisResult, DisposeAbstractValue> { private readonly Dictionary<IFieldSymbol, PointsToAbstractValue>? _trackedInstanceFieldLocations; private ImmutableHashSet<INamedTypeSymbol> DisposeOwnershipTransferLikelyTypes => DataFlowAnalysisContext.DisposeOwnershipTransferLikelyTypes; private bool DisposeOwnershipTransferAtConstructor => DataFlowAnalysisContext.DisposeOwnershipTransferAtConstructor; private bool DisposeOwnershipTransferAtMethodCall => DataFlowAnalysisContext.DisposeOwnershipTransferAtMethodCall; public DisposeDataFlowOperationVisitor(DisposeAnalysisContext analysisContext) : base(analysisContext) { Debug.Assert(IDisposableNamedType != null); Debug.Assert(CollectionNamedTypes.All(ct => ct.TypeKind == TypeKind.Interface)); Debug.Assert(analysisContext.DisposeOwnershipTransferLikelyTypes != null); Debug.Assert(analysisContext.PointsToAnalysisResult != null); if (analysisContext.TrackInstanceFields) { _trackedInstanceFieldLocations = []; } } public ImmutableDictionary<IFieldSymbol, PointsToAbstractValue> TrackedInstanceFieldPointsToMap { get { if (_trackedInstanceFieldLocations == null) { throw new InvalidOperationException(); } return _trackedInstanceFieldLocations.ToImmutableDictionary(); } } protected override DisposeAbstractValue GetAbstractDefaultValue(ITypeSymbol? type) => DisposeAbstractValue.NotDisposable; protected override DisposeAbstractValue GetAbstractValue(AbstractLocation location) => CurrentAnalysisData.TryGetValue(location, out var value) ? value : ValueDomain.UnknownOrMayBeValue; protected override bool HasAnyAbstractValue(DisposeAnalysisData data) => data.Count > 0; protected override void SetAbstractValue(AbstractLocation location, DisposeAbstractValue value) { if (!location.IsNull && location.LocationType != null && (!location.LocationType.IsValueType || location.LocationType.IsRefLikeType) && IsDisposable(location.LocationType)) { CurrentAnalysisData[location] = value; } } protected override void StopTrackingAbstractValue(AbstractLocation location) => CurrentAnalysisData.Remove(location); protected override void ResetCurrentAnalysisData() => ResetAnalysisData(CurrentAnalysisData); protected override DisposeAbstractValue HandleInstanceCreation(IOperation creation, PointsToAbstractValue instanceLocation, DisposeAbstractValue defaultValue) { if (ExecutingExceptionPathsAnalysisPostPass) { base.HandlePossibleThrowingOperation(creation); } defaultValue = DisposeAbstractValue.NotDisposable; var instanceType = creation.Type; if (!IsDisposable(instanceType) || !IsCurrentBlockReachable()) { return defaultValue; } // Handle special cases where we don't want to track the type although we know // it is disposable (user option, special types...) if (DataFlowAnalysisContext.IsDisposableTypeNotRequiringToBeDisposed(instanceType)) { return defaultValue; } SetAbstractValue(instanceLocation, DisposeAbstractValue.NotDisposed); return DisposeAbstractValue.NotDisposed; } private void HandleDisposingOperation(IOperation disposingOperation, IOperation? disposedInstance) { if (disposedInstance == null || !IsDisposable(disposedInstance.Type)) { return; } PointsToAbstractValue instanceLocation = GetPointsToAbstractValue(disposedInstance); foreach (AbstractLocation location in instanceLocation.Locations) { if (CurrentAnalysisData.TryGetValue(location, out var currentDisposeValue)) { DisposeAbstractValue disposeValue = currentDisposeValue.WithNewDisposingOperation(disposingOperation); SetAbstractValue(location, disposeValue); } } } private void HandlePossibleInvalidatingOperation(IOperation invalidatedInstance) { PointsToAbstractValue instanceLocation = GetPointsToAbstractValue(invalidatedInstance); foreach (AbstractLocation location in instanceLocation.Locations) { if (CurrentAnalysisData.TryGetValue(location, out var currentDisposeValue) && currentDisposeValue.Kind != DisposeAbstractValueKind.NotDisposable) { SetAbstractValue(location, DisposeAbstractValue.Invalid); } } } private void HandlePossibleEscapingOperation(IOperation escapingOperation, ImmutableHashSet<AbstractLocation> escapedLocations) { foreach (AbstractLocation escapedLocation in escapedLocations) { if (CurrentAnalysisData.TryGetValue(escapedLocation, out var currentDisposeValue) && currentDisposeValue.Kind != DisposeAbstractValueKind.Unknown) { DisposeAbstractValue newDisposeValue = currentDisposeValue.WithNewEscapingOperation(escapingOperation); SetAbstractValue(escapedLocation, newDisposeValue); } } } protected override void SetAbstractValueForArrayElementInitializer(IArrayCreationOperation arrayCreation, ImmutableArray<AbstractIndex> indices, ITypeSymbol elementType, IOperation initializer, DisposeAbstractValue value) { // Escaping from array element assignment is handled in PointsTo analysis. // We do not need to do anything here. } protected override void SetAbstractValueForAssignment(IOperation target, IOperation? assignedValueOperation, DisposeAbstractValue assignedValue, bool mayBeAssignment = false) { // Assignments should automatically transfer PointsTo value. // We do not need to do anything here. } protected override void SetAbstractValueForTupleElementAssignment(AnalysisEntity tupleElementEntity, IOperation assignedValueOperation, DisposeAbstractValue assignedValue) { // Assigning to tuple elements should automatically transfer PointsTo value. // We do not need to do anything here. } protected override void SetValueForParameterPointsToLocationOnEntry(IParameterSymbol parameter, PointsToAbstractValue pointsToAbstractValue) { if (DisposeOwnershipTransferLikelyTypes.Contains(parameter.Type) || (DisposeOwnershipTransferAtConstructor && parameter.ContainingSymbol.IsConstructor())) { SetAbstractValue(pointsToAbstractValue, DisposeAbstractValue.NotDisposed); } } protected override void EscapeValueForParameterPointsToLocationOnExit(IParameterSymbol parameter, AnalysisEntity analysisEntity, ImmutableHashSet<AbstractLocation> escapedLocations) { Debug.Assert(!escapedLocations.IsEmpty); Debug.Assert(parameter.RefKind != RefKind.None); var escapedDisposableLocations = escapedLocations.Where(l => IsDisposable(l.LocationType)); SetAbstractValue(escapedDisposableLocations, ValueDomain.UnknownOrMayBeValue); } protected override DisposeAbstractValue ComputeAnalysisValueForEscapedRefOrOutArgument(IArgumentOperation operation, DisposeAbstractValue defaultValue) { Debug.Assert(operation.Parameter!.RefKind is RefKind.Ref or RefKind.Out); // Special case: don't flag "out" arguments for "bool TryGetXXX(..., out value)" invocations. if (operation.Parent is IInvocationOperation invocation && invocation.TargetMethod.ReturnType.SpecialType == SpecialType.System_Boolean && invocation.TargetMethod.Name.StartsWith("TryGet", StringComparison.Ordinal) && invocation.Arguments[^1] == operation) { return DisposeAbstractValue.NotDisposable; } return base.ComputeAnalysisValueForEscapedRefOrOutArgument(operation, defaultValue); } protected override DisposeAnalysisData MergeAnalysisData(DisposeAnalysisData value1, DisposeAnalysisData value2) => DisposeAnalysisDomainInstance.Merge(value1, value2); protected override void UpdateValuesForAnalysisData(DisposeAnalysisData targetAnalysisData) => UpdateValuesForAnalysisData(targetAnalysisData, CurrentAnalysisData); protected override DisposeAnalysisData GetClonedAnalysisData(DisposeAnalysisData analysisData) => GetClonedAnalysisDataHelper(CurrentAnalysisData); public override DisposeAnalysisData GetEmptyAnalysisData() => GetEmptyAnalysisDataHelper(); protected override DisposeAnalysisData GetExitBlockOutputData(DisposeAnalysisResult analysisResult) => GetClonedAnalysisDataHelper(analysisResult.ExitBlockOutput.Data); protected override void ApplyMissingCurrentAnalysisDataForUnhandledExceptionData(DisposeAnalysisData dataAtException, ThrownExceptionInfo throwBranchWithExceptionType) => ApplyMissingCurrentAnalysisDataForUnhandledExceptionData(dataAtException, CurrentAnalysisData); protected override bool Equals(DisposeAnalysisData value1, DisposeAnalysisData value2) => EqualsHelper(value1, value2); #region Visitor methods public override DisposeAbstractValue DefaultVisit(IOperation operation, object? argument) { _ = base.DefaultVisit(operation, argument); return DisposeAbstractValue.NotDisposable; } public override DisposeAbstractValue Visit(IOperation? operation, object? argument) { var value = base.Visit(operation, argument); if (operation != null) HandlePossibleEscapingOperation(operation, GetEscapedLocations(operation)); return value; } protected override void HandlePossibleThrowingOperation(IOperation operation) { // Handle possible throwing operation. // Note that we handle the cases for object creation and method invocation // separately as these also lead to NotDisposed allocations, but // they should not be considered as part of current state when possible exception occurs. if (operation != null && operation.Kind != OperationKind.ObjectCreation && (operation is not IInvocationOperation invocation || invocation.TargetMethod.IsLambdaOrLocalFunctionOrDelegate())) { base.HandlePossibleThrowingOperation(operation); } } // FxCop compat: Catches things like static calls to File.Open() and Create() private static bool IsDisposableCreationSpecialCase(IMethodSymbol targetMethod) => targetMethod.IsStatic && (targetMethod.Name.StartsWith("create", StringComparison.OrdinalIgnoreCase) || targetMethod.Name.StartsWith("open", StringComparison.OrdinalIgnoreCase)); public override DisposeAbstractValue VisitInvocation_NonLambdaOrDelegateOrLocalFunction( IMethodSymbol method, IOperation? visitedInstance, ImmutableArray<IArgumentOperation> visitedArguments, bool invokedAsDelegate, IOperation originalOperation, DisposeAbstractValue defaultValue) { var value = base.VisitInvocation_NonLambdaOrDelegateOrLocalFunction(method, visitedInstance, visitedArguments, invokedAsDelegate, originalOperation, defaultValue); var disposeMethodKind = GetDisposeMethodKind(method); switch (disposeMethodKind) { case DisposeMethodKind.Dispose: case DisposeMethodKind.DisposeBool: case DisposeMethodKind.DisposeAsync: HandleDisposingOperation(originalOperation, visitedInstance); return value; case DisposeMethodKind.Close: // FxCop compat: Calling "this.Close" shouldn't count as disposing the object within the implementation of Dispose. if (visitedInstance?.Kind != OperationKind.InstanceReference) { goto case DisposeMethodKind.Dispose; } break; default: // FxCop compat: Catches things like static calls to File.Open() and Create() if (IsDisposableCreationSpecialCase(method)) { var instanceLocation = GetPointsToAbstractValue(originalOperation); return HandleInstanceCreation(originalOperation, instanceLocation, value); } break; } if (ExecutingExceptionPathsAnalysisPostPass) { base.HandlePossibleThrowingOperation(originalOperation); } return value; } protected override void ApplyInterproceduralAnalysisResult( DisposeAnalysisData resultData, bool isLambdaOrLocalFunction, bool hasDelegateTypeArgument, DisposeAnalysisResult analysisResult) { base.ApplyInterproceduralAnalysisResult(resultData, isLambdaOrLocalFunction, hasDelegateTypeArgument, analysisResult); // Apply the tracked instance field locations from interprocedural analysis. if (_trackedInstanceFieldLocations != null && analysisResult.TrackedInstanceFieldPointsToMap != null) { foreach (var (field, pointsToValue) in analysisResult.TrackedInstanceFieldPointsToMap) { if (!_trackedInstanceFieldLocations.ContainsKey(field)) { _trackedInstanceFieldLocations.Add(field, pointsToValue); } } } } protected override void PostProcessArgument(IArgumentOperation operation, bool isEscaped) { base.PostProcessArgument(operation, isEscaped); if (isEscaped) { // Discover if a disposable object is being passed into the creation method for this new disposable object // and if the new disposable object assumes ownership of that passed in disposable object. if (IsDisposeOwnershipTransfer()) { var pointsToValue = GetPointsToAbstractValue(operation.Value); HandlePossibleEscapingOperation(operation, pointsToValue.Locations); return; } else if (FlowBranchConditionKind == ControlFlowConditionKind.WhenFalse && operation.Parameter?.RefKind == RefKind.Out && operation.Parent is IInvocationOperation invocation && invocation.TargetMethod.ReturnType.SpecialType == SpecialType.System_Boolean) { // Case 1: // // Assume 'obj' is not a valid object on the 'else' path. // if (TryXXX(out IDisposable obj)) // { // obj.Dispose(); // } // // return; // Case 2: // if (!TryXXX(out IDisposable obj)) // { // return; // Assume 'obj' is not a valid object on this path. // } // // obj.Dispose(); HandlePossibleInvalidatingOperation(operation); return; } } // Ref or out argument values from callee might be escaped by assigning to field. if (operation.Parameter?.RefKind is RefKind.Out or RefKind.Ref) { HandlePossibleEscapingOperation(operation, GetEscapedLocations(operation)); } return; // Local functions. bool IsDisposeOwnershipTransfer() { if (operation.Parameter == null || operation.Parameter.RefKind == RefKind.Out) { // Out arguments are always owned by the caller. return false; } return operation.Parent switch { IObjectCreationOperation => DisposeOwnershipTransferAtConstructor || DisposeOwnershipTransferLikelyTypes.Contains(operation.Parameter.Type), IInvocationOperation invocation => DisposeOwnershipTransferAtMethodCall || IsDisposableCreationSpecialCase(invocation.TargetMethod) && DisposeOwnershipTransferLikelyTypes.Contains(operation.Parameter.Type), _ => false, }; } } public override DisposeAbstractValue VisitFieldReference(IFieldReferenceOperation operation, object? argument) { var value = base.VisitFieldReference(operation, argument); if (_trackedInstanceFieldLocations != null && !operation.Field.IsStatic && operation.Instance?.Kind == OperationKind.InstanceReference) { var pointsToAbstractValue = GetPointsToAbstractValue(operation); if (pointsToAbstractValue.Kind == PointsToAbstractValueKind.KnownLocations && pointsToAbstractValue.Locations.Count == 1) { var location = pointsToAbstractValue.Locations.Single(); if (location.IsAnalysisEntityDefaultLocation) { if (!_trackedInstanceFieldLocations.TryGetValue(operation.Field, out _)) { // First field reference on any control flow path. // Create a default instance to represent the object referenced by the field at start of the method and // check if the instance has NotDisposed state, indicating it is a disposable field that must be tracked. if (HandleInstanceCreation(operation, pointsToAbstractValue, defaultValue: DisposeAbstractValue.NotDisposable) == DisposeAbstractValue.NotDisposed) { _trackedInstanceFieldLocations.Add(operation.Field, pointsToAbstractValue); } } else if (!CurrentAnalysisData.ContainsKey(location)) { // This field has already started being tracked on a different control flow path. // Process the default instance creation on this control flow path as well. var disposedState = HandleInstanceCreation(operation, pointsToAbstractValue, DisposeAbstractValue.NotDisposable); Debug.Assert(disposedState == DisposeAbstractValue.NotDisposed); } } } } return value; } public override DisposeAbstractValue VisitBinaryOperatorCore(IBinaryOperation operation, object? argument) { var value = base.VisitBinaryOperatorCore(operation, argument); // Handle null-check for a disposable symbol on a control flow branch. // var x = flag ? new Disposable() : null; // if (x == null) // { // // Disposable allocation above cannot exist on this code path. // } // // if (x == null) // { // // This code path // } var isNullEqualsOnWhenTrue = FlowBranchConditionKind == ControlFlowConditionKind.WhenTrue && (operation.OperatorKind == BinaryOperatorKind.Equals || operation.OperatorKind == BinaryOperatorKind.ObjectValueEquals); // if (x != null) { ... } // else // { // // This code path // } var isNullNotEqualsOnWhenFalse = FlowBranchConditionKind == ControlFlowConditionKind.WhenFalse && (operation.OperatorKind == BinaryOperatorKind.NotEquals || operation.OperatorKind == BinaryOperatorKind.ObjectValueNotEquals); if (isNullEqualsOnWhenTrue || isNullNotEqualsOnWhenFalse) { if (GetNullAbstractValue(operation.RightOperand) == NullAbstractValue.Null) { // if (x == null) HandlePossibleInvalidatingOperation(operation.LeftOperand); } else if (GetNullAbstractValue(operation.LeftOperand) == NullAbstractValue.Null) { // if (null == x) HandlePossibleInvalidatingOperation(operation.RightOperand); } } return value; } public override DisposeAbstractValue VisitIsNull(IIsNullOperation operation, object? argument) { var value = base.VisitIsNull(operation, argument); // Handle null-check for a disposable symbol on a control flow branch. // See comments in VisitBinaryOperatorCore override above for further details. if (FlowBranchConditionKind == ControlFlowConditionKind.WhenTrue) { HandlePossibleInvalidatingOperation(operation.Operand); } return value; } #endregion } } } |