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<T>,...)" 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.
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.0default.This is a different mechanism from the state-machine issue and needs a fix in the matcher rather than in the parser.
Reproduction
Real example. Source key:
Coverage entry for that same method:
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.NormalizeMethodNameends with: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
`1and<>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.