Skip to content

Bug: exempt-async-member-lambdas-remain-counted #558

Description

@drmoisan
  • Work Mode: full-bug

Summary

Lambda bodies declared inside a member that carries [ExcludeFromCodeCoverage] and is async (or
an iterator) remain in the coverage denominator after the issue #457 closure filter. This is residual
(a) of #457: a deliberate, documented scope choice rather than an oversight, recorded so it can be
addressed on its own terms rather than by widening #457.

Remove-CoberturaExemptClosureCoverage (added by #457 in
scripts/vscode/Invoke-MSTestWithCoverage.ClosureFilter.ps1) infers that a member is exempt from its
absence from the report's instrumented method set. An async member emits no plain <method>
element - its whole body moves to a state machine class Type.<Member>d__<N> - so the filter must
admit d__ class names as proof that the declaring member exists. That admission is mandatory in the
other direction: without it, covered lambdas inside non-exempt async members would be deleted,
failing #457's required direction 2. The consequence is that an attributed async member also enters
the presence set, and its lambdas are retained.

Environment

  • OS/version: Windows 11 Pro 10.0.26200
  • .NET/framework: .NET Framework 4.8.1 (net481); post-processing runs under PowerShell 7
  • Command/flags used: pwsh -NoProfile -File scripts/vscode/Invoke-MSTestWithCoverage.ps1 -SearchRoot . -Configuration Debug -CoverageOutput "coverage\coverage.cobertura.xml"
  • Data source or fixture: any first-party C# file with an async member carrying [ExcludeFromCodeCoverage] that declares a lambda in its body

Steps to Reproduce

  1. Take a member that carries [ExcludeFromCodeCoverage], is declared async, and declares one or more lambdas in its body. QuickFiler.Controllers.QfcItemController.ToggleExpansionAsync (QuickFiler/Controllers/QfcItemController.Navigation.cs:191-192) is a live example of the attributed async shape.
  2. Run the coverage pipeline.
  3. Inspect the post-processed Cobertura report for that file.
  4. Observe that the closure class carrying the member's lambdas survives the filter, and its lines remain in lines-valid.

Expected Behavior

A lambda declared inside a member carrying [ExcludeFromCodeCoverage] is excluded from the coverage
denominator regardless of whether that member is async.

Actual Behavior

The lambda's lines are retained when the declaring member is async, because the member's state
machine class admits it to the presence set. The lines are correctly removed when the declaring
member is not async.

Logs / Screenshots

  • Attached minimal logs or screenshot

  • Measured evidence from the Bug: excludefromcodecoverage-does-not-suppress-nested-lambdas #457 [P0-T12] probe, recorded in
    docs/features/active/2026-08-10-excludefromcodecoverage-nested-lambdas-457/evidence/baseline/async-d-state-machine-probe.2026-08-11T00-38.md:

    name="QuickFiler.Controllers.QfcItemController.&lt;ToggleExpansionAsync&gt;d__203"
    

    ToggleExpansionAsync carries [System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage], and the
    attribute was verified present in commit 6b821480 (2026-07-03), 35 days before the raw corpus was
    captured (2026-08-07T02:19:25Z). Probe answer: YES - the collector does emit a d__ class for an
    attributed async member.

Impact / Severity

  • Blocker
  • High
  • Medium
  • Low

The failure mode is under-exclusion: a file measures no better than it truly is. No coverage is
wrongly deleted, and no gate can be passed that should have failed. The cost is a residual, invisible
ceiling on any file that uses the "thin exempt production forwarder" seam pattern on an async
member. 62 first-party members carry [ExcludeFromCodeCoverage] and are declared async (enumerated
in the probe artifact), though only those that also declare lambdas are affected.

Source

From: docs/features/potential/2026-08-11-exempt-async-member-lambdas-remain-counted.md

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions