Summary
Methods the C# compiler rewrites are never matched to their coverage and silently default to 0.0. Because CRAP cubes the uncovered fraction, a fully covered async method can be reported as one of the worst methods in the codebase.
On the project I evaluated, 50 of 60 methods reported over threshold were already covered. The tool's output was ~83% false positives.
Reproduction
PageViewUpdater.UpdateAsync is 100% covered. The tool was handed the coverage file containing that fact and reported:
coverage=0 complexity=18 crap=342
The same file, at the same moment, contains:
<class name="...PageViewUpdater/<UpdateAsync>d__5" line-rate="1">
As a unit test:
[Fact]
public void AsyncMethod_CoverageOnCompilerGeneratedStateMachine_IsAttributedToSourceMethod()
{
var complexity = new[] { MakeComplexity("UpdateAsync", signature: "(int)") };
var coverage = new[]
{
MakeCoverage("MoveNext", className: "MyApp.Service/<UpdateAsync>d__5", coverage: 1.0)
};
var result = MethodCoverageMatcher.Match(complexity, coverage);
result.Methods.Single().Coverage.Should().Be(1.0); // fails: 0.0
}
Root cause
CoberturaMethodParser.NormalizeClassName only converts the nested-type separator:
var result = className.Replace('/', '.');
So MyApp.Service/<UpdateAsync>d__5 + method MoveNext produces the key
MyApp.Service.<UpdateAsync>d__5.MoveNext(), which cannot match the source key
MyApp.Service.UpdateAsync(int). Both passes in MethodCoverageMatcher miss and the
left-outer-join default of 0.0 applies.
This affects async methods, iterators, and generic methods. In the project I tested there were 1,014 MoveNext entries in a single coverage file.
Two facets
-
State machines. Recognise Outer/<Method>d__N and attribute MoveNext to the owning method. The generated type carries no parameter list, so the recovered key has no signature and must resolve via the existing name-only fallback pass.
-
Generic arity. The Roslyn side normalizes GetPracticeRights<TWorkflowStatus> to GetPracticeRights<>, while the state machine spells it <GetPracticeRights>d__9`1. The recovered key needs the same backtick-to-angle-bracket conversion MethodKeyHelper.NormalizeBacktickGenerics already performs, or the two still won't meet.
Note on ordering
Fixing this increases the impact of the multiple-coverage-file bug (separate issue), because recovered state-machine keys can only match through the name-only pass, which is exactly the pass that breaks when duplicate entries are present. The two should land together.
Summary
Methods the C# compiler rewrites are never matched to their coverage and silently default to
0.0. Because CRAP cubes the uncovered fraction, a fully covered async method can be reported as one of the worst methods in the codebase.On the project I evaluated, 50 of 60 methods reported over threshold were already covered. The tool's output was ~83% false positives.
Reproduction
PageViewUpdater.UpdateAsyncis 100% covered. The tool was handed the coverage file containing that fact and reported:The same file, at the same moment, contains:
As a unit test:
Root cause
CoberturaMethodParser.NormalizeClassNameonly converts the nested-type separator:So
MyApp.Service/<UpdateAsync>d__5+ methodMoveNextproduces the keyMyApp.Service.<UpdateAsync>d__5.MoveNext(), which cannot match the source keyMyApp.Service.UpdateAsync(int). Both passes inMethodCoverageMatchermiss and theleft-outer-join default of
0.0applies.This affects async methods, iterators, and generic methods. In the project I tested there were 1,014
MoveNextentries in a single coverage file.Two facets
State machines. Recognise
Outer/<Method>d__Nand attributeMoveNextto the owning method. The generated type carries no parameter list, so the recovered key has no signature and must resolve via the existing name-only fallback pass.Generic arity. The Roslyn side normalizes
GetPracticeRights<TWorkflowStatus>toGetPracticeRights<>, while the state machine spells it<GetPracticeRights>d__9`1. The recovered key needs the same backtick-to-angle-bracket conversionMethodKeyHelper.NormalizeBacktickGenericsalready performs, or the two still won't meet.Note on ordering
Fixing this increases the impact of the multiple-coverage-file bug (separate issue), because recovered state-machine keys can only match through the name-only pass, which is exactly the pass that breaks when duplicate entries are present. The two should land together.