Skip to content

Fix S2952 FP: Internal Disposable fields must allow Dispose() call on re-creation. #9781

@Aycon3296

Description

@Aycon3296

Description

The rule requires that all fields implementing the IDisposable interface be disposed in the parent's Dispose() method.
The rule should not be triggered if the field is disposed AND in both the method AND the Dispose() function of the containing class.
This is because a class can create multiple IDisposable instances during its lifecycle, store them in the same field, and free them multiple times when replacing them. Calling the Dispose() method early promotes more efficient memory cleanup than passively losing the reference.

Reproducer

internal sealed class Foo: IDisposable
{
    // Any IDisposable field
    private CancellationTokenSource _disposableMember;
    
    private CancellationToken _processToken;
    private readonly SemaphoreSlim _processSync;

    private bool _isDisposed;
    
    private int MagicNumber { get; set; }

    public bool IsWorkComplete { get; set; }

    // I cannot annotate this constructor which instantiate Process through property in .NET Framework 4.7.2
    public Foo()
    {
        _processSync = new SemaphoreSlim(1, 1);
        _disposableMember = new CancellationTokenSource();
        _processToken = _disposableMember.Token;
        IsWorkComplete = false;
        MagicNumber = -1;
        _isDisposed = false;
    }

    private Task<int> Process
    {
        get;
        set
        {
            // Dispose old instance at once
            // HERE WARNING
            _disposableMember.Dispose();

            // Recreate instance
            _disposableMember = new CancellationTokenSource();
            _processToken = _disposableMember.Token;
            field = value;
        }
    }

    public Task<int> GetHardWorkProcessAsync(CancellationToken token = default) // Intentionally synchronous here
    {
        // Attach token
        token.Register(_disposableMember.Cancel);
        return StartHardWorkAsync(token); 
    }

    public int Bar(int defaultMagicNumber = -1)
    {
        if (IsWorkComplete)
            return MagicNumber;

        return defaultMagicNumber;
    }
    
    public async Task<int> BarAsync(int defaultMagicNumber = -1, CancellationToken token = default)
    {
        await _processSync .WaitAsync(token);
        try
        {
            if (IsWorkComplete)
                return MagicNumber;

            token.Register(_disposableMember.Cancel);
            MagicNumber = await Process.ConfigureAwait(false);
            IsWorkComplete = true;

            return MagicNumber;
        }
        catch (Exception)
        {
            // Intentional exception suppression here
            // You can catch the process exception by awaiting the task from GetHardWorkProcessAsync()
            Process = StartHardWorkAsync(_processToken);
            IsWorkComplete = false;
            return defaultMagicNumber;
        }
        finally
        {
            _processSync.Release();
        }
    }
    
    private static async Task<int> StartHardWorkAsync(CancellationToken token = default)
    {
        await Task.Delay(1000, token);
        return 42;
    }
    
    public void Dispose()
    {
        if (_isDisposed)
            return;
        
        _disposableMember.Dispose(); // The member is Disposed!
        _processSync.Dispose();
        _isDisposed = true;
    }
}

Product and Version

SonarQube for IDE (Jetbrains Rider) v11.9.0.83922

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions