CS S1121 FP on async blocks and just parentheses as well

Then tell us:

  • What language is this for?

    • .NET 8 (C# 12)
  • Which rule?

    • csharpsquid:S1121
  • Why do you believe it’s a false-positive/false-negative?

    • It triggers the issue without having the problems (actual conditional or branching block) mentioned in the description.
  • Are you using

    • SonarQube Server (10.3 (build 82913))
  • How can we reproduce the problem? Give us a self-contained snippet of code (formatted text, no screenshots)

public class Bar
{
    private Foo? _fooCache;
    private Task<Foo?>? _taskFooCache;
    private readonly IFooProvider _fooProvider = new FooProvider();

    private async Task Test()
    {
        var foo = await GetFoo();
        var fooNullable = await GetFooNullable();
        var fooNullable2 = await GetFooNullable2();
        var fooTriggered = await GetFooTriggered();
        var fooAsyncTriggered = await GetFooAsyncTriggered();
        var fooNullableAsyncTriggered = await GetFooNullableAsyncTriggered();
    }

    private async Task<Foo> GetFoo() =>
        _fooCache ??= await _fooProvider.GetFooAsync()
            ?? throw new Exception("Not Found");

    private async Task<Foo> GetFooTriggered() =>
        (_fooCache ??= await _fooProvider.GetFooAsync())
        ?? throw new Exception("Not Found");

    private async Task<Foo?> GetFooNullable() =>
        _fooCache ??= await _fooProvider.GetFooAsync();

    private async Task<Foo?> GetFooNullable2() =>
        (_fooCache ??= await _fooProvider.GetFooAsync());

    private async Task<Foo> GetFooAsyncTriggered() =>
        await (_taskFooCache ??= _fooProvider.GetFooAsync())
        ?? throw new Exception("Not Found");

    private async Task<Foo?> GetFooNullableAsyncTriggered() =>
        await (_taskFooCache ??= _fooProvider.GetFooAsync());

    public interface IFooProvider
    {
        Task<Foo?> GetFooAsync();
    }

    public class FooProvider : IFooProvider
    {
        public Task<Foo?> GetFooAsync() => Task.FromResult<Foo?>(new());
    }

    public record Foo;
}

Hi @hergendy, welcome to the community and thanks for raising this!

I can confirm this behaviour.
This method in particular I think is an FP, while technically this is within an await expression and as such a subexpression, it should probably be an exception to the rule. I will add a reproducer to our codebase for it.

    private async Task<Foo?> GetFooNullableAsyncTriggered() =>
        await (_taskFooCache ??= _fooProvider.GetFooAsync());

While in the other instances the rule does appear to be working as intended.

in GetFooTriggered an assignment is occuring on the left hand side of a null coalescing expression which is raising as intended. While in GetFoo a coalescing expression is occuring on the rhs of a null coalescing assignment expression.

e.g. GetFoo and GetFooTwo do not raise here but do raise in GetFooTriggered as the assignment is part of the null coalescing expression.

    public class Tester
    {
        public int? possible;

        private int? GetFoo() =>
            possible ??= FooProvider.GetFooAsync() ?? 1;

        private int? GetFooTwo() =>
            possible ??= (FooProvider.GetFooAsync() ?? 1);

        private int? GetFooTriggered() =>
            (possible ??= FooProvider.GetFooAsync()) ?? 2;

        sealed class FooProvider
        {
            public static int? GetFooAsync() => 3;
        }
    }