Skip to content

Generic methods are unmatched because Coverlet writes no arity on the method name #7

Description

@dillon7f

Summary

A generic method that is not async has no state machine, so it appears in Cobertura as an ordinary method. Coverlet writes its name with no generic arity at all, while the Roslyn side normalizes the type parameters to angle brackets. The two keys never meet, and the method falls through to the 0.0 default.

This is a different mechanism from the state-machine issue and needs a fix in the matcher rather than in the parser.

Reproduction

[Fact]
public void GenericMethod_CoverageEntryCarriesNoArity_IsStillMatched()
{
    var complexity = new[] { MakeComplexity("Apply<T>", signature: "(int)") };
    var coverage = new[] { MakeCoverage("Apply", signature: "(System.Int32)", coverage: 1.0) };

    var result = MethodCoverageMatcher.Match(complexity, coverage);

    result.Methods.Single().Coverage.Should().Be(1.0);   // fails: 0.0
}

Real example. Source key:

...QueryExtensions.FilterExpressionEngine.ApplyFilterRules<>(IQueryable<T>, ...)

Coverage entry for that same method:

<method name="ApplyFilterRules" signature="(System.Linq.IQueryable`1&lt;T&gt;,...)" line-rate="1" branch-rate="0.9285" />

Reported as 0% covered with a CRAP score of 272, making it the second-worst method in the project. It is ~93% branch covered.

Root cause

CoberturaMethodParser.NormalizeMethodName ends with:

// Generic methods: Find`1 → Find<>
return MethodKeyHelper.NormalizeBacktickGenerics(methodName);

That handles a backtick if one is present, but for a generic method Coverlet emits the bare name with no backtick. There is nothing to convert, and the arity cannot be recovered from the coverage entry.

Suggested fix

Since the arity is unavailable on the coverage side, erase it from both sides instead. Add a third fallback pass keyed on the name-only key with the method's arity removed, keeping the declaring type's arity (which both sides do supply, as `1 and <> respectively).

The pass should carry the same single-candidate guard as the existing name-only fallback so it still declines to guess between overloads, and its matches need excluding from the orphan count.

Impact

On the project I evaluated, four methods were affected, together worth 442 CRAP: ApplyFilterRules<T> (272), GenerateCsv<T> (72), CreateSort<TKey> (56), Build<T> (42). All four are well covered.

Fixing all three matching issues took that project from 60 methods flagged over threshold to 13, and crap load from 525 to 113.

Activity

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

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions