Skip to content

Bug: local-functions-in-exempt-members-remain-counted #559

Description

@drmoisan
  • Work Mode: full-bug

Summary

Local functions declared inside a member that carries [ExcludeFromCodeCoverage] remain in the
coverage denominator after the issue #457 closure filter. This is residual (b) of #457: a deliberate
scope choice, recorded so it can be addressed on its own terms rather than by widening #457.

A local function is emitted as <method name="&lt;Member&gt;g__Local|N_M"> inside the declaring
type's own
<class> element rather than inside a compiler-generated closure type, and it does not
inherit the enclosing member's attribute. The #457 filter scopes strictly to closure classes - those
whose name carries the .<>c marker - so a local function on a non-closure class is never a candidate
for removal.

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 a member carrying [ExcludeFromCodeCoverage] that declares a local function in its body

Steps to Reproduce

  1. Take a member that carries [ExcludeFromCodeCoverage] and declares a local function in its body.
  2. Run the coverage pipeline.
  3. Inspect the post-processed Cobertura report for that file.
  4. Observe a <method name="&lt;Member&gt;g__Local|N_M"> element still present on the declaring type's own <class>, with its lines counted in lines-valid.

Expected Behavior

A local function declared inside a member carrying [ExcludeFromCodeCoverage] is excluded from the
coverage denominator, exactly as the attributed member's own lines are.

Actual Behavior

The local function's lines are retained, because it lives on the declaring type's own class, which the
#457 filter never mutates.

Logs / Screenshots

  • Attached minimal logs or screenshot

  • The shape is pinned by regression case 5 part B in
    tests/scripts/vscode/Invoke-MSTestWithCoverage.ClosureFilter.Tests.ps1, whose fixture carries a
    declaring class Ns.B whose only method is &lt;Exempt&gt;g__Local|7_0:

    <class name="Ns.B" filename="Ns\B.cs" ...>
      <methods><method name="&lt;Exempt&gt;g__Local|7_0" ...>...</method></methods>
    </class>

    That test asserts Ns.B is retained byte-for-byte unmutated, which is the current, deliberate
    behaviour.

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. The cost is a residual ceiling on files that use local functions inside exempt
members.

Source

From: docs/features/potential/2026-08-11-local-functions-in-exempt-members-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