Skip to content

CMTK0003 misses dropped async assertions through Task wrappers and branches #152

Open

Description

@CaffeinatedCoder

Problem

The 2.3.0 CMTK0003 guard for unawaited asynchronous assertions misses ordinary statement shapes. A failing assertion can therefore leave a faulted task unobserved and a test can pass without the promised diagnostic.

Reproduced against the real built analyzer and Testing assemblies at release tip 99c6032d9364a5f94b71ed8f7dce1cdada200870, by running Compilation.WithAnalyzers on compilable consumer snippets. No compiler errors occur in these examples.

public static void Check(Task<Option<int>> pending)
{
    pending.ShouldBeNone();                       // CMTK0003, as expected
    pending.ShouldBeNone().ConfigureAwait(false); // no diagnostic
    pending.ShouldBeNone().WaitAsync(TimeSpan.FromSeconds(1)); // no diagnostic
}

ShouldBeNone returns a non-generic Task. The same gap affects command ShouldBeSuccess, expected-value ShouldBeError, ShouldBeAbsent and ShouldBeNull, and both Task and ValueTask input overloads. Both ConfigureAwait overloads and both timeout/token WaitAsync forms were reproduced. Generic-result assertions such as ShouldBeSome().ConfigureAwait(false) are correctly diagnosed when called directly.

There is also a branch shape gap even when the assertion returns Task:

(choose ? first.ShouldBeSome() : second.ShouldBeSome()).ConfigureAwait(false);
(otherIntTask ?? first.ShouldBeSome()).ConfigureAwait(false);
(choose switch
{
    true => first.ShouldBeSome(),
    false => second.ShouldBeSome()
}).ConfigureAwait(false);

Each compiles with no CMTK0003. The corresponding awaited expressions and explicit _ = discards correctly remain silent. A runtime check with Some(42) confirmed ShouldBeNone returns a faulted Task without throwing synchronously; observing that task throws TypeKitAssertionException.

Cause

Tasks.PassesOn in IgnoredOutcomeAnalyzer.cs unwraps ConfigureAwait/WaitAsync on Task and ValueTask only, leaving non-generic Task wrappers opaque. Separately, the new IsPendingAssertion check in Analyze only examines the one outer passedOn invocation, outside the Producers loop that already enumerates conditional/coalesce/switch branches.

Expected

Apply the pending-assertion check to every dropped producer, and follow non-generic Task pass-throughs for these assertions without changing the existing outcome and held-task rules. Cover the input class across assertion return types, Task/ValueTask inputs, direct/conditional/coalesce/switch forms and ConfigureAwait/WaitAsync wrappers, with awaited/held/returned/discarded controls.

This guard is new to 2.3.0. Fixing its reach before shipping avoids leaving its newly promised behavior incomplete when the minor-version Error-severity compatibility boundary freezes again.

Found by the maintainer-requested pre-release review.

Activity

  1. added this to the 2.3.0 milestone on Oct 29, 2026
  2. added
    bugSomething isn't working
    silentCompiles or answers and does less than it appears to
    severity/mediumInconsistency or failure in a narrow scenario
    on Oct 29, 2026
  3. CaffeinatedCoder commented on Oct 29, 2026

    @CaffeinatedCoder
    OwnerAuthor

    Fix on fix/cmtk0003-wrapped-assertions (stacked on #156; its PR opens once #156 merges). Cause as stated: Tasks.PassesOn now hands on a plain Task's ConfigureAwait and WaitAsync, and the pending-assertion check runs inside the Producers loop. New catalogue Variants/DroppedTaskShapes (25 calls x 9 pass-throughs x 6 forms x 6 uses, 6,831 statements) in DroppedTaskVariantTests; no sibling beyond the two forms here. A neighbour outside this issue, Task.WhenAll of dropped assertions, is #157.

    The rule that lets this reach ship in 2.3.0 is decision 118 (design/adr/0118-an-error-may-learn-a-form-only-new-api-can-write.md); the fix commit cc1980b calls it 117, its number before #151 took 0117.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/option-resultOption, Result and their extensionsbugSomething isn't workingpackage/analyzersAffects CodoMetis.TypeKit.Analyzerspackage/testingAffects CodoMetis.TypeKit.Testingseverity/mediumInconsistency or failure in a narrow scenariosilentCompiles or answers and does less than it appears to

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions